1pub use crate::assemble::TableGrid;
15use image::RgbImage;
16
17pub const SIDE: u32 = 448;
19#[allow(clippy::excessive_precision)]
22pub const MEAN: [f32; 3] = [0.94247851, 0.94254675, 0.94292611];
23#[allow(clippy::excessive_precision)]
24pub const STD: [f32; 3] = [0.17910956, 0.17940403, 0.17931663];
25pub const MAX_STEPS: usize = 1024;
27pub const EMBED_DIM: usize = 512;
29
30pub const START: i64 = 2;
32pub const END: i64 = 3;
33pub const ECEL: i64 = 4; pub const FCEL: i64 = 5; pub const LCEL: i64 = 6; pub const UCEL: i64 = 7; pub const XCEL: i64 = 8; pub const NL: i64 = 9; pub const CHED: i64 = 10; pub const RHED: i64 = 11; pub const SROW: i64 = 12; const CELL_TAGS: [i64; 6] = [FCEL, ECEL, XCEL, CHED, RHED, SROW];
44
45#[derive(Debug, Clone)]
49pub struct TableCell {
50 pub row: usize,
51 pub col: usize,
52 pub colspan: usize,
53 pub rowspan: usize,
54 pub tag: i64,
55 pub class: i64,
56 pub cx: f32,
57 pub cy: f32,
58 pub w: f32,
59 pub h: f32,
60}
61
62pub fn preprocess_input(img: &RgbImage) -> Vec<f32> {
67 let nn = (SIDE * SIDE) as usize;
68 let side = SIDE as usize;
69 let (sw, sh) = (img.width() as i32, img.height() as i32);
70 let sxr = sw as f32 / SIDE as f32;
71 let syr = sh as f32 / SIDE as f32;
72 let mut data = vec![0f32; 3 * nn];
73 for h in 0..side {
74 let fy = (h as f32 + 0.5) * syr - 0.5;
75 let wy = fy - fy.floor();
76 let y0c = (fy.floor() as i32).clamp(0, sh - 1) as u32;
77 let y1c = (fy.floor() as i32 + 1).clamp(0, sh - 1) as u32;
78 for w in 0..side {
79 let fx = (w as f32 + 0.5) * sxr - 0.5;
80 let wx = fx - fx.floor();
81 let x0c = (fx.floor() as i32).clamp(0, sw - 1) as u32;
82 let x1c = (fx.floor() as i32 + 1).clamp(0, sw - 1) as u32;
83 let p00 = img.get_pixel(x0c, y0c);
84 let p01 = img.get_pixel(x1c, y0c);
85 let p10 = img.get_pixel(x0c, y1c);
86 let p11 = img.get_pixel(x1c, y1c);
87 let idx = w * side + h; for c in 0..3 {
89 let top = p00[c] as f32 * (1.0 - wx) + p01[c] as f32 * wx;
90 let bot = p10[c] as f32 * (1.0 - wx) + p11[c] as f32 * wx;
91 let v = top * (1.0 - wy) + bot * wy;
92 data[c * nn + idx] = (v / 255.0 - MEAN[c]) / STD[c];
93 }
94 }
95 }
96 data
97}
98
99pub fn correct(raw: i64, prev_ucel: bool) -> i64 {
103 let mut tag = raw;
104 if tag == XCEL {
105 tag = LCEL;
106 }
107 if prev_ucel && tag == LCEL {
108 tag = FCEL;
109 }
110 tag
111}
112
113#[derive(Default)]
120pub struct BboxBook {
121 pub tags: Vec<i64>,
123 pub otsl: Vec<i64>,
125 pub hiddens: Vec<f32>,
127 pub n: usize,
129 pub merge: std::collections::HashMap<usize, i64>,
131 prev_ucel: bool,
132 skip: bool,
133 first_lcel: bool,
134 bbox_ind: usize,
135 cur_bbox_ind: usize,
136}
137
138impl BboxBook {
139 pub fn new() -> Self {
140 Self {
141 tags: vec![START],
142 skip: true, first_lcel: true,
144 ..Default::default()
145 }
146 }
147
148 pub fn step(&mut self, raw: i64, hidden: &[f32]) -> bool {
151 let tag = correct(raw, self.prev_ucel);
152 if tag == END {
153 return false;
154 }
155 if !self.skip && matches!(tag, FCEL | ECEL | CHED | RHED | SROW | NL | UCEL) {
157 self.hiddens.extend_from_slice(hidden);
158 self.n += 1;
159 if !self.first_lcel {
160 self.merge.insert(self.cur_bbox_ind, self.bbox_ind as i64);
161 }
162 self.bbox_ind += 1;
163 }
164 if tag != LCEL {
165 self.first_lcel = true;
166 } else if self.first_lcel {
167 self.hiddens.extend_from_slice(hidden);
168 self.n += 1;
169 self.first_lcel = false;
170 self.cur_bbox_ind = self.bbox_ind;
171 self.merge.insert(self.cur_bbox_ind, -1);
172 self.bbox_ind += 1;
173 }
174 self.skip = matches!(tag, NL | UCEL | XCEL);
175 self.prev_ucel = tag == UCEL;
176 self.otsl.push(tag);
177 self.tags.push(tag);
178 true
179 }
180}
181
182fn mergebboxes(b1: [f32; 4], b2: [f32; 4]) -> [f32; 4] {
185 let new_w = (b2[0] + b2[2] / 2.0) - (b1[0] - b1[2] / 2.0);
186 let new_h = (b2[1] + b2[3] / 2.0) - (b1[1] - b1[3] / 2.0);
187 let new_left = b1[0] - b1[2] / 2.0;
188 let new_top = (b2[1] - b2[3] / 2.0).min(b1[1] - b1[3] / 2.0);
189 [new_left + new_w / 2.0, new_top + new_h / 2.0, new_w, new_h]
190}
191
192pub fn merge_spans(
196 boxes: &[[f32; 4]],
197 classes: &[i64],
198 merge: &std::collections::HashMap<usize, i64>,
199) -> (Vec<[f32; 4]>, Vec<i64>) {
200 let skip: std::collections::HashSet<usize> = merge
201 .values()
202 .filter(|&&v| v >= 0)
203 .map(|&v| v as usize)
204 .collect();
205 let mut out = Vec::new();
206 let mut out_classes = Vec::new();
207 for (i, &b) in boxes.iter().enumerate() {
208 let class = classes.get(i).copied().unwrap_or(2);
209 if let Some(&j) = merge.get(&i) {
210 let partner = if j < 0 { boxes.len() - 1 } else { j as usize };
211 out.push(mergebboxes(b, boxes[partner.min(boxes.len() - 1)]));
212 out_classes.push(class);
213 } else if !skip.contains(&i) {
214 out.push(b);
215 out_classes.push(class);
216 }
217 }
218 (out, out_classes)
219}
220
221pub fn build_table_cells(otsl: &[i64], boxes: &[[f32; 4]], classes: &[i64]) -> Vec<TableCell> {
227 let mut grid: Vec<Vec<i64>> = vec![Vec::new()];
229 for &t in otsl {
230 if t == NL {
231 grid.push(Vec::new());
232 } else {
233 grid.last_mut().unwrap().push(t);
234 }
235 }
236 let mut cells = Vec::new();
237 let mut cell_id = 0usize;
238 for (r, row) in grid.iter().enumerate() {
239 for (c, &tag) in row.iter().enumerate() {
240 if !CELL_TAGS.contains(&tag) {
241 continue;
242 }
243 let mut colspan = 1;
244 while c + colspan < row.len() && matches!(row[c + colspan], LCEL | XCEL) {
245 colspan += 1;
246 }
247 let mut rowspan = 1;
248 while r + rowspan < grid.len()
249 && grid[r + rowspan]
250 .get(c)
251 .is_some_and(|&t| matches!(t, UCEL | XCEL))
252 {
253 rowspan += 1;
254 }
255 let b = boxes.get(cell_id).copied().unwrap_or([0.0; 4]);
256 let class = classes.get(cell_id).copied().unwrap_or(2);
258 cells.push(TableCell {
259 row: r,
260 col: c,
261 colspan,
262 rowspan,
263 tag,
264 class,
265 cx: b[0],
266 cy: b[1],
267 w: b[2],
268 h: b[3],
269 });
270 cell_id += 1;
271 }
272 }
273 cells
274}
275
276pub fn argmax(v: &[f32]) -> usize {
280 v.iter()
281 .enumerate()
282 .max_by(|a, b| a.1.total_cmp(b.1))
283 .map(|(i, _)| i)
284 .unwrap_or(0)
285}
286
287use crate::pdfium_backend::TextCell;
288use crate::tf_match::{PdfWord, TfCell};
289
290pub fn table_rows(cells: &[TableCell], region: [f32; 4], words: &[TextCell]) -> Option<TableGrid> {
297 let table_words: Vec<PdfWord> = words
301 .iter()
302 .enumerate()
303 .filter(|(_, w)| !w.text.trim().is_empty())
304 .filter_map(|(wi, w)| {
305 let (l, t, r, b) = (w.l as f64, w.t as f64, w.r as f64, w.b as f64);
306 let area = (r - l) * (b - t);
307 let iw = (r.min(region[2] as f64) - l.max(region[0] as f64)).max(0.0);
308 let ih = (b.min(region[3] as f64) - t.max(region[1] as f64)).max(0.0);
309 if area > 0.0 && iw * ih / area > 0.8 {
310 Some(PdfWord {
311 id: wi,
312 bbox: [l, t, r, b],
313 text: w.text.trim().to_string(),
314 })
315 } else {
316 None
317 }
318 })
319 .collect();
320
321 if !table_words.is_empty() && !simple_match() {
322 return docling_match_rows(cells, region, &table_words, words);
323 }
324
325 let (rw, rh) = (region[2] - region[0], region[3] - region[1]);
326
327 let boxes: Vec<[f32; 4]> = cells
329 .iter()
330 .map(|c| {
331 [
332 region[0] + (c.cx - c.w / 2.0) * rw,
333 region[1] + (c.cy - c.h / 2.0) * rh,
334 region[0] + (c.cx + c.w / 2.0) * rw,
335 region[1] + (c.cy + c.h / 2.0) * rh,
336 ]
337 })
338 .collect();
339
340 let mut cell_words: Vec<Vec<usize>> = vec![Vec::new(); cells.len()];
342 for (wi, w) in words.iter().enumerate() {
343 let wa = ((w.r - w.l) * (w.b - w.t)).max(1.0);
344 let mut best: Option<(f32, usize)> = None;
345 for (ci, b) in boxes.iter().enumerate() {
346 let ix = (w.r.min(b[2]) - w.l.max(b[0])).max(0.0);
347 let iy = (w.b.min(b[3]) - w.t.max(b[1])).max(0.0);
348 let io = ix * iy / wa;
349 if io > 0.0 && best.is_none_or(|(bo, _)| io > bo) {
350 best = Some((io, ci));
351 }
352 }
353 if let Some((_, ci)) = best {
354 cell_words[ci].push(wi);
355 }
356 }
357
358 let num_rows = cells.iter().map(|c| c.row + c.rowspan).max().unwrap_or(0);
359 let num_cols = cells.iter().map(|c| c.col + c.colspan).max().unwrap_or(0);
360 if num_rows == 0 || num_cols == 0 {
361 return None;
362 }
363 let mut grid = vec![vec![String::new(); num_cols]; num_rows];
364 let mut out_cells = Vec::with_capacity(cells.len());
365 for (ci, c) in cells.iter().enumerate() {
366 let wis = std::mem::take(&mut cell_words[ci]);
369 let text = wis
370 .iter()
371 .map(|&i| words[i].text.trim())
372 .collect::<Vec<_>>()
373 .join(" ");
374 let text = normalize_cell_text(text);
375 for row in grid.iter_mut().skip(c.row).take(c.rowspan) {
377 for cell in row.iter_mut().skip(c.col).take(c.colspan) {
378 *cell = text.clone();
379 }
380 }
381 out_cells.push(first_class_cell(text, Some(boxes[ci]), c, c.tag));
382 }
383 Some(TableGrid {
384 rows: grid,
385 cells: out_cells,
386 })
387}
388
389fn first_class_cell(
392 text: String,
393 bbox: Option<[f32; 4]>,
394 c: &TableCell,
395 tag: i64,
396) -> docling_core::TableCell {
397 docling_core::TableCell {
398 text,
399 bbox,
400 start_row: c.row,
401 start_col: c.col,
402 row_span: c.rowspan.max(1),
403 col_span: c.colspan.max(1),
404 column_header: tag == CHED,
405 row_header: tag == RHED,
406 row_section: tag == SROW,
407 }
408}
409
410fn simple_match() -> bool {
413 docling_core::env::flag("DOCLING_RS_TF_SIMPLE_MATCH")
414}
415
416fn normalize_cell_text(text: String) -> String {
421 text.replace("@ ", "@")
422}
423
424fn docling_match_rows(
432 cells: &[TableCell],
433 region: [f32; 4],
434 table_words: &[PdfWord],
435 words: &[TextCell],
436) -> Option<TableGrid> {
437 const SCALE: f64 = 2.0; let sl = (region[0] as f64).round_ties_even() * SCALE;
439 let st = (region[1] as f64).round_ties_even() * SCALE;
440 let sr = (region[2] as f64).round_ties_even() * SCALE;
441 let sb = (region[3] as f64).round_ties_even() * SCALE;
442 let (w2, h2) = (sr - sl, sb - st);
443
444 let tf_cells: Vec<TfCell> = cells
445 .iter()
446 .enumerate()
447 .map(|(i, c)| {
448 let (cx, cy) = (c.cx as f64, c.cy as f64);
449 let (w, h) = (c.w as f64, c.h as f64);
450 TfCell {
451 bbox: [
452 sl + (cx - w / 2.0) * w2,
453 st + (cy - h / 2.0) * h2,
454 sl + (cx + w / 2.0) * w2,
455 st + (cy + h / 2.0) * h2,
456 ],
457 cell_id: i,
458 row_id: c.row,
459 column_id: c.col,
460 cell_class: c.class,
461 colspan_val: if c.colspan > 1 { c.colspan } else { 0 },
462 rowspan_val: if c.rowspan > 1 { c.rowspan } else { 0 },
463 }
464 })
465 .collect();
466
467 let scaled_words: Vec<PdfWord> = table_words
468 .iter()
469 .map(|w| PdfWord {
470 id: w.id,
471 bbox: [
472 w.bbox[0] * SCALE,
473 w.bbox[1] * SCALE,
474 w.bbox[2] * SCALE,
475 w.bbox[3] * SCALE,
476 ],
477 text: w.text.clone(),
478 })
479 .collect();
480
481 #[cfg(feature = "ml")]
484 if let Some(dir) = docling_core::env::nonempty("DOCLING_RS_TF_MATCH_DUMP") {
485 dump_match_inputs(&dir, &tf_cells, &scaled_words);
486 }
487
488 let (cells_wo, final_matches) =
489 crate::tf_match::match_and_post_process(tf_cells, &scaled_words);
490
491 struct Merged {
494 start_row: usize,
495 start_col: usize,
496 row_span: usize,
497 col_span: usize,
498 word_ids: Vec<usize>,
499 bbox: [f32; 4],
502 tag: i64,
504 }
505 let mut merged: Vec<Merged> = Vec::new();
506 let mut key_ix: std::collections::HashMap<(usize, usize), usize> =
507 std::collections::HashMap::new();
508 for (&pdf_id, list) in &final_matches {
509 let tm = list[0].table_cell_id;
510 let Some(cell) = cells_wo.iter().find(|c| c.cell_id == tm) else {
511 continue;
512 };
513 match key_ix.entry((cell.column_id, cell.row_id)) {
514 std::collections::hash_map::Entry::Occupied(e) => {
515 merged[*e.get()].word_ids.push(pdf_id);
516 }
517 std::collections::hash_map::Entry::Vacant(e) => {
518 e.insert(merged.len());
519 merged.push(Merged {
520 start_row: cell.row_id,
521 start_col: cell.column_id,
522 row_span: cell.rowspan_val.max(1),
523 col_span: cell.colspan_val.max(1),
524 word_ids: vec![pdf_id],
525 bbox: [
526 (cell.bbox[0] / 2.0) as f32,
527 (cell.bbox[1] / 2.0) as f32,
528 (cell.bbox[2] / 2.0) as f32,
529 (cell.bbox[3] / 2.0) as f32,
530 ],
531 tag: cells.get(cell.cell_id).map_or(FCEL, |c| c.tag),
532 });
533 }
534 }
535 }
536 if merged.is_empty() {
537 return None;
538 }
539
540 let mut start_cols: Vec<usize> = merged.iter().map(|m| m.start_col).collect();
543 start_cols.sort_unstable();
544 start_cols.dedup();
545 let mut start_rows: Vec<usize> = merged.iter().map(|m| m.start_row).collect();
546 start_rows.sort_unstable();
547 start_rows.dedup();
548 let mut num_rows = 0;
549 let mut num_cols = 0;
550 for m in &mut merged {
551 m.start_col = start_cols.binary_search(&m.start_col).expect("own value");
552 m.start_row = start_rows.binary_search(&m.start_row).expect("own value");
553 num_cols = num_cols.max(m.start_col + m.col_span);
554 num_rows = num_rows.max(m.start_row + m.row_span);
555 }
556 if num_rows == 0 || num_cols == 0 {
557 return None;
558 }
559
560 let mut grid = vec![vec![String::new(); num_cols]; num_rows];
561 let mut out_cells = Vec::with_capacity(merged.len());
562 for m in &merged {
563 let text = m
564 .word_ids
565 .iter()
566 .map(|&i| words[i].text.trim())
567 .collect::<Vec<_>>()
568 .join(" ");
569 let text = normalize_cell_text(text);
570 for row in grid.iter_mut().skip(m.start_row).take(m.row_span) {
571 for cell in row.iter_mut().skip(m.start_col).take(m.col_span) {
572 *cell = text.clone();
573 }
574 }
575 out_cells.push(docling_core::TableCell {
576 text,
577 bbox: Some(m.bbox),
578 start_row: m.start_row,
579 start_col: m.start_col,
580 row_span: m.row_span,
581 col_span: m.col_span,
582 column_header: m.tag == CHED,
583 row_header: m.tag == RHED,
584 row_section: m.tag == SROW,
585 });
586 }
587 Some(TableGrid {
588 rows: grid,
589 cells: out_cells,
590 })
591}
592
593#[cfg(feature = "ml")]
596fn dump_match_inputs(dir: &str, tf_cells: &[TfCell], words: &[PdfWord]) {
597 use std::io::Write;
598 let cells: Vec<String> = tf_cells
599 .iter()
600 .map(|c| {
601 format!(
602 r#"{{"bbox":[{},{},{},{}],"cell_id":{},"row_id":{},"column_id":{},"cell_class":{},"colspan_val":{},"rowspan_val":{}}}"#,
603 c.bbox[0], c.bbox[1], c.bbox[2], c.bbox[3],
604 c.cell_id, c.row_id, c.column_id, c.cell_class,
605 c.colspan_val, c.rowspan_val
606 )
607 })
608 .collect();
609 let ws: Vec<String> = words
610 .iter()
611 .map(|w| {
612 format!(
613 r#"{{"id":{},"bbox":[{},{},{},{}],"text":{}}}"#,
614 w.id,
615 w.bbox[0],
616 w.bbox[1],
617 w.bbox[2],
618 w.bbox[3],
619 serde_json_escape(&w.text)
620 )
621 })
622 .collect();
623 let line = format!(
624 r#"{{"table_cells":[{}],"pdf_cells":[{}]}}"#,
625 cells.join(","),
626 ws.join(",")
627 );
628 if let Ok(mut f) = std::fs::OpenOptions::new()
629 .create(true)
630 .append(true)
631 .open(format!("{dir}/tf_match_dump.jsonl"))
632 {
633 let _ = writeln!(f, "{line}");
634 }
635}
636
637#[cfg(feature = "ml")]
639fn serde_json_escape(s: &str) -> String {
640 let mut out = String::with_capacity(s.len() + 2);
641 out.push('"');
642 for ch in s.chars() {
643 match ch {
644 '"' => out.push_str("\\\""),
645 '\\' => out.push_str("\\\\"),
646 '\n' => out.push_str("\\n"),
647 '\r' => out.push_str("\\r"),
648 '\t' => out.push_str("\\t"),
649 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
650 c => out.push(c),
651 }
652 }
653 out.push('"');
654 out
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 #[test]
662 fn corrections() {
663 assert_eq!(correct(XCEL, false), LCEL); assert_eq!(correct(LCEL, true), FCEL); assert_eq!(correct(XCEL, true), FCEL); assert_eq!(correct(FCEL, false), FCEL);
667 assert_eq!(correct(LCEL, false), LCEL);
668 }
669
670 #[test]
671 fn argmax_behaviour() {
672 assert_eq!(argmax(&[0.1, 0.9, 0.3]), 1);
673 assert_eq!(argmax(&[0.5, 0.5]), 1); assert_eq!(argmax(&[]), 0);
675 }
676
677 #[test]
678 fn book_skips_first_and_collects_hiddens() {
679 let mut b = BboxBook::new();
683 let h = [1.0f32; EMBED_DIM];
684 assert!(b.step(FCEL, &h)); assert!(b.step(FCEL, &h));
686 assert!(b.step(NL, &h));
687 assert!(!b.step(END, &h)); assert_eq!(b.otsl, vec![FCEL, FCEL, NL]);
689 assert_eq!(b.n, 2);
691 assert_eq!(b.hiddens.len(), 2 * EMBED_DIM);
692 assert!(b.merge.is_empty());
693 }
694
695 #[test]
696 fn book_merges_horizontal_span() {
697 let mut b = BboxBook::new();
700 let h = [0.0f32; EMBED_DIM];
701 b.step(FCEL, &h); b.step(FCEL, &h); b.step(LCEL, &h); assert_eq!(b.merge.get(&1), Some(&-1));
705 }
706
707 #[test]
708 fn build_cells_spans() {
709 let otsl = vec![FCEL, LCEL, NL, FCEL, ECEL];
712 let boxes = vec![[0.0; 4]; 3];
713 let classes = vec![2, 2, 2];
714 let cells = build_table_cells(&otsl, &boxes, &classes);
715 assert_eq!(cells.len(), 3);
716 assert_eq!((cells[0].colspan, cells[0].rowspan), (2, 1));
717 assert_eq!((cells[0].row, cells[0].col), (0, 0));
718 assert_eq!((cells[1].row, cells[1].col), (1, 0));
719 }
720}