smart-package-tracker 0.2.0

Generate package tracking IDs and render them as Code 128 barcodes (PNG and SVG)
Documentation
//! Turning payloads into bit patterns.
//!
//! A symbology is anything that maps a string to a grid of dark and light
//! modules. Linear symbologies such as Code 128 produce a single row; matrix
//! symbologies such as QR produce a square. Both are represented as a
//! [`BitMatrix`] inside a [`Symbol`], which is what the renderers consume.
//!
//! Keeping the renderers on this side of the boundary is what makes new
//! symbologies cheap: adding QR means adding a [`Symbology`] implementation,
//! not touching the PNG or SVG code.

#[cfg(feature = "code128")]
pub mod code128;
#[cfg(feature = "qr")]
pub mod qr;

#[cfg(feature = "code128")]
pub use code128::Code128;
#[cfg(feature = "qr")]
pub use qr::{Ecc, Qr, QrVersion};

use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;

use crate::error::Result;

/// Which symbology produced a [`Symbol`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum SymbologyKind {
    /// Code 128, per ISO/IEC 15417.
    Code128,
    /// QR Code, per ISO/IEC 18004.
    ///
    /// QR Code is a registered trademark of Denso Wave Incorporated.
    Qr,
}

impl SymbologyKind {
    /// Short human-readable name, used in error messages.
    pub fn name(self) -> &'static str {
        match self {
            Self::Code128 => "Code 128",
            Self::Qr => "QR Code",
        }
    }

    /// Whether the symbology encodes data along one axis only.
    ///
    /// Linear symbologies take their height from
    /// [`RenderOptions`](crate::RenderOptions); matrix symbologies derive it
    /// from the module grid.
    pub fn is_linear(self) -> bool {
        match self {
            Self::Code128 => true,
            Self::Qr => false,
        }
    }

    /// Quiet zone the specification requires, in modules per side.
    ///
    /// Code 128 requires 10 modules; QR requires 4 on all four sides. Getting
    /// this wrong is the single most common cause of barcodes that "look fine
    /// but will not scan".
    pub fn required_quiet_zone(self) -> u32 {
        match self {
            Self::Code128 => 10,
            Self::Qr => 4,
        }
    }
}

impl fmt::Display for SymbologyKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.name())
    }
}

/// A rectangular grid of dark (`true`) and light (`false`) modules.
///
/// The `Debug` implementation renders ASCII art, which makes failing tests
/// readable at a glance.
#[derive(Clone, PartialEq, Eq)]
pub struct BitMatrix {
    width: u32,
    height: u32,
    bits: Vec<bool>,
}

impl BitMatrix {
    /// Create an all-light matrix.
    ///
    /// # Panics
    ///
    /// Panics if `width` or `height` is zero, which no symbology should ever
    /// produce.
    pub fn new(width: u32, height: u32) -> Self {
        assert!(
            width > 0 && height > 0,
            "a symbol must have a positive size"
        );
        Self {
            width,
            height,
            bits: vec![false; (width as usize) * (height as usize)],
        }
    }

    /// Build a single-row matrix from a run of modules.
    pub fn from_row(row: Vec<bool>) -> Self {
        assert!(!row.is_empty(), "a symbol must have a positive size");
        Self {
            width: row.len() as u32,
            height: 1,
            bits: row,
        }
    }

    /// Build a matrix from row-major module data.
    ///
    /// Returns `None` if `bits.len()` is not exactly `width * height`, or if
    /// either dimension is zero.
    pub fn from_vec(width: u32, height: u32, bits: Vec<bool>) -> Option<Self> {
        if width == 0 || height == 0 {
            return None;
        }
        if bits.len() != (width as usize).checked_mul(height as usize)? {
            return None;
        }
        Some(Self {
            width,
            height,
            bits,
        })
    }

    /// Width in modules.
    pub fn width(&self) -> u32 {
        self.width
    }

