fix(psd): write the layer block after a kept merged-transparency marker (#5673)

A 16/32-bit document saved as PSD has its layers in a global Lr16/Lr32 block, which the writer put first among the global blocks. A Mtrn/Mt16/Mt32 marker kept from the opened file then followed it, and Photoshop refuses such a file (verified in Photoshop 2026 with psd-tools' transparentbg.psd re-saved both ways: Mt32 first opens, Lr32 first doesn't). The layer block now goes right after the last kept marker, as Photoshop orders them. Fixes #5068.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FinnNkandClaude Opus 5.5 authored and GitHub committed 2026-10-14 15:03:59 -07:00
1 parent 1025914b0e
commit 2807deb209
1 file changed
+31 -2
+31 -2
View File
@@ -1746,9 +1746,13 @@ fn document_to_psd_nested(doc: &Document, opts: &PsdExportOptions, depth: u32, c
resources.push(version_info_resource(real_merged));
let has_layers = !ex.records.is_empty();
// Photoshop writes a merged-transparency marker (`Mtrn`, `Mt16`, `Mt32`) before the layer
// block, and won't open a file whose `Lr32` comes first (#5068): the layer block goes right
// after the last kept marker, else first.
let index = global_blocks.iter().rposition(|b| matches!(&b.key, b"Mtrn" | b"Mt16" | b"Mt32")).map_or(0, |i| i + 1);
let placement = match (has_layers, sample) {
(true, SampleType::U16) => LayerInfoPlacement::GlobalBlock { index: 0, signature: *b"8BIM", key: *b"Lr16", padding: None },
(true, SampleType::F32) => LayerInfoPlacement::GlobalBlock { index: 0, signature: *b"8BIM", key: *b"Lr32", padding: None },
(true, SampleType::U16) => LayerInfoPlacement::GlobalBlock { index, signature: *b"8BIM", key: *b"Lr16", padding: None },
(true, SampleType::F32) => LayerInfoPlacement::GlobalBlock { index, signature: *b"8BIM", key: *b"Lr32", padding: None },
_ => LayerInfoPlacement::Section,
};
let records = std::mem::take(&mut ex.records);
@@ -1865,6 +1869,31 @@ mod tests {
assert_eq!(unique, reloaded_ids);
}
/// #5068: Photoshop won't open a file whose `Lr32` comes before the `Mt32` marker it keeps for
/// a 32-bit document's merged transparency (verified in Photoshop 2026 on psd-tools'
/// `transparentbg.psd` re-saved both ways). The layer block follows a kept marker.
#[test]
fn layer_block_follows_a_kept_merged_transparency_marker() {
for (depth, key, marker) in [(SampleType::F32, *b"Lr32", *b"Mt32"), (SampleType::U16, *b"Lr16", *b"Mt16"), (SampleType::F32, *b"Lr32", *b"Mtrn")] {
let mut doc = Document::new("t", photocraft_geom::Size::new(8, 6), ColorMode::Rgb, depth);
let mut s = Surface::new(doc.pixel_format());
s.fill_rect(photocraft_geom::Rect::new(1, 1, 5, 4), &[0.5, 0.2, 0.9, 1.0]);
doc.layers.push(Layer::new("Layer 1", LayerContent::Raster(s)));
doc.metadata.psd_global_blocks = vec![(*b"8BIM", *b"Patt", std::sync::Arc::new(Vec::new())), (*b"8BIM", marker, std::sync::Arc::new(Vec::new()))];
let saved = PsdFile::from_bytes(&document_to_psd(&doc).to_bytes().unwrap()).unwrap();
let LayerInfoPlacement::GlobalBlock { index, key: written, .. } = saved.layer_info_placement else { panic!("{depth:?}: a global layer block") };
assert_eq!(written, key);
let at = saved.global_blocks.iter().position(|b| b.key == marker).expect("the marker is kept");
assert!(at < index, "{depth:?}: {marker:?} at {at}, the layer block at {index}");
}
// Without a marker the layer block stays first.
let mut doc = Document::new("t", photocraft_geom::Size::new(8, 6), ColorMode::Rgb, SampleType::F32);
doc.layers.push(Layer::new("Layer 1", LayerContent::Raster(Surface::new(doc.pixel_format()))));
doc.metadata.psd_global_blocks = vec![(*b"8BIM", *b"Patt", std::sync::Arc::new(Vec::new()))];
let saved = PsdFile::from_bytes(&document_to_psd(&doc).to_bytes().unwrap()).unwrap();
assert!(matches!(saved.layer_info_placement, LayerInfoPlacement::GlobalBlock { index: 0, .. }));
}
#[test]
fn unreadable_lr16_warns_on_import_and_is_not_saved_twice() {
// #2700: a 16-bit file whose Lr16 block claims five layers but holds none.