mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-07 02:41:47 +00:00
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:
1 parent
1025914b0e
commit
2807deb209
1 file changed
+31
-2
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user