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:
parent
bf3012b882
commit
f39c4227ec
|
@ -183,6 +183,16 @@ BARCODE_TYPES = {
|
||||||
'B': BARCODE_TYPE_B,
|
'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
|
# Image format
|
||||||
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"
|
# NOTE: _PRINT_RASTER_IMG is the obsolete ESC/POS "print raster bit image"
|
||||||
|
|
|
@ -263,22 +263,86 @@ class Escpos(object):
|
||||||
self._raw(binascii.unhexlify(bytes(buf, "ascii")))
|
self._raw(binascii.unhexlify(bytes(buf, "ascii")))
|
||||||
self._raw(b'\n')
|
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
|
""" 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
|
:param content: The content of the code. Numeric data will be more efficiently compacted.
|
||||||
printed but also big enough to be read by a smartphone.
|
: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 text: text to generate a QR-Code from
|
: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)
|
# Basic validation
|
||||||
qr_code.add_data(text)
|
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_code.make(fit=True)
|
||||||
qr_img = qr_code.make_image()
|
qr_img = qr_code.make_image()
|
||||||
im = qr_img._img.convert("RGB")
|
im = qr_img._img.convert("RGB")
|
||||||
|
|
||||||
# Convert the RGB image in printable image
|
# Convert the RGB image in printable image
|
||||||
self._convert_image(im)
|
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');
|
||||||
|
|
||||||
|
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):
|
def charcode(self, code):
|
||||||
""" Set Character Code Table
|
""" Set Character Code Table
|
||||||
|
|
|
@ -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')
|
Loading…
Reference in New Issue