smart-package-tracker 0.4.0

Generate package tracking IDs, render them as Code 128 barcodes (PNG and SVG), and read them back out of images
Documentation
//! QR round-trip tests.
//!
//! These decode our own rendered output with `rqrr`, an independent decoder
//! derived from the `quirc` C library. That makes them a genuine end-to-end
//! check: a symbol that encodes correctly but renders with a missing quiet
//! zone, a wrong module size, or a transposed grid will fail here even though
//! every structural assertion still passes.

#![cfg(all(feature = "qr", feature = "png"))]

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

/// Render to PNG, then read it back as greyscale for the decoder.
fn render_greyscale(barcode: &Barcode, options: &RenderOptions) -> (usize, usize, Vec<u8>) {
    let png = barcode.to_png(options).expect("png renders");

    let mut reader = png::Decoder::new(std::io::Cursor::new(&png[..]))
        .read_info()
        .expect("our own png is valid");
    let mut buf = vec![0; reader.output_buffer_size().unwrap()];
    let info = reader.next_frame(&mut buf).expect("valid frame");

    // Rendered with greyscale colours, so the red channel is the luminance.
    let luma = buf[..info.buffer_size()]
        .chunks_exact(4)
        .map(|px| px[0])
        .collect();

    (info.width as usize, info.height as usize, luma)
}

fn decode(barcode: &Barcode, options: &RenderOptions) -> String {
    let (w, h, luma) = render_greyscale(barcode, options);
    let mut img = rqrr::PreparedImage::prepare_from_greyscale(w, h, |x, y| luma[y * w + x]);

    let grids = img.detect_grids();
    assert_eq!(
        grids.len(),
        1,
        "expected exactly one QR grid, found {}",
        grids.len()
    );

    let (_meta, content) = grids[0].decode().expect("grid decodes");
    content
}

/// Big enough modules that the decoder is testing our geometry, not fighting
/// its own resampling.
fn options() -> RenderOptions {
    RenderOptions::builder()
        .module_width(Length::Px(8.0))
        .human_readable(false)
        .build()
        .unwrap()
}

#[test]
fn rendered_qr_is_readable_by_an_independent_decoder() {
    let barcode = Barcode::qr("PKG-9ED9285C").unwrap();
    assert_eq!(decode(&barcode, &options()), "PKG-9ED9285C");
}

#[test]
fn survives_every_error_correction_level() {
    for ecc in [Ecc::Low, Ecc::Medium, Ecc::Quartile, Ecc::High] {
        let barcode = Barcode::qr_with(Qr::new().ecc(ecc), "PKG-9ED9285C").unwrap();
        assert_eq!(decode(&barcode, &options()), "PKG-9ED9285C", "ecc {ecc:?}");
    }
}

#[test]
fn survives_a_pinned_version() {
    let barcode = Barcode::qr_with(Qr::new().version(QrVersion::Fixed(6)), "PKG-9ED9285C").unwrap();
    assert_eq!(barcode.symbol().modules().width(), 41);
    assert_eq!(decode(&barcode, &options()), "PKG-9ED9285C");
}

#[test]
fn round_trips_realistic_payloads() {
    for payload in [
        "PKG-9ED9285C",
        "PKG-8520D28455EB13A9D",
        "https://track.example.com/PKG-9ED9285C",
        "1Z999AA10123456784",
    ] {
        let barcode = Barcode::qr(payload).unwrap();
        assert_eq!(
            decode(&barcode, &options()),
            payload,
            "failed for {payload}"
        );
    }
}

#[test]
fn round_trips_non_latin1_payloads() {
    // QR encodes arbitrary bytes, so it carries payloads Code 128 cannot.
    let payload = "PKG-\u{4e2d}\u{6587}-\u{1f4e6}";
    let barcode = Barcode::qr(payload).unwrap();
    assert_eq!(decode(&barcode, &options()), payload);
}

#[test]
fn human_readable_text_does_not_break_detection() {
    // A real label carries the payload as text under the symbol; confirm that
    // does not confuse a scanner.
    let barcode = Barcode::qr("PKG-9ED9285C").unwrap();
    let with_text = RenderOptions::builder()
        .module_width(Length::Px(8.0))
        .human_readable(true)
        .build()
        .unwrap();
    assert_eq!(decode(&barcode, &with_text), "PKG-9ED9285C");
}

#[test]
fn the_quiet_zone_is_actually_blank_on_all_four_sides() {
    // Unlike a linear barcode, QR needs clear space on every side. Assert the
    // rendered pixels, rather than asserting that some decoder fails without
    // them: a decoder handed a perfectly cropped image is far more forgiving
    // than a scanner pointed at a parcel, so its leniency proves nothing.
    let barcode = Barcode::qr("PKG-9ED9285C").unwrap();
    let options = options();
    let layout = options.layout(barcode.symbol()).unwrap();
    let (w, h, luma) = render_greyscale(&barcode, &options);

    let quiet = layout.quiet_x_px as usize;
    assert!(quiet > 0);

    for y in 0..h {
        for x in 0..w {
            let in_quiet_zone = x < quiet || y < quiet || x >= w - quiet || y >= h - quiet;
            if in_quiet_zone {
                assert_eq!(
                    luma[y * w + x],
                    255,
                    "quiet zone pixel at ({x}, {y}) is not blank"
                );
            }
        }
    }
}

