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}