[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.
This commit is contained in:
Sylvain GARANCHER
2022-09-09 09:55:19 +02:00
committed by Pedro M. Baeza
parent 7e8dce7234
commit 586fd86b06

529
printer_zpl2/models/zpl2.py Normal file
View File

@@ -0,0 +1,529 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# 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)