vesey.techpdftxtractreposrcmain.rs
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]
    }
}