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    /// How this symbology groups elements into fixed-width characters, if it
64    /// is linear.
65    ///
66    /// Returns `None` for matrix symbologies, which have no scan-line
67    /// structure to describe. [`is_linear`](Self::is_linear) and this method
68    /// always agree.
69    pub fn linear_character(self) -> Option<LinearCharacter> {
70        match self {
71            // ISO/IEC 15417: every Code 128 character is three bars and three
72            // spaces totalling 11 modules; the stop pattern adds a fourth bar
73            // and two extra modules.
74            Self::Code128 => Some(LinearCharacter {
75                elements: 6,
76                modules: 11,
77                stop_elements: 7,
78                stop_modules: 13,
79            }),
80            Self::Qr => None,
81        }
82    }
83
84    /// Quiet zone the specification requires, in modules per side.
85    ///
86    /// Code 128 requires 10 modules; QR requires 4 on all four sides. Getting
87    /// this wrong is the single most common cause of barcodes that "look fine
88    /// but will not scan".
89    pub fn required_quiet_zone(self) -> u32 {
90        match self {
91            Self::Code128 => 10,
92            Self::Qr => 4,
93        }
94    }
95}
96
97impl fmt::Display for SymbologyKind {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        f.write_str(self.name())
100    }
101}
102
103/// How a linear symbology groups bars and spaces into fixed-width characters.
104///
105/// Every character of a linear symbology occupies a known number of modules
106/// spread over a known number of elements — bars and spaces — and the symbol
107/// finishes with a wider terminating pattern. A scanner uses this to convert
108/// pixel measurements back into modules one character at a time, instead of
109/// assuming a single module width holds across the whole symbol. That is what
110/// keeps decoding accurate when a label is printed a little off-scale or fed
111/// through a scanner at a slight skew.
112///
113/// This is metadata for the same reason
114/// [`required_quiet_zone`](SymbologyKind::required_quiet_zone) is: it lets
115/// [`Scanner`](crate::scan::Scanner) stay ignorant of which symbology it is
116/// reading, exactly as the renderers are.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct LinearCharacter {
119    /// Bars and spaces in one character.
120    pub elements: u32,
121    /// Modules in one character.
122    pub modules: u32,
123    /// Bars and spaces in the terminating pattern.
124    pub stop_elements: u32,
125    /// Modules in the terminating pattern.
126    pub stop_modules: u32,
127}
128
129/// A rectangular grid of dark (`true`) and light (`false`) modules.
130///
131/// The `Debug` implementation renders ASCII art, which makes failing tests
132/// readable at a glance.
133#[derive(Clone, PartialEq, Eq)]
134pub struct BitMatrix {
135    width: u32,
136    height: u32,
137    bits: Vec<bool>,
138}
139
140impl BitMatrix {
141    /// Create an all-light matrix.
142    ///
143    /// # Panics
144    ///
145    /// Panics if `width` or `height` is zero, which no symbology should ever
146    /// produce.
147    pub fn new(width: u32, height: u32) -> Self {
148        assert!(
149            width > 0 && height > 0,
150            "a symbol must have a positive size"
151        );
152        Self {
153            width,
154            height,
155            bits: vec![false; (width as usize) * (height as usize)],
156        }
157    }
158
159    /// Build a single-row matrix from a run of modules.
160    ///
161    /// # Panics
162    ///
163    /// Panics if `row` is empty, which no symbology should ever produce. Use
164    /// [`from_vec`](Self::from_vec) to get an [`Option`] instead.
165    pub fn from_row(row: Vec<bool>) -> Self {
166        assert!(!row.is_empty(), "a symbol must have a positive size");
167        Self {
168            width: row.len() as u32,
169            height: 1,
170            bits: row,
171        }
172    }
173
174    /// Build a matrix from row-major module data.
175    ///
176    /// Returns `None` if `bits.len()` is not exactly `width * height`, or if
177    /// either dimension is zero.
178    pub fn from_vec(width: u32, height: u32, bits: Vec<bool>) -> Option<Self> {
179        if width == 0 || height == 0 {
180            return None;
181        }
182        if bits.len() != (width as usize).checked_mul(height as usize)? {
183            return None;
184        }
185        Some(Self {
186            width,
187            height,
188            bits,
189        })
190    }
191
192    /// Width in modules.
193    pub fn width(&self) -> u32 {
194        self.width
195    }
196
197    /// Height in modules.
198    pub fn height(&self) -> u32 {
199        self.height
200    }
201
202    /// Whether the module at `(x, y)` is dark. Out-of-bounds reads as light.
203    pub fn get(&self, x: u32, y: u32) -> bool {
204        if x >= self.width || y >= self.height {
205            return false;
206        }
207        self.bits[(y as usize) * (self.width as usize) + (x as usize)]
208    }
209
210    /// Set the module at `(x, y)`. Out-of-bounds writes are ignored.
211    pub fn set(&mut self, x: u32, y: u32, dark: bool) {
212        if x >= self.width || y >= self.height {
213            return;
214        }
215        let w = self.width as usize;
216        self.bits[(y as usize) * w + (x as usize)] = dark;
217    }
218
219    /// One row of modules. An out-of-bounds row reads as empty, matching the
220    /// way [`get`](Self::get) and [`set`](Self::set) tolerate out-of-bounds
221    /// coordinates.
222    pub fn row(&self, y: u32) -> &[bool] {
223        if y >= self.height {
224            return &[];
225        }
226        let w = self.width as usize;
227        let start = (y as usize) * w;
228        &self.bits[start..start + w]
229    }
230}
231
232impl fmt::Debug for BitMatrix {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        writeln!(f, "BitMatrix {}x{}", self.width, self.height)?;
235        for y in 0..self.height {
236            for &dark in self.row(y) {
237                f.write_str(if dark { "#" } else { "." })?;
238            }
239            writeln!(f)?;
240        }
241        Ok(())
242    }
243}
244
245/// An encoded symbol: the module grid plus what it encodes.
246///
247/// The grid contains the symbol only. Quiet zones are a rendering concern and
248/// are added by the renderers according to
249/// [`RenderOptions`](crate::RenderOptions), so that the same `Symbol` can be
250/// drawn with specification-conformant or deliberately tighter margins.
251#[derive(Clone, Debug, PartialEq, Eq)]
252pub struct Symbol {
253    kind: SymbologyKind,
254    modules: BitMatrix,
255    payload: String,
256}
257
258impl Symbol {
259    /// Construct a symbol. Intended for [`Symbology`] implementations.
260    pub fn new(kind: SymbologyKind, modules: BitMatrix, payload: String) -> Self {
261        Self {
262            kind,
263            modules,
264            payload,
265        }
266    }
267
268    /// Which symbology produced this symbol.
269    pub fn kind(&self) -> SymbologyKind {
270        self.kind
271    }
272
273    /// The module grid, excluding quiet zones.
274    pub fn modules(&self) -> &BitMatrix {
275        &self.modules
276    }
277
278    /// The payload this symbol encodes.
279    pub fn payload(&self) -> &str {
280        &self.payload
281    }
282
283    /// Whether this symbol encodes data along one axis only.
284    pub fn is_linear(&self) -> bool {
285        self.kind.is_linear()
286    }
287}
288
289/// Maps a payload to a [`Symbol`].
290///
291/// Implement this to add a symbology. Renderers work against `Symbol`, so an
292/// implementation is all that a new barcode format requires.
293pub trait Symbology {
294    /// Which symbology this is.
295    fn kind(&self) -> SymbologyKind;
296
297    /// Encode `data`.
298    ///
299    /// # Errors
300    ///
301    /// Returns [`Error::EmptyPayload`](crate::Error::EmptyPayload) for an
302    /// empty payload, or [`Error::Unencodable`](crate::Error::Unencodable) if
303    /// the payload contains characters this symbology cannot represent.
304    fn encode(&self, data: &str) -> Result<Symbol>;
305}
306
307/// Recovers a payload from a module grid.
308///
309/// The mirror of [`Symbology`]: an encoder turns a payload into a
310/// [`BitMatrix`], a decoder turns one back.
311/// [`Scanner`](crate::scan::Scanner) works against this trait rather than
312/// against a concrete symbology, which is what keeps the scanning side of the
313/// crate on the same seam as the rendering side.
314pub trait Decoder {
315    /// Which symbology this decodes.
316    fn kind(&self) -> SymbologyKind;
317
318    /// Decode `modules`, which must hold the symbol alone, with no quiet zone.
319    ///
320    /// # Errors
321    ///
322    /// Returns [`Error::Decode`](crate::Error::Decode) if the grid has the
323    /// wrong shape for this symbology, or if the module pattern is not a valid
324    /// symbol.
325    fn decode(&self, modules: &BitMatrix) -> Result<String>;
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use alloc::format;
332
333    #[test]
334    fn matrix_reads_and_writes() {
335        let mut m = BitMatrix::new(3, 2);
336        assert!(!m.get(0, 0));
337        m.set(2, 1, true);
338        assert!(m.get(2, 1));
339        assert_eq!(m.row(1), &[false, false, true]);
340    }
341
342    #[test]
343    fn matrix_ignores_out_of_bounds_access() {
344        let mut m = BitMatrix::new(2, 2);
345        m.set(9, 9, true); // must not panic
346        assert!(!m.get(9, 9));
347    }
348
349    #[test]
350    fn matrix_reads_out_of_bounds_rows_as_empty() {
351        // `get` and `set` tolerate out-of-range coordinates, so `row` must too
352        // rather than panicking on a slice range — this is public API, and the
353        // crate promises not to panic on bad input.
354        let m = BitMatrix::new(3, 2);
355        assert_eq!(m.row(0).len(), 3);
356        assert_eq!(m.row(1).len(), 3);
357        assert!(m.row(2).is_empty());
358        assert!(m.row(u32::MAX).is_empty());
359    }
360
361    #[test]
362    fn debug_renders_ascii_art() {
363        let m = BitMatrix::from_row(vec![true, false, true]);
364        assert!(format!("{m:?}").contains("#.#"));
365    }
366
367    #[test]
368    fn linearity_and_character_structure_agree() {
369        // Two pieces of metadata that have to say the same thing. A scanner
370        // asks for the character structure; a renderer asks whether the
371        // symbology is linear. If they ever disagree, one of them silently
372        // stops working.
373        for kind in [SymbologyKind::Code128, SymbologyKind::Qr] {
374            assert_eq!(
375                kind.is_linear(),
376                kind.linear_character().is_some(),
377                "{kind} disagrees with itself about being linear"
378            );
379        }
380    }
381
382    #[test]
383    fn a_linear_character_is_wider_than_its_element_count() {
384        // Every element needs at least one module, or a scanner cannot
385        // apportion a character's width without losing a bar.
386        for kind in [SymbologyKind::Code128, SymbologyKind::Qr] {
387            let Some(c) = kind.linear_character() else {
388                continue;
389            };
390            assert!(c.modules >= c.elements, "{kind}: character too narrow");
391            assert!(
392                c.stop_modules >= c.stop_elements,
393                "{kind}: stop pattern too narrow"
394            );
395        }
396    }
397
398    #[test]
399    fn code128_requires_a_ten_module_quiet_zone() {
400        assert_eq!(SymbologyKind::Code128.required_quiet_zone(), 10);
401        assert!(SymbologyKind::Code128.is_linear());
402    }
403}