#!/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