1use crate::{
3 geometry_risk::quantile,
4 geometry_runs::{Run, Sample},
5 median,
6};
7use std::collections::{HashMap, HashSet};
8
9pub struct Metadata {
10 pub page: i64,
11 pub source: i64,
12 pub height: f64,
13}
14
15pub struct Style {
16 pub inflated: Vec<usize>,
17 pub scales: Vec<(usize, f64)>,
18}
19
20pub fn prepare(
22 samples: &[Sample],
23 lines: &[Vec<usize>],
24 runs: &[Run],
25 metadata: &[Metadata],
26) -> Option<Style> {
27 if samples.len() != metadata.len()
28 || metadata.iter().any(|m| !m.height.is_finite())
29 || samples
30 .iter()
31 .any(|s| !(s.tight[3] - s.tight[1]).is_finite())
32 {
33 return None;
34 }
35 let mut inflated = Vec::new();
36 for (id, run) in runs.iter().enumerate() {
37 let mut groups: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
38 for index in &run.members {
39 if samples[*index].anchor {
40 let m = &metadata[*index];
41 groups.entry((m.page, m.source)).or_default().push(*index);
42 }
43 }
44 if groups.len() < 3 {
45 continue;
46 }
47 let mut count = 0usize;
48 let mut pages = HashSet::new();
49 for ((page, _), members) in &groups {
50 let fonts = members
51 .iter()
52 .filter_map(|i| (samples[*i].size > 0.0).then_some(samples[*i].size))
53 .collect();
54 let tight = quantile(
55 members
56 .iter()
57 .map(|i| samples[*i].tight[3] - samples[*i].tight[1])
58 .collect(),
59 0.75,
60 );
61 let scale = median(fonts).max(tight);
62 let height = metadata[members[0]].height;
63 if scale >= 4.0 && height > 1.50 * scale && height > 1.80 * 0.1_f64.max(tight) {
64 count += 1;
65 pages.insert(*page);
66 }
67 }
68 if count >= 3 && count as f64 / groups.len() as f64 >= 0.50 && pages.len() >= 2 {
69 inflated.push(id);
70 }
71 }
72 let mut scales = Vec::new();
73 if !inflated.is_empty() {
75 for (line_index, line) in lines.iter().enumerate() {
76 let mut counts = HashMap::<usize, usize>::new();
77 let mut order = Vec::new();
78 for index in line {
79 let sample = &samples[*index];
80 if sample.anchor {
81 let count = counts.entry(sample.run).or_default();
82 if *count == 0 {
83 order.push(sample.run);
84 }
85 *count += 1;
86 }
87 }
88 let Some(&first) = order.first() else {
89 continue;
90 };
91 let mut dominant = first;
92 for id in order {
93 if counts[&id] > counts[&dominant] {
94 dominant = id;
95 }
96 }
97 let members: Vec<_> = line
98 .iter()
99 .copied()
100 .filter(|i| samples[*i].anchor && samples[*i].run == dominant)
101 .collect();
102 let fonts = members
103 .iter()
104 .filter_map(|i| (samples[*i].size > 0.0).then_some(samples[*i].size))
105 .collect();
106 let tight = quantile(
107 members
108 .iter()
109 .map(|i| samples[*i].tight[3] - samples[*i].tight[1])
110 .collect(),
111 0.75,
112 );
113 let scale = median(fonts).max(tight).max(0.1);
114 if scale >= 4.0 {
115 scales.push((line_index, scale));
116 }
117 }
118 }
119 Some(Style { inflated, scales })
120}
121
122#[derive(Default)]
124pub struct Document {
125 samples: Vec<Sample>,
126 metadata: Vec<Metadata>,
127 lines: Vec<Vec<usize>>,
128 line_keys: Vec<(i64, i64)>,
129 line_ids: HashMap<(i64, i64), usize>,
130}
131
132pub type Report = (
133 usize,
134 usize,
135 usize,
136 Option<f64>,
137 Option<f64>,
138 f64,
139 f64,
140 bool,
141 bool,
142 bool,
143);
144pub type Result = (Vec<usize>, Vec<((i64, i64), f64)>, Vec<Report>);
145
146pub type Legacy = (Option<crate::geometry::Box4>, crate::geometry::Size, i32);
148
149pub struct Prepared {
150 pub samples: Vec<Sample>,
151 pub metadata: Vec<Metadata>,
152 pub lines: Vec<Vec<usize>>,
153 pub line_keys: Vec<(i64, i64)>,
154 pub runs: Vec<Run>,
155 pub style: Style,
156 pub restored: Vec<Option<crate::geometry::Box4>>,
157}
158
159impl Document {
160 pub fn append(&mut self, page: i64, source: i64, height: f64, records: Vec<Sample>) {
162 if records.is_empty() {
163 return;
164 }
165 let key = (page, source);
166 let line = *self.line_ids.entry(key).or_insert_with(|| {
167 self.line_keys.push(key);
168 self.lines.push(Vec::new());
169 self.lines.len() - 1
170 });
171 for sample in records {
172 self.lines[line].push(self.samples.len());
173 self.samples.push(sample);
174 self.metadata.push(Metadata {
175 page,
176 source,
177 height,
178 });
179 }
180 }
181
182 pub fn prepare(mut self, families: &[usize], legacy: Option<Vec<Legacy>>) -> Option<Prepared> {
184 for line in &self.lines {
185 let heights = line
186 .iter()
187 .filter_map(|i| {
188 self.samples[*i]
189 .anchor
190 .then_some(self.samples[*i].tight[3] - self.samples[*i].tight[1])
191 })
192 .collect::<Vec<_>>();
193 if heights.iter().any(|h| !h.is_finite()) {
194 return None;
195 }
196 let q75 = quantile(heights, 0.75);
197 for index in line {
198 let sample = &mut self.samples[*index];
199 sample.anchor =
200 sample.anchor && q75 > 0.0 && sample.tight[3] - sample.tight[1] >= 0.38 * q75;
201 }
202 }
203 let mut runs = crate::geometry_runs::group(&self.samples, families)?;
204 let style = prepare(&self.samples, &self.lines, &runs, &self.metadata)?;
205 let mut restored = Vec::new();
206 if !style.inflated.is_empty() {
207 if let Some(legacy) = legacy {
208 if legacy.len() != self.samples.len() {
209 return None;
210 }
211 for (sample, (raw, size, angle)) in self.samples.iter_mut().zip(legacy) {
212 let bbox = crate::geometry::clip(crate::geometry::normalize(raw, false), size);
213 if let Some(bbox) = bbox {
214 sample.source = crate::geometry::rotate(bbox, size, angle);
215 }
216 restored.push(bbox);
217 }
218 }
219 }
220 crate::geometry_runs::complete(&self.samples, &self.lines, families, &mut runs)?;
221 Some(Prepared {
222 samples: self.samples,
223 metadata: self.metadata,
224 lines: self.lines,
225 line_keys: self.line_keys,
226 runs,
227 style,
228 restored,
229 })
230 }
231
232 pub fn finish(self, families: Vec<usize>) -> Option<Result> {
234 let Prepared {
235 runs,
236 style,
237 line_keys,
238 ..
239 } = self.prepare(&families, None)?;
240 let reports = reports(&runs, &style);
241 Some((
242 style.inflated,
243 style
244 .scales
245 .into_iter()
246 .map(|(i, v)| (line_keys[i], v))
247 .collect(),
248 reports,
249 ))
250 }
251}
252
253pub fn reports(runs: &[Run], style: &Style) -> Vec<Report> {
255 let inflated: HashSet<_> = style.inflated.iter().copied().collect();
256 let mut reports = Vec::with_capacity(runs.len());
257 for (id, run) in runs.iter().enumerate() {
258 let count = run.ratios.len();
259 reports.push((
260 id,
261 run.members.len(),
262 count,
263 (count > 0).then(|| median(run.ratios.clone())),
264 (count > 0).then(|| quantile(run.ratios.clone(), 0.9)),
265 if count > 0 {
266 run.ratios.iter().filter(|v| **v > 1.35).count() as f64 / count as f64
267 } else {
268 0.0
269 },
270 if count > 0 {
271 run.overlaps.iter().filter(|v| **v).count() as f64 / count as f64
272 } else {
273 0.0
274 },
275 run.strong,
276 run.sibling,
277 inflated.contains(&id),
278 ));
279 }
280 reports
281}