diff --git a/src/tables/detect_rects.rs b/src/tables/detect_rects.rs
index 348c6e8..6e31175 100644
--- a/src/tables/detect_rects.rs
+++ b/src/tables/detect_rects.rs
@@ -367,15 +367,20 @@ pub(crate) fn detect_table_from_rect_group(
) -> Option
{
// First, try normal detection with all rects.
let no_skip: Vec = vec![false; group_rects.len()];
- if let Some(table) = try_build_grid(items, group_rects, page, &no_skip, false) {
- return Some(table);
+ match try_build_grid(items, group_rects, page, &no_skip, false) {
+ GridResult::Ok(table) => return Some(table),
+ GridResult::FewNonEmptyRows => {
+ // propagate_merged_cells likely collapsed text into row 0
+ // due to a full-page background rect — retry below.
+ }
+ GridResult::Failed => return None,
}
- // If normal detection failed, check if the group contains page-origin
- // background rects (starting near (0,0), spanning nearly the full group).
- // If so, retry with those rects excluded from X-edge extraction and
- // propagate_merged_cells. This handles PDFs where a full-page background
- // fill adds spurious margin columns and collapses all rows.
+ // Check if the group contains page-origin background rects (starting
+ // near (0,0), spanning nearly the full group). If so, retry with those
+ // rects excluded from X-edge extraction and propagate_merged_cells.
+ // This handles PDFs where a full-page background fill adds spurious
+ // margin columns and collapses all rows.
let origin_tol = 5.0;
let group_x_min = group_rects
.iter()
@@ -417,7 +422,7 @@ pub(crate) fn detect_table_from_rect_group(
if is_page_bg.iter().any(|&b| b) && y_edge_count >= 12 {
debug!(" retrying without page-background rects");
- if let Some(table) = try_build_grid(items, group_rects, page, &is_page_bg, true) {
+ if let GridResult::Ok(table) = try_build_grid(items, group_rects, page, &is_page_bg, true) {
return Some(table);
}
}
@@ -425,6 +430,17 @@ pub(crate) fn detect_table_from_rect_group(
None
}
+/// Result from `try_build_grid` — distinguishes "few non-empty rows"
+/// (fixable by excluding page-background rects) from other failures.
+enum GridResult {
+ Ok(Table),
+ /// Grid was structurally valid but too few rows had content —
+ /// likely caused by `propagate_merged_cells` collapsing text.
+ FewNonEmptyRows,
+ /// Grid failed for structural reasons (bad dimensions, low fill, etc.)
+ Failed,
+}
+
/// Core grid-building logic. `skip_rects[i]` marks rects to exclude from
/// X-edge extraction and propagate_merged_cells (but they're still used for
/// fill-ratio checking). When `strict` is true, apply higher thresholds
@@ -435,7 +451,7 @@ fn try_build_grid(
page: u32,
skip_rects: &[bool],
strict: bool,
-) -> Option {
+) -> GridResult {
// Extract unique X and Y edges from all rects.
// Skip X edges from marked rects (page backgrounds add page-boundary
// edges that create empty margin columns).
@@ -467,7 +483,7 @@ fn try_build_grid(
x_edges.len(),
y_edges.len()
);
- return None;
+ return GridResult::Failed;
}
// Sort column edges left-to-right, row edges top-to-bottom (highest Y first for PDF)
@@ -480,7 +496,7 @@ fn try_build_grid(
let num_rows = row_edges.len() - 1;
if num_cols < 2 || num_rows < 2 {
- return None;
+ return GridResult::Failed;
}
// Reject grids that are too large — form-style PDFs with scattered field
@@ -488,7 +504,7 @@ fn try_build_grid(
// chi-square) can legitimately have 20+ columns, so allow up to 25.
if num_cols > 25 {
debug!(" rejected: {} columns > 25", num_cols);
- return None;
+ return GridResult::Failed;
}
// Verify that cell-sized rects actually fill the grid
@@ -525,7 +541,7 @@ fn try_build_grid(
// Require at least 30% of cells to be backed by rects
if fill_ratio < 0.3 {
debug!(" rejected: fill ratio {:.2} < 0.30", fill_ratio);
- return None;
+ return GridResult::Failed;
}
// Build table: assign text items to cells
@@ -551,7 +567,7 @@ fn try_build_grid(
// Skip if no text was assigned
if item_indices.is_empty() {
debug!(" rejected: no text items assigned to grid");
- return None;
+ return GridResult::Failed;
}
// Skip tables with too few rows of content.
@@ -567,7 +583,7 @@ fn try_build_grid(
" rejected: only {} non-empty rows (need {})",
non_empty_rows, min_rows
);
- return None;
+ return GridResult::FewNonEmptyRows;
}
// Content density check: reject tables where most cells are empty.
@@ -584,7 +600,7 @@ fn try_build_grid(
" rejected: content ratio {:.2} < {:.2} ({} non-empty / {} total)",
content_ratio, min_content, non_empty_cells, total_cells as u32
);
- return None;
+ return GridResult::Failed;
}
// In strict mode, reject tables where any single cell has very long text —
@@ -601,7 +617,7 @@ fn try_build_grid(
" rejected: max cell length {} > 200 (likely paragraph text)",
max_cell_len
);
- return None;
+ return GridResult::Failed;
}
}
@@ -612,11 +628,11 @@ fn try_build_grid(
.any(|row| row.get(col).is_some_and(|c| !c.trim().is_empty()));
if !col_has_content {
debug!(" rejected: column {} is completely empty", col);
- return None;
+ return GridResult::Failed;
}
}
- Some(Table {
+ GridResult::Ok(Table {
columns,
rows,
cells,