"""Geometry and layout validation for the PT-E560BT 63x20 mm label.""" import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from reportlab.lib.units import mm from pypdf import PdfReader from ptouch_label import ( Box, LabelConfig, PtouchLabelRenderer, compute_geometry, code_density_warning, ) # Label content size, and the driver's own media size. TARGET_W = 63.0 TARGET_H = 20.0 DRIVER_MEDIA = (65.0, 24.0) def main() -> int: cfg = LabelConfig() cfg.static_lines = ["MOTO ADV SRL", "Art: CABLU X", "Ver: 2026"] cfg.qr_prefix = "INV-" renderer = PtouchLabelRenderer(cfg) out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_ptouch.pdf") path = renderer.render_pdf("INV-00123", out) # With match_driver_media the page is the driver's media size and the label # content sits centred inside it. Without it the page equals the content. # NB: page_size_mm is the PAGE; page_width_mm/page_height_mm are the CONTENT. content_w, content_h = cfg.page_width_mm, cfg.page_height_mm ox, oy = cfg.origin_mm expected_page = DRIVER_MEDIA if cfg.match_driver_media else (TARGET_W, TARGET_H) page = PdfReader(path).pages[0] w_mm = float(page.mediabox.width) / mm h_mm = float(page.mediabox.height) / mm print(f"PDF page : {w_mm:.3f} x {h_mm:.3f} mm (expected {expected_page[0]} x {expected_page[1]})") ok_size = ( abs(w_mm - expected_page[0]) < 0.01 and abs(h_mm - expected_page[1]) < 0.01 ) print(f"page size : {'PASS' if ok_size else 'FAIL'}") # The label content itself must still measure 63 x 20 mm. content_ok = abs(content_w - TARGET_W) < 0.01 and abs(content_h - TARGET_H) < 0.01 print(f"content : {content_w:.2f} x {content_h:.2f} mm at ({ox:.2f}, {oy:.2f})" f" {'PASS' if content_ok else 'FAIL'}") # Content must fit wholly inside the page when offset. fits = (ox >= -0.01 and oy >= -0.01 and ox + content_w <= w_mm + 0.01 and oy + content_h <= h_mm + 0.01) print(f"content fits: {'PASS' if fits else 'FAIL'}") ok_size = ok_size and content_ok and fits geo = compute_geometry(cfg) print("\nBoxes (mm, origin bottom-left):") for key in ("printable", "logo", "text", "code"): b = geo[key] print(f" {key:10s} x={b.x:6.2f} y={b.y:6.2f} w={b.w:6.2f} h={b.h:6.2f}") print(f"\ncol_code_mm : {cfg.col_code_mm:.2f}") print(f"payload : {cfg.build_code_payload('INV-00123')!r}") print("\nText lines (printable band = %.2f mm):" % cfg.printable_height_mm) for ln in geo["lines"]: print( f" line {ln['index']} baseline={ln['baseline_mm']:6.2f} " f"font={ln['font_pt']:4.1f} variable={ln['is_variable']}" ) # Every line must stay inside the printable band band_top = cfg.gutter_mm + cfg.printable_height_mm band_bottom = cfg.gutter_mm inside = all( band_bottom - 0.01 <= ln["box"].y and ln["box"].y + ln["box"].h <= band_top + 0.01 for ln in geo["lines"] ) print(f"lines in band: {'PASS' if inside else 'FAIL'}") # Columns must tile the full width without overlap tiled = abs((cfg.col_logo_mm + cfg.col_text_mm + cfg.col_code_mm) - cfg.page_width_mm) < 1e-9 print(f"columns tile : {'PASS' if tiled else 'FAIL'}") print("\ndensity :", code_density_warning(cfg, "INV-00123") or "OK (scannable)") preview = renderer.render_preview("INV-00123", dpi=300) print(f"preview px : {preview.size} @300dpi") preview.save(os.path.join(os.path.dirname(out), "test_ptouch_preview.png")) ok_code = verify_qr_roundtrip(cfg, "INV-00123") ok_ink = verify_column_ink(preview, cfg) return 0 if (ok_size and inside and tiled and ok_code and ok_ink) else 1 def verify_qr_roundtrip(cfg, inventory: str) -> bool: """Re-read the QR out of the rendered preview and compare to the source. Downsampling the rendered image back to one pixel per module proves the symbol survived the dot-aligned scaling and is geometrically intact. """ import qrcode payload = cfg.build_code_payload(inventory) qr = qrcode.QRCode(border=cfg.qr_border_modules, box_size=1) qr.add_data(payload) qr.make(fit=True) expected = qr.get_matrix() renderer = PtouchLabelRenderer(cfg) bitmap = renderer.code_bitmap(inventory) # Inspect the rendered preview at preview DPI and sample each module centre. preview_dpi = 300 img = renderer.render_preview(inventory, dpi=preview_dpi) gray = img.convert("L") px_per_mm = preview_dpi / 25.4 box = renderer._code_box(cfg, bitmap) module_mm = box.w / bitmap.modules ox, oy = cfg.origin_mm page_h = cfg.page_size_mm[1] mismatches = 0 for row in range(bitmap.modules): for col in range(bitmap.modules): cx = int((ox + box.x + (col + 0.5) * module_mm) * px_per_mm) # Layout y is measured from the bottom; preview y grows downward. mm_y = oy + box.y + (bitmap.modules - row - 0.5) * module_mm cy = int((page_h - mm_y) * px_per_mm) if cx >= gray.width or cy >= gray.height or cx < 0 or cy < 0: mismatches += 1 continue dark = gray.getpixel((cx, cy)) < 128 if dark != bool(expected[row][col]): mismatches += 1 total = bitmap.modules * bitmap.modules ok = mismatches == 0 print( f"qr roundtrip: {'PASS' if ok else 'FAIL'} " f"({total - mismatches}/{total} modules match, {bitmap.modules} modules, " f"{bitmap.dots_per_module:.0f} dots/module)" ) return ok def verify_column_ink(preview, cfg) -> bool: """Confirm each column actually received content, and none is empty.""" dpi = 300 px_per_mm = dpi / 25.4 gray = preview.convert("L") ox, oy = cfg.origin_mm def has_ink(x0, x1) -> bool: # Coordinates are relative to the label content, so shift by the origin. depth = cfg.page_size_mm[1] region = gray.crop( ( int(ox + x0 * px_per_mm), int((depth - oy - cfg.page_height_mm + cfg.gutter_mm) * px_per_mm), int(ox + x1 * px_per_mm), int((depth - oy - cfg.gutter_mm) * px_per_mm), ) ) # Any pixel meaningfully darker than paper return region.getextrema()[0] < 200 text_ink = has_ink(cfg.col_logo_mm, cfg.col_logo_mm + cfg.col_text_mm) code_ink = has_ink(cfg.col_logo_mm + cfg.col_text_mm, cfg.page_width_mm) print(f"ink in text col: {'PASS' if text_ink else 'FAIL'}") print(f"ink in code col: {'PASS' if code_ink else 'FAIL'}") return text_ink and code_ink if __name__ == "__main__": raise SystemExit(main())