1use crate::pdfium_backend::TextCell;
8use image::RgbImage;
9use ort::session::Session;
10use ort::value::{DynValue, Tensor};
11
12const SIDE: u32 = 448;
13#[allow(clippy::excessive_precision)]
16const MEAN: [f32; 3] = [0.94247851, 0.94254675, 0.94292611];
17#[allow(clippy::excessive_precision)]
18const STD: [f32; 3] = [0.17910956, 0.17940403, 0.17931663];
19const MAX_STEPS: usize = 1024;
20const N_LAYERS: usize = 6;
23const EMBED_DIM: usize = 512;
24
25pub const START: i64 = 2;
27pub const END: i64 = 3;
28pub 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; #[derive(Debug, Clone)]
42pub struct TableCell {
43 pub row: usize,
44 pub col: usize,
45 pub colspan: usize,
46 pub rowspan: usize,
47 pub tag: i64,
48 pub class: i64,
49 pub cx: f32,
50 pub cy: f32,
51 pub w: f32,
52 pub h: f32,
53}
54
55pub struct TableFormer {
56 encoder: Session,
57 decoder: Session,
58 bbox: Session,
59 style: DecoderStyle,
63}
64
65#[derive(Clone, Copy, PartialEq, Eq)]
67enum DecoderStyle {
68 Legacy,
71 KvStacked,
74 KvHoisted,
79}
80
81const KV_HEADS: usize = 8;
84const KV_HEAD_DIM: usize = 64;
85
86#[derive(Default)]
90struct DecodeCache {
91 a: Option<DynValue>,
92 b: Option<DynValue>,
93}
94
95type EmptyCache = (Tensor<f32>, Option<Tensor<f32>>);
98
99struct EncodeOut {
104 ck: DynValue,
105 cv: DynValue,
106 eo: DynValue,
107 per_layer: Vec<(String, DynValue)>,
110}
111
112impl TableFormer {
113 pub fn load() -> Option<Self> {
117 Self::load_with(crate::intra_threads())
118 }
119
120 pub fn load_with(intra: usize) -> Option<Self> {
124 let enc = std::env::var("DOCLING_TABLEFORMER_ENCODER")
125 .unwrap_or_else(|_| crate::resolve_asset("models/tableformer/encoder.onnx"));
126 let dec = std::env::var("DOCLING_TABLEFORMER_DECODER").unwrap_or_else(|_| {
137 let candidates: &[&str] = if crate::prefer_fp32() {
138 &[
139 "models/tableformer/decoder_kv.onnx",
140 "models/tableformer/decoder.onnx",
141 ]
142 } else {
143 &[
148 "models/tableformer/decoder_kv_int8.onnx",
149 "models/tableformer/decoder_kv.onnx",
150 "models/tableformer/decoder_int8.onnx",
151 "models/tableformer/decoder.onnx",
152 ]
153 };
154 candidates
155 .iter()
156 .map(|p| crate::resolve_asset(p))
157 .find(|p| std::path::Path::new(p).exists())
158 .unwrap_or_else(|| "models/tableformer/decoder.onnx".to_string())
159 });
160 let bbx = std::env::var("DOCLING_TABLEFORMER_BBOX")
161 .unwrap_or_else(|_| crate::resolve_asset("models/tableformer/bbox.onnx"));
162 if crate::timing::enabled() {
163 eprintln!("docling-pdf: tableformer decoder: {dec}");
164 }
165 if [&enc, &dec, &bbx]
166 .iter()
167 .any(|p| !std::path::Path::new(p).exists())
168 {
169 warn_missing_once(&enc, &dec, &bbx);
176 return None;
177 }
178 let build = |path: &str, mem_pattern: bool| -> Result<Session, String> {
184 let builder = Session::builder()
185 .map_err(|e| e.to_string())?
186 .with_intra_threads(intra)
187 .map_err(|e| e.to_string())?
188 .with_memory_pattern(mem_pattern)
189 .map_err(|e| e.to_string())?;
190 crate::ep::apply(builder)?
191 .commit_from_file(path)
192 .map_err(|e| format!("tableformer load {path}: {e}"))
193 };
194 match (build(&enc, true), build(&dec, false), build(&bbx, true)) {
195 (Ok(encoder), Ok(decoder), Ok(bbox)) => {
196 let has = |n: &str| decoder.inputs().iter().any(|i| i.name() == n);
197 let style = if has("cross_kt_0") {
198 DecoderStyle::KvHoisted
199 } else if has("cache_k") {
200 DecoderStyle::KvStacked
201 } else {
202 DecoderStyle::Legacy
203 };
204 if style == DecoderStyle::KvHoisted
205 && !encoder.outputs().iter().any(|o| o.name() == "cross_kt_0")
206 {
207 eprintln!(
208 "docling-pdf: tableformer decoder needs per-layer cross tensors \
209 (cross_kt_*) the encoder doesn't emit — re-download or re-export \
210 the model set (scripts/install/export_tableformer.py); \
211 falling back to geometric tables"
212 );
213 return None;
214 }
215 Some(Self {
216 encoder,
217 decoder,
218 bbox,
219 style,
220 })
221 }
222 _ => None,
223 }
224 }
225
226 fn encode(&mut self, img: &RgbImage) -> Result<EncodeOut, String> {
230 let input = preprocess(img)?;
231 let mut enc_out = self
232 .encoder
233 .run(ort::inputs!["image" => input])
234 .map_err(|e| format!("tableformer: encode: {e}"))?;
235 let mut per_layer = Vec::new();
236 if self.style == DecoderStyle::KvHoisted {
237 for prefix in ["cross_kt_", "cross_v_"] {
238 for i in 0.. {
239 let name = format!("{prefix}{i}");
240 match enc_out.remove(&name) {
241 Some(v) => per_layer.push((name, v)),
242 None => break,
243 }
244 }
245 }
246 if per_layer.is_empty() {
247 return Err("tableformer: encoder emitted no cross_kt_* outputs".into());
248 }
249 }
250 let mut grab = |name: &str| -> Result<DynValue, String> {
251 enc_out
252 .remove(name)
253 .ok_or_else(|| format!("tableformer: encoder output {name} missing"))
254 };
255 Ok(EncodeOut {
256 ck: grab("cross_k")?,
257 cv: grab("cross_v")?,
258 eo: grab("enc_out")?,
259 per_layer,
260 })
261 }
262
263 fn decode_step(
272 &mut self,
273 tags: &[i64],
274 enc: &EncodeOut,
275 cache: &mut DecodeCache,
276 empty: &EmptyCache,
277 ) -> Result<(i64, Vec<f32>), String> {
278 crate::timing::timed("tf.decode_step", || {
279 self.decode_step_inner(tags, enc, cache, empty)
280 })
281 }
282
283 fn decode_step_inner(
284 &mut self,
285 tags: &[i64],
286 enc: &EncodeOut,
287 cache: &mut DecodeCache,
288 empty: &EmptyCache,
289 ) -> Result<(i64, Vec<f32>), String> {
290 let mut dout = match self.style {
291 DecoderStyle::KvHoisted => {
292 let last = *tags.last().expect("decode starts from <start>");
295 let tag_t = Tensor::from_array(([1usize, 1usize], vec![last]))
296 .map_err(|e| format!("tableformer: tag: {e}"))?;
297 let mut inputs: Vec<(
298 std::borrow::Cow<'_, str>,
299 ort::session::SessionInputValue<'_>,
300 )> = Vec::with_capacity(3 + enc.per_layer.len());
301 inputs.push(("tag".into(), tag_t.into()));
302 match (cache.a.as_ref(), cache.b.as_ref()) {
303 (Some(k), Some(v)) => {
304 inputs.push(("cache_k".into(), k.into()));
305 inputs.push(("cache_v".into(), v.into()));
306 }
307 _ => {
308 inputs.push(("cache_k".into(), (&empty.0).into()));
309 inputs.push((
310 "cache_v".into(),
311 empty
312 .1
313 .as_ref()
314 .expect("kv empty cache has both halves")
315 .into(),
316 ));
317 }
318 }
319 for (name, v) in &enc.per_layer {
320 inputs.push((name.as_str().into(), v.into()));
321 }
322 self.decoder.run(inputs)
323 }
324 DecoderStyle::KvStacked => {
325 let last = *tags.last().expect("decode starts from <start>");
329 let tag_t = Tensor::from_array(([1usize, 1usize], vec![last]))
330 .map_err(|e| format!("tableformer: tag: {e}"))?;
331 match (cache.a.as_ref(), cache.b.as_ref()) {
332 (Some(k), Some(v)) => self.decoder.run(ort::inputs![
333 "tag" => tag_t, "cross_k" => &enc.ck, "cross_v" => &enc.cv,
334 "cache_k" => k, "cache_v" => v]),
335 _ => self.decoder.run(ort::inputs![
336 "tag" => tag_t, "cross_k" => &enc.ck, "cross_v" => &enc.cv,
337 "cache_k" => &empty.0,
338 "cache_v" => empty.1.as_ref().expect("kv empty cache has both halves")]),
339 }
340 }
341 DecoderStyle::Legacy => {
342 let tags_t = Tensor::from_array(([tags.len(), 1usize], tags.to_vec()))
343 .map_err(|e| format!("tableformer: tags: {e}"))?;
344 match cache.a.as_ref() {
345 None => self.decoder.run(ort::inputs![
346 "tags" => tags_t, "cross_k" => &enc.ck, "cross_v" => &enc.cv,
347 "cache" => &empty.0]),
348 Some(c) => self.decoder.run(ort::inputs![
349 "tags" => tags_t, "cross_k" => &enc.ck, "cross_v" => &enc.cv,
350 "cache" => c]),
351 }
352 }
353 }
354 .map_err(|e| format!("tableformer: decode: {e}"))?;
355 let (_, logits) = dout["logits"]
356 .try_extract_tensor::<f32>()
357 .map_err(|e| format!("tableformer: logits: {e}"))?;
358 let raw = argmax(logits) as i64;
359 let (_, hidden) = dout["hidden"]
360 .try_extract_tensor::<f32>()
361 .map_err(|e| format!("tableformer: hidden: {e}"))?;
362 let hidden = hidden.to_vec();
363 if self.style != DecoderStyle::Legacy {
364 cache.a = Some(
365 dout.remove("out_cache_k")
366 .ok_or_else(|| "tableformer: out_cache_k missing".to_string())?,
367 );
368 cache.b = Some(
369 dout.remove("out_cache_v")
370 .ok_or_else(|| "tableformer: out_cache_v missing".to_string())?,
371 );
372 } else {
373 cache.a = Some(
374 dout.remove("out_cache")
375 .ok_or_else(|| "tableformer: decoder output out_cache missing".to_string())?,
376 );
377 }
378 Ok((raw, hidden))
379 }
380
381 fn empty_cache(&self) -> Result<EmptyCache, String> {
385 let alloc = self.decoder.allocator();
386 if self.style != DecoderStyle::Legacy {
387 let mk = || {
388 Tensor::<f32>::new(alloc, [N_LAYERS, 1, KV_HEADS, 0usize, KV_HEAD_DIM])
389 .map_err(|e| format!("tableformer: empty kv cache: {e}"))
390 };
391 Ok((mk()?, Some(mk()?)))
392 } else {
393 let c = Tensor::<f32>::new(alloc, [N_LAYERS, 0usize, 1, EMBED_DIM])
394 .map_err(|e| format!("tableformer: empty cache: {e}"))?;
395 Ok((c, None))
396 }
397 }
398
399 pub fn predict_otsl(&mut self, img: &RgbImage) -> Result<Vec<i64>, String> {
401 let enc = self.encode(img)?;
402 let mut tags: Vec<i64> = vec![START];
405 let mut out: Vec<i64> = Vec::new();
406 let mut prev_ucel = false;
407 let mut cache = DecodeCache::default();
408 let empty = self.empty_cache()?;
409 while out.len() < MAX_STEPS {
410 let (raw, _hidden) = self.decode_step(&tags, &enc, &mut cache, &empty)?;
411 let mut tag = raw;
412 if tag == XCEL {
413 tag = LCEL;
414 }
415 if prev_ucel && tag == LCEL {
416 tag = FCEL;
417 }
418 if tag == END {
419 break;
420 }
421 out.push(tag);
422 tags.push(tag);
423 prev_ucel = tag == UCEL;
424 }
425 Ok(out)
426 }
427
428 pub fn predict_table_structure(&mut self, img: &RgbImage) -> Result<Vec<TableCell>, String> {
434 let enc = self.encode(img)?;
435
436 let mut tags: Vec<i64> = vec![START];
437 let mut otsl: Vec<i64> = Vec::new();
438 let mut hiddens: Vec<f32> = Vec::new(); let mut n = 0usize;
440 let mut prev_ucel = false;
441 let mut skip = true; let mut first_lcel = true;
443 let mut bbox_ind = 0usize;
444 let mut cur_bbox_ind = 0usize;
445 let mut merge: std::collections::HashMap<usize, i64> = std::collections::HashMap::new();
446 let mut cache = DecodeCache::default();
447 let empty = self.empty_cache()?;
448 while otsl.len() < MAX_STEPS {
449 let (raw, hidden) = self.decode_step(&tags, &enc, &mut cache, &empty)?;
450 let mut tag = raw;
451 if tag == XCEL {
452 tag = LCEL;
453 }
454 if prev_ucel && tag == LCEL {
455 tag = FCEL;
456 }
457 if tag == END {
458 break;
459 }
460 if !skip && matches!(tag, FCEL | ECEL | CHED | RHED | SROW | NL | UCEL) {
462 hiddens.extend_from_slice(&hidden);
463 n += 1;
464 if !first_lcel {
465 merge.insert(cur_bbox_ind, bbox_ind as i64);
466 }
467 bbox_ind += 1;
468 }
469 if tag != LCEL {
470 first_lcel = true;
471 } else if first_lcel {
472 hiddens.extend_from_slice(&hidden);
473 n += 1;
474 first_lcel = false;
475 cur_bbox_ind = bbox_ind;
476 merge.insert(cur_bbox_ind, -1);
477 bbox_ind += 1;
478 }
479 skip = matches!(tag, NL | UCEL | XCEL);
480 prev_ucel = tag == UCEL;
481 otsl.push(tag);
482 tags.push(tag);
483 }
484 if n == 0 {
485 return Ok(Vec::new());
486 }
487 let tag_h = Tensor::from_array(([n, 512usize], hiddens))
488 .map_err(|e| format!("tableformer: tag_h: {e}"))?;
489 let bout = self
490 .bbox
491 .run(ort::inputs!["enc_out" => &enc.eo, "tag_h" => tag_h])
492 .map_err(|e| format!("tableformer: bbox: {e}"))?;
493 let (_, raw) = bout["boxes"]
494 .try_extract_tensor::<f32>()
495 .map_err(|e| format!("tableformer: boxes: {e}"))?;
496 let boxes: Vec<[f32; 4]> = raw
497 .chunks_exact(4)
498 .map(|c| [c[0], c[1], c[2], c[3]])
499 .collect();
500 let (_, craw) = bout["classes"]
502 .try_extract_tensor::<f32>()
503 .map_err(|e| format!("tableformer: classes: {e}"))?;
504 let classes: Vec<i64> = craw.chunks_exact(3).map(|c| argmax(c) as i64).collect();
505 let (merged, merged_classes) = merge_spans(&boxes, &classes, &merge);
506 Ok(build_table_cells(&otsl, &merged, &merged_classes))
507 }
508
509 pub fn predict_table_rows(
516 &mut self,
517 page_image: &RgbImage,
518 region: [f32; 4],
519 words: &[TextCell],
520 ) -> Option<Vec<Vec<String>>> {
521 let sf = 1024.0 / page_image.height() as f32;
530 let pw = (page_image.width() as f32 * sf) as u32;
531 let page1024 = crate::timing::timed("tableformer.inter_area", || {
532 crate::resample::inter_area(page_image, pw, 1024)
533 });
534 let k = 2.0 * 1024.0 / page_image.height() as f64;
535 let px = |v: f32| (v as f64).round_ties_even() * k;
536 let x = (px(region[0]).round_ties_even()).max(0.0) as u32;
537 let y = (px(region[1]).round_ties_even()).max(0.0) as u32;
538 let x2 = (px(region[2]).round_ties_even() as u32).min(page1024.width());
539 let y2 = (px(region[3]).round_ties_even() as u32).min(page1024.height());
540 if x2 <= x || y2 <= y {
541 return None;
542 }
543 let crop = image::imageops::crop_imm(&page1024, x, y, x2 - x, y2 - y).to_image();
544 let cells = crate::timing::timed("tableformer.structure", || {
545 self.predict_table_structure(&crop)
546 })
547 .ok()?;
548 if cells.is_empty() {
549 return None;
550 }
551 let table_words: Vec<crate::tf_match::PdfWord> = words
555 .iter()
556 .enumerate()
557 .filter(|(_, w)| !w.text.trim().is_empty())
558 .filter_map(|(wi, w)| {
559 let (l, t, r, b) = (w.l as f64, w.t as f64, w.r as f64, w.b as f64);
560 let area = (r - l) * (b - t);
561 let iw = (r.min(region[2] as f64) - l.max(region[0] as f64)).max(0.0);
562 let ih = (b.min(region[3] as f64) - t.max(region[1] as f64)).max(0.0);
563 if area > 0.0 && iw * ih / area > 0.8 {
564 Some(crate::tf_match::PdfWord {
565 id: wi,
566 bbox: [l, t, r, b],
567 text: w.text.trim().to_string(),
568 })
569 } else {
570 None
571 }
572 })
573 .collect();
574
575 if !table_words.is_empty() && !simple_match() {
576 return docling_match_rows(&cells, region, &table_words, words);
577 }
578
579 let (rw, rh) = (region[2] - region[0], region[3] - region[1]);
580
581 let boxes: Vec<[f32; 4]> = cells
583 .iter()
584 .map(|c| {
585 [
586 region[0] + (c.cx - c.w / 2.0) * rw,
587 region[1] + (c.cy - c.h / 2.0) * rh,
588 region[0] + (c.cx + c.w / 2.0) * rw,
589 region[1] + (c.cy + c.h / 2.0) * rh,
590 ]
591 })
592 .collect();
593
594 let mut cell_words: Vec<Vec<usize>> = vec![Vec::new(); cells.len()];
596 for (wi, w) in words.iter().enumerate() {
597 let wa = ((w.r - w.l) * (w.b - w.t)).max(1.0);
598 let mut best: Option<(f32, usize)> = None;
599 for (ci, b) in boxes.iter().enumerate() {
600 let ix = (w.r.min(b[2]) - w.l.max(b[0])).max(0.0);
601 let iy = (w.b.min(b[3]) - w.t.max(b[1])).max(0.0);
602 let io = ix * iy / wa;
603 if io > 0.0 && best.is_none_or(|(bo, _)| io > bo) {
604 best = Some((io, ci));
605 }
606 }
607 if let Some((_, ci)) = best {
608 cell_words[ci].push(wi);
609 }
610 }
611
612 let num_rows = cells.iter().map(|c| c.row + c.rowspan).max().unwrap_or(0);
613 let num_cols = cells.iter().map(|c| c.col + c.colspan).max().unwrap_or(0);
614 if num_rows == 0 || num_cols == 0 {
615 return None;
616 }
617 let mut grid = vec![vec![String::new(); num_cols]; num_rows];
618 for (ci, c) in cells.iter().enumerate() {
619 let wis = std::mem::take(&mut cell_words[ci]);
623 let text = wis
624 .iter()
625 .map(|&i| words[i].text.trim())
626 .collect::<Vec<_>>()
627 .join(" ");
628 let text = normalize_cell_text(text);
629 for row in grid.iter_mut().skip(c.row).take(c.rowspan) {
631 for cell in row.iter_mut().skip(c.col).take(c.colspan) {
632 *cell = text.clone();
633 }
634 }
635 }
636 Some(grid)
637 }
638}
639
640fn dump_match_inputs(
643 dir: &str,
644 tf_cells: &[crate::tf_match::TfCell],
645 words: &[crate::tf_match::PdfWord],
646) {
647 use std::io::Write;
648 let cells: Vec<String> = tf_cells
649 .iter()
650 .map(|c| {
651 format!(
652 r#"{{"bbox":[{},{},{},{}],"cell_id":{},"row_id":{},"column_id":{},"cell_class":{},"colspan_val":{},"rowspan_val":{}}}"#,
653 c.bbox[0], c.bbox[1], c.bbox[2], c.bbox[3],
654 c.cell_id, c.row_id, c.column_id, c.cell_class,
655 c.colspan_val, c.rowspan_val
656 )
657 })
658 .collect();
659 let ws: Vec<String> = words
660 .iter()
661 .map(|w| {
662 format!(
663 r#"{{"id":{},"bbox":[{},{},{},{}],"text":{}}}"#,
664 w.id,
665 w.bbox[0],
666 w.bbox[1],
667 w.bbox[2],
668 w.bbox[3],
669 serde_json_escape(&w.text)
670 )
671 })
672 .collect();
673 let line = format!(
674 r#"{{"table_cells":[{}],"pdf_cells":[{}]}}"#,
675 cells.join(","),
676 ws.join(",")
677 );
678 if let Ok(mut f) = std::fs::OpenOptions::new()
679 .create(true)
680 .append(true)
681 .open(format!("{dir}/tf_match_dump.jsonl"))
682 {
683 let _ = writeln!(f, "{line}");
684 }
685}
686
687fn serde_json_escape(s: &str) -> String {
689 let mut out = String::with_capacity(s.len() + 2);
690 out.push('"');
691 for ch in s.chars() {
692 match ch {
693 '"' => out.push_str("\\\""),
694 '\\' => out.push_str("\\\\"),
695 '\n' => out.push_str("\\n"),
696 '\r' => out.push_str("\\r"),
697 '\t' => out.push_str("\\t"),
698 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
699 c => out.push(c),
700 }
701 }
702 out.push('"');
703 out
704}
705
706fn simple_match() -> bool {
709 std::env::var("DOCLING_RS_TF_SIMPLE_MATCH").is_ok_and(|v| !v.is_empty() && v != "0")
710}
711
712fn normalize_cell_text(text: String) -> String {
718 text.replace("@ ", "@")
719}
720
721fn docling_match_rows(
729 cells: &[TableCell],
730 region: [f32; 4],
731 table_words: &[crate::tf_match::PdfWord],
732 words: &[TextCell],
733) -> Option<Vec<Vec<String>>> {
734 const SCALE: f64 = 2.0; let sl = (region[0] as f64).round_ties_even() * SCALE;
736 let st = (region[1] as f64).round_ties_even() * SCALE;
737 let sr = (region[2] as f64).round_ties_even() * SCALE;
738 let sb = (region[3] as f64).round_ties_even() * SCALE;
739 let (w2, h2) = (sr - sl, sb - st);
740
741 let tf_cells: Vec<crate::tf_match::TfCell> = cells
742 .iter()
743 .enumerate()
744 .map(|(i, c)| {
745 let (cx, cy) = (c.cx as f64, c.cy as f64);
746 let (w, h) = (c.w as f64, c.h as f64);
747 crate::tf_match::TfCell {
748 bbox: [
749 sl + (cx - w / 2.0) * w2,
750 st + (cy - h / 2.0) * h2,
751 sl + (cx + w / 2.0) * w2,
752 st + (cy + h / 2.0) * h2,
753 ],
754 cell_id: i,
755 row_id: c.row,
756 column_id: c.col,
757 cell_class: c.class,
758 colspan_val: if c.colspan > 1 { c.colspan } else { 0 },
759 rowspan_val: if c.rowspan > 1 { c.rowspan } else { 0 },
760 }
761 })
762 .collect();
763
764 let scaled_words: Vec<crate::tf_match::PdfWord> = table_words
765 .iter()
766 .map(|w| crate::tf_match::PdfWord {
767 id: w.id,
768 bbox: [
769 w.bbox[0] * SCALE,
770 w.bbox[1] * SCALE,
771 w.bbox[2] * SCALE,
772 w.bbox[3] * SCALE,
773 ],
774 text: w.text.clone(),
775 })
776 .collect();
777
778 if let Ok(dir) = std::env::var("DOCLING_RS_TF_MATCH_DUMP") {
781 if !dir.is_empty() {
782 dump_match_inputs(&dir, &tf_cells, &scaled_words);
783 }
784 }
785
786 let (cells_wo, final_matches) =
787 crate::tf_match::match_and_post_process(tf_cells, &scaled_words);
788
789 struct Merged {
792 start_row: usize,
793 start_col: usize,
794 row_span: usize,
795 col_span: usize,
796 word_ids: Vec<usize>,
797 }
798 let mut merged: Vec<Merged> = Vec::new();
799 let mut key_ix: std::collections::HashMap<(usize, usize), usize> =
800 std::collections::HashMap::new();
801 for (&pdf_id, list) in &final_matches {
802 let tm = list[0].table_cell_id;
803 let Some(cell) = cells_wo.iter().find(|c| c.cell_id == tm) else {
804 continue;
805 };
806 match key_ix.entry((cell.column_id, cell.row_id)) {
807 std::collections::hash_map::Entry::Occupied(e) => {
808 merged[*e.get()].word_ids.push(pdf_id);
809 }
810 std::collections::hash_map::Entry::Vacant(e) => {
811 e.insert(merged.len());
812 merged.push(Merged {
813 start_row: cell.row_id,
814 start_col: cell.column_id,
815 row_span: cell.rowspan_val.max(1),
816 col_span: cell.colspan_val.max(1),
817 word_ids: vec![pdf_id],
818 });
819 }
820 }
821 }
822 if merged.is_empty() {
823 return None;
824 }
825
826 let mut start_cols: Vec<usize> = merged.iter().map(|m| m.start_col).collect();
829 start_cols.sort_unstable();
830 start_cols.dedup();
831 let mut start_rows: Vec<usize> = merged.iter().map(|m| m.start_row).collect();
832 start_rows.sort_unstable();
833 start_rows.dedup();
834 let mut num_rows = 0;
835 let mut num_cols = 0;
836 for m in &mut merged {
837 m.start_col = start_cols.binary_search(&m.start_col).expect("own value");
838 m.start_row = start_rows.binary_search(&m.start_row).expect("own value");
839 num_cols = num_cols.max(m.start_col + m.col_span);
840 num_rows = num_rows.max(m.start_row + m.row_span);
841 }
842 if num_rows == 0 || num_cols == 0 {
843 return None;
844 }
845
846 let mut grid = vec![vec![String::new(); num_cols]; num_rows];
847 for m in &merged {
848 let text = m
849 .word_ids
850 .iter()
851 .map(|&i| words[i].text.trim())
852 .collect::<Vec<_>>()
853 .join(" ");
854 let text = normalize_cell_text(text);
855 for row in grid.iter_mut().skip(m.start_row).take(m.row_span) {
856 for cell in row.iter_mut().skip(m.start_col).take(m.col_span) {
857 *cell = text.clone();
858 }
859 }
860 }
861 Some(grid)
862}
863
864fn warn_missing_once(enc: &str, dec: &str, bbx: &str) {
871 static WARNED: std::sync::Once = std::sync::Once::new();
872 WARNED.call_once(|| {
873 eprintln!(
874 "docling.rs: TableFormer models not found (checked {enc}, {dec}, {bbx}); \
875 tables will use geometric reconstruction instead of ML table-structure \
876 recognition. Set DOCLING_TABLEFORMER_ENCODER / DOCLING_TABLEFORMER_DECODER \
877 / DOCLING_TABLEFORMER_BBOX to enable it (see README.md)."
878 );
879 });
880}
881
882fn preprocess(img: &RgbImage) -> Result<Tensor<f32>, String> {
887 let nn = (SIDE * SIDE) as usize;
888 let side = SIDE as usize;
889 let (sw, sh) = (img.width() as i32, img.height() as i32);
890 let sxr = sw as f32 / SIDE as f32;
891 let syr = sh as f32 / SIDE as f32;
892 let mut data = vec![0f32; 3 * nn];
893 for h in 0..side {
894 let fy = (h as f32 + 0.5) * syr - 0.5;
895 let wy = fy - fy.floor();
896 let y0c = (fy.floor() as i32).clamp(0, sh - 1) as u32;
897 let y1c = (fy.floor() as i32 + 1).clamp(0, sh - 1) as u32;
898 for w in 0..side {
899 let fx = (w as f32 + 0.5) * sxr - 0.5;
900 let wx = fx - fx.floor();
901 let x0c = (fx.floor() as i32).clamp(0, sw - 1) as u32;
902 let x1c = (fx.floor() as i32 + 1).clamp(0, sw - 1) as u32;
903 let p00 = img.get_pixel(x0c, y0c);
904 let p01 = img.get_pixel(x1c, y0c);
905 let p10 = img.get_pixel(x0c, y1c);
906 let p11 = img.get_pixel(x1c, y1c);
907 let idx = w * side + h; for c in 0..3 {
909 let top = p00[c] as f32 * (1.0 - wx) + p01[c] as f32 * wx;
910 let bot = p10[c] as f32 * (1.0 - wx) + p11[c] as f32 * wx;
911 let v = top * (1.0 - wy) + bot * wy;
912 data[c * nn + idx] = (v / 255.0 - MEAN[c]) / STD[c];
913 }
914 }
915 }
916 Tensor::from_array(([1usize, 3, side, side], data))
917 .map_err(|e| format!("tableformer: input: {e}"))
918}
919
920fn mergebboxes(b1: [f32; 4], b2: [f32; 4]) -> [f32; 4] {
923 let new_w = (b2[0] + b2[2] / 2.0) - (b1[0] - b1[2] / 2.0);
924 let new_h = (b2[1] + b2[3] / 2.0) - (b1[1] - b1[3] / 2.0);
925 let new_left = b1[0] - b1[2] / 2.0;
926 let new_top = (b2[1] - b2[3] / 2.0).min(b1[1] - b1[3] / 2.0);
927 [new_left + new_w / 2.0, new_top + new_h / 2.0, new_w, new_h]
928}
929
930fn merge_spans(
934 boxes: &[[f32; 4]],
935 classes: &[i64],
936 merge: &std::collections::HashMap<usize, i64>,
937) -> (Vec<[f32; 4]>, Vec<i64>) {
938 let skip: std::collections::HashSet<usize> = merge
939 .values()
940 .filter(|&&v| v >= 0)
941 .map(|&v| v as usize)
942 .collect();
943 let mut out = Vec::new();
944 let mut out_classes = Vec::new();
945 for (i, &b) in boxes.iter().enumerate() {
946 let class = classes.get(i).copied().unwrap_or(2);
947 if let Some(&j) = merge.get(&i) {
948 let partner = if j < 0 { boxes.len() - 1 } else { j as usize };
949 out.push(mergebboxes(b, boxes[partner.min(boxes.len() - 1)]));
950 out_classes.push(class);
951 } else if !skip.contains(&i) {
952 out.push(b);
953 out_classes.push(class);
954 }
955 }
956 (out, out_classes)
957}
958
959const CELL_TAGS: [i64; 6] = [FCEL, ECEL, XCEL, CHED, RHED, SROW];
960
961fn build_table_cells(otsl: &[i64], boxes: &[[f32; 4]], classes: &[i64]) -> Vec<TableCell> {
967 let mut grid: Vec<Vec<i64>> = vec![Vec::new()];
969 for &t in otsl {
970 if t == NL {
971 grid.push(Vec::new());
972 } else {
973 grid.last_mut().unwrap().push(t);
974 }
975 }
976 let mut cells = Vec::new();
977 let mut cell_id = 0usize;
978 for (r, row) in grid.iter().enumerate() {
979 for (c, &tag) in row.iter().enumerate() {
980 if !CELL_TAGS.contains(&tag) {
981 continue;
982 }
983 let mut colspan = 1;
984 while c + colspan < row.len() && matches!(row[c + colspan], LCEL | XCEL) {
985 colspan += 1;
986 }
987 let mut rowspan = 1;
988 while r + rowspan < grid.len()
989 && grid[r + rowspan]
990 .get(c)
991 .is_some_and(|&t| matches!(t, UCEL | XCEL))
992 {
993 rowspan += 1;
994 }
995 let b = boxes.get(cell_id).copied().unwrap_or([0.0; 4]);
996 let class = classes.get(cell_id).copied().unwrap_or(2);
998 cells.push(TableCell {
999 row: r,
1000 col: c,
1001 colspan,
1002 rowspan,
1003 tag,
1004 class,
1005 cx: b[0],
1006 cy: b[1],
1007 w: b[2],
1008 h: b[3],
1009 });
1010 cell_id += 1;
1011 }
1012 }
1013 cells
1014}
1015
1016fn argmax(v: &[f32]) -> usize {
1017 v.iter()
1018 .enumerate()
1019 .max_by(|a, b| a.1.total_cmp(b.1))
1020 .map(|(i, _)| i)
1021 .unwrap_or(0)
1022}