use crate::Packer;
use crate::Texture;
use flow_rectpack::FreeRectHeuristic;
use rich_rust::console::Console;
use rich_rust::interactive::Status;
use clap::{Parser, ValueEnum};
use tokio::io;
use tokio::task;
use std::time::Instant;
use log::info;
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
const NAME: &str = "https://github.com/hfsoulz/flow-texpack.git";
const ABOUT: &str = "
flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR,
JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that
can be read by a game.";
const LONG_ABOUT: &str = "
flow-texpack is a program that will allow you to generate texture atlas from input images (BMP, HDR,
JPG, PNG, TGA, TIFF, WEBP). The application generates both texture atlas and descriptions file that
can be read by a game.
Examples:
flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v
flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r -p 2 -v --load-filter png tga
flow-texpack -i data -e data/tiles -o out/atlas -m -t -u -r --atlas-size pot2048 --rect-heuristic area-fit -v
flow-texpack --input-file input.txt --exlude-file exclude.txt -o out/atlas -v
flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-size -v
flow-texpack -i data/characters data/tiles -o out/atlas -m -t -u -r --adjust-fit -v";
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum AtlasDescriptor {
Json,
Txt,
TxtDesc,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum AtlasImage {
Png,
Tga,
Tiff,
Webp,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum LoadFilter {
Bmp,
Hdr,
Jpg,
Png,
Tga,
Tiff,
Webp,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum AtlasSize {
Pot64 = 64,
Pot128 = 128,
Pot256 = 256,
Pot512 = 512,
Pot1024 = 1024,
Pot2048 = 2048,
Pot4096 = 4096,
Pot8192 = 8192,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum RectHeuristic {
ShortSideFit,
LongSideFit,
AreaFit,
BottomLeft,
ContactPoint,
}
#[derive(Parser, Debug)]
#[command(version, about = ABOUT, long_about = LONG_ABOUT)]
struct CliArgs {
#[arg(short = 'i', long = "input", value_delimiter = ' ', num_args = 1.., group = "input_group")]
input: Option<Vec<PathBuf>>,
#[arg(long = "input-file", group = "input_group", verbatim_doc_comment)]
input_file: Option<PathBuf>,
#[arg(short = 'e', long = "exclude", value_delimiter = ' ', num_args = 1.., group = "exclude_group")]
exclude: Option<Vec<PathBuf>>,
#[arg(long = "exclude-file", group = "exclude_group", verbatim_doc_comment)]
exclude_file: Option<PathBuf>,
#[arg(short = 'o', long = "output", requires = "input_group")]
output: PathBuf,
#[arg(long = "atlas-descriptor", value_enum, default_value_t = AtlasDescriptor::Json)]
atlas_descriptor: AtlasDescriptor,
#[arg(long = "atlas-image", value_enum, default_value_t = AtlasImage::Png)]
atlas_image: AtlasImage,
#[arg(long = "atlas-size", value_enum, default_value_t = AtlasSize::Pot1024)]
atlas_size: AtlasSize,
#[arg(long = "load-filter", value_enum, value_delimiter = ' ', num_args = 1..)]
load_filter: Option<Vec<LoadFilter>>,
#[arg(long = "max-atlases", default_value_t = 64, value_parser = clap::value_parser!(u16).range(1..=4096))]
max_atlases: u16,
#[arg(short = 'm', long = "premultiply")]
premultiply: bool,
#[arg(short = 't', long = "trim")]
trim: bool,
#[arg(short = 'f', long = "force")]
force: bool,
#[arg(short = 'u', long = "unique")]
unique: bool,
#[arg(short = 'r', long = "rotate")]
rotate: bool,
#[arg(long = "force-square")]
force_square: bool,
#[arg(long = "adjust-size")]
adjust_size: bool,
#[arg(long = "adjust-fit")]
adjust_fit: bool,
#[arg(long = "generate-mipmaps")]
generate_mipmaps: bool,
#[arg(short = 'p', long = "pad", default_value_t = 1, value_parser = clap::value_parser!(u8).range(0..=16) )]
pad: u8,
#[arg(long = "rect-heuristic", value_enum, default_value_t = RectHeuristic::LongSideFit)]
rect_heuristic: RectHeuristic,
#[arg(short = 'v', long = "verbose")]
verbose: bool,
#[arg(short = 'l', long = "log")]
log: bool,
}
pub struct App {
cli_args: CliArgs,
console: Arc<Console>,
input_files: HashSet<PathBuf>,
exclude_files: HashSet<PathBuf>,
supported_extensions_load: HashSet<OsString>,
supported_extensions_save: HashSet<OsString>,
textures: Vec<Texture>,
packers: Vec<Arc<Packer>>,
hasher: DefaultHasher,
hash_value: u64,
}
impl Default for App {
fn default() -> Self {
Self {
cli_args: CliArgs::parse(),
console: Console::new().shared(),
input_files: HashSet::new(),
exclude_files: HashSet::new(),
supported_extensions_load: HashSet::from([
OsString::from("bmp"),
OsString::from("hdr"),
OsString::from("jpg"),
OsString::from("png"),
OsString::from("tga"),
OsString::from("tiff"),
OsString::from("webp"),
]),
supported_extensions_save: HashSet::from([
OsString::from("png"),
OsString::from("tga"),
OsString::from("tiff"),
OsString::from("webp"),
]),
textures: Vec::new(),
packers: Vec::new(),
hasher: DefaultHasher::new(),
hash_value: 0,
}
}
}
impl App {
pub async fn run(&mut self) {
let start_time = Instant::now();
self.initialize().await;
if !self.identical_hash().await {
if !self.input_files.is_empty() {
self.remove_old_files();
self.load_textures().await;
self.sort_textures();
if !exists_dir(&self.cli_args.output)
&& let Some(parent_dir) = self.cli_args.output.parent()
{
create_dir_all(&parent_dir.to_path_buf());
}
self.pack_textures();
self.save_atlas_images().await;
self.save_atlas_descriptor();
self.save_input_hash();
} else {
if self.cli_args.verbose {
self.console.print("[dim]No input files...[/]");
}
}
} else {
if self.cli_args.verbose {
self.console
.print("[dim]Identical hash value. No need to continue...[/]");
}
}
if self.cli_args.verbose {
self.console.print("");
self.console.print(&format!(
"[dim]Completed in {:.1}s[/]",
start_time.elapsed().as_secs_f64()
));
self.console.print("[green]Done![/]");
}
}
async fn initialize(&mut self) {
self.init_logger();
self.prepare().await;
self.log_options();
}
async fn identical_hash(&mut self) -> bool {
if !self.cli_args.force {
if self.cli_args.verbose {
if let Ok(_status) = Status::new(&self.console, "Checking input hash...") {
return self.check_input_hash().await;
}
} else {
return self.check_input_hash().await;
}
}
false
}
fn init_logger(&self) {
if self.cli_args.log {
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!("[{}] {}", record.level(), message))
})
.level(log::LevelFilter::Debug)
.chain(
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.append(false)
.open("flow-texpack.log")
.unwrap(),
)
.apply()
.unwrap();
}
}
fn log_options(&self) {
info!("Options that will be used:");
info!("Exclude files:");
for path in &self.exclude_files {
info!("\t {}", path.display());
}
info!("Input files (does not include excludes above):");
for path in &self.input_files {
info!("\t {}", path.display());
}
info!("Load filters:");
for ext in &self.supported_extensions_load {
info!("\t {}", ext.display());
}
info!("Output dir: {}", self.cli_args.output.display());
info!("AtlasDescriptor: {:?}", self.cli_args.atlas_descriptor);
info!("AtlasImage: {:?}", self.cli_args.atlas_image);
info!("AtlasSize: {:?}", self.cli_args.atlas_size);
info!("LoadFilter: {:?}", self.cli_args.load_filter);
info!("Max atlases: {}", self.cli_args.max_atlases);
info!("Premultiply: {}", self.cli_args.premultiply);
info!("Trim: {}", self.cli_args.trim);
info!("Force: {}", self.cli_args.force);
info!("Unique: {}", self.cli_args.unique);
info!("Rotate: {}", self.cli_args.rotate);
info!("Force square: {}", self.cli_args.force_square);
info!("Adjust size: {}", self.cli_args.adjust_size);
info!("Adjust fit: {}", self.cli_args.adjust_fit);
info!("Generate mipmaps: {}", self.cli_args.generate_mipmaps);
info!("Pad: {}", self.cli_args.pad);
info!("Rect heuristic: {:?}", self.cli_args.rect_heuristic);
info!("Verbose: {:?}", self.cli_args.verbose);
}
async fn prepare(&mut self) {
self.prepare_load_filter();
if let Some(exclude_vec) = self.cli_args.exclude.clone() {
for path in &exclude_vec {
self.prepare_exclude_files(path).await;
}
} else if let Some(exclude_file) = self.cli_args.exclude_file.clone()
&& exclude_file.is_file()
{
self.read_exclude_file(&exclude_file).await;
}
if let Some(input_vec) = self.cli_args.input.clone() {
for path in &input_vec {
self.prepare_input_files(path).await;
}
} else if let Some(input_file) = self.cli_args.input_file.clone()
&& input_file.is_file()
{
self.read_input_file(&input_file).await;
}
}
fn prepare_load_filter(&mut self) {
if let Some(load_filters) = self.cli_args.load_filter.clone() {
self.supported_extensions_load.clear();
for load_filter in load_filters {
let ext = get_load_filter_extension(load_filter);
self.supported_extensions_load.insert(OsString::from(ext));
}
}
}
#[async_recursion::async_recursion]
async fn prepare_exclude_files(&mut self, path: &PathBuf) {
if path.is_file() {
self.add_exclude_file(path);
} else {
let mut reader = tokio::fs::read_dir(path).await.unwrap();
while let Some(f) = reader.next_entry().await.unwrap() {
if f.path().is_dir() {
self.prepare_exclude_files(&f.path()).await;
} else if f.path().is_file() {
self.add_exclude_file(&f.path());
}
}
}
}
fn add_exclude_file(&mut self, path: &Path) {
if let Some(extension) = path.extension() {
let ext = OsString::from(extension);
if self.supported_extensions_load.contains(&ext) {
self.exclude_files.insert(path.to_path_buf());
}
}
}
async fn read_exclude_file(&mut self, exclude_file: &PathBuf) {
let contents = tokio::fs::read_to_string(exclude_file).await.unwrap();
for line in contents.lines() {
let path = PathBuf::from(line);
if path.is_file() {
self.exclude_files.insert(path.clone());
} else {
self.prepare_exclude_files(&path).await;
}
}
}
#[async_recursion::async_recursion]
async fn prepare_input_files(&mut self, path: &PathBuf) {
if path.is_file() {
self.add_input_file(path);
} else {
let mut reader = tokio::fs::read_dir(path).await.unwrap();
while let Some(f) = reader.next_entry().await.unwrap() {
if f.path().is_dir() {
self.prepare_input_files(&f.path()).await;
} else if f.path().is_file() {
self.add_input_file(&f.path());
}
}
}
}
fn add_input_file(&mut self, path: &PathBuf) {
if let Some(extension) = path.extension() {
let ext = OsString::from(extension);
if self.supported_extensions_load.contains(&ext) && !self.exclude_files.contains(path) {
self.input_files.insert(path.clone());
if !self.cli_args.force {
let data = std::fs::read(path).unwrap();
data.hash(&mut self.hasher);
}
}
}
}
async fn read_input_file(&mut self, input_file: &PathBuf) {
let contents = tokio::fs::read_to_string(input_file).await.unwrap();
for line in contents.lines() {
let path = PathBuf::from(line);
if path.is_file() {
self.input_files.insert(path.clone());
} else {
self.prepare_input_files(&path).await;
}
}
}
async fn check_input_hash(&mut self) -> bool {
self.hash_value = self.hasher.finish();
let old_hash_value = self.get_old_input_hash().await;
info!(
"Hash value new: {} old: {}",
self.hash_value, old_hash_value
);
if !self.cli_args.force && self.cli_args.verbose {
self.console.print("[dim]Checked input hash[/]");
}
if self.hash_value == old_hash_value {
info!("Identical hash value. No need to continue...");
return true;
}
false
}
async fn get_old_input_hash(&self) -> u64 {
let file_path = format!("{}.hash", self.cli_args.output.display());
let result = tokio::fs::read_to_string(file_path).await;
match result {
Ok(old_hash_value) => old_hash_value.parse().unwrap(),
Err(_) => 0,
}
}
fn remove_old_files(&self) {
let hash_file_path = PathBuf::from(format!("{}.hash", self.cli_args.output.display()));
let json_file_path = PathBuf::from(format!("{}.json", self.cli_args.output.display()));
let txt_file_path = PathBuf::from(format!("{}.txt", self.cli_args.output.display()));
if exists_file(&hash_file_path) {
remove_file(&hash_file_path);
}
if exists_file(&json_file_path) {
remove_file(&json_file_path);
}
if exists_file(&txt_file_path) {
remove_file(&txt_file_path);
}
let mut removed = false;
for i in 0..4096 {
for img_ext in &self.supported_extensions_save {
let atlas_file_path = PathBuf::from(format!(
"{}{}.{}",
self.cli_args.output.display(),
i,
img_ext.display()
));
if exists_file(&atlas_file_path) {
remove_file(&atlas_file_path);
removed = true;
}
}
if !removed {
break;
}
}
}
async fn load_textures(&mut self) {
let mut join_handles: Vec<task::JoinHandle<Texture>> = Vec::new();
for path in &self.input_files {
join_handles.push(tokio::spawn(load_texture(
path.clone(),
self.cli_args.premultiply,
self.cli_args.trim,
self.cli_args.adjust_fit,
self.cli_args.pad.into(),
self.cli_args.atlas_size as u32,
)));
}
if self.cli_args.verbose {
if let Ok(_status) = Status::new(&self.console, "Loading textures...") {
for join_handle in join_handles {
self.textures.push(join_handle.await.unwrap());
}
}
} else {
for join_handle in join_handles {
self.textures.push(join_handle.await.unwrap());
}
}
if self.cli_args.verbose {
self.console.print("[dim]Loaded textures[/]");
}
}
fn sort_textures(&mut self) {
if self.cli_args.verbose {
if let Ok(_status) = Status::new(&self.console, "Sorting textures by area...") {
self.textures.sort_by_key(|a| a.get_area());
}
} else {
self.textures.sort_by_key(|a| a.get_area());
}
if self.cli_args.verbose {
self.console.print("[dim]Sorted textures[/]");
}
}
fn pack_textures(&mut self) {
if self.cli_args.verbose {
if let Ok(_status) = Status::new(&self.console, "Packing textures...") {
self.pack();
}
} else {
self.pack();
}
if self.cli_args.verbose {
self.console.print("[dim]Packed textures[/]");
}
}
fn pack(&mut self) {
while !self.textures.is_empty() {
let width: u32 = self.cli_args.atlas_size as u32;
let height = width;
let mut packer = Packer::new(
width,
height,
self.cli_args.pad as i32,
self.cli_args.generate_mipmaps,
)
.unwrap();
let heuristic = self.convert_rect_heuristic(&self.cli_args.rect_heuristic);
packer.pack(
&mut self.textures,
self.cli_args.unique,
self.cli_args.rotate,
self.cli_args.force_square,
self.cli_args.adjust_size,
heuristic,
);
self.packers.push(packer.shared());
if self.packers.len() > self.cli_args.max_atlases as usize {
panic!(
"Packing failed. There is a limit of {} atlases being created. Use a larger atlas output size (--atlas-size SIZE)",
self.cli_args.max_atlases
);
}
let lock = self.packers.last().unwrap().state.lock().unwrap();
if lock.textures.is_empty() {
panic!(
"Packing failed: Could not fit texture {}",
self.textures.last().unwrap().file_name
);
}
}
}
async fn save_atlas_images(&self) {
if self.cli_args.verbose {
if let Ok(_status) = Status::new(&self.console, "Writing atlas images...") {
self.save_images().await;
}
} else {
self.save_images().await;
}
}
async fn save_images(&self) {
let image_extension = get_atlas_image_extension(self.cli_args.atlas_image);
let mut join_handles: Vec<task::JoinHandle<PathBuf>> = Vec::new();
for i in 0..self.packers.len() {
let file_path = PathBuf::from(format!(
"{}{}.{}",
self.cli_args.output.display(),
i,
image_extension
));
if let Some(packer) = self.packers.get(i) {
join_handles.push(tokio::spawn(save_image(
file_path.clone(),
packer.clone(),
self.cli_args.atlas_image,
)));
}
}
for join_handle in join_handles {
let file_path = join_handle.await.unwrap();
if self.cli_args.verbose {
let msg = format!("Wrote '{}'", file_path.display());
info!("{}", msg);
self.console.print(&format!("[dim]{}[/]", msg));
}
}
}
fn save_atlas_descriptor(&self) {
match self.cli_args.atlas_descriptor {
AtlasDescriptor::Json => self.save_atlas_json(),
AtlasDescriptor::Txt => self.save_atlas_txt(),
AtlasDescriptor::TxtDesc => self.save_atlas_txt(),
}
}
fn save_atlas_json(&self) {
let file_path = PathBuf::from(format!("{}.json", self.cli_args.output.display()));
let mut file = std::fs::File::create(file_path.clone()).unwrap();
file.write_all(String::from("{\n").as_bytes()).unwrap();
file.write_all(String::from("\t\"ImageAtlas\":\n").as_bytes())
.unwrap();
file.write_all(String::from("\t{\n").as_bytes()).unwrap();
file.write_all(String::from("\t\t\"info\":\n").as_bytes())
.unwrap();
file.write_all(String::from("\t\t{\n").as_bytes()).unwrap();
file.write_all(
format!("\t\t\t\"numberOfAtlasImages\": {},\n", self.packers.len()).as_bytes(),
)
.unwrap();
file.write_all(format!("\t\t\t\"generatedWith\": \"{}\"\n", NAME).as_bytes())
.unwrap();
file.write_all(String::from("\t\t},\n").as_bytes()).unwrap();
file.write_all(String::from("\t\t\"AtlasImage\":\n").as_bytes())
.unwrap();
file.write_all(String::from("\t\t[\n").as_bytes()).unwrap();
for i in 0..self.packers.len() {
let img_ext = get_atlas_image_extension(self.cli_args.atlas_image);
let file_path_stripped =
PathBuf::from(format!("{}{}", self.cli_args.output.display(), i));
if let Some(packer) = self.packers.get(i) {
if i > 0 {
file.write_all(String::from(",\n").as_bytes()).unwrap();
}
if let Some(file_name) = file_path_stripped.file_name()
&& let Some(file_name_str) = file_name.to_str()
{
packer.save_json(&mut file, file_name_str, &img_ext);
}
}
}
file.write_all(String::from("\n\t\t]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t}\n").as_bytes()).unwrap();
file.write_all(String::from("}\n").as_bytes()).unwrap();
if self.cli_args.verbose {
let msg = format!("Wrote '{}'", file_path.display());
info!("{}", msg);
self.console.print(&format!("[dim]{}[/]", msg));
}
}
fn save_atlas_txt(&self) {
let file_path = PathBuf::from(format!("{}.txt", self.cli_args.output.display()));
let mut file = std::fs::File::create(file_path.clone()).unwrap();
if self.cli_args.atlas_descriptor == AtlasDescriptor::TxtDesc {
self.write_txt_header(&mut file);
}
file.write_all(format!("{},{}\n", self.packers.len(), NAME).as_bytes())
.unwrap();
for i in 0..self.packers.len() {
let img_ext = get_atlas_image_extension(self.cli_args.atlas_image);
let file_path_stripped =
PathBuf::from(format!("{}{}", self.cli_args.output.display(), i));
if let Some(packer) = self.packers.get(i)
&& let Some(file_name) = file_path_stripped.file_name()
&& let Some(file_name_str) = file_name.to_str()
{
packer.save_txt(&mut file, file_name_str, &img_ext);
}
}
if self.cli_args.verbose {
let msg = format!("Wrote '{}'", file_path.display());
info!("{}", msg);
self.console.print(&format!("[dim]{}[/]", msg));
}
}
fn write_txt_header(&self, file: &mut fs::File) {
file.write_all(String::from("/*\n").as_bytes()).unwrap();
file.write_all(
String::from("\t ************************************************\n").as_bytes(),
)
.unwrap();
file.write_all(format!("\t * Generated with: {}\n", NAME).as_bytes())
.unwrap();
file.write_all(
String::from("\t ************************************************\n").as_bytes(),
)
.unwrap();
file.write_all(String::from("\n").as_bytes()).unwrap();
file.write_all(
String::from("\t ************************************************\n").as_bytes(),
)
.unwrap();
file.write_all(String::from("\t * Format description:\n").as_bytes())
.unwrap();
file.write_all(
String::from("\t ************************************************\n").as_bytes(),
)
.unwrap();
file.write_all(String::from("\t [info]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t numberOfAtlasImages,generatedWith\n").as_bytes())
.unwrap();
file.write_all(String::from("\n").as_bytes()).unwrap();
file.write_all(String::from("\t [AtlasImage (repeated numberOfAtlasImages)]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t atlasImageName,numberOfImages,atlasImageWidth,atlasImageHeight,generateMipMaps\n").as_bytes())
.unwrap();
file.write_all(String::from("\n").as_bytes()).unwrap();
file.write_all(String::from("\t [Image (repeated numberOfImages)]\n").as_bytes())
.unwrap();
file.write_all(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())
.unwrap();
file.write_all(String::from("\n").as_bytes()).unwrap();
file.write_all(String::from("\t Text format example:\n").as_bytes())
.unwrap();
file.write_all(String::from("\t [info]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t [AtlasImage]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t [Image]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t [Image]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t ...\n").as_bytes()).unwrap();
file.write_all(String::from("\t [AtlasImage]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t [Image]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t [Image]\n").as_bytes())
.unwrap();
file.write_all(String::from("\t ...\n").as_bytes()).unwrap();
file.write_all(String::from("*/@\n").as_bytes()).unwrap();
}
fn convert_rect_heuristic(&self, heuristic: &RectHeuristic) -> FreeRectHeuristic {
match heuristic {
RectHeuristic::ShortSideFit => FreeRectHeuristic::ShortSideFit,
RectHeuristic::LongSideFit => FreeRectHeuristic::LongSideFit,
RectHeuristic::AreaFit => FreeRectHeuristic::AreaFit,
RectHeuristic::BottomLeft => FreeRectHeuristic::BottomLeft,
RectHeuristic::ContactPoint => FreeRectHeuristic::ContactPoint,
}
}
fn save_input_hash(&self) {
let file_path = PathBuf::from(format!("{}.hash", self.cli_args.output.display()));
let data = format!("{}", self.hash_value);
write_file_sync(&file_path, data.as_bytes()).unwrap();
if self.cli_args.verbose {
self.console
.print(&format!("[dim]Wrote '{}'[/]", file_path.display()));
}
}
}
pub fn get_atlas_image_extension(atlas_image: AtlasImage) -> String {
match atlas_image {
AtlasImage::Png => String::from("png"),
AtlasImage::Tga => String::from("tga"),
AtlasImage::Tiff => String::from("tiff"),
AtlasImage::Webp => String::from("webp"),
}
}
pub fn get_load_filter_extension(load_filter: LoadFilter) -> String {
match load_filter {
LoadFilter::Bmp => String::from("bmp"),
LoadFilter::Hdr => String::from("hdr"),
LoadFilter::Jpg => String::from("jpg"),
LoadFilter::Png => String::from("png"),
LoadFilter::Tga => String::from("tga"),
LoadFilter::Tiff => String::from("tiff"),
LoadFilter::Webp => String::from("webp"),
}
}
pub fn create_dir_all(dir: &PathBuf) {
match fs::create_dir_all(dir) {
Ok(()) => info!("Created dir: '{}'", dir.display()),
Err(err) => panic!(
"Failed to create dir: '{}'. Error msg: '{}'",
dir.display(),
err
),
};
}
pub fn remove_dir_all(dir: &PathBuf) {
match fs::remove_dir_all(dir) {
Ok(()) => info!("Removed dir: '{}'", dir.display()),
Err(err) => panic!(
"Failed to remove dir: '{}'. Error msg: '{}'",
dir.display(),
err
),
};
}
pub fn remove_file(file_path: &PathBuf) {
match fs::remove_file(file_path) {
Ok(()) => info!("Removed file: '{}'", file_path.display()),
Err(err) => panic!(
"Failed to remove file: '{}'. Error msg: '{}'",
file_path.display(),
err
),
};
}
pub fn exists_dir(dir: &Path) -> bool {
dir.exists()
}
pub fn exists_file(file_path: &Path) -> bool {
file_path.is_file()
}
pub fn write_file_sync(file_path: &PathBuf, data: &[u8]) -> io::Result<()> {
let mut file = std::fs::File::create(file_path)?;
file.write_all(data)?;
info!("Wrote '{}' successfully", file_path.display());
Ok(())
}
async fn load_texture(
file_path: PathBuf,
premultiply: bool,
trim: bool,
adjust_fit: bool,
pad: u32,
atlas_size: u32,
) -> Texture {
let mut texture = Texture::default();
texture.load(&file_path, premultiply, trim, adjust_fit, pad, atlas_size);
texture
}
async fn save_image(file_path: PathBuf, packer: Arc<Packer>, image_type: AtlasImage) -> PathBuf {
packer.save_image(&file_path, image_type);
file_path
}