use super::Pdf417Decoder;
use super::encode::ROW_HEIGHT;
use crate::error::{Error, Result};
use crate::geometry::{Location, Point, Quad};
use crate::image::GrayFrame;
use crate::imgproc::homography::Homography;
use crate::imgproc::line::{Line, ransac_line};
use crate::imgproc::rng::Prng;
use crate::imgproc::sample::sample_bilinear;
use crate::imgproc::threshold::otsu_threshold;
use crate::output::{BitMatrix, Encoding};
use crate::pipeline::{Candidate, Hints};
use crate::symbol::Symbol;
use crate::symbology::Symbology;
use crate::traits::{Analyze, Decode, Detect};
const QUIET_ZONE: usize = 2;
const START_RATIOS: [i32; 8] = [8, 1, 1, 1, 1, 1, 1, 3];
const STOP_RATIOS: [i32; 9] = [7, 1, 1, 3, 1, 1, 1, 2, 1];
const ROW_OVERHEAD_MODULES: f32 = 69.0;
const MATCH_TOLERANCE: f32 = 0.45;
#[derive(Debug, Default, Clone, Copy)]
pub struct Pdf417Scanner;
impl Pdf417Scanner {
pub fn new() -> Self {
Pdf417Scanner
}
}
impl Detect for Pdf417Scanner {
fn detect(&self, frame: &GrayFrame<'_>, _hints: &Hints) -> Vec<Candidate> {
match locate(frame) {
Some(loc) => vec![Candidate {
location: loc.as_location(),
symbology: Some(Symbology::Pdf417),
fingerprint: None,
known: None,
}],
None => Vec::new(),
}
}
}
impl Analyze for Pdf417Scanner {
fn analyze(&self, frame: &GrayFrame<'_>, candidate: &Candidate) -> Result<Symbol> {
if let Some(known) = &candidate.known {
return Ok(known.clone());
}
scan(frame).ok_or_else(|| Error::undecodable("PDF417 sampler could not decode the frame"))
}
}
pub fn scan(frame: &GrayFrame<'_>) -> Option<Symbol> {
let matrix = sample_grid(frame)?;
Pdf417Decoder::new().decode(&Encoding::Matrix(matrix)).ok()
}
pub fn sample_grid(frame: &GrayFrame<'_>) -> Option<BitMatrix> {
Some(locate(frame)?.sample(frame))
}
struct Binary {
bits: Vec<bool>,
width: usize,
height: usize,
threshold: u8,
}
impl Binary {
fn from_frame(frame: &GrayFrame<'_>) -> Binary {
let w = frame.width();
let h = frame.height();
let threshold = otsu_threshold(frame);
let mut bits = vec![false; w * h];
for y in 0..h {
for x in 0..w {
bits[y * w + x] = frame.get_unchecked(x, y) <= threshold;
}
}
Binary {
bits,
width: w,
height: h,
threshold,
}
}
#[inline]
fn dark(&self, x: usize, y: usize) -> bool {
self.bits[y * self.width + x]
}
}
type Run = (bool, usize, i32);
fn encode_row(bin: &Binary, y: usize) -> Vec<Run> {
let w = bin.width;
let mut runs = Vec::new();
let mut cur = bin.dark(0, y);
let mut start = 0usize;
for x in 1..w {
let d = bin.dark(x, y);
if d != cur {
runs.push((cur, start, (x - start) as i32));
cur = d;
start = x;
}
}
runs.push((cur, start, (w - start) as i32));
runs
}
fn match_ratios(lengths: &[i32], ratios: &[i32]) -> Option<f32> {
debug_assert_eq!(lengths.len(), ratios.len());
let total: i32 = lengths.iter().sum();
let ratio_total: i32 = ratios.iter().sum();
if total < ratio_total {
return None; }
let unit = total as f32 / ratio_total as f32;
if unit < 1.0 {
return None;
}
for (&len, &r) in lengths.iter().zip(ratios) {
let expected = r as f32 * unit;
let allowed = (expected * MATCH_TOLERANCE).max(unit * 0.5);
if (len as f32 - expected).abs() > allowed {
return None;
}
}
Some(unit)
}
fn find_start(runs: &[Run]) -> Option<(f32, f32)> {
if runs.len() < START_RATIOS.len() {
return None;
}
for i in 0..=runs.len() - START_RATIOS.len() {
if !runs[i].0 {
continue;
}
let lens: Vec<i32> = runs[i..i + START_RATIOS.len()]
.iter()
.map(|r| r.2)
.collect();
if let Some(unit) = match_ratios(&lens, &START_RATIOS) {
return Some((runs[i].1 as f32, unit));
}
}
None
}
fn find_stop(runs: &[Run]) -> Option<f32> {
if runs.len() < STOP_RATIOS.len() {
return None;
}
for i in (0..=runs.len() - STOP_RATIOS.len()).rev() {
if !runs[i].0 {
continue;
}
let lens: Vec<i32> = runs[i..i + STOP_RATIOS.len()].iter().map(|r| r.2).collect();
if match_ratios(&lens, &STOP_RATIOS).is_some() {
let last = &runs[i + STOP_RATIOS.len() - 1];
return Some((last.1 as i32 + last.2) as f32);
}
}
None
}
struct Located {
corners: [Point; 4],
grid_w: usize,
grid_h: usize,
threshold: u8,
module_size: f32,
}
impl Located {
fn as_location(&self) -> Location {
let dx = self.corners[1].x - self.corners[0].x;
let dy = self.corners[1].y - self.corners[0].y;
Location {
outline: Quad::new(self.corners),
rotation: Some(dy.atan2(dx)),
module_size: Some(self.module_size),
}
}
fn sample(&self, frame: &GrayFrame<'_>) -> BitMatrix {
let src = [
Point::new(0.0, 0.0),
Point::new(self.grid_w as f32, 0.0),
Point::new(self.grid_w as f32, self.grid_h as f32),
Point::new(0.0, self.grid_h as f32),
];
let grid_to_img = match Homography::from_correspondences(src, self.corners) {
Ok(h) => h,
Err(_) => return BitMatrix::new(self.grid_w, self.grid_h, QUIET_ZONE),
};
let mut matrix = BitMatrix::new(self.grid_w, self.grid_h, QUIET_ZONE);
for my in 0..self.grid_h {
for mx in 0..self.grid_w {
let (px, py) = grid_to_img.map_f64(mx as f64 + 0.5, my as f64 + 0.5);
if sample_dark(frame, px, py, self.threshold) {
matrix.set(mx, my, true);
}
}
}
matrix
}
}
fn sample_dark(frame: &GrayFrame<'_>, px: f64, py: f64, threshold: u8) -> bool {
let mut sum = 0.0f64;
let mut n = 0.0f64;
for dy in -1..=1 {
for dx in -1..=1 {
let v = sample_bilinear(frame, px + f64::from(dx), py + f64::from(dy));
sum += v;
n += 1.0;
}
}
(sum / n) <= f64::from(threshold)
}
fn midpoint(a: Point, b: Point) -> Point {
Point::new((a.x + b.x) / 2.0, (a.y + b.y) / 2.0)
}
fn down_dir(line: &Line) -> (f32, f32) {
(-line.b, line.a)
}
fn centroid(pts: &[Point]) -> Point {
let n = pts.len() as f32;
Point::new(
pts.iter().map(|p| p.x).sum::<f32>() / n,
pts.iter().map(|p| p.y).sum::<f32>() / n,
)
}
fn walk_bar(bin: &Binary, start: Point, dir: (f32, f32), sign: f32) -> f32 {
let mut dist = 0.0f32;
let mut last_dark = 0.0f32;
let mut gap = 0;
let limit = (bin.width + bin.height) as f32;
while dist <= limit {
dist += 0.5;
let x = start.x + sign * dir.0 * dist;
let y = start.y + sign * dir.1 * dist;
let xi = x.round();
let yi = y.round();
if xi < 0.0 || yi < 0.0 || xi >= bin.width as f32 || yi >= bin.height as f32 {
break;
}
if bin.dark(xi as usize, yi as usize) {
last_dark = dist;
gap = 0;
} else {
gap += 1;
if gap > 5 {
break; }
}
}
sign * last_dark
}
fn guard_corners(
bin: &Binary,
line: &Line,
pts: &[Point],
into: Point,
module: f32,
offset_modules: f32,
) -> (Point, Point) {
let c = centroid(pts);
let mut normal = (line.a, line.b);
if (into.x - c.x) * normal.0 + (into.y - c.y) * normal.1 < 0.0 {
normal = (-normal.0, -normal.1);
}
let off = offset_modules * module;
let interior = Point::new(c.x + normal.0 * off, c.y + normal.1 * off);
let dir = (-line.b, line.a);
let t_a = walk_bar(bin, interior, dir, 1.0);
let t_b = walk_bar(bin, interior, dir, -1.0);
let e1 = Point::new(c.x + t_a * dir.0, c.y + t_a * dir.1);
let e2 = Point::new(c.x + t_b * dir.0, c.y + t_b * dir.1);
if e1.y <= e2.y { (e1, e2) } else { (e2, e1) }
}
fn median(values: &[f32]) -> f32 {
if values.is_empty() {
return 0.0;
}
let mut v = values.to_vec();
v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = v.len() / 2;
if v.len().is_multiple_of(2) {
(v[mid - 1] + v[mid]) / 2.0
} else {
v[mid]
}
}
const MIN_ANCHOR_ROWS: usize = 6;
fn locate(frame: &GrayFrame<'_>) -> Option<Located> {
if frame.width() < 40 || frame.height() < 9 {
return None;
}
let bin = Binary::from_frame(frame);
let mut left_pts = Vec::new();
let mut right_pts = Vec::new();
let mut start_units = Vec::new();
for y in 0..bin.height {
let runs = encode_row(&bin, y);
if let Some((lx, unit)) = find_start(&runs) {
left_pts.push(Point::new(lx, y as f32 + 0.5));
start_units.push(unit);
}
if let Some(rx) = find_stop(&runs) {
right_pts.push(Point::new(rx, y as f32 + 0.5));
}
}
if left_pts.len() < MIN_ANCHOR_ROWS || right_pts.len() < MIN_ANCHOR_ROWS {
return None;
}
let unit_along = median(&start_units);
let ransac_thr = (unit_along * 0.6).clamp(1.5, 6.0);
let mut rng = Prng::new(0x9E37_79B9);
let (left_line, left_idx) = ransac_line(&left_pts, 200, ransac_thr, &mut rng)?;
let (right_line, right_idx) = ransac_line(&right_pts, 200, ransac_thr, &mut rng)?;
if left_idx.len() < MIN_ANCHOR_ROWS || right_idx.len() < MIN_ANCHOR_ROWS {
return None;
}
let left_in: Vec<Point> = left_idx.iter().map(|&i| left_pts[i]).collect();
let right_in: Vec<Point> = right_idx.iter().map(|&i| right_pts[i]).collect();
let interior = centroid(&[centroid(&left_in), centroid(&right_in)]);
let (tl0, bl0) = guard_corners(&bin, &left_line, &left_in, interior, unit_along, 3.5);
let (tr0, br0) = guard_corners(&bin, &right_line, &right_in, interior, unit_along, 14.5);
let left_mid = midpoint(tl0, bl0);
let right_mid = midpoint(tr0, br0);
let mut d_left = down_dir(&left_line);
let mut d_right = down_dir(&right_line);
if d_left.1 < 0.0 {
d_left = (-d_left.0, -d_left.1);
}
if d_right.1 < 0.0 {
d_right = (-d_right.0, -d_right.1);
}
let mut y_axis = ((d_left.0 + d_right.0) / 2.0, (d_left.1 + d_right.1) / 2.0);
let yl = (y_axis.0 * y_axis.0 + y_axis.1 * y_axis.1).sqrt().max(1e-6);
y_axis = (y_axis.0 / yl, y_axis.1 / yl);
let half_h = (tl0.distance(bl0) + tr0.distance(br0)) / 4.0;
let tl = Point::new(
left_mid.x - y_axis.0 * half_h,
left_mid.y - y_axis.1 * half_h,
);
let bl = Point::new(
left_mid.x + y_axis.0 * half_h,
left_mid.y + y_axis.1 * half_h,
);
let tr = Point::new(
right_mid.x - y_axis.0 * half_h,
right_mid.y - y_axis.1 * half_h,
);
let br = Point::new(
right_mid.x + y_axis.0 * half_h,
right_mid.y + y_axis.1 * half_h,
);
let width_px = left_mid.distance(right_mid);
let cos_tilt = (left_line.a.abs() + right_line.a.abs()) / 2.0;
let module_guard = (unit_along * cos_tilt).max(0.5);
let width_modules = width_px / module_guard;
let columns = (((width_modules - ROW_OVERHEAD_MODULES) / 17.0).round() as i64).clamp(1, 30);
let grid_w = 17 * columns as usize + 69;
let module_size = width_px / grid_w as f32;
if module_size < 1.0 {
return None;
}
let rows = (2.0 * half_h / module_size / ROW_HEIGHT as f32).round() as i64;
let rows = rows.clamp(3, 90);
let grid_h = ROW_HEIGHT * rows as usize;
Some(Located {
corners: [tl, tr, br, bl],
grid_w,
grid_h,
threshold: bin.threshold,
module_size,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_ratio_matches_ideal_run() {
let lens: Vec<i32> = START_RATIOS.iter().map(|&r| r * 4).collect();
let unit = match_ratios(&lens, &START_RATIOS).unwrap();
assert!((unit - 4.0).abs() < 0.01);
}
#[test]
fn stop_ratio_matches_ideal_run() {
let lens: Vec<i32> = STOP_RATIOS.iter().map(|&r| r * 5).collect();
let unit = match_ratios(&lens, &STOP_RATIOS).unwrap();
assert!((unit - 5.0).abs() < 0.01);
}
#[test]
fn ratio_rejects_wrong_shape() {
let lens = [4, 4, 4, 4, 4, 4, 4, 4];
assert!(match_ratios(&lens, &START_RATIOS).is_none());
}
#[test]
fn median_odd_and_even() {
assert!((median(&[3.0, 1.0, 2.0]) - 2.0).abs() < 1e-6);
assert!((median(&[1.0, 2.0, 3.0, 4.0]) - 2.5).abs() < 1e-6);
}
}