smart-package-tracker 0.3.0

Generate package tracking IDs and render them as Code 128 barcodes (PNG and SVG)
Documentation

smart-package-tracker

Generate package tracking IDs and render them as Code 128 barcodes, in PNG and SVG.

use smart_package_tracker::{Barcode, RenderOptions, TrackingId};

let id = TrackingId::generate()?;        // PKG-9ED9285C
let barcode = Barcode::code128(&id)?;

let options = RenderOptions::default();  // 300 dpi, 13 mil, 25 mm tall
barcode.to_png_file("label.png", &options)?;
barcode.to_svg_file("label.svg", &options)?;

Design

TrackingId ──▶ Symbology::encode ──▶ Symbol ──▶ Renderer::render ──▶ bytes
               (Code 128 | QR)       (BitMatrix)  (Png | Svg)

Encoding and rendering are independent, joined by a dimension-agnostic bit grid. A linear barcode is a one-row BitMatrix; a matrix symbology such as QR is a square one. Renderers consume the grid, not the symbology, so adding a format means implementing Symbology and touching no rendering code — QR support was added without a single change to the PNG or SVG renderers.

Both renderers share a single Layout calculation, so PNG and SVG describe identical geometry at identical physical size — an SVG preview matches what the PNG prints.

What it does carefully

  • Specification-conformant quiet zones. Code 128 requires 10 modules per side. Omitting them is the most common cause of barcodes that look fine and will not scan, so it is the default and must be opted out of.
  • Whole-pixel modules. Module widths snap to integer pixels. A bar that straddles a pixel boundary renders as a grey edge and costs you the contrast a scanner depends on.
  • Physical units. Lengths are millimetres, mils, or inches, converted against an explicit DPI. PNG output carries a pHYs chunk, so a 300 dpi label prints at 300 dpi.
  • Set C compression. Digit runs encode two per symbol, which roughly halves the width of numeric carrier tracking numbers.
  • Reproducible output. Identical inputs produce byte-identical files.
  • No panics on bad input. Every fallible operation returns Result, and out-of-range accessors return empty rather than panicking.

QR codes

The same identifier, as a QR code:

use smart_package_tracker::symbology::{Ecc, Qr, QrVersion};
use smart_package_tracker::{Barcode, RenderOptions};

let barcode = Barcode::qr("PKG-9ED9285C")?;
barcode.to_png_file("qr.png", &RenderOptions::default())?;

// Or configure error correction and pin the symbol size, so every label
// comes out the same dimensions regardless of payload length.
let barcode = Barcode::qr_with(
    Qr::new().ecc(Ecc::Quartile).version(QrVersion::Fixed(6)),
    "PKG-9ED9285C",
)?;

Error correction defaults to Ecc::Medium (~15% damage tolerance). Ecc::Quartile or Ecc::High are worth the extra size for codes that will be scuffed, curved around a parcel, or partly obscured. QR carries arbitrary bytes, so unlike Code 128 it can encode payloads outside Latin-1.

Only full QR (versions 1–40) is supported. Micro QR is excluded: it holds very little data, reader support is patchy, and it uses a 2-module quiet zone rather than 4.

Rendered QR output is verified in the test suite by decoding it with rqrr, an independent decoder derived from the quirc C library, so a symbol that encodes correctly but renders with a missing quiet zone or a transposed grid fails the build. rqrr is a dev-dependency and is not in the published dependency graph.

Tracking ID entropy

TrackingId::generate() defaults to 32 bits of randomness, which produces the familiar PKG-9ED9285C shape. Random IDs collide on the birthday bound:

Entropy Body 1% collision risk at 50% collision risk at
32 bits 8 hex chars ~9,000 IDs ~77,000 IDs
48 bits 12 hex chars ~2.4 million ~20 million
64 bits 16 hex chars ~610 million ~5.1 billion

Configure 64 bits for anything that will issue more than a few thousand IDs. Widening later is a data migration; choosing it now is one line:

use smart_package_tracker::{Checksum, IdGenerator};

let generator = IdGenerator::builder()
    .prefix("PKG")
    .entropy_bits(64)
    .checksum(Checksum::Iso7064Mod37_36)  // catches misreads and typos
    .build()?;

let id = generator.generate()?;
generator.validate(&id)?;

Randomness alone never guarantees uniqueness. Keep a unique constraint at the storage layer and retry on conflict.

When something goes wrong

Every fallible call returns Result<_, Error>. Error is #[non_exhaustive], so match with a _ arm. What each variant means in practice:

