Files
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

162 lines
5.7 KiB
Python

"""Generate golden data for the Rust lamp-label crate.
Writes into ``crates/lamp-label/tests/golden/``:
* ``qr_matrices.txt``: for each ID, the QR code Python ``qrcode`` 8.2 builds
for ``render_label`` (ERROR_CORRECT_M, fit=True, automatic mask): a header
line ``<id> version=<v> mask=<m> size=<n>`` followed by ``n`` rows of
``0``/``1`` (1 = dark), quiet zone not included.
* ``label_<id>.png``: the complete 202x202 label from Python ``render_label``.
* ``text_info.txt``: per ID the font, chosen size, PIL text bbox and the draw
origin used by ``render_label``.
Run with: C:/dev/HPCS/.venv/Scripts/python.exe -W ignore crates/lamp-label/tools/golden.py
"""
import argparse
import importlib.util
import os
import re
from pathlib import Path
import qrcode
from PIL import Image, ImageDraw, ImageFont
OUT = Path(__file__).resolve().parent.parent / "tests" / "golden"
DOTS = (202, 202)
TEXT_HEIGHT = 52
QR_IDS = [f"L{i:04d}" for i in range(1, 21)] + [
"L0999",
"L1000",
"L12345",
"L123456",
# mode / version coverage
"12345678", # numeric mode
"LAMP-7 X", # alphanumeric with space
"lamp_7", # upper-cased, '_' -> byte mode
"https://example.com/lamp/L0006?x=1", # alnum chunk + byte chunk
"L" + "0123456789" * 13, # byte 'L' + numeric chunk, version 4
"L" + "ABCDEFGHIJ" * 20, # alphanumeric, version 8 (version info)
]
TEXT_IDS = ["L0001", "L0006", "L0123", "L12345", "L9", "L123456", "L0123456789012", "LAVA-7", "WWWWWWWWWW", "lamp_7"]
# ---- verbatim copy of label_printer.py (HPCS) -------------------------------
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 text_layout(lamp_id):
"""Same loop as render_label, returning what it decided."""
draw = ImageDraw.Draw(Image.new("1", DOTS, 1))
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
origin = (
(DOTS[0] - (box[2] - box[0])) // 2 - box[0],
DOTS[1] - TEXT_HEIGHT + (TEXT_HEIGHT - (box[3] - box[1])) // 2 - box[1],
)
return font, size, box, origin
def file_safe(lamp_id):
return re.sub(r"[^A-Za-z0-9_-]", "_", lamp_id)[:40]
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", ".txt"):
f.unlink()
lines = []
for lamp_id in QR_IDS:
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)
mask = qr.best_mask_pattern() # deterministic; same choice make() used
qr.make(fit=True)
n = qr.modules_count
lines.append(f"{lamp_id} version={qr.version} mask={mask} size={n}")
for row in qr.modules:
lines.append("".join("1" if m else "0" for m in row))
(OUT / "qr_matrices.txt").write_bytes(("\n".join(lines) + "\n").encode("utf-8"))
names = []
for lamp_id in dict.fromkeys(QR_IDS + TEXT_IDS):
im = render_label(lamp_id)
if reference is not None:
assert reference.render_label(lamp_id).tobytes() == im.tobytes(), lamp_id
name = f"label_{file_safe(lamp_id)}.png"
im.save(OUT / name)
names.append(f"{lamp_id}\t{name}")
(OUT / "labels.txt").write_bytes(("\n".join(names) + "\n").encode("utf-8"))
info = []
for lamp_id in TEXT_IDS:
font, size, box, origin = text_layout(lamp_id)
path = Path(getattr(font, "path", "builtin")).name
info.append(
f"{lamp_id}\tfont={path}\tsize={size}\tbbox={box[0]},{box[1]},{box[2]},{box[3]}\torigin={origin[0]},{origin[1]}"
)
(OUT / "text_info.txt").write_bytes(("\n".join(info) + "\n").encode("utf-8"))
print(f"{len(QR_IDS)} QR matrices, {len(names)} labels, {len(info)} text layouts -> {OUT}")
print("reference render_label checked" if reference else "reference render_label NOT checked")
for line in info:
print(line)
if __name__ == "__main__":
main()