Code 128 now chooses code sets to minimise the symbol count. Digit-heavy barcodes were being drawn up to 1.76x denser than callers sized them for.
The bug
Code Set C packs two digits per symbol; sets A and B take one character each. rubar always used Code B, so a digit run cost twice the symbols it needed to. Callers compute physical width from the symbol count, so bars came out proportionally finer than intended — silent, still decodable in software, and visible only on a scanner or under a caliper.
| payload | correct | drew before |
|---|---|---|
ABC123 |
101 | 101 |
009312345678901234 |
134 | 233 |
12345678901234567890 |
145 | 255 |
(01)20197344223371 |
134 | 222 |
(01)20197344223371(21)00000001 |
189 | 332 |
Changes
- Minimal code-set planning.
encode_code128runs a backward DP over the token stream to pick the start code set and latch points. Planning spans the whole symbol list, so a digit run split across severalDataentries still packs as one run. gs1::to_symbolsno longer hardcodesStartB. That pin defeated planning on exactly the digit-heavy payloads GS1-128 carries. Its output now begins with the FNC1 designator and the encoder picks the start set. Callers passing its output straight toencode_code128need no change.- Start symbols now pin the initial code set and plan onward, rather than forcing one set for the whole payload. Never produces a worse barcode, and it turns the old
StartC-plus-odd-digit-count error into a valid encoding. Datais validated as ASCII. Accented text already failed inside barcoders, so the real change is narrow: the seven in-band switch and function characters (Ć,Ź,À, ...) previously smuggled a code-set switch through aDatapayload and silently rendered a barcode that didn't contain the requested character. That is nowInvalidCharacter.
Compatibility
Source-compatible. total_modules keeps its meaning exactly — drawn modules, no quiet zone; only the value changes. Rust consumers on rubar-core = "0.2.0" need a real version edit, since pre-1.0 caret semantics won't resolve 0.3.0 on a cargo update.
Verification
Checked against an independent exact minimal encoder (sklib's min_symbol_count): 108 payloads assert total_modules == count * 11 + 35. Confirmed end-to-end through rupdf's PDF path, where every row of the density harness now reads drawn == counted at ratio 1.00.
One deliberate gap: the planner never emits SHIFT, because barcoders cannot express it — its parse() does not model shift state, so the character after a shift marker is looked up in the wrong set. SHIFT beats latch-there-and-back by exactly one symbol, and only for payloads alternating between control characters (ASCII < 32) and Code-B-only characters (ASCII >= 96). Verified zero divergence across 21,613 printable, GS1 and real-world payloads.
Wheels
Binary wheels for macOS arm64, Linux x86_64 and Linux aarch64 on Python 3.11, 3.12 and 3.13 — a wider matrix than 0.2.0, which shipped 3.11 + 3.13 on two platforms. Other platforms build from sdist.
Also published to crates.io as rubar-core 0.3.0.