From f58a9f646c9b4cfa64a57371c8c7ea4ecb10061c Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 3 Feb 2026 15:43:49 +0800 Subject: [PATCH] Add support for layer groups in PSD files --- src/scripts/emote/pimg.rs | 282 +++++++++++++++++++++++++-------- src/scripts/qlie/image/dpng.rs | 3 +- src/utils/img.rs | 6 +- src/utils/psd/mod.rs | 143 +++++++++++++++-- src/utils/psd/types.rs | 127 +++++++-------- 5 files changed, 417 insertions(+), 144 deletions(-) diff --git a/src/scripts/emote/pimg.rs b/src/scripts/emote/pimg.rs index 598e7f5..6fc9e88 100644 --- a/src/scripts/emote/pimg.rs +++ b/src/scripts/emote/pimg.rs @@ -97,6 +97,215 @@ impl ScriptBuilder for PImgBuilder { } } +struct PImgLayer<'a> { + data: &'a PsbValueFixed, + name: &'a str, + layer_id: i64, + /// seems is layer type in PSD files + layer_type: i64, + left: u32, + top: u32, + width: u32, + height: u32, + opacity: u8, + visible: bool, + type_: i64, + children: Vec>, +} + +impl std::fmt::Debug for PImgLayer<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PImgLayer") + .field("layer_id", &self.layer_id) + .field("layer_type", &self.layer_type) + .field("name", &self.name) + .field("left", &self.left) + .field("top", &self.top) + .field("width", &self.width) + .field("height", &self.height) + .field("opacity", &self.opacity) + .field("visible", &self.visible) + .field("type", &self.type_) + .field("children", &self.children) + .finish() + } +} + +impl<'a> PImgLayer<'a> { + pub fn new(data: &'a PsbValueFixed, layers: &'a PsbValueFixed) -> Result { + let layer_id = data["layer_id"] + .as_i64() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid layer_id"))?; + let layer_type = data["layer_type"] + .as_i64() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid layer_type"))?; + let name = data["name"] + .as_str() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid name"))?; + let left = data["left"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid left"))?; + let top = data["top"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid top"))?; + let width = data["width"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid width"))?; + let height = data["height"] + .as_u32() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid height"))?; + let opacity = data["opacity"] + .as_u8() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid opacity"))?; + let visible = data["visible"].as_i64().unwrap_or(1) != 0; + let type_ = data["type"] + .as_i64() + .ok_or_else(|| anyhow::anyhow!("Layer does not have a valid type"))?; + let mut children = Vec::new(); + for layer in layers.members() { + if layer_type == 2 || layer_type == 1 { + if let Some(parent_id) = layer["group_layer_id"].as_i64() { + if parent_id == layer_id { + children.push(PImgLayer::new(layer, layers)?); + } + } + } else if layer_type == 0 { + if let Some(base_id) = layer["diff_id"].as_i64() { + if base_id == layer_id { + children.push(PImgLayer::new(layer, layers)?); + } + } + } + } + Ok(Self { + data, + layer_id, + layer_type, + name, + left, + top, + width, + height, + opacity, + visible, + type_, + children, + }) + } + + fn len(&self) -> usize { + 1 + self.children.iter().map(|c| c.len()).sum::() + } + + fn load_img(&self, img: &PImg) -> Result { + if self.layer_type == 2 || self.layer_type == 1 { + anyhow::bail!("Group layers do not have image data"); + } + if self.layer_id == -1 { + // Generate a empty image + Ok(ImageData { + width: self.width, + height: self.height, + color_type: ImageColorType::Rgba, + depth: 8, + data: vec![0u8; (self.width * self.height * 4) as usize], + }) + } else { + let tlg = img.load_img(self.layer_id).map_err(|e| { + anyhow::anyhow!("Failed to load image for layer_id {}: {}", self.layer_id, e) + })?; + let mut img = ImageData { + width: tlg.width, + height: tlg.height, + color_type: match tlg.color { + TlgColorType::Bgr24 => ImageColorType::Bgr, + TlgColorType::Bgra32 => ImageColorType::Bgra, + TlgColorType::Grayscale8 => ImageColorType::Grayscale, + }, + depth: 8, + data: tlg.data.clone(), + }; + convert_to_rgba(&mut img)?; + Ok(img) + } + } + + fn save_to_psd(&self, img: &PImg, psd: &mut PsdWriter, base: &mut ImageData) -> Result<()> { + if self.children.is_empty() { + let img = self.load_img(img)?; + let mut visible = self.visible; + if !self.data["diff_id"].is_none() { + visible = false; // Diff layers are always hide by default + } + if visible { + draw_on_img_with_opacity(base, &img, self.left, self.top, self.opacity)?; + } + let option = PsdLayerOption { + visible, + opacity: self.opacity, + }; + psd.add_layer(self.name, self.left, self.top, img, Some(option))?; + } else { + psd.add_layer_group_end()?; + if self.layer_type == 0 { + let img = self.load_img(img)?; + let visible = self.visible; + if visible { + draw_on_img_with_opacity(base, &img, self.left, self.top, self.opacity)?; + } + let option = PsdLayerOption { + visible, + opacity: self.opacity, + }; + psd.add_layer(self.name, self.left, self.top, img, Some(option))?; + } + for child in &self.children { + child.save_to_psd(img, psd, base)?; + } + let option = if self.layer_type == 0 { + None + } else { + Some(PsdLayerOption { + visible: self.visible, + opacity: self.opacity, + }) + }; + psd.add_layer_group(self.name, self.layer_type == 2, option)?; + } + Ok(()) + } +} + +#[derive(Debug)] +struct PImgLayerRoot<'a> { + layers: Vec>, +} + +impl<'a> PImgLayerRoot<'a> { + pub fn new(layers: &'a PsbValueFixed) -> Result { + let mut root_layers = Vec::new(); + for layer in layers.members() { + if layer["group_layer_id"].is_none() && layer["diff_id"].is_none() { + root_layers.push(PImgLayer::new(layer, layers)?); + } + } + Ok(Self { + layers: root_layers, + }) + } + + fn len(&self) -> usize { + self.layers.iter().map(|l| l.len()).sum() + } + + fn save_to_psd(&self, img: &PImg, psd: &mut PsdWriter, base: &mut ImageData) -> Result<()> { + for layer in &self.layers { + layer.save_to_psd(img, psd, base)?; + } + Ok(()) + } +} + #[derive(Debug)] /// Emote PImg Script pub struct PImg { @@ -226,7 +435,7 @@ impl Script for PImg { let height = psb["height"] .as_u32() .ok_or(anyhow::anyhow!("missing height"))?; - let mut psd = PsdWriter::new(width, height, ImageColorType::Rgba, 8)? + let mut psd = PsdWriter::new(width, height, ImageColorType::Rgba, 8, encoding)? .compress(self.psd_compress) .zlib_compression_level(self.zlib_compression_level); let mut base = ImageData { @@ -236,75 +445,14 @@ impl Script for PImg { 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 layers = PImgLayerRoot::new(&psb["layers"])?; + if layers.len() != psb["layers"].len() { + return Err(anyhow::anyhow!("Layer hierarchy is invalid")); } + layers.save_to_psd(self, &mut psd, &mut base)?; let file = std::fs::File::create(filename)?; let mut writer = std::io::BufWriter::new(file); - psd.save(base, &mut writer, encoding)?; + psd.save(base, &mut writer)?; Ok(()) } } diff --git a/src/scripts/qlie/image/dpng.rs b/src/scripts/qlie/image/dpng.rs index 911f25e..e32a520 100644 --- a/src/scripts/qlie/image/dpng.rs +++ b/src/scripts/qlie/image/dpng.rs @@ -227,6 +227,7 @@ impl Script for DpngImage { self.img.header.image_height, ImageColorType::Rgba, 8, + encoding, )? .compress(self.config.psd_compress) .zlib_compression_level(self.config.zlib_compression_level); @@ -266,7 +267,7 @@ impl Script for DpngImage { } let file = std::fs::File::create(filename)?; let mut writer = std::io::BufWriter::new(file); - psd.save(base, &mut writer, encoding)?; + psd.save(base, &mut writer)?; Ok(()) } } diff --git a/src/utils/img.rs b/src/utils/img.rs index 21a231e..32943a4 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -793,9 +793,9 @@ pub fn draw_on_img_with_opacity( let src_comp = diff_pixel[i] as u16; let dst_comp = base_pixel_orig[i] as u16; - let numerator = src_comp * src_alpha_u16 - + (dst_comp * dst_alpha_u16 * (255 - src_alpha_u16)) / 255; - base.data[base_idx + i] = (numerator / out_alpha_u16) as u8; + let numerator = src_comp as u32 * src_alpha_u16 as u32 + + (dst_comp as u32 * dst_alpha_u16 as u32 * (255 - src_alpha_u16) as u32) / 255; + base.data[base_idx + i] = (numerator / out_alpha_u16 as u32) as u8; } base.data[base_idx + 3] = out_alpha_u16 as u8; } diff --git a/src/utils/psd/mod.rs b/src/utils/psd/mod.rs index 2249fea..924d3ae 100644 --- a/src/utils/psd/mod.rs +++ b/src/utils/psd/mod.rs @@ -3,6 +3,7 @@ mod types; use crate::ext::io::*; use crate::types::*; +use crate::utils::encoding::*; use crate::utils::img::*; use crate::utils::struct_pack::*; use anyhow::Result; @@ -35,11 +36,31 @@ pub struct PsdWriter { color_type: ImageColorType, compress: bool, zlib_compression_level: u32, + encoding: Encoding, +} + +fn encode_unicode_layer(name: &str) -> Result { + let layer = UnicodeLayer { + name: UnicodeString(name.to_string()), + }; + let mut data = MemWriter::new(); + layer.pack(&mut data, true, Encoding::Utf16BE, &None)?; + Ok(AdditionalLayerInfo { + signature: *IMAGE_RESOURCE_SIGNATURE, + key: *b"luni", + data: data.into_inner(), + }) } impl PsdWriter { /// Creates a new PSD writer with the specified dimensions, color type, and bit depth. - pub fn new(width: u32, height: u32, color_type: ImageColorType, depth: u8) -> Result { + pub fn new( + width: u32, + height: u32, + color_type: ImageColorType, + depth: u8, + encoding: Encoding, + ) -> Result { let color_type = match color_type { ImageColorType::Bgr => ImageColorType::Rgb, ImageColorType::Bgra => ImageColorType::Rgba, @@ -91,6 +112,7 @@ impl PsdWriter { color_type, compress: true, zlib_compression_level: 6, + encoding, }) } @@ -215,12 +237,13 @@ impl PsdWriter { image_data: d, }); } + let encoded = encode_string(self.encoding, &name, false)?; let layer = LayerRecord { base: layer_base, layer_mask: None, layer_blending_ranges, - layer_name: PascalString4(name.to_string()), - infos: vec![], + layer_name: PascalString4(encoded), + infos: vec![encode_unicode_layer(name)?], }; self.psd .layer_and_mask_info @@ -240,15 +263,115 @@ impl PsdWriter { Ok(()) } + /// Adds the start of a layer group to the PSD file. + pub fn add_layer_group( + &mut self, + name: &str, + is_closed: bool, + option: Option, + ) -> Result<()> { + let type_info = SectionDividerSetting { + typ: if is_closed { 2 } else { 1 }, + }; + let mut data = MemWriter::new(); + type_info.pack(&mut data, true, self.encoding, &None)?; + let encoded = encode_string(self.encoding, &name, false)?; + let flags = if let Some(opt) = &option { + opt.to_flags() + } else { + 0 + }; + let opacity = if let Some(opt) = &option { + opt.opacity + } else { + 255 + }; + let layer = LayerRecord { + base: LayerRecordBase { + top: 0, + left: 0, + bottom: 0, + right: 0, + channels: 0, + channel_infos: vec![], + blend_mode_signature: *IMAGE_RESOURCE_SIGNATURE, + blend_mode_key: *b"pass", + opacity, + clipping: 0, + flags, + filler: 0, + }, + layer_mask: None, + layer_blending_ranges: LayerBlendingRanges { + gray_blend_dest: 0xFFFF, + gray_blend_source: 0xFFFF, + channel_ranges: vec![], + }, + layer_name: PascalString4(encoded), + infos: vec![ + AdditionalLayerInfo { + signature: *IMAGE_RESOURCE_SIGNATURE, + key: *b"lsct", + data: data.into_inner(), + }, + encode_unicode_layer(name)?, + ], + }; + self.psd + .layer_and_mask_info + .layer_info + .layer_records + .push(layer); + self.psd.layer_and_mask_info.layer_info.layer_count += 1; + Ok(()) + } + + /// Adds the end of a layer group to the PSD file. + pub fn add_layer_group_end(&mut self) -> Result<()> { + let type_info = SectionDividerSetting { typ: 3 }; + let mut data = MemWriter::new(); + type_info.pack(&mut data, true, self.encoding, &None)?; + let layer = LayerRecord { + base: LayerRecordBase { + top: 0, + left: 0, + bottom: 0, + right: 0, + channels: 0, + channel_infos: vec![], + blend_mode_signature: *IMAGE_RESOURCE_SIGNATURE, + blend_mode_key: *b"norm", + opacity: 255, + clipping: 0, + flags: 0, + filler: 0, + }, + layer_mask: None, + layer_blending_ranges: LayerBlendingRanges { + gray_blend_dest: 0xFFFF, + gray_blend_source: 0xFFFF, + channel_ranges: vec![], + }, + layer_name: PascalString4(b"".to_vec()), + infos: vec![AdditionalLayerInfo { + signature: *IMAGE_RESOURCE_SIGNATURE, + key: *b"lsct", + data: data.into_inner(), + }], + }; + self.psd + .layer_and_mask_info + .layer_info + .layer_records + .push(layer); + self.psd.layer_and_mask_info.layer_info.layer_count += 1; + Ok(()) + } + /// Saves the PSD file to the specified writer with the given encoding. /// /// * `data` - The final composite image data to be saved in the PSD file. - pub fn save( - &mut self, - data: ImageData, - mut writer: T, - encoding: Encoding, - ) -> Result<()> { + pub fn save(&mut self, data: ImageData, mut writer: T) -> Result<()> { if data.color_type == ImageColorType::Bgr { convert_bgr_to_rgb(&mut data.clone())?; } @@ -382,7 +505,7 @@ impl PsdWriter { let compression = if self.compress { 1 } else { 0 }; self.psd.image_data.image_data = planar_data; self.psd.image_data.compression = compression; - self.psd.pack(&mut writer, true, encoding, &None)?; + self.psd.pack(&mut writer, true, self.encoding, &None)?; Ok(()) } } diff --git a/src/utils/psd/types.rs b/src/utils/psd/types.rs index 205d5ef..c7b042d 100644 --- a/src/utils/psd/types.rs +++ b/src/utils/psd/types.rs @@ -1,6 +1,5 @@ use crate::ext::io::*; use crate::types::*; -use crate::utils::encoding::*; use crate::utils::struct_pack::*; use anyhow::Result; use msg_tool_macro::*; @@ -10,57 +9,51 @@ use std::io::{Read, Seek, Write}; pub const PSD_SIGNATURE: &[u8; 4] = b"8BPS"; pub const IMAGE_RESOURCE_SIGNATURE: &[u8; 4] = b"8BIM"; -// #[derive(Debug, Clone)] -// pub struct UnicodeString(pub String); +#[derive(Debug, Clone)] +pub struct UnicodeString(pub String); -// impl StructPack for UnicodeString { -// fn pack( -// &self, -// writer: &mut W, -// big: bool, -// encoding: Encoding, -// info: &Option>, -// ) -> Result<()> { -// let mut encoded: Vec<_> = self.0.encode_utf16().collect(); -// encoded.push(0); // null-terminator -// let len = encoded.len() as u32; -// len.pack(writer, big, encoding, info)?; -// for c in encoded { -// c.pack(writer, big, encoding, info)?; -// } -// Ok(()) -// } -// } +impl StructPack for UnicodeString { + fn pack( + &self, + writer: &mut W, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + let encoded: Vec<_> = self.0.encode_utf16().collect(); + let len = encoded.len() as u32; + len.pack(writer, big, encoding, info)?; + for c in encoded { + c.pack(writer, big, encoding, info)?; + } + Ok(()) + } +} -// impl StructUnpack for UnicodeString { -// fn unpack( -// reader: &mut R, -// big: bool, -// encoding: Encoding, -// info: &Option>, -// ) -> Result { -// let len = u32::unpack(reader, big, encoding, info)?; -// if len == 0 { -// return Ok(UnicodeString(String::new())); -// } -// let mut encoded: Vec = Vec::with_capacity((len as usize) - 1); -// for _ in 0..len - 1 { -// let c = u16::unpack(reader, big, encoding, info)?; -// encoded.push(c); -// } -// // null-terminator -// let null = u16::unpack(reader, big, encoding, info)?; -// if null != 0 { -// return Err(anyhow::anyhow!("Expected null-terminator in UnicodeString")); -// } -// let string = String::from_utf16(&encoded) -// .map_err(|e| anyhow::anyhow!("Failed to decode UTF-16 string: {}", e))?; -// Ok(UnicodeString(string)) -// } -// } +impl StructUnpack for UnicodeString { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let len = u32::unpack(reader, big, encoding, info)?; + if len == 0 { + return Ok(UnicodeString(String::new())); + } + let mut encoded: Vec = Vec::with_capacity(len as usize); + for _ in 0..len { + let c = u16::unpack(reader, big, encoding, info)?; + encoded.push(c); + } + let string = String::from_utf16(&encoded) + .map_err(|e| anyhow::anyhow!("Failed to decode UTF-16 string: {}", e))?; + Ok(UnicodeString(string)) + } +} #[derive(Debug, Clone)] -pub struct PascalString(pub String); +pub struct PascalString(pub Vec); impl StructPack for PascalString { fn pack( @@ -70,13 +63,12 @@ impl StructPack for PascalString { encoding: Encoding, info: &Option>, ) -> Result<()> { - let mut encoded = encode_string(encoding, &self.0, true)?; - let len = encoded.len() as u8; + let len = self.0.len() as u8; len.pack(writer, big, encoding, info)?; + writer.write_all(&self.0)?; if len % 2 == 0 { - encoded.push(0); // padding byte + writer.write_u8(0)?; // padding byte } - writer.write_all(&encoded)?; Ok(()) } } @@ -93,13 +85,12 @@ impl StructUnpack for PascalString { if len % 2 == 0 { reader.read_u8()?; // padding byte } - let string = decode_to_string(encoding, &encoded, true)?; - Ok(PascalString(string)) + Ok(PascalString(encoded)) } } #[derive(Debug, Clone)] -pub struct PascalString4(pub String); +pub struct PascalString4(pub Vec); impl StructPack for PascalString4 { fn pack( @@ -109,16 +100,15 @@ impl StructPack for PascalString4 { encoding: Encoding, info: &Option>, ) -> Result<()> { - let mut encoded = encode_string(encoding, &self.0, true)?; - let len = encoded.len() as u8; + let len = self.0.len() as u8; len.pack(writer, big, encoding, info)?; let padding = 4 - (len as usize + 1) % 4; + writer.write_all(&self.0)?; if padding != 4 { for _ in 0..padding { - encoded.push(0); // padding bytes + writer.write_u8(0)?; // padding byte } } - writer.write_all(&encoded)?; Ok(()) } } @@ -144,8 +134,7 @@ impl StructUnpack for PascalString4 { } } } - let string = decode_to_string(encoding, &encoded, true)?; - Ok(PascalString4(string)) + Ok(PascalString4(encoded)) } } @@ -321,12 +310,13 @@ impl StructPack for LayerInfo { for layer_record in &self.layer_records { layer_record.pack(&mut mem, big, encoding, info)?; } + let mut index = 0usize; for i in 0..self.layer_count { let layer = &self.layer_records[i as usize]; let info = Some(Box::new(layer.clone()) as Box); - for j in 0..layer.base.channels { - let index = (i as usize) * (layer.base.channels as usize) + (j as usize); + for _ in 0..layer.base.channels { let data = &self.channel_image_data[index]; + index += 1; data.pack(&mut mem, big, encoding, &info)?; } } @@ -931,3 +921,14 @@ impl StructPack for PsdFile { Ok(()) } } + +#[derive(Debug, Clone, StructPack, StructUnpack)] +pub struct SectionDividerSetting { + pub typ: u32, + // TODO: implement the rest fields +} + +#[derive(Debug, Clone, StructPack, StructUnpack)] +pub struct UnicodeLayer { + pub name: UnicodeString, +}