1use crate::{cell_regions, error};
4use rapiddoc_core::{Error, ErrorCode, Rect};
5use serde::Serialize;
6use std::collections::HashSet;
7
8#[derive(Debug, Clone, Copy)]
10pub struct Separator {
11 pub vertical: bool,
12 pub coordinate: f64,
13 pub start: f64,
14 pub end: f64,
15}
16
17#[derive(Debug, Clone)]
19pub struct Glyph {
20 pub index: usize,
21 pub bounds: Rect,
22 pub text: String,
23 pub generated: bool,
24}
25
26#[derive(Debug, Clone, Serialize)]
28pub struct Cell {
29 pub row: usize,
30 pub column: usize,
31 pub row_span: usize,
32 pub column_span: usize,
33 pub text: String,
34 pub character_indices: Vec<usize>,
35}
36
37#[derive(Debug, Clone, Serialize)]
39pub struct Table {
40 pub bounds: Rect,
41 pub rows: usize,
42 pub columns: usize,
43 pub cells: Vec<Cell>,
44}
45
46#[derive(Debug, Serialize)]
48pub struct RecoveredGrid {
49 pub table: Table,
50 pub x_tracks: Vec<f64>,
51 pub y_tracks: Vec<f64>,
52 pub coverage_work: usize,
53}
54pub struct TrackPlan {
55 pub x_tracks: Vec<f64>,
56 pub y_tracks: Vec<f64>,
57 pub lines: Vec<Separator>,
58 pub coverage_work: usize,
59}
60
61#[derive(Debug, Clone)]
63pub struct GridConfig {
64 pub max_cells: usize,
65 pub max_separators: usize,
66 pub tolerance: f64,
67 pub minimum_coverage: f64,
68 pub max_coverage_work: usize,
69}
70
71impl Default for GridConfig {
72 fn default() -> Self {
73 Self {
74 max_cells: 100_000,
75 max_separators: 100_000,
76 tolerance: 1.,
77 minimum_coverage: 0.9,
78 max_coverage_work: 50_000_000,
79 }
80 }
81}
82
83pub fn recover(
86 region: Rect,
87 lines: &[Separator],
88 glyphs: Vec<Glyph>,
89 config: &GridConfig,
90) -> Result<Table, Error> {
91 Ok(recover_with_tracks(region, lines, glyphs, config)?.table)
92}
93
94pub fn recover_with_tracks(
95 region: Rect,
96 lines: &[Separator],
97 glyphs: Vec<Glyph>,
98 config: &GridConfig,
99) -> Result<RecoveredGrid, Error> {
100 let plan = plan_tracks(region, lines, config)?;
101 let table = assemble(&plan.x_tracks, &plan.y_tracks, &plan.lines, glyphs, config)?;
102 Ok(RecoveredGrid {
103 table,
104 x_tracks: plan.x_tracks,
105 y_tracks: plan.y_tracks,
106 coverage_work: plan.coverage_work,
107 })
108}
109
110pub fn plan_tracks(
112 region: Rect,
113 lines: &[Separator],
114 config: &GridConfig,
115) -> Result<TrackPlan, Error> {
116 if lines.len() > rapiddoc_core::resources::limit(config.max_separators) {
117 return Err(error(ErrorCode::ResourceLimit, "分隔线超过限制"));
118 }
119 if !config.tolerance.is_finite() || config.tolerance < 0. {
120 return Err(error(ErrorCode::InvalidInput, "无效轨道容差"));
121 }
122 let [left, top, right, bottom] = region.into();
123 if right <= left || bottom <= top {
124 return Err(error(ErrorCode::InvalidInput, "表格区域必须有正面积"));
125 }
126 let mut selected = Vec::new();
127 let mut xs = Vec::new();
128 let mut ys = Vec::new();
129 for line in lines {
130 if ![line.coordinate, line.start, line.end]
131 .iter()
132 .all(|v| v.is_finite())
133 {
134 return Err(error(ErrorCode::InvalidInput, "分隔线几何无效"));
135 }
136 let (low, high, first, last) = if line.vertical {
137 (left, right, top, bottom)
138 } else {
139 (top, bottom, left, right)
140 };
141 if line.coordinate < low - config.tolerance || line.coordinate > high + config.tolerance {
142 continue;
143 }
144 let start = line.start.min(line.end).max(first);
145 let end = line.start.max(line.end).min(last);
146 if end <= start {
147 continue;
148 }
149 selected.push(Separator {
150 vertical: line.vertical,
151 coordinate: line.coordinate,
152 start,
153 end,
154 });
155 if line.vertical {
156 xs.push(line.coordinate);
157 } else {
158 ys.push(line.coordinate);
159 }
160 }
161 let xs = tracks(xs, config.tolerance);
162 let ys = tracks(ys, config.tolerance);
163 let cells = xs
164 .len()
165 .saturating_sub(1)
166 .checked_mul(ys.len().saturating_sub(1))
167 .ok_or_else(|| error(ErrorCode::ResourceLimit, "网格尺寸溢出"))?;
168 let coverage_work = cells
169 .checked_mul(2)
170 .and_then(|n| n.checked_mul(selected.len()))
171 .ok_or_else(|| error(ErrorCode::ResourceLimit, "覆盖查询预算溢出"))?;
172 if cells > rapiddoc_core::resources::limit(config.max_cells)
173 || coverage_work > rapiddoc_core::resources::limit(config.max_coverage_work)
174 {
175 return Err(error(ErrorCode::ResourceLimit, "网格或覆盖查询超过预算"));
176 }
177 Ok(TrackPlan {
178 x_tracks: xs,
179 y_tracks: ys,
180 lines: selected,
181 coverage_work,
182 })
183}
184
185fn tracks(mut values: Vec<f64>, tolerance: f64) -> Vec<f64> {
187 values.sort_by(f64::total_cmp);
188 let mut output = Vec::new();
189 let mut index = 0;
190 while index < values.len() {
191 let first = values[index];
192 let mut end = index + 1;
193 while end < values.len() && values[end] - first <= tolerance {
194 end += 1;
195 }
196 output.push(values[index..end].iter().sum::<f64>() / (end - index) as f64);
197 index = end;
198 }
199 output
200}
201
202pub fn assemble(
204 xs: &[f64],
205 ys: &[f64],
206 lines: &[Separator],
207 mut glyphs: Vec<Glyph>,
208 config: &GridConfig,
209) -> Result<Table, Error> {
210 for tracks in [xs, ys] {
211 if tracks.len() < 2
212 || tracks.iter().any(|v| !v.is_finite())
213 || tracks.windows(2).any(|w| w[0] >= w[1])
214 {
215 return Err(error(ErrorCode::InvalidInput, "轨道必须有限且严格递增"));
216 }
217 }
218 if !config.tolerance.is_finite()
219 || config.tolerance < 0.
220 || !config.minimum_coverage.is_finite()
221 || config.minimum_coverage <= 0.
222 || config.minimum_coverage > 1.
223 {
224 return Err(error(ErrorCode::InvalidInput, "无效覆盖阈值"));
225 }
226 let rows = ys.len() - 1;
227 let columns = xs.len() - 1;
228 let count = rows
229 .checked_mul(columns)
230 .ok_or_else(|| error(ErrorCode::ResourceLimit, "网格尺寸溢出"))?;
231 if count > rapiddoc_core::resources::limit(config.max_cells)
232 || lines.len() > rapiddoc_core::resources::limit(config.max_separators)
233 {
234 return Err(error(ErrorCode::ResourceLimit, "网格超过预算"));
235 }
236 let work = count
237 .checked_mul(2)
238 .and_then(|value| value.checked_mul(lines.len()))
239 .ok_or_else(|| error(ErrorCode::ResourceLimit, "覆盖查询预算溢出"))?;
240 if work > rapiddoc_core::resources::limit(config.max_coverage_work) {
241 return Err(error(ErrorCode::ResourceLimit, "覆盖查询超过计算预算"));
242 }
243 if lines.iter().any(|line| {
244 ![line.coordinate, line.start, line.end]
245 .iter()
246 .all(|v| v.is_finite())
247 }) {
248 return Err(error(ErrorCode::InvalidInput, "分隔线包含非有限值"));
249 }
250 let mut queries = Vec::new();
252 let mut connections = Vec::new();
253 for (vertical, coordinate, start, end) in [
254 (true, xs[0], ys[0], ys[rows]),
255 (true, xs[columns], ys[0], ys[rows]),
256 (false, ys[0], xs[0], xs[columns]),
257 (false, ys[rows], xs[0], xs[columns]),
258 ] {
259 queries.push((
260 u8::from(vertical),
261 vec![coordinate],
262 start,
263 end,
264 config.tolerance,
265 ));
266 }
267 for row in 0..rows {
268 for column in 0..columns - 1 {
269 queries.push((
270 1,
271 vec![xs[column + 1]],
272 ys[row],
273 ys[row + 1],
274 config.tolerance,
275 ));
276 connections.push((row * columns + column, row * columns + column + 1));
277 }
278 }
279 for row in 0..rows - 1 {
280 for column in 0..columns {
281 queries.push((
282 0,
283 vec![ys[row + 1]],
284 xs[column],
285 xs[column + 1],
286 config.tolerance,
287 ));
288 connections.push((row * columns + column, (row + 1) * columns + column));
289 }
290 }
291 let rules = lines
292 .iter()
293 .map(|line| {
294 (
295 u8::from(line.vertical),
296 line.coordinate,
297 line.start,
298 line.end,
299 )
300 })
301 .collect();
302 let coverages = docvortex_core::tables::coverage_batch(rules, queries)
303 .ok_or_else(|| error(ErrorCode::InvalidInput, "分隔线查询无效"))?;
304 if coverages[..4]
305 .iter()
306 .any(|value| *value < config.minimum_coverage)
307 {
308 return Err(error(
309 ErrorCode::InvalidInput,
310 "外边界不完整,不能认领物理表格",
311 ));
312 }
313 let missing = connections
314 .into_iter()
315 .zip(&coverages[4..])
316 .filter_map(|(edge, coverage)| (*coverage < config.minimum_coverage).then_some(edge))
317 .collect();
318 let regions = cell_regions(
319 rows,
320 columns,
321 missing,
322 rapiddoc_core::resources::limit(config.max_cells),
323 )?;
324 let mut cells: Vec<Cell> = regions
325 .into_iter()
326 .map(|region| Cell {
327 row: region.row,
328 column: region.column,
329 row_span: region.row_span,
330 column_span: region.column_span,
331 text: String::new(),
332 character_indices: Vec::new(),
333 })
334 .collect();
335 let mut owner = vec![0usize; count];
336 for (index, cell) in cells.iter().enumerate() {
337 for row in cell.row..cell.row + cell.row_span {
338 for column in cell.column..cell.column + cell.column_span {
339 owner[row * columns + column] = index;
340 }
341 }
342 }
343 let mut seen = HashSet::new();
344 glyphs.sort_by_key(|glyph| glyph.index);
345 let mut assignments = Vec::with_capacity(glyphs.len());
346 for glyph in &glyphs {
347 if !seen.insert(glyph.index) {
348 return Err(error(ErrorCode::InvalidInput, "字符来源索引重复"));
349 }
350 let [left, top, right, bottom] = glyph.bounds.into();
351 let x = left / 2. + right / 2.;
352 let y = top / 2. + bottom / 2.;
353 if x < xs[0] || x > xs[columns] || y < ys[0] || y > ys[rows] {
354 assignments.push(None);
355 continue;
356 }
357 let column = xs
358 .partition_point(|value| *value <= x)
359 .saturating_sub(1)
360 .min(columns - 1);
361 let row = ys
362 .partition_point(|value| *value <= y)
363 .saturating_sub(1)
364 .min(rows - 1);
365 assignments.push(Some(owner[row * columns + column]));
366 }
367 let mut before = Vec::with_capacity(glyphs.len());
369 let mut previous = None;
370 for (glyph, assignment) in glyphs.iter().zip(&assignments) {
371 before.push(previous);
372 if !glyph.text.chars().all(char::is_whitespace) {
373 previous = *assignment;
374 }
375 }
376 let mut after = vec![None; glyphs.len()];
377 let mut next = None;
378 for index in (0..glyphs.len()).rev() {
379 after[index] = next;
380 if !glyphs[index].text.chars().all(char::is_whitespace) {
381 next = assignments[index];
382 }
383 }
384 for (index, glyph) in glyphs.into_iter().enumerate() {
385 let assignment = if glyph.generated && glyph.text.chars().all(char::is_whitespace) {
386 if before[index] == after[index] {
387 before[index]
388 } else {
389 None
390 }
391 } else {
392 assignments[index]
393 };
394 let Some(assignment) = assignment else {
395 continue;
396 };
397 let cell = &mut cells[assignment];
398 cell.text.push_str(&glyph.text);
399 cell.character_indices.push(glyph.index);
400 }
401 Ok(Table {
402 bounds: Rect::try_from([xs[0], ys[0], xs[columns], ys[rows]])?,
403 rows,
404 columns,
405 cells,
406 })
407}