zedbar/config.rs
1//! Type-safe configuration system for Zedbar decoders
2//!
3//! This module provides a compile-time verified configuration API that prevents
4//! invalid configuration combinations. The type system ensures you can only
5//! set configurations that are valid for each symbology.
6//!
7//! # Choosing a starting point
8//!
9//! [`DecoderConfig::new()`] starts empty — you opt in to each symbology you
10//! want. [`DecoderConfig::all()`] starts with every supported symbology
11//! enabled, for callers (CLI tools, exploratory scripts) that want to scan
12//! anything they can find.
13//!
14//! # Examples
15//!
16//! ## Opt-in (recommended)
17//!
18//! ```
19//! use zedbar::config::*;
20//! use zedbar::DecoderConfig;
21//!
22//! let config = DecoderConfig::new()
23//! .enable(Ean13)
24//! .enable(Code39)
25//! .set_length_limits(Code39, 4, 20) // ✓ Code39 supports variable length
26//! .position_tracking(true);
27//! ```
28//!
29//! ## Kitchen sink
30//!
31//! ```
32//! use zedbar::config::*;
33//! use zedbar::DecoderConfig;
34//!
35//! let config = DecoderConfig::all();
36//! ```
37//!
38//! ## Type-Safe Compile Errors
39//!
40//! The following configurations will NOT compile:
41//!
42//! ```compile_fail
43//! # use zedbar::config::*;
44//! # use zedbar::DecoderConfig;
45//! # let config = DecoderConfig::new();
46//! // ❌ EAN-13 has fixed length, doesn't support length limits
47//! config.set_length_limits(Ean13, 1, 20);
48//! ```
49//!
50//! ```compile_fail
51//! # use zedbar::config::*;
52//! # use zedbar::DecoderConfig;
53//! # let config = DecoderConfig::new();
54//! // ❌ QR codes don't use length limits
55//! config.set_length_limits(QrCode, 1, 100);
56//! ```
57
58use crate::SymbolType;
59use std::collections::{HashMap, HashSet};
60
61pub(crate) mod internal;
62pub mod symbologies;
63
64// Re-export symbology types for convenience
65pub use symbologies::*;
66
67// ============================================================================
68// Configuration Value Types
69// ============================================================================
70
71/// Enable/disable flag for a symbology
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct Enable(pub bool);
74
75/// Add checksum validation
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct AddChecksum(pub bool);
78
79/// Emit checksum in decoded data
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct EmitChecksum(pub bool);
82
83/// Binary mode (for 2D codes like QR)
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Binary(pub bool);
86
87/// Minimum symbol length
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct MinLength(pub u32);
90
91/// Maximum symbol length
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct MaxLength(pub u32);
94
95/// Edge detection uncertainty threshold
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct Uncertainty(pub u32);
98
99/// Position tracking enable/disable
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct PositionTracking(pub bool);
102
103/// Test inverted images
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct TestInverted(pub bool);
106
107/// Horizontal scan density
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct XDensity(pub u32);
110
111/// Vertical scan density
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct YDensity(pub u32);
114
115// ============================================================================
116// Capability Traits
117// ============================================================================
118
119/// Marker trait for symbologies that can be enabled/disabled
120pub trait SupportsEnable: Symbology {}
121
122/// Marker trait for symbologies that support checksum configuration
123pub trait SupportsChecksum: Symbology {}
124
125/// Marker trait for symbologies that support variable length limits
126pub trait SupportsLengthLimits: Symbology {}
127
128/// Marker trait for symbologies that support uncertainty configuration
129pub trait SupportsUncertainty: Symbology {}
130
131/// Base trait that all symbology types must implement
132pub trait Symbology: Sized {
133 /// The corresponding SymbolType enum value
134 const TYPE: SymbolType;
135
136 /// Human-readable name
137 const NAME: &'static str;
138}
139
140// ============================================================================
141// User-Facing Configuration Builder
142// ============================================================================
143
144/// Type-safe configuration builder for zedbar decoders
145///
146/// This builder uses the type system to ensure only valid configurations
147/// can be set for each symbology type.
148///
149/// Start from [`DecoderConfig::new()`] (empty — opt in to each symbology
150/// you want) or [`DecoderConfig::all()`] (full kitchen-sink for exploratory
151/// use).
152///
153/// # Example
154/// ```no_run
155/// use zedbar::config::*;
156/// use zedbar::DecoderConfig;
157///
158/// let config = DecoderConfig::new()
159/// .enable(Ean13)
160/// .enable(Code39)
161/// .set_checksum(Code39, true, false)
162/// .set_length_limits(Code39, 4, 20)
163/// .position_tracking(true);
164/// ```
165#[derive(Debug, Clone)]
166pub struct DecoderConfig {
167 /// Which symbologies are enabled
168 pub(crate) enabled: HashSet<SymbolType>,
169
170 /// Checksum configuration: (add_check, emit_check)
171 pub(crate) checksum_flags: HashMap<SymbolType, (bool, bool)>,
172
173 /// Length limits: (min, max)
174 pub(crate) length_limits: HashMap<SymbolType, (u32, u32)>,
175
176 /// Uncertainty threshold per symbology
177 pub(crate) uncertainty: HashMap<SymbolType, u32>,
178
179 /// Global scanner configuration
180 pub(crate) position_tracking: bool,
181 pub(crate) test_inverted: bool,
182 pub(crate) x_density: u32,
183 pub(crate) y_density: u32,
184 pub(crate) retry_undecoded_regions: bool,
185}
186
187impl DecoderConfig {
188 /// Create an empty configuration with no symbologies enabled.
189 ///
190 /// Opt in to each symbology you want via [`enable`](Self::enable); only
191 /// the decoders you ask for will run.
192 ///
193 /// Global scanner settings (position tracking, scan density, etc.) are
194 /// initialized to sensible defaults; per-symbology config (length limits,
195 /// checksum behavior, uncertainty) for variants like UPC-A, UPC-E,
196 /// ISBN-10, and ISBN-13 is preconfigured so that enabling them produces
197 /// reasonable output.
198 ///
199 /// # Example
200 /// ```
201 /// use zedbar::config::*;
202 /// use zedbar::DecoderConfig;
203 ///
204 /// let config = DecoderConfig::new()
205 /// .enable(QrCode);
206 /// ```
207 #[allow(
208 clippy::new_without_default,
209 reason = "`::new()` intentionally omits all symbologies, but that is unergonomic as a `Default::default` implementation"
210 )]
211 pub fn new() -> Self {
212 let mut config = Self {
213 enabled: HashSet::new(),
214 checksum_flags: HashMap::new(),
215 length_limits: HashMap::new(),
216 uncertainty: HashMap::new(),
217 position_tracking: true,
218 test_inverted: false,
219 x_density: 1,
220 y_density: 1,
221 retry_undecoded_regions: false,
222 };
223
224 // Preconfigure per-symbology defaults so that enabling a symbology
225 // gives reasonable behavior out of the box. These have no effect
226 // until the corresponding symbology is enabled.
227
228 // EAN-13 and EAN-8: emit checksum
229 for sym in [SymbolType::Ean13, SymbolType::Ean8] {
230 config.checksum_flags.insert(sym, (false, true));
231 }
232
233 // UPC-A, UPC-E, ISBN-10, ISBN-13: emit checksum when surfaced as
234 // EAN variants
235 for sym in [
236 SymbolType::Upca,
237 SymbolType::Upce,
238 SymbolType::Isbn10,
239 SymbolType::Isbn13,
240 ] {
241 config.checksum_flags.insert(sym, (false, true));
242 }
243
244 // Default length limits for variable-length symbologies
245 config.length_limits.insert(SymbolType::I25, (6, 256));
246 config.length_limits.insert(SymbolType::Codabar, (4, 256));
247 config.length_limits.insert(SymbolType::Code39, (1, 256));
248
249 // Default uncertainty values
250 config.uncertainty.insert(SymbolType::Codabar, 1);
251
252 config
253 }
254
255 /// Create a configuration with the full set of supported symbologies
256 /// enabled.
257 ///
258 /// Enables EAN-13, EAN-8, I2/5, DataBar, DataBar-Expanded, Codabar,
259 /// Code 39, Code 93, Code 128, QR Code, and SQ Code. UPC-A, UPC-E,
260 /// ISBN-10, and ISBN-13 are *not* enabled — they can be opted in as
261 /// variant labels of EAN-13/EAN-8 via [`enable`](Self::enable).
262 ///
263 /// Prefer [`new()`](Self::new) and explicit [`enable`](Self::enable)
264 /// calls when you know which formats you need.
265 ///
266 /// # Example
267 /// ```
268 /// use zedbar::DecoderConfig;
269 ///
270 /// let config = DecoderConfig::all();
271 /// ```
272 pub fn all() -> Self {
273 let mut config = Self::new();
274 config.enabled.insert(SymbolType::Ean13);
275 config.enabled.insert(SymbolType::Ean8);
276 config.enabled.insert(SymbolType::I25);
277 config.enabled.insert(SymbolType::Databar);
278 config.enabled.insert(SymbolType::DatabarExp);
279 config.enabled.insert(SymbolType::Codabar);
280 config.enabled.insert(SymbolType::Code39);
281 config.enabled.insert(SymbolType::Code93);
282 config.enabled.insert(SymbolType::Code128);
283 config.enabled.insert(SymbolType::QrCode);
284 config.enabled.insert(SymbolType::SqCode);
285 config
286 }
287
288 // ========================================================================
289 // Per-Symbology Configuration
290 // ========================================================================
291
292 /// Enable a symbology
293 pub fn enable<S: Symbology + SupportsEnable>(mut self, _: S) -> Self {
294 self.enabled.insert(S::TYPE);
295 self
296 }
297
298 /// Disable a symbology
299 pub fn disable<S: Symbology + SupportsEnable>(mut self, _: S) -> Self {
300 self.enabled.remove(&S::TYPE);
301 self
302 }
303
304 /// Enable a symbology by its runtime [`SymbolType`].
305 ///
306 /// Prefer [`enable`](Self::enable) when the symbology is known at
307 /// compile time — it carries the [`SupportsEnable`] capability bound.
308 /// This runtime variant is for callers parsing config from strings,
309 /// CLI flags, or other dynamic sources.
310 pub fn enable_type(mut self, sym: SymbolType) -> Self {
311 self.enabled.insert(sym);
312 self
313 }
314
315 /// Check if a symbology is enabled
316 pub fn is_enabled(&self, sym: SymbolType) -> bool {
317 self.enabled.contains(&sym)
318 }
319
320 /// Configure checksum behavior for a symbology, enabling it if not
321 /// already enabled.
322 ///
323 /// # Arguments
324 /// * `add_check` - Validate checksum during decoding
325 /// * `emit_check` - Include checksum digit in decoded data
326 pub fn set_checksum<S: Symbology + SupportsChecksum>(
327 mut self,
328 _: S,
329 add_check: bool,
330 emit_check: bool,
331 ) -> Self {
332 self.enabled.insert(S::TYPE);
333 self.checksum_flags.insert(S::TYPE, (add_check, emit_check));
334 self
335 }
336
337 /// Set minimum and maximum length limits, enabling the symbology if not
338 /// already enabled.
339 ///
340 /// Only valid for variable-length symbologies like Code39, Code128, etc.
341 pub fn set_length_limits<S: Symbology + SupportsLengthLimits>(
342 mut self,
343 _: S,
344 min: u32,
345 max: u32,
346 ) -> Self {
347 assert!(min <= max, "min length must be <= max length");
348 assert!(max <= 256, "max length must be <= 256");
349 self.enabled.insert(S::TYPE);
350 self.length_limits.insert(S::TYPE, (min, max));
351 self
352 }
353
354 /// Set uncertainty threshold for edge detection, enabling the symbology
355 /// if not already enabled.
356 ///
357 /// Higher values are more tolerant of poor quality images but may
358 /// produce more false positives.
359 pub fn set_uncertainty<S: Symbology + SupportsUncertainty>(
360 mut self,
361 _: S,
362 threshold: u32,
363 ) -> Self {
364 self.enabled.insert(S::TYPE);
365 self.uncertainty.insert(S::TYPE, threshold);
366 self
367 }
368
369 // ========================================================================
370 // Global Scanner Configuration
371 // ========================================================================
372
373 /// Enable or disable position tracking
374 ///
375 /// When enabled, the scanner records the pixel coordinates of each
376 /// detected symbol.
377 pub fn position_tracking(mut self, enabled: bool) -> Self {
378 self.position_tracking = enabled;
379 self
380 }
381
382 /// Enable or disable inverted image testing
383 ///
384 /// When enabled, if no symbols are found in the normal image, the
385 /// scanner will try again with an inverted (negative) image.
386 pub fn test_inverted(mut self, enabled: bool) -> Self {
387 self.test_inverted = enabled;
388 self
389 }
390
391 /// Set scan density for both axes
392 ///
393 /// Higher density means more scan lines, which improves detection
394 /// but increases processing time. A value of 1 means scan every line.
395 pub fn scan_density(mut self, x: u32, y: u32) -> Self {
396 assert!(x > 0, "x density must be > 0");
397 assert!(y > 0, "y density must be > 0");
398 self.x_density = x;
399 self.y_density = y;
400 self
401 }
402
403 /// Automatically retry undecoded QR finder regions by cropping and
404 /// upscaling them.
405 ///
406 /// When enabled, if the initial scan detects QR finder patterns but
407 /// fails to decode the QR code (e.g. because it is too small), the
408 /// scanner will crop each undecoded region with padding, upscale it
409 /// at several resolutions, and re-scan. Decoded symbol coordinates
410 /// are mapped back to the original image frame.
411 ///
412 /// Default: `false`.
413 pub fn retry_undecoded_regions(mut self, enabled: bool) -> Self {
414 self.retry_undecoded_regions = enabled;
415 self
416 }
417
418 /// Set horizontal scan density
419 pub fn x_density(mut self, density: u32) -> Self {
420 assert!(density > 0, "density must be > 0");
421 self.x_density = density;
422 self
423 }
424
425 /// Set vertical scan density
426 pub fn y_density(mut self, density: u32) -> Self {
427 assert!(density > 0, "density must be > 0");
428 self.y_density = density;
429 self
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_new_is_empty() {
439 let config = DecoderConfig::new();
440 for sym in SymbolType::ALL.iter() {
441 assert!(
442 !config.is_enabled(*sym),
443 "{sym:?} should be disabled in DecoderConfig::new()",
444 );
445 }
446 assert!(config.position_tracking);
447 assert!(!config.test_inverted);
448 assert_eq!(config.x_density, 1);
449 assert_eq!(config.y_density, 1);
450 }
451
452 #[test]
453 fn test_all_enables_kitchen_sink() {
454 let config = DecoderConfig::all();
455 for sym in [
456 SymbolType::Ean13,
457 SymbolType::Ean8,
458 SymbolType::I25,
459 SymbolType::Databar,
460 SymbolType::DatabarExp,
461 SymbolType::Codabar,
462 SymbolType::Code39,
463 SymbolType::Code93,
464 SymbolType::Code128,
465 SymbolType::QrCode,
466 SymbolType::SqCode,
467 ] {
468 assert!(
469 config.is_enabled(sym),
470 "{sym:?} should be enabled in ::all()"
471 );
472 }
473 // UPC/ISBN variants stay opt-in.
474 for sym in [
475 SymbolType::Upca,
476 SymbolType::Upce,
477 SymbolType::Isbn10,
478 SymbolType::Isbn13,
479 ] {
480 assert!(!config.is_enabled(sym));
481 }
482 }
483
484 #[test]
485 fn test_builder_pattern() {
486 let config = DecoderConfig::all()
487 .enable(Ean13)
488 .disable(Code39)
489 .set_checksum(Code39, true, false) // re-enables Code39
490 .disable(Code39)
491 .position_tracking(false)
492 .scan_density(2, 2);
493
494 assert!(config.is_enabled(SymbolType::Ean13));
495 assert!(!config.is_enabled(SymbolType::Code39));
496 assert!(!config.position_tracking);
497 assert_eq!(config.x_density, 2);
498 assert_eq!(config.y_density, 2);
499 }
500
501 #[test]
502 fn test_setters_auto_enable() {
503 let config = DecoderConfig::new()
504 .set_length_limits(Code39, 4, 20)
505 .set_checksum(Codabar, true, false)
506 .set_uncertainty(QrCode, 2);
507 assert!(config.is_enabled(SymbolType::Code39));
508 assert!(config.is_enabled(SymbolType::Codabar));
509 assert!(config.is_enabled(SymbolType::QrCode));
510 }
511
512 #[test]
513 fn test_type_safe_length_limits() {
514 // Variable-length symbologies can have length limits
515 let config = DecoderConfig::new()
516 .set_length_limits(Code39, 5, 20)
517 .set_length_limits(Code128, 1, 50)
518 .set_length_limits(I25, 6, 30);
519
520 assert_eq!(
521 config.length_limits.get(&SymbolType::Code39),
522 Some(&(5, 20))
523 );
524 assert_eq!(
525 config.length_limits.get(&SymbolType::Code128),
526 Some(&(1, 50))
527 );
528 assert_eq!(config.length_limits.get(&SymbolType::I25), Some(&(6, 30)));
529 }
530
531 #[test]
532 fn test_checksum_configuration() {
533 let config = DecoderConfig::new()
534 .set_checksum(Codabar, true, false) // validate but don't emit
535 .set_checksum(Ean13, false, true); // don't validate, do emit
536
537 assert_eq!(
538 config.checksum_flags.get(&SymbolType::Codabar),
539 Some(&(true, false))
540 );
541 assert_eq!(
542 config.checksum_flags.get(&SymbolType::Ean13),
543 Some(&(false, true))
544 );
545 }
546
547 #[test]
548 fn test_uncertainty_configuration() {
549 let config = DecoderConfig::new()
550 .set_uncertainty(Code39, 2)
551 .set_uncertainty(QrCode, 0)
552 .set_uncertainty(Codabar, 1);
553
554 assert_eq!(config.uncertainty.get(&SymbolType::Code39), Some(&2));
555 assert_eq!(config.uncertainty.get(&SymbolType::QrCode), Some(&0));
556 assert_eq!(config.uncertainty.get(&SymbolType::Codabar), Some(&1));
557 }
558
559 #[test]
560 fn test_config_to_state_conversion() {
561 let config = DecoderConfig::new()
562 .enable(Ean13)
563 .set_checksum(Ean13, false, true)
564 .position_tracking(true)
565 .scan_density(2, 3);
566
567 let state: internal::DecoderState = (&config).into();
568
569 // Verify EAN is enabled
570 assert!(state.is_enabled(SymbolType::Ean13));
571 assert!(state.ean_enabled());
572
573 // Verify checksum config
574 let ean13_config = state.get(SymbolType::Ean13).unwrap();
575 assert!(!ean13_config.checksum.add_check);
576 assert!(ean13_config.checksum.emit_check);
577
578 // Verify scanner config
579 assert_eq!(state.scanner.x_density, 2);
580 assert_eq!(state.scanner.y_density, 3);
581 assert!(state.scanner.position_tracking);
582 }
583}