feat(core)!: zero-allocation no_std encoding (0.2.0)#3
Merged
Conversation
Redesign the whole crate around a caller-provided module buffer so the default build performs no heap allocation and does not even link `alloc` (any accidental allocation is now a compile error). - BarcodeEncoder now requires `encode_into(input, &mut [bool]) -> Encoded`; every symbology (linear, EAN/UPC, GS1, postal, 2D, QR) streams its modules into the caller buffer using fixed stack scratch and the new `common::buffer::SliceWriter`. - EncodeError is now allocation-free (`&'static str` / `char` payloads, plus `BufferTooSmall`); `common::svg` renders SVG into any `core::fmt::Write` sink without allocating. - `alloc` becomes an optional feature gating the owned `encode()` -> BarcodeOutput convenience and `to_svg_string()`; `std` implies `alloc`; `image` implies `std`. BREAKING CHANGE: `encode()` now requires the `alloc` feature and the trait associated `Error` type is removed in favour of `EncodeError`. Verified: builds/tests/clippy green with no features, `alloc`, and all features; Miri clean (no UB, no leaks) on the no-alloc and alloc paths.
Add clippy + build + test jobs for --no-default-features so CI proves the default configuration compiles and passes without linking alloc.
Describe the no-heap default, the feature matrix (alloc/std/image), and a stack-buffer encode_into example; bump versions to 0.2.
There was a problem hiding this comment.
Pull request overview
This PR refactors the barcodes crate to make encoding zero-allocation by default and fully no_std without linking alloc, introducing an encode_into() API that writes modules into a caller-provided &mut [bool] and returns an allocation-free Encoded shape descriptor. It also adds allocation-free SVG streaming via core::fmt::Write, and gates the owned encode() / to_svg_string() conveniences behind an alloc feature.
Changes:
- Replaces per-symbology
encode() -> BarcodeOutputimplementations withencode_into() -> Encoded, using fixed-capacity stack scratch +common::buffer::SliceWriter. - Introduces allocation-free
Encoded+EncodeErrorand adds streaming SVG writers incommon::svg. - Updates crate docs/README, feature flags (
alloc,stdimpliesalloc), and CI to validate--no-default-features(no heap).
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/twod/pdf417.rs | Converts PDF417 to encode_into() + stack scratch + slice writer output. |
| src/twod/datamatrix.rs | Converts Data Matrix to encode_into() + fixed scratch buffers and direct grid writes. |
| src/twod/aztec.rs | Converts Aztec to encode_into() with fixed-capacity grids/bits and no heap. |
| src/qrcode.rs | Ports BarcodeEncoder impl to encode_into() with stack scratch buffers and caller module buffer. |
| src/postal/rm4scc.rs | Converts RM4SCC to encode_into() with fixed input/state buffers and SliceWriter. |
| src/postal/imb.rs | Converts IMb to encode_into() with fixed digit buffer and SliceWriter. |
| src/linear/itf.rs | Converts ITF to encode_into() and eliminates padding allocation via virtual leading zero. |
| src/linear/code93.rs | Converts Code 93 to encode_into() with fixed value buffer and SliceWriter. |
| src/linear/code39.rs | Converts Code 39 to encode_into() with SliceWriter-based pattern expansion. |
| src/linear/code128.rs | Converts Code 128 to encode_into() with fixed symbol buffer and SliceWriter. |
| src/linear/codabar.rs | Converts Codabar to encode_into() with SliceWriter-based pattern expansion. |
| src/lib.rs | Makes crate #![no_std] by default and gates alloc/std extern crates behind features. |
| src/gs1/gs1_128.rs | Removes allocation in GS1-128 parsing/encoding by borrowing slices and fixed buffers. |
| src/gs1/databar.rs | Converts DataBar to encode_into() and replaces Vec-based expansion with SliceWriter. |
| src/ean_upc/upce.rs | Converts UPC-E to encode_into() with SliceWriter and alloc-free errors. |
| src/ean_upc/upca.rs | Converts UPC-A to encode_into() with SliceWriter and alloc-free errors. |
| src/ean_upc/ean8.rs | Converts EAN-8 to encode_into() with SliceWriter and alloc-free errors. |
| src/ean_upc/ean13.rs | Converts EAN-13 to encode_into() with SliceWriter and alloc-free errors. |
| src/common/types.rs | Introduces Encoded and gates owned output types behind alloc. |
| src/common/traits.rs | Replaces trait contract with encode_into() and adds alloc-gated default encode(). |
| src/common/svg.rs | Adds allocation-free SVG rendering that streams to any core::fmt::Write. |
| src/common/output.rs | Reworks owned BarcodeOutput::to_svg_string() as an alloc wrapper over streaming SVG. |
| src/common/mod.rs | Exposes new buffer/svg modules and gates output behind alloc. |
| src/common/errors.rs | Makes EncodeError allocation-free and adds BufferTooSmall + InvalidCharacter. |
| src/common/buffer.rs | Adds SliceWriter for bounds-checked writing into caller-provided module buffers. |
| README.md | Documents zero-allocation core, new feature matrix, and updates examples/version. |
| Cargo.toml | Bumps to 0.2.0 and introduces alloc feature; makes std imply alloc. |
| Cargo.lock | Updates package version to 0.2.0. |
| .github/workflows/ci.yml | Adds clippy/build/test jobs for --no-default-features to enforce “no heap”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+306
to
+308
| // Step 2: Determine rows and columns from the total codeword count. | ||
| let ec_count = 1usize << (ec_level + 1); | ||
| let total_data = data_codewords.len(); | ||
| let total_codewords = total_data + ec_count; | ||
| let total_codewords = data_len + ec_count; |
Comment on lines
+329
to
+333
| // Assemble: length descriptor + data padded to (capacity - ec_count). | ||
| let padded_data_len = (capacity - ec_count).max(data_len); | ||
| let mut n = 0; | ||
| all_codewords[n] = (total_codewords + 1) as u16; // length descriptor | ||
| n += 1; |
Comment on lines
+217
to
+222
| continue; | ||
| } | ||
| symbols.push(byte - 0x20); | ||
| push!(byte - 0x20); |
Comment on lines
+20
to
+25
| pub fn write_svg<W: Write>(encoded: Encoded, buf: &[bool], out: &mut W) -> fmt::Result { | ||
| match encoded { | ||
| Encoded::Linear { len, height } => write_linear(&buf[..len], height, out), | ||
| Encoded::Matrix { width, height } => write_matrix(&buf[..width * height], width, out), | ||
| } | ||
| } |
Comment on lines
+57
to
+61
| return Ok(BarcodeOutput::Linear(LinearBarcode { | ||
| bars: buf, | ||
| height, | ||
| text: None, | ||
| })); |
Port the standard ISO/IEC 16022 ECC 200 fixes to the zero-allocation core: standard symbol-character placement (utah + corner cases), correct finder/timing tracks, 253-state padding, and the multi-region sizes 32x32-48x48 (up to 174 data codewords) — all using fixed stack scratch, no heap. Verified with libdmtx (dmtxread): sizes 10x10-48x48 decode back to the exact input. Round-trip / ISO RS vector / 253-state padding tests run under both the no-alloc and alloc configurations.
Audit with real decoders (ZXing, zbar) found several encoders produced unscannable output due to corrupted lookup tables: - EAN L_CODE digits 6-9 had the wrong final module (0 instead of 1), breaking EAN-13, EAN-8 and UPC-E (UPC-A only used digits 0-5 so it happened to work). - UPC-E parity table was the exact complement of the ECC 200 standard. - The Code 39 pattern table was wrong for 34 of 43 characters. All corrected against the standards and verified end-to-end: EAN-13, EAN-8, UPC-A, UPC-E and Code 39 now decode back to the exact input with ZXingReader and zbarimg. These are pre-existing bugs (also present in 0.1.x).
Port the Code-B GS1-128 fix to the zero-allocation core: encode the whole message in Code Set B with a leading FNC1 instead of starting in Code C with Code-B AI values (which a reader misread as numeric pairs). Verified with ZXingReader. With this, feat/zero-alloc has all the scannability fixes (EAN/UPC/Code 39 via a7f4ad9, GS1-128 here).
Replace the non-conformant 'representative' encoder with a real implementation: byte compaction, Reed-Solomon over GF(929) using the standard EC coefficient tables, auto EC level, and the standard low-level codeword patterns (embedded from the ISO/IEC 15438 table). Each row is emitted at 3x height so square-module rendering scans. Verified with the ZXing decoder (ZXingReader): text, mixed and URL inputs decode back to the exact input. Codeword pattern table and EC coefficients sourced from ZXing (Apache-2.0).
…24724) Replace the 'simplified' encoder with a real RSS-14 implementation: the standard combinatorial element-width generation, group/checksum tables, and finder patterns, producing the 96-module DataBar Omnidirectional pattern. Verified with ZXing and zbar: 13/14-digit GTINs decode back to the correct (01) GTIN-14. Tables/algorithm ported from zint (BSD-3-Clause).
Replace the 'simplified' encoder with a real implementation: Binary Shift high-level encoding, Reed-Solomon over the Aztec Galois fields (GF(64/256/ 1024) + GF(16) for the mode message), bit stuffing, and the standard bull's-eye / mode-message / spiral layout with alignment grid. Compact (1-4 layers) and full-range (1-12 layers) symbols. Verified with ZXing (ZXingReader): text and URL inputs decode back to the exact input. Layout/RS ported from ZXing (Apache-2.0).
Rewrite the postal encoders to their real specifications, emitting a 3-row matrix (ascender / tracker / descender) instead of a placeholder linear pattern. - IMb (USPS-B-3200): u128 field accumulation, 11-bit CRC frame check, base-636/1365 codeword conversion and Appendix D character/bar tables. Verified bit-for-bit against the canonical DAFT reference vector. - RM4SCC: KRSET alphabet, per-character 4-bar states, top/bottom check-character derivation, start/stop bars. Tables ported from zint. All 147 tests pass; clippy clean on --no-default-features and --all-features; rustfmt clean.
The shared check_digit routine weighted digits from the left, which only yields the GS1-correct result when the data length is even (EAN-13's 12 digits). For UPC-A and UPC-E (11 data digits) the rightmost digit received weight 1 instead of 3, producing an invalid check digit — UPC-A symbols failed to scan (verified with ZXing and zbar). Weight from the right instead (rightmost data digit ×3), which is the length-independent GS1 rule and leaves EAN-13/EAN-8 output unchanged. Add a UPC-A regression test (03600029145 -> check 2).
Add a package `exclude` so `examples/` (a dev-only generation harness that needs the `image` feature) and `generated_barcodes/` (throwaway PNGs) are never shipped to crates.io, and gitignore the generated images.
Reproducible example that renders a PNG for every symbology (for manual decoder testing). Gated with required-features = ["image"] so it is skipped by default/no-default-features builds, and excluded from the published crate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Reworks the entire crate to be zero-allocation: the default build is now
pure
no_stdand does not linkallocat all, so any stray heapallocation becomes a compile error.
Key changes
BarcodeEncoder::encode_into(input, &mut [bool]) -> Encoded— all 14symbologies (linear, EAN/UPC, GS1, postal, PDF417, Data Matrix, Aztec, QR)
write their modules into a caller-provided buffer using fixed stack scratch.
common::buffer::SliceWriter— bounds-checked slice writer(
EncodeError::BufferTooSmall).EncodeError(&'static str/charpayloads, noString/format!).common::svg— renders SVG into anycore::fmt::Writesink, no heap.allocfeature gates the ownedencode() -> BarcodeOutputconvenience and
to_svg_string();stdimpliesalloc;imageimpliesstd.Breaking changes (→ 0.2.0)
encode()now requires theallocfeature.type Erroris removed; all encoders useEncodeError.pattern (constant row width) — this alters the PDF417 visual pattern, which
was already a "representative" (non-spec) encoding.
Verification
-D warnings--no-default-features(no heap)--features alloc--all-featurescargo fmt --all --check✅.#![forbid(unsafe_code)]retained in all 29 sourcefiles. CI gains build + test + clippy jobs for
--no-default-features.