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}