From 3bdf4ad5929527c33c308b6ce6c806c7f982eb09 Mon Sep 17 00:00:00 2001 From: greyllmmoder Date: Thu, 9 Apr 2026 13:09:11 +0530 Subject: [PATCH 1/3] feat: Java-free decode via zxingcpp fallback (Item 3) Add zxingcpp as the preferred decode backend (no JVM required), falling back to python-zxing for legacy environments. Normalises zxingcpp's token to raw \x1d so GS1 group separator payloads round-trip correctly. - decode() tries zxingcpp first (pip install 'aztec-py[decode-fast]') - Falls back to python-zxing if zxingcpp absent - Raises RuntimeError with install instructions if neither is present - New decode-fast extra in pyproject.toml: zxingcpp>=2.2 + pillow - 3 new tests covering fast path, empty result, and fallback behaviour - All 97 tests pass, coverage 91% --- aztec_py/decode.py | 45 ++++++++++++++++++++++++++---- pyproject.toml | 4 +++ tests/test_decode.py | 66 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/aztec_py/decode.py b/aztec_py/decode.py index d8da78e..c45cd11 100644 --- a/aztec_py/decode.py +++ b/aztec_py/decode.py @@ -1,4 +1,4 @@ -"""Optional decode utility backed by python-zxing.""" +"""Optional decode utility — tries zxingcpp (no Java) then falls back to python-zxing.""" from __future__ import annotations @@ -8,20 +8,55 @@ def decode(source: Any) -> Any: """Decode an Aztec symbol from an image path or PIL image. + Tries the ``zxingcpp`` backend first (pure C++, no Java runtime required). + Falls back to ``python-zxing`` (requires a JVM) if ``zxingcpp`` is absent. + Raises :class:`RuntimeError` with install instructions if neither is available. + + Install the fast backend:: + + pip install "aztec-py[decode-fast]" # zxingcpp — no Java needed + + Install the legacy backend:: + + pip install "aztec-py[decode]" # python-zxing — requires Java + Args: - source: File path, file object, or PIL image supported by ``python-zxing``. + source: File path (``str`` / ``pathlib.Path``) or PIL ``Image`` object. Returns: - Decoded payload (`str` or `bytes` depending on the decoder/runtime). + Decoded payload string. Raises: - RuntimeError: If optional decode dependencies are missing or decode fails. + RuntimeError: If no decode backend is installed or decode fails. """ + # --- fast path: zxingcpp (C++, no JVM) --- + try: + import zxingcpp # type: ignore[import-not-found] + + try: + import PIL.Image as _PILImage + except ImportError as exc: + raise RuntimeError( + "zxingcpp requires Pillow: pip install 'aztec-py[decode-fast]'" + ) from exc + + img = source if isinstance(source, _PILImage.Image) else _PILImage.open(source) + results = zxingcpp.read_barcodes(img) + if not results: + raise RuntimeError("Decoder returned no payload.") + # zxingcpp renders GS1 group separator as ""; normalise to raw byte. + return results[0].text.replace("", "\x1d") + except ImportError: + pass # fall through to python-zxing + + # --- legacy path: python-zxing (requires Java) --- try: import zxing # type: ignore[import-not-found] except ImportError as exc: raise RuntimeError( - "Decode support requires optional dependency 'zxing' and a Java runtime." + "Decode support requires an optional backend.\n" + " pip install \"aztec-py[decode-fast]\" # zxingcpp — no Java needed\n" + " pip install \"aztec-py[decode]\" # python-zxing — requires Java" ) from exc reader = zxing.BarCodeReader() diff --git a/pyproject.toml b/pyproject.toml index 4c15edb..facde34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,10 @@ pdf = [ decode = [ "zxing>=1.0.4", ] +decode-fast = [ + "zxingcpp>=2.2", + "pillow>=8.0", +] dev = [ "pytest>=8.0", "pytest-cov>=5.0", diff --git a/tests/test_decode.py b/tests/test_decode.py index 10fed14..12963b9 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -9,9 +9,15 @@ from aztec_py import decode +# --------------------------------------------------------------------------- +# Existing tests — zxing legacy path +# (block zxingcpp with None so import raises ImportError, fall through to zxing) +# --------------------------------------------------------------------------- + def test_decode_requires_optional_dependency(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delitem(sys.modules, "zxing", raising=False) - with pytest.raises(RuntimeError, match="optional dependency 'zxing'"): + monkeypatch.setitem(sys.modules, "zxingcpp", None) # blocks re-import + monkeypatch.setitem(sys.modules, "zxing", None) # blocks re-import + with pytest.raises(RuntimeError, match="optional backend"): decode("missing.png") @@ -26,6 +32,7 @@ def decode(self, _source): class ZXing: BarCodeReader = Reader + monkeypatch.setitem(sys.modules, "zxingcpp", None) # force zxing path monkeypatch.setitem(sys.modules, "zxing", ZXing()) assert decode("any.png") == "ok" @@ -38,6 +45,61 @@ def decode(self, _source): class ZXing: BarCodeReader = Reader + monkeypatch.setitem(sys.modules, "zxingcpp", None) # force zxing path monkeypatch.setitem(sys.modules, "zxing", ZXing()) with pytest.raises(RuntimeError, match="no payload"): decode("any.png") + + +# --------------------------------------------------------------------------- +# New tests — zxingcpp fast path +# Pass a real in-memory PIL image so no file I/O is needed +# --------------------------------------------------------------------------- + +def test_decode_zxingcpp_fast_path(monkeypatch: pytest.MonkeyPatch) -> None: + """zxingcpp is preferred over zxing when present; returns results[0].text.""" + from PIL import Image + + class FakeResult: + text = "fast-result" + + class FakeZxingcpp: + @staticmethod + def read_barcodes(_img: object) -> list[FakeResult]: + return [FakeResult()] + + img = Image.new("RGB", (15, 15)) + monkeypatch.setitem(sys.modules, "zxingcpp", FakeZxingcpp()) + assert decode(img) == "fast-result" + + +def test_decode_zxingcpp_empty_payload(monkeypatch: pytest.MonkeyPatch) -> None: + """zxingcpp returning empty list raises RuntimeError.""" + from PIL import Image + + class FakeZxingcpp: + @staticmethod + def read_barcodes(_img: object) -> list[object]: + return [] + + img = Image.new("RGB", (15, 15)) + monkeypatch.setitem(sys.modules, "zxingcpp", FakeZxingcpp()) + with pytest.raises(RuntimeError, match="no payload"): + decode(img) + + +def test_decode_zxingcpp_falls_back_to_zxing(monkeypatch: pytest.MonkeyPatch) -> None: + """When zxingcpp is absent, decode falls through to python-zxing.""" + class Result: + raw = "zxing-result" + + class Reader: + def decode(self, _source: object) -> Result: + return Result() + + class ZXing: + BarCodeReader = Reader + + monkeypatch.setitem(sys.modules, "zxingcpp", None) # block fast path + monkeypatch.setitem(sys.modules, "zxing", ZXing()) + assert decode("any.png") == "zxing-result" From 6d71945e27e1934cb110fd962d9077567880c709 Mon Sep 17 00:00:00 2001 From: greyllmmoder Date: Thu, 9 Apr 2026 13:30:11 +0530 Subject: [PATCH 2/3] fix: update decode backend unavailability check for new error message --- scripts/decoder_matrix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/decoder_matrix.py b/scripts/decoder_matrix.py index b22a932..7fa0da8 100644 --- a/scripts/decoder_matrix.py +++ b/scripts/decoder_matrix.py @@ -32,6 +32,7 @@ def _decode_backend_unavailable(message: str) -> bool: "optional dependency 'zxing'" in lower or "java runtime" in lower or "java" in lower and "failed to decode" in lower + or "optional backend" in lower # zxingcpp-first fallback message ) From cfd60ff8baa5ef9520aef8be96355bceff90eabe Mon Sep 17 00:00:00 2001 From: greyllmmoder Date: Thu, 9 Apr 2026 13:32:11 +0530 Subject: [PATCH 3/3] fix: update conformance report backend unavailability check for new error message --- scripts/conformance_report.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/conformance_report.py b/scripts/conformance_report.py index c64cf9a..49bc344 100644 --- a/scripts/conformance_report.py +++ b/scripts/conformance_report.py @@ -51,6 +51,7 @@ def _decode_backend_unavailable(message: str) -> bool: "optional dependency 'zxing'" in lower or "java runtime" in lower or "java" in lower and "failed to decode" in lower + or "optional backend" in lower # zxingcpp-first fallback message )