Skip to main content

rapiddoc_native/
lib.rs

1//! 原生规则的校验适配层;复用固定来源内核,禁止未校验索引进入并查集。
2
3use docvortex_core::{median, quantile};
4use rapiddoc_core::{Error, ErrorCode};
5pub mod bar_geometry;
6pub mod geometry_plan;
7pub mod inline_evidence;
8pub mod raster_charts;
9pub mod text_noise;
10pub mod visual_text;
11
12/// Exact upper-quartile body-height queries shared with the upstream kernel.
13pub use docvortex_core::note_index::HeightIndex as TableNoteHeightIndex;
14pub use docvortex_core::row_geometry::RowGeometry as TableNoteRowGeometry;
15
16pub type NativeLaneGeometry = docvortex_core::rule_text::LaneGeometry;
17pub type NativeTailLine = docvortex_core::rule_text::TailLine;
18fn lane_finite(n: f64) -> bool {
19    n.is_finite() && n.abs() <= 1e15
20}
21fn validate_lane_geometry(lines: &[NativeLaneGeometry]) -> Result<(), Error> {
22    if lines.is_empty() || lines.len() > rapiddoc_core::resources::limit(512) {
23        return Err(error(ErrorCode::ResourceLimit, "栏带输入行数超限"));
24    }
25    if lines.iter().any(|l| {
26        l.0.iter().any(|&n| !lane_finite(n))
27            || l.0[2] <= l.0[0]
28            || l.0[3] <= l.0[1]
29            || !lane_finite(l.1)
30            || l.1 <= 0.
31    }) {
32        return Err(error(ErrorCode::InvalidInput, "栏带几何无效"));
33    }
34    Ok(())
35}
36fn validate_lane_bounds(lanes: &[(f64, f64)]) -> Result<(), Error> {
37    if lanes.is_empty() || lanes.len() > rapiddoc_core::resources::limit(512) {
38        return Err(error(ErrorCode::ResourceLimit, "栏带数量超限"));
39    }
40    if lanes
41        .iter()
42        .any(|&(l, r)| !lane_finite(l) || !lane_finite(r))
43    {
44        return Err(error(ErrorCode::InvalidInput, "栏带边界无效"));
45    }
46    Ok(())
47}
48/// 校验完成后借用普通几何调用固定内核,不接收Python指针。
49pub fn text_supported_lane_intervals(
50    lines: &[NativeLaneGeometry],
51    width: f64,
52    median: f64,
53) -> Result<Vec<(f64, f64, usize)>, Error> {
54    validate_lane_geometry(lines)?;
55    if !lane_finite(width) || width <= 0. || !lane_finite(median) || median <= 0. {
56        return Err(error(ErrorCode::InvalidInput, "栏带尺度无效"));
57    }
58    Ok(docvortex_core::rule_text::supported_lane_intervals(
59        lines, width, median,
60    ))
61}
62pub fn text_lane_assignments(
63    lines: &[NativeLaneGeometry],
64    lanes: &[(f64, f64)],
65    tolerance: f64,
66    nested: Option<(f64, f64)>,
67) -> Result<Vec<Option<usize>>, Error> {
68    validate_lane_geometry(lines)?;
69    validate_lane_bounds(lanes)?;
70    if !lane_finite(tolerance)
71        || tolerance < 0.
72        || nested.is_some_and(|(t, b)| !lane_finite(t) || !lane_finite(b) || b < t)
73    {
74        return Err(error(ErrorCode::InvalidInput, "栏带分配参数无效"));
75    }
76    Ok(docvortex_core::rule_text::lane_assignments(
77        lines, lanes, tolerance, nested,
78    ))
79}
80pub fn text_short_tail_destinations(
81    lines: &[NativeTailLine],
82    lanes: &[(f64, f64)],
83    median: f64,
84) -> Result<Vec<(usize, usize)>, Error> {
85    validate_lane_bounds(lanes)?;
86    if lines.len() > rapiddoc_core::resources::limit(512) {
87        return Err(error(ErrorCode::ResourceLimit, "短尾行数超限"));
88    }
89    if !lane_finite(median)
90        || median <= 0.
91        || lines.iter().any(|l| {
92            l.0.iter().any(|&n| !lane_finite(n))
93                || !lane_finite(l.1)
94                || l.1 <= 0.
95                || !lane_finite(l.3)
96                || l.4.is_some_and(|n| !n.is_finite())
97                || l.5.is_some_and(|n| !lane_finite(n))
98                || l.9 >= lanes.len()
99        })
100    {
101        return Err(error(ErrorCode::InvalidInput, "短尾快照无效"));
102    }
103    Ok(docvortex_core::rule_text::short_tail_destinations(
104        lines, lanes, median,
105    ))
106}
107// The Python formula fallback permits finite degenerate/inverted rectangles.
108// Keep the public strict adapters above unchanged; replay uses the same kernels.
109fn validate_formula_lanes(lines: &[NativeLaneGeometry]) -> Result<(), Error> {
110    if lines.is_empty() || lines.len() > rapiddoc_core::resources::limit(4096) {
111        return Err(error(ErrorCode::ResourceLimit, "公式栏带行数超限"));
112    }
113    if lines
114        .iter()
115        .any(|l| l.0.iter().any(|&n| !lane_finite(n)) || !lane_finite(l.1) || l.1 <= 0.)
116    {
117        return Err(error(ErrorCode::InvalidInput, "公式栏带几何无效"));
118    }
119    Ok(())
120}
121fn validate_formula_bounds(lanes: &[(f64, f64)]) -> Result<(), Error> {
122    if lanes.is_empty() || lanes.len() > rapiddoc_core::resources::limit(4096) {
123        return Err(error(ErrorCode::ResourceLimit, "公式栏带数量超限"));
124    }
125    if lanes
126        .iter()
127        .any(|&(l, r)| !lane_finite(l) || !lane_finite(r))
128    {
129        return Err(error(ErrorCode::InvalidInput, "公式栏带边界无效"));
130    }
131    Ok(())
132}
133pub fn text_formula_lane_intervals(
134    lines: &[NativeLaneGeometry],
135    width: f64,
136    median: f64,
137) -> Result<Vec<(f64, f64, usize)>, Error> {
138    validate_formula_lanes(lines)?;
139    if !lane_finite(width) || width <= 0. || !lane_finite(median) || median <= 0. {
140        return Err(error(ErrorCode::InvalidInput, "公式栏带尺度无效"));
141    }
142    Ok(docvortex_core::rule_text::supported_lane_intervals(
143        lines, width, median,
144    ))
145}
146pub fn text_formula_lane_assignments(
147    lines: &[NativeLaneGeometry],
148    lanes: &[(f64, f64)],
149    tolerance: f64,
150    nested: Option<(f64, f64)>,
151) -> Result<Vec<Option<usize>>, Error> {
152    validate_formula_lanes(lines)?;
153    validate_formula_bounds(lanes)?;
154    if !lane_finite(tolerance)
155        || tolerance < 0.
156        || nested.is_some_and(|(t, b)| !lane_finite(t) || !lane_finite(b) || b < t)
157    {
158        return Err(error(ErrorCode::InvalidInput, "公式栏带分配参数无效"));
159    }
160    Ok(docvortex_core::rule_text::lane_assignments(
161        lines, lanes, tolerance, nested,
162    ))
163}
164pub fn text_formula_tail_destinations(
165    lines: &[NativeTailLine],
166    lanes: &[(f64, f64)],
167    median: f64,
168) -> Result<Vec<(usize, usize)>, Error> {
169    validate_formula_bounds(lanes)?;
170    if lines.len() > rapiddoc_core::resources::limit(4096) {
171        return Err(error(ErrorCode::ResourceLimit, "公式短尾行数超限"));
172    }
173    if !lane_finite(median)
174        || median <= 0.
175        || lines.iter().any(|l| {
176            l.0.iter().any(|&n| !lane_finite(n))
177                || !lane_finite(l.1)
178                || l.1 <= 0.
179                || !lane_finite(l.3)
180                || l.4.is_some_and(|n| !n.is_finite())
181                || l.5.is_some_and(|n| !lane_finite(n))
182                || l.9 >= lanes.len()
183        })
184    {
185        return Err(error(ErrorCode::InvalidInput, "公式短尾快照无效"));
186    }
187    Ok(docvortex_core::rule_text::short_tail_destinations(
188        lines, lanes, median,
189    ))
190}
191pub mod grid;
192pub mod lines;
193pub mod script_geometry;
194pub type NativeGridIndex = (Vec<f64>, Vec<f64>, Vec<Vec<usize>>);
195pub type NativeRule = (u8, f64, f64, f64);
196/// statistics.mean的有限[0.5,1]子域:精确二进制和/整数分母,一次ties-to-even舍入。
197pub fn table_decisiveness_mean(values: &[f64]) -> Result<f64, Error> {
198    if values.is_empty()
199        || values.len() > rapiddoc_core::resources::limit(20000)
200        || values
201            .iter()
202            .any(|v| !v.is_finite() || !(0.5..=1.).contains(v))
203    {
204        return Err(error(ErrorCode::InvalidInput, "隔断可信度均值输入无效"));
205    }
206    if values.iter().all(|v| *v == 1.) {
207        return Ok(1.);
208    }
209    let (total, power) = docvortex_core::exact_mean::exact_sum_parts(values)
210        .ok_or_else(|| error(ErrorCode::InvalidInput, "隔断可信度精确和失败"))?;
211    // 非全1的均值指数为-1;尾数的53位整数表示为total*2^(power+53)/count。
212    let numerator = (total as u128) << (power + 53);
213    let denominator = values.len() as u128;
214    let mut q = numerator / denominator;
215    let r = numerator % denominator;
216    if 2 * r > denominator || (2 * r == denominator && q % 2 == 1) {
217        q += 1
218    }
219    Ok(q as f64 * 2f64.powi(-53))
220}
221/// 旧拓扑分量语义:非矩形返回None而不是把正常候选拒绝变成错误。
222pub fn table_component_specs(
223    rows: usize,
224    cols: usize,
225    edges: Vec<(usize, usize)>,
226) -> Result<Option<Vec<CellRegion>>, Error> {
227    let count = rows
228        .checked_mul(cols)
229        .filter(|n| *n > 0 && *n <= rapiddoc_core::resources::limit(10000))
230        .ok_or_else(|| error(ErrorCode::ResourceLimit, "原生表格原子格超限"))?;
231    if edges.len() > count * 2 {
232        return Err(error(ErrorCode::ResourceLimit, "原生表格连接超限"));
233    }
234    if edges.iter().any(|&(a, b)| {
235        a >= count
236            || b >= count
237            || !(a / cols == b / cols && a.abs_diff(b) == 1
238                || a % cols == b % cols && a.abs_diff(b) == cols)
239    }) {
240        return Err(error(ErrorCode::InvalidInput, "原生表格连接无效"));
241    }
242    let parents = docvortex_core::tables::grid_parents(count, edges);
243    let (_, specs) = docvortex_core::tables::component_specs(parents, rows, cols);
244    Ok(specs.map(|s| {
245        s.into_iter()
246            .map(|(row, column, row_span, column_span)| CellRegion {
247                row,
248                column,
249                row_span,
250                column_span,
251            })
252            .collect()
253    }))
254}
255/// 固定vendor合并内核;簇均值由兼容层按旧Python版本的求和语义提供。
256pub fn table_merge_rules(
257    rules: Vec<NativeRule>,
258    coordinates: Vec<(u8, Vec<f64>)>,
259    tolerance: f64,
260    join: f64,
261) -> Result<Vec<NativeRule>, Error> {
262    if rules.len() > rapiddoc_core::resources::limit(20000)
263        || coordinates.iter().map(|c| c.1.len()).sum::<usize>()
264            > rapiddoc_core::resources::limit(20000)
265    {
266        return Err(error(ErrorCode::ResourceLimit, "表格线段超过预算"));
267    }
268    if !tolerance.is_finite()
269        || tolerance < 0.
270        || !join.is_finite()
271        || join < 0.
272        || rules.iter().any(|r| {
273            r.0 > 1
274                || [r.1, r.2, r.3]
275                    .iter()
276                    .any(|n| !n.is_finite() || n.abs() > 1e9)
277        })
278        || coordinates
279            .iter()
280            .any(|c| c.0 > 1 || c.1.iter().any(|n| !n.is_finite() || n.abs() > 1e9))
281    {
282        return Err(error(ErrorCode::InvalidInput, "表格线段无效"));
283    }
284    docvortex_core::tables::merge_rules(rules, coordinates, tolerance, join)
285        .ok_or_else(|| error(ErrorCode::InvalidInput, "表格线段合并失败"))
286}
287pub fn table_assign_cells(
288    glyphs: Vec<[f64; 4]>,
289    specs: Vec<[f64; 4]>,
290    index: Option<NativeGridIndex>,
291) -> Result<Vec<(Option<usize>, bool)>, Error> {
292    table_assign_cells_bounded(glyphs, specs, index, 3000)
293}
294pub fn table_assign_cells_page(
295    glyphs: Vec<[f64; 4]>,
296    specs: Vec<[f64; 4]>,
297    index: Option<NativeGridIndex>,
298) -> Result<Vec<(Option<usize>, bool)>, Error> {
299    table_assign_cells_bounded(glyphs, specs, index, usize::MAX)
300}
301fn table_assign_cells_bounded(
302    glyphs: Vec<[f64; 4]>,
303    specs: Vec<[f64; 4]>,
304    index: Option<NativeGridIndex>,
305    limit: usize,
306) -> Result<Vec<(Option<usize>, bool)>, Error> {
307    let limit = rapiddoc_core::resources::limit(limit);
308    if glyphs.len() > limit || specs.len() > rapiddoc_core::resources::limit(2000) {
309        return Err(error(ErrorCode::ResourceLimit, "原生表格落格输入超过预算"));
310    }
311    if glyphs
312        .iter()
313        .chain(&specs)
314        .flatten()
315        .any(|x| !x.is_finite() || x.abs() > 1e9)
316    {
317        return Err(error(ErrorCode::InvalidInput, "原生表格落格坐标无效"));
318    }
319    docvortex_core::tables::assign_cells(glyphs, specs, index)
320        .ok_or_else(|| error(ErrorCode::InvalidInput, "原生表格落格索引无效"))
321}
322mod table_latin;
323mod table_unicode;
324pub fn python_latin(c: char) -> bool {
325    let n = c as u32;
326    let a = table_latin::LATIN;
327    let i = a.partition_point(|&(x, _)| x <= n);
328    i > 0 && n <= a[i - 1].1
329}
330pub fn table_cell_visual_groups(
331    boxes: Vec<[f64; 4]>,
332    ids: Vec<usize>,
333    rows: Vec<i64>,
334    height: f64,
335) -> Result<Vec<Vec<usize>>, Error> {
336    table_cell_visual_groups_bounded(boxes, ids, rows, height, 3000)
337}
338pub fn table_cell_visual_groups_page(
339    boxes: Vec<[f64; 4]>,
340    ids: Vec<usize>,
341    rows: Vec<i64>,
342    height: f64,
343) -> Result<Vec<Vec<usize>>, Error> {
344    table_cell_visual_groups_bounded(boxes, ids, rows, height, usize::MAX)
345}
346fn table_cell_visual_groups_bounded(
347    boxes: Vec<[f64; 4]>,
348    ids: Vec<usize>,
349    rows: Vec<i64>,
350    height: f64,
351    limit: usize,
352) -> Result<Vec<Vec<usize>>, Error> {
353    let limit = rapiddoc_core::resources::limit(limit);
354    if boxes.len() > limit {
355        return Err(error(ErrorCode::ResourceLimit, "单元格字形超过预算"));
356    }
357    if !height.is_finite()
358        || height.abs() > 1e9
359        || boxes
360            .iter()
361            .flatten()
362            .any(|x| !x.is_finite() || x.abs() > 1e9)
363    {
364        return Err(error(ErrorCode::InvalidInput, "单元格字形几何无效"));
365    }
366    docvortex_core::tables::cell_visual_groups(boxes, ids, rows, height)
367        .ok_or_else(|| error(ErrorCode::InvalidInput, "单元格字形组无效"))
368}
369pub mod text_boundary;
370mod text_unicode_data;
371
372/// CPython Unicode16 printable/decimal属性;不依赖宿主Python或Rust Unicode版本。
373pub fn python_printable(c: char) -> bool {
374    let n = c as u32;
375    let ranges = table_unicode::PRINTABLE;
376    let i = ranges.partition_point(|&(start, _)| start <= n);
377    i > 0 && n <= ranges[i - 1].1
378}
379pub fn python_decimal(c: char) -> Option<u8> {
380    table_unicode::DECIMALS
381        .binary_search_by_key(&(c as u32), |&(n, _)| n)
382        .ok()
383        .map(|i| table_unicode::DECIMALS[i].1)
384}
385/// 复用固定vendor视觉分行内核;输入和二次工作上限在调用前校验。
386pub fn table_visual_rows(
387    boxes: Vec<[f64; 4]>,
388    ids: Vec<usize>,
389    height: f64,
390) -> Result<Vec<Vec<usize>>, Error> {
391    table_visual_rows_bounded(boxes, ids, height, 3000)
392}
393/// Full page path; same vendor kernel, independently bounded from diagnostics.
394pub fn table_visual_rows_page(
395    boxes: Vec<[f64; 4]>,
396    ids: Vec<usize>,
397    height: f64,
398) -> Result<Vec<Vec<usize>>, Error> {
399    table_visual_rows_bounded(boxes, ids, height, usize::MAX)
400}
401fn table_visual_rows_bounded(
402    boxes: Vec<[f64; 4]>,
403    ids: Vec<usize>,
404    height: f64,
405    limit: usize,
406) -> Result<Vec<Vec<usize>>, Error> {
407    let limit = rapiddoc_core::resources::limit(limit);
408    if boxes.len() > limit {
409        return Err(error(ErrorCode::ResourceLimit, "原生表格字形超过预算"));
410    }
411    if boxes.len() != ids.len()
412        || !height.is_finite()
413        || boxes
414            .iter()
415            .flatten()
416            .any(|x| !x.is_finite() || x.abs() > 1e9)
417    {
418        return Err(error(ErrorCode::InvalidInput, "原生表格分行输入无效"));
419    }
420    docvortex_core::tables::visual_rows(boxes, ids, height)
421        .ok_or_else(|| error(ErrorCode::InvalidInput, "原生表格分行输入无效"))
422}
423
424/// 合并单元格的矩形范围,位置为零基行列,跨度必须为正。
425#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct CellRegion {
427    pub row: usize,
428    pub column: usize,
429    pub row_span: usize,
430    pub column_span: usize,
431}
432
433/// 根据缺失分隔边合并相邻原子格;拒绝越界、跨行伪邻接及 L 形连通分量。
434pub fn cell_regions(
435    rows: usize,
436    columns: usize,
437    missing_edges: Vec<(usize, usize)>,
438    max_cells: usize,
439) -> Result<Vec<CellRegion>, Error> {
440    let max_cells = rapiddoc_core::resources::limit(max_cells);
441    let count = rows
442        .checked_mul(columns)
443        .ok_or_else(|| error(ErrorCode::ResourceLimit, "网格尺寸溢出"))?;
444    if rows == 0 || columns == 0 {
445        return Err(error(ErrorCode::InvalidInput, "网格必须有行和列"));
446    }
447    if count > max_cells || missing_edges.len() > count.saturating_mul(2) {
448        return Err(error(ErrorCode::ResourceLimit, "网格超过限制"));
449    }
450    for &(first, last) in &missing_edges {
451        if first >= count || last >= count {
452            return Err(error(ErrorCode::InvalidInput, "网格边索引越界"));
453        }
454        let adjacent = first / columns == last / columns && first.abs_diff(last) == 1
455            || first % columns == last % columns && first.abs_diff(last) == columns;
456        if !adjacent {
457            return Err(error(ErrorCode::InvalidInput, "仅允许连接相邻单元格"));
458        }
459    }
460    let parents = docvortex_core::tables::grid_parents(count, missing_edges);
461    let (_, specs) = docvortex_core::tables::component_specs(parents, rows, columns);
462    let specs =
463        specs.ok_or_else(|| error(ErrorCode::InvalidInput, "非矩形连通分量不能作为合并单元格"))?;
464    Ok(specs
465        .into_iter()
466        .map(|(row, column, row_span, column_span)| CellRegion {
467            row,
468            column,
469            row_span,
470            column_span,
471        })
472        .collect())
473}
474
475/// 线段在目标区间的联合覆盖率;反向线段允许,所有几何必须有限。
476pub fn interval_coverage(intervals: Vec<(f64, f64)>, start: f64, end: f64) -> Result<f64, Error> {
477    if !start.is_finite()
478        || !end.is_finite()
479        || end <= start
480        || intervals
481            .iter()
482            .any(|(a, b)| !a.is_finite() || !b.is_finite())
483    {
484        return Err(error(ErrorCode::InvalidInput, "覆盖查询的几何无效"));
485    }
486    let rules = intervals.into_iter().map(|(a, b)| (0, 0., a, b)).collect();
487    let result = docvortex_core::tables::coverage_batch(rules, vec![(0, vec![0.], start, end, 0.)])
488        .ok_or_else(|| error(ErrorCode::InvalidInput, "覆盖查询内核拒绝输入"))?;
489    result
490        .first()
491        .copied()
492        .ok_or_else(|| error(ErrorCode::InvalidInput, "覆盖查询内核返回空结果"))
493}
494
495/// 构造规则层错误,保留稳定分类。
496pub(crate) fn error(code: ErrorCode, message: &str) -> Error {
497    Error {
498        code,
499        message: message.into(),
500    }
501}
502
503#[cfg(test)]
504mod lane_adapter_tests {
505    use super::*;
506    #[test]
507    fn lane_kernels_reject_empty_nonfinite_and_invalid_lane_indices() {
508        assert!(text_supported_lane_intervals(&[], 100., 10.).is_err());
509        let line = ([0., 0., 50., 10.], 10., true, false);
510        assert!(text_supported_lane_intervals(&[line], f64::NAN, 10.).is_err());
511        assert!(text_lane_assignments(&[line], &[], 1., None).is_err());
512        let tail = (line.0, 10., None, 0., None, None, false, None, 0, 1);
513        assert!(text_short_tail_destinations(&[tail], &[(0., 100.)], 10.).is_err());
514        assert_eq!(
515            text_supported_lane_intervals(&[line], 100., 10.).unwrap(),
516            vec![(0., 50., 1)]
517        );
518    }
519}