diff --git a/src/cli_args.rs b/src/cli_args.rs index 2d8923f2853acde71095b88031407327fc7c2725..3ad341776e4bb9422e498c22a9bb3bbaf3b23e8d 100644 --- a/src/cli_args.rs +++ b/src/cli_args.rs @@ -2,7 +2,7 @@ use clap::Parser; use serde::{Deserialize, Serialize}; // use crate::commands::{calculate_mapping, execute_pipeline, extrude, flip, palette, remap_image, rescale, rotate, write_palette, FlipDirection, RotateDegree, Rotate}; -use crate::commands::{Extrude, Flip, Palette, Pipeline, Reduce, Remap, Rotate, Scale}; +use crate::commands::{Extrude, Flip, Palette, Pipeline, Reduce, Remap, Rotate, Scale, Split}; use crate::load_image; @@ -39,6 +39,9 @@ pub enum Args { #[clap(name = "reduce")] #[serde(alias = "reduce")] Reduce(Reduce), + #[clap(name = "split")] + #[serde(alias = "split")] + Split(Split), } impl Args { @@ -97,6 +100,10 @@ impl Args { Ok(()) } + Args::Split(split) => { + let image = load_image(&split.input, None)?; + split.run(&image) + } Args::Pipeline(pipeline) => pipeline.execute(), } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b9cf99709e2b19afb44a6198c3793d44e51acaf5..c2538e25d4e4d57d742b0db79c879eb96989837b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,16 +2,18 @@ mod extrude; mod flip; mod palette; mod pipeline; +mod reduce; mod remap; mod rotate; mod scale; -mod reduce; +mod split; pub use extrude::Extrude; pub use flip::Flip; pub use palette::Palette; pub use pipeline::Pipeline; +pub use reduce::Reduce; pub use remap::Remap; pub use rotate::Rotate; pub use scale::Scale; -pub use reduce::Reduce; +pub use split::Split; diff --git a/src/commands/split.rs b/src/commands/split.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a688a21c08ae6e0427161227d6a10813938e6e7 --- /dev/null +++ b/src/commands/split.rs @@ -0,0 +1,184 @@ +use crate::utils::{RgbaOutputFormat, SpriteData}; +use anyhow::Error; +use clap::Parser; +use image::imageops::tile; +use image::{ + save_buffer, save_buffer_with_format, GenericImage, GenericImageView, ImageBuffer, ImageFormat, + Pixel, Rgba, RgbaImage, +}; +use num_traits::cast::ToPrimitive; +use num_traits::AsPrimitive; +use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator}; +use serde::{Deserialize, Serialize}; +use std::io::Read; +use std::path::PathBuf; + +#[inline(always)] +fn tile_size() -> u32 { + 32 +} + +/// Take a spritesheet and split into individual sprites, skipping empty space +#[derive(Parser, Serialize, Deserialize, Clone, Debug)] +#[clap(author, version = "0.5.0")] +pub struct Split { + /// The path to the spritesheet file + #[serde(default)] + pub input: String, + /// The base path for all of the created sprites. Should be a directory, which will be created if it does not exist + #[serde(default)] + pub output: String, + + /// The amount of horizontal space between each sprite in the image + #[clap(short = 'x', long, default_value_t = 0)] + #[serde(default)] + space_x: u32, + /// The amount of vertical space between each sprite in the image + #[clap(short = 'y', long, default_value_t = 0)] + #[serde(default)] + space_y: u32, + /// The amount of space around the edge of the image + #[clap(short, long, default_value_t = 0)] + #[serde(default)] + padding: u32, + /// The size of each tile in the spritesheet. Assumed to be square tiles + #[clap(short, long, default_value_t = 32)] + #[serde(default = "tile_size")] + tile_size: u32, + /// When set, files will be numbered consecutively, regardless of empty sprites. By default, files + /// will be numbered based on their inferred "tile id", based on position in the sheet + #[clap(short, long, default_value_t = false)] + #[serde(default)] + sequential: bool, +} + +fn params_to_columns( + dimension: impl AsPrimitive<f32>, + tile_size: impl AsPrimitive<f32>, + padding: impl AsPrimitive<f32>, + spacing: impl AsPrimitive<f32>, +) -> u32 { + let dimension = dimension.as_(); + let tile_size = tile_size.as_(); + let padding = padding.as_(); + let spacing = spacing.as_(); + + let base = (dimension - (2.0 * padding) - spacing) / (tile_size + spacing); + base.floor() as u32 + if spacing == 0.0 { 0 } else { 1 } +} + +impl Split { + pub fn run(&self, image: &RgbaImage) -> anyhow::Result<()> { + log::info!( + "Image loaded with size {} x {}", + image.width(), + image.height() + ); + + let columns = params_to_columns(image.width(), self.tile_size, self.padding, self.space_x); + let rows = params_to_columns(image.height(), self.tile_size, self.padding, self.space_y); + log::info!("Inferred sheet contains {} columns", columns); + log::info!("Inferred sheet contains {} rows", rows); + + std::fs::create_dir_all(&self.output)?; + let path_base = PathBuf::from(&self.output); + + let mut file_count = 0; + for x in 0..columns { + for y in 0..rows { + let img_x = self.padding + (x * self.tile_size) + (x * self.space_x); + let img_y = self.padding + (y * self.tile_size) + (y * self.space_y); + // let img_y = y * self.tile_size; + + let view = image.view(img_x, img_y, self.tile_size, self.tile_size); + + if view + .pixels() + .any(|(_, _, pixel)| pixel.channels().get(3).map(|c| c > &0).unwrap_or(false)) + { + view.to_image().save_with_format( + path_base.join(format!("{}.png", file_count)), + ImageFormat::Png, + )?; + + if self.sequential { + file_count += 1; + } + } + + if !self.sequential { + file_count += 1; + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn no_pad_space() { + let columns = super::params_to_columns(160, 16, 0, 0); + let rows = super::params_to_columns(160, 16, 0, 0); + + assert_eq!(columns, 10); + assert_eq!(rows, 10); + + let columns = super::params_to_columns(512, 16, 0, 0); + let rows = super::params_to_columns(160, 16, 0, 0); + + assert_eq!(columns, 32); + assert_eq!(rows, 10); + } + + #[test] + fn some_pad() { + let columns = super::params_to_columns(168, 16, 4, 0); + let rows = super::params_to_columns(168, 16, 4, 0); + + assert_eq!(columns, 10); + assert_eq!(rows, 10); + + let columns = super::params_to_columns(520, 16, 4, 0); + let rows = super::params_to_columns(168, 16, 4, 0); + + assert_eq!(columns, 32); + assert_eq!(rows, 10); + } + #[test] + fn some_space() { + let columns = super::params_to_columns(168, 16, 0, 0); + let rows = super::params_to_columns(168, 16, 0, 0); + + assert_eq!(columns, 10); + assert_eq!(rows, 10); + + let columns = super::params_to_columns(520, 16, 0, 0); + let rows = super::params_to_columns(168, 16, 0, 0); + + assert_eq!(columns, 32); + assert_eq!(rows, 10); + } + #[test] + fn different_spacing() { + let space_x = 3; + let space_y = 2; + + let columns = super::params_to_columns(189, 13, 0, space_x); + let rows = super::params_to_columns(159, 13, 0, space_y); + + assert_eq!(columns, 12); + assert_eq!(rows, 9); + } + + #[test] + fn correct_conversion() { + let columns = super::params_to_columns(434, 32, 3, 1); + let rows = super::params_to_columns(302, 32, 6, 1); + + assert_eq!(columns, 13); + assert_eq!(rows, 9); + } +}