feat(tables): extract rects from filled m/l/h paths for row-stripe tables

PDFs like fare1011.pdf draw table row stripes and borders using
m/l/h/f* filled paths instead of `re` operators. Extract axis-aligned
rectangles from these filled subpaths as a third-priority rect source
(after `re` rects and clip-path rects).

Key changes:
- Track completed subpaths in `h` handler for f/f* rect extraction
- Update W/W* handler to read from pending_subpaths when pending_lines
  is empty (after h clears them)
- Add row-stripe fallback in detect_rects when clustering produces no
  large clusters (non-overlapping row stripes), with ≥15 rects and
  ≥10 rows required to prevent decorative fill false positives
- Revert body-font upper bound to 1.05x (no longer needed at 1.10x)

fare1011.pdf: 12-col heuristic table → 14-col rect-based table with
all fare values correctly captured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-03-05 23:27:01 -08:00
co-authored by Claude Opus 4.6
parent 5ab92e03e3
commit b4d127eae3
2 changed files with 97 additions and 2 deletions
+24
View File
@@ -261,6 +261,30 @@ pub fn detect_tables_from_rects(
}
}
}
// Row-stripe fallback: when clustering produces no large clusters
// (row stripes don't overlap so each is its own cluster of 1),
// try all page rects directly as a row-stripe table.
// Require ≥15 rects and ≥10 result rows to avoid decorative fill false positives.
if tables.is_empty() && clusters.is_empty() && page_rects.len() >= 15 {
if let Some(table) = detect_row_stripe_table(items, &page_rects, page) {
if table.rows.len() >= 10 {
debug!(
"page {}: row-stripe fallback succeeded ({} rects, {} rows)",
page,
page_rects.len(),
table.rows.len()
);
tables.push(table);
} else {
debug!(
"page {}: row-stripe fallback rejected: only {} rows",
page,
table.rows.len()
);
}
}
}
}
// On rect-sparse pages (≤ 6 rects), a few cell-border rects may define the