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