switch from treepoem
This commit is contained in:
parent
c6dcb50bbb
commit
85b466f482
3 changed files with 77 additions and 56 deletions
83
app.py
83
app.py
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
43
uv.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue