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