use anyhow::{Context, Result};
use clap::Parser;
use pdfcrop::{crop_pdf, BoundingBox, CropOptions, Margins};
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "pdfcrop",
version,
about = "Crop PDF files with automatic bounding box detection",
long_about = "Margins are calculated and removed for each page in the file.\n\
This is a Rust implementation compatible with the original pdfcrop from TeX Live."
)]
struct Args {
#[arg(value_name = "INPUT")]
input: String,
#[arg(value_name = "OUTPUT")]
output: Option<String>,
#[arg(long, value_name = "MARGINS")]
margins: Option<String>,
#[arg(long, value_name = "BBOX")]
bbox: Option<String>,
#[arg(long, value_name = "BBOX")]
bbox_odd: Option<String>,
#[arg(long, value_name = "BBOX")]
bbox_even: Option<String>,
#[arg(long, short)]
verbose: bool,
#[arg(long, short)]
debug: bool,
#[arg(long)]
clip: bool,
#[arg(long)]
shrink_to_content: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let (input_data, output_path) = if args.input == "-" {
if args.output.is_none() {
anyhow::bail!("Output file must be specified when reading from stdin");
}
let mut buffer = Vec::new();
io::stdin()
.read_to_end(&mut buffer)
.context("Failed to read from stdin")?;
(buffer, args.output.unwrap())
} else {
let input_path = PathBuf::from(&args.input);
let data = fs::read(&input_path)
.with_context(|| format!("Failed to read input file: {}", args.input))?;
let output_path = args.output.unwrap_or_else(|| {
let mut output = input_path.clone();
if let Some(stem) = input_path.file_stem() {
let mut new_name = stem.to_os_string();
new_name.push("-crop");
if let Some(ext) = input_path.extension() {
new_name.push(".");
new_name.push(ext);
}
output.set_file_name(new_name);
}
output.to_string_lossy().to_string()
});
(data, output_path)
};
if args.verbose || args.debug {
eprintln!(
"Input: {}",
if args.input == "-" {
"stdin"
} else {
&args.input
}
);
eprintln!("Output: {}", output_path);
}
let margins = if let Some(margin_str) = args.margins {
Margins::from_str(&margin_str).map_err(|e| anyhow::anyhow!("Invalid margins: {}", e))?
} else {
Margins::none()
};
if args.verbose || args.debug {
eprintln!(
"Margins: left={}, top={}, right={}, bottom={}",
margins.left, margins.top, margins.right, margins.bottom
);
}
let bbox_override = if let Some(bbox_str) = args.bbox {
Some(BoundingBox::from_str(&bbox_str).map_err(|e| anyhow::anyhow!("Invalid bbox: {}", e))?)
} else {
None
};
let bbox_odd = if let Some(bbox_str) = args.bbox_odd {
Some(
BoundingBox::from_str(&bbox_str)
.map_err(|e| anyhow::anyhow!("Invalid bbox-odd: {}", e))?,
)
} else {
None
};
let bbox_even = if let Some(bbox_str) = args.bbox_even {
Some(
BoundingBox::from_str(&bbox_str)
.map_err(|e| anyhow::anyhow!("Invalid bbox-even: {}", e))?,
)
} else {
None
};
let options = CropOptions {
margins,
bbox_override,
bbox_odd,
bbox_even,
page_bboxes: None, page_range: None, bbox_method: pdfcrop::BBoxMethod::ContentStream, verbose: args.verbose || args.debug,
clip_content: args.clip, shrink_to_content: args.shrink_to_content,
};
if args.verbose {
eprintln!("\nCropping PDF...");
}
let cropped_data = crop_pdf(&input_data, options).context("Failed to crop PDF")?;
if args.verbose {
eprintln!("\nWriting output to: {}", output_path);
}
fs::write(&output_path, cropped_data)
.with_context(|| format!("Failed to write output file: {}", output_path))?;
if args.verbose {
eprintln!("Done!");
}
Ok(())
}