From 1d4cbf99fd3aeed469c292f6960071b105f9f092 Mon Sep 17 00:00:00 2001 From: "Loan (LSE)" Date: Wed, 28 Jun 2023 14:39:19 +0200 Subject: [PATCH] [FIX] hw_drivers: support for TM-U escpos printer models Before this commit: When printing a receipt with an epson printers models TM-U2X0 (like TM-U220), random characters will be printed instead of the receipt. This happened as a picture of the receipt is sent to the IoT box, but the TM-U2X0 models does not support all "Bit Image commands". We currently use the GS v 0 one which is not supported by this printer models. See: https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=94 In addition, of that, the command itself is obsolete. After this commit: The support for another command "ESC *" is added which is not obsolete and supported by TM-U2X0 models. Notes: - Compared to "GS v 0", the receipt printed with "ESC *" is worst: - It take longer to print (5 seconds vs 1 second) - There is consistently some thin "empty white lines" - Most of the code is inspired from `python-escpos`: https://github.com/python-escpos/python-escpos/blob/master/src/escpos/escpos.py Due to the drawbacks, making it the default way to print would be a bad idea. As such, we can configure the mode (with other parameters) using particular name for the printer (which can be done by adding it manually with cups) opw-3351084,3341907 closes odoo/odoo#131332 X-original-commit: f30085bf673712c205e7ff318961d597297648d0 Signed-off-by: Joseph Caburnay (jcb) Signed-off-by: Loan Sens (lse) --- .../iot_handlers/drivers/PrinterDriver_L.py | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/addons/hw_drivers/iot_handlers/drivers/PrinterDriver_L.py b/addons/hw_drivers/iot_handlers/drivers/PrinterDriver_L.py index 1b9dfbeae7d..809057ee9b9 100644 --- a/addons/hw_drivers/iot_handlers/drivers/PrinterDriver_L.py +++ b/addons/hw_drivers/iot_handlers/drivers/PrinterDriver_L.py @@ -194,7 +194,8 @@ class PrinterDriver(Driver): return raster_init + raster_page_length + raster_data + raster_close - def format_escpos(self, im): + def format_escpos_bit_image_raster(self, im): + """ prints with the `GS v 0`-command """ width = int((im.width + 7) / 8) raster_send = b'\x1d\x76\x30\x00' @@ -208,7 +209,111 @@ class PrinterDriver(Driver): raster_data += raster_send + width.to_bytes(2, 'little') + slice_height.to_bytes(2, 'little') + im_slice dots = dots[width*max_slice_height:] - return raster_data + RECEIPT_PRINTER_COMMANDS['escpos']['cut'] + return raster_data + + def extract_columns_from_picture(self, im, line_height): + # Code inspired from python esc pos library: + # https://github.com/python-escpos/python-escpos/blob/4a0f5855ef118a2009b843a3a106874701d8eddf/src/escpos/image.py#L73-L89 + width_pixels, height_pixels = im.size + for left in range(0, width_pixels, line_height): + box = (left, 0, left + line_height, height_pixels) + im_chunk = im.transform( + (line_height, height_pixels), + Image.EXTENT, + box + ) + yield im_chunk.tobytes() + + def format_escpos_bit_image_column(self, im, high_density_vertical=True, + high_density_horizontal=True, + size_scale=100): + """ prints with the `ESC *`-command + reference: https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=88 + + :param im: PIL image to print + :param high_density_vertical: print in high density in vertical direction + :param high_density_horizontal: print in high density in horizontal direction + :param size_scale: picture scale in percentage, + e.g: 50 -> half the size (horizontally and vertically) + """ + size_scale_ratio = size_scale / 100 + size_scale_width = int(im.width * size_scale_ratio) + size_scale_height = int(im.height * size_scale_ratio) + im = im.resize((size_scale_width, size_scale_height)) + # escpos ESC * command print column per column + # (instead of usual row by row). + # So we transpose the picture to ease the calculations + im = im.transpose(Image.ROTATE_270).transpose(Image.FLIP_LEFT_RIGHT) + + # Most of the code here is inspired from python escpos library + # https://github.com/python-escpos/python-escpos/blob/4a0f5855ef118a2009b843a3a106874701d8eddf/src/escpos/escpos.py#L237C9-L251 + ESC = b'\x1b' + density_byte = (1 if high_density_horizontal else 0) + \ + (32 if high_density_vertical else 0) + nL = im.height & 0xFF + nH = (im.height >> 8) & 0xFF + HEADER = ESC + b'*' + bytes([density_byte, nL, nH]) + + raster_data = ESC + b'3\x10' # Adjust line-feed size + line_height = 24 if high_density_vertical else 8 + for column in self.extract_columns_from_picture(im, line_height): + raster_data += HEADER + column + b'\n' + raster_data += ESC + b'2' # Reset line-feed size + return raster_data + + def format_escpos(self, im): + # Epson support different command to print pictures. + # We use by default "GS v 0", but it is incompatible with certain + # printer models (like TM-U2x0) + # As we are pretty limited in the information that we have, we will + # use the printer name to parse some configuration value + # Printer name examples: + # EpsonTMM30 + # -> Print using raster mode + # TM-U220__IMC_LDV_LDH_SCALE70__ + # -> Print using column bit image mode (without vertical and + # horizontal density and a scale of 70%) + + # Default image printing mode + image_mode = 'raster' + + options_str = self.device_name.split('__') + option_str = "" + if len(options_str) > 2: + option_str = options_str[1].upper() + if option_str.startswith('IMC'): + image_mode = 'column' + + if image_mode == 'column': + # Default printing mode parameters + high_density_vertical = True + high_density_horizontal = True + scale = 100 + + # Parse the printer name to get the needed parameters + # The separator need to not be filtered by `get_identifier` + options = option_str.split('_') + for option in options: + if option == 'LDV': + high_density_vertical = False + elif option == 'LDH': + high_density_horizontal = False + elif option.startswith('SCALE'): + scale_value_str = re.search(r'\d+$', option) + if scale_value_str is not None: + scale = int(scale_value_str.group()) + else: + raise ValueError( + "Missing printer SCALE parameter integer " + "value in option: " + option) + + res = self.format_escpos_bit_image_column(im, + high_density_vertical, + high_density_horizontal, + scale) + else: + res = self.format_escpos_bit_image_raster(im) + return res + RECEIPT_PRINTER_COMMANDS['escpos']['cut'] def print_status(self): """Prints the status ticket of the IoTBox on the current printer."""