-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: files * feat: trim sprite * feat: up version * feat: update formats * chore: refactoring and updating dependencies * feat: resize image * chore: refactor * chore: refactor * feat: Resolve issues #4 and #7 * fix: ci * chore: refactor * feat: frames test
- Loading branch information
Showing
9 changed files
with
458 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
/target | ||
.env |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#[derive(Debug)] | ||
pub struct Frame { | ||
x: u32, | ||
y: u32, | ||
width: u32, | ||
height: u32, | ||
} | ||
|
||
impl Frame { | ||
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { | ||
Self { | ||
x, | ||
y, | ||
width, | ||
height, | ||
} | ||
} | ||
|
||
pub fn get_x(&self) -> u32 { | ||
self.x | ||
} | ||
|
||
pub fn get_y(&self) -> u32 { | ||
self.y | ||
} | ||
|
||
pub fn get_width(&self) -> u32 { | ||
self.width | ||
} | ||
|
||
pub fn get_height(&self) -> u32 { | ||
self.height | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,199 +1,5 @@ | ||
use image::imageops::crop_imm; | ||
use image::{DynamicImage, GenericImage, RgbaImage}; | ||
use std::error::Error; | ||
use walkdir::WalkDir; | ||
mod frame; | ||
mod sprite; | ||
mod spriterator; | ||
|
||
/// Supported file extensions for image files. | ||
const SUPPORTED_EXTENSIONS: [&str; 2] = ["png", "webp"]; | ||
|
||
/// A struct that generates sprite sheets by arranging images in a grid layout | ||
/// and trimming transparent areas from each sprite. | ||
pub struct Spriterator { | ||
dir_path: String, | ||
max_width: u32, | ||
max_height: u32, | ||
} | ||
|
||
impl Spriterator { | ||
/// Creates a new `Spriterator` instance. | ||
/// | ||
/// # Arguments | ||
/// | ||
/// * `dir_path` - Path to the directory containing images. | ||
/// * `max_width` - Maximum width for each sprite. | ||
/// * `max_height` - Maximum height for each sprite. | ||
pub fn new(dir_path: &str, max_width: u32, max_height: u32) -> Self { | ||
Self { | ||
dir_path: dir_path.to_string(), | ||
max_width, | ||
max_height, | ||
} | ||
} | ||
|
||
/// Generates a vector of sprites by arranging images in rows, respecting the specified maximum | ||
/// width and height. Each sprite is trimmed to remove transparent areas. | ||
/// | ||
/// # Returns | ||
/// | ||
/// A `Result` containing a vector of `RgbaImage` objects on success, or an error if image | ||
/// loading or processing fails. | ||
pub fn generate(&self) -> Result<Vec<RgbaImage>, Box<dyn Error>> { | ||
let images = self.get_images()?; | ||
|
||
let mut sprites = Vec::new(); | ||
let mut current_sprite = RgbaImage::new(self.max_width, self.max_height); | ||
let (mut current_x, mut current_y, mut row_height) = (0, 0, 0); | ||
|
||
for img in &images { | ||
// Move to the next row if the current image exceeds max width | ||
if current_x + img.width() > self.max_width { | ||
current_y += row_height; | ||
current_x = 0; | ||
row_height = 0; | ||
} | ||
|
||
// Start a new sprite if the current image exceeds max height | ||
if current_y + img.height() > self.max_height { | ||
let trimmed_sprite = self.trim_transparent(¤t_sprite); | ||
sprites.push(trimmed_sprite); | ||
|
||
current_sprite = RgbaImage::new(self.max_width, self.max_height); | ||
current_x = 0; | ||
current_y = 0; | ||
row_height = 0; | ||
} | ||
|
||
current_sprite.copy_from(img, current_x, current_y)?; | ||
row_height = row_height.max(img.height()); | ||
current_x += img.width(); | ||
} | ||
|
||
let trimmed_sprite = self.trim_transparent(¤t_sprite); | ||
sprites.push(trimmed_sprite); | ||
|
||
Ok(sprites) | ||
} | ||
|
||
/// Retrieves images from the specified directory that match the supported file extensions | ||
/// and checks their dimensions. | ||
/// | ||
/// # Returns | ||
/// | ||
/// A `Result` containing a vector of `DynamicImage` objects on success, or an error if | ||
/// no valid images are found or loading fails. | ||
fn get_images(&self) -> Result<Vec<DynamicImage>, Box<dyn Error>> { | ||
let images: Vec<DynamicImage> = WalkDir::new(&self.dir_path) | ||
.into_iter() | ||
.filter_map(|entry| { | ||
let path = entry.ok()?.path().to_path_buf(); | ||
|
||
let is_image = path | ||
.extension() | ||
.and_then(|ext| ext.to_str()) | ||
.map(|ext| SUPPORTED_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str())) | ||
.unwrap_or(false); | ||
|
||
if path.is_file() && is_image { | ||
let img = image::open(&path) | ||
.map_err(|e| { | ||
eprintln!("Error opening image {}: {}", path.display(), e); | ||
e | ||
}) | ||
.ok()?; | ||
|
||
if img.width() > self.max_width || img.height() > self.max_height { | ||
eprintln!( | ||
"Error: Image {} dimensions {}x{} exceed max dimensions {}x{}.", | ||
path.display(), | ||
img.width(), | ||
img.height(), | ||
self.max_width, | ||
self.max_height | ||
); | ||
return None; | ||
} | ||
|
||
Some(img) | ||
} else { | ||
None | ||
} | ||
}) | ||
.collect(); | ||
|
||
if images.is_empty() { | ||
return Err(format!( | ||
"No images with supported extensions {:?} were found in the specified directory.", | ||
SUPPORTED_EXTENSIONS | ||
) | ||
.into()); | ||
} | ||
|
||
Ok(images) | ||
} | ||
|
||
/// Trims transparent areas from the sprite by cropping to the smallest non-transparent area. | ||
/// | ||
/// # Arguments | ||
/// | ||
/// * `sprite` - The sprite image to trim. | ||
/// | ||
/// # Returns | ||
/// | ||
/// An `RgbaImage` containing the trimmed sprite. | ||
fn trim_transparent(&self, sprite: &RgbaImage) -> RgbaImage { | ||
let (mut max_x, mut max_y) = (0, 0); | ||
let mut min_x = sprite.width(); | ||
let mut min_y = sprite.height(); | ||
|
||
for (x, y, pixel) in sprite.enumerate_pixels() { | ||
if pixel[3] > 0 { | ||
max_x = max_x.max(x); | ||
max_y = max_y.max(y); | ||
min_x = min_x.min(x); | ||
min_y = min_y.min(y); | ||
} | ||
} | ||
|
||
let is_completely_transparent = max_x == 0 && max_y == 0 && sprite.get_pixel(0, 0)[3] == 0; | ||
if is_completely_transparent { | ||
return RgbaImage::new(1, 1); | ||
} | ||
|
||
crop_imm(sprite, min_x, min_y, max_x - min_x + 1, max_y - min_y + 1).to_image() | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use image::Rgba; | ||
|
||
#[test] | ||
fn test_spriterator_creation() { | ||
let spriterator = Spriterator::new("test_dir", 1024, 1024); | ||
assert_eq!(spriterator.dir_path, "test_dir"); | ||
assert_eq!(spriterator.max_width, 1024); | ||
assert_eq!(spriterator.max_height, 1024); | ||
} | ||
|
||
#[test] | ||
fn test_empty_directory_error() { | ||
let spriterator = Spriterator::new("empty_dir", 1024, 1024); | ||
let result = spriterator.generate(); | ||
assert!(result.is_err()); | ||
} | ||
|
||
#[test] | ||
fn test_trim_transparent() { | ||
let spriterator = Spriterator::new("test_dir", 1024, 1024); | ||
let mut image = RgbaImage::new(10, 10); | ||
for x in 2..8 { | ||
for y in 2..8 { | ||
image.put_pixel(x, y, Rgba([255, 0, 0, 255])); | ||
} | ||
} | ||
let trimmed = spriterator.trim_transparent(&image); | ||
assert_eq!(trimmed.width(), 6); | ||
assert_eq!(trimmed.height(), 6); | ||
} | ||
} | ||
pub use spriterator::Spriterator; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
use image::RgbaImage; | ||
|
||
use crate::frame::Frame; | ||
|
||
#[derive(Debug)] | ||
pub struct Sprite { | ||
image: RgbaImage, | ||
frames: Vec<Frame>, | ||
} | ||
|
||
impl Sprite { | ||
pub fn new(image: RgbaImage) -> Self { | ||
Self { | ||
image, | ||
frames: Vec::new(), | ||
} | ||
} | ||
|
||
pub fn get_image(&self) -> &RgbaImage { | ||
&self.image | ||
} | ||
|
||
pub fn get_frames(&self) -> &Vec<Frame> { | ||
&self.frames | ||
} | ||
|
||
pub fn add_frame(&mut self, x: u32, y: u32, width: u32, height: u32) { | ||
self.frames.push(Frame::new(x, y, width, height)); | ||
} | ||
|
||
pub fn save(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> { | ||
self.image.save(path)?; | ||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.