    /// Height in modules.
    pub fn height(&self) -> u32 {
        self.height
    }

    /// Whether the module at `(x, y)` is dark. Out-of-bounds reads as light.
    pub fn get(&self, x: u32, y: u32) -> bool {
        if x >= self.width || y >= self.height {
            return false;
        }
        self.bits[(y as usize) * (self.width as usize) + (x as usize)]
    }

    /// Set the module at `(x, y)`. Out-of-bounds writes are ignored.
    pub fn set(&mut self, x: u32, y: u32, dark: bool) {
        if x >= self.width || y >= self.height {
            return;
        }
        let w = self.width as usize;
        self.bits[(y as usize) * w + (x as usize)] = dark;
    }

    /// One row of modules.
    pub fn row(&self, y: u32) -> &[bool] {
        let w = self.width as usize;
        let start = (y as usize) * w;
        &self.bits[start..start + w]
    }
}

impl fmt::Debug for BitMatrix {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "BitMatrix {}x{}", self.width, self.height)?;
        for y in 0..self.height {
            for &dark in self.row(y) {
                f.write_str(if dark { "#" } else { "." })?;
            }
            writeln!(f)?;
        }
        Ok(())
    }
}

/// An encoded symbol: the module grid plus what it encodes.
///
/// The grid contains the symbol only. Quiet zones are a rendering concern and
/// are added by the renderers according to
/// [`RenderOptions`](crate::RenderOptions), so that the same `Symbol` can be
/// drawn with specification-conformant or deliberately tighter margins.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Symbol {
    kind: SymbologyKind,
    modules: BitMatrix,
    payload: String,
}

impl Symbol {
    /// Construct a symbol. Intended for [`Symbology`] implementations.
    pub fn new(kind: SymbologyKind, modules: BitMatrix, payload: String) -> Self {
        Self {
            kind,
            modules,
            payload,
        }
    }

    /// Which symbology produced this symbol.
    pub fn kind(&self) -> SymbologyKind {
        self.kind
    }

    /// The module grid, excluding quiet zones.
    pub fn modules(&self) -> &BitMatrix {
        &self.modules
    }

    /// The payload this symbol encodes.
    pub fn payload(&self) -> &str {
        &self.payload
    }

    /// Whether this symbol encodes data along one axis only.
    pub fn is_linear(&self) -> bool {
        self.kind.is_linear()
    }
}

/// Maps a payload to a [`Symbol`].
///
/// Implement this to add a symbology. Renderers work against `Symbol`, so an
/// implementation is all that a new barcode format requires.
pub trait Symbology {
    /// Which symbology this is.
    fn kind(&self) -> SymbologyKind;

    /// Encode `data`.
    ///
    /// # Errors
    ///
    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) for an
    /// empty payload, or [`Error::Unencodable`](crate::Error::Unencodable) if
    /// the payload contains characters this symbology cannot represent.
    fn encode(&self, data: &str) -> Result<Symbol>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::format;

    #[test]
    fn matrix_reads_and_writes() {
        let mut m = BitMatrix::new(3, 2);
        assert!(!m.get(0, 0));
        m.set(2, 1, true);
        assert!(m.get(2, 1));
        assert_eq!(m.row(1), &[false, false, true]);
    }

    #[test]
    fn matrix_ignores_out_of_bounds_access() {
        let mut m = BitMatrix::new(2, 2);
        m.set(9, 9, true); // must not panic
        assert!(!m.get(9, 9));
    }

    #[test]
    fn debug_renders_ascii_art() {
        let m = BitMatrix::from_row(vec![true, false, true]);
        assert!(format!("{m:?}").contains("#.#"));
    }

    #[test]
    fn code128_requires_a_ten_module_quiet_zone() {
        assert_eq!(SymbologyKind::Code128.required_quiet_zone(), 10);
        assert!(SymbologyKind::Code128.is_linear());
    }
}