rapiddoc-native 0.1.0

Native layout and document analysis for RapidDoc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
//! 原生规则的校验适配层;复用固定来源内核,禁止未校验索引进入并查集。

use docvortex_core::{median, quantile};
use rapiddoc_core::{Error, ErrorCode};
pub mod bar_geometry;
pub mod geometry_plan;
pub mod inline_evidence;
pub mod raster_charts;
pub mod text_noise;
pub mod visual_text;

/// Exact upper-quartile body-height queries shared with the upstream kernel.
pub use docvortex_core::note_index::HeightIndex as TableNoteHeightIndex;
pub use docvortex_core::row_geometry::RowGeometry as TableNoteRowGeometry;

pub type NativeLaneGeometry = docvortex_core::rule_text::LaneGeometry;
pub type NativeTailLine = docvortex_core::rule_text::TailLine;
fn lane_finite(n: f64) -> bool {
    n.is_finite() && n.abs() <= 1e15
}
fn validate_lane_geometry(lines: &[NativeLaneGeometry]) -> Result<(), Error> {
    if lines.is_empty() || lines.len() > rapiddoc_core::resources::limit(512) {
        return Err(error(ErrorCode::ResourceLimit, "栏带输入行数超限"));
    }
    if lines.iter().any(|l| {
        l.0.iter().any(|&n| !lane_finite(n))
            || l.0[2] <= l.0[0]
            || l.0[3] <= l.0[1]
            || !lane_finite(l.1)
            || l.1 <= 0.
    }) {
        return Err(error(ErrorCode::InvalidInput, "栏带几何无效"));
    }
    Ok(())
}
fn validate_lane_bounds(lanes: &[(f64, f64)]) -> Result<(), Error> {
    if lanes.is_empty() || lanes.len() > rapiddoc_core::resources::limit(512) {
        return Err(error(ErrorCode::ResourceLimit, "栏带数量超限"));
    }
    if lanes
        .iter()
        .any(|&(l, r)| !lane_finite(l) || !lane_finite(r))
    {
        return Err(error(ErrorCode::InvalidInput, "栏带边界无效"));
    }
    Ok(())
}
/// 校验完成后借用普通几何调用固定内核,不接收Python指针。
pub fn text_supported_lane_intervals(
    lines: &[NativeLaneGeometry],
    width: f64,
    median: f64,
) -> Result<Vec<(f64, f64, usize)>, Error> {
    validate_lane_geometry(lines)?;
    if !lane_finite(width) || width <= 0. || !lane_finite(median) || median <= 0. {
        return Err(error(ErrorCode::InvalidInput, "栏带尺度无效"));
    }
    Ok(docvortex_core::rule_text::supported_lane_intervals(
        lines, width, median,
    ))
}
pub fn text_lane_assignments(
    lines: &[NativeLaneGeometry],
    lanes: &[(f64, f64)],
    tolerance: f64,
    nested: Option<(f64, f64)>,
) -> Result<Vec<Option<usize>>, Error> {
    validate_lane_geometry(lines)?;
    validate_lane_bounds(lanes)?;
    if !lane_finite(tolerance)
        || tolerance < 0.
        || nested.is_some_and(|(t, b)| !lane_finite(t) || !lane_finite(b) || b < t)
    {
        return Err(error(ErrorCode::InvalidInput, "栏带分配参数无效"));
    }
    Ok(docvortex_core::rule_text::lane_assignments(
        lines, lanes, tolerance, nested,
    ))
}
pub fn text_short_tail_destinations(
    lines: &[NativeTailLine],
    lanes: &[(f64, f64)],
    median: f64,
) -> Result<Vec<(usize, usize)>, Error> {
    validate_lane_bounds(lanes)?;
    if lines.len() > rapiddoc_core::resources::limit(512) {
        return Err(error(ErrorCode::ResourceLimit, "短尾行数超限"));
    }
    if !lane_finite(median)
        || median <= 0.
        || lines.iter().any(|l| {
            l.0.iter().any(|&n| !lane_finite(n))
                || !lane_finite(l.1)
                || l.1 <= 0.
                || !lane_finite(l.3)
                || l.4.is_some_and(|n| !n.is_finite())
                || l.5.is_some_and(|n| !lane_finite(n))
                || l.9 >= lanes.len()
        })
    {
        return Err(error(ErrorCode::InvalidInput, "短尾快照无效"));
    }
    Ok(docvortex_core::rule_text::short_tail_destinations(
        lines, lanes, median,
    ))
}
// The Python formula fallback permits finite degenerate/inverted rectangles.
// Keep the public strict adapters above unchanged; replay uses the same kernels.
fn validate_formula_lanes(lines: &[NativeLaneGeometry]) -> Result<(), Error> {
    if lines.is_empty() || lines.len() > rapiddoc_core::resources::limit(4096) {
        return Err(error(ErrorCode::ResourceLimit, "公式栏带行数超限"));
    }
    if lines
        .iter()
        .any(|l| l.0.iter().any(|&n| !lane_finite(n)) || !lane_finite(l.1) || l.1 <= 0.)
    {
        return Err(error(ErrorCode::InvalidInput, "公式栏带几何无效"));
    }
    Ok(())
}
fn validate_formula_bounds(lanes: &[(f64, f64)]) -> Result<(), Error> {
    if lanes.is_empty() || lanes.len() > rapiddoc_core::resources::limit(4096) {
        return Err(error(ErrorCode::ResourceLimit, "公式栏带数量超限"));
    }
    if lanes
        .iter()
        .any(|&(l, r)| !lane_finite(l) || !lane_finite(r))
    {
        return Err(error(ErrorCode::InvalidInput, "公式栏带边界无效"));
    }
    Ok(())
}
pub fn text_formula_lane_intervals(
    lines: &[NativeLaneGeometry],
    width: f64,
    median: f64,
) -> Result<Vec<(f64, f64, usize)>, Error> {
    validate_formula_lanes(lines)?;
    if !lane_finite(width) || width <= 0. || !lane_finite(median) || median <= 0. {
        return Err(error(ErrorCode::InvalidInput, "公式栏带尺度无效"));
    }
    Ok(docvortex_core::rule_text::supported_lane_intervals(
        lines, width, median,
    ))
}
pub fn text_formula_lane_assignments(
    lines: &[NativeLaneGeometry],
    lanes: &[(f64, f64)],
    tolerance: f64,
    nested: Option<(f64, f64)>,
) -> Result<Vec<Option<usize>>, Error> {
    validate_formula_lanes(lines)?;
    validate_formula_bounds(lanes)?;
    if !lane_finite(tolerance)
        || tolerance < 0.
        || nested.is_some_and(|(t, b)| !lane_finite(t) || !lane_finite(b) || b < t)
    {
        return Err(error(ErrorCode::InvalidInput, "公式栏带分配参数无效"));
    }
    Ok(docvortex_core::rule_text::lane_assignments(
        lines, lanes, tolerance, nested,
    ))
}
pub fn text_formula_tail_destinations(
    lines: &[NativeTailLine],
    lanes: &[(f64, f64)],
    median: f64,
) -> Result<Vec<(usize, usize)>, Error> {
    validate_formula_bounds(lanes)?;
    if lines.len() > rapiddoc_core::resources::limit(4096) {
        return Err(error(ErrorCode::ResourceLimit, "公式短尾行数超限"));
    }
    if !lane_finite(median)
        || median <= 0.
        || lines.iter().any(|l| {
            l.0.iter().any(|&n| !lane_finite(n))
                || !lane_finite(l.1)
                || l.1 <= 0.
                || !lane_finite(l.3)
                || l.4.is_some_and(|n| !n.is_finite())
                || l.5.is_some_and(|n| !lane_finite(n))
                || l.9 >= lanes.len()
        })
    {
        return Err(error(ErrorCode::InvalidInput, "公式短尾快照无效"));
    }
    Ok(docvortex_core::rule_text::short_tail_destinations(
        lines, lanes, median,
    ))
}
pub mod grid;
pub mod lines;
pub mod script_geometry;
pub type NativeGridIndex = (Vec<f64>, Vec<f64>, Vec<Vec<usize>>);
pub type NativeRule = (u8, f64, f64, f64);
/// statistics.mean的有限[0.5,1]子域:精确二进制和/整数分母,一次ties-to-even舍入。
pub fn table_decisiveness_mean(values: &[f64]) -> Result<f64, Error> {
    if values.is_empty()
        || values.len() > rapiddoc_core::resources::limit(20000)
        || values
            .iter()
            .any(|v| !v.is_finite() || !(0.5..=1.).contains(v))
    {
        return Err(error(ErrorCode::InvalidInput, "隔断可信度均值输入无效"));
    }
    if values.iter().all(|v| *v == 1.) {
        return Ok(1.);
    }
    let (total, power) = docvortex_core::exact_mean::exact_sum_parts(values)
        .ok_or_else(|| error(ErrorCode::InvalidInput, "隔断可信度精确和失败"))?;
    // 非全1的均值指数为-1;尾数的53位整数表示为total*2^(power+53)/count。
    let numerator = (total as u128) << (power + 53);
    let denominator = values.len() as u128;
    let mut q = numerator / denominator;
    let r = numerator % denominator;
    if 2 * r > denominator || (2 * r == denominator && q % 2 == 1) {
        q += 1
    }
    Ok(q as f64 * 2f64.powi(-53))
}
/// 旧拓扑分量语义:非矩形返回None而不是把正常候选拒绝变成错误。
pub fn table_component_specs(
    rows: usize,
    cols: usize,
    edges: Vec<(usize, usize)>,
) -> Result<Option<Vec<CellRegion>>, Error> {
    let count = rows
        .checked_mul(cols)
        .filter(|n| *n > 0 && *n <= rapiddoc_core::resources::limit(10000))
        .ok_or_else(|| error(ErrorCode::ResourceLimit, "原生表格原子格超限"))?;
    if edges.len() > count * 2 {
        return Err(error(ErrorCode::ResourceLimit, "原生表格连接超限"));
    }
    if edges.iter().any(|&(a, b)| {
        a >= count
            || b >= count
            || !(a / cols == b / cols && a.abs_diff(b) == 1
                || a % cols == b % cols && a.abs_diff(b) == cols)
    }) {
        return Err(error(ErrorCode::InvalidInput, "原生表格连接无效"));
    }
    let parents = docvortex_core::tables::grid_parents(count, edges);
    let (_, specs) = docvortex_core::tables::component_specs(parents, rows, cols);
    Ok(specs.map(|s| {
        s.into_iter()
            .map(|(row, column, row_span, column_span)| CellRegion {
                row,
                column,
                row_span,
                column_span,
            })
            .collect()
    }))
}
/// 固定vendor合并内核;簇均值由兼容层按旧Python版本的求和语义提供。
pub fn table_merge_rules(
    rules: Vec<NativeRule>,
    coordinates: Vec<(u8, Vec<f64>)>,
    tolerance: f64,
    join: f64,
) -> Result<Vec<NativeRule>, Error> {
    if rules.len() > rapiddoc_core::resources::limit(20000)
        || coordinates.iter().map(|c| c.1.len()).sum::<usize>()
            > rapiddoc_core::resources::limit(20000)
    {
        return Err(error(ErrorCode::ResourceLimit, "表格线段超过预算"));
    }
    if !tolerance.is_finite()
        || tolerance < 0.
        || !join.is_finite()
        || join < 0.
        || rules.iter().any(|r| {
            r.0 > 1
                || [r.1, r.2, r.3]
                    .iter()
                    .any(|n| !n.is_finite() || n.abs() > 1e9)
        })
        || coordinates
            .iter()
            .any(|c| c.0 > 1 || c.1.iter().any(|n| !n.is_finite() || n.abs() > 1e9))
    {
        return Err(error(ErrorCode::InvalidInput, "表格线段无效"));
    }
    docvortex_core::tables::merge_rules(rules, coordinates, tolerance, join)
        .ok_or_else(|| error(ErrorCode::InvalidInput, "表格线段合并失败"))
}
pub fn table_assign_cells(
    glyphs: Vec<[f64; 4]>,
    specs: Vec<[f64; 4]>,
    index: Option<NativeGridIndex>,
) -> Result<Vec<(Option<usize>, bool)>, Error> {
    table_assign_cells_bounded(glyphs, specs, index, 3000)
}
pub fn table_assign_cells_page(
    glyphs: Vec<[f64; 4]>,
    specs: Vec<[f64; 4]>,
    index: Option<NativeGridIndex>,
) -> Result<Vec<(Option<usize>, bool)>, Error> {
    table_assign_cells_bounded(glyphs, specs, index, usize::MAX)
}
fn table_assign_cells_bounded(
    glyphs: Vec<[f64; 4]>,
    specs: Vec<[f64; 4]>,
    index: Option<NativeGridIndex>,
    limit: usize,
) -> Result<Vec<(Option<usize>, bool)>, Error> {
    let limit = rapiddoc_core::resources::limit(limit);
    if glyphs.len() > limit || specs.len() > rapiddoc_core::resources::limit(2000) {
        return Err(error(ErrorCode::ResourceLimit, "原生表格落格输入超过预算"));
    }
    if glyphs
        .iter()
        .chain(&specs)
        .flatten()
        .any(|x| !x.is_finite() || x.abs() > 1e9)
    {
        return Err(error(ErrorCode::InvalidInput, "原生表格落格坐标无效"));
    }
    docvortex_core::tables::assign_cells(glyphs, specs, index)
        .ok_or_else(|| error(ErrorCode::InvalidInput, "原生表格落格索引无效"))
}
mod table_latin;
mod table_unicode;
pub fn python_latin(c: char) -> bool {
    let n = c as u32;
    let a = table_latin::LATIN;
    let i = a.partition_point(|&(x, _)| x <= n);
    i > 0 && n <= a[i - 1].1
}
pub fn table_cell_visual_groups(
    boxes: Vec<[f64; 4]>,
    ids: Vec<usize>,
    rows: Vec<i64>,
    height: f64,
) -> Result<Vec<Vec<usize>>, Error> {
    table_cell_visual_groups_bounded(boxes, ids, rows, height, 3000)
}
pub fn table_cell_visual_groups_page(
    boxes: Vec<[f64; 4]>,
    ids: Vec<usize>,
    rows: Vec<i64>,
    height: f64,
) -> Result<Vec<Vec<usize>>, Error> {
    table_cell_visual_groups_bounded(boxes, ids, rows, height, usize::MAX)
}
fn table_cell_visual_groups_bounded(
    boxes: Vec<[f64; 4]>,
    ids: Vec<usize>,
    rows: Vec<i64>,
    height: f64,
    limit: usize,
) -> Result<Vec<Vec<usize>>, Error> {
    let limit = rapiddoc_core::resources::limit(limit);
    if boxes.len() > limit {
        return Err(error(ErrorCode::ResourceLimit, "单元格字形超过预算"));
    }
    if !height.is_finite()
        || height.abs() > 1e9
        || boxes
            .iter()
            .flatten()
            .any(|x| !x.is_finite() || x.abs() > 1e9)
    {
        return Err(error(ErrorCode::InvalidInput, "单元格字形几何无效"));
    }
    docvortex_core::tables::cell_visual_groups(boxes, ids, rows, height)
        .ok_or_else(|| error(ErrorCode::InvalidInput, "单元格字形组无效"))
}
pub mod text_boundary;
mod text_unicode_data;

