Implement printer-side rendering of QR codes for printers that support it.

Expand settings on escpos.qr to include ec, size,
model and 'native' (send image or send esc/pos QR command).

Default is set as native=False, so existing code will continue to
render QR codes as images.
This commit is contained in:
Michael Billington 2016-03-20 00:48:29 +11:00
parent bf3012b882
commit f39c4227ec
3 changed files with 167 additions and 12 deletions

View File

@ -183,6 +183,16 @@ BARCODE_TYPES = {
'B': BARCODE_TYPE_B,
}
## QRCode error correction levels
QR_ECLEVEL_L = 0;
QR_ECLEVEL_M = 1;
QR_ECLEVEL_Q = 2;
QR_ECLEVEL_H = 3;
## QRcode models
QR_MODEL_1 = 1;
QR_MODEL_2 = 2;
QR_MICRO = 3;
# Image format
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"

View File

@ -263,22 +263,86 @@ class Escpos(object):
self._raw(binascii.unhexlify(bytes(buf, "ascii")))
self._raw(b'\n')
def qr(self, text):
def qr(self, content, ec=QR_ECLEVEL_L, size=3, model=QR_MODEL_2, native=False):
""" Print QR Code for the provided string
Prints a QR-code. The size has been adjusted to version 4, so it is small enough to be
printed but also big enough to be read by a smartphone.
:param text: text to generate a QR-Code from
:param content: The content of the code. Numeric data will be more efficiently compacted.
:param ec: Error-correction level to use. One of QR_ECLEVEL_L (default), QR_ECLEVEL_M, QR_ECLEVEL_Q or QR_ECLEVEL_H.
Higher error correction results in a less compact code.
:param size: Pixel size to use. Must be 1-16 (default 3)
:param model: QR code model to use. Must be one of QR_MODEL_1, QR_MODEL_2 (default) or QR_MICRO (not supported by all printers).
:param native: True to render the code on the printer, False to render the code as an image and send it to the printer (Default)
"""
qr_code = qrcode.QRCode(version=4, box_size=4, border=1, error_correction=qrcode.constants.ERROR_CORRECT_H)
qr_code.add_data(text)
qr_code.make(fit=True)
qr_img = qr_code.make_image()
im = qr_img._img.convert("RGB")
# Basic validation
if ec not in [QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q]:
raise ValueError("Invalid error correction level")
if not 1 <= size <= 16:
raise ValueError("Invalid block size (must be 1-16)")
if model not in [QR_MODEL_1, QR_MODEL_2, QR_MICRO]:
raise ValueError("Invalid QR model (must be one of QR_MODEL_1, QR_MODEL_2, QR_MICRO)")
if content == "":
# Handle edge case by printing nothing.
return
if not native:
# Map ESC/POS error correction levels to python 'qrcode' library constant and render to an image
if model != QR_MODEL_2:
raise ValueError("Invalid QR mocel for qrlib rendering (must be QR_MODEL_2)")
python_qr_ec = {
QR_ECLEVEL_H: qrcode.constants.ERROR_CORRECT_H,
QR_ECLEVEL_L: qrcode.constants.ERROR_CORRECT_L,
QR_ECLEVEL_M: qrcode.constants.ERROR_CORRECT_M,
QR_ECLEVEL_Q: qrcode.constants.ERROR_CORRECT_Q
}
qr_code = qrcode.QRCode(version=None, box_size=size, border=1, error_correction=python_qr_ec[ec])
qr_code.add_data(content)
qr_code.make(fit=True)
qr_img = qr_code.make_image()
im = qr_img._img.convert("RGB")
# Convert the RGB image in printable image
self._convert_image(im)
return
# Native 2D code printing
cn = b'1' # Code type for QR code
# Select model: 1, 2 or micro.
self._send_2d_code_data(six.int2byte(65), cn, six.int2byte(48 + model) + six.int2byte(0));
# Set dot size.
self._send_2d_code_data(six.int2byte(67), cn, six.int2byte(size));
# Set error correction level: L, M, Q, or H
self._send_2d_code_data(six.int2byte(69), cn, six.int2byte(48 + ec));
# Send content & print
self._send_2d_code_data(six.int2byte(80), cn, content.encode('utf-8'), b'0');
self._send_2d_code_data(six.int2byte(81), cn, b'', b'0');
# Convert the RGB image in printable image
self._convert_image(im)
def _send_2d_code_data(self, fn, cn, data, m=b''):
""" Wrapper for GS ( k, to calculate and send correct data length.
:param fn: Function to use.
:param cn: Output code type. Affects available data.
:param data: Data to send.
:param m: Modifier/variant for function. Often '0' where used.
"""
if len(m) > 1 or len(cn) != 1 or len(fn) != 1:
raise ValueError("cn and fn must be one byte each.")
header = self._int_low_high(len(data) + len(m) + 2, 2);
self._raw(GS + b'(k' + header + cn + fn + m + data)
@staticmethod
def _int_low_high(inp_number, out_bytes):
""" Generate multiple bytes for a number: In lower and higher parts, or more parts as needed.
:param inp_number: Input number
:param out_bytes: The number of bytes to output (1 - 4).
"""
max_input = (256 << (out_bytes * 8) - 1);
if not 1 <= out_bytes <= 4:
raise ValueError("Can only output 1-4 byes")
if not 0 <= inp_number <= max_input:
raise ValueError("Number too large. Can only output up to {0} in {1} byes".format(max_input, out_bytes))
outp = b'';
for _ in range(0, out_bytes):
outp += six.int2byte(inp_number % 256)
inp_number = inp_number // 256
return outp
def charcode(self, code):
""" Set Character Code Table

View File

@ -0,0 +1,81 @@
#!/usr/bin/python
"""test native QR code printing
:author: `Michael Billington <michael.billington@gmail.com>`_
:organization: `python-escpos <https://github.com/python-escpos>`_
:copyright: Copyright (c) 2016 `Michael Billington <michael.billington@gmail.com>`_
:license: GNU GPL v3
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from nose.tools import with_setup
import escpos.printer as printer
import os
from escpos.constants import QR_ECLEVEL_H, QR_MODEL_1
devfile = 'testfile'
def setup_testfile():
"""create a testfile as devfile"""
fhandle = open(devfile, 'a')
try:
os.utime(devfile, None)
finally:
fhandle.close()
def teardown_testfile():
"""destroy testfile again"""
os.remove(devfile)
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_defaults():
"""test QR code with defaults"""
instance = printer.File(devfile=devfile)
instance.qr("1234", native=True)
instance.flush()
with open(devfile, "rb") as f:
assert(f.read() == b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0')
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_empty():
"""test QR printing blank code"""
instance = printer.File(devfile=devfile)
instance.qr("", native=True)
instance.flush()
with open(devfile, "rb") as f:
assert(f.read() == b'')
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_ec():
"""test QR error correction setting"""
instance = printer.File(devfile=devfile)
instance.qr("1234", native=True, ec=QR_ECLEVEL_H)
instance.flush()
with open(devfile, "rb") as f:
assert(f.read() == b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E3\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0')
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_size():
"""test QR box size"""
instance = printer.File(devfile=devfile)
instance.qr("1234", native=True, size=7)
instance.flush()
with open(devfile, "rb") as f:
assert(f.read() == b'\x1d(k\x04\x001A2\x00\x1d(k\x03\x001C\x07\x1d(k\x03\x001E0\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0')
@with_setup(setup_testfile, teardown_testfile)
def test_function_qr_model():
"""test QR model"""
instance = printer.File(devfile=devfile)
instance.qr("1234", native=True, model=QR_MODEL_1)
instance.flush()
with open(devfile, "rb") as f:
assert(f.read() == b'\x1d(k\x04\x001A1\x00\x1d(k\x03\x001C\x03\x1d(k\x03\x001E0\x1d(k\x07\x001P01234\x1d(k\x03\x001Q0')