From 1f28e523fd6f2ab36949df16948eb707afed9f98 Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Thu, 30 Apr 2026 10:56:05 -0700 Subject: [PATCH] tables: keep wrapped labels in TSR output (#73) --- napi/package.json | 2 +- src/lib.rs | 32 ++++++++-- tests/fixtures/government_positions_women.pdf | Bin 0 -> 41942 bytes tests/integration_tests.rs | 55 ++++++++++++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/government_positions_women.pdf diff --git a/napi/package.json b/napi/package.json index 379939c..51edf36 100644 --- a/napi/package.json +++ b/napi/package.json @@ -1,6 +1,6 @@ { "name": "@firecrawl/pdf-inspector", - "version": "1.8.2", + "version": "1.8.3", "description": "Fast PDF classification and text extraction. Detect text-based vs scanned PDFs, extract text by region with quality checks. Native Rust performance via napi-rs.", "main": "index.js", "types": "index.d.ts", diff --git a/src/lib.rs b/src/lib.rs index 658d1fe..15c53ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2152,13 +2152,15 @@ fn try_expand_multi_row_cells( /// * `phantom_empty_row` — a row whose every cell is empty, surrounded /// above and below by rows with content. SLANet sometimes emits an /// extra row that doesn't correspond to any visible PDF row. -/// * `multi_row_in_cell` — at least one `rowspan==1` cell encloses -/// PDF text items that cluster into two distinct visual lines +/// * `multi_row_in_cell` — at least one non-label `rowspan==1` cell +/// encloses PDF text items that cluster into two distinct visual lines /// separated by a whitespace gap larger than the line height. Cells /// declared as `rowspan>1` are excluded since they are *expected* -/// to span multiple lines. SLANet's row under-detection on -/// tightly-packed tables produces the rowspan==1-but-multi-line -/// pattern (the FNBO failure mode). +/// to span multiple lines. First-row/first-column wraps are ignored +/// unless the in-place row expansion has enough support to repair them, +/// because those are often legitimate wrapped headers or row labels. +/// SLANet's row under-detection on tightly-packed tables produces the +/// rowspan==1-but-multi-line pattern (the FNBO failure mode). fn detect_tsr_quality_issue( buffer: &[u8], input: &TsrTableInput, @@ -2216,6 +2218,8 @@ fn detect_tsr_quality_issue( }; let expanded_cells = try_expand_multi_row_cells(cells, &items, page_h, coords, adaptive_threshold); + let first_row = cells.iter().map(|cell| cell.row).min().unwrap_or(0); + let first_col = cells.iter().map(|cell| cell.col).min().unwrap_or(0); for cell in cells { // rowspan>1 cells are intentionally multi-line — skip them. @@ -2229,14 +2233,30 @@ fn detect_tsr_quality_issue( if cell_items.len() < 2 { continue; } - if cluster_tsr_cell_text_lines(cell_items).len() >= 2 { + if cluster_tsr_cell_text_lines(cell_items).len() < 2 { + continue; + } + if expanded_cells.is_some() { return Ok(Some(TsrQualityIssue::MultiRowInCell { expanded_cells })); } + if !is_wrapped_tsr_label_cell(cell, first_row, first_col) { + return Ok(Some(TsrQualityIssue::MultiRowInCell { + expanded_cells: None, + })); + } } Ok(None) } +fn is_wrapped_tsr_label_cell( + cell: &tables::StructuredCell, + first_row: usize, + first_col: usize, +) -> bool { + cell.is_header || cell.row == first_row || cell.col == first_col +} + /// Auto-fallback variant of [`extract_tables_with_structure_mem`]: /// runs the TSR-hybrid path, checks the resulting cells for known /// SLANet detection pathologies (phantom rows, multi-row-in-cell text), diff --git a/tests/fixtures/government_positions_women.pdf b/tests/fixtures/government_positions_women.pdf new file mode 100644 index 0000000000000000000000000000000000000000..60a79178a275147fa3b999fc2a7f762d9fbfabff GIT binary patch literal 41942 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 z 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 literal 0 HcmV?d00001 diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 7614d05..ef6d536 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2532,6 +2532,61 @@ fn test_auto_expands_under_counted_vector_grid_rows() { ); } +#[test] +fn test_auto_keeps_wrapped_header_vector_grid_doc51() { + use pdf_inspector::{extract_tables_with_structure_auto_mem, TsrTableInput}; + + let buf = std::fs::read("tests/fixtures/government_positions_women.pdf").unwrap(); + let crop = [0.0, 0.0, 612.0, 792.0]; + let grid = detect_vector_grid_in_region_mem(&buf, 0, crop, 200.0) + .unwrap() + .expect("expected doc 51 vector grid"); + assert_eq!( + grid.cell_bboxes.len(), + 36, + "doc 51 should have a 9x4 vector grid" + ); + + let results = extract_tables_with_structure_auto_mem( + &buf, + &[TsrTableInput { + page: 0, + crop_pdf_pt_bbox: crop, + render_dpi: 200.0, + structure_tokens: grid.structure_tokens, + cell_bboxes: grid.cell_bboxes, + }], + ) + .unwrap(); + + assert_eq!(results.len(), 1); + let r = &results[0]; + assert!( + r.fallback_reason.is_none(), + "wrapped header/label text should not trigger heuristic fallback: {:?}\n{}", + r.fallback_reason, + r.markdown + ); + let md = &r.markdown; + assert!(md.contains("Government Position"), "missing header: {md}"); + assert!( + md.contains("Aquino Administration"), + "missing Aquino header: {md}" + ); + assert!( + md.contains("Ramos Administration"), + "missing Ramos header: {md}" + ); + assert!( + md.contains("City Municipal Councilor"), + "row label was truncated: {md}" + ); + assert!( + !md.contains("|Position||Administration"), + "heuristic fallback split the header row: {md}" + ); +} + #[test] fn test_auto_returns_empty_inputs() { use pdf_inspector::extract_tables_with_structure_auto_mem;