diff --git a/Cargo.toml b/Cargo.toml index 0821ef5..a78d8cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ zstd = { version = "0.13", optional = true } default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jieba"] all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "musica", "qlie", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] -all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "hexen-haus-img", "kirikiri-img", "softpal-img", "will-plus-img"] +all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "hexen-haus-img", "kirikiri-img", "qlie-img", "softpal-img", "will-plus-img"] all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "musica-arc", "qlie-arc", "softpal-arc"] all-audio = ["bgi-audio", "circus-audio"] artemis = ["stylua", "utils-escape"] @@ -95,6 +95,7 @@ musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] qlie = [] qlie-arc = ["qlie", "utils-mmx"] +qlie-img = ["qlie", "image"] silky = [] softpal = ["int-enum"] softpal-arc = ["softpal"] diff --git a/README.md b/README.md index df3863f..ce12378 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,10 @@ msg-tool create -t | Archive Type | Feature Name | Name | Unpack | Pack | Remarks | |---|---|---|---|---|---| | `qlie-pack` | `qlie-arc` | Qlie Pack Archive (.pack) | ✔️ | ❌ | Currently only v3.1 are supported | + +| Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | +|---|---|---|---|---|---|---|---|---| +| `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/scripts/mod.rs b/src/scripts/mod.rs index 2846453..1f450d0 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -170,6 +170,8 @@ lazy_static::lazy_static! { Box::new(qlie::script::QlieScriptBuilder::new()), #[cfg(feature = "qlie-arc")] Box::new(qlie::archive::pack::QliePackArchiveBuilder::new()), + #[cfg(feature = "qlie-img")] + Box::new(qlie::image::dpng::DpngImageBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/qlie/archive/pack/mod.rs b/src/scripts/qlie/archive/pack/mod.rs index 18f5483..6547d81 100644 --- a/src/scripts/qlie/archive/pack/mod.rs +++ b/src/scripts/qlie/archive/pack/mod.rs @@ -284,10 +284,15 @@ impl Script for QliePackArchive { fn detect_script_type(_name: &str, buf: &[u8], buf_len: usize) -> Option { if super::super::script::is_this_format(buf, buf_len) { - Some(ScriptType::Qlie) - } else { - None + return Some(ScriptType::Qlie); } + #[cfg(feature = "qlie-img")] + { + if buf_len >= 4 && buf.starts_with(b"DPNG") { + return Some(ScriptType::QlieDpng); + } + } + None } #[derive(Debug)] diff --git a/src/scripts/qlie/image/dpng.rs b/src/scripts/qlie/image/dpng.rs new file mode 100644 index 0000000..4b90246 --- /dev/null +++ b/src/scripts/qlie/image/dpng.rs @@ -0,0 +1,139 @@ +//! Qlie tiled PNG image (.png) +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::img::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use std::io::{Read, Seek, Write}; + +#[derive(StructPack, StructUnpack, Debug, Clone)] +struct DpngHeader { + /// DPNG + magic: [u8; 4], + /// Seems to be always 1 + _unk1: u32, + tile_count: u32, + image_width: u32, + image_height: u32, +} + +#[derive(StructPack, StructUnpack, Debug, Clone)] +struct Tile { + x: u32, + y: u32, + width: u32, + height: u32, + size: u32, + _unk: u64, + #[pack_vec_len(self.size)] + #[unpack_vec_len(size)] + png_data: Vec, +} + +#[derive(StructPack, StructUnpack, Debug, Clone)] +struct DpngFile { + header: DpngHeader, + #[pack_vec_len(self.header.tile_count)] + #[unpack_vec_len(header.tile_count)] + tiles: Vec, +} + +#[derive(Debug)] +/// Qlie DPNG image builder +pub struct DpngImageBuilder {} + +impl DpngImageBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for DpngImageBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Utf8 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + _encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(DpngImage::new(MemReader::new(buf), config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["png"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::QlieDpng + } + + fn is_image(&self) -> bool { + true + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"DPNG") { + Some(20) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct DpngImage { + img: DpngFile, +} + +impl DpngImage { + 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"); + } + if img.tiles.is_empty() { + anyhow::bail!("DPNG image has no tiles"); + } + Ok(DpngImage { img }) + } +} + +impl Script for DpngImage { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_image(&self) -> bool { + true + } + + fn export_image(&self) -> Result { + let mut base = load_png(MemReaderRef::new(&self.img.tiles[0].png_data))?; + convert_to_rgba(&mut base)?; + let mut base = draw_on_canvas( + base, + self.img.header.image_width, + self.img.header.image_height, + self.img.tiles[0].x, + self.img.tiles[0].y, + )?; + for tile in &self.img.tiles[1..] { + let mut diff = load_png(MemReaderRef::new(&tile.png_data))?; + convert_to_rgba(&mut diff)?; + draw_on_image(&mut base, &diff, tile.x, tile.y)?; + } + Ok(base) + } +} diff --git a/src/scripts/qlie/image/mod.rs b/src/scripts/qlie/image/mod.rs new file mode 100644 index 0000000..90d00a5 --- /dev/null +++ b/src/scripts/qlie/image/mod.rs @@ -0,0 +1,2 @@ +//! Qlie Engine picture module +pub mod dpng; diff --git a/src/scripts/qlie/mod.rs b/src/scripts/qlie/mod.rs index 1c1d586..d2e77a0 100644 --- a/src/scripts/qlie/mod.rs +++ b/src/scripts/qlie/mod.rs @@ -1,4 +1,6 @@ //! Qlie Engine script module #[cfg(feature = "qlie-arc")] pub mod archive; +#[cfg(feature = "qlie-img")] +pub mod image; pub mod script; diff --git a/src/types.rs b/src/types.rs index 28809b4..3c788a3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -781,6 +781,9 @@ pub enum ScriptType { #[cfg(feature = "qlie-arc")] /// Qlie Pack Archive (.pack) QliePack, + #[cfg(feature = "qlie-img")] + /// Qlie tiled PNG image (.png) + QlieDpng, #[cfg(feature = "silky")] /// Silky Engine Mes script Silky, diff --git a/src/utils/img.rs b/src/utils/img.rs index 36fa437..766365b 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -179,6 +179,60 @@ pub fn convert_rgba_to_bgra(data: &mut ImageData) -> Result<()> { Ok(()) } +/// Converts a Grayscale image to RGB format. +pub fn convert_grayscale_to_rgb(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Grayscale { + return Err(anyhow::anyhow!("Image is not Grayscale")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "Grayscale to RGB conversion only supports 8-bit depth" + )); + } + let mut new_data = Vec::with_capacity(data.data.len() * 3); + for &gray in &data.data { + new_data.push(gray); // R + new_data.push(gray); // G + new_data.push(gray); // B + } + data.data = new_data; + data.color_type = ImageColorType::Rgb; + Ok(()) +} + +/// Converts a Grayscale image to RGBA format. +pub fn convert_grayscale_to_rgba(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Grayscale { + return Err(anyhow::anyhow!("Image is not Grayscale")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "Grayscale to RGBA conversion only supports 8-bit depth" + )); + } + let mut new_data = Vec::with_capacity(data.data.len() * 4); + for &gray in &data.data { + new_data.push(gray); // R + new_data.push(gray); // G + new_data.push(gray); // B + new_data.push(255); // A + } + data.data = new_data; + data.color_type = ImageColorType::Rgba; + Ok(()) +} + +/// Converts an image to RGBA format. +pub fn convert_to_rgba(data: &mut ImageData) -> Result<()> { + match data.color_type { + ImageColorType::Rgb => convert_rgb_to_rgba(data), + ImageColorType::Bgr => convert_bgr_to_bgra(data), + ImageColorType::Rgba => Ok(()), + ImageColorType::Bgra => convert_bgra_to_rgba(data), + ImageColorType::Grayscale => convert_grayscale_to_rgba(data), + } +} + /// Encodes an image to the specified format and writes it to a file. /// /// * `data` - The image data to encode. @@ -601,6 +655,60 @@ pub fn apply_opacity(img: &mut ImageData, opacity: u8) -> Result<()> { Ok(()) } +/// Draws an image on another image. The pixel data of `diff` will completely overwrite the pixel data of `base`. +/// +/// * `base` - The base image to draw on. +/// * `diff` - The image to draw. +/// * `left` - The horizontal offset to start drawing the image. +/// * `top` - The vertical offset to start drawing the image. +pub fn draw_on_image(base: &mut ImageData, diff: &ImageData, left: u32, top: u32) -> Result<()> { + if base.color_type != diff.color_type { + return Err(anyhow::anyhow!("Image color types do not match")); + } + if base.depth != diff.depth { + return Err(anyhow::anyhow!("Image depths do not match")); + } + + let bits_per_pixel = base.color_type.bpp(base.depth) as usize; + if bits_per_pixel == 0 || bits_per_pixel % 8 != 0 { + return Err(anyhow::anyhow!( + "Unsupported pixel bit layout: {} bits", + bits_per_pixel + )); + } + let bpp = bits_per_pixel / 8; + + let base_stride = base.width as usize * bpp; + let diff_stride = diff.width as usize * bpp; + + for y in 0..diff.height { + let base_y = top + y; + if base_y >= base.height { + continue; + } + + for x in 0..diff.width { + let base_x = left + x; + if base_x >= base.width { + continue; + } + + let diff_idx = (y as usize * diff_stride) + (x as usize * bpp); + let base_idx = (base_y as usize * base_stride) + (base_x as usize * bpp); + + // safety: bounds should hold given width/height checks, but guard to avoid panics + if diff_idx + bpp > diff.data.len() || base_idx + bpp > base.data.len() { + continue; + } + + base.data[base_idx..base_idx + bpp] + .copy_from_slice(&diff.data[diff_idx..diff_idx + bpp]); + } + } + + Ok(()) +} + /// Draws an image on another image with specified opacity. /// /// * `base` - The base image to draw on.