use anyhow::Result;
use clap::Args;
use pixa::compress::compress_image;
use std::path::{Path, PathBuf};
use super::style::{arrow, bold, dim, fail_mark, green, ok_mark, red, skip_mark, yellow};
use super::{collect_inputs, ensure_parent, format_size, mirror_path};
#[derive(Args)]
pub struct CompressArgs {
pub input: PathBuf,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(short, long)]
pub recursive: bool,
#[arg(long, value_name = "PIXELS")]
pub max: Option<u32>,
}
pub fn run(args: CompressArgs) -> Result<()> {
let inputs = collect_inputs(&args.input, args.recursive)?;
if inputs.is_empty() {
println!("{} No images found.", yellow("!"));
return Ok(());
}
let single_file = inputs.len() == 1 && !args.input.is_dir();
if single_file {
let out_path = args
.output
.clone()
.unwrap_or_else(|| default_file_output(&inputs[0]));
ensure_parent(&out_path)?;
process_one(&inputs[0], &out_path, args.max)?;
return Ok(());
}
let output_root = args
.output
.clone()
.unwrap_or_else(|| default_dir_output(&args.input));
let input_root = args.input.as_path();
let mut ok = 0u32;
let mut failed = 0u32;
let mut total_orig = 0u64;
let mut total_comp = 0u64;
for input in &inputs {
let out_path = mirror_path(input, input_root, Some(&output_root));
if let Err(e) = ensure_parent(&out_path) {
eprintln!("{} {}: {e}", fail_mark(), input.display());
failed += 1;
continue;
}
match compress_image(input, &out_path, args.max) {
Ok(r) => {
ok += 1;
total_orig += r.original_size;
total_comp += r.compressed_size;
print_line(input, &out_path, &r);
}
Err(e) => {
failed += 1;
eprintln!(
"{} {}: {}",
fail_mark(),
input.display(),
red(&e.to_string())
);
}
}
}
print_summary(ok, failed);
if total_orig > 0 {
let pct = (1.0 - total_comp as f64 / total_orig as f64) * 100.0;
println!(
"{} {} {} {} {}",
dim("total"),
red(&format_size(total_orig)),
arrow(),
red(&format_size(total_comp)),
red(&format!("-{pct:.1}%")),
);
}
Ok(())
}
fn process_one(input: &Path, output: &Path, max_edge: Option<u32>) -> Result<()> {
match compress_image(input, output, max_edge) {
Ok(r) => {
print_line(input, output, &r);
Ok(())
}
Err(e) => {
eprintln!(
"{} {}: {}",
fail_mark(),
input.display(),
red(&e.to_string())
);
Err(anyhow::anyhow!(e))
}
}
}
fn print_line(input: &Path, output: &Path, r: &pixa::compress::CompressResult) {
if r.kept_original {
println!(
"{} {} {} {} {} {}",
skip_mark(),
green(&input.display().to_string()),
arrow(),
output.display(),
red(&format_size(r.original_size)),
dim("(already optimal, kept original)"),
);
} else {
println!(
"{} {} {} {} {} {} {} {}",
ok_mark(),
green(&input.display().to_string()),
arrow(),
output.display(),
red(&format_size(r.original_size)),
arrow(),
red(&format_size(r.compressed_size)),
red(&format!("-{:.1}%", r.savings_percent)),
);
}
}
fn print_summary(ok: u32, failed: u32) {
let parts = [
(ok, "ok", green as fn(&str) -> String),
(failed, "failed", red as fn(&str) -> String),
];
let msg: Vec<String> = parts
.iter()
.filter(|(n, _, _)| *n > 0)
.map(|(n, label, col)| col(&format!("{n} {label}")))
.collect();
println!("\n{} {}", bold("Summary"), msg.join(", "));
}
fn default_file_output(input: &Path) -> PathBuf {
let stem = input
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "image".to_string());
let ext = input
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let name = if ext.is_empty() {
format!("{stem}.min")
} else {
format!("{stem}.min.{ext}")
};
input.with_file_name(name)
}
fn default_dir_output(input: &Path) -> PathBuf {
let name = input
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "out".to_string());
input.with_file_name(format!("{name}.min"))
}