use std::path::PathBuf;
use clap::Parser;
use dxpdf::{RenderOptions, DEFAULT_IMAGE_DPI};
#[derive(Parser)]
#[command(
name = "dxpdf",
about = "DOCX files to PDF converter",
allow_negative_numbers = true
)]
struct Cli {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long, default_value_t = DEFAULT_IMAGE_DPI, value_parser = parse_image_dpi)]
image_dpi: f32,
}
const MIN_CLI_IMAGE_DPI: f32 = 1.0;
const MAX_CLI_IMAGE_DPI: f32 = 2400.0;
fn parse_image_dpi(s: &str) -> Result<f32, String> {
let dpi: f32 = s
.parse()
.map_err(|_| format!("`{s}` is not a valid number"))?;
if !dpi.is_finite() || !(MIN_CLI_IMAGE_DPI..=MAX_CLI_IMAGE_DPI).contains(&dpi) {
return Err(format!(
"must be between {MIN_CLI_IMAGE_DPI} and {MAX_CLI_IMAGE_DPI} DPI (got `{s}`)"
));
}
Ok(dpi)
}
fn main() -> Result<(), dxpdf::Error> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
let cli = Cli::parse();
let output = cli
.output
.unwrap_or_else(|| cli.input.with_extension("pdf"));
let options = RenderOptions::default().with_image_dpi(cli.image_dpi);
let docx_bytes = std::fs::read(&cli.input)?;
let pdf_bytes = dxpdf::convert_with_options(&docx_bytes, &options)?;
std::fs::write(&output, &pdf_bytes)?;
eprintln!("Converted {} -> {}", cli.input.display(), output.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_dpi_accepts_in_range_and_the_default() {
assert_eq!(parse_image_dpi("220").unwrap(), 220.0);
assert_eq!(parse_image_dpi("1").unwrap(), MIN_CLI_IMAGE_DPI);
assert_eq!(parse_image_dpi("2400").unwrap(), MAX_CLI_IMAGE_DPI);
assert_eq!(
parse_image_dpi(&DEFAULT_IMAGE_DPI.to_string()).unwrap(),
DEFAULT_IMAGE_DPI
);
}
#[test]
fn image_dpi_rejects_out_of_range_and_non_numeric() {
for bad in ["0", "-5", "-300", "2401", "nan", "inf", "-inf", "abc", ""] {
assert!(
parse_image_dpi(bad).is_err(),
"`{bad}` should be rejected, not silently clamped"
);
}
}
}