1use crate::{geometry::Box4, median};
3use std::collections::HashMap;
4
5pub struct Character {
6 pub bbox: Box4,
7 pub tight: Option<Box4>,
8 pub index: usize,
9 pub fragment: String,
10 pub visible: bool,
11 pub space: bool,
12 pub marker: bool,
13 pub bold: bool,
14}
15pub struct Line {
16 pub bbox: Box4,
17 pub source: i64,
18 pub members: Vec<usize>,
19}
20pub struct Drawing {
21 pub bbox: Box4,
22 pub width: f64,
23}
24pub type Payload = (Box4, String, Vec<(usize, usize, u8)>, i64);
25struct Candidate {
26 line: Line,
27 visible: Vec<usize>,
28 height: f64,
29 center: f64,
30 bottom: f64,
31 styles: Vec<u8>,
32}
33struct Match {
34 line: usize,
35 style: usize,
36 start: usize,
37 end: usize,
38 distance: f64,
39 overlap: f64,
40}
41
42fn valid(b: Box4) -> bool {
44 b.iter().all(|v| v.is_finite()) && b[2] > b[0] && b[3] > b[1]
45}
46
47fn candidate(mut line: Line, chars: &[Character], min_bold: usize) -> Option<Candidate> {
49 if !valid(line.bbox) {
50 return None;
51 }
52 line.members.sort_by_key(|&i| chars[i].index);
53 let visible: Vec<_> = line
54 .members
55 .iter()
56 .enumerate()
57 .filter_map(|(i, &m)| (chars[m].visible && valid(chars[m].bbox)).then_some(i))
58 .collect();
59 if visible.is_empty() {
60 return None;
61 }
62 let height = median(
63 visible
64 .iter()
65 .map(|&i| {
66 let b = chars[line.members[i]].bbox;
67 b[3] - b[1]
68 })
69 .collect(),
70 );
71 if height <= 0.0 {
72 return None;
73 }
74 let body: Vec<_> = visible
75 .iter()
76 .map(|&i| chars[line.members[i]].bbox)
77 .filter(|b| b[3] - b[1] >= 0.8 * height)
78 .collect();
79 if body.is_empty() {
80 return None;
81 }
82 let center = median(body.iter().map(|b| (b[1] + b[3]) / 2.0).collect());
83 let bottom = median(body.iter().map(|b| b[3]).collect());
84 let mut styles: Vec<u8> = line
85 .members
86 .iter()
87 .map(|&i| u8::from(chars[i].bold))
88 .collect();
89 let comparable: Vec<_> = line
90 .members
91 .iter()
92 .enumerate()
93 .filter_map(|(i, &m)| (!chars[m].fragment.is_empty()).then_some(i))
94 .collect();
95 let mut start = 0;
96 while start < comparable.len() {
97 if styles[comparable[start]] & 1 == 0 {
98 start += 1;
99 continue;
100 }
101 let mut end = start + 1;
102 while end < comparable.len() && styles[comparable[end]] & 1 != 0 {
103 end += 1;
104 }
105 let short = comparable[start..end]
106 .iter()
107 .map(|&i| chars[line.members[i]].fragment.chars().count())
108 .sum::<usize>()
109 < min_bold;
110 let marker = start == 0
111 && end < comparable.len()
112 && comparable[start..end]
113 .iter()
114 .all(|&i| chars[line.members[i]].marker)
115 && {
116 let a = comparable[end - 1];
117 let b = comparable[end];
118 line.members[a + 1..b].iter().any(|&i| chars[i].space) || {
119 let left = chars[line.members[a]].bbox;
120 let right = chars[line.members[b]].bbox;
121 valid(left) && valid(right) && right[0] - left[2] >= 0.5 * height
122 }
123 };
124 if short || marker {
125 for &i in &comparable[start..end] {
126 styles[i] &= !1;
127 }
128 }
129 start = end;
130 }
131 Some(Candidate {
132 line,
133 visible,
134 height,
135 center,
136 bottom,
137 styles,
138 })
139}
140
141fn drawing_match(
143 line: &Candidate,
144 chars: &[Character],
145 drawing: &Drawing,
146 style: usize,
147 thresholds: &[f64; 8],
148) -> Option<Match> {
149 let b = drawing.bbox;
150 let length = b[2] - b[0];
151 if length < thresholds[0] * line.height {
152 return None;
153 }
154 let y = (b[1] + b[3]) / 2.0;
155 let target = if style == 0 { line.bottom } else { line.center };
156 let distance = (y - target).abs() / line.height;
157 if distance > thresholds[1 + style] || drawing.width.max(0.0) > thresholds[3] * line.height {
158 return None;
159 }
160 let hit: Vec<_> = line
161 .visible
162 .iter()
163 .copied()
164 .filter(|&i| {
165 let c = chars[line.line.members[i]].bbox;
166 b[0] <= (c[0] + c[2]) / 2.0 && (c[0] + c[2]) / 2.0 <= b[2]
167 })
168 .collect();
169 if hit.is_empty() {
170 return None;
171 }
172 if style == 1 {
173 let boxes: Vec<_> = hit
174 .iter()
175 .map(|&i| chars[line.line.members[i]].tight.filter(|&b| valid(b)))
176 .collect();
177 if boxes.iter().all(Option::is_some)
178 && !boxes.iter().flatten().any(|b| b[1] <= y && y <= b[3])
179 {
180 return None;
181 }
182 }
183 let left = hit
184 .iter()
185 .map(|&i| chars[line.line.members[i]].bbox[0])
186 .fold(f64::INFINITY, f64::min);
187 let right = hit
188 .iter()
189 .map(|&i| chars[line.line.members[i]].bbox[2])
190 .fold(f64::NEG_INFINITY, f64::max);
191 if (right - left) / length < thresholds[4]
192 || (b[0] - left).abs().min((b[2] - right).abs()) > thresholds[5] * line.height
193 {
194 return None;
195 }
196 let overlap = (line.line.bbox[2].min(b[2]) - line.line.bbox[0].max(b[0])).max(0.0)
197 / (line.line.bbox[2] - line.line.bbox[0])
198 .min(length)
199 .max(0.01);
200 Some(Match {
201 line: 0,
202 style,
203 start: *hit.first().unwrap(),
204 end: hit.last().unwrap() + 1,
205 distance,
206 overlap,
207 })
208}
209
210pub fn detect(
212 chars: &[Character],
213 lines: Vec<Line>,
214 drawings: &[Drawing],
215 thresholds: [f64; 8],
216 min_bold: usize,
217) -> Option<Vec<Payload>> {
218 let mut candidates: Vec<_> = lines
219 .into_iter()
220 .filter_map(|line| candidate(line, chars, min_bold))
221 .collect();
222 if candidates.is_empty() {
223 return Some(Vec::new());
224 }
225 if !drawings.is_empty() {
226 let grid = median(candidates.iter().map(|c| c.height).collect()).max(1.0);
227 let mut anchors: HashMap<i64, Vec<(usize, usize)>> = HashMap::new();
228 let mut tops: HashMap<i64, Vec<usize>> = HashMap::new();
229 for (i, c) in candidates.iter().enumerate() {
230 if (c.line.bbox[1] / grid).abs() > 1e15 {
231 return None;
232 }
233 tops.entry((c.line.bbox[1] / grid).floor() as i64)
234 .or_default()
235 .push(i);
236 for (style, target) in [c.bottom, c.center].into_iter().enumerate() {
237 let tolerance = thresholds[1 + style] * c.height;
238 let start = ((target - tolerance) / grid).floor();
239 let end = ((target + tolerance) / grid).floor();
240 if start.abs() > 1e15 || end.abs() > 1e15 || end - start > 4096.0 {
241 return None;
242 }
243 for cell in start as i64..=end as i64 {
244 anchors.entry(cell).or_default().push((i, style));
245 }
246 }
247 }
248 for drawing in drawings {
249 if !valid(drawing.bbox) {
250 continue;
251 }
252 let y = (drawing.bbox[1] + drawing.bbox[3]) / 2.0;
253 if (y / grid).abs() > 1e15 {
254 return None;
255 }
256 let mut best: Option<Match> = None;
257 for &(i, style) in anchors
258 .get(&((y / grid).floor() as i64))
259 .into_iter()
260 .flatten()
261 {
262 let c = &candidates[i];
263 let Some(mut m) = drawing_match(c, chars, drawing, style, &thresholds) else {
264 continue;
265 };
266 if style == 0 {
267 let max_top = y + thresholds[6] * c.height;
268 let start = (y / grid).floor();
269 let end = (max_top / grid).floor();
270 if end.abs() > 1e15 || end - start > 4096.0 {
271 return None;
272 }
273 let fraction = (start as i64..=end as i64).any(|cell| {
274 tops.get(&cell).into_iter().flatten().any(|&j| {
275 let b = candidates[j].line.bbox;
276 j != i
277 && y <= b[1]
278 && b[1] <= max_top
279 && (drawing.bbox[2].min(b[2]) - drawing.bbox[0].max(b[0])).max(0.0)
280 / (b[2] - b[0]).max(0.01)
281 >= thresholds[7]
282 })
283 });
284 if fraction {
285 continue;
286 }
287 }
288 m.line = i;
289 let key = (m.distance, -m.overlap, c.line.source, m.style);
290 if best.as_ref().is_none_or(|b| {
291 key < (
292 b.distance,
293 -b.overlap,
294 candidates[b.line].line.source,
295 b.style,
296 )
297 }) {
298 best = Some(m);
299 }
300 }
301 if let Some(m) = best {
302 for value in &mut candidates[m.line].styles[m.start..m.end] {
303 *value |= 2 << m.style;
304 }
305 }
306 }
307 }
308 let mut output = Vec::new();
309 for c in candidates {
310 let mut text = String::new();
311 let mut ranges = Vec::new();
312 let mut offset = 0;
313 let mut active = 0;
314 let mut active_start = 0;
315 for (i, &m) in c.line.members.iter().enumerate() {
316 let fragment = &chars[m].fragment;
317 if fragment.is_empty() {
318 continue;
319 }
320 let style = c.styles[i];
321 if style != active {
322 if active != 0 {
323 ranges.push((active_start, offset, active));
324 }
325 active_start = offset;
326 active = style;
327 }
328 text.push_str(fragment);
329 offset += fragment.chars().count();
330 }
331 if active != 0 {
332 ranges.push((active_start, offset, active));
333 }
334 if !text.is_empty() {
335 output.push((c.line.bbox, text, ranges, c.line.source));
336 }
337 }
338 if !output.iter().any(|r| !r.2.is_empty()) {
339 return Some(Vec::new());
340 }
341 output.sort_by(|a, b| {
342 a.3.cmp(&b.3)
343 .then_with(|| a.0[1].partial_cmp(&b.0[1]).unwrap())
344 .then_with(|| a.0[0].partial_cmp(&b.0[0]).unwrap())
345 });
346 Some(output)
347}