use crate::text::{HorizontalRule, Ruling};
const FRAME_EDGE_TOLERANCE: f64 = 8.0;
const MERGE_GAP: f64 = 30.0;
const OVERLAP_SHARE: f64 = 0.5;
const HOLD_SLACK: f64 = 1.0;
const EDGE_SLACK: f64 = 2.0;
#[doc(hidden)]
pub fn vector_proposals(
lines: &[[f64; 4]],
rules: &[HorizontalRule],
rulings: &[Ruling],
proposals: &[Vec<usize>],
) -> Vec<(&'static str, Vec<usize>)> {
let mut seen: std::collections::HashSet<Vec<usize>> = proposals
.iter()
.map(|p| {
let mut p = p.clone();
p.sort_unstable();
p
})
.collect();
let mut out: Vec<(&'static str, Vec<usize>)> = Vec::new();
let mut offer =
|detector: &'static str, hull: [f64; 4], out: &mut Vec<(&'static str, Vec<usize>)>| {
let held = held_by(lines, hull);
if held.len() >= 2 && seen.insert(held.clone()) {
out.push((detector, held));
}
};
for lattice in crate::learned::lattice::lattices(rulings) {
offer("lattice", lattice.bbox(), &mut out);
}
for hull in ruled_frames(rules) {
offer("ruled_frame", hull, &mut out);
}
let hulls: Vec<[f64; 4]> = proposals.iter().filter_map(|p| hull_of(lines, p)).collect();
for &hull in &hulls {
if let Some(grown) = grown_to_rules(hull, rules) {
offer("grown", grown, &mut out);
}
}
for (k, &upper) in hulls.iter().enumerate() {
let under = hulls
.iter()
.enumerate()
.filter(|&(j, &h)| j != k && h[3] <= upper[1] + EDGE_SLACK && shares_column(upper, h))
.max_by(|a, b| a.1[3].total_cmp(&b.1[3]));
if let Some((_, &lower)) = under {
if upper[1] - lower[3] <= MERGE_GAP {
let joined = [
upper[0].min(lower[0]),
lower[1],
upper[2].max(lower[2]),
upper[3],
];
offer("merged", joined, &mut out);
}
}
}
out
}
fn held_by(lines: &[[f64; 4]], hull: [f64; 4]) -> Vec<usize> {
(0..lines.len())
.filter(|&i| {
let b = lines[i];
let (cx, cy) = ((b[0] + b[2]) * 0.5, (b[1] + b[3]) * 0.5);
cx >= hull[0] - HOLD_SLACK
&& cx <= hull[2] + HOLD_SLACK
&& cy >= hull[1] - HOLD_SLACK
&& cy <= hull[3] + HOLD_SLACK
})
.collect()
}
fn hull_of(lines: &[[f64; 4]], ids: &[usize]) -> Option<[f64; 4]> {
let mut hull = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for &i in ids {
let b = lines.get(i)?;
hull = [
hull[0].min(b[0]),
hull[1].min(b[1]),
hull[2].max(b[2]),
hull[3].max(b[3]),
];
}
hull[0].is_finite().then_some(hull)
}
fn shares_column(a: [f64; 4], b: [f64; 4]) -> bool {
let overlap = a[2].min(b[2]) - a[0].max(b[0]);
overlap > OVERLAP_SHARE * (a[2] - a[0]).min(b[2] - b[0])
}
fn ruled_frames(rules: &[HorizontalRule]) -> Vec<[f64; 4]> {
let mut order: Vec<&HorizontalRule> = rules.iter().collect();
order.sort_by(|a, b| b.y.total_cmp(&a.y));
let mut sets: Vec<Vec<&HorizontalRule>> = Vec::new();
for rule in order {
match sets.iter_mut().find(|set| {
(set[0].left - rule.left).abs() <= FRAME_EDGE_TOLERANCE
&& (set[0].right - rule.right).abs() <= FRAME_EDGE_TOLERANCE
}) {
Some(set) => set.push(rule),
None => sets.push(vec![rule]),
}
}
sets.into_iter()
.filter_map(|set| {
let top = set.iter().map(|r| r.y).fold(f64::NEG_INFINITY, f64::max);
let bottom = set.iter().map(|r| r.y).fold(f64::INFINITY, f64::min);
if top - bottom <= EDGE_SLACK {
return None;
}
let left = set.iter().map(|r| r.left).fold(f64::INFINITY, f64::min);
let right = set
.iter()
.map(|r| r.right)
.fold(f64::NEG_INFINITY, f64::max);
Some([left, bottom, right, top])
})
.collect()
}
fn grown_to_rules(hull: [f64; 4], rules: &[HorizontalRule]) -> Option<[f64; 4]> {
let across: Vec<&HorizontalRule> = rules
.iter()
.filter(|r| {
r.right.min(hull[2]) - r.left.max(hull[0]) > OVERLAP_SHARE * (hull[2] - hull[0])
})
.collect();
let top = across
.iter()
.map(|r| r.y)
.filter(|&y| y >= hull[3] - EDGE_SLACK)
.fold(f64::INFINITY, f64::min);
let bottom = across
.iter()
.map(|r| r.y)
.filter(|&y| y <= hull[1] + EDGE_SLACK)
.fold(f64::NEG_INFINITY, f64::max);
let top = if top.is_finite() { top } else { hull[3] };
let bottom = if bottom.is_finite() { bottom } else { hull[1] };
(top != hull[3] || bottom != hull[1]).then_some([hull[0], bottom, hull[2], top])
}
#[cfg(test)]
mod tests {
use super::*;
fn line(x0: f64, x1: f64, y: f64) -> [f64; 4] {
[x0, y, x1, y + 9.0]
}
fn rule(left: f64, right: f64, y: f64) -> HorizontalRule {
HorizontalRule { left, right, y }
}
#[test]
fn rules_with_one_measure_frame_a_table() {
let mut lines = vec![line(72.0, 300.0, 720.0)]; for r in 0..5 {
let y = 690.0 - 14.0 * f64::from(r);
lines.push(line(72.0, 160.0, y));
lines.push(line(400.0, 440.0, y));
}
let rules = [rule(70.0, 450.0, 705.0), rule(70.0, 450.0, 620.0)];
let found = vector_proposals(&lines, &rules, &[], &[]);
let frame = found
.iter()
.find(|(d, _)| *d == "ruled_frame")
.expect("no frame proposed");
assert_eq!(frame.1, (1..11).collect::<Vec<_>>());
}
#[test]
fn a_piece_grows_to_its_rules_and_joins_the_piece_under_it() {
let mut lines = Vec::new();
for r in 0..6 {
let y = 690.0 - 14.0 * f64::from(r);
lines.push(line(72.0, 160.0, y));
lines.push(line(400.0, 440.0, y));
}
let rules = [rule(70.0, 450.0, 705.0), rule(70.0, 450.0, 605.0)];
let upper: Vec<usize> = (0..6).collect();
let lower: Vec<usize> = (6..12).collect();
let found = vector_proposals(&lines, &rules, &[], &[upper, lower]);
let all: Vec<usize> = (0..12).collect();
assert!(
found
.iter()
.any(|(d, held)| (*d == "grown" || *d == "ruled_frame") && *held == all),
"{found:?}"
);
let merged: Vec<&(&str, Vec<usize>)> =
found.iter().filter(|(d, _)| *d == "merged").collect();
assert!(
merged.is_empty() || merged.iter().all(|(_, h)| *h == all),
"{found:?}"
);
}
#[test]
fn a_proposal_is_offered_once() {
let mut lines = Vec::new();
for r in 0..4 {
let y = 690.0 - 14.0 * f64::from(r);
lines.push(line(72.0, 160.0, y));
lines.push(line(400.0, 440.0, y));
}
let rules = [rule(70.0, 450.0, 705.0), rule(70.0, 450.0, 640.0)];
let whole: Vec<usize> = (0..8).collect();
let found = vector_proposals(&lines, &rules, &[], &[whole]);
assert!(found.is_empty(), "{found:?}");
}
}