diff --git a/crates/io/tests/psd32_export.rs b/crates/io/tests/psd32_export.rs index cbb4a3152..9ff2b4113 100644 --- a/crates/io/tests/psd32_export.rs +++ b/crates/io/tests/psd32_export.rs @@ -203,3 +203,36 @@ fn merged_oracle_does_not_treat_extra_channels_as_transparency() { assert_eq!(merged_composite(&file).unwrap(), vec![[2.0, 2.0, 2.0, 1.0]], "{mode:?}"); } } + +// Photoshop saves a transparent 32-bit document with a positive layer count and an `Mt32` +// ("Saving Merged Transparency") global block: the merged image's first extra channel, named +// "Transparency", is its transparency (psd-tools transparentbg.psd). The oracle reads it as the +// composite's alpha, and opening the file doesn't turn it into an alpha channel. +#[test] +fn mt32_marks_the_merged_transparency() { + let mut doc = Document::new("transparent", Size::new(2, 1), ColorMode::Rgb, SampleType::F32); + let fmt = doc.pixel_format(); + let mut layer = photocraft_doc::Layer::raster("shape", fmt); + if let Some(s) = layer.surface_mut() { + s.fill_rect(photocraft_geom::Rect::new(0, 0, 1, 1), &[0.0, 0.0, 0.0, 1.0]); + } + doc.layers.push(layer); + let mut file = PsdFile::from_bytes(&export_psd(&doc)).unwrap(); + file.header.channels = 4; + if let Some(li) = file.layer_info.as_mut() { + li.merged_alpha = false; + } + file.image_data.compression = Compression::Raw; + let plane = |v: [f32; 2]| v.into_iter().flat_map(f32::to_be_bytes).collect::>(); + file.image_data.data = [plane([0.0, 1.0]), plane([0.0, 1.0]), plane([0.0, 1.0]), plane([1.0, 0.0])].concat(); + let name: Vec = [0u8, 0, 0, 13].into_iter().chain("Transparency\0".encode_utf16().flat_map(u16::to_be_bytes)).collect(); + file.resources.retain(|r| r.id != 1045); + file.resources.push(photocraft_psd::ImageResource::new(1045, name)); + assert!(!file.merged_has_alpha(), "a positive layer count alone"); + file.global_blocks.push(photocraft_psd::TaggedBlock::new(*b"Mt32", Vec::new())); + assert!(file.merged_has_alpha()); + let merged = merged_composite(&file).unwrap(); + assert_eq!(merged.iter().map(|p| p[3]).collect::>(), vec![1.0, 0.0]); + let opened = import("transparent.psd", &file.to_bytes().unwrap()).unwrap(); + assert!(opened.document.channels.is_empty(), "{:?}", opened.document.channels.iter().map(|c| &c.name).collect::>()); +} diff --git a/crates/psd/src/file.rs b/crates/psd/src/file.rs index 1e86665ad..effaa1fa1 100644 --- a/crates/psd/src/file.rs +++ b/crates/psd/src/file.rs @@ -272,7 +272,10 @@ impl PsdFile { self.image_data.decode(&self.header) } - /// `true` if the merged image carries a transparency channel. + /// `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). pub fn merged_has_alpha(&self) -> bool { let Some(color) = self.header.color_mode.color_channels() else { return false; @@ -280,8 +283,9 @@ impl PsdFile { 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) => li.merged_alpha, + Some(li) => li.merged_alpha || marked, None => true, } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index cb2ca13ac..590b9febc 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (palette PNGs open in Indexed Color mode, #4804; earlier: CMYK Invert, Posterize and Threshold on the inks; 16-bit Lab Invert; Photoshop-oracle floor 145/258; `.atn` import maps 112 events; DDS textures open, #1327; DPX read and write, #1049; #2465 fixed: CS2 placed layers with only a `PlLd` block open their contents; Photoshop Actions `.atn` read and write; `.shc` contour files read and write; earlier the same day: indexed BMP palette mode; Save keeps a JPEG's quality and a WebP's encoding) · **Target:** Adobe Photoshop 2026 +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (PSD: `Mt16`/`Mt32` merged transparency read; palette PNGs open in Indexed Color mode, #4804; earlier: CMYK Invert, Posterize and Threshold on the inks; 16-bit Lab Invert; Photoshop-oracle floor 145/258; `.atn` import maps 112 events; DDS textures open, #1327; DPX read and write, #1049; #2465 fixed: CS2 placed layers with only a `PlLd` block open their contents; Photoshop Actions `.atn` read and write; `.shc` contour files read and write; earlier the same day: indexed BMP palette mode; Save keeps a JPEG's quality and a WebP's encoding) · **Target:** Adobe Photoshop 2026 Every format Photoshop 2026 (27.11.0) reads or writes, with PhotoCraft's support, fidelity and tests. Photoshop's list comes from its `Info.plist` `CFBundleDocumentTypes`, the PiPL read/write @@ -142,6 +142,7 @@ remaining gaps in #1606 stay tracked with per-sample corpus ceilings. | Date | Change | Summary | |---|---|---| +| 2026-10-13 | minor | PSD: a "Saving Merged Transparency" global block (`Mtrn`, `Mt16`, `Mt32`; Photoshop's 32-bit documents keep a positive layer count) marks the merged image's first extra channel as its transparency: opening such a file no longer adds a "Transparency" alpha channel, and the oracle compares psd-tools transparentbg.psd's alpha (83.7 % → 0.9 % of pixels off; its shape edges remain) | | 2026-10-13 | minor | Palette PNGs open in Indexed Color mode (#4804) | | 2026-10-12 | minor | `.atn`: 112 events mapped (Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, points, Bitmap) | | 2026-10-12 | minor | DDS textures open, read-only (#1327) |