Skip to main content

docvortex_core/
rule_graphics.rs

1//! 新增图形规则的批量数值内核,不修改候选集合或判定阈值。
2
3/// 横线对及其完整原行成员索引,顺序与 Python 嵌套循环一致。
4pub type CodeRuleWindow = (usize, usize, Vec<usize>);
5
6/// 每个区域返回全部满足原占首框面积比例的成员,重复框和原成员顺序均保留。
7pub fn overlap_member_groups(
8    members: &[[f64; 4]],
9    regions: &[[f64; 4]],
10    threshold: f64,
11) -> Vec<Vec<usize>> {
12    regions
13        .iter()
14        .map(|&region| {
15            members
16                .iter()
17                .enumerate()
18                .filter(|(_, bbox)| code_overlap(**bbox, region, false) >= threshold)
19                .map(|(i, _)| i)
20                .collect()
21        })
22        .collect()
23}
24
25/// 按原交集及面积公式计算覆盖率;退化框的零值条件也保持一致。
26fn code_overlap(first: [f64; 4], second: [f64; 4], smaller: bool) -> f64 {
27    let width = 0.0_f64.max(first[2].min(second[2]) - first[0].max(second[0]));
28    let height = 0.0_f64.max(first[3].min(second[3]) - first[1].max(second[1]));
29    let area = if smaller {
30        ((first[2] - first[0]) * (first[3] - first[1]))
31            .min((second[2] - second[0]) * (second[3] - second[1]))
32    } else {
33        0.0_f64.max(first[2] - first[0]) * 0.0_f64.max(first[3] - first[1])
34    };
35    if area > 0.0 {
36        width * height / area
37    } else {
38        0.0
39    }
40}
41
42/// 全量扫描横线对及内部轨道,仅迁移数值筛选,文字结构判定和最终框仍在 Python。
43pub fn code_rule_windows(
44    horizontal: &[[f64; 4]],
45    vertical: &[[f64; 4]],
46    lines: &[[f64; 4]],
47    excluded: &[[f64; 4]],
48    em: f64,
49    page_height: f64,
50) -> Vec<CodeRuleWindow> {
51    let tolerance = 2.0_f64.max(0.75 * em);
52    let mut output = Vec::new();
53    for (top_index, top) in horizontal.iter().enumerate() {
54        for (bottom_index, bottom) in horizontal.iter().enumerate().skip(top_index + 1) {
55            if (top[0] - bottom[0]).abs() > tolerance || (top[2] - bottom[2]).abs() > tolerance {
56                continue;
57            }
58            let bbox = [
59                top[0].min(bottom[0]),
60                top[1].min(bottom[1]),
61                top[2].max(bottom[2]),
62                top[3].max(bottom[3]),
63            ];
64            let height = bbox[3] - bbox[1];
65            if !(6.0 * em <= height && height <= 0.5 * page_height)
66                || excluded
67                    .iter()
68                    .any(|&other| code_overlap(bbox, other, true) >= 0.5)
69                || horizontal.iter().any(|&rule| {
70                    top[3] + 0.5 * em < rule[1]
71                        && rule[1] < bottom[1] - 0.5 * em
72                        && code_overlap(rule, bbox, false) >= 0.8
73                        && rule[2] - rule[0] >= 0.6 * (bbox[2] - bbox[0])
74                })
75                || vertical.iter().any(|rule| {
76                    let center = (rule[0] + rule[2]) / 2.0;
77                    bbox[0] + em < center
78                        && center < bbox[2] - em
79                        && 0.0_f64.max(rule[3].min(bbox[3]) - rule[1].max(bbox[1]))
80                            / 0.0_f64.max(height)
81                            >= 0.6
82                })
83            {
84                continue;
85            }
86            let members = lines
87                .iter()
88                .enumerate()
89                .filter(|(_, line)| {
90                    let center_x = (line[0] + line[2]) / 2.0;
91                    let center_y = (line[1] + line[3]) / 2.0;
92                    bbox[0] - 0.5 * em <= center_x
93                        && center_x <= bbox[2] + 0.5 * em
94                        && top[3] <= center_y
95                        && center_y <= bottom[1]
96                })
97                .map(|(i, _)| i)
98                .collect();
99            output.push((top_index, bottom_index, members));
100        }
101    }
102    output
103}
104
105/// 规范字体、flags、有效字重、有效字符数与按首次出现排序的每字三份框样本。
106pub type GlyphFontGroup = (
107    String,
108    i32,
109    Option<f64>,
110    usize,
111    Vec<(String, Vec<[f64; 4]>)>,
112);
113
114/// 以已验证的新鲜快照分组 ASCII 字形,字号不参与字体键,重复成员和插入顺序不去重。
115pub fn glyph_font_groups(
116    data: &crate::text_snapshot::TextSnapshot,
117    rows: &[Vec<usize>],
118    bold: &[bool],
119) -> Vec<GlyphFontGroup> {
120    let mut groups: Vec<GlyphFontGroup> = Vec::new();
121    let mut font_keys = std::collections::HashMap::new();
122    let mut font_groups = vec![None; data.fonts.len()];
123    let mut letters: Vec<[Option<usize>; 128]> = Vec::new();
124    for &index in rows.iter().flatten() {
125        let ch = &data.chars[index];
126        if ch.text.len() != 1 || !ch.text.as_bytes()[0].is_ascii_alphabetic() {
127            continue;
128        }
129        let font = &data.fonts[ch.font];
130        if font.flags & (1 << 6) != 0 || bold[ch.font] {
131            continue;
132        }
133        let Some(bbox) = ch.tight else {
134            continue;
135        };
136        let group = if let Some(group) = font_groups[ch.font] {
137            group
138        } else {
139            let bytes = font.name.as_bytes();
140            let name = if bytes.len() >= 7
141                && bytes[..6].iter().all(u8::is_ascii_uppercase)
142                && bytes[6] == b'+'
143            {
144                font.name[7..].to_owned()
145            } else {
146                font.name.clone()
147            };
148            let weight = font.weight.max(0);
149            let key = (name.clone(), font.flags, weight);
150            let group = if let Some(&group) = font_keys.get(&key) {
151                group
152            } else {
153                let group = groups.len();
154                groups.push((
155                    name,
156                    font.flags,
157                    (weight > 0).then_some(weight as f64),
158                    0,
159                    Vec::new(),
160                ));
161                letters.push([None; 128]);
162                font_keys.insert(key, group);
163                group
164            };
165            font_groups[ch.font] = Some(group);
166            group
167        };
168        groups[group].3 += 1;
169        let letter = ch.text.as_bytes()[0] as usize;
170        let letter_group = if let Some(position) = letters[group][letter] {
171            position
172        } else {
173            let position = groups[group].4.len();
174            groups[group].4.push((ch.text.clone(), Vec::new()));
175            letters[group][letter] = Some(position);
176            position
177        };
178        let samples = &mut groups[group].4[letter_group].1;
179        if samples.len() < 3 {
180            samples.push(bbox);
181        }
182    }
183    groups
184}
185
186/// 每个原路径只检查两个方向一次,单个矩形只贡献一次支持票。
187pub fn compound_baselines(groups: &[Vec<[f64; 4]>], em: f64) -> Vec<(bool, bool)> {
188    groups
189        .iter()
190        .map(|boxes| {
191            let shared = |edge: usize| {
192                boxes.len() <= 1
193                    || boxes.iter().any(|seed| {
194                        [seed[edge], seed[edge + 2]].iter().any(|baseline| {
195                            boxes
196                                .iter()
197                                .filter(|b| {
198                                    (b[edge] - baseline)
199                                        .abs()
200                                        .min((b[edge + 2] - baseline).abs())
201                                        <= 0.35 * em
202                                })
203                                .count() as f64
204                                >= 0.75 * boxes.len() as f64
205                        })
206                    })
207            };
208            (shared(0), shared(1))
209        })
210        .collect()
211}
212
213/// 数值刻度稳定分组并核验等差值与间距;每次追加仍采用原样本中位数作为锚点。
214pub fn raster_axis_groups(lines: &[([f64; 4], f64, f64)]) -> Vec<(bool, Vec<usize>, f64)> {
215    let mut output = Vec::new();
216    for vertical in [true, false] {
217        let coordinate = |i: usize| {
218            if vertical {
219                lines[i].0[2]
220            } else {
221                (lines[i].0[1] + lines[i].0[3]) / 2.0
222            }
223        };
224        let position = |i: usize| {
225            if vertical {
226                (lines[i].0[1] + lines[i].0[3]) / 2.0
227            } else {
228                (lines[i].0[0] + lines[i].0[2]) / 2.0
229            }
230        };
231        let mut order: Vec<_> = (0..lines.len()).collect();
232        order.sort_by(|&a, &b| coordinate(a).partial_cmp(&coordinate(b)).unwrap());
233        let mut groups: Vec<Vec<usize>> = Vec::new();
234        for i in order {
235            let append = groups.last().is_some_and(|prior| {
236                let anchor = crate::median(prior.iter().map(|&j| coordinate(j)).collect());
237                let mut heights: Vec<_> = prior.iter().map(|&j| lines[j].1).collect();
238                heights.push(lines[i].1);
239                (coordinate(i) - anchor).abs() <= 0.4 * crate::median(heights)
240            });
241            if append {
242                groups.last_mut().unwrap().push(i);
243            } else {
244                groups.push(vec![i]);
245            }
246        }
247        for mut ticks in groups {
248            if ticks.len() < 5 {
249                continue;
250            }
251            ticks.sort_by(|&a, &b| position(a).partial_cmp(&position(b)).unwrap());
252            let em = crate::median(ticks.iter().map(|&j| lines[j].1).collect());
253            let steps: Vec<_> = ticks
254                .windows(2)
255                .map(|p| lines[p[1]].2 - lines[p[0]].2)
256                .collect();
257            let gaps: Vec<_> = ticks
258                .windows(2)
259                .map(|p| position(p[1]) - position(p[0]))
260                .collect();
261            if steps[0].abs() < 1e-8
262                || steps
263                    .iter()
264                    .any(|s| (s - steps[0]).abs() > 0.01 * steps[0].abs())
265                || gaps.iter().copied().reduce(f64::max).unwrap()
266                    - gaps.iter().copied().reduce(f64::min).unwrap()
267                    > 0.4 * em
268                || gaps.iter().any(|&g| g < em)
269                || ticks
270                    .iter()
271                    .any(|&j| lines[j].1 < 0.8 * em || lines[j].1 > 1.2 * em)
272            {
273                continue;
274            }
275            output.push((vertical, ticks, em));
276        }
277    }
278    output
279}
280
281/// 新路径先放在合并组首位,其余成员按既有组顺序拼接,保留原规则遍历与平局行为。
282pub fn isolated_path_groups(boxes: &[[f64; 4]], em: f64) -> Option<Vec<Vec<usize>>> {
283    let mut groups: Vec<Vec<usize>> = Vec::new();
284    for (i, b) in boxes.iter().enumerate() {
285        let mut retained = Vec::new();
286        let mut merged = vec![i];
287        for group in groups {
288            let mut connected = false;
289            for &j in &group {
290                let a = boxes[j];
291                let x = (b[0] - a[2]).max(a[0] - b[2]).max(0.0);
292                let y = (b[1] - a[3]).max(a[1] - b[3]).max(0.0);
293                let distance = x.hypot(y);
294                let limit = 0.35 * em;
295                // CPython 与平台 hypot 的末位舍入可能不同,临界输入整批使用参考实现。
296                if (distance - limit).abs() <= 8.0 * f64::EPSILON * distance.abs().max(limit.abs())
297                {
298                    return None;
299                }
300                if distance <= limit {
301                    connected = true;
302                    break;
303                }
304            }
305            if connected {
306                merged.extend(group);
307            } else {
308                retained.push(group);
309            }
310        }
311        retained.push(merged);
312        groups = retained;
313    }
314    Some(groups)
315}
316
317/// 用绘制顺序和完全包含判断覆盖,返回地址本身以便 Python 保留可见性映射顺序。
318pub fn overpainted_addresses(
319    texts: &[(usize, usize, [f64; 4])],
320    covers: &[(usize, [f64; 4])],
321) -> Vec<usize> {
322    texts
323        .iter()
324        .filter(|(order, _, b)| {
325            covers.iter().any(|(later, c)| {
326                later > order && c[0] <= b[0] && c[1] <= b[1] && c[2] >= b[2] && c[3] >= b[3]
327            })
328        })
329        .map(|(_, a, _)| *a)
330        .collect()
331}
332
333/// 按完整横线与成员框筛选贴邻表头,返回原成员顺序,不处理年份或最终成员认领。
334pub fn year_header_groups(lines: &[[f64; 4]], rules: &[[f64; 4]], em: f64) -> Vec<Vec<usize>> {
335    rules
336        .iter()
337        .map(|r| {
338            lines
339                .iter()
340                .enumerate()
341                .filter_map(|(i, b)| {
342                    (r[0] - 0.3 * em <= b[0]
343                        && b[2] <= r[2] + 0.3 * em
344                        && 0.0 <= r[1] - b[3]
345                        && r[1] - b[3] <= 0.8 * em)
346                        .then_some(i)
347                })
348                .collect()
349        })
350        .collect()
351}