Skip to main content

smart_package_tracker/symbology/
mod.rs

1//! Turning payloads into bit patterns.
2//!
3//! A symbology is anything that maps a string to a grid of dark and light
4//! modules. Linear symbologies such as Code 128 produce a single row; matrix
5//! symbologies such as QR produce a square. Both are represented as a
6//! [`BitMatrix`] inside a [`Symbol`], which is what the renderers consume.
7//!
8//! Keeping the renderers on this side of the boundary is what makes new
9//! symbologies cheap: adding QR means adding a [`Symbology`] implementation,
10//! not touching the PNG or SVG code.
11
12#[cfg(feature = "code128")]
13pub mod code128;
14
15#[cfg(feature = "code128")]
16pub use code128::Code128;
17
18use alloc::string::String;
19use alloc::vec;
20use alloc::vec::Vec;
21use core::fmt;
22
23use crate::error::Result;
24
25/// Which symbology produced a [`Symbol`].
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[non_exhaustive]
29pub enum SymbologyKind {
30    /// Code 128, per ISO/IEC 15417.
31    Code128,
32}
33
34impl SymbologyKind {
35    /// Short human-readable name, used in error messages.
36    pub fn name(self) -> &'static str {
37        match self {
38            Self::Code128 => "Code 128",
39        }
40    }
41
42    /// Whether the symbology encodes data along one axis only.
43    ///
44    /// Linear symbologies take their height from
45    /// [`RenderOptions`](crate::RenderOptions); matrix symbologies derive it
46    /// from the module grid.
47    pub fn is_linear(self) -> bool {
48        match self {
49            Self::Code128 => true,
50        }
51    }
52
53    /// Quiet zone the specification requires, in modules per side.
54    ///
55    /// Code 128 requires 10 modules. Getting this wrong is the single most
56    /// common cause of barcodes that "look fine but will not scan".
57    pub fn required_quiet_zone(self) -> u32 {
58        match self {
59            Self::Code128 => 10,
60        }
61    }
62}
63
64impl fmt::Display for SymbologyKind {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(self.name())
67    }
68}
69
70/// A rectangular grid of dark (`true`) and light (`false`) modules.
71///
72/// The `Debug` implementation renders ASCII art, which makes failing tests
73/// readable at a glance.
74#[derive(Clone, PartialEq, Eq)]
75pub struct BitMatrix {
76    width: u32,
77    height: u32,
78    bits: Vec<bool>,
79}
80
81impl BitMatrix {
82    /// Create an all-light matrix.
83    ///
84    /// # Panics
85    ///
86    /// Panics if `width` or `height` is zero, which no symbology should ever
87    /// produce.
88    pub fn new(width: u32, height: u32) -> Self {
89        assert!(
90            width > 0 && height > 0,
91            "a symbol must have a positive size"
92        );
93        Self {
94            width,
95            height,
96            bits: vec![false; (width as usize) * (height as usize)],
97        }
98    }
99
100    /// Build a single-row matrix from a run of modules.
101    pub fn from_row(row: Vec<bool>) -> Self {
102        assert!(!row.is_empty(), "a symbol must have a positive size");
103        Self {
104            width: row.len() as u32,
105            height: 1,
106            bits: row,
107        }
108    }
109
110    /// Width in modules.
111    pub fn width(&self) -> u32 {
112        self.width
113    }
114
115    /// Height in modules.
116    pub fn height(&self) -> u32 {
117        self.height
118    }
119
120    /// Whether the module at `(x, y)` is dark. Out-of-bounds reads as light.
121    pub fn get(&self, x: u32, y: u32) -> bool {
122        if x >= self.width || y >= self.height {
123            return false;
124        }
125        self.bits[(y as usize) * (self.width as usize) + (x as usize)]
126    }
127
128    /// Set the module at `(x, y)`. Out-of-bounds writes are ignored.
129    pub fn set(&mut self, x: u32, y: u32, dark: bool) {
130        if x >= self.width || y >= self.height {
131            return;
132        }
133        let w = self.width as usize;
134        self.bits[(y as usize) * w + (x as usize)] = dark;
135    }
136
137    /// One row of modules.
138    pub fn row(&self, y: u32) -> &[bool] {
139        let w = self.width as usize;
140        let start = (y as usize) * w;
141        &self.bits[start..start + w]
142    }
143}
144
145impl fmt::Debug for BitMatrix {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        writeln!(f, "BitMatrix {}x{}", self.width, self.height)?;
148        for y in 0..self.height {
149            for &dark in self.row(y) {
150                f.write_str(if dark { "#" } else { "." })?;
151            }
152            writeln!(f)?;
153        }
154        Ok(())
155    }
156}
157
158/// An encoded symbol: the module grid plus what it encodes.
159///
160/// The grid contains the symbol only. Quiet zones are a rendering concern and
161/// are added by the renderers according to
162/// [`RenderOptions`](crate::RenderOptions), so that the same `Symbol` can be
163/// drawn with specification-conformant or deliberately tighter margins.
164#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct Symbol {
166    kind: SymbologyKind,
167    modules: BitMatrix,
168    payload: String,
169}
170
171impl Symbol {
172    /// Construct a symbol. Intended for [`Symbology`] implementations.
173    pub fn new(kind: SymbologyKind, modules: BitMatrix, payload: String) -> Self {
174        Self {
175            kind,
176            modules,
177            payload,
178        }
179    }
180
181    /// Which symbology produced this symbol.
182    pub fn kind(&self) -> SymbologyKind {
183        self.kind
184    }
185
186    /// The module grid, excluding quiet zones.
187    pub fn modules(&self) -> &BitMatrix {
188        &self.modules
189    }
190
191    /// The payload this symbol encodes.
192    pub fn payload(&self) -> &str {
193        &self.payload
194    }
195
196    /// Whether this symbol encodes data along one axis only.
197    pub fn is_linear(&self) -> bool {
198        self.kind.is_linear()
199    }
200}
201
202/// Maps a payload to a [`Symbol`].
203///
204/// Implement this to add a symbology. Renderers work against `Symbol`, so an
205/// implementation is all that a new barcode format requires.
206pub trait Symbology {
207    /// Which symbology this is.
208    fn kind(&self) -> SymbologyKind;
209
210    /// Encode `data`.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) for an
215    /// empty payload, or [`Error::Unencodable`](crate::Error::Unencodable) if
216    /// the payload contains characters this symbology cannot represent.
217    fn encode(&self, data: &str) -> Result<Symbol>;
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use alloc::format;
224
225    #[test]
226    fn matrix_reads_and_writes() {
227        let mut m = BitMatrix::new(3, 2);
228        assert!(!m.get(0, 0));
229        m.set(2, 1, true);
230        assert!(m.get(2, 1));
231        assert_eq!(m.row(1), &[false, false, true]);
232    }
233
234    #[test]
235    fn matrix_ignores_out_of_bounds_access() {
236        let mut m = BitMatrix::new(2, 2);
237        m.set(9, 9, true); // must not panic
238        assert!(!m.get(9, 9));
239    }
240
241    #[test]
242    fn debug_renders_ascii_art() {
243        let m = BitMatrix::from_row(vec![true, false, true]);
244        assert!(format!("{m:?}").contains("#.#"));
245    }
246
247    #[test]
248    fn code128_requires_a_ten_module_quiet_zone() {
249        assert_eq!(SymbologyKind::Code128.required_quiet_zone(), 10);
250        assert!(SymbologyKind::Code128.is_linear());
251    }
252}