1use std::fmt::Write as _;
5
6use ravenrs_core::{Color, Rect};
7use serde::{Deserialize, Serialize};
8
9use crate::vector::{Shape, append_rectangle};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14#[non_exhaustive]
15#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
16pub enum Symbology {
17 #[default]
19 Code128,
20 Code39,
22 Ean13,
24 Ean8,
26 UpcA,
28 Itf,
30 Qr,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum Pattern {
38 Linear {
40 modules: Vec<bool>,
42 text: String,
44 },
45 Matrix {
47 size: usize,
49 cells: Vec<bool>,
51 },
52}
53
54const QUIET_LINEAR: f32 = 10.0;
56const QUIET_MATRIX: f32 = 4.0;
58
59pub fn encode(symbology: Symbology, data: &str) -> Result<Pattern, String> {
61 match symbology {
62 Symbology::Code128 => code128(data),
63 Symbology::Code39 => code39(data),
64 Symbology::Ean13 => ean13(data),
65 Symbology::Ean8 => ean8(data),
66 Symbology::UpcA => upc_a(data),
67 Symbology::Itf => itf(data),
68 Symbology::Qr => qr(data),
69 }
70}
71
72pub fn modules_across(pattern: &Pattern) -> usize {
75 match pattern {
76 Pattern::Linear { modules, .. } => modules.len() + 2 * QUIET_LINEAR as usize,
77 Pattern::Matrix { size, .. } => size + 2 * QUIET_MATRIX as usize,
78 }
79}
80
81pub fn shape(pattern: &Pattern, rect: Rect, color: Color) -> Shape {
83 let mut path = Vec::new();
84 match pattern {
85 Pattern::Linear { modules, .. } => {
86 let module = rect.width() / (modules.len() as f32 + 2.0 * QUIET_LINEAR);
87 let left = rect.x() + QUIET_LINEAR * module;
88 for (start, length) in runs(modules) {
89 let x = left + start as f32 * module;
90 append_rectangle(&mut path, x, rect.y(), x + length as f32 * module, rect.max_y(), 0.0);
91 }
92 }
93 Pattern::Matrix { size, cells } => {
94 let module = rect.width().min(rect.height()) / (*size as f32 + 2.0 * QUIET_MATRIX);
95 let side = *size as f32 * module;
96 let (left, top) =
97 (rect.x() + (rect.width() - side) / 2.0, rect.y() + (rect.height() - side) / 2.0);
98 for (row, cells) in cells.chunks((*size).max(1)).enumerate() {
99 let y = top + row as f32 * module;
100 for (start, length) in runs(cells) {
101 let x = left + start as f32 * module;
102 append_rectangle(&mut path, x, y, x + length as f32 * module, y + module, 0.0);
103 }
104 }
105 }
106 }
107 Shape::filled(path, color)
108}
109
110fn runs(modules: &[bool]) -> impl Iterator<Item = (usize, usize)> + '_ {
112 let mut at = 0;
113 std::iter::from_fn(move || {
114 while modules.get(at) == Some(&false) {
115 at += 1;
116 }
117 let start = at;
118 while modules.get(at) == Some(&true) {
119 at += 1;
120 }
121 (at > start).then_some((start, at - start))
122 })
123}
124
125fn push_widths(modules: &mut Vec<bool>, widths: &str) {
127 for (index, width) in widths.bytes().enumerate() {
128 modules.extend(std::iter::repeat_n(index % 2 == 0, usize::from(width - b'0')));
129 }
130}
131
132const CODE128: [&str; 107] = [
134 "212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213",
135 "221312", "231212", "112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132",
136 "221231", "213212", "223112", "312131", "311222", "321122", "321221", "312212", "322112", "322211",
137 "212123", "212321", "232121", "111323", "131123", "131321", "112313", "132113", "132311", "211313",
138 "231113", "231311", "112133", "112331", "132131", "113123", "113321", "133121", "313121", "211331",
139 "231131", "213113", "213311", "213131", "311123", "311321", "331121", "312113", "312311", "332111",
140 "314111", "221411", "431111", "111224", "111422", "121124", "121421", "141122", "141221", "112214",
141 "112412", "122114", "122411", "142112", "142211", "241211", "221114", "413111", "241112", "134111",
142 "111242", "121142", "121241", "114212", "124112", "124211", "411212", "421112", "421211", "212141",
143 "214121", "412121", "111143", "111341", "131141", "114113", "114311", "411113", "411311", "113141",
144 "114131", "311141", "411131", "211412", "211214", "211232", "2331112",
145];
146const CODE128_START_B: usize = 104;
147const CODE128_START_C: usize = 105;
148const CODE128_TO_C: usize = 99;
149const CODE128_TO_B: usize = 100;
150const CODE128_STOP: usize = 106;
151const CODE128_DIGITS_FOR_C: usize = 6;
153
154fn code128(data: &str) -> Result<Pattern, String> {
155 if data.is_empty() {
156 return Err("Code 128 needs some text".to_owned());
157 }
158 let bytes = data.as_bytes();
159 if !bytes.iter().all(|b| (32..=126).contains(b)) {
160 return Err("Code 128 takes printable ASCII text".to_owned());
161 }
162 let digits_at = |at: usize| bytes[at..].iter().take_while(|b| b.is_ascii_digit()).count();
163 let mut symbols = Vec::new();
164 let mut in_c = digits_at(0) >= 4 && digits_at(0) % 2 == 0;
165 symbols.push(if in_c { CODE128_START_C } else { CODE128_START_B });
166 let mut at = 0;
167 while at < bytes.len() {
168 let digits = digits_at(at);
169 if in_c {
170 if digits >= 2 {
171 symbols.push(usize::from((bytes[at] - b'0') * 10 + (bytes[at + 1] - b'0')));
172 at += 2;
173 continue;
174 }
175 symbols.push(CODE128_TO_B);
176 in_c = false;
177 } else if digits >= CODE128_DIGITS_FOR_C {
178 if digits % 2 == 1 {
180 symbols.push(usize::from(bytes[at] - 32));
181 at += 1;
182 }
183 symbols.push(CODE128_TO_C);
184 in_c = true;
185 continue;
186 }
187 symbols.push(usize::from(bytes[at] - 32));
188 at += 1;
189 }
190 let check = symbols.iter().enumerate().map(|(i, s)| s * i.max(1)).sum::<usize>() % 103;
191 symbols.push(check);
192 symbols.push(CODE128_STOP);
193 let mut modules = Vec::new();
194 for symbol in symbols {
195 push_widths(&mut modules, CODE128[symbol]);
196 }
197 Ok(Pattern::Linear { modules, text: data.to_owned() })
198}
199
200const CODE39_CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%*";
201const CODE39: [&str; 44] = [
203 "nnnwwnwnn",
204 "wnnwnnnnw",
205 "nnwwnnnnw",
206 "wnwwnnnnn",
207 "nnnwwnnnw",
208 "wnnwwnnnn",
209 "nnwwwnnnn",
210 "nnnwnnwnw",
211 "wnnwnnwnn",
212 "nnwwnnwnn",
213 "wnnnnwnnw",
214 "nnwnnwnnw",
215 "wnwnnwnnn",
216 "nnnnwwnnw",
217 "wnnnwwnnn",
218 "nnwnwwnnn",
219 "nnnnnwwnw",
220 "wnnnnwwnn",
221 "nnwnnwwnn",
222 "nnnnwwwnn",
223 "wnnnnnnww",
224 "nnwnnnnww",
225 "wnwnnnnwn",
226 "nnnnwnnww",
227 "wnnnwnnwn",
228 "nnwnwnnwn",
229 "nnnnnnwww",
230 "wnnnnnwwn",
231 "nnwnnnwwn",
232 "nnnnwnwwn",
233 "wwnnnnnnw",
234 "nwwnnnnnw",
235 "wwwnnnnnn",
236 "nwnnwnnnw",
237 "wwnnwnnnn",
238 "nwwnwnnnn",
239 "nwnnnnwnw",
240 "wwnnnnwnn",
241 "nwwnnnwnn",
242 "nwnwnwnnn",
243 "nwnwnnnwn",
244 "nwnnnwnwn",
245 "nnnwnwnwn",
246 "nwnnwnwnn",
247];
248const CODE39_WIDE: usize = 3;
250
251fn code39(data: &str) -> Result<Pattern, String> {
252 if data.is_empty() {
253 return Err("Code 39 needs some text".to_owned());
254 }
255 let upper = data.to_ascii_uppercase();
256 if let Some(bad) = upper.chars().find(|c| *c == '*' || !CODE39_CHARS.contains(*c)) {
257 return Err(format!("Code 39 does not have '{bad}'"));
258 }
259 let mut modules = Vec::new();
260 for c in std::iter::once('*').chain(upper.chars()).chain(std::iter::once('*')) {
261 let index = CODE39_CHARS.find(c).unwrap_or(0);
262 for (element, kind) in CODE39[index].bytes().enumerate() {
263 let width = if kind == b'w' { CODE39_WIDE } else { 1 };
264 modules.extend(std::iter::repeat_n(element % 2 == 0, width));
265 }
266 modules.push(false);
268 }
269 modules.pop();
270 Ok(Pattern::Linear { modules, text: upper })
271}
272
273fn gs1_check(digits: &[u8]) -> u8 {
275 let sum: u32 =
276 digits.iter().rev().enumerate().map(|(i, d)| u32::from(*d) * if i % 2 == 0 { 3 } else { 1 }).sum();
277 ((10 - sum % 10) % 10) as u8
278}
279
280fn gs1_digits(name: &str, data: &str, length: usize) -> Result<Vec<u8>, String> {
282 if !data.bytes().all(|b| b.is_ascii_digit()) {
283 return Err(format!("{name} takes only digits"));
284 }
285 let mut digits: Vec<u8> = data.bytes().map(|b| b - b'0').collect();
286 if digits.len() == length - 1 {
287 digits.push(gs1_check(&digits));
288 } else if digits.len() != length {
289 return Err(format!("{name} takes {} or {length} digits", length - 1));
290 } else if gs1_check(&digits[..length - 1]) != digits[length - 1] {
291 return Err(format!("{name}: the check digit of {data} is wrong"));
292 }
293 Ok(digits)
294}
295
296const EAN_L: [&str; 10] = ["3211", "2221", "2122", "1411", "1132", "1231", "1114", "1312", "1213", "3112"];
299const EAN13_PARITY: [&str; 10] =
301 ["LLLLLL", "LLGLGG", "LLGGLG", "LLGGGL", "LGLLGG", "LGGLLG", "LGGGLL", "LGLGLG", "LGLGGL", "LGGLGL"];
302
303fn push_symbol(modules: &mut Vec<bool>, widths: &str, starts_dark: bool, reversed: bool) {
304 let mut ordered: Vec<u8> = widths.bytes().collect();
305 if reversed {
306 ordered.reverse();
307 }
308 for (index, width) in ordered.iter().enumerate() {
309 modules.extend(std::iter::repeat_n((index % 2 == 0) == starts_dark, usize::from(width - b'0')));
310 }
311}
312
313fn ean_modules(left: &[u8], parity: &str, right: &[u8]) -> Vec<bool> {
314 let mut modules = vec![true, false, true];
315 for (digit, set) in left.iter().zip(parity.bytes()) {
316 push_symbol(&mut modules, EAN_L[usize::from(*digit)], false, set == b'G');
318 }
319 modules.extend([false, true, false, true, false]);
320 for digit in right {
321 push_symbol(&mut modules, EAN_L[usize::from(*digit)], true, false);
323 }
324 modules.extend([true, false, true]);
325 modules
326}
327
328fn digits_text(digits: &[u8]) -> String {
329 let mut text = String::with_capacity(digits.len());
330 for digit in digits {
331 let _ = write!(text, "{digit}");
332 }
333 text
334}
335
336fn ean13(data: &str) -> Result<Pattern, String> {
337 let digits = gs1_digits("EAN-13", data, 13)?;
338 let modules = ean_modules(&digits[1..7], EAN13_PARITY[usize::from(digits[0])], &digits[7..]);
339 Ok(Pattern::Linear { modules, text: digits_text(&digits) })
340}
341
342fn ean8(data: &str) -> Result<Pattern, String> {
343 let digits = gs1_digits("EAN-8", data, 8)?;
344 let modules = ean_modules(&digits[..4], "LLLL", &digits[4..]);
345 Ok(Pattern::Linear { modules, text: digits_text(&digits) })
346}
347
348fn upc_a(data: &str) -> Result<Pattern, String> {
349 let digits = gs1_digits("UPC-A", data, 12)?;
350 let modules = ean_modules(&digits[..6], "LLLLLL", &digits[6..]);
351 Ok(Pattern::Linear { modules, text: digits_text(&digits) })
352}
353
354const ITF: [&str; 10] =
356 ["nnwwn", "wnnnw", "nwnnw", "wwnnn", "nnwnw", "wnwnn", "nwwnn", "nnnww", "wnnwn", "nwnwn"];
357const ITF_WIDE: usize = 3;
358
359fn itf(data: &str) -> Result<Pattern, String> {
360 if data.is_empty() || !data.bytes().all(|b| b.is_ascii_digit()) {
361 return Err("Interleaved 2 of 5 takes only digits".to_owned());
362 }
363 if !data.len().is_multiple_of(2) {
364 return Err("Interleaved 2 of 5 takes an even number of digits".to_owned());
365 }
366 let mut modules = vec![true, false, true, false];
367 let width = |kind: u8| if kind == b'w' { ITF_WIDE } else { 1 };
368 for pair in data.as_bytes().chunks(2) {
369 let bars = ITF[usize::from(pair[0] - b'0')].as_bytes();
370 let spaces = ITF[usize::from(pair[1] - b'0')].as_bytes();
371 for (bar, space) in bars.iter().zip(spaces) {
372 modules.extend(std::iter::repeat_n(true, width(*bar)));
373 modules.extend(std::iter::repeat_n(false, width(*space)));
374 }
375 }
376 modules.extend(std::iter::repeat_n(true, ITF_WIDE));
377 modules.extend([false, true]);
378 Ok(Pattern::Linear { modules, text: data.to_owned() })
379}
380
381fn qr(data: &str) -> Result<Pattern, String> {
382 if data.is_empty() {
383 return Err("a QR Code needs some text".to_owned());
384 }
385 let code = qrcodegen::QrCode::encode_text(data, qrcodegen::QrCodeEcc::Medium)
386 .map_err(|_| "the text is too long for a QR Code".to_owned())?;
387 let size = code.size();
388 let cells =
389 (0..size).flat_map(|y| (0..size).map(move |x| (x, y))).map(|(x, y)| code.get_module(x, y)).collect();
390 Ok(Pattern::Matrix { size: size as usize, cells })
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use crate::vector::PathSegment;
397
398 fn modules(symbology: Symbology, data: &str) -> Vec<bool> {
399 match encode(symbology, data) {
400 Ok(Pattern::Linear { modules, .. }) => modules,
401 other => panic!("{other:?}"),
402 }
403 }
404
405 fn text(symbology: Symbology, data: &str) -> String {
406 match encode(symbology, data) {
407 Ok(Pattern::Linear { text, .. }) => text,
408 other => panic!("{other:?}"),
409 }
410 }
411
412 fn bits(modules: &[bool]) -> String {
413 modules.iter().map(|m| if *m { '1' } else { '0' }).collect()
414 }
415
416 #[test]
417 fn the_code128_table_is_consistent() {
418 let mut seen = std::collections::HashSet::new();
419 for (value, widths) in CODE128.iter().enumerate() {
420 let total: u32 = widths.bytes().map(|b| u32::from(b - b'0')).sum();
421 assert_eq!(total, if value == CODE128_STOP { 13 } else { 11 }, "{value}");
422 assert!(seen.insert(widths), "{value} is repeated");
423 }
424 }
425
426 #[test]
427 fn code128_has_the_known_check_symbol() {
428 let symbols_end = modules(Symbology::Code128, "PJJ123C");
430 let expected: String = [CODE128_START_B, 48, 42, 42, 17, 18, 19, 35, 55, CODE128_STOP]
431 .iter()
432 .flat_map(|s| {
433 let mut part = Vec::new();
434 push_widths(&mut part, CODE128[*s]);
435 part
436 })
437 .map(|m| if m { '1' } else { '0' })
438 .collect();
439 assert_eq!(bits(&symbols_end), expected);
440 }
441
442 #[test]
443 fn code128_uses_pairs_for_digits() {
444 assert!(
445 modules(Symbology::Code128, "12345678").len() < modules(Symbology::Code128, "1234567A").len()
446 );
447 for data in ["1", "12", "123", "1234", "12345", "A123456", "A1234567", "1234567", "12A34", "123456A"]
449 {
450 assert!(encode(Symbology::Code128, data).is_ok(), "{data}");
451 }
452 }
453
454 #[test]
455 fn check_digits_are_added_or_verified() {
456 assert_eq!(text(Symbology::Ean13, "400638133393"), "4006381333931");
457 assert_eq!(text(Symbology::Ean13, "4006381333931"), "4006381333931");
458 assert!(encode(Symbology::Ean13, "4006381333932").is_err());
459 assert_eq!(text(Symbology::Ean8, "9638507"), "96385074");
460 assert_eq!(text(Symbology::UpcA, "03600029145"), "036000291452");
461 assert!(encode(Symbology::Ean13, "12345").is_err());
462 assert!(encode(Symbology::Ean13, "abcdefghijkl").is_err());
463 }
464
465 #[test]
466 fn gs1_codes_have_their_standard_lengths() {
467 assert_eq!(modules(Symbology::Ean13, "4006381333931").len(), 95);
468 assert_eq!(modules(Symbology::Ean8, "96385074").len(), 67);
469 assert_eq!(modules(Symbology::UpcA, "036000291452").len(), 95);
470 }
471
472 #[test]
473 fn code39_refuses_what_it_does_not_have() {
474 assert_eq!(text(Symbology::Code39, "abc-1"), "ABC-1");
475 assert!(encode(Symbology::Code39, "a*b").is_err());
476 assert!(encode(Symbology::Code39, "ç").is_err());
477 assert!(encode(Symbology::Code39, "").is_err());
478 }
479
480 #[test]
481 fn itf_needs_an_even_number_of_digits() {
482 assert!(encode(Symbology::Itf, "1234").is_ok());
483 assert!(encode(Symbology::Itf, "123").is_err());
484 assert!(encode(Symbology::Itf, "12a4").is_err());
485 }
486
487 #[test]
488 fn qr_codes_are_square_with_finder_patterns() {
489 let Ok(Pattern::Matrix { size, cells }) = encode(Symbology::Qr, "https://example.com") else {
490 panic!()
491 };
492 assert_eq!(cells.len(), size * size);
493 assert_eq!((size - 21) % 4, 0, "a valid version");
494 assert!(cells[..7].iter().all(|c| *c), "top of the finder pattern");
495 assert!(encode(Symbology::Qr, "").is_err());
496 assert!(encode(Symbology::Qr, &"x".repeat(5000)).is_err());
497 }
498
499 #[test]
500 fn shapes_stay_inside_their_box_and_keep_the_quiet_zone() {
501 let rect = Rect::new(10.0, 20.0, 200.0, 50.0);
502 for (symbology, data) in
503 [(Symbology::Code128, "ABC123"), (Symbology::Qr, "x"), (Symbology::Ean13, "400638133393")]
504 {
505 let pattern = encode(symbology, data).map_err(|e| e.to_string());
506 let drawn = shape(&pattern.unwrap_or_else(|e| panic!("{e}")), rect, Color::BLACK);
507 assert!(!drawn.path.is_empty());
508 for segment in &drawn.path {
509 if let PathSegment::Move(p) | PathSegment::Line(p) = segment {
510 assert!(p.x > rect.x() && p.x < rect.max_x() + 0.001, "{p:?}");
511 assert!(p.y >= rect.y() - 0.001 && p.y <= rect.max_y() + 0.001, "{p:?}");
512 }
513 }
514 }
515 }
516}