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//! # Examples
8//!
9//! ## Valid Configuration
10//!
11//! ```
12//! use zedbar::config::*;
13//! use zedbar::DecoderConfig;
14//!
15//! let config = DecoderConfig::new()
16//!     .enable(Ean13)
17//!     .enable(Code39)
18//!     .set_length_limits(Code39, 4, 20)   // ✓ Code39 supports variable length
19//!     .position_tracking(true);
20//! ```
21//!
22//! ## Type-Safe Compile Errors
23//!
24//! The following configurations will NOT compile:
25//!
26//! ```compile_fail
27//! # use zedbar::config::*;
28//! # use zedbar::DecoderConfig;
29//! # let config = DecoderConfig::new();
30//! // ❌ EAN-13 has fixed length, doesn't support length limits
31//! config.set_length_limits(Ean13, 1, 20);
32//! ```
33//!
34//! ```compile_fail
35//! # use zedbar::config::*;
36//! # use zedbar::DecoderConfig;
37//! # let config = DecoderConfig::new();
38//! // ❌ QR codes don't use length limits
39//! config.set_length_limits(QrCode, 1, 100);
40//! ```
41
42use crate::SymbolType;
43use std::collections::{HashMap, HashSet};
44
45pub(crate) mod internal;
46pub mod symbologies;
47
48// Re-export symbology types for convenience
49pub use symbologies::*;
50
51// ============================================================================
52// Configuration Value Types
53// ============================================================================
54
55/// Enable/disable flag for a symbology
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct Enable(pub bool);
58
59/// Add checksum validation
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct AddChecksum(pub bool);
62
63/// Emit checksum in decoded data
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct EmitChecksum(pub bool);
66
67/// Binary mode (for 2D codes like QR)
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct Binary(pub bool);
70
71/// Minimum symbol length
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct MinLength(pub u32);
74
75/// Maximum symbol length
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct MaxLength(pub u32);
78
79/// Edge detection uncertainty threshold
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Uncertainty(pub u32);
82
83/// Position tracking enable/disable
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct PositionTracking(pub bool);
86
87/// Test inverted images
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub struct TestInverted(pub bool);
90
91/// Horizontal scan density
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct XDensity(pub u32);
94
95/// Vertical scan density
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct YDensity(pub u32);
98
99// ============================================================================
100// Capability Traits
101// ============================================================================
102
103/// Marker trait for symbologies that can be enabled/disabled
104pub trait SupportsEnable: Symbology {}
105
106/// Marker trait for symbologies that support checksum configuration
107pub trait SupportsChecksum: Symbology {}
108
109/// Marker trait for symbologies that support variable length limits
110pub trait SupportsLengthLimits: Symbology {}
111
112/// Marker trait for symbologies that support uncertainty configuration
113pub trait SupportsUncertainty: Symbology {}
114
115/// Base trait that all symbology types must implement
116pub trait Symbology: Sized {
117    /// The corresponding SymbolType enum value
118    const TYPE: SymbolType;
119
120    /// Human-readable name
121    const NAME: &'static str;
122}
123
124// ============================================================================
125// User-Facing Configuration Builder
126// ============================================================================
127
128/// Type-safe configuration builder for zedbar decoders
129///
130/// This builder uses the type system to ensure only valid configurations
131/// can be set for each symbology type.
132///
133/// # Example
134/// ```no_run
135/// use zedbar::config::*;
136///
137/// let config = DecoderConfig::new()
138///     .enable(Ean13)
139///     .enable(Code39)
140///     .set_checksum(Code39, true, false)
141///     .set_length_limits(Code39, 4, 20)
142///     .position_tracking(true);
143/// ```
144#[derive(Debug, Clone)]
145pub struct DecoderConfig {
146    /// Which symbologies are enabled
147    pub(crate) enabled: HashSet<SymbolType>,
148
149    /// Checksum configuration: (add_check, emit_check)
150    pub(crate) checksum_flags: HashMap<SymbolType, (bool, bool)>,
151
152    /// Length limits: (min, max)
153    pub(crate) length_limits: HashMap<SymbolType, (u32, u32)>,
154
155    /// Uncertainty threshold per symbology
156    pub(crate) uncertainty: HashMap<SymbolType, u32>,
157
158    /// Global scanner configuration
159    pub(crate) position_tracking: bool,
160    pub(crate) test_inverted: bool,
161    pub(crate) x_density: u32,
162    pub(crate) y_density: u32,
163    pub(crate) retry_undecoded_regions: bool,
164}
165
166impl Default for DecoderConfig {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl DecoderConfig {
173    /// Create a new configuration with sensible defaults
174    ///
175    /// By default:
176    /// - EAN-13, EAN-8, I25, DataBar, Codabar, Code39, Code93, Code128, QR, SQ are enabled
177    /// - UPC-A, UPC-E, ISBN-10, ISBN-13 can be emitted as variants (not separately enabled)
178    /// - Position tracking is enabled
179    /// - Scan density is 1x1
180    /// - Inverted image testing is disabled
181    pub fn new() -> Self {
182        let mut config = Self {
183            enabled: HashSet::new(),
184            checksum_flags: HashMap::new(),
185            length_limits: HashMap::new(),
186            uncertainty: HashMap::new(),
187            position_tracking: true,
188            test_inverted: false,
189            x_density: 1,
190            y_density: 1,
191            retry_undecoded_regions: false,
192        };
193
194        // Enable common symbologies by default
195        config.enabled.insert(SymbolType::Ean13);
196        config.enabled.insert(SymbolType::Ean8);
197        // Note: UPC-A, UPC-E, ISBN-10, ISBN-13 are NOT enabled by default
198        // They can be emitted as variants of EAN-13/EAN-8 when detected
199        config.enabled.insert(SymbolType::I25);
200        config.enabled.insert(SymbolType::Databar);
201        config.enabled.insert(SymbolType::DatabarExp);
202        config.enabled.insert(SymbolType::Codabar);
203        config.enabled.insert(SymbolType::Code39);
204        config.enabled.insert(SymbolType::Code93);
205        config.enabled.insert(SymbolType::Code128);
206        config.enabled.insert(SymbolType::QrCode);
207        config.enabled.insert(SymbolType::SqCode);
208
209        // Set default checksum behavior for EAN/UPC
210        // EAN-13 and EAN-8 are enabled with emit_check
211        for sym in [SymbolType::Ean13, SymbolType::Ean8] {
212            config.checksum_flags.insert(sym, (false, true)); // don't add, do emit
213        }
214
215        // UPC-A, UPC-E, ISBN variants are not enabled but have emit_check
216        // This allows them to be reported as variants when EAN decoding detects them
217        for sym in [
218            SymbolType::Upca,
219            SymbolType::Upce,
220            SymbolType::Isbn10,
221            SymbolType::Isbn13,
222        ] {
223            config.checksum_flags.insert(sym, (false, true)); // don't add, do emit
224        }
225
226        // Set default length limits for variable-length symbologies
227        config.length_limits.insert(SymbolType::I25, (6, 256));
228        config.length_limits.insert(SymbolType::Codabar, (4, 256));
229        config.length_limits.insert(SymbolType::Code39, (1, 256));
230
231        // Set default uncertainty values
232        config.uncertainty.insert(SymbolType::Codabar, 1);
233
234        config
235    }
236
237    // ========================================================================
238    // Per-Symbology Configuration
239    // ========================================================================
240
241    /// Enable a symbology
242    pub fn enable<S: Symbology + SupportsEnable>(mut self, _: S) -> Self {
243        self.enabled.insert(S::TYPE);
244        self
245    }
246
247    /// Disable a symbology
248    pub fn disable<S: Symbology + SupportsEnable>(mut self, _: S) -> Self {
249        self.enabled.remove(&S::TYPE);
250        self
251    }
252
253    /// Disable all symbologies
254    ///
255    /// Useful when you want to start with a clean slate and only enable
256    /// specific symbologies.
257    ///
258    /// # Example
259    /// ```
260    /// use zedbar::config::*;
261    /// use zedbar::DecoderConfig;
262    ///
263    /// let config = DecoderConfig::new()
264    ///     .disable_all()
265    ///     .enable(QrCode)
266    ///     .enable(Code39);
267    /// ```
268    pub fn disable_all(mut self) -> Self {
269        self.enabled.clear();
270        self
271    }
272
273    /// Check if a symbology is enabled
274    pub fn is_enabled(&self, sym: SymbolType) -> bool {
275        self.enabled.contains(&sym)
276    }
277
278    /// Configure checksum behavior for a symbology
279    ///
280    /// # Arguments
281    /// * `add_check` - Validate checksum during decoding
282    /// * `emit_check` - Include checksum digit in decoded data
283    pub fn set_checksum<S: Symbology + SupportsChecksum>(
284        mut self,
285        _: S,
286        add_check: bool,
287        emit_check: bool,
288    ) -> Self {
289        self.checksum_flags.insert(S::TYPE, (add_check, emit_check));
290        self
291    }
292
293    /// Set minimum and maximum length limits
294    ///
295    /// Only valid for variable-length symbologies like Code39, Code128, etc.
296    pub fn set_length_limits<S: Symbology + SupportsLengthLimits>(
297        mut self,
298        _: S,
299        min: u32,
300        max: u32,
301    ) -> Self {
302        assert!(min <= max, "min length must be <= max length");
303        assert!(max <= 256, "max length must be <= 256");
304        self.length_limits.insert(S::TYPE, (min, max));
305        self
306    }
307
308    /// Set uncertainty threshold for edge detection
309    ///
310    /// Higher values are more tolerant of poor quality images but may
311    /// produce more false positives.
312    pub fn set_uncertainty<S: Symbology + SupportsUncertainty>(
313        mut self,
314        _: S,
315        threshold: u32,
316    ) -> Self {
317        self.uncertainty.insert(S::TYPE, threshold);
318        self
319    }
320
321    // ========================================================================
322    // Global Scanner Configuration
323    // ========================================================================
324
325    /// Enable or disable position tracking
326    ///
327    /// When enabled, the scanner records the pixel coordinates of each
328    /// detected symbol.
329    pub fn position_tracking(mut self, enabled: bool) -> Self {
330        self.position_tracking = enabled;
331        self
332    }
333
334    /// Enable or disable inverted image testing
335    ///
336    /// When enabled, if no symbols are found in the normal image, the
337    /// scanner will try again with an inverted (negative) image.
338    pub fn test_inverted(mut self, enabled: bool) -> Self {
339        self.test_inverted = enabled;
340        self
341    }
342
343    /// Set scan density for both axes
344    ///
345    /// Higher density means more scan lines, which improves detection
346    /// but increases processing time. A value of 1 means scan every line.
347    pub fn scan_density(mut self, x: u32, y: u32) -> Self {
348        assert!(x > 0, "x density must be > 0");
349        assert!(y > 0, "y density must be > 0");
350        self.x_density = x;
351        self.y_density = y;
352        self
353    }
354
355    /// Automatically retry undecoded QR finder regions by cropping and
356    /// upscaling them.
357    ///
358    /// When enabled, if the initial scan detects QR finder patterns but
359    /// fails to decode the QR code (e.g. because it is too small), the
360    /// scanner will crop each undecoded region with padding, upscale it
361    /// at several resolutions, and re-scan. Decoded symbol coordinates
362    /// are mapped back to the original image frame.
363    ///
364    /// Default: `false`.
365    pub fn retry_undecoded_regions(mut self, enabled: bool) -> Self {
366        self.retry_undecoded_regions = enabled;
367        self
368    }
369
370    /// Set horizontal scan density
371    pub fn x_density(mut self, density: u32) -> Self {
372        assert!(density > 0, "density must be > 0");
373        self.x_density = density;
374        self
375    }
376
377    /// Set vertical scan density
378    pub fn y_density(mut self, density: u32) -> Self {
379        assert!(density > 0, "density must be > 0");
380        self.y_density = density;
381        self
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_default_config() {
391        let config = DecoderConfig::new();
392        assert!(config.is_enabled(SymbolType::Ean13));
393        assert!(config.is_enabled(SymbolType::Code39));
394        assert!(config.position_tracking);
395        assert!(!config.test_inverted);
396        assert_eq!(config.x_density, 1);
397        assert_eq!(config.y_density, 1);
398    }
399
400    #[test]
401    fn test_builder_pattern() {
402        let config = DecoderConfig::new()
403            .enable(Ean13)
404            .disable(Code39)
405            .set_checksum(Code39, true, false)
406            .position_tracking(false)
407            .scan_density(2, 2);
408
409        assert!(config.is_enabled(SymbolType::Ean13));
410        assert!(!config.is_enabled(SymbolType::Code39));
411        assert!(!config.position_tracking);
412        assert_eq!(config.x_density, 2);
413        assert_eq!(config.y_density, 2);
414    }
415
416    #[test]
417    fn test_type_safe_length_limits() {
418        // Variable-length symbologies can have length limits
419        let config = DecoderConfig::new()
420            .set_length_limits(Code39, 5, 20)
421            .set_length_limits(Code128, 1, 50)
422            .set_length_limits(I25, 6, 30);
423
424        assert_eq!(
425            config.length_limits.get(&SymbolType::Code39),
426            Some(&(5, 20))
427        );
428        assert_eq!(
429            config.length_limits.get(&SymbolType::Code128),
430            Some(&(1, 50))
431        );
432        assert_eq!(config.length_limits.get(&SymbolType::I25), Some(&(6, 30)));
433    }
434
435    #[test]
436    fn test_checksum_configuration() {
437        let config = DecoderConfig::new()
438            .set_checksum(Codabar, true, false) // validate but don't emit
439            .set_checksum(Ean13, false, true); // don't validate, do emit
440
441        assert_eq!(
442            config.checksum_flags.get(&SymbolType::Codabar),
443            Some(&(true, false))
444        );
445        assert_eq!(
446            config.checksum_flags.get(&SymbolType::Ean13),
447            Some(&(false, true))
448        );
449    }
450
451    #[test]
452    fn test_uncertainty_configuration() {
453        let config = DecoderConfig::new()
454            .set_uncertainty(Code39, 2)
455            .set_uncertainty(QrCode, 0)
456            .set_uncertainty(Codabar, 1);
457
458        assert_eq!(config.uncertainty.get(&SymbolType::Code39), Some(&2));
459        assert_eq!(config.uncertainty.get(&SymbolType::QrCode), Some(&0));
460        assert_eq!(config.uncertainty.get(&SymbolType::Codabar), Some(&1));
461    }
462
463    #[test]
464    fn test_config_to_state_conversion() {
465        let config = DecoderConfig::new()
466            .enable(Ean13)
467            .set_checksum(Ean13, false, true)
468            .position_tracking(true)
469            .scan_density(2, 3);
470
471        let state: internal::DecoderState = (&config).into();
472
473        // Verify EAN is enabled
474        assert!(state.is_enabled(SymbolType::Ean13));
475        assert!(state.ean_enabled());
476
477        // Verify checksum config
478        let ean13_config = state.get(SymbolType::Ean13).unwrap();
479        assert!(!ean13_config.checksum.add_check);
480        assert!(ean13_config.checksum.emit_check);
481
482        // Verify scanner config
483        assert_eq!(state.scanner.x_density, 2);
484        assert_eq!(state.scanner.y_density, 3);
485        assert!(state.scanner.position_tracking);
486    }
487}