1 // flow-texpack: A program that will allow you to generate texture atlas.
2 // zlib License (see LICENSE)
7 use flow_rbp::FreeRectHeuristic;
8 use rich_rust::console::Console;
9 use rich_rust::interactive::Status;
11 use clap::{Parser, ValueEnum};
16 use std::time::Instant;
20 use std::collections::HashSet;
21 use std::ffi::OsString;
23 use std::hash::{DefaultHasher, Hash, Hasher};
25 use std::path::PathBuf;
28 /// name of the application:
29 const NAME: &'static str = "https://luflow.net/git-repos/flow-texpack.git";
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.";
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.
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";
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
56 /// atlas descriptor type: Txt
58 /// atlas descriptor type: Txt (with description header)
62 /// Specifies the different atlas image types.
63 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
65 /// atlas image type: PNG
67 /// atlas image type: TGA
69 /// atlas image type: TIFF
71 /// atlas image type: Webp
75 /// Specifies the different load filter types.
76 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
78 /// load filter type: BMP
80 /// load filter type: HDR
82 /// load filter type: JPG
84 /// load filter type: PNG
86 /// load filter type: TGA
88 /// load filter type: TIFF
90 /// load filter type: Webp
94 /// Specifies the different atlas output sizes.
95 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
97 /// power-of-two 64x64 size:
99 /// power-of-two 128x128 size:
101 /// power-of-two 256x256 size:
103 /// power-of-two 512x512 size:
105 /// power-of-two 1024x1024 size:
107 /// power-of-two 2048x2048 size:
109 /// power-of-two 4096x4096 size:
111 /// power-of-two 8192x8192 size:
115 /// Specifies the different heuristic types.
116 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
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.
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.
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.
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.
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.
140 #[derive(Parser, Debug)]
141 #[command(version, about = ABOUT, long_about = LONG_ABOUT)]
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>>,
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>,
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>>,
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>,
162 #[arg(short = 'o', long = "output", requires = "input_group")]
166 #[arg(long = "atlas-descriptor", value_enum, default_value_t = AtlasDescriptor::Json)]
167 atlas_descriptor: AtlasDescriptor,
170 #[arg(long = "atlas-image", value_enum, default_value_t = AtlasImage::Png)]
171 atlas_image: AtlasImage,
173 /// max atlas output size POT
174 #[arg(long = "atlas-size", value_enum, default_value_t = AtlasSize::Pot1024)]
175 atlas_size: AtlasSize,
178 #[arg(long = "load-filter", value_enum, value_delimiter = ' ', num_args = 1..)]
179 load_filter: Option<Vec<LoadFilter>>,
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))]
185 /// enable premultiply the pixels of the textures by their alpha channel
186 #[arg(short = 'm', long = "premultiply")]
189 /// enable trim excess transparency off the textures
190 #[arg(short = 't', long = "trim")]
193 /// enable force packer to re-pack (ignore stored hashes)
194 #[arg(short = 'f', long = "force")]
197 /// enable remove duplicate textures from the atlas
198 #[arg(short = 'u', long = "unique")]
201 /// enable rotation of textures (90 degrees clockwise)
202 #[arg(short = 'r', long = "rotate")]
205 /// enable force atlas POT square size
206 #[arg(long = "force-square")]
209 /// enable adjust atlas size automatically so that texture will fit
210 #[arg(long = "adjust-size")]
213 /// enable adjust texture size so that it will fit given atlas size
214 #[arg(long = "adjust-fit")]
217 /// enable generation of mipmaps (rendering hint)
218 #[arg(long = "generate-mipmaps")]
219 generate_mipmaps: bool,
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) )]
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,
229 /// enable verbose output of progress
230 #[arg(short = 'v', long = "verbose")]
233 /// enable logging to log file 'flow-texpack.log'
234 #[arg(short = 'l', long = "log")]
238 /// Specifies the properties of a App.
240 /// is the cli arguments program is called with:
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:
263 /// Instantiates a new App instance.
267 /// If initialization failed.
268 pub fn new() -> 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"),
283 supported_extensions_save: HashSet::from([
284 OsString::from("png"),
285 OsString::from("tga"),
286 OsString::from("tiff"),
287 OsString::from("webp"),
289 textures: Vec::new(),
291 hasher: DefaultHasher::new(),
296 /// Execute the main application loop.
300 /// If something unexpected happens.
301 pub async fn run(&mut self) {
303 let start_time = Instant::now();
306 self.initialize().await;
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();
313 // load all input textures:
314 self.load_textures().await;
316 // sort textures by area (largest area first):
317 self.sort_textures();
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());
326 // pack the textures:
327 self.pack_textures();
329 // save atlas image/s:
330 self.save_atlas_images().await;
332 // save atlas descriptor:
333 self.save_atlas_descriptor();
335 // save new hash value:
336 self.save_input_hash();
338 if self.cli_args.verbose {
339 self.console.print("[dim]No input files...[/]");
343 if self.cli_args.verbose {
345 .print("[dim]Identical hash value. No need to continue...[/]");
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()
356 self.console.print("[green]Done![/]");
360 /// initialize logger and prepare input files vector:
361 async fn initialize(&mut self) {
362 // initialize logger:
365 // prepare input files:
366 self.prepare().await;
368 // log cli_args options:
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;
381 return self.check_input_hash().await;
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))
395 .level(log::LevelFilter::Debug)
397 std::fs::OpenOptions::new()
402 .open("flow-texpack.log")
410 /// log command line arguments program was called with:
411 fn log_options(&self) {
412 info!("Options that will be used:");
414 info!("Exclude files:");
415 for path in &self.exclude_files {
416 info!("\t {}", path.display());
419 info!("Input files (does not include excludes above):");
420 for path in &self.input_files {
421 info!("\t {}", path.display());
424 info!("Load filters:");
425 for ext in &self.supported_extensions_load {
426 info!("\t {}", ext.display());
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);
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();
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;
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;
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;
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;
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));
487 /// prepare exclude files vector:
488 #[async_recursion::async_recursion]
489 async fn prepare_exclude_files(&mut self, path: &PathBuf) {
491 self.add_exclude_file(path);
493 let mut reader = tokio::fs::read_dir(path).await.unwrap();
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());
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());
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();
521 for line in contents.lines() {
522 let path = PathBuf::from(line);
525 self.exclude_files.insert(path.clone());
527 self.prepare_exclude_files(&path).await;
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) {
537 self.add_input_file(path);
539 let mut reader = tokio::fs::read_dir(path).await.unwrap();
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());
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());
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);
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();
576 for line in contents.lines() {
577 let path = PathBuf::from(line);
580 self.input_files.insert(path.clone());
582 self.prepare_input_files(&path).await;
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;
593 "Hash value new: {} old: {}",
594 self.hash_value, old_hash_value
597 if !self.cli_args.force && self.cli_args.verbose {
598 self.console.print("[dim]Checked input hash[/]");
601 if self.hash_value == old_hash_value {
602 info!("Identical hash value. No need to continue...");
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;
614 let old_hash_value = match result {
615 Ok(old_hash_value) => old_hash_value.parse().unwrap(),
619 return old_hash_value;
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()));
628 if exists_file(&hash_file_path) {
629 remove_file(&hash_file_path);
632 if exists_file(&json_file_path) {
633 remove_file(&json_file_path);
636 if exists_file(&txt_file_path) {
637 remove_file(&txt_file_path);
640 let mut removed = false;
642 for img_ext in &self.supported_extensions_save {
643 let atlas_file_path = PathBuf::from(format!(
645 self.cli_args.output.display(),
649 if exists_file(&atlas_file_path) {
650 remove_file(&atlas_file_path);
660 /// loads all textures in input_files vector as separate async tasks and then store each in
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(
667 self.cli_args.premultiply,
669 self.cli_args.adjust_fit,
670 self.cli_args.pad.try_into().unwrap(),
671 self.cli_args.atlas_size as u32,
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());
682 for join_handle in join_handles {
683 self.textures.push(join_handle.await.unwrap());
687 if self.cli_args.verbose {
688 self.console.print("[dim]Loaded textures[/]");
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...") {
697 .sort_by(|a, b| a.get_area().cmp(&b.get_area()));
701 .sort_by(|a, b| a.get_area().cmp(&b.get_area()));
704 if self.cli_args.verbose {
705 self.console.print("[dim]Sorted textures[/]");
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...") {
720 if self.cli_args.verbose {
721 self.console.print("[dim]Packed textures[/]");
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`:
729 while !self.textures.is_empty() {
730 let width: u32 = self.cli_args.atlas_size as u32;
733 let mut packer = Packer::new(
736 self.cli_args.pad as i32,
737 self.cli_args.generate_mipmaps,
741 let heuristic = self.convert_rect_heuristic(&self.cli_args.rect_heuristic);
744 self.cli_args.unique,
745 self.cli_args.rotate,
746 self.cli_args.force_square,
747 self.cli_args.adjust_size,
751 self.packers.push(packer.shared());
753 if self.packers.len() > self.cli_args.max_atlases as usize {
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
760 let lock = self.packers.last().unwrap().state.lock().unwrap();
761 if lock.textures.is_empty() {
763 "Packing failed: Could not fit texture {}",
764 self.textures.last().unwrap().file_name
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;
777 self.save_images().await;
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();
786 for i in 0..self.packers.len() {
787 let file_path = PathBuf::from(format!(
789 self.cli_args.output.display(),
794 if let Some(packer) = self.packers.get(i) {
795 join_handles.push(tokio::spawn(save_image(
798 self.cli_args.atlas_image,
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());
808 self.console.print(&format!("[dim]{}[/]", msg));
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(),
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();
827 file.write(String::from("{\n").as_bytes()).unwrap();
828 file.write(String::from("\t\"ImageAtlas\":\n").as_bytes())
830 file.write(String::from("\t{\n").as_bytes()).unwrap();
833 file.write(String::from("\t\t\"info\":\n").as_bytes())
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())
838 file.write(format!("\t\t\t\"generatedWith\": \"{}\"\n", NAME).as_bytes())
840 file.write(String::from("\t\t},\n").as_bytes()).unwrap();
841 file.write(String::from("\t\t\"AtlasImage\":\n").as_bytes())
843 file.write(String::from("\t\t[\n").as_bytes()).unwrap();
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));
850 if let Some(packer) = self.packers.get(i) {
852 file.write(String::from(",\n").as_bytes()).unwrap();
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);
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();
866 if self.cli_args.verbose {
867 let msg = format!("Wrote '{}'", file_path.display());
869 self.console.print(&format!("[dim]{}[/]", msg));
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();
878 if self.cli_args.atlas_descriptor == AtlasDescriptor::TxtDesc {
879 self.write_txt_header(&mut file);
883 file.write(format!("{},{}\n", self.packers.len(), NAME).as_bytes())
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));
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);
900 if self.cli_args.verbose {
901 let msg = format!("Wrote '{}'", file_path.display());
903 self.console.print(&format!("[dim]{}[/]", msg));
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();
911 String::from("\t ************************************************\n").as_bytes(),
914 file.write(format!("\t * Generated with: {}\n", NAME).as_bytes())
917 String::from("\t ************************************************\n").as_bytes(),
920 file.write(String::from("\n").as_bytes()).unwrap();
922 String::from("\t ************************************************\n").as_bytes(),
925 file.write(String::from("\t * Format description:\n").as_bytes())
928 String::from("\t ************************************************\n").as_bytes(),
931 file.write(String::from("\t [info]\n").as_bytes()).unwrap();
932 file.write(String::from("\t numberOfAtlasImages,generatedWith\n").as_bytes())
934 file.write(String::from("\n").as_bytes()).unwrap();
935 file.write(String::from("\t [AtlasImage (repeated numberOfAtlasImages)]\n").as_bytes())
937 file.write(String::from("\t atlasImageName,numberOfImages,atlasImageWidth,atlasImageHeight,generateMipMaps\n").as_bytes())
939 file.write(String::from("\n").as_bytes()).unwrap();
940 file.write(String::from("\t [Image (repeated numberOfImages)]\n").as_bytes())
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())
944 file.write(String::from("\n").as_bytes()).unwrap();
945 file.write(String::from("\t Text format example:\n").as_bytes())
947 file.write(String::from("\t [info]\n").as_bytes()).unwrap();
948 file.write(String::from("\t [AtlasImage]\n").as_bytes())
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())
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();
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,
971 return output_heuristic;
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);
979 write_file_sync(&file_path, &data.as_bytes()).unwrap();
981 if self.cli_args.verbose {
983 .print(&format!("[dim]Wrote '{}'[/]", file_path.display()));
988 /// Get the atlas image extension for given `atlas_image`.
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"),
1001 return image_extension;
1004 /// Get the load filter extension for given `load_filter`.
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"),
1020 return image_extension;
1023 /// Create given `dir` recursively.
1027 /// * `dir` - is the directory to create recursively.
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()),
1036 "Failed to create dir: '{}'. Error msg: '{}'",
1043 /// Remove given `dir` recursively.
1047 /// * `dir` - is the directory to remove recursively.
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()),
1056 "Failed to remove dir: '{}'. Error msg: '{}'",
1063 /// Remove given `file_path`.
1067 /// * `file_path` - is the path to file to remove.
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()),
1076 "Failed to remove file: '{}'. Error msg: '{}'",
1077 file_path.display(),
1083 /// Determine whether given `dir` exists.
1087 /// * `dir` - is the directory.
1088 pub fn exists_dir<'a>(dir: &'a PathBuf) -> bool {
1089 return dir.as_path().exists();
1092 /// Determine whether given `file_path` exists.
1096 /// * `file_path` - is the file path.
1097 pub fn exists_file<'a>(file_path: &'a PathBuf) -> bool {
1098 return file_path.is_file();
1101 /// Write `data` to given `file_path`.
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)?;
1111 // write data to file:
1112 file.write_all(data)?;
1114 info!("Wrote '{}' successfully", file_path.display());
1118 /// load an individual texture from disk and return it (used for async tasks):
1119 async fn load_texture(
1127 let mut texture = Texture::new();
1129 texture.load(&file_path, premultiply, trim, adjust_fit, pad, atlas_size);
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);