use clap::Parser; use image::math::Rect; use image::{GenericImage, GenericImageView, Rgba, RgbaImage}; use serde::{Deserialize, Serialize}; use std::ops::Deref; use crunch_cli::utils::{RgbaOutputFormat, SpriteData}; #[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<Pixel = Rgba<u8>>, ) -> 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::from_pixel(new_width, new_height, Rgba::from([0, 0, 0, 0])); for sprite in views.iter() { let (width, height) = sprite.data.dimensions(); let (img_x, img_y) = sprite.data.offsets(); let target_x = self.padding + img_x + (sprite.tx * self.space_x); let target_y = self.padding + img_y + (sprite.ty * self.space_y); new_image.copy_from(sprite.data.deref(), target_x, target_y)?; if self.extrude { // Left Side new_image.copy_within( Rect { x: target_x, y: target_y, width: 1, height, }, target_x.saturating_sub(1), target_y, ); // Right Side new_image.copy_within( Rect { x: target_x + width - 1, y: target_y, width: 1, height, }, target_x + width, target_y, ); // Top Side new_image.copy_within( Rect { x: target_x, y: target_y, width, height: 1, }, target_x, target_y.saturating_sub(1), ); // Bottom Side new_image.copy_within( Rect { x: target_x, y: target_y + height - 1, width, height: 1, }, target_x, target_y + height, ); } } Ok(new_image) } }