148 lines
4.5 KiB
Python
148 lines
4.5 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 fastapi.middleware.cors import CORSMiddleware
|
||
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")
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["https://grocy-zyczliwa.oleszczyk.eu"],
|
||
allow_methods=["POST"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
|
||
@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": 5.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:
|
||
"""
|
||
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_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",
|
||
}
|