Files
lamp-testbench/crates/brother-ql/tools/golden.py
T
grabowski 2a3f28001c Lamp testbench in Rust around the HPCS 6500
lampbench station / export / label / tm30 / calibrate replace the Python
lamp_station.py, lamp_export.py, label_printer.py, tm30.py and
calibration.py that lived in HPCS6500-py. The driver is a submodule
(HPCS6500/, like mppt-testbench). Workspace crates: tm30 (TM-30-18 port of
colour-science 0.4.7), brother-ql (raster protocol, byte-identical to
brother_ql) and lamp-label (QR + ID labels for the QL-820NWB).

Bundles are byte-identical to lamp_export.py's (golden tests); the station
runs end to end in a test against the simulated instrument.
2026-10-22 17:26:14 +07:00

214 lines
8.7 KiB
Python

"""Generate golden test data for the Rust brother-ql crate.
For every case this writes the input image(s) as 1-bit PNG plus the output of
Python ``brother_ql.conversion.convert`` (brother_ql_next 0.12) as ``.bin``
into ``crates/brother-ql/tests/golden/`` and lists the cases in
``manifest.txt``. ``tests/golden.rs`` re-runs the conversion in Rust and
requires identical bytes.
Run with a Python that has brother_ql_next, qrcode and pillow, e.g.:
C:/dev/HPCS/.venv/Scripts/python.exe -W ignore crates/brother-ql/tools/golden.py
``render_label`` below is a verbatim copy of the HPCS ``label_printer.py``
function. Pass ``--reference path/to/label_printer.py`` (or set
``LABEL_PRINTER_PY``) to import the original instead and assert both agree.
"""
import argparse
import importlib.util
import os
import random
import sys
from pathlib import Path
import qrcode
from PIL import Image, ImageDraw, ImageFont
from brother_ql.conversion import convert
from brother_ql.raster import BrotherQLRaster
OUT = Path(__file__).resolve().parent.parent / "tests" / "golden"
# ---- verbatim copy of label_printer.py (HPCS) -------------------------------
DOTS = (202, 202)
TEXT_HEIGHT = 52
def _font(size):
for name in ("arialbd.ttf", "arial.ttf", "DejaVuSans-Bold.ttf"):
try:
return ImageFont.truetype(name, size)
except OSError:
continue
return ImageFont.load_default(size)
def render_label(lamp_id: str) -> Image.Image:
image = Image.new("1", DOTS, 1)
qr = qrcode.QRCode(
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=1,
border=2,
)
qr.add_data(lamp_id.upper())
qr.make(fit=True)
modules = len(qr.get_matrix())
qr_size = DOTS[1] - TEXT_HEIGHT
qr.box_size = max(1, qr_size // modules)
qr_img = qr.make_image().get_image().convert("1")
image.paste(qr_img, ((DOTS[0] - qr_img.width) // 2, (qr_size - qr_img.height) // 2))
draw = ImageDraw.Draw(image)
size = 44
while size > 10:
font = _font(size)
box = draw.textbbox((0, 0), lamp_id, font=font)
if box[2] - box[0] <= DOTS[0] - 8:
break
size -= 2
draw.text(
((DOTS[0] - (box[2] - box[0])) // 2 - box[0], DOTS[1] - TEXT_HEIGHT + (TEXT_HEIGHT - (box[3] - box[1])) // 2 - box[1]),
lamp_id,
font=font,
fill=0,
)
return image
# -----------------------------------------------------------------------------
def random_image(w, h, seed, p=0.5):
rng = random.Random(seed)
im = Image.new("1", (w, h), 1)
px = im.load()
for y in range(h):
for x in range(w):
if rng.random() < p:
px[x, y] = 0
return im
def synthetic():
w, h = 202, 202
imgs = {
"white_202": Image.new("1", (w, h), 1),
"black_202": Image.new("1", (w, h), 0),
}
chk = Image.new("1", (w, h), 1)
px = chk.load()
for y in range(h):
for x in range(w):
if (x + y) % 2:
px[x, y] = 0
imgs["checker_202"] = chk
for name, (x, y) in {
"corner_tl": (0, 0),
"corner_tr": (w - 1, 0),
"corner_bl": (0, h - 1),
"corner_br": (w - 1, h - 1),
}.items():
im = Image.new("1", (w, h), 1)
im.putpixel((x, y), 0)
imgs[name] = im
imgs["random_202"] = random_image(w, h, 1234)
return imgs
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--reference", default=os.environ.get("LABEL_PRINTER_PY"))
args = ap.parse_args()
reference = None
if args.reference and Path(args.reference).exists():
spec = importlib.util.spec_from_file_location("label_printer_ref", args.reference)
reference = importlib.util.module_from_spec(spec)
spec.loader.exec_module(reference)
OUT.mkdir(parents=True, exist_ok=True)
for f in OUT.iterdir():
if f.suffix in (".png", ".bin") or f.name == "manifest.txt":
f.unlink()
images = {}
for lamp_id in ("L0001", "L0006", "L0123", "L12345", "L9"):
im = render_label(lamp_id)
if reference is not None:
ref = reference.render_label(lamp_id)
assert ref.tobytes() == im.tobytes(), f"embedded render_label differs for {lamp_id}"
images[f"label_{lamp_id}"] = im
images.update(synthetic())
images["random_404"] = random_image(404, 404, 404)
images["random_696x300"] = random_image(696, 300, 696)
images["random_500x200"] = random_image(500, 200, 500)
images["random_300x696"] = random_image(300, 696, 300)
images["random_1392x150"] = random_image(1392, 150, 1392)
images["random_306x300"] = random_image(306, 300, 306)
images["random_271x696"] = random_image(271, 696, 2716)
images["random_236"] = random_image(236, 236, 236)
images["sparse_1164x301"] = random_image(1164, 301, 1164, p=0.01)
images["random_696x271"] = random_image(696, 271, 271)
images["sparse_696x301"] = random_image(696, 301, 301, p=0.01)
for name, im in images.items():
im.save(OUT / f"{name}.png")
cases = [] # (case name, model, label, [image names], kwargs)
base = list(images)[:13] # 5 labels + 8 synthetic 202x202 images
for name in base:
for copies in (1, 2):
cases.append((f"{name}_x{copies}", "QL-820NWB", "23x23", [name] * copies, {"cut": True}))
q = "QL-820NWB"
cases += [
("label_L0006_nocut", q, "23x23", ["label_L0006"], {"cut": False}),
("label_L0006_compress_x2", q, "23x23", ["label_L0006"] * 2, {"compress": True}),
("random_202_compress", q, "23x23", ["random_202"], {"compress": True}),
("checker_202_compress", q, "23x23", ["checker_202"], {"compress": True}),
("black_202_compress", q, "23x23", ["black_202"], {"compress": True}),
("white_202_compress", q, "23x23", ["white_202"], {"compress": True}),
("corner_tl_rot0", q, "23x23", ["corner_tl"], {"rotate": 0}),
("corner_tl_rot90", q, "23x23", ["corner_tl"], {"rotate": 90}),
("corner_tl_rot180", q, "23x23", ["corner_tl"], {"rotate": 180}),
("corner_tl_rot270", q, "23x23", ["corner_tl"], {"rotate": 270}),
("corner_br_rot0", q, "23x23", ["corner_br"], {"rotate": 0}),
("random_202_thr100", q, "23x23", ["random_202"], {"threshold": 100}),
("random_202_thr0", q, "23x23", ["random_202"], {"threshold": 0}),
("random_202_thr50", q, "23x23", ["random_202"], {"threshold": 50}),
("random_202_lq", q, "23x23", ["random_202"], {"hq": False}),
("random_202_dither", q, "23x23", ["random_202"], {"dither": True}),
("random_202_dither_thr100", q, "23x23", ["random_202"], {"dither": True, "threshold": 100}),
("multi_labels", q, "23x23", ["label_L0001", "label_L0006", "label_L12345"], {"cut": True}),
("random_404_dpi600", q, "23x23", ["random_404"], {"dpi_600": True}),
("random_404_dpi600_compress", q, "23x23", ["random_404"], {"dpi_600": True, "compress": True}),
("endless62_696", q, "62", ["random_696x300"], {}),
("endless62_resize500", q, "62", ["random_500x200"], {}),
("endless62_rot90", q, "62", ["random_300x696"], {"rotate": 90}),
("endless62_dpi600", q, "62", ["random_1392x150"], {"dpi_600": True}),
("endless29_306", q, "29", ["random_306x300"], {"compress": True}),
("diecut62x29_auto_rotate", q, "62x29", ["random_271x696"], {}),
("round_d24", q, "d24", ["random_236"], {}),
("ql1100_102", "QL-1100", "102", ["sparse_1164x301"], {"compress": True}),
("ql1100_62", "QL-1100", "62", ["sparse_696x301"], {"compress": True}),
("ql700_62_compress_ignored", "QL-700", "62", ["random_696x300"], {"compress": True}),
("ql500_29", "QL-500", "29", ["random_306x300"], {}),
("ql570_62x29", "QL-570", "62x29", ["random_696x271"], {"cut": False}),
("ql800_23x23_x2", "QL-800", "23x23", ["label_L0006"] * 2, {"compress": True}),
]
lines = ["# name model label images options... (generated by tools/golden.py)"]
for name, model, label, imgs, kwargs in cases:
inputs = [Image.open(OUT / f"{i}.png") for i in imgs]
for im in inputs:
assert im.mode == "1", (i, im.mode)
data = convert(qlr=BrotherQLRaster(model), images=inputs, label=label, **kwargs)
(OUT / f"{name}.bin").write_bytes(data)
opts = " ".join(f"{k}={v}" for k, v in kwargs.items())
lines.append(f"{name} {model} {label} {','.join(i + '.png' for i in imgs)} {opts}".rstrip())
(OUT / "manifest.txt").write_bytes(("\n".join(lines) + "\n").encode("ascii"))
total = sum(f.stat().st_size for f in OUT.iterdir())
print(f"{len(cases)} cases, {len(images)} images, {total} bytes in {OUT}")
print("reference render_label checked" if reference else "reference render_label NOT checked")
if __name__ == "__main__":
main()