chore: Add missing debug_ygaps.rs binary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-12 16:02:04 -08:00
co-authored by Claude Opus 4.6
parent 906051b832
commit f94be6093c
+199
View File
@@ -0,0 +1,199 @@
//! Debug tool: Print Y positions and gaps between consecutive lines
//!
//! Usage: debug_ygaps <pdf_file> [page_number]
//!
//! Shows text lines grouped by page with Y coordinates, gaps from previous line,
//! font sizes, and whether each gap would be treated as a paragraph break.
use pdf_inspector::extract_text_with_positions;
use pdf_inspector::extractor::{group_into_lines, TextLine};
use std::env;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <pdf_file> [page_number]", args[0]);
eprintln!();
eprintln!("Prints Y positions and gaps between consecutive text lines.");
eprintln!("If page_number is given, only that page is shown.");
process::exit(1);
}
let pdf_path = &args[1];
let filter_page: Option<u32> = args.get(2).and_then(|s| s.parse().ok());
let items = match extract_text_with_positions(pdf_path) {
Ok(items) => items,
Err(e) => {
eprintln!("Error extracting text: {}", e);
process::exit(1);
}
};
if items.is_empty() {
eprintln!("No text items found in PDF.");
process::exit(0);
}
// Compute base font size (most common font size >= 9pt)
let base_size = {
let mut size_counts: std::collections::HashMap<i32, usize> =
std::collections::HashMap::new();
for item in &items {
if item.font_size >= 9.0 {
let key = (item.font_size * 10.0) as i32;
*size_counts.entry(key).or_insert(0) += 1;
}
}
size_counts
.into_iter()
.max_by_key(|&(_, count)| count)
.map(|(size_key, _)| size_key as f32 / 10.0)
.unwrap_or(10.0)
};
eprintln!("Base font size: {:.1}pt", base_size);
eprintln!(
"Paragraph break threshold: y_gap > {:.1} (base * 1.8)",
base_size * 1.8
);
eprintln!();
// Group into lines
let lines = group_into_lines(items);
// Get unique pages
let mut pages: Vec<u32> = lines.iter().map(|l| l.page).collect();
pages.sort();
pages.dedup();
for page in pages {
if let Some(fp) = filter_page {
if page != fp {
continue;
}
}
let page_lines: Vec<&TextLine> = lines.iter().filter(|l| l.page == page).collect();
println!("===== PAGE {} ({} lines) =====", page, page_lines.len());
println!(
"{:>8} {:>8} {:>8} {:>6} {:>5} {}",
"Y", "Gap", "GapRatio", "Font", "Bold", "Text (first 80 chars)"
);
println!("{}", "-".repeat(120));
let mut prev_y: Option<f32> = None;
for line in &page_lines {
let font_size = line.items.first().map(|i| i.font_size).unwrap_or(0.0);
let is_bold = line.items.first().map(|i| i.is_bold).unwrap_or(false);
let text = line.text();
let display_text: String = text.chars().take(80).collect();
let (gap_str, ratio_str, marker) = if let Some(py) = prev_y {
let gap = py - line.y;
let ratio = gap / base_size;
let is_para = gap > base_size * 1.8;
let marker = if is_para { " <<PARA>>" } else { "" };
(
format!("{:8.1}", gap),
format!("{:8.2}", ratio),
marker.to_string(),
)
} else {
(
" ---".to_string(),
" ---".to_string(),
String::new(),
)
};
println!(
"{:8.1} {} {} {:6.1} {:>5} {}{}",
line.y,
gap_str,
ratio_str,
font_size,
if is_bold { "B" } else { "" },
display_text,
marker
);
prev_y = Some(line.y);
}
println!();
// Summary statistics for this page
let mut gaps: Vec<f32> = Vec::new();
let mut prev_y: Option<f32> = None;
for line in &page_lines {
if let Some(py) = prev_y {
let gap = py - line.y;
if gap > 0.0 && gap < 200.0 {
gaps.push(gap);
}
}
prev_y = Some(line.y);
}
if !gaps.is_empty() {
gaps.sort_by(|a, b| a.partial_cmp(b).unwrap());
let min = gaps.first().unwrap();
let max = gaps.last().unwrap();
let median = gaps[gaps.len() / 2];
let mean: f32 = gaps.iter().sum::<f32>() / gaps.len() as f32;
println!(" Gap statistics for page {}:", page);
println!(" Count: {}", gaps.len());
println!(" Min: {:6.1} (ratio: {:.2})", min, min / base_size);
println!(" Max: {:6.1} (ratio: {:.2})", max, max / base_size);
println!(
" Median: {:6.1} (ratio: {:.2})",
median,
median / base_size
);
println!(" Mean: {:6.1} (ratio: {:.2})", mean, mean / base_size);
// Histogram of gap ratios
println!();
println!(" Gap ratio histogram (gap / base_size):");
let buckets: Vec<f32> = vec![
0.0,
0.5,
1.0,
1.2,
1.5,
1.8,
2.0,
2.5,
3.0,
5.0,
10.0,
f32::INFINITY,
];
for i in 0..buckets.len() - 1 {
let count = gaps
.iter()
.filter(|&&g| {
let r = g / base_size;
r >= buckets[i] && r < buckets[i + 1]
})
.count();
if count > 0 {
let label = if buckets[i + 1] == f32::INFINITY {
format!("{:4.1}+ ", buckets[i])
} else {
format!("{:4.1}-{:<4.1}", buckets[i], buckets[i + 1])
};
let bar: String = "#".repeat(count.min(60));
println!(" {} | {:3} {}", label, count, bar);
}
}
println!();
}
}
}