Files
photocraft/crates/psd/src/file.rs
Higgs32584andClaude Opus 5.5 cd75fa78ef PSD/PSB: a 16/32-bit file's "Transparency" channel stays the merged transparency on re-save (#4654)
Photoshop's 16-bit PSBs can mark the merged transparency only by naming it
"Transparency" in the alpha channel names (positive Lr16 layer count, no Mt16).
The importer read it as a user alpha channel, and the next save wrote a second
one. merged_has_alpha now also accepts that name when the layers sit in a
global Lr16/Lr32 block and the names cover every extra channel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:38:42 -04:00

587 lines
28 KiB
Rust

//! The top-level [`PsdFile`] model, parser and writer.
use crate::compression::{Compression, MAX_DECODED_BYTES, decode_planes, validate_planes};
use crate::error::{PsdError, Result};
use crate::header::{ColorMode, Header, Version};
use crate::image_data::ImageData;
use crate::io::{Reader, WriteExt};
use crate::layer::{LayerInfo, LayerRecord};
use crate::resources::{self, ImageResource, ResourceData, ids};
use crate::tagged::{TaggedBlock, read_blocks_with, uses_long_length, write_blocks};
/// Where the layer info was stored in the file.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LayerInfoPlacement {
/// In the layer info field of the layer and mask section (8-bit files).
#[default]
Section,
/// In a global `Lr16` / `Lr32` / `Layr` tagged block (16/32-bit files).
/// The block is re-inserted at `index` among `global_blocks` on write.
GlobalBlock {
/// Position among the global blocks.
index: usize,
/// Block signature (`8BIM` / `8B64`).
signature: [u8; 4],
/// Block key.
key: [u8; 4],
/// Padding after the block (see [`TaggedBlock::padding`]).
padding: Option<Vec<u8>>,
},
}
/// Global layer mask info, stored raw with typed accessors.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GlobalLayerMask {
/// Raw bytes (without the length field).
pub data: Vec<u8>,
}
impl GlobalLayerMask {
fn be16(&self, at: usize) -> Option<u16> {
self.data.get(at..at + 2).map(|b| u16::from_be_bytes([b[0], b[1]]))
}
/// Overlay color space.
pub fn overlay_color_space(&self) -> Option<u16> {
self.be16(0)
}
/// Four color components.
pub fn color_components(&self) -> Option<[u16; 4]> {
Some([self.be16(2)?, self.be16(4)?, self.be16(6)?, self.be16(8)?])
}
/// Opacity 0..=100.
pub fn opacity(&self) -> Option<u16> {
self.be16(10)
}
/// Kind: 0 color selected, 1 color protected, 128 per layer.
pub fn kind(&self) -> Option<u8> {
self.data.get(12).copied()
}
}
/// A complete PSD/PSB file.
///
/// `PsdFile::from_bytes(b)?.to_bytes()? == b` for unmodified well-formed
/// files, and `from_bytes(to_bytes(f)) == f` for any model `f`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PsdFile {
/// Header.
pub header: Header,
/// Color mode data (palette for indexed, duotone spec, …).
pub color_mode_data: Vec<u8>,
/// Image resources in stored order.
pub resources: Vec<ImageResource>,
/// Layer info (`None` when the layer info length is 0 and no
/// `Lr16`/`Lr32` block holds layers).
pub layer_info: Option<LayerInfo>,
/// Where [`Self::layer_info`] is stored.
pub layer_info_placement: LayerInfoPlacement,
/// Global layer mask (`None` when the field is absent entirely).
pub global_layer_mask: Option<GlobalLayerMask>,
/// Global additional layer information blocks.
pub global_blocks: Vec<TaggedBlock>,
/// Bytes at the end of the layer and mask section that do not form a block.
pub layer_mask_trailing: Vec<u8>,
/// Merged image data.
pub image_data: ImageData,
}
impl PsdFile {
/// Parses a PSD or PSB file.
pub fn from_bytes(data: &[u8]) -> Result<Self> {
Self::from_bytes_and_merged(data, |_| false).map(|(file, _)| file)
}
/// Parses like [`Self::from_bytes`] and, when `want` asks for it, decodes the merged image
/// (as [`Self::decode_merged`] would) in the same pass. `want` sees the file with everything
/// but the merged image parsed (its `image_data` is still empty). A ZIP merged image is then
/// inflated once for both the parse's check of the stream and the decode, not twice.
///
/// Errors are those of [`Self::from_bytes`]; a merged image that passes the parse's check but
/// cannot be decoded is the inner `Err`, as [`Self::decode_merged`] would return it.
pub fn from_bytes_and_merged(data: &[u8], want: impl FnOnce(&PsdFile) -> bool) -> Result<(Self, Option<Result<Vec<u8>>>)> {
Self::from_bytes_and_merged_recovering(data, want, |_| false)
}
/// Parses like [`Self::from_bytes_and_merged`], but lets `recover` accept damage confined to
/// the merged image after all layer records and channel data have been read successfully.
/// Callers should opt in only when they can reconstruct the document from its layers.
///
/// The original encoded image bytes are preserved and its parse error is returned as
/// `Some(Err(..))`, even when `want` did not request a decode. The caller must report that
/// error rather than silently discarding the merged image (which may hold extra channels).
/// Header/layer errors, missing compression fields and parse safety-limit errors stay fatal.
pub fn from_bytes_and_merged_recovering(
data: &[u8],
want: impl FnOnce(&PsdFile) -> bool,
recover: impl FnOnce(&PsdFile) -> bool,
) -> Result<(Self, Option<Result<Vec<u8>>>)> {
let mut r = Reader::new(data);
let header = Header::read(&mut r)?;
let psb = header.version.is_psb();
let cmd_len = r.u32()?;
let color_mode_data = r.bytes_u64(u64::from(cmd_len))?.to_vec();
let resources = resources::read_section(&mut r)?;
let lm_len = r.len_field(psb)?;
let mut s = r.sub(lm_len)?;
let mut layer_info = None;
let mut global_layer_mask = None;
let mut global_blocks = Vec::new();
let mut layer_mask_trailing = Vec::new();
let mut layer_info_placement = LayerInfoPlacement::Section;
if !s.is_empty() {
let li_len = s.len_field(psb)?;
if li_len > 0 {
layer_info = Some(LayerInfo::read_body(s.bytes_u64(li_len)?, header.version)?);
}
if s.remaining() >= 4 {
let n = s.u32()?;
global_layer_mask = Some(GlobalLayerMask { data: s.bytes_u64(u64::from(n))?.to_vec() });
let target_key = match header.depth {
16 => Some(b"Lr16"),
32 => Some(b"Lr32"),
_ => None,
};
let mut candidate_seen = match target_key {
Some(_) => false,
None => layer_info.as_ref().is_some_and(|i| !i.layers.is_empty()),
};
let (blocks, trailing) = read_blocks_with(&mut s, header.version, 4, |index, signature, key, data, padding| {
let is_candidate = match target_key {
Some(k) => &key == k,
None => matches!(&key, b"Layr" | b"Lr16" | b"Lr32"),
};
if candidate_seen || !is_candidate {
return false;
}
candidate_seen = true;
let Ok(info) = LayerInfo::read_body(data, header.version) else {
if target_key.is_some() {
layer_info = None;
}
return false;
};
layer_info = Some(info);
layer_info_placement = LayerInfoPlacement::GlobalBlock { index, signature, key, padding: padding.clone() };
true
})?;
global_blocks = blocks;
layer_mask_trailing = trailing;
} else {
layer_mask_trailing = s.peek_rest().to_vec();
}
}
let image_data = ImageData { compression: Compression::Raw, data: Vec::new() };
let mut file =
PsdFile { header, color_mode_data, resources, layer_info, layer_info_placement, global_layer_mask, global_blocks, layer_mask_trailing, image_data };
let mut encoded = r.clone();
let merged = if want(&file) {
file.read_merged_decoded(&mut r).map(Some)
} else {
ImageData::read(&mut r, &file.header).map(|image| {
file.image_data = image;
None
})
};
let merged = match merged {
Ok(merged) => merged,
Err(error) => {
if matches!(error, PsdError::LimitExceeded(_)) || !recover(&file) {
return Err(error);
}
let compression = Compression::from_u16(encoded.u16()?);
let source = encoded.peek_rest();
let mut data = Vec::new();
data.try_reserve_exact(source.len()).map_err(|_| PsdError::LimitExceeded("not enough memory to retain the damaged merged image"))?;
data.extend_from_slice(source);
file.image_data = ImageData { compression, data };
Some(Err(error))
}
};
Ok((file, merged))
}
/// Reads the merged image into `self.image_data` and decodes it. A ZIP stream within the
/// decode limit is inflated once: by the decode, whose stream check is the one
/// [`ImageData::read`] makes. Only when the decode fails is that check run on its own, to
/// tell a file the parse rejects (the outer `Err`) from one it accepts (the inner `Err`).
fn read_merged_decoded(&mut self, r: &mut Reader<'_>) -> Result<Result<Vec<u8>>> {
let layout = ImageData::layout(&self.header);
let compression = Compression::from_u16(r.clone().u16()?);
let zip = matches!(compression, Compression::Zip | Compression::ZipPrediction);
if !zip || layout.total_bytes()? > MAX_DECODED_BYTES {
// Nothing is inflated twice here: the parse's check reads sizes only (or skips an
// over-limit stream, which the decode then refuses).
self.image_data = ImageData::read(r, &self.header)?;
return Ok(self.decode_merged());
}
r.u16()?;
let data = r.peek_rest();
let decoded = decode_planes(compression, data, &layout);
if decoded.is_err() {
validate_planes(compression, data, &layout)?;
}
self.image_data = ImageData { compression, data: data.to_vec() };
r.skip(data.len())?;
Ok(decoded)
}
/// Serializes the file.
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let v = self.header.version;
let psb = v.is_psb();
// Reserve the pixel data up front: appending it to an empty Vec reallocates and copies
// repeatedly. Everything else (headers, resources, records) is small by comparison.
let hint = self.layer_info.as_ref().map_or(0, LayerInfo::channel_data_len).saturating_add(self.image_data.data.len());
let mut out = Vec::new();
out.try_reserve(hint).map_err(|_| PsdError::LimitExceeded("not enough memory to serialize the file"))?;
self.header.write(&mut out);
out.put_len(self.color_mode_data.len() as u64, false)?;
out.put(&self.color_mode_data);
resources::write_section(&mut out, &self.resources)?;
let in_block = matches!(self.layer_info_placement, LayerInfoPlacement::GlobalBlock { .. });
// An entirely empty section is written as a zero length. (A section
// containing only a zero layer-info length cannot be distinguished in
// the model and is normalized to this form.)
let section_empty =
self.layer_info.is_none() && self.global_layer_mask.is_none() && self.global_blocks.is_empty() && self.layer_mask_trailing.is_empty();
let at = out.begin_len(psb);
if !section_empty {
let li = out.begin_len(psb);
if let (Some(info), false) = (&self.layer_info, in_block) {
info.write_body(&mut out, v)?;
}
out.end_len(li, psb)?;
let need_mask_field = self.global_layer_mask.is_some() || !self.global_blocks.is_empty() || (in_block && self.layer_info.is_some());
if need_mask_field {
let gm = self.global_layer_mask.as_ref().map_or(&[][..], |g| &g.data[..]);
out.put_len(gm.len() as u64, false)?;
out.put(gm);
}
match (&self.layer_info, &self.layer_info_placement) {
(Some(info), LayerInfoPlacement::GlobalBlock { index, signature, key, padding }) => {
let (before, after) = self.global_blocks.split_at((*index).min(self.global_blocks.len()));
write_blocks(&mut out, before, v, 4)?;
// Write into the final buffer: a temporary LayerInfo payload and a clone
// of all neighboring opaque blocks otherwise double the save's memory.
out.put(signature);
out.put(key);
let long = uses_long_length(v, key);
let at = out.begin_len(long);
let start = out.len();
info.write_body(&mut out, v)?;
let len = out.len() - start;
out.end_len(at, long)?;
match padding {
Some(bytes) => out.put(bytes),
None => out.extend(std::iter::repeat_n(0u8, (4 - len % 4) % 4)),
}
write_blocks(&mut out, after, v, 4)?;
}
_ => write_blocks(&mut out, &self.global_blocks, v, 4)?,
}
out.put(&self.layer_mask_trailing);
}
out.end_len(at, psb)?;
out.put_u16(self.image_data.compression.as_u16());
out.put(&self.image_data.data);
Ok(out)
}
/// Layer records in file order (bottom-most first); empty if none.
pub fn layers(&self) -> &[LayerRecord] {
self.layer_info.as_ref().map_or(&[], |i| &i.layers)
}
/// Mutable layer records (creates an empty layer info if absent).
pub fn layers_mut(&mut self) -> &mut Vec<LayerRecord> {
&mut self.layer_info.get_or_insert_with(LayerInfo::default).layers
}
/// First resource with the given id.
pub fn resource(&self, id: u16) -> Option<&ImageResource> {
self.resources.iter().find(|r| r.id == id)
}
/// The global `Lr16` / `Lr32` / `Layr` block that should have held the layers but could not
/// be parsed, with the parse error. [`Self::from_bytes`] keeps such a block verbatim in
/// [`Self::global_blocks`] and leaves [`Self::layer_info`] `None`, so the file reads as
/// flattened; this tells a reader that its layers were lost rather than absent.
pub fn unreadable_layer_block(&self) -> Option<(&TaggedBlock, PsdError)> {
if self.layer_info.is_some() {
return None;
}
let block = self.global_blocks.iter().find(|b| matches!(&b.key, b"Lr16" | b"Lr32" | b"Layr"))?;
LayerInfo::read_body(&block.data, self.header.version).err().map(|e| (block, e))
}
/// First global block with the given key.
pub fn global_block(&self, key: &[u8; 4]) -> Option<&TaggedBlock> {
self.global_blocks.iter().find(|b| &b.key == key)
}
/// ICC profile bytes (resource 1039).
pub fn icc_profile(&self) -> Option<&[u8]> {
self.resource(ids::ICC_PROFILE).map(|r| &r.data[..])
}
/// Resolution info (resource 1005).
pub fn resolution(&self) -> Option<crate::resources::ResolutionInfo> {
match self.resource(ids::RESOLUTION_INFO)?.parsed()? {
Ok(ResourceData::ResolutionInfo(r)) => Some(r),
_ => None,
}
}
/// The "has real merged data" flag of resource 1057, if present.
pub fn has_real_merged_data(&self) -> Option<bool> {
match self.resource(ids::VERSION_INFO)?.parsed()? {
Ok(ResourceData::VersionInfo(v)) => Some(v.has_real_merged_data),
_ => None,
}
}
/// Decodes the merged image into planar big-endian samples.
pub fn decode_merged(&self) -> Result<Vec<u8>> {
self.image_data.decode(&self.header)
}
/// `true` if the merged image carries a transparency channel: its first extra channel, when
/// the layer count was stored negative or a "Saving Merged Transparency" global block (`Mtrn`,
/// `Mt16`, `Mt32`) is present. Photoshop marks 32-bit documents with `Mt32` and a positive
/// layer count (psd-tools transparentbg.psd). Photoshop also names the merged transparency
/// "Transparency" and lists it first among the alpha channel names, and some of its 16-bit
/// PSBs carry no other mark (#4654): when the layers sit in a global `Lr16`/`Lr32` block, that
/// name with one name per extra channel marks it too.
pub fn merged_has_alpha(&self) -> bool {
let Some(color) = self.header.color_mode.color_channels() else {
return false;
};
if self.header.channels <= color {
return false;
}
let marked = self.global_blocks.iter().any(|b| matches!(&b.key, b"Mtrn" | b"Mt16" | b"Mt32"));
match &self.layer_info {
Some(li) => {
let in_block = matches!(self.layer_info_placement, LayerInfoPlacement::GlobalBlock { .. });
li.merged_alpha || marked || (in_block && self.names_merged_transparency(self.header.channels - color))
}
None => true,
}
}
/// `true` when the alpha channel names (resource 1045, else 1006) hold exactly `extra` names
/// and the first is "Transparency", Photoshop's name for the merged transparency.
fn names_merged_transparency(&self, extra: u16) -> bool {
const NAME: &str = "Transparency";
let mut names = Vec::new();
if let Some(r) = self.resource(1045) {
let mut at = 0usize;
while let Some(len) = r.data.get(at..at.saturating_add(4)).and_then(|b| b.try_into().ok()).map(u32::from_be_bytes) {
at = at.saturating_add(4);
let end = (len as usize).checked_mul(2).and_then(|n| at.checked_add(n));
let Some(units) = end.and_then(|end| r.data.get(at..end)) else { break };
let units: Vec<u16> = units.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect();
names.push(String::from_utf16_lossy(&units).trim_end_matches('\0').to_string());
at = at.saturating_add(units.len() * 2);
}
} else if let Some(r) = self.resource(1006) {
let mut at = 0usize;
while let Some(&len) = r.data.get(at) {
let Some(name) = r.data.get(at + 1..at + 1 + usize::from(len)) else { break };
names.push(String::from_utf8_lossy(name).into_owned());
at += 1 + usize::from(len);
}
}
names.len() == usize::from(extra) && names.first().is_some_and(|n| n == NAME)
}
/// Checks structural invariants that the writer relies on.
pub fn validate(&self) -> Result<()> {
self.header.validate()?;
if self.header.version == Version::Psd && (self.header.width > 30_000 || self.header.height > 30_000) {
return Err(PsdError::LimitExceeded("PSD dimensions exceed 30000; use PSB"));
}
if self.header.color_mode == ColorMode::Indexed && self.color_mode_data.len() != 768 {
return Err(PsdError::invalid("indexed color mode requires a 768-byte palette"));
}
Ok(())
}
/// Reads a file from disk (native targets only).
#[cfg(not(target_arch = "wasm32"))]
pub fn open(path: impl AsRef<std::path::Path>) -> std::io::Result<Result<Self>> {
Ok(Self::from_bytes(&std::fs::read(path)?))
}
/// Writes the file to disk (native targets only).
#[cfg(not(target_arch = "wasm32"))]
pub fn save(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
let b = self.to_bytes().map_err(std::io::Error::other)?;
std::fs::write(path, b)
}
}
#[cfg(test)]
mod write_tests {
use super::*;
use crate::{Compression, testgen};
#[test]
fn global_blocks_default_to_four_byte_padding() {
for version in [Version::Psd, Version::Psb] {
for len in 1usize..8 {
let mut file = testgen::small(version, Compression::Raw);
file.global_blocks = vec![TaggedBlock::new(*b"TEST", vec![7; len]), TaggedBlock::new(*b"NEXT", vec![8; 4])];
let bytes = file.to_bytes().unwrap();
let start = bytes.windows(8).position(|b| b == b"8BIMTEST").unwrap() + 12;
let next = bytes.windows(8).position(|b| b == b"8BIMNEXT").unwrap();
assert_eq!(next - start, len.next_multiple_of(4));
let mut expected = vec![7; len];
expected.resize(len.next_multiple_of(4), 0);
assert_eq!(&bytes[start..start + expected.len()], expected);
assert_eq!(PsdFile::from_bytes(&bytes).unwrap(), file);
}
}
}
#[test]
fn streamed_layer_info_equals_a_materialized_tagged_block() {
for version in [Version::Psd, Version::Psb] {
for key in [*b"Lr16", *b"Lr32", *b"Layr"] {
for index in [0, 1, 2, usize::MAX] {
for padding in [None, Some(vec![]), Some(vec![42]), Some(vec![1, 2, 3])] {
let mut file = testgen::small(version, Compression::Raw);
file.global_blocks = vec![TaggedBlock::new(*b"zz01", vec![11]), TaggedBlock::new(*b"zz02", vec![12, 13])];
file.layer_info_placement = LayerInfoPlacement::GlobalBlock { index, signature: *b"8B64", key, padding: padding.clone() };
// An odd body catches padding calculated against the final file offset.
file.layer_info.as_mut().unwrap().padding = Some(vec![77]);
let actual = file.to_bytes().unwrap();
let mut materialized = file.clone();
let mut data = Vec::new();
materialized.layer_info.take().unwrap().write_body(&mut data, version).unwrap();
materialized.global_blocks.insert(index.min(materialized.global_blocks.len()), TaggedBlock { signature: *b"8B64", key, data, padding });
materialized.layer_info_placement = LayerInfoPlacement::Section;
assert_eq!(actual, materialized.to_bytes().unwrap(), "{version:?} {key:?} {index}");
}
}
}
}
}
/// #2926: a PSB whose ZIP merged image declares more than `MAX_DECODED_BYTES` opens without
/// inflating that stream (its layers are still usable); decoding the composite reports the limit.
#[test]
fn over_budget_zip_merged_image_opens_without_inflating() {
let mut bytes = testgen::small(Version::Psb, Compression::Zip).to_bytes().unwrap();
// Header: height at 14, width at 18 (big-endian u32).
bytes[14..18].copy_from_slice(&300_000u32.to_be_bytes());
bytes[18..22].copy_from_slice(&300_000u32.to_be_bytes());
let file = PsdFile::from_bytes(&bytes).unwrap();
assert!(!file.layers().is_empty());
assert!(matches!(file.decode_merged(), Err(PsdError::LimitExceeded(_))));
let (same, merged) = PsdFile::from_bytes_and_merged(&bytes, |_| true).unwrap();
assert_eq!(same, file);
assert!(matches!(merged, Some(Err(PsdError::LimitExceeded(_)))));
}
/// Parsing and decoding a ZIP merged image together inflates it once (the parse's stream
/// check is the decode's), with the same model, pixels and errors as the two steps apart.
#[test]
fn merged_zip_image_is_inflated_once_when_decoded_with_the_parse() {
use crate::compression::INFLATES;
let inflates = |f: &dyn Fn()| {
let before = INFLATES.with(std::cell::Cell::get);
f();
INFLATES.with(std::cell::Cell::get) - before
};
for compression in [Compression::Zip, Compression::ZipPrediction] {
let bytes = testgen::small(Version::Psd, compression).to_bytes().unwrap();
let file = PsdFile::from_bytes(&bytes).unwrap();
let pixels = file.decode_merged().unwrap();
let apart = inflates(&|| drop(PsdFile::from_bytes(&bytes).unwrap().decode_merged().unwrap()));
let together = inflates(&|| {
let (parsed, merged) = PsdFile::from_bytes_and_merged(&bytes, |f| f.image_data.data.is_empty()).unwrap();
assert_eq!(parsed, file);
assert_eq!(merged.unwrap().unwrap(), pixels);
});
assert_eq!(together + 1, apart, "{compression:?}");
let (parsed, merged) = PsdFile::from_bytes_and_merged(&bytes, |_| false).unwrap();
assert_eq!((parsed, merged), (file, None));
// A broken stream (its checksum) is still the parse's error.
let mut broken = bytes.clone();
*broken.last_mut().unwrap() ^= 0xff;
let alone = PsdFile::from_bytes(&broken).unwrap_err().to_string();
assert_eq!(PsdFile::from_bytes_and_merged(&broken, |_| true).unwrap_err().to_string(), alone);
}
}
#[test]
fn global_layer_info_serialization_propagates_layer_limit_errors() {
let mut file = testgen::small(Version::Psd, Compression::Raw);
file.layer_info = Some(LayerInfo { layers: vec![LayerRecord::default(); 32768], ..Default::default() });
file.layer_info_placement = LayerInfoPlacement::GlobalBlock { index: usize::MAX, signature: *b"8BIM", key: *b"Lr16", padding: None };
assert!(matches!(file.to_bytes(), Err(PsdError::LimitExceeded(_))));
}
#[test]
fn global_layers_preserve_order_padding_and_first_candidate() {
for version in [Version::Psd, Version::Psb] {
for depth in [16, 32] {
let mut file = testgen::layered(version, ColorMode::Rgb, depth, Compression::Raw);
let key = if depth == 16 { *b"Lr16" } else { *b"Lr32" };
file.global_blocks = vec![TaggedBlock::new(*b"BEFR", vec![1]), TaggedBlock::new(*b"AFTR", vec![2, 3])];
file.layer_info_placement = LayerInfoPlacement::GlobalBlock { index: 1, signature: *b"8B64", key, padding: Some(vec![7, 8]) };
file.layer_mask_trailing = vec![9; 4];
let bytes = file.to_bytes().unwrap();
let parsed = PsdFile::from_bytes(&bytes).unwrap();
assert_eq!(parsed, file);
assert_eq!(parsed.to_bytes().unwrap(), bytes);
let mut duplicate = Vec::new();
file.layer_info.as_ref().unwrap().write_body(&mut duplicate, version).unwrap();
file.global_blocks.push(TaggedBlock::new(key, duplicate));
assert_eq!(PsdFile::from_bytes(&file.to_bytes().unwrap()).unwrap(), file);
file.global_blocks.insert(0, TaggedBlock::new(key, vec![255]));
let bytes = file.to_bytes().unwrap();
let parsed = PsdFile::from_bytes(&bytes).unwrap();
assert!(parsed.layer_info.is_none());
assert_eq!(parsed.global_blocks.len(), 5);
assert_eq!(parsed.to_bytes().unwrap(), bytes);
}
}
}
#[test]
fn global_layers_prioritized_over_compatibility_section() {
for version in [Version::Psd, Version::Psb] {
for depth in [16, 32] {
let file = testgen::layered(version, ColorMode::Rgb, depth, Compression::Raw);
let key = if depth == 16 { *b"Lr16" } else { *b"Lr32" };
let mut lr_data = Vec::new();
file.layer_info.as_ref().unwrap().write_body(&mut lr_data, version).unwrap();
// Photoshop writes an empty (or 8-bit compatibility) Layer Info section
// alongside the global Lr16/Lr32 block (#4928).
let mut compat_file = file.clone();
compat_file.layer_info =
Some(LayerInfo { layers: vec![LayerRecord { name: b"CompatibilityLayer".to_vec(), ..Default::default() }], ..Default::default() });
compat_file.layer_info_placement = LayerInfoPlacement::Section;
compat_file.global_blocks.insert(0, TaggedBlock::new(key, lr_data));
let bytes = compat_file.to_bytes().unwrap();
let parsed = PsdFile::from_bytes(&bytes).unwrap();
assert_eq!(parsed.layers().len(), file.layers().len());
assert_ne!(parsed.layers()[0].name(), "CompatibilityLayer");
assert_eq!(parsed.layers()[0].name(), file.layers()[0].name());
assert!(matches!(parsed.layer_info_placement, LayerInfoPlacement::GlobalBlock { key: k, .. } if k == key));
}
}
}
}