diff --git a/Cargo.toml b/Cargo.toml index dbdf3ab..5b5c8e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ circus = [] circus-arc = ["circus"] circus-audio = ["circus", "flate2", "int-enum", "lossless-audio"] circus-img = ["circus", "image", "flate2", "zstd"] -emote-img = ["base64", "emote-psb", "image", "json", "libtlg-rs", "url"] +emote-img = ["base64", "emote-psb", "image", "json", "libtlg-rs", "url", "utils-psd"] entis-gls = ["xml5ever", "markup5ever", "markup5ever_rcdom", "int-enum"] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] diff --git a/README.md b/README.md index 4546e83..a66542d 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ msg-tool create -t | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| | `emote-psb`/`psb` | `emote-img` | Emote PSB File | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | +| `emote-pimg` | `emote-img` | Emote Multiple Image File (.pimg) | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | `--emote-pimg-psd` is required. | | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/src/args.rs b/src/args.rs index 225b91c..9e9dfa0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -660,6 +660,10 @@ pub struct Arg { #[arg(long, global = true, action = ArgAction::SetTrue)] /// Whether to disable compression for image data in PSD files. pub psd_no_compress: bool, + #[cfg(feature = "emote-img")] + #[arg(long, global = true, action = ArgAction::SetTrue)] + /// Export Emote PIMG images as PSD files. + pub emote_pimg_psd: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/ext/psb.rs b/src/ext/psb.rs index 44bc05c..165dbc9 100644 --- a/src/ext/psb.rs +++ b/src/ext/psb.rs @@ -651,6 +651,10 @@ pub struct PsbListFixed { } impl PsbListFixed { + pub fn new() -> Self { + PsbListFixed { values: vec![] } + } + /// Converts this PSB list to a original PSB list. pub fn to_psb(self, warn_on_none: bool) -> PsbList { let v: Vec<_> = self diff --git a/src/main.rs b/src/main.rs index 7c7c2e6..4d7c0a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3367,6 +3367,8 @@ fn main() { qlie_dpng_psd: arg.qlie_dpng_psd, #[cfg(feature = "utils-psd")] psd_compress: !arg.psd_no_compress, + #[cfg(feature = "emote-img")] + emote_pimg_psd: arg.emote_pimg_psd, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/emote/pimg.rs b/src/scripts/emote/pimg.rs index 672dc9c..598e7f5 100644 --- a/src/scripts/emote/pimg.rs +++ b/src/scripts/emote/pimg.rs @@ -5,6 +5,7 @@ use crate::scripts::base::*; use crate::try_option; use crate::types::*; use crate::utils::img::*; +use crate::utils::psd::*; use anyhow::Result; use emote_psb::PsbReader; use libtlg_rs::*; @@ -101,6 +102,9 @@ impl ScriptBuilder for PImgBuilder { pub struct PImg { psb: VirtualPsbFixed, overlay: Option, + psd: bool, + psd_compress: bool, + zlib_compression_level: u32, } impl PImg { @@ -114,6 +118,9 @@ impl PImg { Ok(Self { psb, overlay: config.emote_pimg_overlay, + psd: config.emote_pimg_psd, + psd_compress: config.psd_compress, + zlib_compression_level: config.zlib_compression_level, }) } @@ -137,7 +144,15 @@ impl PImg { impl Script for PImg { fn default_output_script_type(&self) -> OutputScriptType { - OutputScriptType::Json + OutputScriptType::Custom + } + + fn is_output_supported(&self, output: OutputScriptType) -> bool { + matches!(output, OutputScriptType::Custom) + } + + fn custom_output_extension<'a>(&'a self) -> &'a str { + "psd" } fn default_format_type(&self) -> FormatOptions { @@ -145,11 +160,11 @@ impl Script for PImg { } fn is_image(&self) -> bool { - true + !self.psd } fn is_multi_image(&self) -> bool { - true + !self.psd } fn export_multi_image<'a>( @@ -202,6 +217,96 @@ impl Script for PImg { bases, })) } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let psb = self.psb.root(); + let width = psb["width"] + .as_u32() + .ok_or(anyhow::anyhow!("missing width"))?; + let height = psb["height"] + .as_u32() + .ok_or(anyhow::anyhow!("missing height"))?; + let mut psd = PsdWriter::new(width, height, ImageColorType::Rgba, 8)? + .compress(self.psd_compress) + .zlib_compression_level(self.zlib_compression_level); + let mut base = ImageData { + width, + height, + color_type: ImageColorType::Rgba, + depth: 8, + data: vec![0u8; (width * height * 4) as usize], + }; + let mut new_layers = PsbListFixed::new(); + for layer in psb["layers"].members() { + if layer["diff_id"].is_none() { + new_layers.values.push(layer.clone()); + } + } + for layer in psb["layers"].members() { + if !layer["diff_id"].is_none() { + new_layers.values.push(layer.clone()); + } + } + for layer in new_layers.iter() { + let layer_id = layer["layer_id"] + .as_i64() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid layer_id"))?; + let layer_name = layer["name"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid name"))?; + let width = layer["width"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid width"))?; + let height = layer["height"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid height"))?; + let top = layer["top"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid top"))?; + let left = layer["left"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid left"))?; + let opacity = layer["opacity"] + .as_u8() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid opacity"))?; + let mut visible = layer["visible"].as_u8().unwrap_or(1) != 0; + if !layer["diff_id"].is_none() { + visible = false; // Always hide diff layers + } + let img = self.load_img(layer_id)?; + let mut layer = ImageData { + width: img.width, + height: img.height, + color_type: match img.color { + TlgColorType::Bgr24 => ImageColorType::Bgr, + TlgColorType::Bgra32 => ImageColorType::Bgra, + TlgColorType::Grayscale8 => ImageColorType::Grayscale, + }, + depth: 8, + data: img.data.clone(), + }; + if img.width != width || img.height != height { + return Err(anyhow::anyhow!( + "Layer ID {} size mismatch: expected {}x{}, got {}x{}", + layer_id, + width, + height, + img.width, + img.height + )); + } + convert_to_rgba(&mut layer)?; + let option = PsdLayerOption { opacity, visible }; + if visible { + draw_on_img_with_opacity(&mut base, &layer, left, top, opacity)?; + } + psd.add_layer(layer_name, left, top, layer, Some(option))?; + } + let file = std::fs::File::create(filename)?; + let mut writer = std::io::BufWriter::new(file); + psd.save(base, &mut writer, encoding)?; + Ok(()) + } } struct PImgIter<'a> { diff --git a/src/scripts/qlie/image/dpng.rs b/src/scripts/qlie/image/dpng.rs index e567d24..911f25e 100644 --- a/src/scripts/qlie/image/dpng.rs +++ b/src/scripts/qlie/image/dpng.rs @@ -239,7 +239,13 @@ impl Script for DpngImage { .ok_or_else(|| anyhow::anyhow!("DPNG image has no valid tiles with PNG data"))?; let mut base = load_png(MemReaderRef::new(&tile.png_data))?; convert_to_rgba(&mut base)?; - psd.add_layer(&format!("layer_{}", idx), tile.x, tile.y, base.clone())?; + psd.add_layer( + &format!("layer_{}", idx), + tile.x, + tile.y, + base.clone(), + None, + )?; let mut base = draw_on_canvas( base, self.img.header.image_width, @@ -256,7 +262,7 @@ impl Script for DpngImage { 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)?; - psd.add_layer(&format!("layer_{}", idx2), tile.x, tile.y, diff)?; + psd.add_layer(&format!("layer_{}", idx2), tile.x, tile.y, diff, None)?; } let file = std::fs::File::create(filename)?; let mut writer = std::io::BufWriter::new(file); diff --git a/src/types.rs b/src/types.rs index 0774b95..aca55f7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -613,6 +613,9 @@ pub struct ExtraConfig { #[default(true)] /// Whether to use compression for image data in PSD files. pub psd_compress: bool, + #[cfg(feature = "emote-img")] + /// Export Emote PIMG images as PSD files. + pub emote_pimg_psd: bool, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] diff --git a/src/utils/psd/mod.rs b/src/utils/psd/mod.rs index 540d875..2249fea 100644 --- a/src/utils/psd/mod.rs +++ b/src/utils/psd/mod.rs @@ -9,6 +9,26 @@ use anyhow::Result; use std::io::Write; use types::*; +#[derive(Debug, Clone, msg_tool_macro::Default)] +pub struct PsdLayerOption { + #[default(true)] + /// Whether the layer is visible. + pub visible: bool, + #[default(255)] + /// The opacity of the layer (0-255). + pub opacity: u8, +} + +impl PsdLayerOption { + fn to_flags(&self) -> u8 { + let mut flags = 0u8; + if !self.visible { + flags |= 0b0000_0010; + } + flags + } +} + /// A simple PSD writer. pub struct PsdWriter { psd: PsdFile, @@ -86,8 +106,21 @@ impl PsdWriter { self } - /// Add a visible layer to the PSD file. - pub fn add_layer(&mut self, name: &str, x: u32, y: u32, mut data: ImageData) -> Result<()> { + /// Add a layer to the PSD file. + /// + /// * `name` - The name of the layer. + /// * `x` - The x position of the layer. + /// * `y` - The y position of the layer. + /// * `data` - The image data of the layer. + /// * `option` - The options for the layer. + pub fn add_layer( + &mut self, + name: &str, + x: u32, + y: u32, + mut data: ImageData, + option: Option, + ) -> Result<()> { if data.color_type == ImageColorType::Bgr { convert_bgr_to_rgb(&mut data)?; } @@ -106,6 +139,16 @@ impl PsdWriter { channel_ids.push(-1); // Alpha } } + let flags = if let Some(opt) = &option { + opt.to_flags() + } else { + 0 + }; + let opacity = if let Some(opt) = &option { + opt.opacity + } else { + 255 + }; let mut layer_base = LayerRecordBase { top: y as i32, left: x as i32, @@ -115,9 +158,9 @@ impl PsdWriter { channel_infos: Vec::new(), blend_mode_signature: *IMAGE_RESOURCE_SIGNATURE, blend_mode_key: *b"norm", - opacity: 255, + opacity, clipping: 0, - flags: 1, + flags, filler: 0, }; let mut channel_ranges = Vec::new(); diff --git a/src/utils/psd/types.rs b/src/utils/psd/types.rs index 5946e13..205d5ef 100644 --- a/src/utils/psd/types.rs +++ b/src/utils/psd/types.rs @@ -112,8 +112,11 @@ impl StructPack for PascalString4 { let mut encoded = encode_string(encoding, &self.0, true)?; let len = encoded.len() as u8; len.pack(writer, big, encoding, info)?; - while (len as usize + 1) % 4 != 0 { - encoded.push(0); // padding bytes + let padding = 4 - (len as usize + 1) % 4; + if padding != 4 { + for _ in 0..padding { + encoded.push(0); // padding bytes + } } writer.write_all(&encoded)?; Ok(()) @@ -129,8 +132,17 @@ impl StructUnpack for PascalString4 { ) -> Result { let len = u8::unpack(reader, big, encoding, info)?; let encoded = reader.read_exact_vec(len as usize)?; - while (len as usize + 1) % 4 != 0 { - reader.read_u8()?; // padding bytes + let padding = 4 - (len as usize + 1) % 4; + if padding != 4 { + for _ in 0..padding { + let pad_byte = reader.read_u8()?; + if pad_byte != 0 { + return Err(anyhow::anyhow!( + "Expected padding byte to be 0, got {}", + pad_byte + )); + } + } } let string = decode_to_string(encoding, &encoded, true)?; Ok(PascalString4(string))