Skip to main content

docvortex_core/
geometry.rs

1//! 按批处理字符框、视觉行分隔和 canonical 几何,保留原始索引顺序。
2
3use crate::median;
4pub type Box4 = [f64; 4];
5pub type Size = [f64; 2];
6
7/// 共享风险筛查和完整样本的相邻锚点统计,保留两条路径的正宽度准入差异。
8pub fn anchor_pairs(
9    records: Vec<(usize, Box4, Box4, Size, f64)>,
10    positive_source: bool,
11) -> Option<Vec<(usize, f64, f64, bool)>> {
12    if records.iter().any(|r| {
13        r.1.iter()
14            .chain(r.2.iter())
15            .chain(r.3.iter())
16            .any(|v| !v.is_finite())
17            || !r.4.is_finite()
18    }) {
19        return None;
20    }
21    let mut result = Vec::new();
22    for (index, pair) in records.windows(2).enumerate() {
23        let (a, b) = (&pair[0], &pair[1]);
24        if a.0 != b.0 {
25            continue;
26        }
27        let height = (a.2[3] - a.2[1]).max(b.2[3] - b.2[1]);
28        let width = (a.2[2] - a.2[0]).max(b.2[2] - b.2[0]);
29        let shift = (a.3[1] - b.3[1]).abs();
30        let advance = b.3[0] - a.3[0];
31        let limit = (5.0 * a.4.max(1.0)).max(8.0 * width);
32        if [height, width, shift, advance, limit]
33            .iter()
34            .any(|v| !v.is_finite())
35        {
36            return None;
37        }
38        if shift > 0.5_f64.max(0.25 * height) || !(0.1 < advance && advance <= limit) {
39            continue;
40        }
41        let source_width = a.1[2] - a.1[0];
42        if positive_source && source_width <= 0.0 {
43            continue;
44        }
45        let ratio = source_width / advance;
46        let overlap = a.1[2] - b.2[0];
47        let following_width = b.2[2] - b.2[0];
48        if [ratio, overlap, following_width]
49            .iter()
50            .any(|v| !v.is_finite())
51        {
52            return None;
53        }
54        result.push((
55            index,
56            advance,
57            ratio,
58            overlap >= 0.05 * following_width.max(0.1),
59        ));
60    }
61    Some(result)
62}
63
64/// 规范并验证矩形;strict 模式不交换端点,与脚本几何契约一致。
65pub fn normalize(raw: Option<Box4>, strict: bool) -> Option<Box4> {
66    let mut b = raw?;
67    if b.iter().any(|v| !v.is_finite()) {
68        return None;
69    }
70    if !strict {
71        if b[2] < b[0] {
72            b.swap(0, 2);
73        }
74        if b[3] < b[1] {
75            b.swap(1, 3);
76        }
77    }
78    if b[2] <= b[0] || b[3] <= b[1] {
79        None
80    } else {
81        Some(b)
82    }
83}
84
85/// 按 Python min/max 保留 NaN 和相等值时的首项比较语义。
86fn minimum(a: f64, b: f64) -> f64 {
87    if b < a {
88        b
89    } else {
90        a
91    }
92}
93/// 按 Python max 保留首项的有符号零。
94fn maximum(a: f64, b: f64) -> f64 {
95    if b > a {
96        b
97    } else {
98        a
99    }
100}
101
102/// 在已校验矩形上裁剪,不重新交换端点。
103pub fn clip(raw: Option<Box4>, size: Size) -> Option<Box4> {
104    let b = raw?;
105    let b = [
106        maximum(0.0, minimum(size[0], b[0])),
107        maximum(0.0, minimum(size[1], b[1])),
108        maximum(0.0, minimum(size[0], b[2])),
109        maximum(0.0, minimum(size[1], b[3])),
110    ];
111    if b[2] <= b[0] || b[3] <= b[1] {
112        None
113    } else {
114        Some(b)
115    }
116}
117
118/// 使用既有页面视觉方向变换,不采用三角函数或坐标近似。
119pub fn rotate(b: Box4, size: Size, angle: i32) -> Box4 {
120    match angle {
121        270 => [size[1] - b[3], b[0], size[1] - b[1], b[2]],
122        90 => [b[1], size[0] - b[2], b[3], size[0] - b[0]],
123        180 => [
124            size[0] - b[2],
125            size[1] - b[3],
126            size[0] - b[0],
127            size[1] - b[1],
128        ],
129        _ => b,
130    }
131}
132
133/// 转换 origin,保持现有独立点坐标规则。
134pub fn rotate_point(p: Size, size: Size, angle: i32) -> Size {
135    match angle {
136        270 => [size[1] - p[1], p[0]],
137        90 => [p[1], size[0] - p[0]],
138        180 => [size[0] - p[0], size[1] - p[1]],
139        _ => p,
140    }
141}
142
143/// 求有限框并集,相等值保留最先出现的输入。
144pub fn union(boxes: &[Box4]) -> Option<Box4> {
145    let mut b = *boxes.first()?;
146    for a in &boxes[1..] {
147        b = [
148            minimum(b[0], a[0]),
149            minimum(b[1], a[1]),
150            maximum(b[2], a[2]),
151            maximum(b[3], a[3]),
152        ];
153    }
154    Some(b)
155}
156
157/// 为视觉 run 一次计算所有框、字距阈值、分隔和输出并集。
158pub fn visual_runs(
159    raw: Vec<Option<Box4>>,
160    overrides: Vec<Option<Box4>>,
161    flags: Vec<u8>,
162    size: Size,
163    angle: i32,
164) -> Vec<(usize, usize, Option<Box4>)> {
165    let boxes: Vec<_> = raw
166        .into_iter()
167        .zip(overrides)
168        .map(|(b, o)| clip(normalize(o, false).or_else(|| normalize(b, false)), size))
169        .collect();
170    let local: Vec<_> = boxes
171        .iter()
172        .map(|b| b.map(|v| rotate(v, size, angle)))
173        .collect();
174    let visible: Vec<_> = (0..flags.len())
175        .filter(|i| flags[*i] & 1 != 0 && local[*i].is_some())
176        .collect();
177    if visible.is_empty() {
178        return Vec::new();
179    }
180    let width = median(
181        visible
182            .iter()
183            .map(|i| 0.1_f64.max(local[*i].unwrap()[2] - local[*i].unwrap()[0]))
184            .collect(),
185    );
186    let page_width = if angle == 90 || angle == 270 {
187        size[1]
188    } else {
189        size[0]
190    };
191    let hard = 15.0_f64.max(3.0 * width).max(0.02 * page_width);
192    let gaps: Vec<_> = visible
193        .windows(2)
194        .map(|w| {
195            let a = local[w[0]].unwrap();
196            let b = local[w[1]].unwrap();
197            (a[0] - b[2]).max(b[0] - a[2]).max(0.0)
198        })
199        .collect();
200    let regular: Vec<_> = gaps.iter().copied().filter(|v| *v < hard).collect();
201    let regular = if regular.len() >= 3 {
202        median(regular)
203    } else {
204        0.0
205    };
206    let soft = 8.0_f64.max(2.2 * width).max(3.0 * regular);
207    let mut boundaries = vec![0];
208    for (w, gap) in visible.windows(2).zip(gaps) {
209        if gap >= hard || ((w[0] + 1..w[1]).any(|i| flags[i] & 2 != 0) && gap >= soft) {
210            boundaries.push(w[1]);
211        }
212    }
213    boundaries.push(flags.len());
214    boundaries
215        .windows(2)
216        .map(|w| {
217            let b: Vec<_> = (w[0]..w[1])
218                .filter(|i| flags[*i] & 1 != 0)
219                .filter_map(|i| boxes[i])
220                .collect();
221            (w[0], w[1], union(&b))
222        })
223        .collect()
224}
225
226pub type SourceRow = (Option<Box4>, Option<Box4>, Option<Box4>, Option<Size>, f64);
227pub type PreparedRow = (Box4, Box4, Size, Box4, Box4, Size);
228
229/// 批量实现 source side-map 选择、裁剪及局部坐标,不改变字体或风险规则。
230pub fn source_rows(rows: Vec<SourceRow>, size: Size, angle: i32) -> Vec<Option<PreparedRow>> {
231    rows.into_iter()
232        .map(|(raw, side, tight, origin, rotation)| {
233            let raw = normalize(raw, false);
234            let tight = normalize(tight, false);
235            let source = if rotation.is_finite() && rotation.abs() <= 1e-9 {
236                raw
237            } else {
238                let side = normalize(side, false);
239                match (raw, side) {
240                    (Some(r), Some(s)) => {
241                        let stable = (r[2] - r[0])
242                            .max(tight.map_or(0.0, |b| b[2] - b[0]))
243                            .max(0.1);
244                        Some(if s[2] - s[0] > 1.6 * stable { r } else { s })
245                    }
246                    _ => side.or(raw),
247                }
248            };
249            let source = clip(source, size)?;
250            let tight = clip(tight, size)?;
251            let origin = origin.filter(|p| p.iter().all(|v| v.is_finite()))?;
252            Some((
253                source,
254                tight,
255                origin,
256                rotate(source, size, angle),
257                rotate(tight, size, angle),
258                rotate_point(origin, size, angle),
259            ))
260        })
261        .collect()
262}