Skip to content
Snippets Groups Projects
split.rs 4.69 KiB
Newer Older
Louis's avatar
Louis committed


use clap::Parser;
Louis's avatar
Louis committed

Louis's avatar
Louis committed
	GenericImageView, ImageFormat,
	Pixel, RgbaImage,
Louis's avatar
Louis committed

use num_traits::AsPrimitive;
Louis's avatar
Louis committed

use serde::{Deserialize, Serialize};
Louis's avatar
Louis committed

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);
Louis's avatar
Louis committed
		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);
	}
}