aboutsummaryrefslogtreecommitdiff
path: root/tools/gen_code_vectors.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/gen_code_vectors.py')
-rw-r--r--tools/gen_code_vectors.py110
1 files changed, 109 insertions, 1 deletions
diff --git a/tools/gen_code_vectors.py b/tools/gen_code_vectors.py
index 970ca06..a87b0c9 100644
--- a/tools/gen_code_vectors.py
+++ b/tools/gen_code_vectors.py
@@ -15,10 +15,13 @@ from __future__ import annotations
import argparse
import random
+import re
import string
from pathlib import Path
+from typing import Any, cast
import barcode_ref as ref
+import segno
CHECK_ADD, CHECK_FIX, CHECK_OFF = 0, 1, 2
TEXT_ON = 0x10
@@ -92,6 +95,94 @@ def cases(rng: random.Random, count: int) -> list[tuple[int, str, int, str]]:
return out
+QR_LEVELS = "LMQH"
+LEVEL_CONSTS = [
+ segno.consts.ERROR_LEVEL_L,
+ segno.consts.ERROR_LEVEL_M,
+ segno.consts.ERROR_LEVEL_Q,
+ segno.consts.ERROR_LEVEL_H,
+]
+
+
+def qr_cases(rng: random.Random, count: int) -> list[tuple[str, int, int, int]]:
+ """(payload, level 0-3, version 0 = auto, mask -1 = auto) for QR.
+
+ Single-mode payloads only (numeric, alphanumeric or bytes), so segno's
+ one-segment encoding and the watch's optimal segmentation agree: a digit
+ run inside other text would be a cheaper numeric segment on the watch.
+ Mixed content is covered by the decoding check instead.
+ """
+ fixed = [
+ ("01234567", 1, 1, 2),
+ ("HELLO WORLD", 2, 1, 6),
+ ("hello, world", 0, 2, 0),
+ ("Zürich ✓ fake", 1, 5, 3),
+ ("https://example.com/member/abc", 1, 0, -1),
+ ("314159265358979323846264338327950288419716939937510", 3, 0, -1),
+ ("THE QUICK BROWN FOX 0123456789 $%*+-./:", 2, 7, 4),
+ ("v" * 120, 1, 10, 5),
+ ("Z" * 300, 0, 0, -1),
+ ("9" * 700, 1, 0, -1),
+ ("byte data " * 40, 3, 0, -1),
+ ]
+ out = list(fixed)
+ for _ in range(count):
+ kind = rng.randrange(3)
+ n = rng.randint(1, 60)
+ if kind == 0:
+ payload = digits(rng, n)
+ elif kind == 1:
+ payload = "".join(rng.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 $%*+-./:") for _ in range(n))
+ if payload.isdigit():
+ payload += "A"
+ else:
+ payload = "".join(rng.choice(string.ascii_lowercase + "!?,;") for _ in range(n))
+ if not payload.isdigit() and re.search(r"\d{4,}", payload):
+ continue
+ out.append((payload, rng.randrange(4), 0, -1))
+ return out
+
+
+def single_mode_bits(payload: str, version: int) -> int:
+ """Bits of payload as one numeric, alphanumeric or byte segment."""
+ c = 0 if version <= 9 else (1 if version <= 26 else 2)
+ if payload.isdigit():
+ n = len(payload)
+ return 4 + (10, 12, 14)[c] + 10 * (n // 3) + (0, 4, 7)[n % 3]
+ if all(ch in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" for ch in payload):
+ n = len(payload)
+ return 4 + (9, 11, 13)[c] + 11 * (n // 2) + 6 * (n % 2)
+ return 4 + (8, 16, 16)[c] + 8 * len(payload.encode())
+
+
+def segno_pads_extra_byte(payload: str, level: int, version: int) -> bool:
+ """True when segno would insert an extra zero byte: after the terminator
+ its write_padding_bits adds 8 - (length % 8) bits even when the stream
+ already ends on a codeword boundary (ISO/IEC 18004 7.4.10 adds none).
+ The watch follows the standard, so such payloads make no exact reference."""
+ groups = cast(dict[int, dict[int, tuple[Any, ...]]], segno.consts.ECC)[version][LEVEL_CONSTS[level]]
+ capacity = 8 * sum(g.num_blocks * g.num_data for g in groups)
+ bits = single_mode_bits(payload, version)
+ terminated = bits + min(4, capacity - bits)
+ return bool(terminated < capacity and terminated % 8 == 0)
+
+
+def qr_reference(payload: str, level: int, version: int, mask: int) -> tuple[int, int, str]:
+ """segno's (version, mask, matrix as hex of row-major bits) for the case."""
+ qr = segno.make_qr(
+ payload,
+ error=QR_LEVELS[level],
+ version=version or None,
+ mask=None if mask < 0 else mask,
+ boost_error=False,
+ encoding="utf-8",
+ )
+ bits = "".join(str(b) for row in qr.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 int(qr.designator.split("-")[0]), qr.mask, packed.hex()
+
+
def render(rows: list[tuple[int, str, int, str]], seed: int) -> str:
lines = []
for symbology, payload, opts, text in rows:
@@ -99,12 +190,29 @@ def render(rows: list[tuple[int, str, int, str]], seed: int) -> str:
escaped = payload.replace("\\", "\\\\").replace('"', '\\"')
lines.append(f' [{symbology}, "{escaped}", {opts}, "{modules}"]')
body = ",\n".join(lines)
+ qr_lines = []
+ for case in qr_cases(random.Random(seed + 1), 20):
+ payload, level, version, mask = case
+ ref_version, ref_mask, hexbits = qr_reference(payload, level, version, mask)
+ while segno_pads_extra_byte(payload, level, ref_version):
+ # Lengthen by one character of the same kind until segno's padding
+ # quirk doesn't apply.
+ payload += payload[-1]
+ ref_version, ref_mask, hexbits = qr_reference(payload, level, version, mask)
+ escaped = payload.replace("\\", "\\\\").replace('"', '\\"')
+ qr_lines.append(f' ["{escaped}", {level}, {version}, {mask}, {ref_version}, {ref_mask}, "{hexbits}"]')
+ qr_body = ",\n".join(qr_lines)
return (
"// Author: cirakg & claude ai\n"
"// Summary: zxing-cpp reference module rows for the linear barcode encoders (generated; do not edit).\n"
f"//\n// Generated by tools/gen_code_vectors.py, seed {seed}. Rows: [symbology, payload, options,\n"
"// expected modules without quiet zones]. Fake payloads only.\n\n"
- "import Toybox.Lang;\n\n(:test)\nmodule CodeVectors {\n const LINEAR = [\n" + body + "\n ];\n}\n"
+ "import Toybox.Lang;\n\n(:test)\nmodule CodeVectors {\n const LINEAR = [\n"
+ + body
+ + "\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"
)