diff --git a/barcode/codex.py b/barcode/codex.py index 9a60c31..50071c9 100755 --- a/barcode/codex.py +++ b/barcode/codex.py @@ -267,7 +267,14 @@ def _convert_or_buffer(self, char: str) -> int | None: raise RuntimeError(f"Character {char} could not be converted in charset C.") def _try_to_optimize(self, encoded: list[int]) -> list[int]: - if encoded[1] in code128.TO: + # A START_C followed by an immediate charset switch can be folded into a + # single START code. In charset C, however, the code number 99 is the + # data pair "99" (not the TO_C switch marker), so it must never be folded + # away -- doing so silently dropped a leading "99" from the barcode. + if encoded[0] == code128.START_CODES["C"] and encoded[1] in ( + code128.C["TO_A"], + code128.C["TO_B"], + ): encoded[:2] = [code128.TO[encoded[1]]] return encoded diff --git a/docs/changelog.rst b/docs/changelog.rst index 6309ee6..f47c5d4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog --------- +Unreleased +~~~~~~~~~~ +* Fixed a leading ``99`` digit pair being silently dropped from Code128 + barcodes: in charset C the value ``99`` collided with the charset-C start + marker and was mistaken for a redundant switch code. (#251) + v0.16.2 ~~~~~~~ * Add support for Python 3.13. diff --git a/tests/test_builds.py b/tests/test_builds.py index aac11eb..fb56c23 100755 --- a/tests/test_builds.py +++ b/tests/test_builds.py @@ -15,3 +15,38 @@ def test_ean8_builds_with_longer_bars() -> None: ean = get_barcode("ean8", "40267708", options={"guardbar": True}) bc = ean.build() assert ref == bc[0] + + +# START_C symbol as produced by ``_build`` before any charset switch. +_START_C = 105 + + +def _decode_code128_c(encoded: list[int]) -> str: + """Decode a pure charset-C Code128 symbol list back to its digit string.""" + assert encoded[0] == _START_C + return "".join(f"{n:02d}" for n in encoded[1:]) + + +def test_code128_leading_99_not_stripped() -> None: + # Regression test for #251: in charset C the pair "99" encodes to the code + # number 99, which collided with the TO_C switch marker. ``_try_to_optimize`` + # therefore folded it away, silently dropping the leading "99" from the + # barcode (e.g. "9912345678" was encoded as "12345678"). + bc = get_barcode("code128", "9912345678") + assert bc.encoded == [_START_C, 99, 12, 34, 56, 78] + assert _decode_code128_c(bc.encoded) == "9912345678" + + +def test_code128_other_leading_pairs_unaffected() -> None: + # Pairs whose code number does not collide with a switch code always worked + # and must keep working. + for code, first_pair in (("0012345678", 0), ("1212345678", 12)): + bc = get_barcode("code128", code) + assert bc.encoded[:2] == [_START_C, first_pair] + assert _decode_code128_c(bc.encoded) == code + + +def test_code128_start_charset_folding_preserved() -> None: + # The START-code folding optimisation (START_C + an immediate charset switch + # collapsed into a single START code) must still apply for genuine switches. + assert get_barcode("code128", "Wikipedia").encoded[0] == 104 # START_B