Skip to content

feat: read barcodes back out of images - #2

Merged
ferris007 merged 1 commit into
mainfrom
feat/image-scanning
Aug 22, 2026
Merged

ferris007 merged 1 commit into
mainfrom
feat/image-scanning

Conversation

@ferris007

@ferris007 ferris007 commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

Adds a scan feature that runs the rendering pipeline backwards, meeting it at the same BitMatrix seam:

image ──▶ GrayImage ──▶ binarize ──▶ BitMatrix ──▶ Decoder::decode ──▶ payload
let found = scan::scan_png_file("label.png")?;
assert_eq!(found.payload(), "PKG-9ED9285C");

Design

Nothing in src/scan/ matches on SymbologyKind. The one thing a scan line needs to know about a symbology — how it groups bars and spaces into fixed-width characters — is new SymbologyKind::linear_character() metadata, sitting alongside required_quiet_zone() and is_linear(). A unit test asserts it agrees with is_linear() for every variant so the two cannot drift.

Two decisions worth the review time:

  • Runs are converted to modules one character at a time, not against a single module-width estimate for the whole symbol. A character is a fixed module count by definition, so its measured width is a local ruler and drift never accumulates past one character. survives_module_widths_that_do_not_divide_evenly is the test that fails without this.
  • The threshold is per block, smoothed across neighbours. follows_a_lighting_gradient builds an image where the darkest paper is darker than the lightest ink — no global threshold can separate it.

A scan never returns a guess: every payload has passed Code 128's own check character. A wrong answer routes a parcel to the wrong depot and looks correct doing it, which is worse than a miss.

Scope

Handled: any module width; extra margin or a crop flush to the bars; the symbol anywhere in a larger image; light-on-dark; uneven lighting; moderate speckle and blur; a label applied on its side, since columns are swept as well as rows.

Not handled, and documented as such: rotation off the axes, perspective, curved surfaces, and QR. This reads rendered labels, flatbed scans and screenshots — not camera frames.

Boundary change

AGENTS.md and the README both listed image scanning as deliberately out of scope. That boundary is rewritten rather than dropped: scanning is in scope as far as it stays arithmetic. Camera-grade decoding is a scanning engine and belongs in one. QR scanning stays out at a concrete cost — rqrr would add seven crates to the published graph, so it remains a dev-dependency verifying our own output.

Cost

No new dependencies — verified by diffing cargo tree with and without the feature. Builds no_std; added to the bare-metal CI job and the feature matrix. Roughly 0.6 ms on a real label, 165 ms worst case on 12 MP of pure noise.

Tests

tests/scan.rs (16 cases) starts from the crate's own renderer and degrades the image the way reality does: quarter-turn rotation, inverted colours, transparent background, 1-pixel modules, HRI text present, pasted into a larger page with a rule across it, salt-and-pepper plus box blur, no quiet zone, and a QR code as a clean miss. Noise comes from a fixed seed so the suite cannot pass on the desk and fail in CI.

cargo test --all-features, clippy -D warnings, fmt --check, doc, the no_std target, and the full feature matrix are all clean.

Adds a `scan` feature that runs the rendering pipeline backwards and meets
it at the same seam:

    image -> GrayImage -> binarize -> BitMatrix -> Decoder::decode -> payload

Nothing in src/scan/ matches on SymbologyKind. The one thing a scan line
needs to know about a symbology — how it groups bars and spaces into
fixed-width characters — is new SymbologyKind::linear_character metadata,
alongside required_quiet_zone and is_linear.

Pixel runs are converted to modules one character at a time rather than
against a single module-width estimate for the whole symbol, so a printed
width that drifts from nominal cannot accumulate into a misread. The
threshold is computed per block and smoothed across neighbours, which
follows a lighting gradient no global threshold can.

Every payload returned has passed Code 128's own check character: a smudge
is a miss, not a plausible wrong tracking number routed to the wrong depot.

Handled: any module width, extra margin or a crop flush to the bars, the
symbol anywhere in a larger image, light-on-dark, uneven lighting, moderate
speckle and blur, and a label applied on its side, since columns are swept
as well as rows. Not handled, and documented as such: rotation off the
axes, perspective, curved surfaces, and QR.

This reverses the "image scanning is out of scope" boundary in AGENTS.md
and the README. The boundary is rewritten rather than dropped: scanning is
in scope as far as it stays arithmetic. Camera-grade decoding is a scanning
engine and belongs in one. QR scanning stays out at a concrete cost — rqrr
would add seven crates to the published graph, so it remains a
dev-dependency verifying our own output.

No new dependencies; the resolved graph is unchanged. Builds no_std.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M0MN8P909V0PRD86646T6KS4
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ae918b23-cd34-48b5-87df-40033a322ff3


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ferris007
ferris007 merged commit 1f77e40 into main Aug 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant