Skip to content

Commit

Permalink
Receipt logos: add support for black-and-white binary netpbm images. (#…
Browse files Browse the repository at this point in the history
…282)

This adds a dependency on the imagesize package
  • Loading branch information
jayaddison authored May 9, 2024
1 parent b0ca275 commit e66bbc4
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
python3-cups \
python3-dateutil \
python3-httplib2 \
python3-imagesize \
python3-odf \
python3-psycopg2 \
python3-qrcode \
Expand Down
Binary file added examples/data/cc.pbm
Binary file not shown.
73 changes: 72 additions & 1 deletion quicktill/pdrivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
_qrcode_supported = True
except ImportError:
_qrcode_supported = False
import imagesize

from reportlab.pdfgen import canvas
from reportlab.lib.units import toLength
Expand Down Expand Up @@ -110,7 +111,18 @@ def __str__(self):

class ImageElement(ReceiptElement):
def __init__(self, image):
self.image_data = 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

def __str__(self):
return "Image"
Expand Down Expand Up @@ -296,6 +308,18 @@ def _lpgetstatus(f):
return "error light is on" # LP_PERRORP


def _chunks(iterable, chunk_size):
"""Produce chunks of up-to a parameterised size from an iterable input."""
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == chunk_size:
yield chunk
chunk = []
if chunk:
yield chunk


class linux_lpprinter(fileprinter):
"""Print to a lp device file - /dev/lp? or /dev/usblp? on Linux
Expand Down Expand Up @@ -592,6 +616,9 @@ class escpos:
ep_unidirectional_off = bytes([27, 85, 0])
# follow ep_bitimage_sd with 16-bit little-endian data length
ep_bitimage_sd = bytes([27, 42, 0])
ep_bitimage_dd_v24 = bytes([27, 42, 33]) # double-density, 24-dot vertical
ep_line_spacing_none = bytes([27, 51, 0])
ep_line_spacing_default = bytes([27, 50])
ep_short_feed = bytes([27, 74, 5])
ep_half_dot_feed = bytes([27, 74, 1])

Expand Down Expand Up @@ -689,6 +716,9 @@ def process_canvas(self, canvas, f):
("%s%s%s%s%s\n" % (
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)
elif hasattr(i, 'qrcode_data'):
self._qrcode(i.qrcode_data, f)
else:
Expand All @@ -701,6 +731,47 @@ 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

# Calculate padding required to center the image
padding = (self.dpl - width) // 2
padchars = [False] * padding

# 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)

# Compact up to twenty-four bit-lines into each row
rows = []
for linerange in _chunks(lines, 24):
row = []
for column in zip(*linerange):
for segment in _chunks(column, 8):
binary = str().join("1" if bit else "0" for bit in segment)
row.append(int(binary or "0", base=2))
rows.append(bytes(row))

# Write the commands to render the padded image
f.write(escpos.ep_line_spacing_none)
f.write(escpos.ep_unidirectional_on)
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)

# Clear the line for subsequent content
f.write(b'\r\n')

def _qrcode_native(self, data, f):
# Set the size of a "module", in dots. The default is apparently
# 3 (which is also the lowest). The maximum is 16.
Expand Down
5 changes: 5 additions & 0 deletions quicktill/printer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import base64

from . import td, ui, tillconfig, payment
from decimal import Decimal
from .models import Delivery, VatBand, Business, Transaction, PayType
Expand Down Expand Up @@ -25,6 +27,9 @@ def print_receipt(printer, transid):
if not trans.lines:
return
with printer as d:
if tillconfig.publogo():
image = base64.b64decode(tillconfig.publogo())
d.printimage(image)
d.printline(f"\t{tillconfig.pubname}", emph=1)
for i in tillconfig.pubaddr().splitlines():
d.printline(f"\t{i}", colour=1)
Expand Down
7 changes: 7 additions & 0 deletions quicktill/tillconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
label_printers = []
cash_drawer = None

publogo = config.ConfigItem(
'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'"
)
)
pubname = config.ConfigItem(
'core:sitename', "Default site name", display_name="Site name",
description="Site name to be printed on receipts")
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ install_requires =
pycups
python-dateutil
cryptography
imagesize ~= 1.4
python_requires = >=3.8

[options.extras_require]
Expand Down

0 comments on commit e66bbc4

Please sign in to comment.