Skip to main content

docvortex_core/
geometry_risk.rs

1//! 在文档内连续累积 X、Y 和跨页样式风险,不回传逐字符中间对象。
2use 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
13/// 保留线性插值分位数,不复用其他模块的取整采样规则。
14pub(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    /// 消费一行原始锚点;校验失败返回 None,由边界选择显式参考计算。
47    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        // 先验证相邻对;任何后续拒绝都会要求边界丢弃整个累积器。
74        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        // Python max 在并列时选择首簇,不能使用选择末项的 max_by_key。
116        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    /// 汇总完整文档统计,保留 split 提前返回时 style 为 false 的语义。
164    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}