fix(tables): Detect dense body-font tables and fix table-only page rendering

Raise BodyFont max_rows from 100 to 200 so tables with ~115 rows
(like the Closed Business Accounts PDF) pass validation. Add date
pattern recognition (MM/DD/YYYY, YYYY-MM-DD) to looks_like_table_data()
so date columns count toward the 30% data threshold.

Fix a bug where pages whose text is entirely consumed by tables
produced empty output — the post-loop cleanup only checked
current_page (stuck at 0), missing all table content. Now iterates
all pages with uninserted tables/images.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-14 13:33:41 -08:00
co-authored by Claude Opus 4.6
parent b2510ffe3e
commit b6907494b6
2 changed files with 44 additions and 23 deletions
+33 -22
View File
@@ -489,32 +489,43 @@ fn to_markdown_from_lines_with_tables_and_images(
in_paragraph = true;
}
// Insert any remaining tables for the last page
if let Some(tables) = page_tables.get(&current_page) {
for (idx, (_, table_md)) in tables.iter().enumerate() {
if !inserted_tables.contains(&(current_page, idx)) {
if in_paragraph {
output.push_str("\n\n");
in_paragraph = false;
// Insert any remaining tables and images for all pages
// (handles the case where all text was consumed by tables, leaving no lines to iterate)
let mut remaining_pages: Vec<u32> = page_tables
.keys()
.chain(page_images.keys())
.copied()
.collect();
remaining_pages.sort();
remaining_pages.dedup();
for page in remaining_pages {
if let Some(tables) = page_tables.get(&page) {
for (idx, (_, table_md)) in tables.iter().enumerate() {
if !inserted_tables.contains(&(page, idx)) {
if in_paragraph {
output.push_str("\n\n");
in_paragraph = false;
}
output.push('\n');
output.push_str(table_md);
output.push('\n');
inserted_tables.insert((page, idx));
}
output.push('\n');
output.push_str(table_md);
output.push('\n');
}
}
}
// Insert any remaining images for the last page
if let Some(images) = page_images.get(&current_page) {
for (idx, (_, image_md)) in images.iter().enumerate() {
if !inserted_images.contains(&(current_page, idx)) {
if in_paragraph {
output.push_str("\n\n");
in_paragraph = false;
if let Some(images) = page_images.get(&page) {
for (idx, (_, image_md)) in images.iter().enumerate() {
if !inserted_images.contains(&(page, idx)) {
if in_paragraph {
output.push_str("\n\n");
in_paragraph = false;
}
output.push('\n');
output.push_str(image_md);
output.push('\n');
inserted_images.insert((page, idx));
}
output.push('\n');
output.push_str(image_md);
output.push('\n');
}
}
}
+11 -1
View File
@@ -373,7 +373,7 @@ fn detect_table_in_region(items: &[(usize, &TextItem)], mode: TableDetectionMode
// Validation 3: tables shouldn't have too many rows (likely misdetected text)
let max_rows = match mode {
TableDetectionMode::SmallFont => 200,
TableDetectionMode::BodyFont => 100,
TableDetectionMode::BodyFont => 200,
};
if rows.len() > max_rows {
return None;
@@ -550,6 +550,16 @@ fn looks_like_table_data(s: &str) -> bool {
return true;
}
// Dates: MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, etc.
if s.len() <= 10
&& s.chars().filter(|c| c.is_ascii_digit()).count() >= 4
&& (s.contains('/') || s.contains('-'))
&& s.chars()
.all(|c| c.is_ascii_digit() || c == '/' || c == '-')
{
return true;
}
// Part numbers / model codes (alphanumeric, typically short)
// e.g., "NA555", "NE555", "LM358"
if s.len() <= 10