aboutsummaryrefslogtreecommitdiff
path: root/tools/gen_qr_tables.py
blob: 64c516a249b10b97cbb3a282ca125de33e25b7e0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python3
# Author:  cirakg & claude ai
# Summary: Generate source/codes/QrTables.mc (QR error-correction block structure) from segno's published constants.
"""QR block structure for versions 1-40 and levels L, M, Q, H (ISO/IEC 18004
table 9), taken from segno so it isn't typed by hand. Each entry is
[ec codewords per block, blocks in group 1, data codewords per group-1
block, blocks in group 2, data codewords per group-2 block].

    gen_qr_tables.py [--out source/codes/QrTables.mc]
"""

from __future__ import annotations

import argparse
from pathlib import Path
from typing import Any, cast

import segno.consts as c

LEVELS = [c.ERROR_LEVEL_L, c.ERROR_LEVEL_M, c.ERROR_LEVEL_Q, c.ERROR_LEVEL_H]
# segno.consts.ECC: version -> level -> tuple of EC(num_blocks, num_total, num_data); untyped in segno.
ECC = cast(dict[int, dict[int, tuple[Any, ...]]], c.ECC)


def table() -> list[list[int]]:
    rows = []
    for version in range(1, 41):
        for level in LEVELS:
            groups = ECC[version][level]
            ec = groups[0].num_total - groups[0].num_data
            g1 = groups[0]
            g2 = groups[1] if len(groups) > 1 else None
            assert all(g.num_total - g.num_data == ec for g in groups)
            rows.append([ec, g1.num_blocks, g1.num_data, g2.num_blocks if g2 else 0, g2.num_data if g2 else 0])
    return rows


def alignment() -> list[list[int]]:
    """Alignment pattern centre coordinates per version (ISO/IEC 18004 annex E)."""
    # segno starts at version 2; version 1 has no alignment pattern.
    return [[]] + [list(c.ALIGNMENT_POS[v - 2]) for v in range(2, 41)]


def render(rows: list[list[int]]) -> str:
    lines = []
    for version in range(1, 41):
        cells = ", ".join(str(n) for level in range(4) for n in rows[(version - 1) * 4 + level])
        lines.append(f"        {cells}{'' if version == 40 else ','} // version {version}: L, M, Q, H")
    return (
        "// Author:  cirakg & claude ai\n"
        "// Summary: QR error-correction block structure per version and level (generated from segno; do not edit).\n"
        "//\n"
        "// Derived from Segno (BSD-3-Clause). See THIRD_PARTY_NOTICES.md and LICENSES/segno.txt.\n"
        "// Generated by tools/gen_qr_tables.py. For version v (1-40) and level e (0 L, 1 M, 2 Q, 3 H), the five\n"
        "// numbers at index ((v - 1) * 4 + e) * 5 are: EC codewords per block, group-1 blocks, data codewords\n"
        "// per group-1 block, group-2 blocks, data codewords per group-2 block (ISO/IEC 18004 table 9).\n\n"
        "import Toybox.Lang;\n\nmodule QrTables {\n    const BLOCKS = [\n"
        + "\n".join(lines)
        + "\n    ];\n\n    // Alignment pattern centres (row and column coordinates) per version, from version 1.\n"
        + "    const ALIGNMENT = [\n"
        + ",\n".join(f"        {pos}" for pos in alignment())
        + "\n    ];\n}\n"
    )


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("--out", type=Path, default=Path("source/codes/QrTables.mc"))
    args = parser.parse_args(argv)
    args.out.write_text(render(table()), encoding="utf-8")
    print(f"wrote the QR block table to {args.out}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())