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,213 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user