aboutsummaryrefslogtreecommitdiff
path: root/tools/gen_code_vectors.py
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 11:24:58 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 11:24:58 +0200
commit1251aab9f8f9f38aa5bdefd4fed1f58236166cf8 (patch)
treed9824a2430cff9f6659a5b14b6e437d6b1568b87 /tools/gen_code_vectors.py
parentd94eb32c80d2dbf00a45d7e69804f8247240a769 (diff)
test(codes): compare Data Matrix and PDF417 with references and decode them, also from screenshots
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/gen_code_vectors.py')
-rw-r--r--tools/gen_code_vectors.py146
1 files changed, 146 insertions, 0 deletions
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"
)