use super::tables::{EAN13_FIRST_PARITY, check_digit, l_widths, upce_expand, upce_from_parity};
use super::{EanMeta, EanVariant};
use crate::image::GrayFrame;
use crate::scan1d::{ScanOptions, scan_edges};
use crate::segment::Segment;
use crate::symbol::{Symbol, SymbolMeta};
use crate::symbology::Symbology;
const MAX_DIGIT_SCORE: f32 = 1.4;
const GUARD_TOL: f32 = 0.55;
pub fn decode_edges(edges: &[f32]) -> Option<Symbol> {
if edges.len() < 3 {
return None;
}
let forward: Vec<f32> = edges.windows(2).map(|w| w[1] - w[0]).collect();
let mut reversed = forward.clone();
reversed.reverse();
for widths in [&forward, &reversed] {
if let Some(sym) = decode_widths(widths) {
return Some(sym);
}
}
None
}
fn decode_widths(w: &[f32]) -> Option<Symbol> {
let (variant, digits) = [try_ean13(w), try_ean8(w), try_upce(w)]
.into_iter()
.flatten()
.next()?;
Some(Symbol::new(
symbology_of(variant),
vec![Segment::numeric(digits)],
SymbolMeta::Ean(EanMeta {
variant,
addon: None,
}),
))
}
fn symbology_of(variant: EanVariant) -> Symbology {
match variant {
EanVariant::Ean13 => Symbology::Ean13,
EanVariant::Ean8 => Symbology::Ean8,
EanVariant::UpcA => Symbology::UpcA,
EanVariant::UpcE => Symbology::UpcE,
EanVariant::Ean2 => Symbology::Ean2,
EanVariant::Ean5 => Symbology::Ean5,
}
}
fn to_ascii(vals: &[u8]) -> Vec<u8> {
vals.iter().map(|&v| b'0' + v).collect()
}
fn guard_ok(elems: &[f32]) -> bool {
let mean = elems.iter().sum::<f32>() / elems.len() as f32;
if mean <= 0.0 {
return false;
}
elems.iter().all(|&e| (e - mean).abs() <= GUARD_TOL * mean)
}
fn classify(w: &[f32], left: bool) -> Option<(u8, bool, f32)> {
let total: f32 = w.iter().sum();
if total <= 0.0 {
return None;
}
let unit = total / 7.0;
let norm = [w[0] / unit, w[1] / unit, w[2] / unit, w[3] / unit];
let score_against = |pat: [u8; 4]| -> f32 {
(0..4)
.map(|i| {
let d = norm[i] - pat[i] as f32;
d * d
})
.sum()
};
let mut best: Option<(u8, bool, f32)> = None;
for d in 0..10u8 {
let l = l_widths(d);
let cands: &[(bool, [u8; 4])] = if left {
&[(false, l), (true, [l[3], l[2], l[1], l[0]])]
} else {
&[(false, l)]
};
for &(even, pat) in cands {
let s = score_against(pat);
if best.is_none_or(|(_, _, bs)| s < bs) {
best = Some((d, even, s));
}
}
}
best.filter(|&(_, _, s)| s <= MAX_DIGIT_SCORE)
}
fn try_ean13(w: &[f32]) -> Option<(EanVariant, Vec<u8>)> {
at_offsets(w, 59, |r| {
if !guard_ok(&r[0..3]) || !guard_ok(&r[27..32]) || !guard_ok(&r[56..59]) {
return None;
}
let mut left = [0u8; 6];
let mut parity = [false; 6];
for i in 0..6 {
let (d, even, _) = classify(&r[3 + i * 4..7 + i * 4], true)?;
left[i] = d;
parity[i] = even;
}
let first = (0..10u8).find(|&f| EAN13_FIRST_PARITY[f as usize] == parity)?;
let mut right = [0u8; 6];
for i in 0..6 {
let (d, _, _) = classify(&r[32 + i * 4..36 + i * 4], false)?;
right[i] = d;
}
let mut vals = Vec::with_capacity(13);
vals.push(first);
vals.extend_from_slice(&left);
vals.extend_from_slice(&right);
if check_digit(&vals[..12]) != vals[12] {
return None;
}
if first == 0 {
Some((EanVariant::UpcA, to_ascii(&vals[1..])))
} else {
Some((EanVariant::Ean13, to_ascii(&vals)))
}
})
}
fn try_ean8(w: &[f32]) -> Option<(EanVariant, Vec<u8>)> {
at_offsets(w, 43, |r| {
if !guard_ok(&r[0..3]) || !guard_ok(&r[19..24]) || !guard_ok(&r[40..43]) {
return None;
}
let mut vals = [0u8; 8];
for i in 0..4 {
let (d, even, _) = classify(&r[3 + i * 4..7 + i * 4], true)?;
if even {
return None; }
vals[i] = d;
}
for i in 0..4 {
let (d, _, _) = classify(&r[24 + i * 4..28 + i * 4], false)?;
vals[4 + i] = d;
}
if check_digit(&vals[..7]) != vals[7] {
return None;
}
Some((EanVariant::Ean8, to_ascii(&vals)))
})
}
fn try_upce(w: &[f32]) -> Option<(EanVariant, Vec<u8>)> {
at_offsets(w, 33, |r| {
if !guard_ok(&r[0..3]) || !guard_ok(&r[27..33]) {
return None;
}
let mut payload = [0u8; 6];
let mut parity = [false; 6];
for i in 0..6 {
let (d, even, _) = classify(&r[3 + i * 4..7 + i * 4], true)?;
payload[i] = d;
parity[i] = even;
}
let (ns, check) = upce_from_parity(&parity)?;
if check_digit(&upce_expand(ns, &payload)) != check {
return None;
}
let mut vals = Vec::with_capacity(8);
vals.push(ns);
vals.extend_from_slice(&payload);
vals.push(check);
Some((EanVariant::UpcE, to_ascii(&vals)))
})
}
fn at_offsets<F>(w: &[f32], len: usize, f: F) -> Option<(EanVariant, Vec<u8>)>
where
F: Fn(&[f32]) -> Option<(EanVariant, Vec<u8>)>,
{
if w.len() < len {
return None;
}
let max_off = (w.len() - len).min(6);
(0..=max_off).step_by(2).find_map(|o| f(&w[o..o + len]))
}
const MIN_CONSENSUS_VOTES: usize = 3;
const DOMINANCE: usize = 2;
pub fn scan(frame: &GrayFrame<'_>, opts: &ScanOptions) -> Option<Symbol> {
let mut tally: Vec<(String, usize, Symbol)> = Vec::new();
for cand in scan_edges(frame, opts) {
let Some(sym) = decode_edges(&cand.edges) else {
continue;
};
let key = format!("{:?}|{}", sym.symbology, sym.text().unwrap_or_default());
if let Some(entry) = tally.iter_mut().find(|(k, _, _)| *k == key) {
entry.1 += 1;
} else {
let mut sym = sym;
sym.location.get_or_insert(cand.location);
tally.push((key, 1, sym));
}
}
tally.sort_by_key(|(_, n, _)| core::cmp::Reverse(*n));
let top = tally.first().map(|(_, n, _)| *n).unwrap_or(0);
let runner_up = tally.get(1).map(|(_, n, _)| *n).unwrap_or(0);
if top < MIN_CONSENSUS_VOTES || top < DOMINANCE * runner_up {
return None;
}
tally.into_iter().next().map(|(_, _, s)| s)
}