diff --git a/crates/rpack_cli/Cargo.toml b/crates/rpack_cli/Cargo.toml index 1c26397..d6cafdc 100644 --- a/crates/rpack_cli/Cargo.toml +++ b/crates/rpack_cli/Cargo.toml @@ -9,7 +9,7 @@ version = "0.3.0" edition = "2024" [features] -default = ["cli", "dds"] +default = ["cli", "dds", "basis"] cli = ["dep:clap", "dep:glob", "config_ext"] basis = ["dep:basis-universal"] dds = ["dep:image_dds"] diff --git a/crates/rpack_cli/src/commands.rs b/crates/rpack_cli/src/commands/mod.rs similarity index 88% rename from crates/rpack_cli/src/commands.rs rename to crates/rpack_cli/src/commands/mod.rs index dbecc94..260ae76 100644 --- a/crates/rpack_cli/src/commands.rs +++ b/crates/rpack_cli/src/commands/mod.rs @@ -1,7 +1,9 @@ use std::io::Write; +use std::path::PathBuf; use clap::Subcommand; use rpack_cli::TilemapGenerationConfig; +use rpack_cli::saving::SaveableImage; use rpack_cli::SaveImageFormat; @@ -56,6 +58,15 @@ pub enum Commands { #[clap(action)] config_path: String, }, + /// Converts a texture between formats + Convert { + /// path of the config to create + #[clap(action)] + source_path: PathBuf, + /// path of the output texture + #[clap(action)] + output_path: PathBuf, + }, } impl Commands { pub(crate) fn run(&self) -> anyhow::Result<()> { @@ -95,9 +106,19 @@ impl Commands { Commands::GenerateFromConfig { config_path } => { Self::generate_tilemap_from_config(config_path) } + Commands::Convert { + source_path, + output_path, + } => Self::convert(source_path, output_path), } } + fn convert(source_path: PathBuf, output_path: PathBuf) -> anyhow::Result<()> { + let image = image::open(source_path)?; + image.save_with_format_autodetection(output_path)?; + Ok(()) + } + fn generate_tilemap( name: Option, size: Option, diff --git a/crates/rpack_cli/src/formats.rs b/crates/rpack_cli/src/formats.rs new file mode 100644 index 0000000..a8ebdf5 --- /dev/null +++ b/crates/rpack_cli/src/formats.rs @@ -0,0 +1,40 @@ +use std::{ffi::OsStr, fmt::Display, path::Path}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Copy, Serialize, Deserialize, PartialEq)] +#[cfg_attr( + all(feature = "cli", not(target_arch = "wasm32")), + derive(clap::ValueEnum) +)] +pub enum SaveImageFormat { + #[default] + Png, + Dds, + Basis, +} + +impl SaveImageFormat { + /// Try to gets file extension from a path + pub fn from_path(v: impl AsRef) -> Option { + let path = v.as_ref(); + let extension = path.extension().and_then(OsStr::to_str)?; + match extension { + "png" => Some(SaveImageFormat::Png), + "dds" => Some(SaveImageFormat::Dds), + "basis" => Some(SaveImageFormat::Basis), + _ => None, + } + } +} + +impl Display for SaveImageFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SaveImageFormat::Png => f.write_str(".png"), + SaveImageFormat::Dds => f.write_str(".dds"), + #[cfg(feature = "basis")] + SaveImageFormat::Basis => f.write_str(".basis"), + } + } +} diff --git a/crates/rpack_cli/src/lib.rs b/crates/rpack_cli/src/lib.rs index cf518d0..6df694d 100644 --- a/crates/rpack_cli/src/lib.rs +++ b/crates/rpack_cli/src/lib.rs @@ -6,13 +6,16 @@ use serde_json::Value; use std::io::Write; use std::{ ffi::OsStr, - fmt::Display, path::{Path, PathBuf}, }; use texture_packer::{TexturePacker, TexturePackerConfig, importer::ImageImporter}; use thiserror::Error; +pub use crate::formats::SaveImageFormat; + +pub mod formats; pub mod packer; +pub mod saving; #[derive(Clone)] pub struct Spritesheet { @@ -89,30 +92,6 @@ where prefix } -#[derive(Clone, Debug, Default, Copy, Serialize, Deserialize, PartialEq)] -#[cfg_attr( - all(feature = "cli", not(target_arch = "wasm32")), - derive(clap::ValueEnum) -)] -pub enum SaveImageFormat { - #[default] - Png, - Dds, - #[cfg(feature = "basis")] - Basis, -} - -impl Display for SaveImageFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SaveImageFormat::Png => f.write_str(".png"), - SaveImageFormat::Dds => f.write_str(".dds"), - #[cfg(feature = "basis")] - SaveImageFormat::Basis => f.write_str(".basis"), - } - } -} - /// Errors that can occur while building a `Spritesheet`. #[non_exhaustive] #[derive(Debug, Error, Clone)] @@ -204,77 +183,6 @@ impl Spritesheet { atlas_asset_json, }) } - - #[cfg(all(feature = "dds", not(target_arch = "wasm32")))] - pub fn save_as_dds(&self, output_path: R) - where - R: AsRef, - { - let rgba_image = self.image_data.to_rgba8(); - - let dds = image_dds::dds_from_image( - &rgba_image, - image_dds::ImageFormat::Rgba8Unorm, - image_dds::Quality::Fast, - image_dds::Mipmaps::GeneratedAutomatic, - ) - .unwrap(); - - let mut writer = - std::io::BufWriter::new(std::fs::File::create(output_path.as_ref()).unwrap()); - dds.write(&mut writer).unwrap(); - } - - #[cfg(all(feature = "basis", not(target_arch = "wasm32")))] - pub fn save_as_basis(&self, output_path: R) -> anyhow::Result<()> - where - R: AsRef, - { - use basis_universal::{BasisTextureFormat, Compressor, Transcoder}; - use image::{EncodableLayout, GenericImageView}; - - let rgba_image = self.image_data.to_rgba8(); - - let channel_count = 4; - let (pixel_width, pixel_height) = self.image_data.dimensions(); - let mut compressor_params = basis_universal::CompressorParams::new(); - compressor_params.set_generate_mipmaps(true); - compressor_params.set_basis_format(BasisTextureFormat::ETC1S); - compressor_params.set_etc1s_quality_level(basis_universal::ETC1S_QUALITY_MAX); - compressor_params.set_print_status_to_stdout(false); - let mut compressor_image = compressor_params.source_image_mut(0); - compressor_image.init( - rgba_image.as_bytes(), - pixel_width, - pixel_height, - channel_count, - ); - - // - // Create the compressor and compress - // - let mut compressor = Compressor::default(); - let compression_time = unsafe { - compressor.init(&compressor_params); - let t0 = std::time::Instant::now(); - compressor.process().expect("Failed to compress the image."); - let t1 = std::time::Instant::now(); - t1 - t0 - }; - - // You could write it to disk like this - let basis_file = compressor.basis_file(); - let transcoder = Transcoder::new(); - let mip_level_count = transcoder.image_level_count(basis_file, 0); - println!( - "Compressed {} mip levels to {} total bytes in {} ms", - mip_level_count, - compressor.basis_file_size(), - compression_time.as_secs_f64() * 1000.0 - ); - std::fs::write(output_path.as_ref(), basis_file)?; - Ok(()) - } } #[derive(Clone, Serialize, Deserialize, Default, PartialEq)] @@ -368,6 +276,8 @@ impl TilemapGenerationConfig { } pub fn generate(&self) -> anyhow::Result<()> { + use crate::saving::SaveableImage; + let working_dir = self.working_dir(); let (file_paths, prefix) = self.get_file_paths_and_prefix(); @@ -401,28 +311,9 @@ impl TilemapGenerationConfig { if Path::new(&atlas_image_path).exists() { std::fs::remove_file(&atlas_image_path).expect("Could not remove the old file"); } - match self.format.unwrap_or_default() { - SaveImageFormat::Dds => { - #[cfg(feature = "dds")] - spritesheet.save_as_dds(&atlas_image_path); - #[cfg(not(feature = "dds"))] - panic!( - "Program is compiled without support for dds. Compile it yourself with feature `dds` enabled." - ); - } - SaveImageFormat::Png => { - spritesheet - .image_data - .save_with_format(&atlas_image_path, image::ImageFormat::Png)?; - } - #[cfg(feature = "basis")] - SaveImageFormat::Basis => { - spritesheet.save_as_basis(&atlas_image_path)?; - panic!( - "Program is compiled without support for basis. Compile it yourself with feature `basis` enabled." - ); - } - } + spritesheet + .image_data + .save_with_format_autodetection(&atlas_image_path)?; let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?; let mut file = std::fs::File::create(&atlas_config_path)?; file.write_all(json.as_bytes())?; diff --git a/crates/rpack_cli/src/saving.rs b/crates/rpack_cli/src/saving.rs new file mode 100644 index 0000000..ab4da7a --- /dev/null +++ b/crates/rpack_cli/src/saving.rs @@ -0,0 +1,122 @@ +use image::{DynamicImage, RgbaImage}; +use std::path::Path; + +use crate::formats::SaveImageFormat; + +pub trait SaveableImage { + fn save_with_format_autodetection>(&self, path: R) -> anyhow::Result<()> { + let output_path = path.as_ref().to_owned(); + let format = SaveImageFormat::from_path(&output_path); + match format { + None => { + let output_extension = output_path + .extension() + .map_or(String::from("png"), |e| e.to_string_lossy().to_string()); + let Some(output_format) = image::ImageFormat::from_extension(&output_extension) + else { + anyhow::bail!("Unsupported output format"); + }; + self.to_rgba8() + .save_with_format(output_path, output_format)?; + } + Some(format) => match format { + SaveImageFormat::Png => { + self.to_rgba8() + .save_with_format(output_path, image::ImageFormat::Png)?; + } + SaveImageFormat::Basis => { + #[cfg(feature = "basis")] + self.save_as_basis(&output_path)?; + #[cfg(not(feature = "basis"))] + anyhow::bail!( + "Program is compiled without support for basis. Compile it yourself with feature `basis` enabled." + ); + } + SaveImageFormat::Dds => { + #[cfg(feature = "dds")] + self.save_as_dds(&output_path)?; + #[cfg(not(feature = "basis"))] + anyhow::bail!( + "Program is compiled without support for basis. Compile it yourself with feature `basis` enabled." + ); + } + }, + } + Ok(()) + } + + fn to_rgba8(&self) -> RgbaImage; + + #[cfg(all(feature = "basis", not(target_arch = "wasm32")))] + fn save_as_basis(&self, output_path: impl AsRef) -> anyhow::Result<()> { + use basis_universal::{BasisTextureFormat, Compressor, Transcoder}; + use image::EncodableLayout; + + let rgba_image = self.to_rgba8(); + + let channel_count = 4; + let (pixel_width, pixel_height) = rgba_image.dimensions(); + let mut compressor_params = basis_universal::CompressorParams::new(); + compressor_params.set_generate_mipmaps(true); + compressor_params.set_basis_format(BasisTextureFormat::ETC1S); + compressor_params.set_etc1s_quality_level(basis_universal::ETC1S_QUALITY_MAX); + compressor_params.set_print_status_to_stdout(false); + let mut compressor_image = compressor_params.source_image_mut(0); + compressor_image.init( + rgba_image.as_bytes(), + pixel_width, + pixel_height, + channel_count, + ); + + // + // Create the compressor and compress + // + let mut compressor = Compressor::default(); + let compression_time = unsafe { + compressor.init(&compressor_params); + let t0 = std::time::Instant::now(); + compressor.process().expect("Failed to compress the image."); + let t1 = std::time::Instant::now(); + t1 - t0 + }; + + // You could write it to disk like this + let basis_file = compressor.basis_file(); + let transcoder = Transcoder::new(); + let mip_level_count = transcoder.image_level_count(basis_file, 0); + println!( + "Compressed {} mip levels to {} total bytes in {} ms", + mip_level_count, + compressor.basis_file_size(), + compression_time.as_secs_f64() * 1000.0 + ); + std::fs::write(output_path.as_ref(), basis_file)?; + Ok(()) + } + + #[cfg(all(feature = "dds", not(target_arch = "wasm32")))] + fn save_as_dds(&self, output_path: R) -> anyhow::Result<()> + where + R: AsRef, + { + let rgba_image = self.to_rgba8(); + + let dds = image_dds::dds_from_image( + &rgba_image, + image_dds::ImageFormat::Rgba8Unorm, + image_dds::Quality::Fast, + image_dds::Mipmaps::GeneratedAutomatic, + )?; + + let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path.as_ref())?); + dds.write(&mut writer)?; + Ok(()) + } +} + +impl SaveableImage for DynamicImage { + fn to_rgba8(&self) -> RgbaImage { + self.to_rgba8() + } +} diff --git a/crates/rpack_egui/Cargo.toml b/crates/rpack_egui/Cargo.toml index 0959da8..9538a4d 100644 --- a/crates/rpack_egui/Cargo.toml +++ b/crates/rpack_egui/Cargo.toml @@ -67,7 +67,7 @@ ico = "0.4.0" name = "Rpack" icon = ["static/base_icon.png"] resources_mapping = [["static/JetBrains*","./"]] -identifier = "io.github.leinnan.rpack" -osx_url_schemes = ["io.github.leinnan.rpack"] +identifier = "com.mevlyshkin.rpack" +osx_url_schemes = ["com.mevlyshkin.rpack"] short_description = "Tilemap Editor" long_description = "Tilemap Editor built with egui in Rust" diff --git a/crates/rpack_egui/static/Info.plist b/crates/rpack_egui/static/Info.plist index 33197cf..eec5839 100644 --- a/crates/rpack_egui/static/Info.plist +++ b/crates/rpack_egui/static/Info.plist @@ -6,7 +6,7 @@ CFBundleName Rpack CFBundleIdentifier - io.github.leinnan.rpack + com.mevlyshkin.rpack CFBundleVersion 0.3.0 CFBundleExecutable