Compare commits

...
Author SHA1 Message Date
Abimael Martell acebe6f7fa Bump version from 1.8.1 to 1.8.2 2026-04-29 17:22:34 -07:00
Abimael Martell c248eeff8f tables: prefer rect edges in cell-grid fallback 2026-04-29 17:22:34 -07:00
4 changed files with 100 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@firecrawl/pdf-inspector",
"version": "1.8.1",
"version": "1.8.2",
"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",
+40
View File
@@ -995,6 +995,46 @@ fn crop_px_bbox_is_plausible(
mod vector_grid_tests {
use super::crop_px_bbox_is_plausible;
/// Regression for `forecast_table_chart.pdf` (doc 128 from the
/// opendataloader-bench corpus). The table has six visual columns, but
/// text X-clustering in the cell-rect fallback previously split wide
/// columns into ten spurious columns.
#[test]
fn forecast_table_chart_six_cols() {
use crate::extractor::content_stream::extract_page_text_items;
use crate::tables::detect_tables_from_rects;
use crate::tounicode::FontCMaps;
use lopdf::Document;
use std::collections::HashSet;
use std::fs;
let path = "tests/fixtures/forecast_table_chart.pdf";
let buf = fs::read(path).unwrap();
let doc = Document::load_mem(&buf).unwrap();
let pages = doc.get_pages();
let &page_id = pages.get(&1).unwrap();
let needed: HashSet<u32> = HashSet::from([1]);
let cmaps = FontCMaps::from_doc_pages_fast(&doc, Some(&needed));
let ((items, rects, _lines), _has_gid, _rotated) =
extract_page_text_items(&doc, page_id, 1, &cmaps, false).unwrap();
let (rect_tables, _) = detect_tables_from_rects(&items, &rects, 1);
assert_eq!(rect_tables.len(), 1, "expected one rect-detected table");
let t = &rect_tables[0];
assert_eq!(
t.columns.len(),
6,
"doc 128 has a 6-column table; got {} edges: {:?}",
t.columns.len(),
t.columns
);
assert!(
t.rows.len() >= 14 && t.rows.len() <= 17,
"row count drift: {}",
t.rows.len()
);
}
#[test]
fn test_crop_px_bbox_is_plausible_bounds() {
let crop = [10.0, 20.0, 110.0, 220.0];
+59 -15
View File
@@ -1587,25 +1587,69 @@ fn detect_row_stripe_table_from_cell_rects(
return None;
}
// Derive columns from text X-position clustering
// Derive columns from text X-position clustering, but prefer rect
// X-edges when they already provide a tighter scaffold. Some PDFs draw
// only the row-index cells in the body plus a full header row; that is
// not dense enough for `try_build_grid`, but the header rects still define
// the real columns. Text starts inside wide cells can otherwise split the
// table into spurious sub-columns.
let columns = cluster_x_positions(&page_items, 15.0);
if columns.len() < 2 {
let text_col_edges = if columns.len() >= 2 {
let mut edges: Vec<f32> = Vec::with_capacity(columns.len() + 1);
let min_x = page_items.iter().map(|(_, i)| i.x).reduce(f32::min)?;
edges.push(min_x - 5.0);
for pair in columns.windows(2) {
edges.push((pair[0] + pair[1]) / 2.0);
}
let max_x_right = page_items
.iter()
.map(|(_, i)| i.x + i.width)
.reduce(f32::max)?;
edges.push(max_x_right + 5.0);
Some(edges)
} else {
None
};
let rect_col_edges = {
let mut x_vals = Vec::with_capacity(content_rects.len() * 2);
for &&(x, _, w, _) in &content_rects {
x_vals.push(x);
x_vals.push(x + w);
}
let mut edges = snap_edges(&x_vals, 6.0);
edges.sort_by(|a, b| a.total_cmp(b));
if (3..=26).contains(&edges.len()) {
Some(edges)
} else {
None
}
};
let col_edges = match (rect_col_edges, text_col_edges) {
(Some(rect_edges), Some(text_edges)) if rect_edges.len() <= text_edges.len() => {
debug!(
" cell-rect using {} rect-derived columns over {} text clusters",
rect_edges.len() - 1,
text_edges.len() - 1
);
rect_edges
}
(_, Some(text_edges)) => text_edges,
(Some(rect_edges), None) => rect_edges,
(None, None) => {
debug!(
" cell-rect rejected: only {} columns from text clustering",
columns.len()
);
return None;
}
};
if col_edges.len() < 3 {
return None;
}
// Build column edges
let mut col_edges: Vec<f32> = Vec::with_capacity(columns.len() + 1);
let min_x = page_items.iter().map(|(_, i)| i.x).reduce(f32::min)?;
col_edges.push(min_x - 5.0);
for pair in columns.windows(2) {
col_edges.push((pair[0] + pair[1]) / 2.0);
}
let max_x_right = page_items
.iter()
.map(|(_, i)| i.x + i.width)
.reduce(f32::max)?;
col_edges.push(max_x_right + 5.0);
let num_cols = col_edges.len() - 1;
let num_rows = row_edges.len() - 1;
Binary file not shown.