Skip to content
Snippets Groups Projects
extrude.rs 3.67 KiB
Newer Older
use clap::Parser;
Louis's avatar
Louis committed

use image::math::Rect;
Louis's avatar
Louis committed
use image::{GenericImage, GenericImageView, Rgba, RgbaImage};

use serde::{Deserialize, Serialize};
use std::ops::Deref;
Louis's avatar
Louis committed
use crunch_cli::utils::{RgbaOutputFormat, SpriteData};
Louis's avatar
Louis committed

#[inline(always)]
fn tile_size() -> u32 {
	32
}
Louis's avatar
Louis committed

/// 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)]
Louis's avatar
Louis committed
	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)]
Louis's avatar
Louis committed
	space_y: u32,
	/// The amount of padding to add around the edge of the spritesheet
	#[clap(short, long, default_value_t = 0)]
	#[serde(default)]
	/// 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()
		);
Louis's avatar
Louis committed

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

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

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

				views.push(payload);
			}
Louis's avatar
Louis committed
		}

		let new_width = (self.padding * 2 + self.space_x * columns) + image.width();
		let new_height = (self.padding * 2 + self.space_y * rows) + image.height();
Louis's avatar
Louis committed

		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() {
Louis's avatar
Louis committed
			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)?;
Louis's avatar
Louis committed

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