spritesheet_detector/
lib.rs1use image::{DynamicImage, GenericImageView};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct SpritesheetInfo {
6 pub sprite_width: u32,
8 pub sprite_height: u32,
10 pub columns: u32,
12 pub rows: u32,
14 pub frame_count: u32,
16}
17
18pub fn analyze_spritesheet(img: &DynamicImage, gap_threshold: Option<u32>) -> SpritesheetInfo {
36 let (width, height) = img.dimensions();
37
38 if width % height == 0 && width != height {
41 let frames = width / height;
42 return SpritesheetInfo {
43 sprite_width: height,
44 sprite_height: height,
45 columns: frames,
46 rows: 1,
47 frame_count: frames,
48 };
49 }
50
51 let margin_color = img.get_pixel(0, 0);
53
54 let mut vertical_boundaries: Vec<u32> = (0..width)
56 .filter(|&x| (0..height).all(|y| img.get_pixel(x, y) == margin_color))
57 .collect();
58 if vertical_boundaries.first() != Some(&0) {
59 vertical_boundaries.insert(0, 0);
60 }
61 if vertical_boundaries.last() != Some(&(width - 1)) {
62 vertical_boundaries.push(width - 1);
63 }
64
65 let mut horizontal_boundaries: Vec<u32> = (0..height)
67 .filter(|&y| (0..width).all(|x| img.get_pixel(x, y) == margin_color))
68 .collect();
69 if horizontal_boundaries.first() != Some(&0) {
70 horizontal_boundaries.insert(0, 0);
71 }
72 if horizontal_boundaries.last() != Some(&(height - 1)) {
73 horizontal_boundaries.push(height - 1);
74 }
75
76 let gap_threshold = gap_threshold.unwrap_or(40);
78 let columns = std::cmp::max(
79 vertical_boundaries
80 .windows(2)
81 .filter(|w| w[1] > w[0] + gap_threshold)
82 .count(),
83 1,
84 ) as u32;
85 let rows = std::cmp::max(
86 horizontal_boundaries
87 .windows(2)
88 .filter(|w| w[1] > w[0] + gap_threshold)
89 .count(),
90 1,
91 ) as u32;
92
93 let sprite_width = width / columns;
95 let sprite_height = height / rows;
96
97 let frame_count = (0..rows)
99 .flat_map(|row_idx| {
100 (0..columns).filter(move |&col_idx| {
101 let x_start = col_idx * sprite_width;
102 let y_start = row_idx * sprite_height;
103 let x_end = x_start + sprite_width;
104 let y_end = y_start + sprite_height;
105 !(y_start..y_end)
106 .all(|y| (x_start..x_end).all(|x| img.get_pixel(x, y) == margin_color))
107 })
108 })
109 .count() as u32;
110
111 SpritesheetInfo {
112 sprite_width,
113 sprite_height,
114 columns,
115 rows,
116 frame_count,
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use image::RgbaImage;
124
125 #[test]
126 fn test_square_frames() {
127 let mut img = RgbaImage::new(200, 50);
129 for pixel in img.pixels_mut() {
131 *pixel = image::Rgba([255, 255, 255, 255]);
132 }
133 let dyn_img = DynamicImage::ImageRgba8(img);
134 let info = analyze_spritesheet(&dyn_img, None);
135 assert_eq!(info.sprite_width, 50);
136 assert_eq!(info.sprite_height, 50);
137 assert_eq!(info.columns, 4);
138 assert_eq!(info.rows, 1);
139 assert_eq!(info.frame_count, 4);
140 }
141 #[test]
142 fn test_asset_example() {
143 let img = image::open("assets/example.png").unwrap();
144 let info = analyze_spritesheet(&img, None);
145 assert_eq!(info.sprite_width, 193);
146 assert_eq!(info.sprite_height, 155);
147 assert_eq!(info.columns, 5);
148 assert_eq!(info.rows, 4);
149 assert_eq!(info.frame_count, 18);
150 }
151}