use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use sprite_slicer::{
AnchorX, AnchorY, ComponentMode, DetectOptions, FrameAlign, GifOptions, GroupOptions,
NormalizeOptions, ProcessSheetOptions, RemoveBgOptions, SliceOptions, detect_frames,
export_gif, group_actions, normalize_frames, process_sprite_sheet, 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),
Process(ProcessArgs),
}
#[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,
}
#[derive(Args, Debug)]
struct ProcessArgs {
#[arg(long)]
input: PathBuf,
#[arg(long)]
output_dir: PathBuf,
#[arg(long)]
rows: u32,
#[arg(long)]
cols: u32,
#[arg(long)]
cell_size: u32,
#[arg(long, default_value = "#FF00FF")]
bg_hex: String,
#[arg(long, default_value_t = 100)]
threshold: u8,
#[arg(long, default_value_t = 150)]
edge_threshold: u8,
#[arg(long, default_value_t = 0.85)]
fit_scale: f32,
#[arg(long, default_value_t = 4)]
trim_border: u32,
#[arg(long, default_value_t = 3)]
edge_clean_depth: u32,
#[arg(long, default_value = "center")]
align: String,
#[arg(long, default_value_t = true)]
shared_scale: bool,
#[arg(long, default_value = "all")]
component_mode: String,
#[arg(long, default_value_t = 0)]
component_padding: u32,
#[arg(long, default_value_t = 1)]
min_component_area: u32,
#[arg(long, default_value_t = 0)]
edge_touch_margin: u32,
#[arg(long, default_value_t = false)]
reject_edge_touch: bool,
#[arg(long, default_value_t = 20)]
gif_delay: u16,
#[arg(long)]
prompt: Option<String>,
#[arg(long, value_delimiter = ',')]
frame_labels: Option<Vec<String>>,
}
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);
}
Commands::Process(args) => {
let output = process_sprite_sheet(ProcessSheetOptions {
input: args.input,
output_dir: args.output_dir,
rows: args.rows,
cols: args.cols,
cell_size: args.cell_size,
bg_hex: args.bg_hex,
threshold: args.threshold,
edge_threshold: args.edge_threshold,
fit_scale: args.fit_scale,
trim_border: args.trim_border,
edge_clean_depth: args.edge_clean_depth,
align: parse_frame_align(&args.align)?,
shared_scale: args.shared_scale,
component_mode: parse_component_mode(&args.component_mode)?,
component_padding: args.component_padding,
min_component_area: args.min_component_area,
edge_touch_margin: args.edge_touch_margin,
reject_edge_touch: args.reject_edge_touch,
gif_delay: args.gif_delay,
frame_labels: args.frame_labels,
prompt: args.prompt,
})?;
println!("output dir: {}", output.output_dir.display());
println!("sheet: {}", output.sheet_path.display());
println!("gif: {}", output.gif_path.display());
println!("metadata: {}", output.metadata_path.display());
println!("frames: {}", output.frame_count);
println!("edge-touch frames: {:?}", output.edge_touch_frames);
}
}
Ok(())
}
fn parse_frame_align(input: &str) -> Result<FrameAlign> {
match input {
"center" => Ok(FrameAlign::Center),
"bottom" => Ok(FrameAlign::Bottom),
"feet" => Ok(FrameAlign::Feet),
_ => Err(anyhow::anyhow!(
"invalid align: {input}; use center|bottom|feet"
)),
}
}
fn parse_component_mode(input: &str) -> Result<ComponentMode> {
match input {
"all" => Ok(ComponentMode::All),
"largest" => Ok(ComponentMode::Largest),
_ => Err(anyhow::anyhow!(
"invalid component-mode: {input}; use all|largest"
)),
}
}