Skip to main content

smart_package_tracker/symbology/
code128.rs

1//! Code 128 encoding and decoding.
2//!
3//! Wraps the [`code128`](https://docs.rs/code128) crate, which implements
4//! ISO/IEC 15417 including automatic character-set selection — digit runs are
5//! compressed into Set C without the caller having to ask, which matters for
6//! the long numeric payloads carriers use.
7//!
8//! The upstream crate reports bar coordinates with its own 10-module quiet
9//! zone already applied. This module strips that offset so a [`Symbol`] holds
10//! the symbol and nothing else, leaving quiet zones to
11//! [`RenderOptions`](crate::RenderOptions).
12
13use alloc::string::{String, ToString};
14use alloc::vec;
15use alloc::vec::Vec;
16
17use super::{BitMatrix, Symbol, Symbology, SymbologyKind};
18use crate::error::{Error, Result};
19
20/// Quiet zone baked into `code128`'s coordinate space, in modules per side.
21const UPSTREAM_QUIET_ZONE: u32 = 10;
22
23/// The Code 128 symbology.
24///
25/// # Examples
26///
27/// ```
28/// use smart_package_tracker::symbology::{Code128, Symbology};
29///
30/// let symbol = Code128.encode("PKG-9ED9285C")?;
31/// assert!(symbol.is_linear());
32/// assert_eq!(symbol.modules().height(), 1);
33/// # Ok::<(), smart_package_tracker::Error>(())
34/// ```
35#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
36pub struct Code128;
37
38impl Symbology for Code128 {
39    fn kind(&self) -> SymbologyKind {
40        SymbologyKind::Code128
41    }
42
43    fn encode(&self, data: &str) -> Result<Symbol> {
44        if data.is_empty() {
45            return Err(Error::EmptyPayload);
46        }
47
48        let code = ::code128::Code128::encode_str(data).ok_or_else(|| Error::Unencodable {
49            symbology: SymbologyKind::Code128.name(),
50            reason: "payload contains characters outside ISO/IEC 8859-1 (Latin-1)".to_string(),
51        })?;
52
53        // `len()` counts the quiet zone on both sides; the symbol itself is
54        // what remains.
55        let total = code.len() as u32;
56        let width = total
57            .checked_sub(2 * UPSTREAM_QUIET_ZONE)
58            .filter(|w| *w > 0)
59            .ok_or_else(|| Error::Unencodable {
60                symbology: SymbologyKind::Code128.name(),
61                reason: "encoder produced a degenerate symbol".to_string(),
62            })?;
63
64        let mut row = vec![false; width as usize];
65        for bar in code.bar_coordinates() {
66            let start = bar.x.saturating_sub(UPSTREAM_QUIET_ZONE) as usize;
67            // Clamp both ends, not just the far one: a bar reported at or past
68            // the trailing quiet zone would otherwise slice backwards and
69            // panic. Upstream does not emit one today; this keeps a version
70            // bump from turning that into a crash.
71            if start >= row.len() {
72                continue;
73            }
74            let end = (start + bar.width as usize).min(row.len());
75            row[start..end].fill(true);
76        }
77
78        Ok(Symbol::new(
79            SymbologyKind::Code128,
80            BitMatrix::from_row(row),
81            data.to_string(),
82        ))
83    }
84}
85
86/// Decode a Code 128 [`Symbol`] back into its payload.
87///
88/// This reconstructs bar/space runs from the module grid and hands them to the
89/// upstream decoder, so it exercises the same conversion the renderers rely
90/// on. That makes it a genuine round-trip check rather than a trivial identity
91/// on the stored payload.
92///
93/// # Errors
94///
95/// Returns [`Error::Decode`] if the symbol is not a linear Code 128 symbol, or
96/// if the module pattern is not a valid Code 128 sequence.
97pub fn decode(symbol: &Symbol) -> Result<String> {
98    if symbol.kind() != SymbologyKind::Code128 {
99        return Err(Error::Decode(alloc::format!(
100            "expected a Code 128 symbol, found {}",
101            symbol.kind()
102        )));
103    }
104
105    let modules = symbol.modules();
106    if modules.height() != 1 {
107        return Err(Error::Decode(
108            "expected a single-row linear symbol".to_string(),
109        ));
110    }
111
112    let bars = to_bars(modules.row(0));
113    if bars.is_empty() {
114        return Err(Error::Decode("symbol contains no bars".to_string()));
115    }
116
117    let bytes = ::code128::decode(&bars).map_err(|e| Error::Decode(alloc::format!("{e:?}")))?;
118
119    // The decoder returns Latin-1 bytes; widening each byte to a `char` is the
120    // exact inverse of the encoder's Latin-1 narrowing.
121    Ok(bytes.into_iter().map(|b| b as char).collect())
122}
123
124/// Convert a module row into the bar/space runs the upstream decoder expects.
125fn to_bars(row: &[bool]) -> Vec<::code128::Bar> {
126    let mut bars = Vec::new();
127    let mut i = 0;
128
129    while i < row.len() {
130        if !row[i] {
131            i += 1;
132            continue;
133        }
134        let bar_start = i;
135        while i < row.len() && row[i] {
136            i += 1;
137        }
138        let width = (i - bar_start) as u8;
139
140        let space_start = i;
141        while i < row.len() && !row[i] {
142            i += 1;
143        }
144        let space = (i - space_start) as u8;
145
146        bars.push(::code128::Bar { width, space });
147    }
148
149    bars
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use alloc::format;
156
157    /// Published Code 128 start patterns (11 modules each).
158    const START_A: &str = "11010000100";
159    const START_B: &str = "11010010000";
160    const START_C: &str = "11010011100";
161    /// Published Code 128 stop pattern (13 modules, including the two extra
162    /// termination bars).
163    const STOP: &str = "1100011101011";
164
165    fn as_bits(symbol: &Symbol) -> String {
166        symbol
167            .modules()
168            .row(0)
169            .iter()
170            .map(|d| if *d { '1' } else { '0' })
171            .collect()
172    }
173
174    #[test]
175    fn starts_with_a_valid_start_pattern() {
176        let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
177        let start = &bits[..11];
178        assert!(
179            start == START_A || start == START_B || start == START_C,
180            "unexpected start pattern {start}"
181        );
182    }
183
184    #[test]
185    fn numeric_payloads_use_the_set_c_start_pattern() {
186        // A pure digit run should be encoded two digits per symbol.
187        let bits = as_bits(&Code128.encode("1234567890").unwrap());
188        assert_eq!(&bits[..11], START_C, "digit runs should start in Set C");
189    }
190
191    #[test]
192    fn ends_with_the_stop_pattern() {
193        let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
194        assert!(bits.ends_with(STOP), "missing or malformed stop pattern");
195    }
196
197    #[test]
198    fn width_is_a_whole_number_of_symbols_plus_the_stop_bars() {
199        // Every Code 128 character occupies 11 modules; the stop pattern adds
200        // two more.
201        let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
202        assert_eq!(
203            (bits.len() - 2) % 11,
204            0,
205            "symbol width {} is not 11n + 2",
206            bits.len()
207        );
208    }
209
210    #[test]
211    fn set_c_halves_the_width_of_long_digit_runs() {
212        let digits = "12345678901234567890"; // 20 digits
213        let letters = "ABCDEFGHIJKLMNOPQRST"; // 20 letters
214        let numeric = Code128.encode(digits).unwrap().modules().width();
215        let alpha = Code128.encode(letters).unwrap().modules().width();
216        assert!(
217            numeric < alpha,
218            "Set C compression not applied: {numeric} modules vs {alpha}"
219        );
220    }
221
222    #[test]
223    fn round_trips_through_the_module_grid() {
224        for payload in [
225            "PKG-9ED9285C",
226            "1234567890123456789012",
227            "A",
228            "Mixed 123 Case!",
229            "~$%^&*()_+",
230        ] {
231            let symbol = Code128.encode(payload).unwrap();
232            let decoded = decode(&symbol).unwrap_or_else(|e| panic!("{payload}: {e}"));
233            assert_eq!(decoded, payload);
234        }
235    }
236
237    #[test]
238    fn rejects_an_empty_payload() {
239        assert!(matches!(Code128.encode(""), Err(Error::EmptyPayload)));
240    }
241
242    #[test]
243    fn rejects_characters_outside_latin1() {
244        let err = Code128.encode("PKG-\u{4e2d}\u{6587}").unwrap_err();
245        assert!(matches!(err, Error::Unencodable { .. }), "got {err:?}");
246    }
247
248    #[test]
249    fn quiet_zone_is_not_part_of_the_symbol() {
250        let symbol = Code128.encode("A").unwrap();
251        let row = symbol.modules().row(0);
252        assert!(row[0], "symbol must begin with a bar, not a quiet zone");
253        assert!(
254            *row.last().unwrap(),
255            "symbol must end with a bar, not a quiet zone"
256        );
257    }
258
259    #[test]
260    fn payload_is_preserved_on_the_symbol() {
261        let symbol = Code128.encode("PKG-9ED9285C").unwrap();
262        assert_eq!(symbol.payload(), "PKG-9ED9285C");
263        assert_eq!(format!("{}", symbol.kind()), "Code 128");
264    }
265}