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    pub fn from_row(row: Vec<bool>) -> Self {
114        assert!(!row.is_empty(), "a symbol must have a positive size");
115        Self {
116            width: row.len() as u32,
117            height: 1,
118            bits: row,
119        }
120    }
121
122    /// Build a matrix from row-major module data.
123    ///
124    /// Returns `None` if `bits.len()` is not exactly `width * height`, or if
125    /// either dimension is zero.
126    pub fn from_vec(width: u32, height: u32, bits: Vec<bool>) -> Option<Self> {
127        if width == 0 || height == 0 {
128            return None;
129        }
130        if bits.len() != (width as usize).checked_mul(height as usize)? {
131            return None;
132        }
133        Some(Self {
134            width,
135            height,
136            bits,
137        })
138    }
139
140    /// Width in modules.
141    pub fn width(&self) -> u32 {
142        self.width
143    }
144
145    /// Height in modules.
146    pub fn height(&self) -> u32 {
147        self.height
148    }
149
150    /// Whether the module at `(x, y)` is dark. Out-of-bounds reads as light.
151    pub fn get(&self, x: u32, y: u32) -> bool {
152        if x >= self.width || y >= self.height {
153            return false;
154        }
155        self.bits[(y as usize) * (self.width as usize) + (x as usize)]
156    }
157
158    /// Set the module at `(x, y)`. Out-of-bounds writes are ignored.
159    pub fn set(&mut self, x: u32, y: u32, dark: bool) {
160        if x >= self.width || y >= self.height {
161            return;
162        }
163        let w = self.width as usize;
164        self.bits[(y as usize) * w + (x as usize)] = dark;
165    }
166
167    /// One row of modules.
168    pub fn row(&self, y: u32) -> &[bool] {
169        let w = self.width as usize;
170        let start = (y as usize) * w;
171        &self.bits[start..start + w]
172    }
173}
174
175impl fmt::Debug for BitMatrix {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        writeln!(f, "BitMatrix {}x{}", self.width, self.height)?;
178        for y in 0..self.height {
179            for &dark in self.row(y) {
180                f.write_str(if dark { "#" } else { "." })?;
181            }
182            writeln!(f)?;
183        }
184        Ok(())
185    }
186}
187
188/// An encoded symbol: the module grid plus what it encodes.
189///
190/// The grid contains the symbol only. Quiet zones are a rendering concern and
191/// are added by the renderers according to
192/// [`RenderOptions`](crate::RenderOptions), so that the same `Symbol` can be
193/// drawn with specification-conformant or deliberately tighter margins.
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct Symbol {
196    kind: SymbologyKind,
197    modules: BitMatrix,
198    payload: String,
199}
200
201impl Symbol {
202    /// Construct a symbol. Intended for [`Symbology`] implementations.
203    pub fn new(kind: SymbologyKind, modules: BitMatrix, payload: String) -> Self {
204        Self {
205            kind,
206            modules,
207            payload,
208        }
209    }
210
211    /// Which symbology produced this symbol.
212    pub fn kind(&self) -> SymbologyKind {
213        self.kind
214    }
215
216    /// The module grid, excluding quiet zones.
217    pub fn modules(&self) -> &BitMatrix {
218        &self.modules
219    }
220
221    /// The payload this symbol encodes.
222    pub fn payload(&self) -> &str {
223        &self.payload
224    }
225
226    /// Whether this symbol encodes data along one axis only.
227    pub fn is_linear(&self) -> bool {
228        self.kind.is_linear()
229    }
230}
231
232/// Maps a payload to a [`Symbol`].
233///
234/// Implement this to add a symbology. Renderers work against `Symbol`, so an
235/// implementation is all that a new barcode format requires.
236pub trait Symbology {
237    /// Which symbology this is.
238    fn kind(&self) -> SymbologyKind;
239
240    /// Encode `data`.
241    ///
242    /// # Errors
243    ///
244    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) for an
245    /// empty payload, or [`Error::Unencodable`](crate::Error::Unencodable) if
246    /// the payload contains characters this symbology cannot represent.
247    fn encode(&self, data: &str) -> Result<Symbol>;
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use alloc::format;
254
255    #[test]
256    fn matrix_reads_and_writes() {
257        let mut m = BitMatrix::new(3, 2);
258        assert!(!m.get(0, 0));
259        m.set(2, 1, true);
260        assert!(m.get(2, 1));
261        assert_eq!(m.row(1), &[false, false, true]);
262    }
263
264    #[test]
265    fn matrix_ignores_out_of_bounds_access() {
266        let mut m = BitMatrix::new(2, 2);
267        m.set(9, 9, true); // must not panic
268        assert!(!m.get(9, 9));
269    }
270
271    #[test]
272    fn debug_renders_ascii_art() {
273        let m = BitMatrix::from_row(vec![true, false, true]);
274        assert!(format!("{m:?}").contains("#.#"));
275    }
276
277    #[test]
278    fn code128_requires_a_ten_module_quiet_zone() {
279        assert_eq!(SymbologyKind::Code128.required_quiet_zone(), 10);
280        assert!(SymbologyKind::Code128.is_linear());
281    }
282}