use crate::utils::{RgbaOutputFormat, SpriteData}; use clap::Parser; use image::{GenericImage, GenericImageView, Pixel, Rgba, RgbaImage}; use num_traits::cast::ToPrimitive; use serde::{Deserialize, Serialize}; #[inline(always)] fn tile_size() -> u32 { 32 } /// Take each tile in an image and expand its borders by a given amount. Optionally fill with /// nearby pixels instead of empty space #[derive(Parser, Serialize, Deserialize, Clone, Debug)] #[clap(author, version = "0.3.0")] pub struct Extrude { /// The path to the spritesheet file #[serde(default)] pub input: String, /// The path to write the extruded spritesheet #[serde(default)] pub output: String, /// The amount of horizontal space to add between each sprite in the image #[clap(short = 'x', long, default_value_t = 0)] #[serde(default)] space_x: u32, /// The amount of vertical space to add between each sprite in the image #[clap(short = 'y', long, default_value_t = 0)] #[serde(default)] space_y: u32, /// The amount of padding to add around the edge of the spritesheet #[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, /// Use nearby pixels for spacing #[clap(short, long)] #[serde(default)] extrude: bool, } impl Extrude { pub fn run(&self, image: &impl GenericImage) -> anyhow::Result<RgbaOutputFormat> { log::info!( "Image loaded with size {} x {}", image.width(), image.height() ); let columns = image.width() / self.tile_size; let rows = image.height() / self.tile_size; log::info!("Inferred sheet contains {} columns", columns); log::info!("Inferred sheet contains {} rows", rows); let mut views = Vec::with_capacity((columns * rows) as usize); for x in 0..columns { for y in 0..rows { let img_x = x * self.tile_size; let img_y = y * self.tile_size; let payload = SpriteData { data: image.view(img_x, img_y, self.tile_size, self.tile_size), x: img_x, y: img_y, tx: x, ty: y, width: self.tile_size, height: self.tile_size, }; views.push(payload); } } let new_width = (self.padding * 2 + self.space_x * columns) + image.width(); let new_height = (self.padding * 2 + self.space_y * rows) + image.height(); log::info!( "Using new image width {} / height {}", new_width, new_height ); let mut new_image = RgbaImage::new(new_width, new_height); let (new_image_x, new_image_y, new_image_width, new_image_height) = new_image.bounds(); for x in new_image_x..new_image_width { for y in new_image_y..new_image_height { new_image.put_pixel(x, y, Rgba::from([0, 0, 0, 0])); } } for sprite in views.iter() { let (img_x, img_y, width, height) = sprite.data.bounds(); for x in 0..width { for y in 0..height { let pix = sprite.data.get_pixel(x, y).to_rgba(); let p = Rgba::from([ pix.0[0].to_u8().unwrap(), pix.0[1].to_u8().unwrap(), pix.0[2].to_u8().unwrap(), pix.0[3].to_u8().unwrap(), ]); new_image.put_pixel( self.padding + (sprite.tx * self.space_x) + img_x + x, self.padding + (sprite.ty * self.space_y) + img_y + y, p, ); } } } Ok(new_image) } }