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(133, 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); } }