]> luflow.net public git repositories - flow-texpack.git/blob - src/texpack/app.rs
Initial commit.
[flow-texpack.git] / src / texpack / app.rs
1 // flow-texpack: A program that will allow you to generate texture atlas.
2 // zlib License (see LICENSE)
3
4 use crate::Packer;
5 use crate::Texture;
6
7 use flow_rbp::FreeRectHeuristic;
8 use rich_rust::console::Console;
9 use rich_rust::interactive::Status;
10
11 use clap::{Parser, ValueEnum};
12
13 use tokio::io;
14 use tokio::task;
15
16 use std::time::Instant;
17
18 use log::info;
19
20 use std::collections::HashSet;
21 use std::ffi::OsString;
22 use std::fs;
23 use std::hash::{DefaultHasher, Hash, Hasher};
24 use std::io::Write;
25 use std::path::PathBuf;
26 use std::sync::Arc;
27
28 /// name of the application:
29 const NAME: &'static str = "https://luflow.net/git-repos/flow-texpack.git";
30
31 /// short about description shown for option '-h':
32 const ABOUT: &'static str = "
33 flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR,
34 JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that
35 can be read by a game.";
36
37 /// long about description shown for option '--help':
38 const LONG_ABOUT: &'static str = "
39 flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR,
40 JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that
41 can be read by a game.
42
43 Examples:
44 flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v
45 flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v --load-filter png tga
46 flow-texpack -i data -e data/tiles -o out/atlas -m -t -u -r --atlas-size pot2048 --rect-heuristic area-fit -v
47 flow-texpack --input-file input.txt --exlude-file exclude.txt -o out/atlas -v
48 flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-size -v
49 flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-fit -v";
50
51 /// Specifies the different atlas descriptor types.
52 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
53 enum AtlasDescriptor {
54 /// atlas descriptor type: JSON
55 Json,
56 /// atlas descriptor type: Txt
57 Txt,
58 /// atlas descriptor type: Txt (with description header)
59 TxtDesc,
60 }
61
62 /// Specifies the different atlas image types.
63 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
64 pub enum AtlasImage {
65 /// atlas image type: PNG
66 Png,
67 /// atlas image type: TGA
68 Tga,
69 /// atlas image type: TIFF
70 Tiff,
71 /// atlas image type: Webp
72 Webp,
73 }
74
75 /// Specifies the different load filter types.
76 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
77 pub enum LoadFilter {
78 /// load filter type: BMP
79 Bmp,
80 /// load filter type: HDR
81 Hdr,
82 /// load filter type: JPG
83 Jpg,
84 /// load filter type: PNG
85 Png,
86 /// load filter type: TGA
87 Tga,
88 /// load filter type: TIFF
89 Tiff,
90 /// load filter type: Webp
91 Webp,
92 }
93
94 /// Specifies the different atlas output sizes.
95 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
96 enum AtlasSize {
97 /// power-of-two 64x64 size:
98 Pot64 = 64,
99 /// power-of-two 128x128 size:
100 Pot128 = 128,
101 /// power-of-two 256x256 size:
102 Pot256 = 256,
103 /// power-of-two 512x512 size:
104 Pot512 = 512,
105 /// power-of-two 1024x1024 size:
106 Pot1024 = 1024,
107 /// power-of-two 2048x2048 size:
108 Pot2048 = 2048,
109 /// power-of-two 4096x4096 size:
110 Pot4096 = 4096,
111 /// power-of-two 8192x8192 size:
112 Pot8192 = 8192,
113 }
114
115 /// Specifies the different heuristic types.
116 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
117 enum RectHeuristic {
118 /// Choose to pack `R` into such `Fi` that `min(wf - w, hf - h)` is the smallest. In other words, we
119 /// minimize the length of the shorter leftover side.
120 ShortSideFit,
121
122 /// Pack `R` into an `Fi` such that `max(wf - w, hf - h)` is the smallest. That is, we minimize
123 /// the length of the longer leftover side.
124 LongSideFit,
125
126 /// Pick the `Fi ∈ F` that is smallest in area to place the next rectangle `R` into. If there is a
127 /// tie, we use the `ShortSideFit` rule to break it.
128 AreaFit,
129
130 /// Orient and place each rectangle to the position where the y-coordinate of the top side of the
131 /// rectangle is the smallest and if there are several such valid positions, pick the one that has
132 /// the smallest x-coordinate value.
133 BottomLeft,
134
135 /// Place `R` into a position where the length of the perimeter of `R` that is touched by the bin
136 /// edge or by a previously packed rectangle is maximized.
137 ContactPoint,
138 }
139
140 #[derive(Parser, Debug)]
141 #[command(version, about = ABOUT, long_about = LONG_ABOUT)]
142 struct CliArgs {
143 /// input files/directories separated by space (' ')
144 #[arg(short = 'i', long = "input", value_delimiter = ' ', num_args = 1.., group = "input_group")]
145 input: Option<Vec<PathBuf>>,
146
147 /// input file containing files/directories
148 /// (each entry needs to be on a new line)
149 #[arg(long = "input-file", group = "input_group", verbatim_doc_comment)]
150 input_file: Option<PathBuf>,
151
152 /// exclude files/directories separated by space (' ')
153 #[arg(short = 'e', long = "exclude", value_delimiter = ' ', num_args = 1.., group = "exclude_group")]
154 exclude: Option<Vec<PathBuf>>,
155
156 /// exclude file containing files/directories
157 /// (each entry needs to be on a new line)
158 #[arg(long = "exclude-file", group = "exclude_group", verbatim_doc_comment)]
159 exclude_file: Option<PathBuf>,
160
161 /// output file
162 #[arg(short = 'o', long = "output", requires = "input_group")]
163 output: PathBuf,
164
165 /// atlas descriptor
166 #[arg(long = "atlas-descriptor", value_enum, default_value_t = AtlasDescriptor::Json)]
167 atlas_descriptor: AtlasDescriptor,
168
169 /// atlas image
170 #[arg(long = "atlas-image", value_enum, default_value_t = AtlasImage::Png)]
171 atlas_image: AtlasImage,
172
173 /// max atlas output size POT
174 #[arg(long = "atlas-size", value_enum, default_value_t = AtlasSize::Pot1024)]
175 atlas_size: AtlasSize,
176
177 /// load filter
178 #[arg(long = "load-filter", value_enum, value_delimiter = ' ', num_args = 1..)]
179 load_filter: Option<Vec<LoadFilter>>,
180
181 /// max atlases that can be created (value in the range 1 - 4096)
182 #[arg(long = "max-atlases", default_value_t = 64, value_parser = clap::value_parser!(u16).range(1..=4096))]
183 max_atlases: u16,
184
185 /// enable premultiply the pixels of the textures by their alpha channel
186 #[arg(short = 'm', long = "premultiply")]
187 premultiply: bool,
188
189 /// enable trim excess transparency off the textures
190 #[arg(short = 't', long = "trim")]
191 trim: bool,
192
193 /// enable force packer to re-pack (ignore stored hashes)
194 #[arg(short = 'f', long = "force")]
195 force: bool,
196
197 /// enable remove duplicate textures from the atlas
198 #[arg(short = 'u', long = "unique")]
199 unique: bool,
200
201 /// enable rotation of textures (90 degrees clockwise)
202 #[arg(short = 'r', long = "rotate")]
203 rotate: bool,
204
205 /// enable force atlas POT square size
206 #[arg(long = "force-square")]
207 force_square: bool,
208
209 /// enable adjust atlas size automatically so that texture will fit
210 #[arg(long = "adjust-size")]
211 adjust_size: bool,
212
213 /// enable adjust texture size so that it will fit given atlas size
214 #[arg(long = "adjust-fit")]
215 adjust_fit: bool,
216
217 /// enable generation of mipmaps (rendering hint)
218 #[arg(long = "generate-mipmaps")]
219 generate_mipmaps: bool,
220
221 /// padding between textures (value in the range 0 - 16)
222 #[arg(short = 'p', long = "pad", default_value_t = 1, value_parser = clap::value_parser!(u8).range(0..=16) )]
223 pad: u8,
224
225 /// heuristic rule to use when deciding where to place a new rectangle
226 #[arg(long = "rect-heuristic", value_enum, default_value_t = RectHeuristic::LongSideFit)]
227 rect_heuristic: RectHeuristic,
228
229 /// enable verbose output of progress
230 #[arg(short = 'v', long = "verbose")]
231 verbose: bool,
232
233 /// enable logging to log file 'flow-texpack.log'
234 #[arg(short = 'l', long = "log")]
235 log: bool,
236 }
237
238 /// Specifies the properties of a App.
239 pub struct App {
240 /// is the cli arguments program is called with:
241 cli_args: CliArgs,
242 /// is the console used for colored text output and progress bars (verbose mode enabled):
243 console: Arc<Console>,
244 /// is the set holding all input files (excluding those in exclude_files if any):
245 input_files: HashSet<PathBuf>,
246 /// is the set holding all exclude files:
247 exclude_files: HashSet<PathBuf>,
248 /// is the set holding supported image extensions for load:
249 supported_extensions_load: HashSet<OsString>,
250 /// is the set holding supported image extensions for save:
251 supported_extensions_save: HashSet<OsString>,
252 /// is the vector holding the loaded textures:
253 textures: Vec<Texture>,
254 /// is the vector holding the packers:
255 packers: Vec<Arc<Packer>>,
256 /// is the default hasher:
257 hasher: DefaultHasher,
258 /// is the hash value:
259 hash_value: u64,
260 }
261
262 impl App {
263 /// Instantiates a new App instance.
264 ///
265 /// # Panics
266 ///
267 /// If initialization failed.
268 pub fn new() -> Self {
269 Self {
270 cli_args: CliArgs::parse(),
271 console: Console::new().shared(),
272 input_files: HashSet::new(),
273 exclude_files: HashSet::new(),
274 supported_extensions_load: HashSet::from([
275 OsString::from("bmp"),
276 OsString::from("hdr"),
277 OsString::from("jpg"),
278 OsString::from("png"),
279 OsString::from("tga"),
280 OsString::from("tiff"),
281 OsString::from("webp"),
282 ]),
283 supported_extensions_save: HashSet::from([
284 OsString::from("png"),
285 OsString::from("tga"),
286 OsString::from("tiff"),
287 OsString::from("webp"),
288 ]),
289 textures: Vec::new(),
290 packers: Vec::new(),
291 hasher: DefaultHasher::new(),
292 hash_value: 0,
293 }
294 }
295
296 /// Execute the main application loop.
297 ///
298 /// # Panics
299 ///
300 /// If something unexpected happens.
301 pub async fn run(&mut self) {
302 // start timer:
303 let start_time = Instant::now();
304
305 // initialize:
306 self.initialize().await;
307
308 if !self.identical_hash().await {
309 if self.input_files.len() > 0 {
310 // remove old atlas files if any:
311 self.remove_old_files();
312
313 // load all input textures:
314 self.load_textures().await;
315
316 // sort textures by area (largest area first):
317 self.sort_textures();
318
319 // make sure out directory exists:
320 if !exists_dir(&self.cli_args.output) {
321 if let Some(parent_dir) = self.cli_args.output.parent() {
322 create_dir_all(&parent_dir.to_path_buf());
323 }
324 }
325
326 // pack the textures:
327 self.pack_textures();
328
329 // save atlas image/s:
330 self.save_atlas_images().await;
331
332 // save atlas descriptor:
333 self.save_atlas_descriptor();
334
335 // save new hash value:
336 self.save_input_hash();
337 } else {
338 if self.cli_args.verbose {
339 self.console.print("[dim]No input files...[/]");
340 }
341 }
342 } else {
343 if self.cli_args.verbose {
344 self.console
345 .print("[dim]Identical hash value. No need to continue...[/]");
346 }
347 }
348
349 // we're done:
350 if self.cli_args.verbose {
351 self.console.print("");
352 self.console.print(&format!(
353 "[dim]Completed in {:.1}s[/]",
354 start_time.elapsed().as_secs_f64()
355 ));
356 self.console.print("[green]Done![/]");
357 }
358 }
359
360 /// initialize logger and prepare input files vector:
361 async fn initialize(&mut self) {
362 // initialize logger:
363 self.init_logger();
364
365 // prepare input files:
366 self.prepare().await;
367
368 // log cli_args options:
369 self.log_options();
370 }
371
372 /// determine whether current hash is the same as previous:
373 async fn identical_hash(&mut self) -> bool {
374 if !self.cli_args.force {
375 // identical hash from prev run = no need to continue:
376 if self.cli_args.verbose {
377 if let Ok(_status) = Status::new(&self.console, "Checking input hash...") {
378 return self.check_input_hash().await;
379 }
380 } else {
381 return self.check_input_hash().await;
382 }
383 }
384
385 return false;
386 }
387
388 /// initialize logger:
389 fn init_logger(&self) {
390 if self.cli_args.log {
391 fern::Dispatch::new()
392 .format(|out, message, record| {
393 out.finish(format_args!("[{}] {}", record.level(), message))
394 })
395 .level(log::LevelFilter::Debug)
396 .chain(
397 std::fs::OpenOptions::new()
398 .create(true)
399 .write(true)
400 .truncate(true)
401 .append(false)
402 .open("flow-texpack.log")
403 .unwrap(),
404 )
405 .apply()
406 .unwrap();
407 }
408 }
409
410 /// log command line arguments program was called with:
411 fn log_options(&self) {
412 info!("Options that will be used:");
413
414 info!("Exclude files:");
415 for path in &self.exclude_files {
416 info!("\t {}", path.display());
417 }
418
419 info!("Input files (does not include excludes above):");
420 for path in &self.input_files {
421 info!("\t {}", path.display());
422 }
423
424 info!("Load filters:");
425 for ext in &self.supported_extensions_load {
426 info!("\t {}", ext.display());
427 }
428
429 info!("Output dir: {}", self.cli_args.output.display());
430 info!("AtlasDescriptor: {:?}", self.cli_args.atlas_descriptor);
431 info!("AtlasImage: {:?}", self.cli_args.atlas_image);
432 info!("AtlasSize: {:?}", self.cli_args.atlas_size);
433 info!("LoadFilter: {:?}", self.cli_args.load_filter);
434 info!("Max atlases: {}", self.cli_args.max_atlases);
435 info!("Premultiply: {}", self.cli_args.premultiply);
436 info!("Trim: {}", self.cli_args.trim);
437 info!("Force: {}", self.cli_args.force);
438 info!("Unique: {}", self.cli_args.unique);
439 info!("Rotate: {}", self.cli_args.rotate);
440 info!("Force square: {}", self.cli_args.force_square);
441 info!("Adjust size: {}", self.cli_args.adjust_size);
442 info!("Adjust fit: {}", self.cli_args.adjust_fit);
443 info!("Generate mipmaps: {}", self.cli_args.generate_mipmaps);
444 info!("Pad: {}", self.cli_args.pad);
445 info!("Rect heuristic: {:?}", self.cli_args.rect_heuristic);
446 info!("Verbose: {:?}", self.cli_args.verbose);
447 }
448
449 /// prepare input files vector (exclude those in exclude/exclude_file if any):
450 async fn prepare(&mut self) {
451 // prepare load filters (default includes all if no args set):
452 self.prepare_load_filter();
453
454 // prepare files to exlude:
455 if let Some(exclude_vec) = self.cli_args.exclude.clone() {
456 for path in &exclude_vec {
457 self.prepare_exclude_files(path).await;
458 }
459 } else if let Some(exclude_file) = self.cli_args.exclude_file.clone() {
460 if exclude_file.is_file() {
461 self.read_exclude_file(&exclude_file).await;
462 }
463 }
464
465 // prepare input files and exclude those above if any:
466 if let Some(input_vec) = self.cli_args.input.clone() {
467 for path in &input_vec {
468 self.prepare_input_files(path).await;
469 }
470 } else if let Some(input_file) = self.cli_args.input_file.clone() {
471 if input_file.is_file() {
472 self.read_input_file(&input_file).await;
473 }
474 }
475 }
476
477 fn prepare_load_filter(&mut self) {
478 if let Some(load_filters) = self.cli_args.load_filter.clone() {
479 self.supported_extensions_load.clear();
480 for load_filter in load_filters {
481 let ext = get_load_filter_extension(load_filter);
482 self.supported_extensions_load.insert(OsString::from(ext));
483 }
484 }
485 }
486
487 /// prepare exclude files vector:
488 #[async_recursion::async_recursion]
489 async fn prepare_exclude_files(&mut self, path: &PathBuf) {
490 if path.is_file() {
491 self.add_exclude_file(path);
492 } else {
493 let mut reader = tokio::fs::read_dir(path).await.unwrap();
494 loop {
495 if let Some(f) = reader.next_entry().await.unwrap() {
496 if f.path().is_dir() {
497 self.prepare_exclude_files(&f.path()).await;
498 } else if f.path().is_file() {
499 self.add_exclude_file(&f.path());
500 }
501 } else {
502 break;
503 }
504 }
505 }
506 }
507
508 fn add_exclude_file(&mut self, path: &PathBuf) {
509 if let Some(extension) = path.extension() {
510 let ext = OsString::from(extension);
511 if self.supported_extensions_load.contains(&ext) {
512 self.exclude_files.insert(path.clone());
513 }
514 }
515 }
516
517 /// read exclude file and parse each line and put dir/file into exclude vector:
518 async fn read_exclude_file(&mut self, exclude_file: &PathBuf) {
519 let contents = tokio::fs::read_to_string(exclude_file).await.unwrap();
520
521 for line in contents.lines() {
522 let path = PathBuf::from(line);
523
524 if path.is_file() {
525 self.exclude_files.insert(path.clone());
526 } else {
527 self.prepare_exclude_files(&path).await;
528 }
529 }
530 }
531
532 /// prepare input files vector (exclude those in exclude_files if any and also filter out so
533 /// that only the supported extensions is included):
534 #[async_recursion::async_recursion]
535 async fn prepare_input_files(&mut self, path: &PathBuf) {
536 if path.is_file() {
537 self.add_input_file(path);
538 } else {
539 let mut reader = tokio::fs::read_dir(path).await.unwrap();
540 loop {
541 if let Some(f) = reader.next_entry().await.unwrap() {
542 if f.path().is_dir() {
543 self.prepare_input_files(&f.path()).await;
544 } else if f.path().is_file() {
545 self.add_input_file(&f.path());
546 }
547 } else {
548 break;
549 }
550 }
551 }
552 }
553
554 /// add input file if it's extension is valid and not to be excluded:
555 fn add_input_file(&mut self, path: &PathBuf) {
556 if let Some(extension) = path.extension() {
557 let ext = OsString::from(extension);
558 if self.supported_extensions_load.contains(&ext) {
559 if !self.exclude_files.contains(path) {
560 self.input_files.insert(path.clone());
561
562 // hash each value if hashing mode:
563 if !self.cli_args.force {
564 let data = std::fs::read(path).unwrap();
565 data.hash(&mut self.hasher);
566 }
567 }
568 }
569 }
570 }
571
572 /// read include file and parse each line and put dir/file into include vector:
573 async fn read_input_file(&mut self, input_file: &PathBuf) {
574 let contents = tokio::fs::read_to_string(input_file).await.unwrap();
575
576 for line in contents.lines() {
577 let path = PathBuf::from(line);
578
579 if path.is_file() {
580 self.input_files.insert(path.clone());
581 } else {
582 self.prepare_input_files(&path).await;
583 }
584 }
585 }
586
587 /// compares current hash value with previous value if any:
588 async fn check_input_hash(&mut self) -> bool {
589 self.hash_value = self.hasher.finish();
590 let old_hash_value = self.get_old_input_hash().await;
591
592 info!(
593 "Hash value new: {} old: {}",
594 self.hash_value, old_hash_value
595 );
596
597 if !self.cli_args.force && self.cli_args.verbose {
598 self.console.print("[dim]Checked input hash[/]");
599 }
600
601 if self.hash_value == old_hash_value {
602 info!("Identical hash value. No need to continue...");
603 return true;
604 }
605
606 return false;
607 }
608
609 /// returns the hash value stored on disk if any exists:
610 async fn get_old_input_hash(&self) -> u64 {
611 let file_path = format!("{}.hash", self.cli_args.output.display());
612 let result = tokio::fs::read_to_string(file_path).await;
613
614 let old_hash_value = match result {
615 Ok(old_hash_value) => old_hash_value.parse().unwrap(),
616 Err(_) => 0,
617 };
618
619 return old_hash_value;
620 }
621
622 /// removes all atlas related files from previous run with same name if any exists:
623 fn remove_old_files(&self) {
624 let hash_file_path = PathBuf::from(format!("{}.hash", self.cli_args.output.display()));
625 let json_file_path = PathBuf::from(format!("{}.json", self.cli_args.output.display()));
626 let txt_file_path = PathBuf::from(format!("{}.txt", self.cli_args.output.display()));
627
628 if exists_file(&hash_file_path) {
629 remove_file(&hash_file_path);
630 }
631
632 if exists_file(&json_file_path) {
633 remove_file(&json_file_path);
634 }
635
636 if exists_file(&txt_file_path) {
637 remove_file(&txt_file_path);
638 }
639
640 let mut removed = false;
641 for i in 0..4096 {
642 for img_ext in &self.supported_extensions_save {
643 let atlas_file_path = PathBuf::from(format!(
644 "{}{}.{}",
645 self.cli_args.output.display(),
646 i,
647 img_ext.display()
648 ));
649 if exists_file(&atlas_file_path) {
650 remove_file(&atlas_file_path);
651 removed = true;
652 }
653 }
654 if !removed {
655 break;
656 }
657 }
658 }
659
660 /// loads all textures in input_files vector as separate async tasks and then store each in
661 /// textures vector:
662 async fn load_textures(&mut self) {
663 let mut join_handles: Vec<task::JoinHandle<Texture>> = Vec::new();
664 for path in &self.input_files {
665 join_handles.push(tokio::spawn(load_texture(
666 path.clone(),
667 self.cli_args.premultiply,
668 self.cli_args.trim,
669 self.cli_args.adjust_fit,
670 self.cli_args.pad.try_into().unwrap(),
671 self.cli_args.atlas_size as u32,
672 )));
673 }
674
675 if self.cli_args.verbose {
676 if let Ok(_status) = Status::new(&self.console, "Loading textures...") {
677 for join_handle in join_handles {
678 self.textures.push(join_handle.await.unwrap());
679 }
680 }
681 } else {
682 for join_handle in join_handles {
683 self.textures.push(join_handle.await.unwrap());
684 }
685 }
686
687 if self.cli_args.verbose {
688 self.console.print("[dim]Loaded textures[/]");
689 }
690 }
691
692 /// sort all textures by area (from largest to smallest):
693 fn sort_textures(&mut self) {
694 if self.cli_args.verbose {
695 if let Ok(_status) = Status::new(&self.console, "Sorting textures by area...") {
696 self.textures
697 .sort_by(|a, b| a.get_area().cmp(&b.get_area()));
698 }
699 } else {
700 self.textures
701 .sort_by(|a, b| a.get_area().cmp(&b.get_area()));
702 }
703
704 if self.cli_args.verbose {
705 self.console.print("[dim]Sorted textures[/]");
706 }
707 }
708
709 /// pack all loaded textures into 1-n bins of atlas size and store each packer in packers
710 /// vector as those will be used to save atlas images/descriptor later as separate async tasks:
711 fn pack_textures(&mut self) {
712 if self.cli_args.verbose {
713 if let Ok(_status) = Status::new(&self.console, "Packing textures...") {
714 self.pack();
715 }
716 } else {
717 self.pack();
718 }
719
720 if self.cli_args.verbose {
721 self.console.print("[dim]Packed textures[/]");
722 }
723 }
724
725 /// pack textures into each packer of atlas size and remove each packed texture from `textures`
726 /// vector and create as many packers as needed up until `max_atlases` or if texture doesn't fit
727 /// in given `atlas size`:
728 fn pack(&mut self) {
729 while !self.textures.is_empty() {
730 let width: u32 = self.cli_args.atlas_size as u32;
731 let height = width;
732
733 let mut packer = Packer::new(
734 width,
735 height,
736 self.cli_args.pad as i32,
737 self.cli_args.generate_mipmaps,
738 )
739 .unwrap();
740
741 let heuristic = self.convert_rect_heuristic(&self.cli_args.rect_heuristic);
742 packer.pack(
743 &mut self.textures,
744 self.cli_args.unique,
745 self.cli_args.rotate,
746 self.cli_args.force_square,
747 self.cli_args.adjust_size,
748 heuristic,
749 );
750
751 self.packers.push(packer.shared());
752
753 if self.packers.len() > self.cli_args.max_atlases as usize {
754 panic!(
755 "Packing failed. There is a limit of {} atlases being created. Use a larger atlas output size (--atlas-size SIZE)",
756 self.cli_args.max_atlases
757 );
758 }
759
760 let lock = self.packers.last().unwrap().state.lock().unwrap();
761 if lock.textures.is_empty() {
762 panic!(
763 "Packing failed: Could not fit texture {}",
764 self.textures.last().unwrap().file_name
765 );
766 }
767 }
768 }
769
770 /// saves all atlas images to disk:
771 async fn save_atlas_images(&self) {
772 if self.cli_args.verbose {
773 if let Ok(_status) = Status::new(&self.console, "Writing atlas images...") {
774 self.save_images().await;
775 }
776 } else {
777 self.save_images().await;
778 }
779 }
780
781 /// saves all atlas images (an async task for each save operation to speed things up):
782 async fn save_images(&self) {
783 let image_extension = get_atlas_image_extension(self.cli_args.atlas_image);
784 let mut join_handles: Vec<task::JoinHandle<PathBuf>> = Vec::new();
785
786 for i in 0..self.packers.len() {
787 let file_path = PathBuf::from(format!(
788 "{}{}.{}",
789 self.cli_args.output.display(),
790 i,
791 image_extension
792 ));
793
794 if let Some(packer) = self.packers.get(i) {
795 join_handles.push(tokio::spawn(save_image(
796 file_path.clone(),
797 packer.clone(),
798 self.cli_args.atlas_image,
799 )));
800 }
801 }
802
803 for join_handle in join_handles {
804 let file_path = join_handle.await.unwrap();
805 if self.cli_args.verbose {
806 let msg = format!("Wrote '{}'", file_path.display());
807 info!("{}", msg);
808 self.console.print(&format!("[dim]{}[/]", msg));
809 }
810 }
811 }
812
813 /// saves atlas descriptor to disk:
814 fn save_atlas_descriptor(&self) {
815 match self.cli_args.atlas_descriptor {
816 AtlasDescriptor::Json => self.save_atlas_json(),
817 AtlasDescriptor::Txt => self.save_atlas_txt(),
818 AtlasDescriptor::TxtDesc => self.save_atlas_txt(),
819 }
820 }
821
822 /// saves atlas descriptor in JSON format:
823 fn save_atlas_json(&self) {
824 let file_path = PathBuf::from(format!("{}.json", self.cli_args.output.display()));
825 let mut file = std::fs::File::create(file_path.clone()).unwrap();
826
827 file.write(String::from("{\n").as_bytes()).unwrap();
828 file.write(String::from("\t\"ImageAtlas\":\n").as_bytes())
829 .unwrap();
830 file.write(String::from("\t{\n").as_bytes()).unwrap();
831
832 // info part:
833 file.write(String::from("\t\t\"info\":\n").as_bytes())
834 .unwrap();
835 file.write(String::from("\t\t{\n").as_bytes()).unwrap();
836 file.write(format!("\t\t\t\"numberOfAtlasImages\": {},\n", self.packers.len()).as_bytes())
837 .unwrap();
838 file.write(format!("\t\t\t\"generatedWith\": \"{}\"\n", NAME).as_bytes())
839 .unwrap();
840 file.write(String::from("\t\t},\n").as_bytes()).unwrap();
841 file.write(String::from("\t\t\"AtlasImage\":\n").as_bytes())
842 .unwrap();
843 file.write(String::from("\t\t[\n").as_bytes()).unwrap();
844
845 for i in 0..self.packers.len() {
846 let img_ext = get_atlas_image_extension(self.cli_args.atlas_image);
847 let file_path_stripped =
848 PathBuf::from(format!("{}{}", self.cli_args.output.display(), i));
849
850 if let Some(packer) = self.packers.get(i) {
851 if i > 0 {
852 file.write(String::from(",\n").as_bytes()).unwrap();
853 }
854
855 if let Some(file_name) = file_path_stripped.file_name() {
856 if let Some(file_name_str) = file_name.to_str() {
857 packer.save_json(&mut file, file_name_str, &img_ext);
858 }
859 }
860 }
861 }
862 file.write(String::from("\n\t\t]\n").as_bytes()).unwrap();
863 file.write(String::from("\t}\n").as_bytes()).unwrap();
864 file.write(String::from("}\n").as_bytes()).unwrap();
865
866 if self.cli_args.verbose {
867 let msg = format!("Wrote '{}'", file_path.display());
868 info!("{}", msg);
869 self.console.print(&format!("[dim]{}[/]", msg));
870 }
871 }
872
873 /// saves atlas descriptor in TXT format:
874 fn save_atlas_txt(&self) {
875 let file_path = PathBuf::from(format!("{}.txt", self.cli_args.output.display()));
876 let mut file = std::fs::File::create(file_path.clone()).unwrap();
877
878 if self.cli_args.atlas_descriptor == AtlasDescriptor::TxtDesc {
879 self.write_txt_header(&mut file);
880 }
881
882 // info part:
883 file.write(format!("{},{}\n", self.packers.len(), NAME).as_bytes())
884 .unwrap();
885
886 for i in 0..self.packers.len() {
887 let img_ext = get_atlas_image_extension(self.cli_args.atlas_image);
888 let file_path_stripped =
889 PathBuf::from(format!("{}{}", self.cli_args.output.display(), i));
890
891 if let Some(packer) = self.packers.get(i) {
892 if let Some(file_name) = file_path_stripped.file_name() {
893 if let Some(file_name_str) = file_name.to_str() {
894 packer.save_txt(&mut file, file_name_str, &img_ext);
895 }
896 }
897 }
898 }
899
900 if self.cli_args.verbose {
901 let msg = format!("Wrote '{}'", file_path.display());
902 info!("{}", msg);
903 self.console.print(&format!("[dim]{}[/]", msg));
904 }
905 }
906
907 /// writes the description header for TXT atlas descriptor:
908 fn write_txt_header(&self, file: &mut fs::File) {
909 file.write(String::from("/*\n").as_bytes()).unwrap();
910 file.write(
911 String::from("\t ************************************************\n").as_bytes(),
912 )
913 .unwrap();
914 file.write(format!("\t * Generated with: {}\n", NAME).as_bytes())
915 .unwrap();
916 file.write(
917 String::from("\t ************************************************\n").as_bytes(),
918 )
919 .unwrap();
920 file.write(String::from("\n").as_bytes()).unwrap();
921 file.write(
922 String::from("\t ************************************************\n").as_bytes(),
923 )
924 .unwrap();
925 file.write(String::from("\t * Format description:\n").as_bytes())
926 .unwrap();
927 file.write(
928 String::from("\t ************************************************\n").as_bytes(),
929 )
930 .unwrap();
931 file.write(String::from("\t [info]\n").as_bytes()).unwrap();
932 file.write(String::from("\t numberOfAtlasImages,generatedWith\n").as_bytes())
933 .unwrap();
934 file.write(String::from("\n").as_bytes()).unwrap();
935 file.write(String::from("\t [AtlasImage (repeated numberOfAtlasImages)]\n").as_bytes())
936 .unwrap();
937 file.write(String::from("\t atlasImageName,numberOfImages,atlasImageWidth,atlasImageHeight,generateMipMaps\n").as_bytes())
938 .unwrap();
939 file.write(String::from("\n").as_bytes()).unwrap();
940 file.write(String::from("\t [Image (repeated numberOfImages)]\n").as_bytes())
941 .unwrap();
942 file.write(String::from("\t name,x,y,w,h,trimmed,rotated,fx,fy,fw,fh (NOTE: fx,fy,fw,fh valid if trimmed==1)\n").as_bytes())
943 .unwrap();
944 file.write(String::from("\n").as_bytes()).unwrap();
945 file.write(String::from("\t Text format example:\n").as_bytes())
946 .unwrap();
947 file.write(String::from("\t [info]\n").as_bytes()).unwrap();
948 file.write(String::from("\t [AtlasImage]\n").as_bytes())
949 .unwrap();
950 file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
951 file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
952 file.write(String::from("\t ...\n").as_bytes()).unwrap();
953 file.write(String::from("\t [AtlasImage]\n").as_bytes())
954 .unwrap();
955 file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
956 file.write(String::from("\t [Image]\n").as_bytes()).unwrap();
957 file.write(String::from("\t ...\n").as_bytes()).unwrap();
958 file.write(String::from("*/@\n").as_bytes()).unwrap();
959 }
960
961 /// converts rect heuristic to the enum used in flow_rbp:
962 fn convert_rect_heuristic(&self, heuristic: &RectHeuristic) -> FreeRectHeuristic {
963 let output_heuristic = match heuristic {
964 RectHeuristic::ShortSideFit => FreeRectHeuristic::ShortSideFit,
965 RectHeuristic::LongSideFit => FreeRectHeuristic::LongSideFit,
966 RectHeuristic::AreaFit => FreeRectHeuristic::AreaFit,
967 RectHeuristic::BottomLeft => FreeRectHeuristic::BottomLeft,
968 RectHeuristic::ContactPoint => FreeRectHeuristic::ContactPoint,
969 };
970
971 return output_heuristic;
972 }
973
974 /// saves input hash value to disk:
975 fn save_input_hash(&self) {
976 let file_path = PathBuf::from(format!("{}.hash", self.cli_args.output.display()));
977 let data = format!("{}", self.hash_value);
978
979 write_file_sync(&file_path, &data.as_bytes()).unwrap();
980
981 if self.cli_args.verbose {
982 self.console
983 .print(&format!("[dim]Wrote '{}'[/]", file_path.display()));
984 }
985 }
986 }
987
988 /// Get the atlas image extension for given `atlas_image`.
989 ///
990 /// # Arguments
991 ///
992 /// * `atlas_image` - is the atlas image type.
993 pub fn get_atlas_image_extension(atlas_image: AtlasImage) -> String {
994 let image_extension = match atlas_image {
995 AtlasImage::Png => String::from("png"),
996 AtlasImage::Tga => String::from("tga"),
997 AtlasImage::Tiff => String::from("tiff"),
998 AtlasImage::Webp => String::from("webp"),
999 };
1000
1001 return image_extension;
1002 }
1003
1004 /// Get the load filter extension for given `load_filter`.
1005 ///
1006 /// # Arguments
1007 ///
1008 /// * `load_filter` - is the load filter.
1009 pub fn get_load_filter_extension(load_filter: LoadFilter) -> String {
1010 let image_extension = match load_filter {
1011 LoadFilter::Bmp => String::from("bmp"),
1012 LoadFilter::Hdr => String::from("hdr"),
1013 LoadFilter::Jpg => String::from("jpg"),
1014 LoadFilter::Png => String::from("png"),
1015 LoadFilter::Tga => String::from("tga"),
1016 LoadFilter::Tiff => String::from("tiff"),
1017 LoadFilter::Webp => String::from("webp"),
1018 };
1019
1020 return image_extension;
1021 }
1022
1023 /// Create given `dir` recursively.
1024 ///
1025 /// # Arguments
1026 ///
1027 /// * `dir` - is the directory to create recursively.
1028 ///
1029 /// # Panics
1030 ///
1031 /// If failed to create given directory.
1032 pub fn create_dir_all<'a>(dir: &'a PathBuf) {
1033 match fs::create_dir_all(dir) {
1034 Ok(()) => info!("Created dir: '{}'", dir.display()),
1035 Err(err) => panic!(
1036 "Failed to create dir: '{}'. Error msg: '{}'",
1037 dir.display(),
1038 err
1039 ),
1040 };
1041 }
1042
1043 /// Remove given `dir` recursively.
1044 ///
1045 /// # Arguments
1046 ///
1047 /// * `dir` - is the directory to remove recursively.
1048 ///
1049 /// # Panics
1050 ///
1051 /// If failed to remove given directory.
1052 pub fn remove_dir_all<'a>(dir: &'a PathBuf) {
1053 match fs::remove_dir_all(dir) {
1054 Ok(()) => info!("Removed dir: '{}'", dir.display()),
1055 Err(err) => panic!(
1056 "Failed to remove dir: '{}'. Error msg: '{}'",
1057 dir.display(),
1058 err
1059 ),
1060 };
1061 }
1062
1063 /// Remove given `file_path`.
1064 ///
1065 /// # Arguments
1066 ///
1067 /// * `file_path` - is the path to file to remove.
1068 ///
1069 /// # Panics
1070 ///
1071 /// If failed to remove given `file_path`.
1072 pub fn remove_file<'a>(file_path: &'a PathBuf) {
1073 match fs::remove_file(file_path) {
1074 Ok(()) => info!("Removed file: '{}'", file_path.display()),
1075 Err(err) => panic!(
1076 "Failed to remove file: '{}'. Error msg: '{}'",
1077 file_path.display(),
1078 err
1079 ),
1080 };
1081 }
1082
1083 /// Determine whether given `dir` exists.
1084 ///
1085 /// # Arguments
1086 ///
1087 /// * `dir` - is the directory.
1088 pub fn exists_dir<'a>(dir: &'a PathBuf) -> bool {
1089 return dir.as_path().exists();
1090 }
1091
1092 /// Determine whether given `file_path` exists.
1093 ///
1094 /// # Arguments
1095 ///
1096 /// * `file_path` - is the file path.
1097 pub fn exists_file<'a>(file_path: &'a PathBuf) -> bool {
1098 return file_path.is_file();
1099 }
1100
1101 /// Write `data` to given `file_path`.
1102 ///
1103 /// # Arguments
1104 ///
1105 /// * `file_path` - is the file path.
1106 /// * `data` - is the data to write.
1107 pub fn write_file_sync<'a>(file_path: &'a PathBuf, data: &'a [u8]) -> io::Result<()> {
1108 // create output file:
1109 let mut file = std::fs::File::create(file_path)?;
1110
1111 // write data to file:
1112 file.write_all(data)?;
1113
1114 info!("Wrote '{}' successfully", file_path.display());
1115 Ok(())
1116 }
1117
1118 /// load an individual texture from disk and return it (used for async tasks):
1119 async fn load_texture(
1120 file_path: PathBuf,
1121 premultiply: bool,
1122 trim: bool,
1123 adjust_fit: bool,
1124 pad: u32,
1125 atlas_size: u32,
1126 ) -> Texture {
1127 let mut texture = Texture::new();
1128
1129 texture.load(&file_path, premultiply, trim, adjust_fit, pad, atlas_size);
1130
1131 return texture;
1132 }
1133
1134 /// saves an individual atlas image to disk (used for async tasks):
1135 async fn save_image(file_path: PathBuf, packer: Arc<Packer>, image_type: AtlasImage) -> PathBuf {
1136 packer.save_image(&file_path, image_type);
1137 return file_path;
1138 }