grocy-ql-labeler/app.py

119 lines
3.7 KiB
Python

# app.py
import asyncio
from functools import lru_cache
from io import BytesIO
from typing import List
from barcode import Code128
from barcode.writer import ImageWriter
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"):
Image.ANTIALIAS = Image.Resampling.LANCZOS
app = FastAPI(title="QL-1060N Stacked Barcode Printer")
@lru_cache(maxsize=256)
def make_code128(data: str) -> Image.Image:
# 1) Instantiate the barcode object (checksum auto-added for Code128)
code = Code128(data, writer=ImageWriter())
# 2) Render to a PIL.Image, passing any writer options and optional text override
pil_img = code.render(
writer_options={
"module_height": 15.0, # bar height
"module_width": 0.5, # bar thickness
"quiet_zone": 1.0, # margin on each side
"font_size": 10, # text size
"text_distance": 1.0, # gap between bars and text
# you can also pass 'format': 'PNG' here if needed
},
text=data, # explicitly draw your data string under the bars
)
# 3) Convert to 1-bit for your Brother QL workflow
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):
data: str = Field(..., description="String to encode")
printer_ip: str = Field(
"192.168.178.49", description="Brother QL-1060N IP on your LAN"
)
model: str = Field("QL-1060N", description="Brother model")
label: str = Field("12", description="12 mm continuous tape")
def compose_label(images: List[Image.Image]) -> Image.Image:
border, spacing = 10, 20
framed = [ImageOps.expand(img, border=border, fill="white") for img in images]
max_h = max(img.height for img in framed)
total_w = sum(img.width for img in framed) + spacing * (len(framed) - 1)
canvas = Image.new("1", (total_w, max_h), 1)
x = 0
for img in framed:
y = (max_h - img.height) // 2
canvas.paste(img, (x, y))
x += img.width + spacing
return canvas
def send_to_printer(image: Image.Image, printer_ip: str, model: str, label: str):
qlr = BrotherQLRaster(model)
qlr.exception_on_warning = True
instructions = convert(
qlr=qlr,
images=[image],
label=label,
rotate="90", # landscape
threshold=70.0,
dither=False,
compress=True,
cut=True,
)
send(
instructions=instructions,
printer_identifier=printer_ip,
backend_identifier="network",
blocking=True,
)
@app.post("/print", summary="Print Code128 + DataMatrix side-by-side")
async def print_stacked(req: PrintRequest):
try:
loop = asyncio.get_running_loop()
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])
send_to_printer(label_img, req.printer_ip, req.model, req.label)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {
"status": "printed",
"model": req.model,
"label_width_mm": req.label,
"layout": "code128 + datamatrix side by side",
}