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}