fix(tables): Use font-size-adaptive threshold for row boundary detection
Row merging occurred in uniform-spacing PDFs (e.g., 210603_ROOFING_BIDRESP) because the fixed 10.0pt cluster threshold matched the exact line spacing, causing adjacent rows to merge in pairs. Now derives threshold from 0.8× median font size, fixes >= comparison, and raises SmallFont max_rows to 200. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
d7b0149703
commit
17634e80f4
+53
-4
@@ -372,7 +372,7 @@ fn detect_table_in_region(items: &[(usize, &TextItem)], mode: TableDetectionMode
|
|||||||
|
|
||||||
// Validation 3: tables shouldn't have too many rows (likely misdetected text)
|
// Validation 3: tables shouldn't have too many rows (likely misdetected text)
|
||||||
let max_rows = match mode {
|
let max_rows = match mode {
|
||||||
TableDetectionMode::SmallFont => 50,
|
TableDetectionMode::SmallFont => 200,
|
||||||
TableDetectionMode::BodyFont => 100,
|
TableDetectionMode::BodyFont => 100,
|
||||||
};
|
};
|
||||||
if rows.len() > max_rows {
|
if rows.len() > max_rows {
|
||||||
@@ -753,15 +753,22 @@ fn find_row_boundaries(items: &[(usize, &TextItem)]) -> Vec<f32> {
|
|||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cluster Y positions - items within 10px are same row
|
// Cluster Y positions - items within a fraction of the median font size are same row.
|
||||||
let cluster_threshold = 10.0;
|
// Using 0.8× median font keeps the threshold between intra-row gaps (~0pt) and
|
||||||
|
// inter-row gaps (≥1× font size), preventing row merging in uniform-spaced PDFs.
|
||||||
|
let cluster_threshold = {
|
||||||
|
let mut font_sizes: Vec<f32> = items.iter().map(|(_, i)| i.font_size).collect();
|
||||||
|
font_sizes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
let median_font = font_sizes[font_sizes.len() / 2];
|
||||||
|
(median_font * 0.8).max(4.0)
|
||||||
|
};
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
let mut cluster_items: Vec<f32> = vec![y_positions[0]];
|
let mut cluster_items: Vec<f32> = vec![y_positions[0]];
|
||||||
|
|
||||||
for &y in &y_positions[1..] {
|
for &y in &y_positions[1..] {
|
||||||
let cluster_center = cluster_items.iter().sum::<f32>() / cluster_items.len() as f32;
|
let cluster_center = cluster_items.iter().sum::<f32>() / cluster_items.len() as f32;
|
||||||
|
|
||||||
if cluster_center - y > cluster_threshold {
|
if cluster_center - y >= cluster_threshold {
|
||||||
// End current cluster (note: Y is descending)
|
// End current cluster (note: Y is descending)
|
||||||
rows.push(cluster_center);
|
rows.push(cluster_center);
|
||||||
cluster_items = vec![y];
|
cluster_items = vec![y];
|
||||||
@@ -1401,4 +1408,46 @@ mod tests {
|
|||||||
tables[0].rows.len()
|
tables[0].rows.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uniform_spacing_rows_not_merged() {
|
||||||
|
// Reproduces the 210603_ROOFING_BIDRESP bug: 8pt font, 10pt line spacing.
|
||||||
|
// With the old fixed 10.0pt threshold and strict `>`, adjacent rows at exactly
|
||||||
|
// 10pt apart were merged in pairs, producing garbled output like
|
||||||
|
// "1 1SC Priority LLC" (two company names joined).
|
||||||
|
let companies = [
|
||||||
|
"SC Priority LLC",
|
||||||
|
"Craft Roofing Co",
|
||||||
|
"Alpha Roofing Inc",
|
||||||
|
"Beta Construction",
|
||||||
|
"Gamma Builders",
|
||||||
|
"Delta Roofing",
|
||||||
|
"Epsilon Contractors",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
// Header row at y=800
|
||||||
|
items.push(make_item("No.", 50.0, 800.0, 8.0));
|
||||||
|
items.push(make_item("Company", 120.0, 800.0, 8.0));
|
||||||
|
items.push(make_item("Bid Amount", 350.0, 800.0, 8.0));
|
||||||
|
|
||||||
|
// 7 data rows, each 10pt apart (exactly the old threshold)
|
||||||
|
for (i, company) in companies.iter().enumerate() {
|
||||||
|
let y = 790.0 - (i as f32 * 10.0); // 10pt uniform spacing
|
||||||
|
items.push(make_item(&format!("{}", i + 1), 50.0, y, 8.0));
|
||||||
|
items.push(make_item(company, 120.0, y, 8.0));
|
||||||
|
items.push(make_item(&format!("${},000", 100 + i * 10), 350.0, y, 8.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tables = detect_tables(&items, 12.0);
|
||||||
|
assert_eq!(tables.len(), 1, "Should detect one table");
|
||||||
|
// 1 header + 7 data = 8 rows total; must NOT merge into 4
|
||||||
|
assert_eq!(
|
||||||
|
tables[0].rows.len(),
|
||||||
|
8,
|
||||||
|
"Each company must be on its own row, got {} rows instead of 8",
|
||||||
|
tables[0].rows.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user