switch from treepoem

This commit is contained in:
Piotr Oleszczyk 2025-06-18 15:03:21 +02:00
parent c6dcb50bbb
commit 85b466f482
3 changed files with 77 additions and 56 deletions

83
app.py
View file

@ -1,14 +1,18 @@
# app.py # app.py
import asyncio
from functools import lru_cache
from io import BytesIO from io import BytesIO
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import List from typing import List
from PIL import Image, ImageOps
import treepoem from barcode import Code128
from brother_ql.raster import BrotherQLRaster from barcode.writer import ImageWriter
from brother_ql.conversion import convert
from brother_ql.backends.helpers import send from brother_ql.backends.helpers import send
from brother_ql.conversion import convert
from brother_ql.raster import BrotherQLRaster
from fastapi import FastAPI, HTTPException
from PIL import Image, ImageOps
from pydantic import BaseModel, Field
from pylibdmtx.pylibdmtx import encode
if not hasattr(Image, "ANTIALIAS"): if not hasattr(Image, "ANTIALIAS"):
Image.ANTIALIAS = Image.Resampling.LANCZOS Image.ANTIALIAS = Image.Resampling.LANCZOS
@ -16,6 +20,29 @@ if not hasattr(Image, "ANTIALIAS"):
app = FastAPI(title="QL-1060N Stacked Barcode Printer") app = FastAPI(title="QL-1060N Stacked Barcode Printer")
@lru_cache(maxsize=256)
def make_code128(data: str) -> Image.Image:
writer_opts = {
"module_height": 15.0,
"module_width": 0.5,
"quiet_zone": 1.0,
"font_size": 10,
"text_distance": 1.0,
}
code = Code128(
data, writer=ImageWriter(), add_checksum=False, writer_options=writer_opts
)
pil_img = code.render(writer_opts) # returns RGB
return pil_img.convert("1")
@lru_cache(maxsize=256)
def make_datamatrix(data: str) -> Image.Image:
dm = encode(data.encode("utf-8"))
pil_img = Image.frombytes("RGB", (dm.width, dm.height), dm.pixels)
return pil_img.convert("1")
class PrintRequest(BaseModel): class PrintRequest(BaseModel):
data: str = Field(..., description="String to encode") data: str = Field(..., description="String to encode")
printer_ip: str = Field( printer_ip: str = Field(
@ -25,35 +52,8 @@ class PrintRequest(BaseModel):
label: str = Field("12", description="12 mm continuous tape") label: str = Field("12", description="12 mm continuous tape")
def make_barcode_image(data: str, symb: str) -> Image.Image:
"""Generate a monochrome barcode image via BWIPP."""
if symb == "code128":
img = treepoem.generate_barcode(
barcode_type="code128",
data=data,
options={"includetext": True, "height": "0.3"},
scale=2,
)
else: # rectangular DataMatrix
img = treepoem.generate_barcode(
barcode_type="datamatrixrectangular",
data=data,
options={},
# options={"columns": "16", "rows": "48"},
scale=2,
)
return img.convert("1")
def compose_label(images: List[Image.Image]) -> Image.Image: def compose_label(images: List[Image.Image]) -> Image.Image:
""" border, spacing = 10, 20
Horizontally stack each barcode image with padding,
on a single monochrome canvas.
"""
border = 10 # white border around each barcode
spacing = 20 # gap between them
# add border
framed = [ImageOps.expand(img, border=border, fill="white") for img in images] framed = [ImageOps.expand(img, border=border, fill="white") for img in images]
max_h = max(img.height for img in framed) max_h = max(img.height for img in framed)
total_w = sum(img.width for img in framed) + spacing * (len(framed) - 1) total_w = sum(img.width for img in framed) + spacing * (len(framed) - 1)
@ -69,9 +69,6 @@ def compose_label(images: List[Image.Image]) -> Image.Image:
def send_to_printer(image: Image.Image, printer_ip: str, model: str, label: str): def send_to_printer(image: Image.Image, printer_ip: str, model: str, label: str):
"""
Rasterize the PIL image for Brother QL and send it via network.
"""
qlr = BrotherQLRaster(model) qlr = BrotherQLRaster(model)
qlr.exception_on_warning = True qlr.exception_on_warning = True
@ -79,7 +76,7 @@ def send_to_printer(image: Image.Image, printer_ip: str, model: str, label: str)
qlr=qlr, qlr=qlr,
images=[image], images=[image],
label=label, label=label,
rotate="90", rotate="90", # landscape
threshold=70.0, threshold=70.0,
dither=False, dither=False,
compress=True, compress=True,
@ -95,12 +92,16 @@ def send_to_printer(image: Image.Image, printer_ip: str, model: str, label: str)
@app.post("/print", summary="Print Code128 + DataMatrix side-by-side") @app.post("/print", summary="Print Code128 + DataMatrix side-by-side")
def print_stacked(req: PrintRequest): async def print_stacked(req: PrintRequest):
try: try:
code_img = make_barcode_image(req.data, "code128") loop = asyncio.get_running_loop()
dm_img = make_barcode_image(req.data, "datamatrix_rect") code_task = loop.run_in_executor(None, make_code128, req.data)
dm_task = loop.run_in_executor(None, make_datamatrix, req.data)
code_img, dm_img = await asyncio.gather(code_task, dm_task)
label_img = compose_label([code_img, dm_img]) label_img = compose_label([code_img, dm_img])
send_to_printer(label_img, req.printer_ip, req.model, req.label) send_to_printer(label_img, req.printer_ip, req.model, req.label)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View file

@ -7,8 +7,13 @@ requires-python = ">=3.11"
dependencies = [ dependencies = [
"brother-ql>=0.9.4", "brother-ql>=0.9.4",
"fastapi>=0.115.13", "fastapi>=0.115.13",
"packaging>=25.0",
"pillow>=11.2.1", "pillow>=11.2.1",
"pydantic>=2.11.7", "pydantic>=2.11.7",
"treepoem>=3.27.1", "pylibdmtx",
"python-barcode>=0.15.1",
"uvicorn>=0.34.3", "uvicorn>=0.34.3",
] ]
[tool.uv.sources]
pylibdmtx = { git = "https://github.com/NaturalHistoryMuseum/pylibdmtx.git" }

43
uv.lock generated
View file

@ -102,9 +102,11 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "brother-ql" }, { name = "brother-ql" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "packaging" },
{ name = "pillow" }, { name = "pillow" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "treepoem" }, { name = "pylibdmtx" },
{ name = "python-barcode" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@ -112,9 +114,11 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "brother-ql", specifier = ">=0.9.4" }, { name = "brother-ql", specifier = ">=0.9.4" },
{ name = "fastapi", specifier = ">=0.115.13" }, { name = "fastapi", specifier = ">=0.115.13" },
{ name = "packaging", specifier = ">=25.0" },
{ name = "pillow", specifier = ">=11.2.1" }, { name = "pillow", specifier = ">=11.2.1" },
{ name = "pydantic", specifier = ">=2.11.7" }, { name = "pydantic", specifier = ">=2.11.7" },
{ name = "treepoem", specifier = ">=3.27.1" }, { name = "pylibdmtx", git = "https://github.com/NaturalHistoryMuseum/pylibdmtx.git" },
{ name = "python-barcode", specifier = ">=0.15.1" },
{ name = "uvicorn", specifier = ">=0.34.3" }, { name = "uvicorn", specifier = ">=0.34.3" },
] ]
@ -136,6 +140,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
] ]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]] [[package]]
name = "packbits" name = "packbits"
version = "0.6" version = "0.6"
@ -281,6 +294,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
] ]
[[package]]
name = "pylibdmtx"
version = "0.1.11"
source = { git = "https://github.com/NaturalHistoryMuseum/pylibdmtx.git#00a6e0691db6cd2951d5b4614a9825962f53a7ff" }
[[package]]
name = "python-barcode"
version = "0.15.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/63/bc2fb47c9ba904b376780917f053b1c85b87085fd44948590e71c11187b0/python-barcode-0.15.1.tar.gz", hash = "sha256:3b1825fbdb11e597466dff4286b4ea9b1e86a57717b59e563ae679726fc854de", size = 228161, upload-time = "2023-07-05T22:56:59.962Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/27/9b5c5bb1938d4e6b12f4c95f40ea905c11df3cd58e128e9305397b9a2697/python_barcode-0.15.1-py3-none-any.whl", hash = "sha256:057636fba37369c22852410c8535b36adfbeb965ddfd4e5b6924455d692e0886", size = 212956, upload-time = "2023-07-05T22:56:58.596Z" },
]
[[package]] [[package]]
name = "pyusb" name = "pyusb"
version = "1.3.1" version = "1.3.1"
@ -311,18 +338,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
] ]
[[package]]
name = "treepoem"
version = "3.27.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d2/75/adcea502d2a4832847b096df58fa69a5bfa3581ecaf9dda3fb066a38d304/treepoem-3.27.1.tar.gz", hash = "sha256:471769fdbc0e9fce37d9f139446db1763ba1c93d4f823e398b364c2a71f8429c", size = 375487, upload-time = "2025-03-22T11:24:13.115Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/93/b943eed507664fbfe81f27dce690bcebe7d4c67edfd615109e137e8d751c/treepoem-3.27.1-py3-none-any.whl", hash = "sha256:8f929b0686fa40bdfe7b2fb0a134a3266481cd321a234ec7a279d7bac07d55a7", size = 372013, upload-time = "2025-03-22T11:24:11.076Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.14.0" version = "4.14.0"