From 36195674c112aac199f16fa275bb68ca8da51415 Mon Sep 17 00:00:00 2001 From: Lucy Linder Date: Thu, 31 Aug 2017 10:39:38 +0200 Subject: [PATCH] add a method to check barcode code format ensure that the code to print is compatible with the ESC/POS formats and also automatically check this format before printing (barcode() method). --- src/escpos/constants.py | 18 ++ src/escpos/escpos.py | 55 +++++- src/escpos/exceptions.py | 5 +- test/test_function_check_barcode.py | 272 ++++++++++++++++++++++++++++ 4 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 test/test_function_check_barcode.py 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 04c1d80..8a01c8b 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 @@ -275,17 +276,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 @@ -353,6 +379,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` @@ -375,12 +405,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_check_barcode.py b/test/test_function_check_barcode.py new file mode 100644 index 0000000..ac9cc0f --- /dev/null +++ b/test/test_function_check_barcode.py @@ -0,0 +1,272 @@ +#!/usr/bin/python +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 + + +def test_barcode_upca(): + bc = 'UPC-A' + + valid_codes = [ + "01234567890", + "012345678905" + ] + + invalid_codes = [ + "01234567890123", # too long + "0123456789", # too short + "72527273-711", # invalid '-' + "A12345678901", # invalid 'A' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_upce(): + bc = 'UPC-E' + + valid_codes = [ + "01234567", + "0123456", + "012345678905" + ] + invalid_codes = [ + "01234567890123", # too long + "012345", # too short + "72527-2", # invalid '-' + "A123456", # invalid 'A' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_ean13(): + bc = 'EAN13' + + valid_codes = [ + "0123456789012", + "012345678901" + ] + invalid_codes = [ + "0123456789", # too short + "A123456789012" # invalid 'A' + "012345678901234", # too long + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_ean8(): + bc = 'EAN8' + + valid_codes = [ + "01234567", + "0123456" + ] + invalid_codes = [ + "012345", # too short + "A123456789012" # invalid 'A' + "012345678901234", # too long + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_code39(): + bc = 'CODE39' + + valid_codes = [ + "ABC-1234", + "ABC-1234-$$-+A", + "*WIKIPEDIA*" # the '*' symbol is not part of the actual code, but it is handled properly by ESCPOS + ] + invalid_codes = [ + "ALKJ_34", # invalid '_' + "A" * 256, # too long + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_itf(): + bc = 'ITF' + + valid_codes = [ + "010203040506070809", + "11221133113344556677889900", + ] + invalid_codes = [ + "010203040", # odd length + "0" * 256, # too long + "AB01", # invalid 'A' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_codabar(): + bc = 'CODABAR' + + valid_codes = [ + "A2030405060B", + "C11221133113344556677889900D", + "D0D", + ] + invalid_codes = [ + "010203040", # no start/stop + "0" * 256, # too long + "AB-01F", # invalid 'B' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_nw7(): + bc = 'NW7' # same as CODABAR + + valid_codes = [ + "A2030405060B", + "C11221133113344556677889900D", + "D0D", + ] + invalid_codes = [ + "010203040", # no start/stop + "0" * 256, # too long + "AB-01F", # invalid 'B' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_code93(): + bc = 'CODE93' + + valid_codes = [ + "A2030405060B", + "+:$&23-7@$", + "D0D", + ] + invalid_codes = [ + "é010203040", # invalid 'é' + "0" * 256, # too long + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_code128(): + bc = 'CODE128' + + valid_codes = [ + "{A2030405060B", + "{C+:$&23-7@$", + "{B0D", + ] + invalid_codes = [ + "010203040", # missing leading { + "0" * 256, # too long + "{D2354AA", # second char not between A-C + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_gs1_128(): + bc = 'GS1-128' # same as code 128 + + valid_codes = [ + "{A2030405060B", + "{C+:$&23-7@$", + "{B0D", + ] + invalid_codes = [ + "010203040", # missing leading { + "0" * 256, # too long + "{D2354AA", # second char not between A-C + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_gs1_omni(): + bc = 'GS1 DATABAR OMNIDIRECTIONAL' + + valid_codes = [ + "0123456789123", + ] + invalid_codes = [ + "01234567891234", # too long + "012345678912", # too short + "012345678A1234", # invalid 'A' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_gs1_trunc(): + bc = 'GS1 DATABAR TRUNCATED' # same as OMNIDIRECTIONAL + + valid_codes = [ + "0123456789123", + ] + invalid_codes = [ + "01234567891234", # too long + "012345678912", # too short + "012345678A1234", # invalid 'A' + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_gs1_limited(): + bc = 'GS1 DATABAR LIMITED' + + valid_codes = [ + "0123456789123", + "0123456789123", + ] + invalid_codes = [ + "01234567891234", # too long + "012345678912", # too short + "012345678A1234", # invalid 'A' + "02345678912341", # invalid start (should be 01) + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes])) + + +def test_barcode_gs1_expanded(): + bc = 'GS1 DATABAR EXPANDED' + + valid_codes = [ + "(9A{A20304+-%&06a0B", + "(1 {C+:$a23-7%", + "(00000001234567678", + ] + invalid_codes = [ + "010203040", # missing leading { + "0" * 256, # too long + "0{D2354AA", # second char not between A-za-z0-9 + "IT will fail", # first char not between 0-9 + ] + + assert (all([printer.Escpos.check_barcode(bc, code) for code in valid_codes])) + assert (not any([printer.Escpos.check_barcode(bc, code) for code in invalid_codes]))