]> luflow.net public git repositories - flow-texpack.git/blob - src/texpack/texture.rs
Initial commit.
[flow-texpack.git] / src / texpack / texture.rs
1 // flow-texpack: A program that will allow you to generate texture atlas.
2 // zlib License (see LICENSE)
3
4 use crate::texpack::app::AtlasImage;
5 use crate::texpack::packer::MAX_SIZE;
6
7 use image::GenericImageView;
8 use image::{DynamicImage, ImageBuffer, ImageReader, Rgba, RgbaImage, imageops::FilterType};
9 use log::info;
10 use std::hash::{DefaultHasher, Hash, Hasher};
11 use std::path::PathBuf;
12
13 /// Specifies the different error types that can occur.
14 #[derive(PartialEq, Clone, Debug)]
15 pub enum TextureError {
16 /// Invalid argument
17 InvalidArg,
18 }
19
20 /// Specifies the properties of a `Texture`.
21 #[derive(Clone, Debug)]
22 pub struct Texture {
23 /// is the file path.
24 pub file_path: PathBuf,
25 /// is the file name.
26 pub file_name: String,
27 /// is the width.
28 pub width: u32,
29 /// is the height.
30 pub height: u32,
31 /// is the orignal x position (valid if trimmed).
32 pub frame_x: i32,
33 /// is the orignal y position (valid if trimmed).
34 pub frame_y: i32,
35 /// is the orignal width (valid if trimmed).
36 pub frame_w: u32,
37 /// is the orignal height (valid if trimmed).
38 pub frame_h: u32,
39 /// is the hash value (width, height and buffer combined).
40 pub hash_value: u64,
41 /// is the raw buffer in RGBA format.
42 pub buffer: RgbaImage,
43 }
44
45 impl Texture {
46 /// Instantiates a new `Texture` instance.
47 pub fn new() -> Self {
48 Self {
49 file_path: PathBuf::new(),
50 file_name: String::new(),
51 width: 0,
52 height: 0,
53 frame_x: 0,
54 frame_y: 0,
55 frame_w: 0,
56 frame_h: 0,
57 hash_value: 0,
58 buffer: RgbaImage::new(1, 1),
59 }
60 }
61
62 /// Instantiates a new `Texture` instance based on given input params.
63 ///
64 /// # Arguments
65 ///
66 /// * `width` - is the `Texture` width.
67 /// * `height` - is the `Texture` height.
68 ///
69 /// # Errors
70 ///
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 {
76 Ok(Self {
77 file_path: PathBuf::new(),
78 file_name: String::new(),
79 width,
80 height,
81 frame_x: 0,
82 frame_y: 0,
83 frame_w: 0,
84 frame_h: 0,
85 hash_value: 0,
86 buffer: RgbaImage::new(width, height),
87 })
88 } else {
89 Err(TextureError::InvalidArg)
90 }
91 }
92
93 /// Load texture.
94 ///
95 /// # Arguments
96 ///
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.
103 ///
104 /// # Panics
105 ///
106 /// If loading fails.
107 pub fn load(
108 &mut self,
109 file_path: &PathBuf,
110 premultiply: bool,
111 trim: bool,
112 adjust_fit: bool,
113 padding: u32,
114 atlas_size: u32,
115 ) {
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);
121 }
122 }
123
124 // load the image:
125 let image = ImageReader::open(file_path).unwrap().decode().unwrap();
126 let (width, height) = image.dimensions();
127 self.update_initial_size(width, height);
128
129 // trim excess transparent pixels off the texture:
130 if trim {
131 self.buffer = self.trim(&image.to_rgba8());
132 } else {
133 self.buffer = image.to_rgba8();
134 }
135
136 // premultiply all the pixels by their alpha value:
137 if premultiply {
138 self.premultiply();
139 }
140
141 // check if needing to adjust / scale texture size to fit atlas size:
142 if adjust_fit
143 && (((self.width + padding) > atlas_size) || ((self.height + padding) > atlas_size))
144 {
145 self.buffer = self.resize_to_fit(padding, atlas_size);
146 }
147
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();
153
154 info!(
155 "Loaded texture: '{}' w: {} h: {}, hash_value: {}",
156 self.file_name, self.width, self.height, self.hash_value
157 );
158 }
159
160 /// Saves texture to disk.
161 ///
162 /// # Arguments
163 ///
164 /// * `file_path` - is the output file path.
165 /// * `image_type` - is the output image type.
166 ///
167 /// # Panics
168 ///
169 /// If save fails.
170 pub fn save(&self, file_path: &PathBuf, image_type: AtlasImage) {
171 let dst_image = DynamicImage::ImageRgba8(self.buffer.clone());
172
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"
180 {
181 dst_image.save(file_path).unwrap();
182 } else {
183 panic!(
184 "Supplied file_path: {} does not have a valid extension that matches image type: {:?}!",
185 file_path.display(),
186 image_type
187 );
188 }
189 }
190 }
191
192 /// Copy pixels from given `src` texture into this texture.
193 ///
194 /// # Arguments
195 ///
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.
199 ///
200 /// # Panics
201 ///
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();
205
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);
210 }
211 }
212 }
213
214 /// Copy pixels from given `src` texture into this texture rotated 90 degrees clockwise.
215 ///
216 /// # Arguments
217 ///
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;
224
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);
229 }
230 }
231 }
232
233 /// Get the texture area (width * height).
234 pub fn get_area(&self) -> u32 {
235 return self.width * self.height;
236 }
237
238 /// Updates initial size properties.
239 fn update_initial_size(&mut self, width: u32, height: u32) {
240 self.frame_x = 0;
241 self.frame_y = 0;
242 self.frame_w = width;
243 self.frame_h = height;
244 self.width = width;
245 self.height = height;
246 }
247
248 /// Trims out excess pixels and returns new RBGA buffer.
249 fn trim(&mut self, img: &RgbaImage) -> RgbaImage {
250 let (width, height) = img.dimensions();
251
252 if width == 0 || height == 0 {
253 return ImageBuffer::new(1, 1);
254 }
255
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;
261
262 for y in 0..height {
263 for x in 0..width {
264 let pixel = img.get_pixel(x, y);
265 if pixel[3] > 0 {
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);
271 }
272 }
273 }
274
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]));
278 }
279
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;
283
284 // no trimming needed -> just clone it:
285 if new_width == width && new_height == height {
286 return img.clone();
287 }
288
289 // crop the image:
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);
295 }
296 }
297
298 let x: i32 = min_x.try_into().unwrap();
299 let y: i32 = min_y.try_into().unwrap();
300 self.frame_x = -x;
301 self.frame_y = -y;
302 self.frame_w = width;
303 self.frame_h = height;
304 self.width = new_width;
305 self.height = new_height;
306
307 return trimmed;
308 }
309
310 /// Premultiply destination pixel by alpha.
311 fn premultiply(&mut self) {
312 let (width, height) = self.buffer.dimensions();
313
314 for y in 0..height {
315 for x in 0..width {
316 // get source pixel:
317 let src_pixel = self.buffer.get_pixel(x, y);
318
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,
325 src_pixel[3],
326 ]);
327
328 // set new pixel value:
329 self.buffer.put_pixel(x, y, dst_pixel);
330 }
331 }
332 }
333
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();
337
338 if src_width == 0 || src_height == 0 || atlas_size == 0 {
339 panic!("Invalid internal buffer state or atlas_size is 0");
340 }
341
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;
348 } else {
349 _scale_factor = src_width as f32 / atlas_size as f32;
350 }
351 info!("scale_factor is: {}", _scale_factor);
352
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;
356
357 // adjust for padding too:
358 new_width -= padding;
359 new_height -= padding;
360
361 // make sure width and height is at least 1 pixel after scaling and padding:
362 if new_width <= 0 {
363 new_width = 1;
364 }
365 if new_height <= 0 {
366 new_height = 1;
367 }
368
369 let src_image = DynamicImage::ImageRgba8(self.buffer.clone());
370 let dst_image = src_image.resize(new_width, new_height, FilterType::Lanczos3);
371 info!(
372 "Resized image from {}x{} to {}x{}",
373 src_width, src_height, new_width, new_height
374 );
375
376 // reset:
377 self.frame_x = 0;
378 self.frame_y = 0;
379 self.frame_w = new_width;
380 self.frame_h = new_height;
381 self.width = new_width;
382 self.height = new_height;
383
384 return dst_image.to_rgba8();
385 }
386 }
387
388 // unit tests:
389 #[cfg(test)]
390 mod tests {
391 use super::*;
392 use crate::texpack::app::{exists_file, get_atlas_image_extension, remove_file};
393 use std::ffi::OsString;
394
395 #[test]
396 fn texture_error() {
397 assert_eq!(
398 Texture::with_details(0, 0).unwrap_err(),
399 TextureError::InvalidArg
400 );
401
402 assert_eq!(
403 Texture::with_details(32, 0).unwrap_err(),
404 TextureError::InvalidArg
405 );
406
407 assert_eq!(
408 Texture::with_details(0, 32).unwrap_err(),
409 TextureError::InvalidArg
410 );
411
412 assert_eq!(
413 Texture::with_details(8192, 8193).unwrap_err(),
414 TextureError::InvalidArg
415 );
416
417 assert_eq!(
418 Texture::with_details(8193, 8192).unwrap_err(),
419 TextureError::InvalidArg
420 );
421 }
422
423 #[test]
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);
433
434 let t2 = Texture::with_details(32, 32).unwrap();
435 assert_eq!(t2.width, 32);
436 assert_eq!(t2.height, 32);
437 }
438
439 #[test]
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"),
450 ];
451
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()));
455
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);
464 }
465 }
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);
471 }
472 }
473
474 #[test]
475 fn texture_save_all_supported_formats() {
476 let atlas_image_types = vec![
477 AtlasImage::Png,
478 AtlasImage::Tga,
479 AtlasImage::Tiff,
480 AtlasImage::Webp,
481 ];
482
483 let mut t1 = Texture::new();
484 t1.load(
485 &PathBuf::from("test_data/white_32x32.png"),
486 false,
487 false,
488 false,
489 0,
490 64,
491 );
492
493 let mut output = Texture::with_details(64, 64).unwrap();
494 output.copy_pixels(&t1, 16, 16);
495
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!(
499 "{}.{}",
500 base_file_path,
501 get_atlas_image_extension(atlas_image_type.clone())
502 ));
503 output.save(&file_path, atlas_image_type.clone());
504
505 assert_eq!(exists_file(&file_path), true);
506 remove_file(&file_path);
507 assert_eq!(exists_file(&file_path), false);
508 }
509 }
510
511 #[test]
512 fn texture_copy_pixels() {
513 let mut t1 = Texture::new();
514 t1.load(
515 &PathBuf::from("test_data/white_32x32.png"),
516 false,
517 false,
518 false,
519 0,
520 64,
521 );
522
523 let mut t2 = Texture::new();
524 t2.load(
525 &PathBuf::from("test_data/red_32x32.png"),
526 false,
527 false,
528 false,
529 0,
530 64,
531 );
532
533 let mut t3 = Texture::new();
534 t3.load(
535 &PathBuf::from("test_data/green_32x32.png"),
536 false,
537 false,
538 false,
539 0,
540 64,
541 );
542
543 let mut t4 = Texture::new();
544 t4.load(
545 &PathBuf::from("test_data/blue_32x32.png"),
546 false,
547 false,
548 false,
549 0,
550 64,
551 );
552
553 let mut output = Texture::with_details(64, 64).unwrap();
554 let file_path = PathBuf::from("test_data/copy_pixels_64x64.png");
555
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);
560
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);
565 }
566
567 #[test]
568 fn texture_copy_pixels_rot_90cw() {
569 let mut t1 = Texture::new();
570 t1.load(
571 &PathBuf::from("test_data/white_32x16.png"),
572 false,
573 false,
574 false,
575 0,
576 64,
577 );
578
579 let mut output = Texture::with_details(64, 64).unwrap();
580 let file_path = PathBuf::from("test_data/copy_pixels_rot_90cw_64x64.png");
581
582 output.copy_pixels_rot_90cw(&t1, 0, 0);
583 output.copy_pixels_rot_90cw(&t1, 32, 0);
584
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);
589 }
590
591 #[test]
592 fn texture_get_area() {
593 let t1 = Texture::with_details(32, 32).unwrap();
594 assert_eq!(t1.get_area(), 1024);
595 }
596 }