diff --git a/README.md b/README.md index 74d9011..ae1d9f4 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ msg-tool create -t | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| -| `qlie-dpng` | `qlie-img` | Qlie tiled PNG image (.png) | ✔️ | ❌ | ❌ | ❌ | ❌ | | +| `qlie-dpng` | `qlie-img` | Qlie tiled PNG image (.png) | ✔️ | ✔️ | ❌ | ❌ | ❌ | | ### Silky Engine | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| diff --git a/src/args.rs b/src/args.rs index 58163e1..67e9ee5 100644 --- a/src/args.rs +++ b/src/args.rs @@ -647,6 +647,11 @@ pub struct Arg { #[arg(long, global = true, action = ArgAction::SetTrue)] /// Whether to compress files in Qlie pack archive. pub qlie_pack_compress_files: bool, + #[cfg(feature = "qlie-img")] + #[arg(long, global = true, action = ArgAction::SetTrue)] + /// Whether to use PNG file directly for Qlie DPNG images when importing. + /// Enable this will disable reencoding PNG files. Useful when the PNG files are already optimized by other tools. + pub qlie_dpng_use_raw_png: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index dff2c3b..7ae690a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1825,7 +1825,9 @@ pub fn import_script( continue; } }; - if let Err(err) = script_file.import_image(img_data, writer) { + if let Err(err) = + script_file.import_image(img_data, &out_path.to_string_lossy(), writer) + { eprintln!("Error importing image to script '{}': {}", filename, err); COUNTER.inc_error(); if arg.backtrace { @@ -2190,7 +2192,7 @@ pub fn import_script( dep_graph.0 = patched_f.clone(); } utils::files::make_sure_dir_exists(&patched_f)?; - script.import_image_filename(data, &patched_f)?; + script.import_image_filename(data, &out_f, &patched_f)?; return Ok(types::ScriptResult::Ok); } let mut of = match &arg.output_type { @@ -2871,7 +2873,7 @@ pub fn create_file( pb.to_string_lossy().into_owned() } }; - builder.create_image_file_filename(data, &output, &config)?; + builder.create_image_file_filename(data, &output, input, &config)?; return Ok(()); } @@ -3359,6 +3361,8 @@ fn main() { qlie_pack_keyfile: arg.qlie_pack_keyfile.clone(), #[cfg(feature = "qlie-arc")] qlie_pack_compress_files: arg.qlie_pack_compress_files, + #[cfg(feature = "qlie-img")] + qlie_dpng_use_raw_png: arg.qlie_dpng_use_raw_png, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 2b18b34..3824e7b 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -202,12 +202,14 @@ pub trait ScriptBuilder: std::fmt::Debug { /// Creates an image file from the given data. /// /// * `data` - The image data to write. + /// * `filename` - The path to the image file. /// * `writer` - A writer with seek capabilities to write the image data. /// * `options` - Additional configuration options. #[cfg(feature = "image")] fn create_image_file<'a>( &'a self, _data: ImageData, + _filename: &str, _writer: Box, _options: &ExtraConfig, ) -> Result<()> { @@ -221,16 +223,18 @@ pub trait ScriptBuilder: std::fmt::Debug { /// * `data` - The image data to write. /// * `filename` - The path to the output file. /// * `options` - Additional configuration options. + /// * `image_filename` - The path to the image file. #[cfg(feature = "image")] fn create_image_file_filename( &self, data: ImageData, filename: &str, + image_filename: &str, options: &ExtraConfig, ) -> Result<()> { let f = std::fs::File::create(filename)?; let f = std::io::BufWriter::new(f); - self.create_image_file(data, Box::new(f), options) + self.create_image_file(data, image_filename, Box::new(f), options) } } @@ -517,8 +521,14 @@ pub trait Script: std::fmt::Debug + std::any::Any { /// Imports an image into this script. /// /// * `data` - The image data to import. + /// * `filename` - The path of the image file. /// * `file` - A writer with seek capabilities to write the patched scripts. - fn import_image<'a>(&'a self, _data: ImageData, _file: Box) -> Result<()> { + fn import_image<'a>( + &'a self, + _data: ImageData, + _filename: &str, + _file: Box, + ) -> Result<()> { Err(anyhow::anyhow!( "This script type does not support to import image." )) @@ -529,10 +539,16 @@ pub trait Script: std::fmt::Debug + std::any::Any { /// /// * `data` - The image data to import. /// * `filename` - The path of the file to write the patched scripts. - fn import_image_filename(&self, data: ImageData, filename: &str) -> Result<()> { + /// * `image_filename` - The path of the image file. + fn import_image_filename( + &self, + data: ImageData, + image_filename: &str, + filename: &str, + ) -> Result<()> { let f = std::fs::File::create(filename)?; let f = std::io::BufWriter::new(f); - self.import_image(data, Box::new(f)) + self.import_image(data, image_filename, Box::new(f)) } #[cfg(feature = "image")] diff --git a/src/scripts/bgi/image/cbg.rs b/src/scripts/bgi/image/cbg.rs index b73033e..10b4d65 100644 --- a/src/scripts/bgi/image/cbg.rs +++ b/src/scripts/bgi/image/cbg.rs @@ -67,6 +67,7 @@ impl ScriptBuilder for BgiCBGBuilder { fn create_image_file<'a>( &'a self, data: ImageData, + _filename: &str, mut writer: Box, _options: &ExtraConfig, ) -> Result<()> { @@ -200,6 +201,7 @@ impl Script for BgiCBG { fn import_image<'a>( &'a self, data: ImageData, + _filename: &str, mut file: Box, ) -> Result<()> { let encoder = CbgEncoder::new(data)?; diff --git a/src/scripts/bgi/image/img.rs b/src/scripts/bgi/image/img.rs index 2b1f373..a28c3cf 100644 --- a/src/scripts/bgi/image/img.rs +++ b/src/scripts/bgi/image/img.rs @@ -85,6 +85,7 @@ impl ScriptBuilder for BgiImageBuilder { fn create_image_file<'a>( &'a self, data: ImageData, + _filename: &str, writer: Box, options: &ExtraConfig, ) -> Result<()> { @@ -257,7 +258,12 @@ impl Script for BgiImage { }) } - fn import_image<'a>(&'a self, data: ImageData, file: Box) -> Result<()> { + fn import_image<'a>( + &'a self, + data: ImageData, + _filename: &str, + file: Box, + ) -> Result<()> { create_image( data, file, diff --git a/src/scripts/circus/image/crx.rs b/src/scripts/circus/image/crx.rs index 1107977..fe2a1d5 100644 --- a/src/scripts/circus/image/crx.rs +++ b/src/scripts/circus/image/crx.rs @@ -144,6 +144,7 @@ impl ScriptBuilder for CrxImageBuilder { fn create_image_file<'a>( &'a self, data: ImageData, + _filename: &str, writer: Box, options: &ExtraConfig, ) -> Result<()> { @@ -1156,6 +1157,7 @@ impl Script for CrxImage { fn import_image<'a>( &'a self, mut data: ImageData, + _filename: &str, mut file: Box, ) -> Result<()> { let mut color_type = match data.color_type { diff --git a/src/scripts/kirikiri/image/tlg.rs b/src/scripts/kirikiri/image/tlg.rs index 73dc478..c6b1666 100644 --- a/src/scripts/kirikiri/image/tlg.rs +++ b/src/scripts/kirikiri/image/tlg.rs @@ -63,6 +63,7 @@ impl ScriptBuilder for TlgImageBuilder { fn create_image_file<'a>( &'a self, mut data: ImageData, + _filename: &str, writer: Box, _options: &ExtraConfig, ) -> Result<()> { @@ -142,6 +143,7 @@ impl Script for TlgImage { fn import_image<'a>( &'a self, mut data: ImageData, + _filename: &str, file: Box, ) -> Result<()> { if data.depth != 8 { diff --git a/src/scripts/qlie/image/dpng.rs b/src/scripts/qlie/image/dpng.rs index bb521fc..6a4dc9a 100644 --- a/src/scripts/qlie/image/dpng.rs +++ b/src/scripts/qlie/image/dpng.rs @@ -86,15 +86,34 @@ impl ScriptBuilder for DpngImageBuilder { None } } + + fn can_create_image_file(&self) -> bool { + true + } + + fn create_image_file<'a>( + &'a self, + data: ImageData, + filename: &str, + writer: Box, + options: &ExtraConfig, + ) -> Result<()> { + if options.qlie_dpng_use_raw_png { + create_raw_png_image(filename, writer, None) + } else { + create_image(data, writer, options) + } + } } #[derive(Debug)] pub struct DpngImage { img: DpngFile, + config: ExtraConfig, } impl DpngImage { - pub fn new(mut data: T, _config: &ExtraConfig) -> Result { + pub fn new(mut data: T, config: &ExtraConfig) -> Result { let img = DpngFile::unpack(&mut data, false, Encoding::Utf8, &None)?; if img.header.magic != *b"DPNG" { anyhow::bail!("Not a valid DPNG image"); @@ -102,7 +121,10 @@ impl DpngImage { if img.tiles.is_empty() { anyhow::bail!("DPNG image has no tiles"); } - Ok(DpngImage { img }) + Ok(DpngImage { + img, + config: config.clone(), + }) } } @@ -146,4 +168,111 @@ impl Script for DpngImage { } Ok(base) } + + fn import_image<'a>( + &'a self, + data: ImageData, + filename: &str, + file: Box, + ) -> Result<()> { + if self.config.qlie_dpng_use_raw_png { + let img = load_png(std::fs::File::open(filename)?)?; + if img.width != self.img.header.image_width + || img.height != self.img.header.image_height + { + eprintln!( + "Warning: Image dimensions do not match original DPNG image (expected {}x{}, got {}x{})", + self.img.header.image_width, + self.img.header.image_height, + img.width, + img.height + ); + crate::COUNTER.inc_warning(); + } + create_raw_png_image(filename, file, Some(img))?; + } else { + if data.width != self.img.header.image_width + || data.height != self.img.header.image_height + { + eprintln!( + "Warning: Image dimensions do not match original DPNG image (expected {}x{}, got {}x{})", + self.img.header.image_width, + self.img.header.image_height, + data.width, + data.height + ); + crate::COUNTER.inc_warning(); + } + create_image(data, file, &self.config)?; + } + Ok(()) + } +} + +fn create_raw_png_image<'a>( + filename: &str, + mut file: Box, + img: Option, +) -> Result<()> { + let img = match img { + Some(img) => img, + None => load_png(std::fs::File::open(filename)?)?, + }; + let header = DpngHeader { + magic: *b"DPNG", + _unk1: 1, + tile_count: 1, + image_width: img.width, + image_height: img.height, + }; + let png_data = crate::utils::files::read_file(filename)?; + let tile = Tile { + x: 0, + y: 0, + width: img.width, + height: img.height, + size: png_data.len() as u32, + _unk: 0, + png_data, + }; + let dpng = DpngFile { + header, + tiles: vec![tile], + }; + dpng.pack(&mut file, false, Encoding::Utf8, &None)?; + Ok(()) +} + +fn create_image<'a>( + image: ImageData, + mut writer: Box, + config: &ExtraConfig, +) -> Result<()> { + let header = DpngHeader { + magic: *b"DPNG", + _unk1: 1, + tile_count: 1, + image_width: image.width, + image_height: image.height, + }; + let mut png_data = MemWriter::new(); + let width = image.width; + let height = image.height; + encode_img_writer(image, ImageOutputType::Png, &mut png_data, config)?; + let png_data = png_data.into_inner(); + let tile = Tile { + x: 0, + y: 0, + width, + height, + size: png_data.len() as u32, + _unk: 0, + png_data, + }; + let dpng = DpngFile { + header, + tiles: vec![tile], + }; + dpng.pack(&mut writer, false, Encoding::Utf8, &None)?; + Ok(()) } diff --git a/src/scripts/softpal/img/pgd/ge.rs b/src/scripts/softpal/img/pgd/ge.rs index 981d339..81273a7 100644 --- a/src/scripts/softpal/img/pgd/ge.rs +++ b/src/scripts/softpal/img/pgd/ge.rs @@ -58,6 +58,7 @@ impl ScriptBuilder for PgdGeBuilder { fn create_image_file<'a>( &'a self, data: ImageData, + _filename: &str, mut writer: Box, options: &ExtraConfig, ) -> Result<()> { @@ -125,6 +126,7 @@ impl Script for PgdGe { fn import_image<'a>( &'a self, data: ImageData, + _filename: &str, mut file: Box, ) -> Result<()> { let mut header = self.header.clone(); diff --git a/src/scripts/softpal/img/pgd/pgd3.rs b/src/scripts/softpal/img/pgd/pgd3.rs index e7b1282..c6faea7 100644 --- a/src/scripts/softpal/img/pgd/pgd3.rs +++ b/src/scripts/softpal/img/pgd/pgd3.rs @@ -153,6 +153,7 @@ impl Script for Pgd3 { fn import_image<'a>( &'a self, data: ImageData, + _filename: &str, mut file: Box, ) -> Result<()> { let mut header = PgdGeHeader { diff --git a/src/types.rs b/src/types.rs index 731cb78..6bbc14b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -602,6 +602,10 @@ pub struct ExtraConfig { #[cfg(feature = "qlie-arc")] /// Whether to compress files in Qlie pack archive. pub qlie_pack_compress_files: bool, + #[cfg(feature = "qlie-img")] + /// Whether to use PNG file directly for Qlie DPNG images when importing. + /// Enable this will disable reencoding PNG files. Useful when the PNG files are already optimized by other tools. + pub qlie_dpng_use_raw_png: bool, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] diff --git a/src/utils/img.rs b/src/utils/img.rs index 766365b..21a231e 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -5,6 +5,7 @@ use crate::ext::io::*; use crate::types::*; use anyhow::Result; use std::convert::TryFrom; +use std::io::Write; /// Reverses the alpha values of an image. /// @@ -240,14 +241,29 @@ pub fn convert_to_rgba(data: &mut ImageData) -> Result<()> { /// * `filename` - The path of the file to write the encoded image to. /// * `config` - Extra configuration. pub fn encode_img( - mut data: ImageData, + data: ImageData, typ: ImageOutputType, filename: &str, config: &ExtraConfig, +) -> Result<()> { + let mut file = crate::utils::files::write_file(filename)?; + encode_img_writer(data, typ, &mut file, config) +} + +/// Encodes an image to the specified format and writes it to a writer. +/// +/// * `data` - The image data to encode. +/// * `typ` - The output image format. +/// * `filename` - The path of the file to write the encoded image to. +/// * `config` - Extra configuration. +pub fn encode_img_writer( + mut data: ImageData, + typ: ImageOutputType, + mut file: &mut T, + config: &ExtraConfig, ) -> Result<()> { match typ { ImageOutputType::Png => { - let mut file = crate::utils::files::write_file(filename)?; let color_type = match data.color_type { ImageColorType::Grayscale => png::ColorType::Grayscale, ImageColorType::Rgb => png::ColorType::Rgb, @@ -280,7 +296,6 @@ pub fn encode_img( } #[cfg(feature = "image-jpg")] ImageOutputType::Jpg => { - let file = crate::utils::files::write_file(filename)?; let color_type = match data.color_type { ImageColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE, ImageColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB, @@ -310,7 +325,6 @@ pub fn encode_img( } #[cfg(feature = "image-webp")] ImageOutputType::Webp => { - let mut file = crate::utils::files::write_file(filename)?; let color_type = match data.color_type { ImageColorType::Rgb => webp::PixelLayout::Rgb, ImageColorType::Rgba => webp::PixelLayout::Rgba, @@ -344,7 +358,6 @@ pub fn encode_img( } #[cfg(feature = "image-jxl")] ImageOutputType::Jxl => { - let mut file = crate::utils::files::write_file(filename)?; let data = encode_jxl(data, config)?; file.write_all(&data)?; Ok(())