From fa9c695256de1c7fe007ba5d6bb9b28b1deb5a5a Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 02:43:08 +0200 Subject: test(codes): compare with zxing-cpp reference rows and decode encoder output and screenshots Co-Authored-By: Claude Opus 5.5 --- tools/barcode_ref.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tools/barcode_ref.py (limited to 'tools/barcode_ref.py') diff --git a/tools/barcode_ref.py b/tools/barcode_ref.py new file mode 100644 index 0000000..2e58b5e --- /dev/null +++ b/tools/barcode_ref.py @@ -0,0 +1,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 -- cgit v1.2.3