Skip to main content

zedbar/
scanner.rs

1//! Image scanner for finding barcodes in 2D images
2//!
3//! The [`Scanner`] is the main entry point for barcode scanning operations.
4//! It processes grayscale images to detect and decode barcodes.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use zedbar::{Image, Scanner};
10//!
11//! // Create a scanner with every supported symbology enabled.
12//! let mut scanner = Scanner::new();
13//!
14//! // Or build a scanner with only the symbologies you need.
15//! use zedbar::{DecoderConfig, config::*};
16//! let config = DecoderConfig::new()
17//!     .enable(QrCode)
18//!     .enable(Ean13);
19//! let mut scanner = Scanner::with_config(config);
20//!
21//! // Scan an image
22//! # let data = vec![0u8; 640 * 480];
23//! let mut image = Image::from_gray(&data, 640, 480).unwrap();
24//! let result = scanner.scan(&mut image);
25//!
26//! for symbol in &result {
27//!     println!("{:?}: {:?}", symbol.symbol_type(), symbol.data_string());
28//! }
29//! ```
30
31use crate::config::DecoderConfig;
32use crate::image::Image;
33use crate::img_scanner::ImageScanner;
34use crate::symbol::Symbol;
35
36/// A region where QR finder patterns were detected but decoding failed.
37///
38/// The bounding box describes the area in the original image where
39/// finder pattern lines were found. To attempt decoding, crop the
40/// image to this region (with padding for the quiet zone) and
41/// upscale before re-scanning.
42///
43/// # Example
44///
45/// ```no_run
46/// # use zedbar::{Image, Scanner};
47/// # let data = vec![0u8; 800 * 600];
48/// # let mut image = Image::from_gray(&data, 800, 600).unwrap();
49/// # let mut scanner = Scanner::new();
50/// let result = scanner.scan(&mut image);
51///
52/// for region in result.finder_regions() {
53///     let pad = region.width.max(region.height) / 2;
54///     let x = region.x.saturating_sub(pad);
55///     let y = region.y.saturating_sub(pad);
56///     let w = (region.width + 2 * pad).min(image.width() - x);
57///     let h = (region.height + 2 * pad).min(image.height() - y);
58///
59///     if let Some(cropped) = image.crop(x, y, w, h) {
60///         if let Some(mut upscaled) = cropped.upscale(4) {
61///             let retry = scanner.scan(&mut upscaled);
62///             for symbol in retry.symbols() {
63///                 println!("Recovered: {}", symbol.data_string().unwrap_or(""));
64///             }
65///         }
66///     }
67/// }
68/// ```
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct FinderRegion {
71    /// X coordinate of the top-left corner (pixels).
72    pub x: u32,
73    /// Y coordinate of the top-left corner (pixels).
74    pub y: u32,
75    /// Width of the bounding box (pixels).
76    pub width: u32,
77    /// Height of the bounding box (pixels).
78    pub height: u32,
79}
80
81/// Result of scanning an image for barcodes.
82///
83/// Contains decoded symbols and metadata about regions where
84/// QR finder patterns were detected but decoding failed.
85///
86/// `ScanResult` implements [`Deref<Target = [Symbol]>`](std::ops::Deref) and
87/// [`IntoIterator`], so most code that previously worked with `Vec<Symbol>`
88/// continues to work unchanged.
89pub struct ScanResult {
90    symbols: Vec<Symbol>,
91    finder_regions: Vec<FinderRegion>,
92}
93
94impl ScanResult {
95    pub(crate) fn new(symbols: Vec<Symbol>, finder_regions: Vec<FinderRegion>) -> Self {
96        Self {
97            symbols,
98            finder_regions,
99        }
100    }
101
102    /// The decoded barcode symbols.
103    pub fn symbols(&self) -> &[Symbol] {
104        &self.symbols
105    }
106
107    /// Consumes the result and returns the decoded symbols.
108    pub fn into_symbols(self) -> Vec<Symbol> {
109        self.symbols
110    }
111
112    /// Regions where QR finder patterns were detected but no QR code
113    /// was successfully decoded.
114    ///
115    /// Each entry is a separate cluster of finder patterns — typically
116    /// one per undecoded QR code in the image. Cropping and upscaling
117    /// each region may yield successful decodes.
118    ///
119    /// Empty when no undecoded regions were found, or when the `qrcode`
120    /// feature is disabled.
121    pub fn finder_regions(&self) -> &[FinderRegion] {
122        &self.finder_regions
123    }
124}
125
126// Backward-compatible: `for symbol in scanner.scan(&mut img)` still works.
127impl IntoIterator for ScanResult {
128    type Item = Symbol;
129    type IntoIter = std::vec::IntoIter<Symbol>;
130
131    fn into_iter(self) -> Self::IntoIter {
132        self.symbols.into_iter()
133    }
134}
135
136impl<'a> IntoIterator for &'a ScanResult {
137    type Item = &'a Symbol;
138    type IntoIter = std::slice::Iter<'a, Symbol>;
139
140    fn into_iter(self) -> Self::IntoIter {
141        self.symbols.iter()
142    }
143}
144
145// Backward-compatible: `result.is_empty()`, `result.len()`, indexing all work.
146impl std::ops::Deref for ScanResult {
147    type Target = [Symbol];
148    fn deref(&self) -> &[Symbol] {
149        &self.symbols
150    }
151}
152
153/// Image scanner that can find barcodes in 2D images
154///
155/// # Example
156/// ```no_run
157/// use zedbar::config::*;
158/// use zedbar::{Scanner, DecoderConfig, Image};
159///
160/// // Create scanner with type-safe configuration
161/// let config = DecoderConfig::new()
162///     .enable(Ean13)
163///     .enable(QrCode)
164///     .position_tracking(true)
165///     .scan_density(1, 1);
166///
167/// let mut scanner = Scanner::with_config(config);
168///
169/// // Scan an image
170/// let data = vec![0u8; 640 * 480];
171/// let mut image = Image::from_gray(&data, 640, 480).unwrap();
172/// let result = scanner.scan(&mut image);
173/// ```
174pub struct Scanner {
175    scanner: ImageScanner,
176    retry_undecoded_regions: bool,
177}
178
179impl Scanner {
180    /// Create a new image scanner with every supported symbology enabled.
181    ///
182    /// Equivalent to `Scanner::with_config(DecoderConfig::all())`. Convenient
183    /// for exploratory use; for production use, prefer
184    /// [`Scanner::with_config()`] with a
185    /// [`DecoderConfig::new()`](DecoderConfig::new) that opts into only the
186    /// symbologies you actually need.
187    pub fn new() -> Self {
188        Self::with_config(DecoderConfig::all())
189    }
190
191    /// Create a new image scanner with custom configuration
192    ///
193    /// This is the recommended way to create a scanner with specific settings.
194    ///
195    /// # Example
196    /// ```no_run
197    /// use zedbar::config::*;
198    /// use zedbar::{Scanner, DecoderConfig};
199    ///
200    /// let config = DecoderConfig::new()
201    ///     .enable(Ean13)
202    ///     .enable(Code39)
203    ///     .set_length_limits(Code39, 4, 20)
204    ///     .position_tracking(true);
205    ///
206    /// let scanner = Scanner::with_config(config);
207    /// ```
208    pub fn with_config(config: DecoderConfig) -> Self {
209        let retry = config.retry_undecoded_regions;
210        Self {
211            scanner: ImageScanner::with_config(config),
212            retry_undecoded_regions: retry,
213        }
214    }
215
216    /// Scan an image for barcodes
217    ///
218    /// Returns a [`ScanResult`] containing decoded symbols and any
219    /// undecoded QR finder regions. Check [`ScanResult::finder_regions()`]
220    /// to find areas that may contain QR codes too small to decode at
221    /// the current resolution.
222    ///
223    /// When [`DecoderConfig::retry_undecoded_regions`] is enabled, each
224    /// undecoded finder region is automatically cropped, upscaled 4x, and
225    /// re-scanned. Successfully decoded symbols have their coordinates
226    /// mapped back to the original image frame.
227    pub fn scan(&mut self, image: &mut Image) -> ScanResult {
228        let (mut symbols, raw_regions) = self.scanner.scan_image(image.as_mut_image());
229        let finder_regions: Vec<FinderRegion> = raw_regions
230            .into_iter()
231            .map(|(x, y, w, h)| FinderRegion {
232                x,
233                y,
234                width: w,
235                height: h,
236            })
237            .collect();
238
239        if !self.retry_undecoded_regions || finder_regions.is_empty() {
240            return ScanResult::new(symbols, finder_regions);
241        }
242
243        // Try multiple scale factors: the adaptive binarization window size
244        // is chosen in power-of-2 steps based on image size, and certain
245        // intermediate sizes land in a range where the window extends too
246        // far into the white quiet zone, causing halo artifacts that break
247        // data extraction. Trying 2x, 4x, and 6x covers the common cases.
248        const SCALES: &[u32] = &[2, 4, 6];
249
250        // Skip retry for regions that cover more than 10% of the image
251        // area — on large images a big region is almost always a false
252        // positive from a 1D barcode. Small images (< 200px on either
253        // side) are exempt because a legitimate single QR often fills
254        // most of the frame.
255        let apply_area_filter = image.width() >= 200 && image.height() >= 200;
256        let image_area = image.width() as u64 * image.height() as u64;
257        let area_limit = image_area / 10;
258
259        let mut unresolved: Vec<FinderRegion> = Vec::new();
260        for region in &finder_regions {
261            if apply_area_filter {
262                let region_area = region.width as u64 * region.height as u64;
263                if region_area > area_limit {
264                    unresolved.push(*region);
265                    continue;
266                }
267            }
268            // Pad by 50% of the region size on each side for quiet zone
269            let pad_x = region.width / 2;
270            let pad_y = region.height / 2;
271            let cx = region.x.saturating_sub(pad_x);
272            let cy = region.y.saturating_sub(pad_y);
273            let cw = (region.width + 2 * pad_x).min(image.width().saturating_sub(cx));
274            let ch = (region.height + 2 * pad_y).min(image.height().saturating_sub(cy));
275
276            let Some(cropped) = image.crop(cx, cy, cw, ch) else {
277                unresolved.push(*region);
278                continue;
279            };
280
281            let mut decoded = false;
282            for &scale in SCALES {
283                let Some(mut upscaled) = cropped.upscale(scale) else {
284                    continue;
285                };
286                let (mut retry_symbols, _) = self.scanner.scan_image(upscaled.as_mut_image());
287                if retry_symbols.is_empty() {
288                    continue;
289                }
290                let half = scale as i32 / 2;
291                for sym in &mut retry_symbols {
292                    for pt in &mut sym.pts {
293                        pt.x = (pt.x + half) / scale as i32 + cx as i32;
294                        pt.y = (pt.y + half) / scale as i32 + cy as i32;
295                    }
296                }
297                for sym in retry_symbols {
298                    if !symbols
299                        .iter()
300                        .any(|s| s.symbol_type() == sym.symbol_type() && s.data == sym.data)
301                    {
302                        symbols.push(sym);
303                    }
304                }
305                decoded = true;
306                break;
307            }
308            if !decoded {
309                unresolved.push(*region);
310            }
311        }
312
313        ScanResult::new(symbols, unresolved)
314    }
315}
316
317impl Default for Scanner {
318    fn default() -> Self {
319        Self::new()
320    }
321}