mirror of https://github.com/Leinnan/rpack.git
Cleanup, better basis support, new convert command
This commit is contained in:
parent
e1b1d562b4
commit
dbe4db42b8
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
size: Option<u32>,
|
||||
|
|
@ -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<Path>) -> Option<Self> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<R>(&self, output_path: R)
|
||||
where
|
||||
R: AsRef<Path>,
|
||||
{
|
||||
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<R>(&self, output_path: R) -> anyhow::Result<()>
|
||||
where
|
||||
R: AsRef<Path>,
|
||||
{
|
||||
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."
|
||||
);
|
||||
}
|
||||
}
|
||||
.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())?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
use image::{DynamicImage, RgbaImage};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::formats::SaveImageFormat;
|
||||
|
||||
pub trait SaveableImage {
|
||||
fn save_with_format_autodetection<R: AsRef<Path>>(&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<Path>) -> 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<R>(&self, output_path: R) -> anyhow::Result<()>
|
||||
where
|
||||
R: AsRef<Path>,
|
||||
{
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<key>CFBundleName</key>
|
||||
<string>Rpack</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.github.leinnan.rpack</string>
|
||||
<string>com.mevlyshkin.rpack</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.3.0</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
|
|||
Loading…
Reference in New Issue