smart_package_tracker/symbology/
code128.rs1use alloc::string::{String, ToString};
14use alloc::vec;
15use alloc::vec::Vec;
16
17use super::{BitMatrix, Decoder, Symbol, Symbology, SymbologyKind};
18use crate::error::{Error, Result};
19
20const UPSTREAM_QUIET_ZONE: u32 = 10;
22
23#[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 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 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
86impl Decoder for Code128 {
87 fn kind(&self) -> SymbologyKind {
88 SymbologyKind::Code128
89 }
90
91 fn decode(&self, modules: &BitMatrix) -> Result<String> {
92 if modules.height() != 1 {
93 return Err(Error::Decode(
94 "expected a single-row linear symbol".to_string(),
95 ));
96 }
97
98 let bars = to_bars(modules.row(0));
99 if bars.is_empty() {
100 return Err(Error::Decode("symbol contains no bars".to_string()));
101 }
102
103 let bytes = ::code128::decode(&bars).map_err(|e| Error::Decode(alloc::format!("{e:?}")))?;
104
105 Ok(bytes.into_iter().map(|b| b as char).collect())
108 }
109}
110
111pub fn decode(symbol: &Symbol) -> Result<String> {
123 if symbol.kind() != SymbologyKind::Code128 {
124 return Err(Error::Decode(alloc::format!(
125 "expected a Code 128 symbol, found {}",
126 symbol.kind()
127 )));
128 }
129
130 Code128.decode(symbol.modules())
131}
132
133fn to_bars(row: &[bool]) -> Vec<::code128::Bar> {
135 let mut bars = Vec::new();
136 let mut i = 0;
137
138 while i < row.len() {
139 if !row[i] {
140 i += 1;
141 continue;
142 }
143 let bar_start = i;
144 while i < row.len() && row[i] {
145 i += 1;
146 }
147 let width = (i - bar_start) as u8;
148
149 let space_start = i;
150 while i < row.len() && !row[i] {
151 i += 1;
152 }
153 let space = (i - space_start) as u8;
154
155 bars.push(::code128::Bar { width, space });
156 }
157
158 bars
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use alloc::format;
165
166 const START_A: &str = "11010000100";
168 const START_B: &str = "11010010000";
169 const START_C: &str = "11010011100";
170 const STOP: &str = "1100011101011";
173
174 fn as_bits(symbol: &Symbol) -> String {
175 symbol
176 .modules()
177 .row(0)
178 .iter()
179 .map(|d| if *d { '1' } else { '0' })
180 .collect()
181 }
182
183 #[test]
184 fn starts_with_a_valid_start_pattern() {
185 let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
186 let start = &bits[..11];
187 assert!(
188 start == START_A || start == START_B || start == START_C,
189 "unexpected start pattern {start}"
190 );
191 }
192
193 #[test]
194 fn numeric_payloads_use_the_set_c_start_pattern() {
195 let bits = as_bits(&Code128.encode("1234567890").unwrap());
197 assert_eq!(&bits[..11], START_C, "digit runs should start in Set C");
198 }
199
200 #[test]
201 fn ends_with_the_stop_pattern() {
202 let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
203 assert!(bits.ends_with(STOP), "missing or malformed stop pattern");
204 }
205
206 #[test]
207 fn width_is_a_whole_number_of_symbols_plus_the_stop_bars() {
208 let bits = as_bits(&Code128.encode("PKG-9ED9285C").unwrap());
211 assert_eq!(
212 (bits.len() - 2) % 11,
213 0,
214 "symbol width {} is not 11n + 2",
215 bits.len()
216 );
217 }
218
219 #[test]
220 fn set_c_halves_the_width_of_long_digit_runs() {
221 let digits = "12345678901234567890"; let letters = "ABCDEFGHIJKLMNOPQRST"; let numeric = Code128.encode(digits).unwrap().modules().width();
224 let alpha = Code128.encode(letters).unwrap().modules().width();
225 assert!(
226 numeric < alpha,
227 "Set C compression not applied: {numeric} modules vs {alpha}"
228 );
229 }
230
231 #[test]
232 fn round_trips_through_the_module_grid() {
233 for payload in [
234 "PKG-9ED9285C",
235 "1234567890123456789012",
236 "A",
237 "Mixed 123 Case!",
238 "~$%^&*()_+",
239 ] {
240 let symbol = Code128.encode(payload).unwrap();
241 let decoded = decode(&symbol).unwrap_or_else(|e| panic!("{payload}: {e}"));
242 assert_eq!(decoded, payload);
243 }
244 }
245
246 #[test]
247 fn rejects_an_empty_payload() {
248 assert!(matches!(Code128.encode(""), Err(Error::EmptyPayload)));
249 }
250
251 #[test]
252 fn rejects_characters_outside_latin1() {
253 let err = Code128.encode("PKG-\u{4e2d}\u{6587}").unwrap_err();
254 assert!(matches!(err, Error::Unencodable { .. }), "got {err:?}");
255 }
256
257 #[test]
258 fn quiet_zone_is_not_part_of_the_symbol() {
259 let symbol = Code128.encode("A").unwrap();
260 let row = symbol.modules().row(0);
261 assert!(row[0], "symbol must begin with a bar, not a quiet zone");
262 assert!(
263 *row.last().unwrap(),
264 "symbol must end with a bar, not a quiet zone"
265 );
266 }
267
268 #[test]
269 fn payload_is_preserved_on_the_symbol() {
270 let symbol = Code128.encode("PKG-9ED9285C").unwrap();
271 assert_eq!(symbol.payload(), "PKG-9ED9285C");
272 assert_eq!(format!("{}", symbol.kind()), "Code 128");
273 }
274}