From c441cb4d376bfa428ed966d24512e728a5140496 Mon Sep 17 00:00:00 2001 From: Stephen Early Date: Thu, 9 May 2024 22:56:43 +0100 Subject: [PATCH] Remove imagesize dependency, use Pillow to load images --- .github/workflows/run-tests.yml | 1 - quicktill/pdrivers.py | 76 ++++++++++++++++++++------------- quicktill/tillconfig.py | 2 +- setup.cfg | 2 +- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5c872ec..de5e85a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -30,7 +30,6 @@ jobs: python3-cups \ python3-dateutil \ python3-httplib2 \ - python3-imagesize \ python3-odf \ python3-psycopg2 \ python3-qrcode \ diff --git a/quicktill/pdrivers.py b/quicktill/pdrivers.py index e5d0cea..a7a560c 100644 --- a/quicktill/pdrivers.py +++ b/quicktill/pdrivers.py @@ -13,7 +13,6 @@ _qrcode_supported = True except ImportError: _qrcode_supported = False -import imagesize from reportlab.pdfgen import canvas from reportlab.lib.units import toLength @@ -23,6 +22,7 @@ from reportlab.platypus import Frame from reportlab.platypus import BaseDocTemplate from reportlab.platypus import PageTemplate +from PIL import Image, ImageOps import logging log = logging.getLogger(__name__) @@ -111,18 +111,10 @@ def __str__(self): class ImageElement(ReceiptElement): def __init__(self, image): - assert image.startswith(b"P4") # only B&W PBM format supported - width, height = imagesize.get(io.BytesIO(image)) - data_lines = [ - line - for line in image.splitlines() - if not line.startswith(b"#") - ] - assert data_lines[0] == b"P4" - assert data_lines[1] == f"{width} {height}".encode("ascii") - self.image_data = bytes().join(data_lines[2:]) - self.image_width = width - self.image_height = height + try: + self.image_data = Image.open(io.BytesIO(image)) + except Exception as e: + self.left = f"Image error: {e}" def __str__(self): return "Image" @@ -717,8 +709,7 @@ def process_canvas(self, canvas, f): left, ' ' * padl, center, ' ' * padr, right)) .encode(self.coding)) elif hasattr(i, 'image_data'): - assert 8 * len(i.image_data) == i.image_width * i.image_height - self._image(i.image_data, i.image_width, i.image_height, f) + self._image(i.image_data, f) elif hasattr(i, 'qrcode_data'): self._qrcode(i.qrcode_data, f) else: @@ -731,24 +722,49 @@ def process_canvas(self, canvas, f): f.write(escpos.ep_fullcut) f.flush() - def _image(self, data, width, height, f): - # Print a PBM bit-image - if width > self.dpl: - # Image too wide for paper - return + def _image(self, image, f): + # If image is too wide, resize it + if image.size[0] > self.dpl: + image = image.resize( + (self.dpl, int(image.size[1] * self.dpl / image.size[0]))) + + image = image.convert("RGBA") + + width, height = image.size + + # Pad height to a multiple of 24 rows + heightpad = 24 - (height % 24) if height % 24 else 0 + # Pad width to center the image + widthpad = (self.dpl - width) // 2 + + # Create a new, completely white image of an appropriate size + primg = Image.new("RGB", (width + widthpad, height + heightpad), + (255, 255, 255)) - # Calculate padding required to center the image - padding = (self.dpl - width) // 2 - padchars = [False] * padding + # Paste the original image onto the new image in the appropriate place + primg.paste(image, box=(widthpad, heightpad // 2), + mask=image.getchannel("A")) + + # Remove colour + primg = primg.convert("L") + + # Convert to black and white + primg = ImageOps.invert(primg).convert("1") + + # Update width and height from padded image + width, height = primg.size + + # data is an iterator yielding pixel values, 0 for white and + # 255 for black + data = iter(primg.getdata()) # Partition the bitmap into lines lines = [] - for chunk in _chunks(data, width // 8): - line = padchars.copy() - for byte in chunk: - for bit in f"{int(byte):08b}": - line.append(bool(int(bit))) - lines.append(line) + try: + while True: + lines.append([bool(next(data)) for _ in range(width)]) + except StopIteration: + pass # Compact up to twenty-four bit-lines into each row rows = [] @@ -763,8 +779,8 @@ def _image(self, data, width, height, f): # Write the commands to render the padded image f.write(escpos.ep_line_spacing_none) f.write(escpos.ep_unidirectional_on) + width_info = width.to_bytes(length=2, byteorder="little") for row in rows: - width_info = (len(row) // 3).to_bytes(length=2, byteorder="little") f.write(escpos.ep_bitimage_dd_v24 + width_info + row + b'\n') f.write(escpos.ep_unidirectional_off) f.write(escpos.ep_line_spacing_default) diff --git a/quicktill/tillconfig.py b/quicktill/tillconfig.py index 87d6223..490afd1 100644 --- a/quicktill/tillconfig.py +++ b/quicktill/tillconfig.py @@ -33,7 +33,7 @@ 'core:sitelogo', None, display_name="Site logo", description=( "Logo image to be printed on receipts. " - "To update it, use 'base64 logo.pbm | runtill config -s core:sitelogo'" + "To update it, use 'base64 filename | runtill config -s core:sitelogo'" ) ) pubname = config.ConfigItem( diff --git a/setup.cfg b/setup.cfg index aef7680..2a52d3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ install_requires = pycups python-dateutil cryptography - imagesize ~= 1.4 + pillow python_requires = >=3.8 [options.extras_require]