use clap::{Parser, ValueEnum}; use pdftxtract::pdf::{self, GridConfig}; use tracing::info; #[derive(Parser)] #[command(about = "Render PDF pages as monospaced text grids")] struct Args { pdf: String, /// Page or range to extract, e.g. "3" or "2-5". Omit for all pages. pages: Option<String>, /// Override grid dimensions, e.g. "200x80" (colsxrows). Omit to autodetect. #[arg(long)] size: Option<String>, /// How to resolve two text items that snap to the same grid cell. #[arg(long, value_enum, default_value_t = CollisionArg::Walk)] collision: CollisionArg, /// Whitespace trimming mode. #[arg(long, value_enum, default_value_t = TrimArg::Collapse)] trim: TrimArg, } #[derive(Clone, ValueEnum)] enum CollisionArg { /// First item placed wins; later items skip occupied cells. First, /// Later item overwrites whatever is already in the cell. Last, /// Later item advances past occupied cells to the next empty one. Walk, } #[derive(Clone, ValueEnum)] enum TrimArg { /// No trimming — print the full grid. None, /// Remove leading/trailing blank rows and columns. Bounds, /// Bounds trim plus collapse internal runs of blank rows/columns to one. Collapse, } fn main() { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .with_writer(std::io::stderr) .init(); let args = Args::parse(); let data = std::fs::read(&args.pdf).expect("Failed to read PDF"); let total = { let file = ::pdf::file::FileOptions::cached() .load(&data[..]) .expect("Failed to load PDF"); file.pages().count() as u32 }; let page_nums = match args.pages.as_deref() { Some(spec) => parse_page_spec(spec), None => (1..=total).collect(), }; let (cols, rows) = match args.size.as_deref() { Some(s) => parse_size(s), None => (None, None), }; info!("Pages: {} (extracting {})", total, page_nums.len()); let collision = match args.collision { CollisionArg::First => pdf::Collision::First, CollisionArg::Last => pdf::Collision::Last, CollisionArg::Walk => pdf::Collision::Walk, }; let trim = match args.trim { TrimArg::None => pdf::Trim::None, TrimArg::Bounds => pdf::Trim::Bounds, TrimArg::Collapse => pdf::Trim::Collapse, }; let pages = pdf::run_backend(data, &page_nums, GridConfig { cols, rows, collision, trim }) .expect("Failed to render PDF"); for page in &pages { for line in &page.lines { println!("{}", line); } } } fn parse_size(s: &str) -> (Option<usize>, Option<usize>) { let (cols_str, rows_str) = s.split_once('x').expect("--size must be WxH, e.g. 200x80"); let cols = if cols_str.is_empty() { None } else { Some(cols_str.trim().parse().expect("Invalid column count")) }; let rows = if rows_str.is_empty() { None } else { Some(rows_str.trim().parse().expect("Invalid row count")) }; (cols, rows) } fn parse_page_spec(spec: &str) -> Vec<u32> { if let Some((start, end)) = spec.split_once('-') { let a: u32 = start.trim().parse().expect("Invalid range start"); let b: u32 = end.trim().parse().expect("Invalid range end"); (a..=b).collect() } else { let n: u32 = spec.trim().parse().expect("Invalid page number"); vec![n] } }