Add support for layer groups in PSD files

This commit is contained in:
2026-02-03 15:43:49 +08:00
parent 8b2eef1028
commit f58a9f646c
5 changed files with 417 additions and 144 deletions

View File

@@ -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<PImgLayer<'a>>,
}
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<Self> {
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::<usize>()
}
fn load_img(&self, img: &PImg) -> Result<ImageData> {
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<PImgLayer<'a>>,
}
impl<'a> PImgLayerRoot<'a> {
pub fn new(layers: &'a PsbValueFixed) -> Result<Self> {
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(())
}
}

View File

@@ -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(())
}
}

View File

@@ -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;
}

View File

@@ -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<AdditionalLayerInfo> {
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<Self> {
pub fn new(
width: u32,
height: u32,
color_type: ImageColorType,
depth: u8,
encoding: Encoding,
) -> Result<Self> {
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<PsdLayerOption>,
) -> 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"</Layer group>".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<T: Write>(
&mut self,
data: ImageData,
mut writer: T,
encoding: Encoding,
) -> Result<()> {
pub fn save<T: Write>(&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(())
}
}

View File

@@ -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<W: Write>(
// &self,
// writer: &mut W,
// big: bool,
// encoding: Encoding,
// info: &Option<Box<dyn Any>>,
// ) -> 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<W: Write>(
&self,
writer: &mut W,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn Any>>,
) -> 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<R: Read + Seek>(
// reader: &mut R,
// big: bool,
// encoding: Encoding,
// info: &Option<Box<dyn Any>>,
// ) -> Result<Self> {
// let len = u32::unpack(reader, big, encoding, info)?;
// if len == 0 {
// return Ok(UnicodeString(String::new()));
// }
// let mut encoded: Vec<u16> = 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<R: Read + Seek>(
reader: &mut R,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn Any>>,
) -> Result<Self> {
let len = u32::unpack(reader, big, encoding, info)?;
if len == 0 {
return Ok(UnicodeString(String::new()));
}
let mut encoded: Vec<u16> = 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<u8>);
impl StructPack for PascalString {
fn pack<W: Write>(
@@ -70,13 +63,12 @@ impl StructPack for PascalString {
encoding: Encoding,
info: &Option<Box<dyn Any>>,
) -> 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<u8>);
impl StructPack for PascalString4 {
fn pack<W: Write>(
@@ -109,16 +100,15 @@ impl StructPack for PascalString4 {
encoding: Encoding,
info: &Option<Box<dyn Any>>,
) -> 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<dyn Any>);
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,
}