summaryrefslogtreecommitdiff
path: root/tools/barcode_ref.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/barcode_ref.py')
-rw-r--r--tools/barcode_ref.py97
1 files changed, 97 insertions, 0 deletions
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