diff --git a/AUTHORS b/AUTHORS index 7ca14d0..5095c38 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,6 +12,7 @@ Hark Joel Lehtonen Kristi ldos +Lucy Linder Manuel F Martinez Michael Billington Michael Elsdörfer diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 75bdc1b..88b91f5 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -225,6 +225,24 @@ BARCODE_TYPE_B = { 'GS1 DATABAR EXPANDED': _SET_BARCODE_TYPE(78), } +BARCODE_FORMATS = { + 'UPC-A': ([(11, 12)], "^[0-9]{11,12}$"), + 'UPC-E': ([(7, 8), (11, 12)], "^([0-9]{7,8}|[0-9]{11,12})$"), + 'EAN13': ([(12, 13)], "^[0-9]{12,13}$"), + 'EAN8': ([(7, 8)], "^[0-9]{7,8}$"), + 'CODE39': ([(1, 255)], "^([0-9A-Z \$\%\+\-\.\/]+|\*[0-9A-Z \$\%\+\-\.\/]+\*)$"), + 'ITF': ([(2, 255)], "^([0-9]{2})+$"), + 'NW7': ([(1, 255)], "^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), + 'CODABAR': ([(1, 255)], "^[A-Da-d][0-9\$\+\-\.\/\:]+[A-Da-d]$"), # Same as NW7 + 'CODE93': ([(1, 255)], "^[\\x00-\\x7F]+$"), + 'CODE128': ([(2, 255)], "^\{[A-C][\\x00-\\x7F]+$"), + 'GS1-128': ([(2, 255)], "^\{[A-C][\\x00-\\x7F]+$"), # same as CODE128 + 'GS1 DATABAR OMNIDIRECTIONAL': ([(13,13)], "^[0-9]{13}$"), + 'GS1 DATABAR TRUNCATED': ([(13,13)], "^[0-9]{13}$"), # same as GS1 omnidirectional + 'GS1 DATABAR LIMITED': ([(13,13)], "^[01][0-9]{12}$"), + 'GS1 DATABAR EXPANDED': ([(2,255)], "^\([0-9][A-Za-z0-9 \!\"\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\_\{]+$"), +} + BARCODE_TYPES = { 'A': BARCODE_TYPE_A, 'B': BARCODE_TYPE_B, diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index c94125c..e3b3a53 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -19,13 +19,14 @@ import qrcode import textwrap import six import time +from re import match as re_match import barcode from barcode.writer import ImageWriter from .constants import ESC, GS, NUL, QR_ECLEVEL_L, QR_ECLEVEL_M, QR_ECLEVEL_H, QR_ECLEVEL_Q from .constants import QR_MODEL_1, QR_MODEL_2, QR_MICRO, BARCODE_TYPES, BARCODE_HEIGHT, BARCODE_WIDTH -from .constants import BARCODE_FONT_A, BARCODE_FONT_B +from .constants import BARCODE_FONT_A, BARCODE_FONT_B, BARCODE_FORMATS from .constants import BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCODE_TXT_BLW from .constants import TXT_SIZE, TXT_NORMAL from .constants import SET_FONT @@ -287,17 +288,42 @@ class Escpos(object): else: self.magic.force_encoding(code) + @staticmethod + def check_barcode(bc, code): + """ + This method checks if the barcode is in the proper format. + The validation concerns the barcode length and the set of characters, but won't compute/validate any checksum. + The full set of requirement for each barcode type is available in the ESC/POS documentation. + + As an example, using EAN13, the barcode `12345678901` will be correct, because it can be rendered by the + printer. But it does not suit the EAN13 standard, because the checksum digit is missing. Adding a wrong + checksum in the end will also be considered correct, but adding a letter won't (EAN13 is numeric only). + + .. todo:: Add a method to compute the checksum for the different standards + + .. todo:: For fixed-length standards with mandatory checksum (EAN, UPC), + compute and add the checksum automatically if missing. + + :param bc: barcode format, see :py:func`~escpos.Escpos.barcode` + :param code: alphanumeric data to be printed as bar code, see :py:func`~escpos.Escpos.barcode` + :return: bool + """ + if bc not in BARCODE_FORMATS: + return False + + bounds, regex = BARCODE_FORMATS[bc] + return any(bound[0] <= len(code) <= bound[1] for bound in bounds) and re_match(regex, code) + def barcode(self, code, bc, height=64, width=3, pos="BELOW", font="A", - align_ct=True, function_type=None): + align_ct=True, function_type=None, check=True): """ Print Barcode This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to - be supported by the unit. Currently you have to check manually whether your barcode text is correct. Uncorrect - barcodes may lead to unexpected printer behaviour. There are two forms of the barcode function. Type A is - default but has fewer barcodes, while type B has some more to choose from. - - .. todo:: Add a method to check barcode codes. Alternatively or as an addition write explanations about each - barcode-type. Research whether the check digits can be computed autmatically. + be supported by the unit. By default, this method will check whether your barcode text is correct, that is + the characters and lengths are supported by ESCPOS. Call the method with `check=False` to disable the check, but + note that uncorrect barcodes may lead to unexpected printer behaviour. + There are two forms of the barcode function. Type A is default but has fewer barcodes, + while type B has some more to choose from. Use the parameters `height` and `width` for adjusting of the barcode size. Please take notice that the barcode will not be printed if it is outside of the printable area. (Which should be impossible with this method, so @@ -365,6 +391,10 @@ class Escpos(object): function based on the current profile. *default*: A + :param check: If this parameter is True, the barcode format will be checked to ensure it meets the bc + requirements as defigned in the esc/pos documentation. See py:func:`~escpos.Escpos.check_barcode` + for more information. *default*: True. + :raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`, :py:exc:`~escpos.exceptions.BarcodeTypeError`, :py:exc:`~escpos.exceptions.BarcodeCodeError` @@ -387,12 +417,19 @@ class Escpos(object): bc_types = BARCODE_TYPES[function_type.upper()] if bc.upper() not in bc_types.keys(): raise BarcodeTypeError(( - "Barcode type '{bc}' not valid for barcode function type " + "Barcode '{bc}' not valid for barcode function type " "{function_type}").format( bc=bc, function_type=function_type, )) + if check and not self.check_barcode(bc, code): + raise BarcodeCodeError(( + "Barcode '{code}' not in a valid format for type '{bc}'").format( + code=code, + bc=bc, + )) + # Align Bar Code() if align_ct: self._raw(TXT_STYLE['align']['center']) diff --git a/src/escpos/exceptions.py b/src/escpos/exceptions.py index 781e3e9..b82e4e0 100644 --- a/src/escpos/exceptions.py +++ b/src/escpos/exceptions.py @@ -77,9 +77,10 @@ class BarcodeSizeError(Error): class BarcodeCodeError(Error): - """ No Barcode code was supplied. + """ No Barcode code was supplied, or it is incorrect. - No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode`. + No data for the barcode has been supplied in :py:meth:`escpos.escpos.Escpos.barcode` or the the `check` parameter + was True and the check failed. The returncode for this exception is `30`. """ def __init__(self, msg=""): diff --git a/test/test_function_barcode.py b/test/test_function_barcode.py index 710e5cd..a8f11a5 100644 --- a/test/test_function_barcode.py +++ b/test/test_function_barcode.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import escpos.printer as printer from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B from escpos.capabilities import Profile, BARCODE_B -from escpos.exceptions import BarcodeTypeError +from escpos.exceptions import BarcodeTypeError, BarcodeCodeError import pytest @@ -36,3 +36,17 @@ def test_lacks_support(bctype, supports_b): instance.barcode('test', bctype) assert instance.output == b'' + + +@pytest.mark.parametrize("bctype,data", [ + ('EAN13', 'AA'), + ('CODE128', '{D2354AA'), +]) +def test_code_check(bctype, data): + """should raise an error if the barcode code is invalid. + """ + instance = printer.Dummy() + with pytest.raises(BarcodeCodeError): + instance.barcode(data, bctype) + + assert instance.output == b'' diff --git a/test/test_function_check_barcode.py b/test/test_function_check_barcode.py new file mode 100644 index 0000000..e249901 --- /dev/null +++ b/test/test_function_check_barcode.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import escpos.printer as printer +import pytest + + +@pytest.mark.parametrize("bctype,data", [ + ('UPC-A', '01234567890'), + ('UPC-A', '012345678905'), + ('UPC-E', '01234567'), + ('UPC-E', '0123456'), + ('UPC-E', '012345678905'), + ('EAN13', '0123456789012'), + ('EAN13', '012345678901'), + ('EAN8', '01234567'), + ('EAN8', '0123456'), + ('CODE39', 'ABC-1234'), + ('CODE39', 'ABC-1234-$$-+A'), + ('CODE39', '*WIKIPEDIA*'), + ('ITF', '010203040506070809'), + ('ITF', '11221133113344556677889900'), + ('CODABAR', 'A2030405060B'), + ('CODABAR', 'C11221133113344556677889900D'), + ('CODABAR', 'D0D'), + ('NW7', 'A2030405060B'), + ('NW7', 'C11221133113344556677889900D'), + ('NW7', 'D0D'), + ('CODE93', 'A2030405060B'), + ('CODE93', '+:$&23-7@$'), + ('CODE93', 'D0D'), + ('CODE128', '{A2030405060B'), + ('CODE128', '{C+:$&23-7@$'), + ('CODE128', '{B0D'), + ('GS1-128', '{A2030405060B'), + ('GS1-128', '{C+:$&23-7@$'), + ('GS1-128', '{B0D'), + ('GS1 DATABAR OMNIDIRECTIONAL', '0123456789123'), + ('GS1 DATABAR TRUNCATED', '0123456789123'), + ('GS1 DATABAR LIMITED', '0123456789123'), + ('GS1 DATABAR EXPANDED', '(9A{A20304+-%&06a0B'), + ('GS1 DATABAR EXPANDED', '(1 {C+:&23-7%'), + ('GS1 DATABAR EXPANDED', '(00000001234567678'), +]) +def test_check_valid_barcode(bctype, data): + assert (printer.Escpos.check_barcode(bctype, data)) + + +@pytest.mark.parametrize("bctype,data", [ + ('UPC-A', '01234567890123'), # too long + ('UPC-A', '0123456789'), # too short + ('UPC-A', '72527273-711'), # invalid '-' + ('UPC-A', 'A12345678901'), # invalid 'A' + ('UPC-E', '01234567890123'), # too long + ('UPC-E', '012345'), # too short + ('UPC-E', '72527-2'), # invalid '-' + ('UPC-E', 'A123456'), # invalid 'A' + ('EAN13', '0123456789'), # too short + ('EAN13', 'A123456789012'), # invalid 'A' + ('EAN13', '012345678901234'), # too long + ('EAN8', '012345'), # too short + ('EAN8', 'A123456789012'), # invalid 'A' + ('EAN8', '012345678901234'), # too long + ('CODE39', 'ALKJ_34'), # invalid '_' + ('CODE39', 'A' * 256), # too long + ('ITF', '010203040'), # odd length + ('ITF', '0' * 256), # too long + ('ITF', 'AB01'), # invalid 'A' + ('CODABAR', '010203040'), # no start/stop + ('CODABAR', '0' * 256), # too long + ('CODABAR', 'AB-01F'), # invalid 'B' + ('NW7', '010203040'), # no start/stop + ('NW7', '0' * 256), # too long + ('NW7', 'AB-01F'), # invalid 'B' + ('CODE93', 'é010203040'), # invalid 'é' + ('CODE93', '0' * 256), # too long + ('CODE128', '010203040'), # missing leading { + ('CODE128', '{D2354AA'), # second char not between A-C + ('CODE128', '0' * 256), # too long + ('GS1-128', '010203040'), # missing leading { + ('GS1-128', '{D2354AA'), # second char not between A-C + ('GS1-128', '0' * 256), # too long + ('GS1 DATABAR OMNIDIRECTIONAL', '01234567891234'), # too long + ('GS1 DATABAR OMNIDIRECTIONAL', '012345678912'), # too short + ('GS1 DATABAR OMNIDIRECTIONAL', '012345678A1234'), # invalid 'A' + ('GS1 DATABAR TRUNCATED', '01234567891234'), # too long + ('GS1 DATABAR TRUNCATED', '012345678912'), # too short + ('GS1 DATABAR TRUNCATED', '012345678A1234'), # invalid 'A' + ('GS1 DATABAR LIMITED', '01234567891234'), # too long + ('GS1 DATABAR LIMITED', '012345678912'), # too short + ('GS1 DATABAR LIMITED', '012345678A1234'), # invalid 'A' + ('GS1 DATABAR LIMITED', '02345678912341'), # invalid start (should be 01) + ('GS1 DATABAR EXPANDED', '010203040'), # missing leading ( + ('GS1-128', '(' + ('0' * 256)), # too long + ('GS1 DATABAR EXPANDED', '(a{D2354AA'), # second char not between 0-9 + ('GS1 DATABAR EXPANDED', 'IT will fail'), # first char not '(' +]) +def test_check_invalid_barcode(bctype, data): + assert (not printer.Escpos.check_barcode(bctype, data))