/// CPython Unicode16 printable/decimal属性;不依赖宿主Python或Rust Unicode版本。
pub fn python_printable(c: char) -> bool {
    let n = c as u32;
    let ranges = table_unicode::PRINTABLE;
    let i = ranges.partition_point(|&(start, _)| start <= n);
    i > 0 && n <= ranges[i - 1].1
}
pub fn python_decimal(c: char) -> Option<u8> {
    table_unicode::DECIMALS
        .binary_search_by_key(&(c as u32), |&(n, _)| n)
        .ok()
        .map(|i| table_unicode::DECIMALS[i].1)
}
/// 复用固定vendor视觉分行内核;输入和二次工作上限在调用前校验。
pub fn table_visual_rows(
    boxes: Vec<[f64; 4]>,
    ids: Vec<usize>,
    height: f64,
) -> Result<Vec<Vec<usize>>, Error> {
    table_visual_rows_bounded(boxes, ids, height, 3000)
}
/// Full page path; same vendor kernel, independently bounded from diagnostics.
pub fn table_visual_rows_page(
    boxes: Vec<[f64; 4]>,
    ids: Vec<usize>,
    height: f64,
) -> Result<Vec<Vec<usize>>, Error> {
    table_visual_rows_bounded(boxes, ids, height, usize::MAX)
}
fn table_visual_rows_bounded(
    boxes: Vec<[f64; 4]>,
    ids: Vec<usize>,
    height: f64,
    limit: usize,
) -> Result<Vec<Vec<usize>>, Error> {
    let limit = rapiddoc_core::resources::limit(limit);
    if boxes.len() > limit {
        return Err(error(ErrorCode::ResourceLimit, "原生表格字形超过预算"));
    }
    if boxes.len() != ids.len()
        || !height.is_finite()
        || boxes
            .iter()
            .flatten()
            .any(|x| !x.is_finite() || x.abs() > 1e9)
    {
        return Err(error(ErrorCode::InvalidInput, "原生表格分行输入无效"));
    }
    docvortex_core::tables::visual_rows(boxes, ids, height)
        .ok_or_else(|| error(ErrorCode::InvalidInput, "原生表格分行输入无效"))
}

