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            let end = (start + bar.width as usize).min(row.len());
68            row[start..end].fill(true);
69        }
70
71        Ok(Symbol::new(
72            SymbologyKind::Code128,
73            BitMatrix::from_row(row),
74            data.to_string(),
75        ))
76    }
77}
78
79/// Decode a Code 128 [`Symbol`] back into its payload.
80///
81/// This reconstructs bar/space runs from the module grid and hands them to the
82/// upstream decoder, so it exercises the same conversion the renderers rely
83/// on. That makes it a genuine round-trip check rather than a trivial identity
84/// on the stored payload.
85///
86/// # Errors
87///
88/// Returns [`Error::Decode`] if the symbol is not a linear Code 128 symbol, or
89/// if the module pattern is not a valid Code 128 sequence.
90pub fn decode(symbol: &Symbol) -> Result<String> {
91    if symbol.kind() != SymbologyKind::Code128 {
92        return Err(Error::Decode(alloc::format!(
93            "expected a Code 128 symbol, found {}",
94            symbol.kind()
95        )));
96    }
97
98    let modules = symbol.modules();
99    if modules.height() != 1 {
100        return Err(Error::Decode(
101            "expected a single-row linear symbol".to_string(),
102        ));
103    }
104
105    let bars = to_bars(modules.row(0));
106    if bars.is_empty() {
107        return Err(Error::Decode("symbol contains no bars".to_string()));
108    }
109
110    let bytes = ::code128::decode(&bars).map_err(|e| Error::Decode(alloc::format!("{e:?}")))?;
111
112    // The decoder returns Latin-1 bytes; widening each byte to a `char` is the
113    // exact inverse of the encoder's Latin-1 narrowing.
114    Ok(bytes.into_iter().map(|b| b as char).collect())
115}
116
117/// Convert a module row into the bar/space runs the upstream decoder expects.
118fn to_bars(row: &[bool]) -> Vec<::code128::Bar> {
119    let mut bars = Vec::new();
120    let mut i = 0;
121
122    while i < row.len() {
123        if !row[i] {
124            i += 1;
125            continue;
126        }
127        let bar_start = i;
128        while i < row.len() && row[i] {
129            i += 1;
130        }
131        let width = (i - bar_start) as u8;
132
133        let space_start = i;
134        while i < row.len() && !row[i] {
135            i += 1;
136        }
137        let space = (i - space_start) as u8;
138
139        bars.push(::code128::Bar { width, space });
140    }
141
142    bars
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use alloc::format;
149
150    /// Published Code 128 start patterns (11 modules each).
151    const START_A: &str = "11010000100";
152    const START_B: &str = "11010010000";
153    const START_C: &str = "11010011100";
154    /// Published Code 128 stop pattern (13 modules, including the two extra
155    /// termination bars).
156    const STOP: &str = "1100011101011";
157
158    fn as_bits(symbol: &Symbol) -> String {
159        symbol
160            .modules()
161            .row(0)
162            .iter()
163            .map(|d| if *d { '1' } else { '0' })
164            .collect()
165    }
166
167    #[test]
168    fn starts_with_a_valid_start_pattern() {
169        let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
170        let start = &bits[..11];
171        assert!(
172            start == START_A || start == START_B || start == START_C,
173            "unexpected start pattern {start}"
174        );
175    }
176
177    #[test]
178    fn numeric_payloads_use_the_set_c_start_pattern() {
179        // A pure digit run should be encoded two digits per symbol.
180        let bits = as_bits(&Code128.encode("1234567890").unwrap());
181        assert_eq!(&bits[..11], START_C, "digit runs should start in Set C");
182    }
183
184    #[test]
185    fn ends_with_the_stop_pattern() {
186        let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
187        assert!(bits.ends_with(STOP), "missing or malformed stop pattern");
188    }
189
190    #[test]
191    fn width_is_a_whole_number_of_symbols_plus_the_stop_bars() {
192        // Every Code 128 character occupies 11 modules; the stop pattern adds
193        // two more.
194        let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
195        assert_eq!(
196            (bits.len() - 2) % 11,
197            0,
198            "symbol width {} is not 11n + 2",
199            bits.len()
200        );
201    }
202
203    #[test]
204    fn set_c_halves_the_width_of_long_digit_runs() {
205        let digits = "12345678901234567890"; // 20 digits
206        let letters = "ABCDEFGHIJKLMNOPQRST"; // 20 letters
207        let numeric = Code128.encode(digits).unwrap().modules().width();
208        let alpha = Code128.encode(letters).unwrap().modules().width();
209        assert!(
210            numeric < alpha,
211            "Set C compression not applied: {numeric} modules vs {alpha}"
212        );
213    }
214
215    #[test]
216    fn round_trips_through_the_module_grid() {
217        for payload in [
218            "PKG-9ED9285C",
219            "1234567890123456789012",
220            "A",
221            "Mixed 123 Case!",
222            "~$%^&*()_+",
223        ] {
224            let symbol = Code128.encode(payload).unwrap();
225            let decoded = decode(&symbol).unwrap_or_else(|e| panic!("{payload}: {e}"));
226            assert_eq!(decoded, payload);
227        }
228    }
229
230    #[test]
231    fn rejects_an_empty_payload() {
232        assert!(matches!(Code128.encode(""), Err(Error::EmptyPayload)));
233    }
234
235    #[test]
236    fn rejects_characters_outside_latin1() {
237        let err = Code128.encode("PKG-\u{4e2d}\u{6587}").unwrap_err();
238        assert!(matches!(err, Error::Unencodable { .. }), "got {err:?}");
239    }
240
241    #[test]
242    fn quiet_zone_is_not_part_of_the_symbol() {
243        let symbol = Code128.encode("A").unwrap();
244        let row = symbol.modules().row(0);
245        assert!(row[0], "symbol must begin with a bar, not a quiet zone");
246        assert!(
247            *row.last().unwrap(),
248            "symbol must end with a bar, not a quiet zone"
249        );
250    }
251
252    #[test]
253    fn payload_is_preserved_on_the_symbol() {
254        let symbol = Code128.encode("PKG-9ED9285C").unwrap();
255        assert_eq!(symbol.payload(), "PKG-9ED9285C");
256        assert_eq!(format!("{}", symbol.kind()), "Code 128");
257    }
258}