1 // flow-texpack: A program that will allow you to generate texture atlas.
2 // zlib License (see LICENSE)
4 use crate::texpack::app::AtlasImage;
5 use crate::texpack::packer::MAX_SIZE;
7 use image::GenericImageView;
8 use image::{DynamicImage, ImageBuffer, ImageReader, Rgba, RgbaImage, imageops::FilterType};
10 use std::hash::{DefaultHasher, Hash, Hasher};
11 use std::path::PathBuf;
13 /// Specifies the different error types that can occur.
14 #[derive(PartialEq, Clone, Debug)]
15 pub enum TextureError {
20 /// Specifies the properties of a `Texture`.
21 #[derive(Clone, Debug)]
24 pub file_path: PathBuf,
26 pub file_name: String,
31 /// is the orignal x position (valid if trimmed).
33 /// is the orignal y position (valid if trimmed).
35 /// is the orignal width (valid if trimmed).
37 /// is the orignal height (valid if trimmed).
39 /// is the hash value (width, height and buffer combined).
41 /// is the raw buffer in RGBA format.
42 pub buffer: RgbaImage,
46 /// Instantiates a new `Texture` instance.
47 pub fn new() -> Self {
49 file_path: PathBuf::new(),
50 file_name: String::new(),
58 buffer: RgbaImage::new(1, 1),
62 /// Instantiates a new `Texture` instance based on given input params.
66 /// * `width` - is the `Texture` width.
67 /// * `height` - is the `Texture` height.
71 /// [`InvalidArg`](crate::texpack::texture::TextureError) error is returned if:
72 /// `width == 0 || width > 8192 ||
73 /// height == 0 || height > 8192`.
74 pub fn with_details(width: u32, height: u32) -> Result<Self, TextureError> {
75 if width > 0 && width <= MAX_SIZE && height > 0 && height <= MAX_SIZE {
77 file_path: PathBuf::new(),
78 file_name: String::new(),
86 buffer: RgbaImage::new(width, height),
89 Err(TextureError::InvalidArg)
97 /// * `file_path` - is the texture file path.
98 /// * `premultiply` - is a flag determining whether to premultiply RBG by alpha channel or not.
99 /// * `trim` - is a flag determining whether to trim excess transparent pixels or not.
100 /// * `adjust_fit` - is a flag determining whether to adjust fit automatically or not.
101 /// * `padding` - is the padding to use between textures.
102 /// * `atlas_size` - is the atlas size.
106 /// If loading fails.
116 // remember file path and file name:
117 self.file_path = file_path.clone();
118 if let Some(file_name) = file_path.file_name() {
119 if let Some(file_name_str) = file_name.to_str() {
120 self.file_name = String::from(file_name_str);
125 let image = ImageReader::open(file_path).unwrap().decode().unwrap();
126 let (width, height) = image.dimensions();
127 self.update_initial_size(width, height);
129 // trim excess transparent pixels off the texture:
131 self.buffer = self.trim(&image.to_rgba8());
133 self.buffer = image.to_rgba8();
136 // premultiply all the pixels by their alpha value:
141 // check if needing to adjust / scale texture size to fit atlas size:
143 && (((self.width + padding) > atlas_size) || ((self.height + padding) > atlas_size))
145 self.buffer = self.resize_to_fit(padding, atlas_size);
148 let mut hasher = DefaultHasher::new();
149 self.width.hash(&mut hasher);
150 self.height.hash(&mut hasher);
151 self.buffer.hash(&mut hasher);
152 self.hash_value = hasher.finish();
155 "Loaded texture: '{}' w: {} h: {}, hash_value: {}",
156 self.file_name, self.width, self.height, self.hash_value
160 /// Saves texture to disk.
164 /// * `file_path` - is the output file path.
165 /// * `image_type` - is the output image type.
170 pub fn save(&self, file_path: &PathBuf, image_type: AtlasImage) {
171 let dst_image = DynamicImage::ImageRgba8(self.buffer.clone());
173 // make sure extension supplied is valid to help out with 'guessing' of type:
174 if let Some(extension) = file_path.extension() {
175 let ext_lc = extension.to_ascii_lowercase();
176 if image_type == AtlasImage::Png && ext_lc == "png"
177 || image_type == AtlasImage::Tga && ext_lc == "tga"
178 || image_type == AtlasImage::Tiff && ext_lc == "tiff"
179 || image_type == AtlasImage::Webp && ext_lc == "webp"
181 dst_image.save(file_path).unwrap();
184 "Supplied file_path: {} does not have a valid extension that matches image type: {:?}!",
192 /// Copy pixels from given `src` texture into this texture.
196 /// * `src` - is the source texture to copy from.
197 /// * `tx` - is the x offset to use when copying.
198 /// * `ty` - is the y offset to use when copying.
202 /// If pixel is out of bounds.
203 pub fn copy_pixels(&mut self, src: &Texture, tx: u32, ty: u32) {
204 let (src_width, src_height) = src.buffer.dimensions();
206 for y in 0..src_height {
207 for x in 0..src_width {
208 let pixel = src.buffer.get_pixel(x, y);
209 self.buffer.put_pixel(x + tx, y + ty, *pixel);
214 /// Copy pixels from given `src` texture into this texture rotated 90 degrees clockwise.
218 /// * `src` - is the source texture to copy from.
219 /// * `tx` - is the x offset to use when copying.
220 /// * `ty` - is the y offset to use when copying.
221 pub fn copy_pixels_rot_90cw(&mut self, src: &Texture, tx: u32, ty: u32) {
222 let (src_width, src_height) = src.buffer.dimensions();
223 let r = src_height - 1;
225 for y in 0..src_height {
226 for x in 0..src_width {
227 let pixel = src.buffer.get_pixel(x, y);
228 self.buffer.put_pixel(r - y + tx, x + ty, *pixel);
233 /// Get the texture area (width * height).
234 pub fn get_area(&self) -> u32 {
235 return self.width * self.height;
238 /// Updates initial size properties.
239 fn update_initial_size(&mut self, width: u32, height: u32) {
242 self.frame_w = width;
243 self.frame_h = height;
245 self.height = height;
248 /// Trims out excess pixels and returns new RBGA buffer.
249 fn trim(&mut self, img: &RgbaImage) -> RgbaImage {
250 let (width, height) = img.dimensions();
252 if width == 0 || height == 0 {
253 return ImageBuffer::new(1, 1);
256 // bounding box of non-transparent pixels:
257 let mut min_x = width;
258 let mut min_y = height;
259 let mut max_x = 0u32;
260 let mut max_y = 0u32;
264 let pixel = img.get_pixel(x, y);
266 // non-transparent pixel:
267 min_x = min_x.min(x);
268 min_y = min_y.min(y);
269 max_x = max_x.max(x);
270 max_y = max_y.max(y);
275 // no non-transparent pixels found, return 1x1 transparent buffer:
276 if max_x < min_x || max_y < min_y {
277 return ImageBuffer::from_pixel(1, 1, Rgba([0, 0, 0, 0]));
280 // calc new dimensions (add 1 for bounds):
281 let new_width = (max_x - min_x) + 1;
282 let new_height = (max_y - min_y) + 1;
284 // no trimming needed -> just clone it:
285 if new_width == width && new_height == height {
290 let mut trimmed = ImageBuffer::new(new_width, new_height);
291 for y in 0..new_height {
292 for x in 0..new_width {
293 let src_pixel = img.get_pixel(min_x + x, min_y + y);
294 trimmed.put_pixel(x, y, *src_pixel);
298 let x: i32 = min_x.try_into().unwrap();
299 let y: i32 = min_y.try_into().unwrap();
302 self.frame_w = width;
303 self.frame_h = height;
304 self.width = new_width;
305 self.height = new_height;
310 /// Premultiply destination pixel by alpha.
311 fn premultiply(&mut self) {
312 let (width, height) = self.buffer.dimensions();
317 let src_pixel = self.buffer.get_pixel(x, y);
319 // premultiply destination pixel by alpha:
320 let alpha = src_pixel[3] as f32 / 255.0;
321 let dst_pixel = Rgba([
322 (src_pixel[0] as f32 * alpha) as u8,
323 (src_pixel[1] as f32 * alpha) as u8,
324 (src_pixel[2] as f32 * alpha) as u8,
328 // set new pixel value:
329 self.buffer.put_pixel(x, y, dst_pixel);
334 /// Resize buffer so that it fits given `atlas_size + padding`.
335 fn resize_to_fit(&mut self, padding: u32, atlas_size: u32) -> RgbaImage {
336 let (src_width, src_height) = self.buffer.dimensions();
338 if src_width == 0 || src_height == 0 || atlas_size == 0 {
339 panic!("Invalid internal buffer state or atlas_size is 0");
342 // calculate scale factor:
343 let mut _scale_factor = 0.0;
344 if src_width > src_height {
345 _scale_factor = src_width as f32 / atlas_size as f32;
346 } else if src_height > src_width {
347 _scale_factor = src_height as f32 / atlas_size as f32;
349 _scale_factor = src_width as f32 / atlas_size as f32;
351 info!("scale_factor is: {}", _scale_factor);
353 // calculate new size of texture:
354 let mut new_width = (src_width as f32 / _scale_factor).floor() as u32;
355 let mut new_height = (src_height as f32 / _scale_factor).floor() as u32;
357 // adjust for padding too:
358 new_width -= padding;
359 new_height -= padding;
361 // make sure width and height is at least 1 pixel after scaling and padding:
369 let src_image = DynamicImage::ImageRgba8(self.buffer.clone());
370 let dst_image = src_image.resize(new_width, new_height, FilterType::Lanczos3);
372 "Resized image from {}x{} to {}x{}",
373 src_width, src_height, new_width, new_height
379 self.frame_w = new_width;
380 self.frame_h = new_height;
381 self.width = new_width;
382 self.height = new_height;
384 return dst_image.to_rgba8();
392 use crate::texpack::app::{exists_file, get_atlas_image_extension, remove_file};
393 use std::ffi::OsString;
398 Texture::with_details(0, 0).unwrap_err(),
399 TextureError::InvalidArg
403 Texture::with_details(32, 0).unwrap_err(),
404 TextureError::InvalidArg
408 Texture::with_details(0, 32).unwrap_err(),
409 TextureError::InvalidArg
413 Texture::with_details(8192, 8193).unwrap_err(),
414 TextureError::InvalidArg
418 Texture::with_details(8193, 8192).unwrap_err(),
419 TextureError::InvalidArg
424 fn texture_basics() {
425 let t1 = Texture::new();
426 assert_eq!(t1.width, 0);
427 assert_eq!(t1.height, 0);
428 assert_eq!(t1.frame_x, 0);
429 assert_eq!(t1.frame_y, 0);
430 assert_eq!(t1.frame_w, 0);
431 assert_eq!(t1.frame_h, 0);
432 assert_eq!(t1.hash_value, 0);
434 let t2 = Texture::with_details(32, 32).unwrap();
435 assert_eq!(t2.width, 32);
436 assert_eq!(t2.height, 32);
440 fn texture_load_all_supported_formats() {
441 let supported_extensions = vec![
442 OsString::from("bmp"),
443 OsString::from("hdr"),
444 OsString::from("jpg"),
445 OsString::from("jpeg"),
446 OsString::from("png"),
447 OsString::from("tga"),
448 OsString::from("tiff"),
449 OsString::from("webp"),
452 for ext in &supported_extensions {
453 let base_file_path = "test_data/white_32x32";
454 let file_path = PathBuf::from(format!("{}.{}", base_file_path, ext.display()));
456 let mut t = Texture::new();
457 t.load(&file_path, false, false, false, 0, 64);
458 assert_eq!(t.width, 32);
459 assert_eq!(t.height, 32);
460 assert_eq!(t.file_path, file_path);
461 if let Some(file_name) = file_path.file_name() {
462 if let Some(file_name_str) = file_name.to_str() {
463 assert_eq!(t.file_name, file_name_str);
466 assert_eq!(t.frame_x, 0);
467 assert_eq!(t.frame_y, 0);
468 assert_eq!(t.frame_w, 32);
469 assert_eq!(t.frame_h, 32);
470 assert_eq!(t.hash_value > 0, true);
475 fn texture_save_all_supported_formats() {
476 let atlas_image_types = vec![
483 let mut t1 = Texture::new();
485 &PathBuf::from("test_data/white_32x32.png"),
493 let mut output = Texture::with_details(64, 64).unwrap();
494 output.copy_pixels(&t1, 16, 16);
496 let base_file_path = "test_data/save_64x64";
497 for atlas_image_type in &atlas_image_types {
498 let file_path = PathBuf::from(format!(
501 get_atlas_image_extension(atlas_image_type.clone())
503 output.save(&file_path, atlas_image_type.clone());
505 assert_eq!(exists_file(&file_path), true);
506 remove_file(&file_path);
507 assert_eq!(exists_file(&file_path), false);
512 fn texture_copy_pixels() {
513 let mut t1 = Texture::new();
515 &PathBuf::from("test_data/white_32x32.png"),
523 let mut t2 = Texture::new();
525 &PathBuf::from("test_data/red_32x32.png"),
533 let mut t3 = Texture::new();
535 &PathBuf::from("test_data/green_32x32.png"),
543 let mut t4 = Texture::new();
545 &PathBuf::from("test_data/blue_32x32.png"),
553 let mut output = Texture::with_details(64, 64).unwrap();
554 let file_path = PathBuf::from("test_data/copy_pixels_64x64.png");
556 output.copy_pixels(&t1, 0, 0);
557 output.copy_pixels(&t2, 32, 0);
558 output.copy_pixels(&t3, 32, 32);
559 output.copy_pixels(&t4, 0, 32);
561 output.save(&file_path, AtlasImage::Png);
562 assert_eq!(exists_file(&file_path), true);
563 remove_file(&file_path);
564 assert_eq!(exists_file(&file_path), false);
568 fn texture_copy_pixels_rot_90cw() {
569 let mut t1 = Texture::new();
571 &PathBuf::from("test_data/white_32x16.png"),
579 let mut output = Texture::with_details(64, 64).unwrap();
580 let file_path = PathBuf::from("test_data/copy_pixels_rot_90cw_64x64.png");
582 output.copy_pixels_rot_90cw(&t1, 0, 0);
583 output.copy_pixels_rot_90cw(&t1, 32, 0);
585 output.save(&file_path, AtlasImage::Png);
586 assert_eq!(exists_file(&file_path), true);
587 remove_file(&file_path);
588 assert_eq!(exists_file(&file_path), false);
592 fn texture_get_area() {
593 let t1 = Texture::with_details(32, 32).unwrap();
594 assert_eq!(t1.get_area(), 1024);