/// 合并单元格的矩形范围,位置为零基行列,跨度必须为正。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CellRegion {
    pub row: usize,
    pub column: usize,
    pub row_span: usize,
    pub column_span: usize,
}

/// 根据缺失分隔边合并相邻原子格;拒绝越界、跨行伪邻接及 L 形连通分量。
pub fn cell_regions(
    rows: usize,
    columns: usize,
    missing_edges: Vec<(usize, usize)>,
    max_cells: usize,
) -> Result<Vec<CellRegion>, Error> {
    let max_cells = rapiddoc_core::resources::limit(max_cells);
    let count = rows
        .checked_mul(columns)
        .ok_or_else(|| error(ErrorCode::ResourceLimit, "网格尺寸溢出"))?;
    if rows == 0 || columns == 0 {
        return Err(error(ErrorCode::InvalidInput, "网格必须有行和列"));
    }
    if count > max_cells || missing_edges.len() > count.saturating_mul(2) {
        return Err(error(ErrorCode::ResourceLimit, "网格超过限制"));
    }
    for &(first, last) in &missing_edges {
        if first >= count || last >= count {
            return Err(error(ErrorCode::InvalidInput, "网格边索引越界"));
        }
        let adjacent = first / columns == last / columns && first.abs_diff(last) == 1
            || first % columns == last % columns && first.abs_diff(last) == columns;
        if !adjacent {
            return Err(error(ErrorCode::InvalidInput, "仅允许连接相邻单元格"));
        }
    }
    let parents = docvortex_core::tables::grid_parents(count, missing_edges);
    let (_, specs) = docvortex_core::tables::component_specs(parents, rows, columns);
    let specs =
        specs.ok_or_else(|| error(ErrorCode::InvalidInput, "非矩形连通分量不能作为合并单元格"))?;
    Ok(specs
        .into_iter()
        .map(|(row, column, row_span, column_span)| CellRegion {
            row,
            column,
            row_span,
            column_span,
        })
        .collect())
}

