rxing/pdf417/detector/
pdf_417_detector.rs

1/*
2 * Copyright 2009 ZXing authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use crate::{
18    common::{BitMatrix, Result},
19    point, Binarizer, BinaryBitmap, DecodeHints, Exceptions, Point,
20};
21
22use std::borrow::Cow;
23
24use super::PDF417DetectorRXingResult;
25
26/*
27 * <p>Encapsulates logic that can detect a PDF417 Code in an image, even if the
28 * PDF417 Code is rotated or skewed, or partially obscured.</p>
29 *
30 * @author SITA Lab (kevin.osullivan@sita.aero)
31 * @author dswitkin@google.com (Daniel Switkin)
32 * @author Guenther Grau
33 */
34
35const INDEXES_START_PATTERN: [u32; 4] = [0, 4, 1, 5];
36const INDEXES_STOP_PATTERN: [u32; 4] = [6, 2, 7, 3];
37const MAX_AVG_VARIANCE: f64 = 0.42;
38const MAX_INDIVIDUAL_VARIANCE: f64 = 0.8;
39
40// B S B S B S B S Bar/Space pattern
41// 11111111 0 1 0 1 0 1 000
42const START_PATTERN: [u32; 8] = [8, 1, 1, 1, 1, 1, 1, 3];
43// 1111111 0 1 000 1 0 1 00 1
44const STOP_PATTERN: [u32; 9] = [7, 1, 1, 3, 1, 1, 1, 2, 1];
45const MAX_PIXEL_DRIFT: u32 = 3;
46const MAX_PATTERN_DRIFT: u32 = 5;
47// if we set the value too low, then we don't detect the correct height of the bar if the start patterns are damaged.
48// if we set the value too high, then we might detect the start pattern from a neighbor barcode.
49const SKIPPED_ROW_COUNT_MAX: u32 = 25;
50// A PDF471 barcode should have at least 3 rows, with each row being >= 3 times the module width.
51// Therefore it should be at least 9 pixels tall. To be conservative, we use about half the size to
52// ensure we don't miss it.
53const ROW_STEP: u32 = 5;
54const BARCODE_MIN_HEIGHT: u32 = 10;
55const ROTATIONS: [u32; 4] = [0, 180, 270, 90];
56
57/**
58 * <p>Detects a PDF417 Code in an image. Checks 0, 90, 180, and 270 degree rotations.</p>
59 *
60 * @param image barcode image to decode
61 * @param hints optional hints to detector
62 * @param multiple if true, then the image is searched for multiple codes. If false, then at most one code will
63 * be found and returned
64 * @return {@link PDF417DetectorRXingResult} encapsulating results of detecting a PDF417 code
65 * @throws NotFoundException if no PDF417 Code can be found
66 */
67pub fn detect_with_hints<B: Binarizer>(
68    image: &mut BinaryBitmap<B>,
69    _hints: &DecodeHints,
70    multiple: bool,
71) -> Result<PDF417DetectorRXingResult> {
72    // TODO detection improvement, tryHarder could try several different luminance thresholds/blackpoints or even
73    // different binarizers
74    //boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
75    //let try_harder = matches!(hints.get(&DecodeHintType::TRY_HARDER), Some(DecodeHintValue::TryHarder(true)));
76
77    let originalMatrix = image.get_black_matrix();
78    for rotation in ROTATIONS {
79        // for (int rotation : ROTATIONS) {
80        let bitMatrix = applyRotation(originalMatrix, rotation)?;
81        let barcodeCoordinates = detect(multiple, &bitMatrix).ok_or(Exceptions::NOT_FOUND)?;
82        if !barcodeCoordinates.is_empty() {
83            return Ok(PDF417DetectorRXingResult::with_rotation(
84                bitMatrix.into_owned(),
85                barcodeCoordinates,
86                rotation,
87            ));
88        }
89    }
90    Ok(PDF417DetectorRXingResult::with_rotation(
91        originalMatrix.clone(),
92        Vec::new(),
93        0,
94    ))
95}
96
97/**
98 * Applies a rotation to the supplied BitMatrix.
99 * @param matrix bit matrix to apply rotation to
100 * @param rotation the degrees of rotation to apply
101 * @return BitMatrix with applied rotation
102 */
103fn applyRotation(matrix: &'_ BitMatrix, rotation: u32) -> Result<Cow<'_, BitMatrix>> {
104    if rotation % 360 == 0 {
105        Ok(Cow::Borrowed(matrix))
106    } else {
107        let mut newMatrix = matrix.clone();
108        newMatrix.rotate(rotation)?;
109        Ok(Cow::Owned(newMatrix))
110    }
111}
112
113/**
114 * Detects PDF417 codes in an image. Only checks 0 degree rotation
115 * @param multiple if true, then the image is searched for multiple codes. If false, then at most one code will
116 * be found and returned
117 * @param bitMatrix bit matrix to detect barcodes in
118 * @return List of Point arrays containing the coordinates of found barcodes
119 */
120pub fn detect(multiple: bool, bitMatrix: &BitMatrix) -> Option<Vec<[Option<Point>; 8]>> {
121    let mut barcodeCoordinates: Vec<[Option<Point>; 8]> = Vec::new();
122    let mut row = 0;
123    let mut column = 0;
124    let mut foundBarcodeInRow = false;
125    while row < bitMatrix.getHeight() {
126        let vertices = findVertices(bitMatrix, row, column)?;
127
128        if vertices[0].is_none() && vertices[3].is_none() {
129            if !foundBarcodeInRow {
130                // we didn't find any barcode so that's the end of searching
131                break;
132            }
133            // we didn't find a barcode starting at the given column and row. Try again from the first column and slightly
134            // below the lowest barcode we found so far.
135            foundBarcodeInRow = false;
136            column = 0;
137            for barcodeCoordinate in &barcodeCoordinates {
138                if let Some(coord_1) = barcodeCoordinate[1] {
139                    row = row.max(coord_1.y as u32);
140                }
141                if let Some(coord_3) = barcodeCoordinate[3] {
142                    row = row.max(coord_3.y as u32);
143                }
144            }
145            row += ROW_STEP;
146            continue;
147        }
148        foundBarcodeInRow = true;
149        barcodeCoordinates.push(vertices);
150        if !multiple {
151            break;
152        }
153        // if we didn't find a right row indicator column, then continue the search for the next barcode after the
154        // start pattern of the barcode just found.
155        if let Some(vert_2) = vertices[2] {
156            column = vert_2.x as u32;
157            row = vert_2.y as u32;
158        } else {
159            column = vertices[4].as_ref().unwrap().x as u32;
160            row = vertices[4].as_ref().unwrap().y as u32;
161        }
162    }
163    Some(barcodeCoordinates)
164}
165
166/**
167 * Locate the vertices and the codewords area of a black blob using the Start
168 * and Stop patterns as locators.
169 *
170 * @param matrix the scanned barcode image.
171 * @return an array containing the vertices:
172 *           vertices[0] x, y top left barcode
173 *           vertices[1] x, y bottom left barcode
174 *           vertices[2] x, y top right barcode
175 *           vertices[3] x, y bottom right barcode
176 *           vertices[4] x, y top left codeword area
177 *           vertices[5] x, y bottom left codeword area
178 *           vertices[6] x, y top right codeword area
179 *           vertices[7] x, y bottom right codeword area
180 */
181fn findVertices(matrix: &BitMatrix, startRow: u32, startColumn: u32) -> Option<[Option<Point>; 8]> {
182    let height = matrix.getHeight();
183    let width = matrix.getWidth();
184    let mut startRow = startRow;
185    let mut startColumn = startColumn;
186
187    let mut result = [None::<Point>; 8]; //Point[8];
188    copyToRXingResult(
189        &mut result,
190        &findRowsWithPattern(matrix, height, width, startRow, startColumn, &START_PATTERN)?,
191        &INDEXES_START_PATTERN,
192    );
193
194    if let Some(result_4) = result[4] {
195        startColumn = result_4.x as u32;
196        startRow = result_4.y as u32;
197    }
198    copyToRXingResult(
199        &mut result,
200        &findRowsWithPattern(matrix, height, width, startRow, startColumn, &STOP_PATTERN)?,
201        &INDEXES_STOP_PATTERN,
202    );
203
204    Some(result)
205}
206
207fn copyToRXingResult(
208    result: &mut [Option<Point>],
209    tmpRXingResult: &[Option<Point>],
210    destinationIndexes: &[u32],
211) {
212    for i in 0..destinationIndexes.len() {
213        result[destinationIndexes[i] as usize] = tmpRXingResult[i];
214    }
215}
216
217fn findRowsWithPattern(
218    matrix: &BitMatrix,
219    height: u32,
220    width: u32,
221    startRow: u32,
222    startColumn: u32,
223    pattern: &[u32],
224) -> Option<[Option<Point>; 4]> {
225    let mut startRow = startRow;
226    let mut result = [None; 4];
227    let mut found = false;
228    let mut counters = vec![0_u32; pattern.len()];
229    while startRow < height {
230        let mut loc_store;
231        if let Some(loc) =
232            findGuardPattern(matrix, startColumn, startRow, width, pattern, &mut counters)
233        {
234            loc_store = Some(loc);
235            while startRow > 0 {
236                startRow -= 1;
237                if let Some(previousRowLoc) =
238                    findGuardPattern(matrix, startColumn, startRow, width, pattern, &mut counters)
239                {
240                    loc_store.replace(previousRowLoc);
241                    // loc_store = Some(previousRowLoc);
242                } else {
243                    startRow += 1;
244                    break;
245                }
246            }
247            result[0] = Some(point(loc_store.as_ref()?[0] as f32, startRow as f32));
248            result[1] = Some(point(loc_store.as_ref()?[1] as f32, startRow as f32));
249            found = true;
250            break;
251        }
252
253        startRow += ROW_STEP;
254    }
255
256    let mut stopRow = startRow + 1;
257    // Last row of the current symbol that contains pattern
258    if found {
259        let mut skippedRowCount = 0;
260        let mut previousRowLoc = [result[0].as_ref()?.x as u32, result[1].as_ref()?.x as u32];
261        while stopRow < height {
262            if let Some(loc) = findGuardPattern(
263                matrix,
264                previousRowLoc[0],
265                stopRow,
266                width,
267                pattern,
268                &mut counters,
269            ) {
270                // a found pattern is only considered to belong to the same barcode if the start and end positions
271                // don't differ too much. Pattern drift should be not bigger than two for consecutive rows. With
272                // a higher number of skipped rows drift could be larger. To keep it simple for now, we allow a slightly
273                // larger drift and don't check for skipped rows.
274                if (previousRowLoc[0] as i32 - loc[0] as i32).unsigned_abs() < MAX_PATTERN_DRIFT
275                    && (previousRowLoc[1] as i32 - loc[1] as i32).unsigned_abs() < MAX_PATTERN_DRIFT
276                {
277                    previousRowLoc = loc;
278                    skippedRowCount = 0;
279                } else if skippedRowCount > SKIPPED_ROW_COUNT_MAX {
280                    break;
281                } else {
282                    skippedRowCount += 1;
283                }
284            } else if skippedRowCount > SKIPPED_ROW_COUNT_MAX {
285                break;
286            } else {
287                skippedRowCount += 1;
288            }
289
290            stopRow += 1;
291        }
292        stopRow -= skippedRowCount + 1;
293        result[2] = Some(point(previousRowLoc[0] as f32, stopRow as f32));
294        result[3] = Some(point(previousRowLoc[1] as f32, stopRow as f32));
295    }
296    if stopRow - startRow < BARCODE_MIN_HEIGHT {
297        result.fill(None);
298    }
299
300    Some(result)
301}
302
303/**
304 * @param matrix row of black/white values to search
305 * @param column x position to start search
306 * @param row y position to start search
307 * @param width the number of pixels to search on this row
308 * @param pattern pattern of counts of number of black and white pixels that are
309 *                 being searched for as a pattern
310 * @param counters array of counters, as long as pattern, to re-use
311 * @return start/end horizontal offset of guard pattern, as an array of two ints.
312 */
313fn findGuardPattern(
314    matrix: &BitMatrix,
315    column: u32,
316    row: u32,
317    width: u32,
318    pattern: &[u32],
319    counters: &mut [u32],
320) -> Option<[u32; 2]> {
321    counters.fill(0);
322    let mut patternStart = column;
323    let mut pixelDrift = 0;
324
325    // if there are black pixels left of the current pixel shift to the left, but only for MAX_PIXEL_DRIFT pixels
326    while matrix.get(patternStart, row) && patternStart > 0 && pixelDrift < MAX_PIXEL_DRIFT {
327        pixelDrift += 1;
328        patternStart -= 1;
329    }
330    let mut x = patternStart;
331    let mut counterPosition = 0;
332    let patternLength = pattern.len();
333    let mut isWhite = false;
334    while x < width {
335        // for (boolean isWhite = false; x < width; x++) {
336        let pixel = matrix.get(x, row);
337        if pixel != isWhite {
338            counters[counterPosition] += 1;
339        } else {
340            if counterPosition == patternLength - 1 {
341                if patternMatchVariance(counters, pattern) < MAX_AVG_VARIANCE {
342                    return Some([patternStart, x]);
343                }
344                patternStart += counters[0] + counters[1];
345
346                counters.copy_within(2..counterPosition - 1 + 2, 0);
347                // System.arraycopy(counters, 2, counters, 0, counterPosition - 1);
348
349                counters[counterPosition - 1] = 0;
350                counters[counterPosition] = 0;
351                counterPosition -= 1;
352            } else {
353                counterPosition += 1;
354            }
355            counters[counterPosition] = 1;
356            isWhite = !isWhite;
357        }
358        x += 1;
359    }
360    if counterPosition == patternLength - 1
361        && patternMatchVariance(counters, pattern) < MAX_AVG_VARIANCE
362    {
363        return Some([patternStart, x - 1]);
364    }
365
366    None
367}
368
369/**
370 * Determines how closely a set of observed counts of runs of black/white
371 * values matches a given target pattern. This is reported as the ratio of
372 * the total variance from the expected pattern proportions across all
373 * pattern elements, to the length of the pattern.
374 *
375 * @param counters observed counters
376 * @param pattern expected pattern
377 * @return ratio of total variance between counters and pattern compared to total pattern size
378 */
379fn patternMatchVariance(counters: &[u32], pattern: &[u32]) -> f64 {
380    let numCounters = counters.len();
381    let total = counters.iter().take(numCounters).sum::<u32>();
382    let patternLength = pattern.iter().take(numCounters).sum::<u32>();
383    // for i in 0..numCounters {
384    //     total += counters[i];
385    //     patternLength += pattern[i];
386    // }
387    if total < patternLength {
388        // If we don't even have one pixel per unit of bar width, assume this
389        // is too small to reliably match, so fail:
390        return f64::INFINITY; //Float.POSITIVE_INFINITY;
391    }
392    // We're going to fake floating-point math in integers. We just need to use more bits.
393    // Scale up patternLength so that intermediate values below like scaledCounter will have
394    // more "significant digits".
395    let unitBarWidth: f64 = total as f64 / patternLength as f64;
396    let maxIndividualVariance = MAX_INDIVIDUAL_VARIANCE * unitBarWidth;
397
398    let mut totalVariance = 0.0;
399    for x in 0..numCounters {
400        let counter = counters[x];
401        let scaledPattern: f64 = pattern[x] as f64 * unitBarWidth;
402        let variance: f64 = if counter as f64 > scaledPattern {
403            counter as f64 - scaledPattern
404        } else {
405            scaledPattern - counter as f64
406        };
407        if variance > maxIndividualVariance {
408            return f64::INFINITY;
409        }
410        totalVariance += variance;
411    }
412    totalVariance / total as f64
413}