use anyhow::{Context, Result};
use clap::{Args, ValueEnum};
use image::{DynamicImage, Rgba, RgbaImage};
use pixa::split::{self, PreviewStyle, SplitOptions};
use pixa::transparent;
use std::path::PathBuf;
use super::style::{arrow, cyan, dim, fail_mark, green, ok_mark, red};
use super::{ensure_parent, format_size};
#[derive(Args)]
pub struct SplitArgs {
pub input: PathBuf,
#[arg(short, long)]
pub output: PathBuf,
#[arg(long, value_delimiter = ',')]
pub names: Vec<String>,
#[arg(long, default_value = "0")]
pub padding: u32,
#[arg(long)]
pub preview: bool,
#[arg(long, value_enum, default_value = "output")]
pub preview_style: PreviewStyleArg,
#[arg(long)]
pub transparent: bool,
#[arg(long, default_value = "200", requires = "transparent")]
pub tolerance: f64,
#[arg(long, requires = "transparent")]
pub despill: bool,
#[arg(long, default_value = "3", requires = "despill")]
pub despill_band: u32,
#[arg(long, default_value = "0", requires = "transparent")]
pub shrink: u32,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum PreviewStyleArg {
Detected,
Output,
Both,
}
impl From<PreviewStyleArg> for PreviewStyle {
fn from(a: PreviewStyleArg) -> Self {
match a {
PreviewStyleArg::Detected => PreviewStyle::Detected,
PreviewStyleArg::Output => PreviewStyle::Output,
PreviewStyleArg::Both => PreviewStyle::Both,
}
}
}
pub fn run(args: SplitArgs) -> Result<()> {
let img = image::open(&args.input)
.with_context(|| format!("Failed to open: {}", args.input.display()))?;
let opts = SplitOptions {
padding: args.padding,
expected_count: if args.names.is_empty() {
None
} else {
Some(args.names.len())
},
};
let result = match split::detect_objects(&img, &opts) {
Ok(r) => r,
Err(e) => {
let preview_path = preview_path(&args.input);
if let Ok(diag) = split::detect_objects(&img, &SplitOptions::default()) {
let _ = split::write_preview(&img, &diag, PreviewStyle::Detected, &preview_path);
eprintln!("{} {}", fail_mark(), e);
eprintln!(" preview written: {}", preview_path.display());
eprintln!(" hint: try --padding or pass --names to enable re-split");
} else {
eprintln!("{} {}", fail_mark(), e);
}
std::process::exit(1);
}
};
let bg_hex = format!(
"#{:02x}{:02x}{:02x}",
result.background[0], result.background[1], result.background[2]
);
println!("{} background {}", ok_mark(), cyan(&bg_hex));
let count = result.objects.len();
if result.resplit_used {
println!(
"{} detected {} {}",
ok_mark(),
red(&format!("{count}")),
dim("(re-split to match --names)")
);
} else {
println!("{} detected {}", ok_mark(), red(&format!("{count}")));
}
println!();
let names: Vec<String> = if args.names.is_empty() {
(1..=count).map(|i| format!("{i}")).collect()
} else {
args.names.clone()
};
let (max_w, max_h) = split::max_dimensions(&result.objects);
let name_width = names.iter().map(|s| s.chars().count()).max().unwrap_or(1);
let median_w = median_width(&result.objects);
for (name, obj) in names.iter().zip(result.objects.iter()) {
let pad = " ".repeat(name_width - name.chars().count());
let coord = format!("({:>4}, {:>4})", obj.x, obj.y);
let detected = format!("{}×{}", obj.w, obj.h);
let marker = if obj.w as f64 > median_w * 1.15 {
dim("(wider)")
} else if (obj.w as f64) < median_w * 0.85 {
dim("(narrower)")
} else {
String::new()
};
println!(
" {name_col}{pad} {coord_col} detected {:>9} {}",
red(&detected),
marker,
name_col = green(name),
coord_col = dim(&coord),
);
}
println!(
"\n{} all outputs padded to {}",
dim("output size:"),
red(&format!("{max_w}×{max_h}"))
);
println!();
std::fs::create_dir_all(&args.output)
.with_context(|| format!("Failed to create output dir: {}", args.output.display()))?;
let mut total_size = 0u64;
let mut saved_paths = Vec::new();
for (name, obj) in names.iter().zip(result.objects.iter()) {
let cropped = if args.transparent {
crop_padded_transparent(
&img,
obj,
max_w,
max_h,
result.background,
args.tolerance,
args.despill,
args.despill_band,
args.shrink,
)
} else {
split::crop_padded(&img, obj, max_w, max_h, result.background)
};
let path = args.output.join(format!("{name}.png"));
ensure_parent(&path)?;
cropped
.save(&path)
.with_context(|| format!("Failed to save: {}", path.display()))?;
if let Ok(meta) = std::fs::metadata(&path) {
total_size += meta.len();
}
saved_paths.push(path);
}
println!(
"saved to {} {}",
green(&args.output.display().to_string()),
dim(&format!(
"({} files, {})",
saved_paths.len(),
format_size(total_size)
)),
);
for p in &saved_paths {
println!(" {} {}", ok_mark(), green(&p.display().to_string()));
}
if args.preview {
let preview = preview_path(&args.input);
split::write_preview(&img, &result, args.preview_style.into(), &preview)
.with_context(|| format!("Failed to write preview: {}", preview.display()))?;
println!("\npreview {} {}", arrow(), preview.display());
}
Ok(())
}
fn preview_path(input: &std::path::Path) -> PathBuf {
let parent = input.parent().unwrap_or(std::path::Path::new("."));
let stem = input
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "image".to_string());
parent.join(format!("{stem}-preview.png"))
}
#[allow(clippy::too_many_arguments)]
fn crop_padded_transparent(
img: &DynamicImage,
obj: &split::DetectedObject,
target_w: u32,
target_h: u32,
background: [u8; 3],
tolerance: f64,
despill: bool,
despill_band: u32,
shrink: u32,
) -> DynamicImage {
let mut cropped = img.crop_imm(obj.x, obj.y, obj.w, obj.h).to_rgba8();
transparent::apply_transparency_to_rgba(
&mut cropped,
background,
tolerance,
despill,
despill_band,
shrink,
);
let tw = target_w.max(obj.w);
let th = target_h.max(obj.h);
let mut canvas = RgbaImage::from_pixel(tw, th, Rgba([0, 0, 0, 0]));
let off_x = (tw - obj.w) / 2;
let off_y = (th - obj.h) / 2;
image::imageops::overlay(&mut canvas, &cropped, off_x as i64, off_y as i64);
DynamicImage::ImageRgba8(canvas)
}
fn median_width(objs: &[split::DetectedObject]) -> f64 {
let mut widths: Vec<u32> = objs.iter().map(|o| o.w).collect();
widths.sort_unstable();
if widths.is_empty() {
0.0
} else {
widths[widths.len() / 2] as f64
}
}