lmocr 0.1.1

Convert PDFs and images to Markdown using OpenRouter multimodal LLMs
use anyhow::{Context, Result};
use regex::Regex;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;

pub fn page_count(path: &Path) -> Result<usize> {
    let output = Command::new("pdfinfo")
        .arg(path)
        .output()
        .map_err(|err| {
            if err.kind() == io::ErrorKind::NotFound {
                anyhow::anyhow!(
                    "pdfinfo not found. Please install Poppler (macOS: brew install poppler)."
                )
            } else {
                err.into()
            }
        })
        .with_context(|| "Failed to run pdfinfo")?;
    if !output.status.success() {
        anyhow::bail!(
            "pdfinfo failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    let re = Regex::new(r"(?m)^Pages:\s+(\d+)\s*$")?;
    let caps = re
        .captures(&stdout)
        .context("Unable to parse page count from pdfinfo output")?;
    let pages: usize = caps
        .get(1)
        .context("Missing page count capture")?
        .as_str()
        .parse()?;
    Ok(pages)
}

pub fn render_page_to_png(
    pdf: &Path,
    page: usize,
    dpi: u32,
    temp_dir: &TempDir,
) -> Result<PathBuf> {
    let prefix = temp_dir.path().join(format!("page_{page}"));
    let status = Command::new("pdftoppm")
        .arg("-r")
        .arg(dpi.to_string())
        .arg("-f")
        .arg(page.to_string())
        .arg("-l")
        .arg(page.to_string())
        .arg("-png")
        .arg("-singlefile")
        .arg(pdf)
        .arg(&prefix)
        .status()
        .map_err(|err| {
            if err.kind() == io::ErrorKind::NotFound {
                anyhow::anyhow!(
                    "pdftoppm not found. Please install Poppler (macOS: brew install poppler)."
                )
            } else {
                err.into()
            }
        })
        .with_context(|| "Failed to run pdftoppm")?;
    if !status.success() {
        anyhow::bail!("pdftoppm failed for page {}", page);
    }
    let image_path = prefix.with_extension("png");
    if !image_path.exists() {
        anyhow::bail!("pdftoppm did not produce {}", image_path.display());
    }
    Ok(image_path)
}

pub fn extract_page_text(pdf: &Path, page: usize) -> Result<Option<String>> {
    let output = Command::new("pdftotext")
        .arg("-f")
        .arg(page.to_string())
        .arg("-l")
        .arg(page.to_string())
        .arg("-layout")
        .arg(pdf)
        .arg("-")
        .output()
        .map_err(|err| {
            if err.kind() == io::ErrorKind::NotFound {
                anyhow::anyhow!(
                    "pdftotext not found. Please install Poppler (macOS: brew install poppler)."
                )
            } else {
                err.into()
            }
        })
        .with_context(|| "Failed to run pdftotext")?;
    if !output.status.success() {
        anyhow::bail!(
            "pdftotext failed for page {}: {}",
            page,
            String::from_utf8_lossy(&output.stderr)
        );
    }
    let text = String::from_utf8_lossy(&output.stdout).to_string();
    if text.trim().is_empty() {
        return Ok(None);
    }
    Ok(Some(text))
}