From f40c0e4d2916cc20de1a07e7c795f7660bad96de Mon Sep 17 00:00:00 2001 From: Sylvain GARANCHER Date: Fri, 9 Sep 2022 09:55:19 +0200 Subject: [PATCH] [IMP] printer_zpl2: Include library inside the module Copied from https://github.com/subteno-it/python-zpl2, as there has been new release (1.2.1) that breaks current code without clear source (no commit on the repo). As the amount of code is not too much, we put it on the module itself, being able to control the whole chain, and to reduce the code with the considerations of current Odoo version. This commit has as author the main commmiter of the library repo. --- printer_zpl2/models/zpl2.py | 529 ++++++++++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 printer_zpl2/models/zpl2.py diff --git a/printer_zpl2/models/zpl2.py b/printer_zpl2/models/zpl2.py new file mode 100644 index 00000000000..00db219238b --- /dev/null +++ b/printer_zpl2/models/zpl2.py @@ -0,0 +1,529 @@ +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copied from https://github.com/subteno-it/python-zpl2, as there has been new releases +# that breaks current code without clear source (no commit on the repo). As the amount +# of code is not too much, we put it on the module itself, being able to control the +# whole chain, and to reduce the code with the considerations of current Odoo version + +import binascii +import math + +try: + from PIL import ImageOps +except: + ImageOps = None + +try: + strcast = unicode +except: + strcast = str + +# Constants for the printer configuration management +CONF_RELOAD_FACTORY = "F" +CONF_RELOAD_NETWORK_FACTORY = "N" +CONF_RECALL_LAST_SAVED = "R" +CONF_SAVE_CURRENT = "S" + +# Command arguments names +ARG_FONT = "font" +ARG_HEIGHT = "height" +ARG_WIDTH = "width" +ARG_ORIENTATION = "orientation" +ARG_THICKNESS = "thickness" +ARG_BLOCK_WIDTH = "block_width" +ARG_BLOCK_LINES = "block_lines" +ARG_BLOCK_SPACES = "block_spaces" +ARG_BLOCK_JUSTIFY = "block_justify" +ARG_BLOCK_LEFT_MARGIN = "block_left_margin" +ARG_CHECK_DIGITS = "check_digits" +ARG_INTERPRETATION_LINE = "interpretation_line" +ARG_INTERPRETATION_LINE_ABOVE = "interpretation_line_above" +ARG_STARTING_MODE = "starting_mode" +ARG_SECURITY_LEVEL = "security_level" +ARG_COLUMNS_COUNT = "columns_count" +ARG_ROWS_COUNT = "rows_count" +ARG_TRUNCATE = "truncate" +ARG_MODE = "mode" +ARG_MODULE_WIDTH = "module_width" +ARG_BAR_WIDTH_RATIO = "bar_width_ratio" +ARG_REVERSE_PRINT = "reverse_print" +ARG_IN_BLOCK = "in_block" +ARG_COLOR = "color" +ARG_ROUNDING = "rounding" +ARG_DIAMETER = "diameter" +ARG_DIAGONAL_ORIENTATION = "diagonal_orientation" +ARG_MODEL = "model" +ARG_MAGNIFICATION_FACTOR = "magnification_factor" +ARG_ERROR_CORRECTION = "error_correction" +ARG_MASK_VALUE = "mask_value" + +# Model values +MODEL_ORIGINAL = 1 +MODEL_ENHANCED = 2 + +# Error Correction +ERROR_CORRECTION_ULTRA_HIGH = "H" +ERROR_CORRECTION_HIGH = "Q" +ERROR_CORRECTION_STANDARD = "M" +ERROR_CORRECTION_HIGH_DENSITY = "L" + +# Boolean values +BOOL_YES = "Y" +BOOL_NO = "N" + +# Orientation values +ORIENTATION_NORMAL = "N" +ORIENTATION_ROTATED = "R" +ORIENTATION_INVERTED = "I" +ORIENTATION_BOTTOM_UP = "B" + +# Diagonal lines orientation values +DIAGONAL_ORIENTATION_LEFT = "L" +DIAGONAL_ORIENTATION_RIGHT = "R" + +# Justify values +JUSTIFY_LEFT = "L" +JUSTIFY_CENTER = "C" +JUSTIFY_JUSTIFIED = "J" +JUSTIFY_RIGHT = "R" + +# Font values +FONT_DEFAULT = "0" +FONT_9X5 = "A" +FONT_11X7 = "B" +FONT_18X10 = "D" +FONT_28X15 = "E" +FONT_26X13 = "F" +FONT_60X40 = "G" +FONT_21X13 = "H" + +# Color values +COLOR_BLACK = "B" +COLOR_WHITE = "W" + +# Barcode types +BARCODE_CODE_11 = "code_11" +BARCODE_INTERLEAVED_2_OF_5 = "interleaved_2_of_5" +BARCODE_CODE_39 = "code_39" +BARCODE_CODE_49 = "code_49" +BARCODE_PDF417 = "pdf417" +BARCODE_EAN_8 = "ean-8" +BARCODE_UPC_E = "upc-e" +BARCODE_CODE_128 = "code_128" +BARCODE_EAN_13 = "ean-13" +BARCODE_QR_CODE = "qr_code" + + +class Zpl2(object): + """ZPL II management class + Allows to generate data for Zebra printers + """ + + def __init__(self): + self.encoding = "utf-8" + self.initialize() + + def initialize(self): + self._buffer = [] + + def output(self): + """Return the full contents to send to the printer""" + return "\n".encode(self.encoding).join(self._buffer) + + def _enforce(self, value, minimum=1, maximum=32000): + """Returns the value, forced between minimum and maximum""" + return min(max(minimum, value), maximum) + + def _write_command(self, data): + """Adds a complete command to buffer""" + self._buffer.append(strcast(data).encode(self.encoding)) + + def _generate_arguments(self, arguments, kwargs): + """Generate a zebra arguments from an argument names list and a dict of + values for these arguments + @param arguments : list of argument names, ORDER MATTERS + @param kwargs : list of arguments values + """ + command_arguments = [] + # Add all arguments in the list, if they exist + for argument in arguments: + if kwargs.get(argument, None) is not None: + if isinstance(kwargs[argument], bool): + kwargs[argument] = kwargs[argument] and BOOL_YES or BOOL_NO + command_arguments.append(kwargs[argument]) + + # Return a zebra formatted string, with a comma between each argument + return ",".join(map(str, command_arguments)) + + def print_width(self, label_width): + """Defines the print width setting on the printer""" + self._write_command("^PW%d" % label_width) + + def configuration_update(self, active_configuration): + """Set the active configuration on the printer""" + self._write_command("^JU%s" % active_configuration) + + def label_start(self): + """Adds the label start command to the buffer""" + self._write_command("^XA") + + def label_encoding(self): + """Adds the label encoding command to the buffer + Fixed value defined to UTF-8 + """ + self._write_command("^CI28") + + def label_end(self): + """Adds the label start command to the buffer""" + self._write_command("^XZ") + + def label_home(self, left, top): + """Define the label top left corner""" + self._write_command("^LH%d,%d" % (left, top)) + + def _field_origin(self, right, down): + """Define the top left corner of the data, from the top left corner of + the label + """ + return "^FO%d,%d" % (right, down) + + def _font_format(self, font_format): + """Send the commands which define the font to use for the current data""" + arguments = [ARG_FONT, ARG_HEIGHT, ARG_WIDTH] + # Add orientation in the font name (only place where there is + # no comma between values) + font_format[ARG_FONT] += font_format.get(ARG_ORIENTATION, ORIENTATION_NORMAL) + # Check that the height value fits in the allowed values + if font_format.get(ARG_HEIGHT) is not None: + font_format[ARG_HEIGHT] = self._enforce(font_format[ARG_HEIGHT], minimum=10) + # Check that the width value fits in the allowed values + if font_format.get(ARG_WIDTH) is not None: + font_format[ARG_WIDTH] = self._enforce(font_format[ARG_WIDTH], minimum=10) + # Generate the ZPL II command + return "^A" + self._generate_arguments(arguments, font_format) + + def _field_block(self, block_format): + """Define a maximum width to print some data""" + arguments = [ + ARG_BLOCK_WIDTH, + ARG_BLOCK_LINES, + ARG_BLOCK_SPACES, + ARG_BLOCK_JUSTIFY, + ARG_BLOCK_LEFT_MARGIN, + ] + return "^FB" + self._generate_arguments(arguments, block_format) + + def _barcode_format(self, barcodeType, barcode_format): + """Generate the commands to print a barcode + Each barcode type needs a specific function + """ + + def _code11(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_CHECK_DIGITS, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "1" + self._generate_arguments(arguments, kwargs) + + def _interleaved2of5(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ARG_CHECK_DIGITS, + ] + return "2" + self._generate_arguments(arguments, kwargs) + + def _code39(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_CHECK_DIGITS, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "3" + self._generate_arguments(arguments, kwargs) + + def _code49(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_STARTING_MODE, + ] + # Use interpretation_line and interpretation_line_above to generate + # a specific interpretation_line value + if kwargs.get(ARG_INTERPRETATION_LINE) is not None: + if kwargs[ARG_INTERPRETATION_LINE]: + if kwargs[ARG_INTERPRETATION_LINE_ABOVE]: + # Interpretation line after + kwargs[ARG_INTERPRETATION_LINE] = "A" + else: + # Interpretation line before + kwargs[ARG_INTERPRETATION_LINE] = "B" + else: + # No interpretation line + kwargs[ARG_INTERPRETATION_LINE] = "N" + return "4" + self._generate_arguments(arguments, kwargs) + + def _pdf417(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_SECURITY_LEVEL, + ARG_COLUMNS_COUNT, + ARG_ROWS_COUNT, + ARG_TRUNCATE, + ] + return "7" + self._generate_arguments(arguments, kwargs) + + def _ean8(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "8" + self._generate_arguments(arguments, kwargs) + + def _upce(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ARG_CHECK_DIGITS, + ] + return "9" + self._generate_arguments(arguments, kwargs) + + def _code128(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ARG_CHECK_DIGITS, + ARG_MODE, + ] + return "C" + self._generate_arguments(arguments, kwargs) + + def _ean13(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "E" + self._generate_arguments(arguments, kwargs) + + def _qrcode(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_MODEL, + ARG_MAGNIFICATION_FACTOR, + ARG_ERROR_CORRECTION, + ARG_MASK_VALUE, + ] + return "Q" + self._generate_arguments(arguments, kwargs) + + barcodeTypes = { + BARCODE_CODE_11: _code11, + BARCODE_INTERLEAVED_2_OF_5: _interleaved2of5, + BARCODE_CODE_39: _code39, + BARCODE_CODE_49: _code49, + BARCODE_PDF417: _pdf417, + BARCODE_EAN_8: _ean8, + BARCODE_UPC_E: _upce, + BARCODE_CODE_128: _code128, + BARCODE_EAN_13: _ean13, + BARCODE_QR_CODE: _qrcode, + } + return "^B" + barcodeTypes[barcodeType](**barcode_format) + + def _barcode_field_default(self, barcode_format): + """Add the data start command to the buffer""" + arguments = [ + ARG_MODULE_WIDTH, + ARG_BAR_WIDTH_RATIO, + ] + return "^BY" + self._generate_arguments(arguments, barcode_format) + + def _field_data_start(self): + """Add the data start command to the buffer""" + return "^FD" + + def _field_reverse_print(self): + """Allows the printed data to appear white over black, or black over white""" + return "^FR" + + def _field_data_stop(self): + """Add the data stop command to the buffer""" + return "^FS" + + def _field_data(self, data): + """Add data to the buffer, between start and stop commands""" + command = "{start}{data}{stop}".format( + start=self._field_data_start(), + data=data, + stop=self._field_data_stop(), + ) + return command + + def font_data(self, right, down, field_format, data): + """Add a full text in the buffer, with needed formatting commands""" + reverse = "" + if field_format.get(ARG_REVERSE_PRINT, False): + reverse = self._field_reverse_print() + block = "" + if field_format.get(ARG_IN_BLOCK, False): + block = self._field_block(field_format) + command = "{origin}{font_format}{reverse}{block}{data}".format( + origin=self._field_origin(right, down), + font_format=self._font_format(field_format), + reverse=reverse, + block=block, + data=self._field_data(data), + ) + self._write_command(command) + + def barcode_data(self, right, down, barcodeType, barcode_format, data): + """Add a full barcode in the buffer, with needed formatting commands""" + command = "{default}{origin}{barcode_format}{data}".format( + default=self._barcode_field_default(barcode_format), + origin=self._field_origin(right, down), + barcode_format=self._barcode_format(barcodeType, barcode_format), + data=self._field_data(data), + ) + self._write_command(command) + + def graphic_box(self, right, down, graphic_format): + """Send the commands to draw a rectangle""" + arguments = [ + ARG_WIDTH, + ARG_HEIGHT, + ARG_THICKNESS, + ARG_COLOR, + ARG_ROUNDING, + ] + # Check that the thickness value fits in the allowed values + if graphic_format.get(ARG_THICKNESS) is not None: + graphic_format[ARG_THICKNESS] = self._enforce(graphic_format[ARG_THICKNESS]) + # Check that the width value fits in the allowed values + if graphic_format.get(ARG_WIDTH) is not None: + graphic_format[ARG_WIDTH] = self._enforce( + graphic_format[ARG_WIDTH], minimum=graphic_format[ARG_THICKNESS] + ) + # Check that the height value fits in the allowed values + if graphic_format.get(ARG_HEIGHT) is not None: + graphic_format[ARG_HEIGHT] = self._enforce( + graphic_format[ARG_HEIGHT], minimum=graphic_format[ARG_THICKNESS] + ) + # Check that the rounding value fits in the allowed values + if graphic_format.get(ARG_ROUNDING) is not None: + graphic_format[ARG_ROUNDING] = self._enforce( + graphic_format[ARG_ROUNDING], minimum=0, maximum=8 + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data="^GB" + self._generate_arguments(arguments, graphic_format), + stop=self._field_data_stop(), + ) + self._write_command(command) + + def graphic_diagonal_line(self, right, down, graphic_format): + """Send the commands to draw a rectangle""" + arguments = [ + ARG_WIDTH, + ARG_HEIGHT, + ARG_THICKNESS, + ARG_COLOR, + ARG_DIAGONAL_ORIENTATION, + ] + # Check that the thickness value fits in the allowed values + if graphic_format.get(ARG_THICKNESS) is not None: + graphic_format[ARG_THICKNESS] = self._enforce(graphic_format[ARG_THICKNESS]) + # Check that the width value fits in the allowed values + if graphic_format.get(ARG_WIDTH) is not None: + graphic_format[ARG_WIDTH] = self._enforce( + graphic_format[ARG_WIDTH], minimum=3 + ) + # Check that the height value fits in the allowed values + if graphic_format.get(ARG_HEIGHT) is not None: + graphic_format[ARG_HEIGHT] = self._enforce( + graphic_format[ARG_HEIGHT], minimum=3 + ) + # Check the given orientation + graphic_format[ARG_DIAGONAL_ORIENTATION] = graphic_format.get( + ARG_DIAGONAL_ORIENTATION, DIAGONAL_ORIENTATION_LEFT + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data="^GD" + self._generate_arguments(arguments, graphic_format), + stop=self._field_data_stop(), + ) + self._write_command(command) + + def graphic_circle(self, right, down, graphic_format): + """Send the commands to draw a circle""" + arguments = [ARG_DIAMETER, ARG_THICKNESS, ARG_COLOR] + # Check that the diameter value fits in the allowed values + if graphic_format.get(ARG_DIAMETER) is not None: + graphic_format[ARG_DIAMETER] = self._enforce( + graphic_format[ARG_DIAMETER], minimum=3, maximum=4095 + ) + # Check that the thickness value fits in the allowed values + if graphic_format.get(ARG_THICKNESS) is not None: + graphic_format[ARG_THICKNESS] = self._enforce( + graphic_format[ARG_THICKNESS], minimum=2, maximum=4095 + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data="^GC" + self._generate_arguments(arguments, graphic_format), + stop=self._field_data_stop(), + ) + self._write_command(command) + + def graphic_field(self, right, down, pil_image): + """Encode a PIL image into an ASCII string suitable for ZPL printers""" + if ImageOps is None: + # Importing ImageOps from PIL didn't work + raise Exception( + "You must install Pillow to be able to use the graphic" + " fields feature" + ) + width, height = pil_image.size + rounded_width = int(math.ceil(width / 8.0) * 8) + # Transform the image : + # - Invert the colors (PIL uses 0 for black, ZPL uses 0 for white) + # - Convert to monochrome in case it is not already + # - Round the width to a multiple of 8 because ZPL needs an integer + # count of bytes per line (each pixel is a bit) + pil_image = ( + ImageOps.invert(pil_image).convert("1").crop((0, 0, rounded_width, height)) + ) + # Convert the image to a two-character hexadecimal values string + ascii_data = binascii.hexlify(pil_image.tobytes()).upper() + # Each byte is composed of two characters + bytes_per_row = rounded_width / 8 + total_bytes = bytes_per_row * height + graphic_image_command = ( + "^GFA,{total_bytes},{total_bytes},{bytes_per_row},{ascii_data}".format( + total_bytes=total_bytes, + bytes_per_row=bytes_per_row, + ascii_data=ascii_data, + ) + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data=graphic_image_command, + stop=self._field_data_stop(), + ) + self._write_command(command)