Skip to main content

docvortex_core/
inline_styles.rs

1//! 在快照字符上连续构造样式候选、匹配装饰线并输出紧凑区间。
2use 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
42/// 与 Python 严格框规范一致,零面积框不参与候选。
43fn valid(b: Box4) -> bool {
44    b.iter().all(|v| v.is_finite()) && b[2] > b[0] && b[3] > b[1]
45}
46
47/// 按原始字符索引稳定排序,过滤短粗体与分离的行首项目符号。
48fn 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
141/// 保留目标距离、覆盖、端点和墨迹穿越的全部阈值,返回原字符半开区间。
142fn 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
210/// 先建立相同网格超集,再逐条绘图线选择同一最优目标;极端网格范围返回参考选择。
211pub 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}