Compare commits

...
Author SHA1 Message Date
Abimael Martell 66103742c3 tables: keep wrapped labels in TSR output 2026-04-30 10:25:38 -07:00
4 changed files with 82 additions and 7 deletions
+1 -1
View File
@@ -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",
+26 -6
View File
@@ -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),
Binary file not shown.
+55
View File
@@ -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;