fix(tables): Increase rect edge snap tolerance to fix table detection
The snap_edges tolerance of 2.0 was too tight for PDFs where header and body cell boundaries differ by ~3pt due to cell padding. This created phantom empty columns that caused valid tables to be rejected. Increase snap and cell coverage tolerances from 2.0/3.0 to 6.0. Also add debug logging to rect-based table detection pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
421690c7c4
commit
ceb0cf5913
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::debug;
|
||||
|
||||
use crate::types::{PdfRect, TextItem};
|
||||
|
||||
use super::Table;
|
||||
@@ -121,6 +123,13 @@ pub fn detect_tables_from_rects(items: &[TextItem], rects: &[PdfRect], page: u32
|
||||
page_rects.push((x, y, w, h));
|
||||
}
|
||||
|
||||
debug!(
|
||||
"page {}: {} rects after size filter (from {} raw)",
|
||||
page,
|
||||
page_rects.len(),
|
||||
rects.iter().filter(|r| r.page == page).count(),
|
||||
);
|
||||
|
||||
// Need a reasonable number of cell rects to form a table
|
||||
if page_rects.len() < 6 {
|
||||
return vec![];
|
||||
@@ -128,6 +137,7 @@ pub fn detect_tables_from_rects(items: &[TextItem], rects: &[PdfRect], page: u32
|
||||
|
||||
// Cluster spatially connected rects into groups
|
||||
let clusters = cluster_rects(&page_rects, 3.0, 6);
|
||||
debug!("page {}: {} clusters with >= 6 rects", page, clusters.len());
|
||||
|
||||
let mut tables = Vec::new();
|
||||
for cluster_indices in &clusters {
|
||||
@@ -160,11 +170,15 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
y_edges.push(y + h);
|
||||
}
|
||||
|
||||
let x_edges = snap_edges(&x_edges, 2.0);
|
||||
let y_edges = snap_edges(&y_edges, 2.0);
|
||||
let x_edges = snap_edges(&x_edges, 6.0);
|
||||
let y_edges = snap_edges(&y_edges, 6.0);
|
||||
|
||||
if x_edges.len() < 3 || y_edges.len() < 4 {
|
||||
// Need at least 2 columns (3 edges) and 3 rows (4 edges)
|
||||
debug!(
|
||||
" rejected: {} x-edges, {} y-edges (need >=3, >=4)",
|
||||
x_edges.len(),
|
||||
y_edges.len()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -198,7 +212,7 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
let x_right = col_edges[col + 1];
|
||||
// Check if any rect approximately covers this cell
|
||||
let cell_covered = group_rects.iter().any(|&(rx, ry, rw, rh)| {
|
||||
let tol = 3.0;
|
||||
let tol = 6.0;
|
||||
rx <= x_left + tol
|
||||
&& (rx + rw) >= x_right - tol
|
||||
&& ry <= y_top + tol
|
||||
@@ -213,8 +227,14 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
let total_cells = (num_cols * num_rows) as f32;
|
||||
let fill_ratio = filled_cells as f32 / total_cells;
|
||||
|
||||
debug!(
|
||||
" grid: {}x{} = {} cells, {} filled, ratio={:.2}",
|
||||
num_rows, num_cols, total_cells as u32, filled_cells, fill_ratio
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -231,6 +251,7 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
|
||||
// Skip if no text was assigned
|
||||
if item_indices.is_empty() {
|
||||
debug!(" rejected: no text items assigned to grid");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -240,6 +261,7 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
.filter(|row| row.iter().any(|c| !c.trim().is_empty()))
|
||||
.count();
|
||||
if non_empty_rows < 2 {
|
||||
debug!(" rejected: only {} non-empty rows", non_empty_rows);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -252,6 +274,10 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
.count();
|
||||
let content_ratio = non_empty_cells as f32 / total_cells;
|
||||
if content_ratio < 0.25 {
|
||||
debug!(
|
||||
" rejected: content ratio {:.2} < 0.25 ({} non-empty / {} total)",
|
||||
content_ratio, non_empty_cells, total_cells as u32
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -261,6 +287,7 @@ pub(crate) fn detect_table_from_rect_group(
|
||||
.iter()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user