Feature/check barcodes (#255)
* 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). * rewrite test using pytest's parametrize functionality * add test for the 'check' argument * update authors list
This commit is contained in:
parent
d78a6f1699
commit
456f5b7aa6
1
AUTHORS
1
AUTHORS
|
@ -12,6 +12,7 @@ Hark
|
||||||
Joel Lehtonen
|
Joel Lehtonen
|
||||||
Kristi
|
Kristi
|
||||||
ldos
|
ldos
|
||||||
|
Lucy Linder
|
||||||
Manuel F Martinez
|
Manuel F Martinez
|
||||||
Michael Billington
|
Michael Billington
|
||||||
Michael Elsdörfer
|
Michael Elsdörfer
|
||||||
|
|
|
@ -225,6 +225,24 @@ BARCODE_TYPE_B = {
|
||||||
'GS1 DATABAR EXPANDED': _SET_BARCODE_TYPE(78),
|
'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 = {
|
BARCODE_TYPES = {
|
||||||
'A': BARCODE_TYPE_A,
|
'A': BARCODE_TYPE_A,
|
||||||
'B': BARCODE_TYPE_B,
|
'B': BARCODE_TYPE_B,
|
||||||
|
|
|
@ -19,13 +19,14 @@ import qrcode
|
||||||
import textwrap
|
import textwrap
|
||||||
import six
|
import six
|
||||||
import time
|
import time
|
||||||
|
from re import match as re_match
|
||||||
|
|
||||||
import barcode
|
import barcode
|
||||||
from barcode.writer import ImageWriter
|
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 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 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 BARCODE_TXT_OFF, BARCODE_TXT_BTH, BARCODE_TXT_ABV, BARCODE_TXT_BLW
|
||||||
from .constants import TXT_SIZE, TXT_NORMAL
|
from .constants import TXT_SIZE, TXT_NORMAL
|
||||||
from .constants import SET_FONT
|
from .constants import SET_FONT
|
||||||
|
@ -287,17 +288,42 @@ class Escpos(object):
|
||||||
else:
|
else:
|
||||||
self.magic.force_encoding(code)
|
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",
|
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
|
""" Print Barcode
|
||||||
|
|
||||||
This method allows to print barcodes. The rendering of the barcode is done by the printer and therefore has to
|
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
|
be supported by the unit. By default, this method will check whether your barcode text is correct, that is
|
||||||
barcodes may lead to unexpected printer behaviour. There are two forms of the barcode function. Type A is
|
the characters and lengths are supported by ESCPOS. Call the method with `check=False` to disable the check, but
|
||||||
default but has fewer barcodes, while type B has some more to choose from.
|
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,
|
||||||
.. todo:: Add a method to check barcode codes. Alternatively or as an addition write explanations about each
|
while type B has some more to choose from.
|
||||||
barcode-type. Research whether the check digits can be computed autmatically.
|
|
||||||
|
|
||||||
Use the parameters `height` and `width` for adjusting of the barcode size. Please take notice that the barcode
|
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
|
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.
|
function based on the current profile.
|
||||||
*default*: A
|
*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`,
|
:raises: :py:exc:`~escpos.exceptions.BarcodeSizeError`,
|
||||||
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
:py:exc:`~escpos.exceptions.BarcodeTypeError`,
|
||||||
:py:exc:`~escpos.exceptions.BarcodeCodeError`
|
:py:exc:`~escpos.exceptions.BarcodeCodeError`
|
||||||
|
@ -387,12 +417,19 @@ class Escpos(object):
|
||||||
bc_types = BARCODE_TYPES[function_type.upper()]
|
bc_types = BARCODE_TYPES[function_type.upper()]
|
||||||
if bc.upper() not in bc_types.keys():
|
if bc.upper() not in bc_types.keys():
|
||||||
raise BarcodeTypeError((
|
raise BarcodeTypeError((
|
||||||
"Barcode type '{bc}' not valid for barcode function type "
|
"Barcode '{bc}' not valid for barcode function type "
|
||||||
"{function_type}").format(
|
"{function_type}").format(
|
||||||
bc=bc,
|
bc=bc,
|
||||||
function_type=function_type,
|
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()
|
# Align Bar Code()
|
||||||
if align_ct:
|
if align_ct:
|
||||||
self._raw(TXT_STYLE['align']['center'])
|
self._raw(TXT_STYLE['align']['center'])
|
||||||
|
|
|
@ -77,9 +77,10 @@ class BarcodeSizeError(Error):
|
||||||
|
|
||||||
|
|
||||||
class BarcodeCodeError(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`.
|
The returncode for this exception is `30`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, msg=""):
|
def __init__(self, msg=""):
|
||||||
|
|
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||||
import escpos.printer as printer
|
import escpos.printer as printer
|
||||||
from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B
|
from escpos.constants import BARCODE_TYPE_A, BARCODE_TYPE_B
|
||||||
from escpos.capabilities import Profile, BARCODE_B
|
from escpos.capabilities import Profile, BARCODE_B
|
||||||
from escpos.exceptions import BarcodeTypeError
|
from escpos.exceptions import BarcodeTypeError, BarcodeCodeError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,3 +36,17 @@ def test_lacks_support(bctype, supports_b):
|
||||||
instance.barcode('test', bctype)
|
instance.barcode('test', bctype)
|
||||||
|
|
||||||
assert instance.output == b''
|
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''
|
||||||
|
|
|
@ -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))
|
Loading…
Reference in New Issue