Skip to main content

docvortex_core/
geometry_style.rs

1//! 复用同一批 run 与样本,连续计算跨页样式异常和全文逐行字号校准。
2use 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
20/// 只复用已有数值,不物化 Python 字符;保持首锚点高度、重复来源行和并列字体规则。
21pub 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    // 全文异常触发后校准所有行,不能只处理属于异常 run 的行。
74    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/// 持有整本样式文档的数值样本,跨阶段不创建 Python 字符对象。
123#[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
146/// 完成锚点、run 和样式计算后的自有数据,布局分支仅在后续修复边界物化。
147pub 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    /// 接收已完成坐标转换和字体编码的一行,保持重复来源行的成员追加次序。
161    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    /// 只在 Rust 内生成完整前置数据,样本坐标不经过 Python 往返。
183    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    /// 样式分支只返回最终诊断,消费完成即释放全部字符数据。
233    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
253/// 从已完成统计的自有 run 导出诊断数值,不构造 Python run 对象。
254pub 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}