#!/usr/bin/env python3 # Author: cirakg & claude ai # Summary: Generate test/data/CodeVectors.mc: zxing-cpp reference module rows for the linear barcode encoders. """Reference module rows for README 12.4 check 1 (all fake payloads). For every linear symbology: fixed edge cases plus seeded random payloads, each with the options the watch uses and zxing-cpp's module row. Where the watch adds something zxing doesn't (a Code 39 or ITF check character, Codabar start/stop characters), the reference is made from the completed text. gen_code_vectors.py [--seed 1212] [--count 25] [--out test/data/CodeVectors.mc] """ 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 ppf.datamatrix.codec_ascii # noqa: F401 (registers the datamatrix.ascii codec) import segno from aztec_code_generator import AztecCode from pdf417gen import encode as pdf417_encode from ppf.datamatrix import DataMatrix CHECK_ADD, CHECK_FIX, CHECK_OFF = 0, 1, 2 TEXT_ON = 0x10 def options(check: int) -> int: return TEXT_ON | (check << 2) | 1 def digits(rng: random.Random, n: int) -> str: return "".join(rng.choice(string.digits) for _ in range(n)) def valid_upce(rng: random.Random) -> str: """Six UPC-E digits that are a canonical zero suppression.""" while True: d = digits(rng, 6) last = d[5] if (last == "3" and d[2] in "012") or (last == "4" and d[3] == "0") or (last in "56789" and d[4] == "0"): continue return d def cases(rng: random.Random, count: int) -> list[tuple[int, str, int, str]]: """(symbology, payload, options, zxing text) tuples.""" out: list[tuple[int, str, int, str]] = [] fixed = [ (ref.EAN13, "590123412345", options(CHECK_ADD), "590123412345"), (ref.EAN13, "5901234123457", options(CHECK_ADD), "5901234123457"), (ref.EAN8, "1234567", options(CHECK_ADD), "1234567"), (ref.UPCA, "01234567890", options(CHECK_ADD), "01234567890"), (ref.UPCE, "0425261", options(CHECK_ADD), "0425261"), (ref.UPCE, "1234565", options(CHECK_ADD), "1234565"), (ref.CODE128, "1234", options(CHECK_ADD), "1234"), (ref.CODE128, "123456789", options(CHECK_ADD), "123456789"), (ref.CODE128, "AB12345678", options(CHECK_ADD), "AB12345678"), (ref.CODE128, "A1234B12345", options(CHECK_ADD), "A1234B12345"), (ref.CODE128, " ~", options(CHECK_ADD), " ~"), (ref.CODE39, "CODE 39", options(CHECK_OFF), "CODE 39"), (ref.CODE39, "CODE-39", options(CHECK_ADD), "CODE-39" + ref.code39_check("CODE-39")), (ref.ITF, "1234", options(CHECK_OFF), "1234"), (ref.ITF, "123", options(CHECK_ADD), "123" + str(ref.gs1_check("123"))), (ref.CODABAR, "A40156B", options(CHECK_ADD), "A40156B"), (ref.CODABAR, "31117013206375", options(CHECK_ADD), "A31117013206375A"), ] out.extend(fixed) printable = string.ascii_letters + string.digits + " !#$%&()*+,-./:;<=>?@[]^_{|}~" for _ in range(count): out.append((ref.EAN13, digits(rng, 12), options(CHECK_ADD), "")) out.append((ref.EAN8, digits(rng, 7), options(CHECK_ADD), "")) out.append((ref.UPCA, digits(rng, 11), options(CHECK_ADD), "")) out.append((ref.UPCE, rng.choice("01") + valid_upce(rng), options(CHECK_ADD), "")) n = rng.randint(1, 20) mixed = "".join(rng.choice(string.digits) if rng.random() < 0.5 else rng.choice(printable) for _ in range(n)) out.append((ref.CODE128, mixed, options(CHECK_ADD), mixed)) c39 = "".join(rng.choice(ref.CODE39_ALPHABET) for _ in range(rng.randint(1, 12))) with_check = rng.random() < 0.5 out.append( ( ref.CODE39, c39, options(CHECK_ADD if with_check else CHECK_OFF), c39 + ref.code39_check(c39) if with_check else c39, ) ) itf = digits(rng, 2 * rng.randint(1, 8)) out.append((ref.ITF, itf, options(CHECK_OFF), itf)) inner = "".join(rng.choice("0123456789-$:/.+") for _ in range(rng.randint(1, 12))) guard = rng.choice("ABCD") + inner + rng.choice("ABCD") out.append((ref.CODABAR, guard, options(CHECK_ADD), guard)) 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 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() AZTEC_EC = [10, 23, 36, 50] def aztec_cases(rng: random.Random, count: int) -> list[tuple[str, int]]: """(payload, EC option 0-3) with one character class each (capitals, small letters or digits, plus spaces): for these the reference's optimal mode sequence and the watch's encodation give the same bits, so the symbols must match exactly. Sizes reach full range with reference grids.""" fixed = [ ("A", 1), ("HELLO WORLD", 1), ("hello world", 0), ("0042 7731", 2), ("FAKE TICKET " * 10, 1), ("abc def " * 40, 3), ("1234567890" * 60, 1), ("Z" * 900, 0), ] out = list(fixed) for _ in range(count): alphabet = rng.choice([string.ascii_uppercase + " ", string.ascii_lowercase + " ", string.digits + " "]) n = rng.randint(1, 200) payload = "".join(rng.choice(alphabet) for _ in range(n)).strip() or "X" out.append((payload, rng.randrange(4))) return out def aztec_reference(payload: str, option: int) -> tuple[int, str]: """aztec_code_generator's (size, matrix as hex of row-major bits).""" code = AztecCode(payload, ec_percent=AZTEC_EC[option]) bits = "".join(str(b) for row in code.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 code.size, 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: modules = ref.reference_modules(symbology, text or payload) 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) 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) aztec_lines = [] for payload, option in aztec_cases(random.Random(seed + 4), 16): size, hexbits = aztec_reference(payload, option) aztec_lines.append(f' ["{payload}", {option}, {size}, "{hexbits}"]') aztec_body = ",\n".join(aztec_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 // [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 // [payload, EC option, aztec_code_generator size, its matrix as hex of row-major bits]\n" + " const AZTEC = [\n" + aztec_body + "\n ];\n}\n" ) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--seed", type=int, default=1212) parser.add_argument("--count", type=int, default=25) parser.add_argument("--out", type=Path, default=Path("test/data/CodeVectors.mc")) args = parser.parse_args(argv) rows = cases(random.Random(args.seed), args.count) args.out.write_text(render(rows, args.seed), encoding="utf-8") print(f"wrote {len(rows)} reference rows to {args.out}") return 0 if __name__ == "__main__": raise SystemExit(main())