/// 线段在目标区间的联合覆盖率;反向线段允许,所有几何必须有限。
pub fn interval_coverage(intervals: Vec<(f64, f64)>, start: f64, end: f64) -> Result<f64, Error> {
    if !start.is_finite()
        || !end.is_finite()
        || end <= start
        || intervals
            .iter()
            .any(|(a, b)| !a.is_finite() || !b.is_finite())
    {
        return Err(error(ErrorCode::InvalidInput, "覆盖查询的几何无效"));
    }
    let rules = intervals.into_iter().map(|(a, b)| (0, 0., a, b)).collect();
    let result = docvortex_core::tables::coverage_batch(rules, vec![(0, vec![0.], start, end, 0.)])
        .ok_or_else(|| error(ErrorCode::InvalidInput, "覆盖查询内核拒绝输入"))?;
    result
        .first()
        .copied()
        .ok_or_else(|| error(ErrorCode::InvalidInput, "覆盖查询内核返回空结果"))
}

/// 构造规则层错误,保留稳定分类。
pub(crate) fn error(code: ErrorCode, message: &str) -> Error {
    Error {
        code,
        message: message.into(),
    }
}

#[cfg(test)]
mod lane_adapter_tests {
    use super::*;
    #[test]
    fn lane_kernels_reject_empty_nonfinite_and_invalid_lane_indices() {
        assert!(text_supported_lane_intervals(&[], 100., 10.).is_err());
        let line = ([0., 0., 50., 10.], 10., true, false);
        assert!(text_supported_lane_intervals(&[line], f64::NAN, 10.).is_err());
        assert!(text_lane_assignments(&[line], &[], 1., None).is_err());
        let tail = (line.0, 10., None, 0., None, None, false, None, 0, 1);
        assert!(text_short_tail_destinations(&[tail], &[(0., 100.)], 10.).is_err());
        assert_eq!(
            text_supported_lane_intervals(&[line], 100., 10.).unwrap(),
            vec![(0., 50., 1)]
        );
    }
}