Skip to main content

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}