# smart-package-tracker
Generate package tracking IDs and render them as Code 128 barcodes, in PNG and SVG.
```rust
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
```text
TrackingId ──▶ Symbology::encode ──▶ Symbol ──▶ Renderer::render ──▶ bytes
(Code 128) (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.
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`.
## 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:
| 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:
```rust
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.
## Feature flags
| `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. |
| `png` | yes | PNG rendering. Implies `std`. |
| `svg` | yes | SVG rendering. No extra dependencies. |
| `serde` | no | `Serialize`/`Deserialize` for the public data types. |
## Dependencies
| [`code128`](https://crates.io/crates/code128) | ISO/IEC 15417 encoding and decoding with automatic character-set selection |
| [`png`](https://crates.io/crates/png) | PNG container. Used directly rather than via `image`, which would add a dozen codec dependencies to write a two-colour bitmap |
| [`getrandom`](https://crates.io/crates/getrandom) | OS cryptographic RNG for ID entropy. Optional (`os-rng`), so bare-metal targets can supply their own |
| [`thiserror`](https://crates.io/crates/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
QR codes, image scanning, shipment events, and carrier integrations. The
`Symbology` and `Renderer` traits are the extension points for the first two.
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, relevant when QR support
lands. Denso Wave has waived patent enforcement for ISO/IEC 18004-conformant
use.
* 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.
## Development
```sh
cargo test --all-features
cargo clippy --all-features -- -D warnings
cargo run --example label
```
## License
Licensed under either of [Apache License 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.