use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use sprite_slicer::{
AnchorX, AnchorY, DetectOptions, GifOptions, GroupOptions, NormalizeOptions, RemoveBgOptions,
SliceOptions, detect_frames, export_gif, group_actions, normalize_frames, remove_background,
slice_sheet,
};
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "Slice sprite sheets into frames and regroup them into animation folders."
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Slice(SliceArgs),
Detect(DetectArgs),
Group(GroupArgsCli),
Gif(GifArgs),
RemoveBg(RemoveBgArgs),
Normalize(NormalizeArgsCli),
}
#[derive(Args, Debug)]
struct SliceArgs {
#[arg(long)]
input: PathBuf,
#[arg(long)]
output: PathBuf,
#[arg(long)]
frame_width: u32,
#[arg(long)]
frame_height: u32,
#[arg(long)]
columns: Option<u32>,
#[arg(long)]
rows: Option<u32>,
#[arg(long, default_value_t = 0)]
offset_x: u32,
#[arg(long, default_value_t = 0)]
offset_y: u32,
#[arg(long, default_value_t = 0)]
gap_x: u32,
#[arg(long, default_value_t = 0)]
gap_y: u32,
#[arg(long, default_value_t = false)]
skip_empty: bool,
#[arg(long, default_value_t = 0)]
alpha_threshold: u8,
#[arg(long, default_value_t = 1)]
min_opaque_pixels: u32,
#[arg(long)]
bg_hex: Option<String>,
#[arg(long, default_value_t = 0)]
bg_threshold: u8,
#[arg(long, default_value = "frames.toml")]
manifest_name: String,
}
#[derive(Args, Debug)]
struct DetectArgs {
#[arg(long)]
input: PathBuf,
#[arg(long)]
output: PathBuf,
#[arg(long, default_value_t = 0)]
alpha_threshold: u8,
#[arg(long, default_value_t = 64)]
min_opaque_pixels: u32,
#[arg(long, default_value_t = 0)]
padding: u32,
#[arg(long, default_value_t = 24)]
row_tolerance: u32,
#[arg(long)]
bg_hex: Option<String>,
#[arg(long, default_value_t = 0)]
bg_threshold: u8,
#[arg(long, default_value = "frames.toml")]
manifest_name: String,
}
#[derive(Args, Debug)]
struct GroupArgsCli {
#[arg(long)]
manifest: PathBuf,
#[arg(long)]
config: PathBuf,
#[arg(long)]
output: PathBuf,
}
#[derive(Args, Debug)]
struct GifArgs {
#[arg(long)]
input: PathBuf,
#[arg(long)]
output: PathBuf,
#[arg(long, default_value_t = 8)]
fps: u16,
#[arg(long, default_value_t = 0)]
repeat: u16,
#[arg(long, default_value_t = 0)]
pad: u32,
}
#[derive(Args, Debug)]
struct RemoveBgArgs {
#[arg(long)]
input: PathBuf,
#[arg(long)]
output: PathBuf,
#[arg(long, default_value = "#000000")]
bg_hex: String,
#[arg(long, default_value_t = 8)]
threshold: u8,
#[arg(long, default_value_t = 0)]
alpha_threshold: u8,
}
#[derive(Args, Debug)]
struct NormalizeArgsCli {
#[arg(long)]
input: PathBuf,
#[arg(long)]
output: PathBuf,
#[arg(long)]
width: Option<u32>,
#[arg(long)]
height: Option<u32>,
#[arg(long, default_value = "center")]
anchor_x: String,
#[arg(long, default_value = "bottom")]
anchor_y: String,
#[arg(long, default_value_t = 0)]
pad: u32,
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Slice(args) => {
let output = slice_sheet(SliceOptions {
input: args.input,
output: args.output,
frame_width: args.frame_width,
frame_height: args.frame_height,
columns: args.columns,
rows: args.rows,
offset_x: args.offset_x,
offset_y: args.offset_y,
gap_x: args.gap_x,
gap_y: args.gap_y,
skip_empty: args.skip_empty,
alpha_threshold: args.alpha_threshold,
min_opaque_pixels: args.min_opaque_pixels,
bg_hex: args.bg_hex,
bg_threshold: args.bg_threshold,
manifest_name: args.manifest_name,
})?;
println!("saved manifest: {}", output.manifest_path.display());
println!("saved frame map: {}", output.index_map_path.display());
println!(
"exported {} frames ({} kept)",
output.frame_count, output.kept_frames
);
}
Commands::Detect(args) => {
let output = detect_frames(DetectOptions {
input: args.input,
output: args.output,
alpha_threshold: args.alpha_threshold,
min_opaque_pixels: args.min_opaque_pixels,
padding: args.padding,
row_tolerance: args.row_tolerance,
bg_hex: args.bg_hex,
bg_threshold: args.bg_threshold,
manifest_name: args.manifest_name,
})?;
println!("saved manifest: {}", output.manifest_path.display());
println!("saved frame map: {}", output.index_map_path.display());
println!(
"detected {} frames in {} rows",
output.detected_frames, output.rows
);
}
Commands::Group(args) => {
let output = group_actions(GroupOptions {
manifest: args.manifest,
config: args.config,
output: args.output,
})?;
println!("saved grouped actions: {}", output.manifest_path.display());
for action in output.actions {
println!("action {}: {} frames", action.name, action.frame_count);
}
}
Commands::Gif(args) => {
let output = export_gif(GifOptions {
input: args.input,
output: args.output,
fps: args.fps,
repeat: args.repeat,
pad: args.pad,
})?;
println!("saved gif: {}", output.output_path.display());
println!("frames: {}", output.frame_count);
println!("canvas: {}x{}", output.canvas_width, output.canvas_height);
println!("fps: {}", output.fps);
}
Commands::RemoveBg(args) => {
let output = remove_background(RemoveBgOptions {
input: args.input,
output: args.output,
bg_hex: args.bg_hex,
threshold: args.threshold,
alpha_threshold: args.alpha_threshold,
})?;
println!("saved transparent png: {}", output.output_path.display());
println!("removed background pixels: {}", output.removed_pixels);
}
Commands::Normalize(args) => {
let output = normalize_frames(NormalizeOptions {
input: args.input,
output: args.output,
width: args.width,
height: args.height,
anchor_x: args
.anchor_x
.parse::<AnchorX>()
.map_err(|error| anyhow::anyhow!(error))?,
anchor_y: args
.anchor_y
.parse::<AnchorY>()
.map_err(|error| anyhow::anyhow!(error))?,
pad: args.pad,
})?;
println!("saved normalized frames: {}", output.output_dir.display());
println!("canvas: {}x{}", output.canvas_width, output.canvas_height);
println!("frames: {}", output.frame_count);
println!("anchor: {} {}", output.anchor_x, output.anchor_y);
}
}
Ok(())
}