From 1251aab9f8f9f38aa5bdefd4fed1f58236166cf8 Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 11:24:58 +0200 Subject: test(codes): compare Data Matrix and PDF417 with references and decode them, also from screenshots Co-Authored-By: Claude Opus 5.5 --- tools/gen_code_vectors.py | 146 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) (limited to 'tools/gen_code_vectors.py') diff --git a/tools/gen_code_vectors.py b/tools/gen_code_vectors.py index a87b0c9..5ce2611 100644 --- a/tools/gen_code_vectors.py +++ b/tools/gen_code_vectors.py @@ -21,7 +21,10 @@ from pathlib import Path from typing import Any, cast import barcode_ref as ref +import ppf.datamatrix.codec_ascii # noqa: F401 (registers the datamatrix.ascii codec) import segno +from pdf417gen import encode as pdf417_encode +from ppf.datamatrix import DataMatrix CHECK_ADD, CHECK_FIX, CHECK_OFF = 0, 1, 2 TEXT_ON = 0x10 @@ -183,6 +186,126 @@ def qr_reference(payload: str, level: int, version: int, mask: int) -> tuple[int return int(qr.designator.split("-")[0]), qr.mask, packed.hex() +def dm_cases(rng: random.Random, count: int) -> list[str]: + """ASCII payloads for Data Matrix (ppf.datamatrix has no Base256 or + extended ASCII; those are covered by decoding). Lengths cover small, + multi-region and the largest (144 x 144, multi-block) sizes.""" + fixed = ["a", "123456", "Hello, World!", "0" * 44, "ABC-0042/fake member", "9" * 300, "x" * 500, "7" * 3116] + printable = string.ascii_letters + string.digits + " .,:;-/+()" + out = list(fixed) + for _ in range(count): + n = rng.choice([rng.randint(1, 40), rng.randint(40, 400)]) + out.append("".join(rng.choice(printable) for _ in range(n))) + return out + + +# Data codewords per square Data Matrix size (ISO/IEC 16022 table 7). +DM_CAPACITY = { + 10: 3, + 12: 5, + 14: 8, + 16: 12, + 18: 18, + 20: 22, + 22: 30, + 24: 36, + 26: 44, + 32: 62, + 36: 86, + 40: 114, + 44: 144, + 48: 174, + 52: 204, + 64: 280, + 72: 368, + 80: 456, + 88: 576, + 96: 696, + 104: 816, + 120: 1050, + 132: 1304, + 144: 1558, +} + + +def ppf_pad_differs(payload: str, size: int) -> bool: + """True when ppf.datamatrix would write a pad codeword of 0 instead of 254. + + ppf computes the 253-state pad as (R + 130) % 254, which turns 254 into 0; + ISO/IEC 16022 5.2.3 only subtracts 254 above 254. The watch follows the + standard, so such payloads make no exact reference. + """ + used = len(payload.encode("datamatrix.ascii")) + capacity = DM_CAPACITY[size] + return any((149 * pos) % 253 == 124 for pos in range(used + 2, capacity + 1)) + + +def dm_reference(payload: str) -> tuple[int, str]: + """ppf.datamatrix's (size, matrix as hex of row-major bits), ASCII only.""" + matrix = DataMatrix(payload, codecs=["ascii"]).matrix + bits = "".join(str(b) for row in matrix for b in row) + bits += "0" * (-len(bits) % 8) + packed = bytes(int(bits[i : i + 8], 2) for i in range(0, len(bits), 8)) + return len(matrix), packed.hex() + + +def pdf417_cases(rng: random.Random, count: int) -> list[tuple[str, int, int]]: + """(payload, columns, level): text in all submodes, digit runs below and + above 13, UTF-8 bytes, exact 6-byte groups, levels 0-6, 1-12 columns.""" + fixed = [ + ("Hello, World!", 3, 2), + ("abcABC123;:@#", 4, 1), + ("Member 0042 - fake card", 2, 2), + ("12345678901234567890123456789012345678901234567890", 3, 3), + ("Zürich ✓ fake data", 4, 2), + ("\x01\x02\x03\x04\x05\x06", 2, 0), + ("tab\there\nnew line\r end", 5, 4), + ("x" * 200, 8, 5), + ("0" * 13 + "abc" + "1" * 12, 3, 2), + ("https://example.com/member/fake?id=0042&level=gold", 6, 6), + ] + out = list(fixed) + printable = string.ascii_letters + string.digits + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + for _ in range(count): + n = rng.randint(1, 120) + payload = "".join(rng.choice(printable) if rng.random() < 0.8 else rng.choice("éßü€") for _ in range(n)) + out.append((payload, rng.randint(1, 12), rng.randint(0, 5))) + return out + + +def pdf417_reference(payload: str, columns: int, level: int) -> tuple[int, int, str] | None: + """pdf417gen's (rows, width, rows of modules as hex), or None when the + combination is outside PDF417's limits (fewer than 3 or more than 90 rows).""" + try: + codes = pdf417_encode(payload, columns=columns, security_level=level) + except ValueError: + return None + rows = ["".join(format(value, "b") for value in row) for row in codes] + bits = "".join(rows) + bits += "0" * (-len(bits) % 8) + packed = bytes(int(bits[i : i + 8], 2) for i in range(0, len(bits), 8)) + return len(rows), len(rows[0]), packed.hex() + + +def mc_string(text: str) -> str: + """A Monkey C string literal body for text (quotes, backslashes, controls escaped).""" + out = [] + for ch in text: + if ch in '\\"': + out.append("\\" + ch) + elif ch == "\n": + out.append("\\n") + elif ch == "\t": + out.append("\\t") + elif ch == "\r": + out.append("\\r") + elif ord(ch) < 32: + out.append(f"\\u{ord(ch):04x}") + else: + out.append(ch) + return "".join(out) + + def render(rows: list[tuple[int, str, int, str]], seed: int) -> str: lines = [] for symbology, payload, opts, text in rows: @@ -202,6 +325,23 @@ def render(rows: list[tuple[int, str, int, str]], seed: int) -> str: escaped = payload.replace("\\", "\\\\").replace('"', '\\"') qr_lines.append(f' ["{escaped}", {level}, {version}, {mask}, {ref_version}, {ref_mask}, "{hexbits}"]') qr_body = ",\n".join(qr_lines) + dm_lines = [] + for dm_case in dm_cases(random.Random(seed + 2), 20): + payload = dm_case + size, hexbits = dm_reference(payload) + while ppf_pad_differs(payload, size): + payload += payload[-1] + size, hexbits = dm_reference(payload) + dm_lines.append(f' ["{payload}", {size}, "{hexbits}"]') + dm_body = ",\n".join(dm_lines) + pdf_lines = [] + for payload, columns, level in pdf417_cases(random.Random(seed + 3), 20): + reference = pdf417_reference(payload, columns, level) + if reference is None or any(ord(ch) < 32 and ch not in "\t\n\r" for ch in payload): + continue + n_rows, width, hexbits = reference + pdf_lines.append(f' ["{mc_string(payload)}", {columns}, {level}, {n_rows}, {width}, "{hexbits}"]') + pdf_body = ",\n".join(pdf_lines) return ( "// Author: cirakg & claude ai\n" "// Summary: zxing-cpp reference module rows for the linear barcode encoders (generated; do not edit).\n" @@ -212,6 +352,12 @@ def render(rows: list[tuple[int, str, int, str]], seed: int) -> str: + "\n ];\n\n // [payload, level, version (0 auto), mask (-1 auto), segno version, segno mask,\n" + " // segno matrix as hex of row-major bits]\n const QR = [\n" + qr_body + + "\n ];\n\n // [payload, ppf.datamatrix size, its matrix as hex of row-major bits] (ASCII mode)\n" + + " const DATAMATRIX = [\n" + + dm_body + + "\n ];\n\n // [payload, columns, level, pdf417gen rows, width, one module row per PDF417 row as hex]\n" + + " const PDF417 = [\n" + + pdf_body + "\n ];\n}\n" ) -- cgit v1.2.3