# app.py import asyncio from functools import lru_cache from typing import List, Optional 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") class PrintRequest(BaseModel): # from LABEL_PRINTER_PARAMS printer_ip: str = Field(..., description="QL-1060N IP on LAN") model: str = Field(..., description="Brother model") label: str = Field(..., description="12 mm tape") # grocy payload: grocycode: str = Field(..., description="Raw GrocyCode") product: str = Field(..., description="Product name") due_date: Optional[str] = Field(None, description="Due date from stock entry") class Config: # ignore any other fields Grocy might send extra = "ignore" @lru_cache(maxsize=256) def make_code128( grocycode: str, product_name: str, due_date: Optional[str] ) -> Image.Image: # 1) Instantiate the barcode object (checksum auto-added for Code128) code = Code128(grocycode, 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": 5.0, # gap between bars and text # you can also pass 'format': 'PNG' here if needed }, text=( " | ".join([product_name, due_date]) if due_date else product_name ), # 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(grocycode: str) -> Image.Image: dm = encode(grocycode.encode("utf-8")) pil_img = Image.frombytes("RGB", (dm.width, dm.height), dm.pixels) return pil_img.convert("1") def compose_label(images: List[Image.Image]) -> Image.Image: """ Horizontally stack each barcode image with padding, first scaling them all to the same height. """ border, spacing = 10, 20 # 1) Determine target inner height (max of original heights) inner_h = max(img.height for img in images) # 2) Scale each image to inner_h, then frame it framed = [] for img in images: w, h = img.size new_w = int(w * inner_h / h) # high-quality up-scaling img_scaled = img.resize((new_w, inner_h), Image.LANCZOS) # add white border framed_img = ImageOps.expand(img_scaled, border=border, fill="white") framed.append(framed_img) # 3) Compute final canvas size total_w = sum(img.width for img in framed) + spacing * (len(framed) - 1) final_h = inner_h + 2 * border # 4) Create canvas and paste each framed image canvas = Image.new("1", (total_w, final_h), 1) x = 0 for img in framed: # vertical center (though they’re all same height now) y = (final_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_from_grocy(req: PrintRequest): try: loop = asyncio.get_running_loop() code_img, dm_img = await asyncio.gather( loop.run_in_executor( None, make_code128, req.grocycode, req.product, req.due_date ), loop.run_in_executor(None, make_datamatrix, req.grocycode), ) 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", }