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
|
#!/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 = range(9)
FORMATS = {
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 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
|