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.
This commit is contained in:
commit
2a3f28001c
241 files changed
+46390
No files matched your search
@@ -0,0 +1,161 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user