diff --git a/doc/user/barcode.rst b/doc/user/barcode.rst index 5d45419..11b12a7 100644 --- a/doc/user/barcode.rst +++ b/doc/user/barcode.rst @@ -1,17 +1,18 @@ Printing Barcodes ----------------- -:Last Reviewed: 2016-07-31 +:Last Reviewed: 2023-05-16 -Most ESC/POS-printers implement barcode-printing. -The barcode-commandset is implemented in the barcode-method. -For a list of compatible barcodes you should check the manual of your printer. -As a rule of thumb: even older Epson-models support most 1D-barcodes. -To be sure just try some implementations and have a look at the notices below. +Many printers implement barcode printing natively. +This hardware renderered barcodes are fast but the supported formats are limited by the printer itself and different between models. +However, almost all printers support printing images, so barcode renderization can be performed externally by software and then sent to the printer as an image. +As a drawback, this operation is much slower and the user needs to know and choose the image implementation method supported by the printer's commandset. barcode-method ~~~~~~~~~~~~~~ -The barcode-method is rather low-level and orients itself on the implementation of ESC/POS. -In the future this class could be supplemented by a high-level class that helps the user generating the payload. +Since version 3.0, the ``barcode`` method unifies the previous ``barcode`` (hardware) and ``soft_barcode`` (software) methods. +It is able to choose automatically the best printer implementation for barcode printing based on the capabilities of the printer and the type of barcode desired. +To achieve this, it relies on the information contained in the escpos-printer-db profiles. +The chosen profile needs to match the capabilities of the printer as closely as possible. .. py:currentmodule:: escpos.escpos diff --git a/examples/barcodes.py b/examples/barcodes.py index ef7944b..16c8083 100644 --- a/examples/barcodes.py +++ b/examples/barcodes.py @@ -2,10 +2,10 @@ from escpos.printer import Usb # Adapt to your needs -p = Usb(0x0416, 0x5011, profile="POS-5890") +p = Usb(0x0416, 0x5011, profile="TM-T88II") # Print software and then hardware barcode with the same content -p.soft_barcode("code39", "123456") +p.barcode("123456", "CODE39", width=2, force_software=True) p.text("\n") p.text("\n") p.barcode("123456", "CODE39") diff --git a/examples/software_barcode.py b/examples/software_barcode.py index 0fdaaee..7c949dd 100644 --- a/examples/software_barcode.py +++ b/examples/software_barcode.py @@ -5,5 +5,5 @@ from escpos.printer import Usb p = Usb(0x0416, 0x5011, profile="POS-5890") # Some software barcodes -p.soft_barcode("code128", "Hello") -p.soft_barcode("code39", "1234") +p.barcode("Hello", "code128", width=2, force_software="bitImageRaster") +p.barcode("1234", "code39", width=2, force_software=True) diff --git a/src/escpos/cli.py b/src/escpos/cli.py index 98487c0..6e692af 100644 --- a/src/escpos/cli.py +++ b/src/escpos/cli.py @@ -161,6 +161,11 @@ ESCPOS_COMMANDS = [ "help": "ESCPOS function type", "choices": ["A", "B"], }, + { + "option_strings": ("--force_software",), + "help": "Force render and print barcode as an image", + "choices": ["graphics", "bitImageColumn", "bitImageRaster"], + }, ], }, { diff --git a/src/escpos/constants.py b/src/escpos/constants.py index 3c018db..2fd0820 100644 --- a/src/escpos/constants.py +++ b/src/escpos/constants.py @@ -27,12 +27,12 @@ FS = b"\x1c" GS = b"\x1d" # Feed control sequences -CTL_LF = b"\n" # Print and line feed -CTL_FF = b"\f" # Form feed -CTL_CR = b"\r" # Carriage return -CTL_HT = b"\t" # Horizontal tab -CTL_SET_HT = ESC + b"\x44" # Set horizontal tab positions -CTL_VT = b"\v" # Vertical tab +CTL_LF = b"\n" #: Print and line feed +CTL_FF = b"\f" #: Form feed +CTL_CR = b"\r" #: Carriage return +CTL_HT = b"\t" #: Horizontal tab +CTL_SET_HT = ESC + b"\x44" #: Set horizontal tab positions +CTL_VT = b"\v" #: Vertical tab # Printer hardware HW_INIT = ESC + b"@" # Clear data in buffer and reset modes @@ -57,8 +57,8 @@ CD_KICK_5 = _CASH_DRAWER(b"\x01", 50, 50) # Sends a pulse to pin 5 [] # Paper Cutter _CUT_PAPER = lambda m: GS + b"V" + m -PAPER_FULL_CUT = _CUT_PAPER(b"\x00") # Full cut paper -PAPER_PART_CUT = _CUT_PAPER(b"\x01") # Partial cut paper +PAPER_FULL_CUT = _CUT_PAPER(b"\x00") #: Full cut paper +PAPER_PART_CUT = _CUT_PAPER(b"\x01") #: Partial cut paper # Beep (please note that the actual beep sequence may differ between devices) BEEP = b"\x07" @@ -168,8 +168,8 @@ TXT_STYLE = { # Fonts SET_FONT = lambda n: ESC + b"\x4d" + n -TXT_FONT_A = SET_FONT(b"\x00") # Font type A -TXT_FONT_B = SET_FONT(b"\x01") # Font type B +TXT_FONT_A = SET_FONT(b"\x00") #: Font type A +TXT_FONT_B = SET_FONT(b"\x01") #: Font type B # Spacing LINESPACING_RESET = ESC + b"2" @@ -179,23 +179,23 @@ LINESPACING_FUNCS = { 180: ESC + b"3", # line_spacing/180 of an inch, 0 <= line_spacing <= 255 } -# Prefix to change the codepage. You need to attach a byte to indicate -# the codepage to use. We use escpos-printer-db as the data source. +#: Prefix to change the codepage. You need to attach a byte to indicate +#: the codepage to use. We use escpos-printer-db as the data source. CODEPAGE_CHANGE = ESC + b"\x74" # Barcode format _SET_BARCODE_TXT_POS = lambda n: GS + b"H" + n -BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS(b"\x00") # HRI barcode chars OFF -BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS(b"\x01") # HRI barcode chars above -BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS(b"\x02") # HRI barcode chars below -BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS(b"\x03") # HRI both above and below +BARCODE_TXT_OFF = _SET_BARCODE_TXT_POS(b"\x00") #: HRI barcode chars OFF +BARCODE_TXT_ABV = _SET_BARCODE_TXT_POS(b"\x01") #: HRI barcode chars above +BARCODE_TXT_BLW = _SET_BARCODE_TXT_POS(b"\x02") #: HRI barcode chars below +BARCODE_TXT_BTH = _SET_BARCODE_TXT_POS(b"\x03") #: HRI both above and below _SET_HRI_FONT = lambda n: GS + b"f" + n -BARCODE_FONT_A = _SET_HRI_FONT(b"\x00") # Font type A for HRI barcode chars -BARCODE_FONT_B = _SET_HRI_FONT(b"\x01") # Font type B for HRI barcode chars +BARCODE_FONT_A = _SET_HRI_FONT(b"\x00") #: Font type A for HRI barcode chars +BARCODE_FONT_B = _SET_HRI_FONT(b"\x01") #: Font type B for HRI barcode chars -BARCODE_HEIGHT = GS + b"h" # Barcode Height [1-255] -BARCODE_WIDTH = GS + b"w" # Barcode Width [2-6] +BARCODE_HEIGHT = GS + b"h" #: Barcode Height [1-255] +BARCODE_WIDTH = GS + b"w" #: Barcode Width [2-6] # NOTE: This isn't actually an ESC/POS command. It's the common prefix to the # two "print bar code" commands: @@ -204,7 +204,7 @@ BARCODE_WIDTH = GS + b"w" # Barcode Width [2-6] # The latter command supports more barcode types _SET_BARCODE_TYPE = lambda m: GS + b"k" + six.int2byte(m) -# Barcodes for printing function type A +#: Barcodes for printing function type A BARCODE_TYPE_A = { "UPC-A": _SET_BARCODE_TYPE(0), "UPC-E": _SET_BARCODE_TYPE(1), @@ -216,8 +216,8 @@ BARCODE_TYPE_A = { "CODABAR": _SET_BARCODE_TYPE(6), # Same as NW7 } -# Barcodes for printing function type B -# The first 8 are the same barcodes as type A +#: Barcodes for printing function type B +#: The first 8 are the same barcodes as type A BARCODE_TYPE_B = { "UPC-A": _SET_BARCODE_TYPE(65), "UPC-E": _SET_BARCODE_TYPE(66), @@ -236,6 +236,7 @@ BARCODE_TYPE_B = { "GS1 DATABAR EXPANDED": _SET_BARCODE_TYPE(78), } +#: supported barcode formats BARCODE_FORMATS = { "UPC-A": ([(11, 12)], "^[0-9]{11,12}$"), "UPC-E": ([(7, 8), (11, 12)], "^([0-9]{7,8}|[0-9]{11,12})$"), diff --git a/src/escpos/escpos.py b/src/escpos/escpos.py index 0788124..22aef57 100644 --- a/src/escpos/escpos.py +++ b/src/escpos/escpos.py @@ -87,6 +87,19 @@ from escpos.image import EscposImage from escpos.capabilities import get_profile, BARCODE_B +# Remove special characters and whitespaces of the supported barcode names, +# convert to uppercase and map them to their original names. +HW_BARCODE_NAMES = { + "".join([char for char in name.upper() if char.isalnum()]): name + for bc_type in BARCODE_TYPES.values() + for name in bc_type +} +SW_BARCODE_NAMES = { + "".join([char for char in name.upper() if char.isalnum()]): name + for name in barcode.PROVIDED_BARCODES +} + + @six.add_metaclass(ABCMeta) class Escpos(object): """ESC/POS Printer object @@ -411,6 +424,24 @@ class Escpos(object): regex, code ) + def _dpi(self) -> int: + """Printer's DPI resolution.""" + try: + dpi = int(self.profile.profile_data["media"]["dpi"]) + except (KeyError, TypeError): + # Calculate the printer's DPI from the width info of the profile. + try: + px = self.profile.profile_data["media"]["width"]["pixels"] + mm = self.profile.profile_data["media"]["width"]["mm"] + mm -= 10 # paper width minus margin =~ printable area + dpi = int(px / (mm / 25.4)) + except (KeyError, TypeError, ZeroDivisionError): + # Value on error. + dpi = 180 + print(f"No printer's DPI info was found: Defaulting to {dpi}.") + self.profile.profile_data["media"]["dpi"] = dpi + return dpi + def barcode( self, code, @@ -422,6 +453,128 @@ class Escpos(object): align_ct=True, function_type=None, check=True, + force_software=False, + ): + """Print barcode. + + Automatic hardware|software barcode renderer according to the printer capabilities. + + Defaults to hardware barcode and its format types if supported. + Automatically switches to software barcode renderer if hardware does not + support a barcode type that is supported by software. (e.g. JAN, ISSN, etc.). + + Set force_software=True to force the software renderer according to the profile. + Set force_software=graphics|bitImageColumn|bitImageRaster to specify a renderer. + + Ignores caps, special chars and whitespaces in barcode type names. + So "EAN13", "ean-13", "Ean_13", "EAN 13" are all accepted. + + :param code: alphanumeric data to be printed as bar code (payload). + + :param bc: barcode format type (EAN13, CODE128, JAN, etc.). + + :param height: barcode module height (in printer dots), has to be between 1 and 255. + *default*: 64 + :type height: int + + :param width: barcode module width (in printer dots), has to be between 2 and 6. + *default*: 3 + :type width: int + + :param pos: text position (ABOVE, BELOW, BOTH, OFF) relative to the barcode + (ignored in software renderer). + *default*: BELOW + + :param font: select font A or B (ignored in software renderer). + *default*: A + + :param align_ct: If *True*, center the barcode. + *default*: True + :type align_ct: bool + + :param function_type: ESCPOS function type A or B. None to guess it from profile + (ignored in software renderer). + *default*: None + + :param check: If *True*, checks that the code meets the requirements of the barcode type. + *default*: True + :type check: bool + + :param force_software: If *True*, force the use of software barcode renderer from profile. + If *"graphics", "bitImageColumn" or "bitImageRaster"*, force the use of specific renderer. + :type force_software: bool | str + + :raises: :py:exc:`~escpos.exceptions.BarcodeCodeError`, + :py:exc:`~escpos.exceptions.BarcodeTypeError` + + .. note:: + Get all supported formats at: + - Hardware: :py:const:`~escpos.constants.BARCODE_FORMATS` + - Software: `Python barcode documentation `_ + """ + hw_modes = ["barcodeA", "barcodeB"] + sw_modes = ["graphics", "bitImageColumn", "bitImageRaster"] + capable = { + "hw": [mode for mode in hw_modes if self.profile.supports(mode)] or None, + "sw": [mode for mode in sw_modes if self.profile.supports(mode)] or None, + } + if (not capable["hw"] and not capable["sw"]) or ( + not capable["sw"] and force_software + ): + raise BarcodeTypeError( + f"""Profile { + self.profile.profile_data['name'] + } - hw barcode: {capable['hw']}, sw barcode: {capable['sw']}""" + ) + + bc_alnum = "".join([char for char in bc.upper() if char.isalnum()]) + capable_bc = { + "hw": HW_BARCODE_NAMES.get(bc_alnum), + "sw": SW_BARCODE_NAMES.get(bc_alnum), + } + if not any([*capable_bc.values()]): + raise BarcodeTypeError(f"Not supported or wrong barcode name {bc}.") + + if force_software or not capable["hw"] or not capable_bc["hw"]: + # Select the best possible capable render mode + impl = capable["sw"][0] + if force_software in capable["sw"]: + # Force to a specific mode + impl = force_software + print(f"Using {impl} software barcode renderer") + # Set barcode type + bc = capable_bc["sw"] or bc + # Get mm per point of the printer + mmxpt = 25.4 / self._dpi() + self._sw_barcode( + bc, + code, + impl=impl, + module_height=height * mmxpt, + module_width=width * mmxpt, + text_distance=3, # TODO: _hw_barcode() size equivalence + font_size=9, # TODO: _hw_barcode() size equivalence + center=align_ct, + ) + return + + print("Using hardware barcode renderer") + bc = capable_bc["hw"] or bc + self._hw_barcode( + code, bc, height, width, pos, font, align_ct, function_type, check + ) + + def _hw_barcode( + self, + code, + bc, + height=64, + width=3, + pos="BELOW", + font="A", + align_ct=True, + function_type=None, + check=True, ): """Print Barcode @@ -444,9 +597,6 @@ class Escpos(object): automatic centering. Please note that when you use center alignment, then the alignment of text will be changed automatically to centered. You have to manually restore the alignment if necessary. - .. todo:: If further barcode-types are needed they could be rendered transparently as an image. (This could also - be of help if the printer does not support types that others do.) - :param code: alphanumeric data to be printed as bar code :param bc: barcode format, possible values are for type A are: @@ -506,27 +656,12 @@ class Escpos(object): :py:exc:`~escpos.exceptions.BarcodeTypeError`, :py:exc:`~escpos.exceptions.BarcodeCodeError` """ - if function_type is None: - # Choose the function type automatically. - if bc in BARCODE_TYPES["A"]: - function_type = "A" - else: - if bc in BARCODE_TYPES["B"]: - if not self.profile.supports(BARCODE_B): - raise BarcodeTypeError( - ( - "Barcode type '{bc} not supported for " - "the current printer profile" - ).format(bc=bc) - ) - function_type = "B" - else: - raise BarcodeTypeError( - ("Barcode type '{bc} is not valid").format(bc=bc) - ) + # If function_type is specified, otherwise use guessing. + ft_guess = [ft for ft in ["A", "B"] if bc in BARCODE_TYPES.get(ft)] + ft_guess = ft_guess or [None] + function_type = function_type or ft_guess[0] - bc_types = BARCODE_TYPES[function_type.upper()] - if bc.upper() not in bc_types.keys(): + if not function_type or not BARCODE_TYPES.get(function_type.upper()): raise BarcodeTypeError( ( "Barcode '{bc}' not valid for barcode function type " @@ -536,6 +671,7 @@ class Escpos(object): function_type=function_type, ) ) + bc_types = BARCODE_TYPES[function_type.upper()] if check and not self.check_barcode(bc, code): raise BarcodeCodeError( @@ -587,16 +723,71 @@ class Escpos(object): if function_type.upper() == "A": self._raw(NUL) - def soft_barcode( + def _sw_barcode( self, barcode_type, data, impl="bitImageColumn", module_height=5, module_width=0.2, - text_distance=1, + text_distance=5, + font_size=10, center=True, ): + """Print Barcode + + This method allows to print barcodes. The rendering of the barcode is done by + the `barcode` library and sent to the printer as image through one of the + printer's supported implementations: graphics, bitImageColumn or bitImageRaster. + + :param barcode_type: barcode format, possible values are: + * ean8 + * ean8-guard + * ean13 + * ean13-guard + * ean + * gtin + * ean14 + * jan + * upc + * upca + * isbn + * isbn13 + * gs1 + * isbn10 + * issn + * code39 + * pzn + * code128 + * itf + * gs1_128 + * codabar + * nw-7 + :type data: str + + :param data: alphanumeric data to be printed as bar code (payload). + :type data: str + + :param impl: image printing mode: + * graphics + * bitImageColumn + * bitImageRaster + + :param module_height: barcode module height (in mm). + :type module_height: int | float + + :param module_width: barcode module width (in mm). + :type module_width: int | float + + :param text_distance: distance from the barcode to the code text (in mm). + :type text_distance: int | float + + :param font_size: font size of the code text (in dots). + :type font_size: int + + :param center: center the barcode. + :type center: bool + """ image_writer = ImageWriter() # Check if barcode type exists @@ -610,11 +801,15 @@ class Escpos(object): # Render the barcode barcode_class = barcode.get_barcode_class(barcode_type) my_code = barcode_class(data, writer=image_writer) + my_code.render( writer_options={ "module_height": module_height, "module_width": module_width, + "quiet_zone": 0, # horizontal padding "text_distance": text_distance, + "font_size": font_size, + "dpi": self._dpi(), # Image dpi has to match the printer's dpi } ) diff --git a/test/test_function_softbarcode.py b/test/test_function_softbarcode.py index 2cbef17..0d55024 100644 --- a/test/test_function_softbarcode.py +++ b/test/test_function_softbarcode.py @@ -13,13 +13,13 @@ def instance(): def test_soft_barcode_ean8_invalid(instance): """test with an invalid barcode""" with pytest.raises(barcode.errors.BarcodeError): - instance.soft_barcode("ean8", "1234") + instance.barcode("1234", "ean8", force_software=True) def test_soft_barcode_ean8(instance): """test with a valid ean8 barcode""" - instance.soft_barcode("ean8", "1234567") + instance.barcode("1234567", "ean8", force_software=True) def test_soft_barcode_ean8_nocenter(instance): - instance.soft_barcode("ean8", "1234567", center=False) + instance.barcode("1234567", "ean8", align_ct=False, force_software=True)