Variant Typical cause What to do
InvalidTrackingId A string from storage or user input is not PREFIX-BODY, or is not A-Z/0-9 The message names the first violation; IDs are uppercase-only by design
IdPolicyMismatch The ID is well-formed but was not minted by this IdGenerator — wrong prefix, wrong body length, or a failed check character Compare against the generator that issued it; a failed check character usually means a misread or typo
InvalidIdConfig IdGenerator::builder() was given a bad prefix or entropy width Prefix: 1–16 chars of A-Z0-9. Entropy: a multiple of 4, between 16 and 512
Entropy The OS randomness source is unavailable The crate never falls back to a weaker source. Use generate_from_entropy with your own bytes
InsufficientEntropy generate_from_entropy was handed too few bytes Supply at least generator.entropy_bytes()
Unencodable Code 128: the payload is outside Latin-1. QR: it does not fit the pinned version, or the version is not 1–40 Use QR for non-Latin-1 payloads; raise the version or lower the ECC level
EmptyPayload An empty string was passed to an encoder Barcodes cannot encode nothing
InvalidRenderOptions Non-positive DPI/width/height, or geometry over 20,000 px on an axis The message names the computed size; reduce module_width, height, or dpi
Render The PNG encoder failed Rare; the message carries the underlying error
Decode Barcode::decode() was called on a non-Code 128 symbol, or on an invalid pattern There is no QR decoder in this crate
Io A *_to_file helper could not write Standard std::io::Error; only present with the std feature

Two failure modes deserve naming because they produce a file that looks correct and does not scan:

  • Quiet zones. QuietZone::None and small QuietZone::Modules(n) values are honoured exactly as asked. QuietZone::Standard is the only setting that guarantees conformance.
  • Human-readable text. The embedded font covers 0-9, A-Z, space, and - . / * + $ % :. Anything else prints as a hollow box. Check with hri_supports(text) before it reaches a label, or disable HRI.

Feature flags

Feature Default Effect
std yes File helpers and std::error::Error. Without it the crate is no_std + alloc.
os-rng yes Seed IDs from the OS CSPRNG. Turn off on targets getrandom does not support, such as bare metal; IdGenerator::generate_from_entropy still works there.
code128 yes Code 128 encoding and decoding.
qr yes QR Code encoding. Implies std; the qrcode crate is not no_std.
png yes PNG rendering. Implies std.
svg yes SVG rendering. No extra dependencies.
serde no Serialize/Deserialize for the public data types.

Dependencies

Crate Why
code128 ISO/IEC 15417 encoding and decoding with automatic character-set selection
qrcode ISO/IEC 18004 QR encoding. Chosen over fast_qr and qrcodegen because it is dual MIT/Apache where both are MIT-only
png PNG container. Used directly rather than via image, which would add a dozen codec dependencies to write a two-colour bitmap
getrandom OS cryptographic RNG for ID entropy. Optional (os-rng), so bare-metal targets can supply their own
thiserror Error derive

All are MIT OR Apache-2.0, as is every one of the 16 crates in the resolved transitive graph. There is no copyleft anywhere in it; cargo deny check in CI enforces that.

The crate builds as no_std + alloc. ID generation, Code 128 encoding, and SVG rendering all work without std; cargo build --target thumbv7em-none-eabihf --no-default-features --features code128,svg is verified in CI.

Human-readable text is drawn with an embedded 5x7 bitmap font rather than a font library, which keeps output identical between PNG and SVG and independent of what fonts a print server has installed. It covers 0-9, A-Z, space, and - . / * + $ % :; other characters draw as a hollow box.

Not implemented yet

Image scanning, shipment events, and carrier integrations. The Symbology and Renderer traits are the extension points for new formats. Carrier integrations belong in separate crates so that network I/O, async runtimes, and vendor licence terms stay out of this dependency graph.

Licensing notes

This crate is MIT OR Apache-2.0.

  • Code 128 is unencumbered — the symbology dates to 1981 and any patents have long expired. ISO/IEC 15417 itself is a paywalled document; this implementation cites it but reproduces none of its text.
  • QR Code is a registered trademark of Denso Wave Incorporated. Denso Wave has waived patent enforcement for ISO/IEC 18004-conformant use. The trademark is acknowledged in the docs and kept out of the crate name.
  • Issuing GS1-conformant identifiers such as SSCCs requires a GS1 company prefix and membership. Carrier APIs require signed developer agreements. Both are reasons to keep carrier integrations in separate crates.

Minimum supported Rust version

1.85, set by getrandom 0.4 (edition 2024). The MSRV is tested in CI and treated as a breaking change.

If you need a lower floor, pinning getrandom = "0.3" brings it down to 1.73 (png then sets the floor). That means tracking a superseded major of the crate supplying the entropy for every tracking ID, which is why it is not the default.

Changelog

See CHANGELOG.md.

Development

cargo test --all-features
cargo clippy --all-features --all-targets -- -D warnings
cargo fmt --all --check
cargo run --example label          # writes label*.png and label*.svg

AGENTS.md is the orientation guide for contributors and coding agents: the repo map, the invariants a change must not break, where to add a new symbology or renderer, and the release procedure. CLAUDE.md points at it.

License

Licensed under either of Apache License 2.0 or MIT license at your option.