aboutsummaryrefslogtreecommitdiff
path: root/tools/barcode_ref.py
blob: 9c4c6d5ab0b0418e48afc8b523e786702a2a79f2 (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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#!/usr/bin/env python3
# Author:  cirakg & claude ai
# Summary: Shared barcode helpers for the reference tools: zxing-cpp module rows, rendering, decoding, check digits.
"""Helpers shared by gen_code_vectors.py and check_codes.py.

Symbology numbers match source/codes/Codes.mc. zxing-cpp is both the
reference encoder (module rows) and the decoder (README 12.4).
"""

from __future__ import annotations

from typing import Any, cast

import zxingcpp
from PIL import Image

QR, CODE128, CODE39, EAN13, EAN8, UPCA, UPCE, ITF, CODABAR, DATAMATRIX, PDF417, AZTEC = range(12)
FORMATS = {
    QR: zxingcpp.BarcodeFormat.QRCode,
    DATAMATRIX: zxingcpp.BarcodeFormat.DataMatrix,
    PDF417: zxingcpp.BarcodeFormat.PDF417,
    AZTEC: zxingcpp.BarcodeFormat.Aztec,
    CODE128: zxingcpp.BarcodeFormat.Code128,
    CODE39: zxingcpp.BarcodeFormat.Code39,
    EAN13: zxingcpp.BarcodeFormat.EAN13,
    EAN8: zxingcpp.BarcodeFormat.EAN8,
    UPCA: zxingcpp.BarcodeFormat.UPCA,
    UPCE: zxingcpp.BarcodeFormat.UPCE,
    ITF: zxingcpp.BarcodeFormat.ITF,
    CODABAR: zxingcpp.BarcodeFormat.Codabar,
}
CODE39_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%"
LINEAR_QUIET_MODULES = 10  # README 12.2


def reference_modules(symbology: int, text: str) -> str:
    """zxing-cpp's module row for text, without quiet zones, as '0'/'1'."""
    barcode = zxingcpp.create_barcode(text, FORMATS[symbology])
    image = barcode.to_image(scale=1, add_hrt=False, add_quiet_zones=False)
    # zxing-cpp images support the buffer protocol; its type stubs don't say so.
    row = bytes(memoryview(cast(Any, image)))[: image.shape[1]]
    return "".join("1" if v < 128 else "0" for v in row)


def render_linear(modules: str, scale: int, height: int = 40) -> Image.Image:
    """Black bars on white with the quiet zone, `scale` pixels per module."""
    quiet = LINEAR_QUIET_MODULES * scale
    image = Image.new("L", (len(modules) * scale + 2 * quiet, height), 255)
    for i, module in enumerate(modules):
        if module == "1":
            image.paste(0, (quiet + i * scale, 0, quiet + (i + 1) * scale, height))
    return image


# What to read each symbology as. Code 39 is read as standard Code 39: by
# default zxing-cpp also tries full-ASCII (extended) Code 39, which turns
# pairs like "+S" into "s" (the app doesn't encode extended Code 39).
READ_FORMATS = {**FORMATS, CODE39: zxingcpp.BarcodeFormat.Code39Std, UPCA: zxingcpp.BarcodeFormat.EAN13}


def render_matrix(packed: bytes, shape: tuple[int, int], scale: int, *, quiet: int, row_height: int = 1) -> Image.Image:
    """A matrix (row-major bits, 8 per byte; shape = width and stored rows,
    each row row_height modules high) as black modules on white with a
    quiet zone."""
    size, rows = shape
    image = Image.new("L", ((size + 2 * quiet) * scale, (rows * row_height + 2 * quiet) * scale), 255)
    for i in range(size * rows):
        if packed[i // 8] & (0x80 >> (i % 8)):
            x, y = i % size, i // size
            left, top = (quiet + x) * scale, (quiet + y * row_height) * scale
            image.paste(0, (left, top, left + scale, top + scale * row_height))
    return image


def decode(image: Image.Image, symbology: int | None = None) -> list[str]:
    """Texts zxing-cpp reads in image (only as `symbology` when given)."""
    if symbology is None:
        return [result.text for result in zxingcpp.read_barcodes(image)]
    # One format works as a format set at runtime; the stubs only allow the set type.
    formats = cast(zxingcpp.BarcodeFormats, READ_FORMATS[symbology])
    return [result.text for result in zxingcpp.read_barcodes(image, formats=formats)]


def gs1_check(digits: str) -> int:
    """GS1 mod-10 check digit of the data digits."""
    total = sum(int(d) * (3 if i % 2 == 0 else 1) for i, d in enumerate(reversed(digits)))
    return (10 - total % 10) % 10


def code39_check(text: str) -> str:
    return CODE39_ALPHABET[sum(CODE39_ALPHABET.index(c) for c in text) % 43]


def upce_expand(code: str) -> str:
    """UPC-E 'NS dddddd C' (8 digits) as the 13-digit EAN form zxing reads."""
    ns, d, check = code[0], code[1:7], code[7]
    last = d[5]
    if last in "012":
        body = d[0:2] + last + "0000" + d[2:5]
    elif last == "3":
        body = d[0:3] + "00000" + d[3:5]
    elif last == "4":
        body = d[0:4] + "00000" + d[4]
    else:
        body = d[0:5] + "0000" + last
    return "0" + ns + body + check


def expected_reading(symbology: int, readable: str) -> str:
    """What zxing-cpp reports when it decodes a code whose human text is `readable`."""
    if symbology == UPCA:
        return "0" + readable
    if symbology == UPCE:
        return upce_expand(readable)
    return readable