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}