#[test]
fn qr_layout_is_square_and_quiet_zoned() {
    let barcode = Barcode::qr("PKG-9ED9285C").unwrap();
    let options = options();
    let layout = options.layout(barcode.symbol()).unwrap();

    assert_eq!(layout.symbol_w_px, layout.symbol_h_px, "QR must be square");
    // Unlike a linear barcode, QR needs a quiet zone on all four sides.
    assert_eq!(layout.quiet_y_px, layout.quiet_x_px);
    assert_eq!(layout.quiet_x_px, 4 * layout.module_px);
    assert_eq!(layout.width_px, layout.symbol_w_px + 2 * layout.quiet_x_px);
    assert_eq!(layout.height_px, layout.symbol_h_px + 2 * layout.quiet_y_px);
}

#[test]
#[cfg(feature = "svg")]
fn svg_and_png_agree_for_matrix_symbols() {
    let barcode = Barcode::qr("PKG-9ED9285C").unwrap();
    let options = options();
    let layout = options.layout(barcode.symbol()).unwrap();

    let svg = barcode.to_svg(&options).unwrap();
    assert!(svg.contains(&format!(
        "viewBox=\"0 0 {} {}\"",
        layout.width_px, layout.height_px
    )));
    // Each module row becomes rects of exactly one module's height.
    assert!(svg.contains(&format!("height=\"{}\"/>", layout.module_px)));
}

#[test]
fn long_payload_text_widens_the_image_instead_of_overflowing() {
    // A compact QR with a long payload produces HRI text wider than the symbol
    // even at the minimum font scale. The image must grow to fit it rather
    // than clip the glyphs at its edge.
    let payload = "PKG-8520D28455EB13A9D-0123456789";
    let barcode = Barcode::qr(payload).unwrap();
    let options = RenderOptions::builder()
        .module_width(Length::Px(1.0))
        .human_readable(true)
        .build()
        .unwrap();

    let layout = options.layout(barcode.symbol()).unwrap();
    let text_w = (payload.chars().count() as u32 * 6 - 1) * layout.hri_scale;

    assert!(
        text_w > layout.symbol_w_px,
        "test needs text wider than the symbol"
    );
    assert!(
        layout.hri_x_px + text_w <= layout.width_px,
        "HRI text runs past the right edge: x={} + w={} > {}",
        layout.hri_x_px,
        text_w,
        layout.width_px
    );
    // And the symbol stays centred within the widened image.
    assert_eq!(
        layout.symbol_x_px + layout.symbol_w_px + layout.symbol_x_px,
        layout.width_px
    );
}

#[test]
fn human_readable_text_stays_out_of_the_quiet_zone() {
    // ISO/IEC 18004 wants four clear modules on every side of the symbol. The
    // human-readable line is drawn below the symbol, and it must clear that
    // band rather than eat into it: the test above proves a cropped image
    // decodes, which says nothing about a scanner finding the symbol on a
    // parcel. Assert on the pixels around the symbol, with HRI enabled.
    let barcode = Barcode::qr("PKG-9ED9285C").unwrap();
    let options = RenderOptions::builder()
        .module_width(Length::Px(8.0))
        .human_readable(true)
        .build()
        .unwrap();
    let layout = options.layout(barcode.symbol()).unwrap();
    let (w, h, luma) = render_greyscale(&barcode, &options);

    let quiet = layout.quiet_x_px;
    assert_eq!(quiet, 4 * layout.module_px);

    // The band around the symbol box, out to the required quiet zone.
    let left = layout.symbol_x_px - quiet;
    let top = layout.symbol_y_px - quiet;
    let right = layout.symbol_x_px + layout.symbol_w_px + quiet;
    let bottom = layout.symbol_y_px + layout.symbol_h_px + quiet;
    assert!(
        right as usize <= w && bottom as usize <= h,
        "quiet zone is clipped by the image"
    );

    for y in top..bottom {
        for x in left..right {
            let inside_symbol = x >= layout.symbol_x_px
                && x < layout.symbol_x_px + layout.symbol_w_px
                && y >= layout.symbol_y_px
                && y < layout.symbol_y_px + layout.symbol_h_px;
            if !inside_symbol {
                assert_eq!(
                    luma[(y as usize) * w + (x as usize)],
                    255,
                    "quiet zone pixel at ({x}, {y}) is not blank"
                );
            }
        }
    }

    // And the text is still there, below the quiet zone.
    let text_rows = (bottom as usize..h).any(|y| (0..w).any(|x| luma[y * w + x] < 128));
    assert!(text_rows, "no human-readable text was drawn");
}