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"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cli", "dds"]
|
default = ["cli", "dds", "basis"]
|
||||||
cli = ["dep:clap", "dep:glob", "config_ext"]
|
cli = ["dep:clap", "dep:glob", "config_ext"]
|
||||||
basis = ["dep:basis-universal"]
|
basis = ["dep:basis-universal"]
|
||||||
dds = ["dep:image_dds"]
|
dds = ["dep:image_dds"]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use rpack_cli::TilemapGenerationConfig;
|
use rpack_cli::TilemapGenerationConfig;
|
||||||
|
use rpack_cli::saving::SaveableImage;
|
||||||
|
|
||||||
use rpack_cli::SaveImageFormat;
|
use rpack_cli::SaveImageFormat;
|
||||||
|
|
||||||
|
|
@ -56,6 +58,15 @@ pub enum Commands {
|
||||||
#[clap(action)]
|
#[clap(action)]
|
||||||
config_path: String,
|
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 {
|
impl Commands {
|
||||||
pub(crate) fn run(&self) -> anyhow::Result<()> {
|
pub(crate) fn run(&self) -> anyhow::Result<()> {
|
||||||
|
|
@ -95,9 +106,19 @@ impl Commands {
|
||||||
Commands::GenerateFromConfig { config_path } => {
|
Commands::GenerateFromConfig { config_path } => {
|
||||||
Self::generate_tilemap_from_config(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(
|
fn generate_tilemap(
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
size: Option<u32>,
|
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::io::Write;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fmt::Display,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use texture_packer::{TexturePacker, TexturePackerConfig, importer::ImageImporter};
|
use texture_packer::{TexturePacker, TexturePackerConfig, importer::ImageImporter};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use crate::formats::SaveImageFormat;
|
||||||
|
|
||||||
|
pub mod formats;
|
||||||
pub mod packer;
|
pub mod packer;
|
||||||
|
pub mod saving;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Spritesheet {
|
pub struct Spritesheet {
|
||||||
|
|
@ -89,30 +92,6 @@ where
|
||||||
prefix
|
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`.
|
/// Errors that can occur while building a `Spritesheet`.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Error, Clone)]
|
#[derive(Debug, Error, Clone)]
|
||||||
|
|
@ -204,77 +183,6 @@ impl Spritesheet {
|
||||||
atlas_asset_json,
|
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)]
|
#[derive(Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
|
|
@ -368,6 +276,8 @@ impl TilemapGenerationConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate(&self) -> anyhow::Result<()> {
|
pub fn generate(&self) -> anyhow::Result<()> {
|
||||||
|
use crate::saving::SaveableImage;
|
||||||
|
|
||||||
let working_dir = self.working_dir();
|
let working_dir = self.working_dir();
|
||||||
|
|
||||||
let (file_paths, prefix) = self.get_file_paths_and_prefix();
|
let (file_paths, prefix) = self.get_file_paths_and_prefix();
|
||||||
|
|
@ -401,28 +311,9 @@ impl TilemapGenerationConfig {
|
||||||
if Path::new(&atlas_image_path).exists() {
|
if Path::new(&atlas_image_path).exists() {
|
||||||
std::fs::remove_file(&atlas_image_path).expect("Could not remove the old file");
|
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
|
spritesheet
|
||||||
.image_data
|
.image_data
|
||||||
.save_with_format(&atlas_image_path, image::ImageFormat::Png)?;
|
.save_with_format_autodetection(&atlas_image_path)?;
|
||||||
}
|
|
||||||
#[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."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?;
|
let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?;
|
||||||
let mut file = std::fs::File::create(&atlas_config_path)?;
|
let mut file = std::fs::File::create(&atlas_config_path)?;
|
||||||
file.write_all(json.as_bytes())?;
|
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"
|
name = "Rpack"
|
||||||
icon = ["static/base_icon.png"]
|
icon = ["static/base_icon.png"]
|
||||||
resources_mapping = [["static/JetBrains*","./"]]
|
resources_mapping = [["static/JetBrains*","./"]]
|
||||||
identifier = "io.github.leinnan.rpack"
|
identifier = "com.mevlyshkin.rpack"
|
||||||
osx_url_schemes = ["io.github.leinnan.rpack"]
|
osx_url_schemes = ["com.mevlyshkin.rpack"]
|
||||||
short_description = "Tilemap Editor"
|
short_description = "Tilemap Editor"
|
||||||
long_description = "Tilemap Editor built with egui in Rust"
|
long_description = "Tilemap Editor built with egui in Rust"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Rpack</string>
|
<string>Rpack</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>io.github.leinnan.rpack</string>
|
<string>com.mevlyshkin.rpack</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.3.0</string>
|
<string>0.3.0</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue