feat: convert thin filled rects to lines for table border detection
Many PDFs (especially spreadsheet exports) draw table borders as thin filled rectangles (height/width < 2pt) instead of stroked paths. These were invisible to our line-based table detector since only stroke operations produced PdfLine objects. Now synthesizes PdfLine from thin rects before line-based detection, enabling table detection on border-drawn PDFs like government forms. Also relaxes the uniform row spacing rejection threshold (CV 0.05→0.02) to accept spreadsheet-exported tables with even row heights. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9c463b9c95
commit
937311ce27
+37
-1
@@ -751,7 +751,43 @@ pub(crate) fn to_markdown_from_items_with_rects_and_lines(
|
||||
|
||||
// 2. Line-based detection on unclaimed items (when rects didn't find tables)
|
||||
if rect_claimed.is_empty() {
|
||||
let line_tables = detect_tables_from_lines(band_items, band_lines, page);
|
||||
// Synthesize PdfLine objects from thin filled rects (border lines
|
||||
// drawn as narrow rectangles instead of stroked paths).
|
||||
let mut augmented_lines: Vec<crate::types::PdfLine> = band_lines.to_vec();
|
||||
for r in band_rects {
|
||||
let (mut w, mut h) = (r.width, r.height);
|
||||
let (mut x, mut y) = (r.x, r.y);
|
||||
if w < 0.0 {
|
||||
x += w;
|
||||
w = -w;
|
||||
}
|
||||
if h < 0.0 {
|
||||
y += h;
|
||||
h = -h;
|
||||
}
|
||||
if h < 2.0 && w >= 10.0 {
|
||||
// Thin horizontal rect → horizontal line
|
||||
let mid_y = y + h / 2.0;
|
||||
augmented_lines.push(crate::types::PdfLine {
|
||||
x1: x,
|
||||
y1: mid_y,
|
||||
x2: x + w,
|
||||
y2: mid_y,
|
||||
page,
|
||||
});
|
||||
} else if w < 2.0 && h >= 10.0 {
|
||||
// Thin vertical rect → vertical line
|
||||
let mid_x = x + w / 2.0;
|
||||
augmented_lines.push(crate::types::PdfLine {
|
||||
x1: mid_x,
|
||||
y1: y,
|
||||
x2: mid_x,
|
||||
y2: y + h,
|
||||
page,
|
||||
});
|
||||
}
|
||||
}
|
||||
let line_tables = detect_tables_from_lines(band_items, &augmented_lines, page);
|
||||
for table in &line_tables {
|
||||
for &idx in &table.item_indices {
|
||||
rect_claimed.insert(idx);
|
||||
|
||||
@@ -185,6 +185,11 @@ pub fn detect_tables_from_lines(items: &[TextItem], lines: &[PdfLine], page: u32
|
||||
.filter(|row| row.iter().any(|cell| !cell.is_empty()))
|
||||
.count();
|
||||
if non_empty_rows < 2 {
|
||||
log::debug!(
|
||||
"detect_lines p{}: rejected — only {} non-empty rows",
|
||||
page,
|
||||
non_empty_rows
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
@@ -199,6 +204,11 @@ pub fn detect_tables_from_lines(items: &[TextItem], lines: &[PdfLine], page: u32
|
||||
.count();
|
||||
let density = filled_cells as f32 / total_cells as f32;
|
||||
if density < 0.15 {
|
||||
log::debug!(
|
||||
"detect_lines p{}: rejected — low density {:.2}",
|
||||
page,
|
||||
density
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
@@ -243,9 +253,17 @@ pub fn detect_tables_from_lines(items: &[TextItem], lines: &[PdfLine], page: u32
|
||||
.map(|s| (s - mean_spacing).powi(2))
|
||||
.sum::<f32>()
|
||||
/ spacings.len() as f32;
|
||||
let cv = variance.sqrt() / mean_spacing; // coefficient of variation
|
||||
// CV < 0.05 means nearly identical spacing — chart grid
|
||||
if cv < 0.05 {
|
||||
let cv = variance.sqrt() / mean_spacing;
|
||||
// CV < 0.02 means nearly identical spacing — likely chart grid.
|
||||
// Spreadsheet-exported tables often have uniform rows (CV 0.03-0.05),
|
||||
// so we use a tighter threshold to avoid false negatives.
|
||||
if cv < 0.02 {
|
||||
log::debug!(
|
||||
"detect_lines p{}: rejected — uniform row spacing (cv={:.4}, mean={:.1})",
|
||||
page,
|
||||
cv,
|
||||
mean_spacing
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user