docvortex_core/
geometry_risk.rs1use std::collections::{HashMap, HashSet};
3
4use crate::{
5 geometry::{anchor_pairs, Box4, Size},
6 median,
7 statistics::ordered_clusters,
8};
9
10pub type Entry = (usize, Box4, Box4, Size, f64);
11pub type RunKey = (String, u64, i32, i32, i32, String);
12
13pub(crate) fn quantile(mut values: Vec<f64>, fraction: f64) -> f64 {
15 if values.is_empty() {
16 return 0.0;
17 }
18 values.sort_by(|a, b| a.partial_cmp(b).unwrap());
19 let position = fraction * (values.len() - 1) as f64;
20 let lower = position.floor() as usize;
21 let upper = position.ceil() as usize;
22 if lower == upper {
23 return values[lower];
24 }
25 let weight = position - lower as f64;
26 values[lower] * (1.0 - weight) + values[upper] * weight
27}
28
29#[derive(Default)]
30struct Run {
31 ratios: Vec<f64>,
32 overlaps: usize,
33 line_count: usize,
34 inflated: HashSet<(usize, i64)>,
35 extreme: usize,
36}
37
38#[derive(Default)]
39pub struct Risk {
40 runs: HashMap<usize, Run>,
41 y_ratios: Vec<f64>,
42 split: bool,
43}
44
45impl Risk {
46 pub fn add_line(
48 &mut self,
49 page: usize,
50 source: i64,
51 height: f64,
52 skip_y: bool,
53 entries: Vec<Entry>,
54 ) -> Option<bool> {
55 if !height.is_finite()
56 || entries.iter().any(|e| {
57 !e.4.is_finite() || e.1.iter().chain(&e.2).chain(&e.3).any(|v| !v.is_finite())
58 })
59 {
60 return None;
61 }
62 if entries.is_empty() {
63 return Some(false);
64 }
65 let q75 = quantile(entries.iter().map(|e| e.2[3] - e.2[1]).collect(), 0.75);
66 if !q75.is_finite() {
67 return None;
68 }
69 let anchors: Vec<_> = entries
70 .into_iter()
71 .filter(|e| e.2[3] - e.2[1] >= 0.38 * q75)
72 .collect();
73 let pairs = anchor_pairs(anchors.clone(), false)?;
75 for (index, _, ratio, overlap) in pairs {
76 let run = self.runs.entry(anchors[index].0).or_default();
77 run.ratios.push(ratio);
78 run.overlaps += usize::from(overlap);
79 }
80 let mut by_run: HashMap<usize, Vec<&Entry>> = HashMap::new();
81 for entry in &anchors {
82 by_run.entry(entry.0).or_default().push(entry);
83 }
84 for (id, group) in by_run {
85 let run = self.runs.entry(id).or_default();
86 run.line_count += 1;
87 let fonts: Vec<_> = group
88 .iter()
89 .filter_map(|e| (e.4 > 0.0).then_some(e.4))
90 .collect();
91 let tight = quantile(group.iter().map(|e| e.2[3] - e.2[1]).collect(), 0.75);
92 let scale = median(fonts).max(tight);
93 if scale >= 4.0 && height > 1.50 * scale && height > 1.80 * 0.1_f64.max(tight) {
94 run.inflated.insert((page, source));
95 }
96 }
97 if anchors.len() < 4 || skip_y {
98 return Some(false);
99 }
100 let groups = ordered_clusters(
101 anchors.iter().map(|e| e.3[1]).collect(),
102 0.5_f64.max(0.25 * q75),
103 0.0,
104 true,
105 )?;
106 if groups
107 .iter()
108 .filter(|g| g.len() >= 3 && g.len() as f64 / anchors.len() as f64 >= 0.20)
109 .count()
110 >= 2
111 {
112 self.split = true;
113 return Some(true);
114 }
115 let mut dominant = &groups[0];
117 for group in &groups[1..] {
118 if group.len() > dominant.len() {
119 dominant = group;
120 }
121 }
122 if dominant.len() as f64 / (anchors.len() as f64) < 0.80 {
123 return Some(false);
124 }
125 let first = &anchors[dominant[0]];
126 let (mut source_top, mut source_bottom, mut tight_top, mut tight_bottom) =
127 (first.1[1], first.1[3], first.2[1], first.2[3]);
128 let mut counts = HashMap::<usize, usize>::new();
129 let mut order = Vec::new();
130 for index in dominant {
131 let entry = &anchors[*index];
132 source_top = source_top.min(entry.1[1]);
133 source_bottom = source_bottom.max(entry.1[3]);
134 tight_top = tight_top.min(entry.2[1]);
135 tight_bottom = tight_bottom.max(entry.2[3]);
136 let count = counts.entry(entry.0).or_default();
137 if *count == 0 {
138 order.push(entry.0);
139 }
140 *count += 1;
141 }
142 let tight_height = tight_bottom - tight_top;
143 if tight_height <= 0.0 {
144 return Some(false);
145 }
146 let ratio = (source_bottom - source_top) / tight_height;
147 if !ratio.is_finite() {
148 return None;
149 }
150 self.y_ratios.push(ratio);
151 if ratio >= 3.0 {
152 let mut winner = order[0];
153 for id in order {
154 if counts[&id] > counts[&winner] {
155 winner = id;
156 }
157 }
158 self.runs.entry(winner).or_default().extreme += 1;
159 }
160 Some(false)
161 }
162
163 pub fn finish(&self) -> (bool, bool) {
165 if self.split {
166 return (true, false);
167 }
168 let layout = self.runs.values().any(|r| {
169 let n = r.ratios.len();
170 n >= 30
171 && median(r.ratios.clone()) >= 1.30
172 && r.ratios.iter().filter(|v| **v > 1.35).count() as f64 / n as f64 >= 0.30
173 && r.overlaps as f64 / n as f64 >= 0.30
174 }) || (!self.y_ratios.is_empty()
175 && (quantile(self.y_ratios.clone(), 0.95) >= 2.20
176 || self.runs.values().any(|r| r.extreme >= 3)));
177 let style = self.runs.values().any(|r| {
178 r.line_count >= 3
179 && r.inflated.len() >= 3
180 && r.inflated.len() as f64 / r.line_count as f64 >= 0.50
181 && r.inflated.iter().map(|v| v.0).collect::<HashSet<_>>().len() >= 2
182 });
183 (layout, style)
184 }
185}