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
//! Human-readable interpretation (HRI): the text printed beneath a barcode.
//!
//! Both renderers draw HRI with the same embedded 5x7 bitmap font rather than
//! with system fonts or SVG `<text>`. That is a deliberate trade:
//!
//! * PNG and SVG output are geometrically identical, so a label previewed as
//!   SVG prints as the same pixels.
//! * Output is byte-for-byte reproducible, which makes golden-file tests
//!   meaningful.
//! * Nothing depends on which fonts a print server happens to have installed —
//!   a common cause of labels that render correctly in development and wrongly
//!   in production.
//!
//! The cost is a limited character set: `0-9`, `A-Z`, space, and
//! `- . / * + $ % :`. Anything else draws as a hollow box, the same
//! "missing glyph" convention text renderers use. Lowercase is not covered;
//! shipping HRI is uppercase in practice. A `<text>`-based HRI mode is a
//! candidate for a later release.

/// Glyph width in font pixels.
pub(crate) const GLYPH_W: u32 = 5;
/// Glyph height in font pixels.
pub(crate) const GLYPH_H: u32 = 7;
/// Horizontal advance per character, including the inter-character gap.
pub(crate) const ADVANCE: u32 = GLYPH_W + 1;

/// Glyph tables and rasterisation, needed only when a renderer is enabled.
#[cfg(any(feature = "png", feature = "svg"))]
mod bitmap {
    use super::{GLYPH_H, GLYPH_W};

    /// Rows of a glyph, most significant of the low five bits leftmost.
    type Glyph = [u8; GLYPH_H as usize];

    #[rustfmt::skip]
    const DIGITS: [Glyph; 10] = [
        [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110], // 0
        [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], // 1
        [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111], // 2
        [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110], // 3
        [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010], // 4
        [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110], // 5
        [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110], // 6
        [0b11111, 0b10001, 0b00001, 0b00010, 0b00100, 0b00100, 0b00100], // 7
        [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110], // 8
        [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100], // 9
    ];

    #[rustfmt::skip]
    const LETTERS: [Glyph; 26] = [
        [0b00100, 0b01010, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001], // A
        [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110], // B
        [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110], // C
        [0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100], // D
        [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111], // E
        [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000], // F
        [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111], // G
        [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001], // H
        [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110], // I
        [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100], // J
        [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001], // K
        [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111], // L
        [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001], // M
        [0b10001, 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001], // N
        [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110], // O
        [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000], // P
        [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101], // Q
        [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001], // R
        [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110], // S
        [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100], // T
        [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110], // U
        [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100], // V
        [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001], // W
        [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001], // X
        [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100], // Y
        [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111], // Z
    ];

    #[rustfmt::skip]
    const SPACE:  Glyph = [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000];
    #[rustfmt::skip]
    const HYPHEN: Glyph = [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000];
    #[rustfmt::skip]
    const PERIOD: Glyph = [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100];
    #[rustfmt::skip]
    const SLASH:  Glyph = [0b00001, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b10000];
    #[rustfmt::skip]
    const STAR:   Glyph = [0b00000, 0b00000, 0b10101, 0b01110, 0b10101, 0b00000, 0b00000];
    #[rustfmt::skip]
    const PLUS:   Glyph = [0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000];
    #[rustfmt::skip]
    const DOLLAR: Glyph = [0b00100, 0b01111, 0b10100, 0b01110, 0b00101, 0b11110, 0b00100];
    #[rustfmt::skip]
    const PERCENT:Glyph = [0b11001, 0b11010, 0b00010, 0b00100, 0b01000, 0b01011, 0b10011];
    #[rustfmt::skip]
    const COLON:  Glyph = [0b00000, 0b00000, 0b01100, 0b01100, 0b00000, 0b01100, 0b01100];
    /// Drawn for characters the font does not cover.
    #[rustfmt::skip]
    pub(crate) const TOFU:   Glyph = [0b11111, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11111];

    /// Look up the glyph for `c`, falling back to the missing-glyph box.
    pub(crate) fn glyph(c: char) -> &'static Glyph {
        match c {
            '0'..='9' => &DIGITS[c as usize - '0' as usize],
            'A'..='Z' => &LETTERS[c as usize - 'A' as usize],
            ' ' => &SPACE,
            '-' => &HYPHEN,
            '.' => &PERIOD,
            '/' => &SLASH,
            '*' => &STAR,
            '+' => &PLUS,
            '$' => &DOLLAR,
            '%' => &PERCENT,
            ':' => &COLON,
            _ => &TOFU,
        }
    }

    /// Whether the module at `(x, y)` within a glyph is set.
    pub(crate) fn pixel(glyph: &Glyph, x: u32, y: u32) -> bool {
        if x >= GLYPH_W || y >= GLYPH_H {
            return false;
        }
        let shift = GLYPH_W - 1 - x;
        (glyph[y as usize] >> shift) & 1 == 1
    }
}

#[cfg(any(feature = "png", feature = "svg"))]
pub(crate) use bitmap::{glyph, pixel};

/// Whether the font has a real glyph for `c` (as opposed to the fallback box).
pub(crate) fn is_supported(c: char) -> bool {
    matches!(c, '0'..='9' | 'A'..='Z' | ' ' | '-' | '.' | '/' | '*' | '+' | '$' | '%' | ':')
}

/// Width in font pixels of `char_count` characters, excluding the trailing gap.
pub(crate) fn text_width(char_count: u32) -> u32 {
    if char_count == 0 {
        0
    } else {
        char_count * ADVANCE - 1
    }
}

#[cfg(all(test, any(feature = "png", feature = "svg")))]
mod tests {
    use super::*;
    use bitmap::TOFU;

    #[test]
    fn every_glyph_fits_five_columns() {
        for c in ('0'..='9').chain('A'..='Z').chain(" -./*+$%:".chars()) {
            for row in glyph(c) {
                assert_eq!(row & !0b11111, 0, "glyph `{c}` has bits outside 5 columns");
            }
        }
    }

    #[test]
    fn distinct_characters_have_distinct_glyphs() {
        // A transcription slip that duplicated a glyph would silently print
        // the wrong character; this catches it.
        let chars: alloc::vec::Vec<char> = ('0'..='9').chain('A'..='Z').collect();
        for (i, a) in chars.iter().enumerate() {
            for b in &chars[i + 1..] {
                assert_ne!(glyph(*a), glyph(*b), "`{a}` and `{b}` share a glyph");
            }
        }
    }

    #[test]
    fn no_alphanumeric_glyph_is_blank() {
        for c in ('0'..='9').chain('A'..='Z') {
            assert!(glyph(c).iter().any(|r| *r != 0), "glyph `{c}` is blank");
        }
    }

    #[test]
    fn unsupported_characters_fall_back_to_the_box() {
        assert_eq!(glyph('a'), &TOFU);
        assert_eq!(glyph('\u{4e2d}'), &TOFU);
        assert!(!is_supported('a'));
        assert!(is_supported('Z'));
    }

    #[test]
    fn pixels_are_addressed_left_to_right() {
        // `1` has its top row as `00100`, so column 2 is set and 0 is not.
        let one = glyph('1');
        assert!(!pixel(one, 0, 0));
        assert!(pixel(one, 2, 0));
        assert!(!pixel(one, 9, 0));
    }

    #[test]
    fn text_width_accounts_for_inter_character_gaps() {
        assert_eq!(text_width(0), 0);
        assert_eq!(text_width(1), 5);
        assert_eq!(text_width(3), 17); // 5 + 1 + 5 + 1 + 5
    }
}