chore: Add missing debug_ygaps.rs binary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
906051b832
commit
f94be6093c
@@ -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!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user