Skip to content
Snippets Groups Projects
Verified Commit e1c74a5b authored by Louis's avatar Louis :fire:
Browse files

Implement split command for subdividing spritesheets

parent 0f29f972
No related branches found
No related tags found
No related merge requests found
...@@ -2,7 +2,7 @@ use clap::Parser; ...@@ -2,7 +2,7 @@ use clap::Parser;
use serde::{Deserialize, Serialize}; 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::{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; use crate::load_image;
...@@ -39,6 +39,9 @@ pub enum Args { ...@@ -39,6 +39,9 @@ pub enum Args {
#[clap(name = "reduce")] #[clap(name = "reduce")]
#[serde(alias = "reduce")] #[serde(alias = "reduce")]
Reduce(Reduce), Reduce(Reduce),
#[clap(name = "split")]
#[serde(alias = "split")]
Split(Split),
} }
impl Args { impl Args {
...@@ -97,6 +100,10 @@ impl Args { ...@@ -97,6 +100,10 @@ impl Args {
Ok(()) Ok(())
} }
Args::Split(split) => {
let image = load_image(&split.input, None)?;
split.run(&image)
}
Args::Pipeline(pipeline) => pipeline.execute(), Args::Pipeline(pipeline) => pipeline.execute(),
} }
} }
......
...@@ -2,16 +2,18 @@ mod extrude; ...@@ -2,16 +2,18 @@ mod extrude;
mod flip; mod flip;
mod palette; mod palette;
mod pipeline; mod pipeline;
mod reduce;
mod remap; mod remap;
mod rotate; mod rotate;
mod scale; mod scale;
mod reduce; mod split;
pub use extrude::Extrude; pub use extrude::Extrude;
pub use flip::Flip; pub use flip::Flip;
pub use palette::Palette; pub use palette::Palette;
pub use pipeline::Pipeline; pub use pipeline::Pipeline;
pub use reduce::Reduce;
pub use remap::Remap; pub use remap::Remap;
pub use rotate::Rotate; pub use rotate::Rotate;
pub use scale::Scale; pub use scale::Scale;
pub use reduce::Reduce; pub use split::Split;
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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment