PSD: Mt16/Mt32 blocks mark the merged image's transparency (#5178)

The merged image carries its transparency in the first extra channel
when the layer count is stored negative, or when a "Saving Merged
Transparency" global block (Mtrn, Mt16, Mt32) is present. Photoshop
saves 32-bit documents the second way: a positive layer count, Mt32,
and a fourth channel named "Transparency" (psd-tools transparentbg).
merged_has_alpha only knew the first, so opening such a file added a
"Transparency" alpha channel to the document, and the oracle compared
it as if it were opaque white (83.7 % of pixels off; now 0.9 %, all on
the shape's edge).

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Timothy DalyandClaude Opus 5.5 authored and GitHub committed 2026-10-14 01:59:32 -04:00
1 parent 7ad75b5db2
commit ff57499774
3 files changed
+41 -3

No files matched your search

+33
View File
@@ -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:?}"); 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::<Vec<u8>>();
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<u8> = [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<_>>(), 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::<Vec<_>>());
}
+6 -2
View File
@@ -272,7 +272,10 @@ impl PsdFile {
self.image_data.decode(&self.header) 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 { pub fn merged_has_alpha(&self) -> bool {
let Some(color) = self.header.color_mode.color_channels() else { let Some(color) = self.header.color_mode.color_channels() else {
return false; return false;
@@ -280,8 +283,9 @@ impl PsdFile {
if self.header.channels <= color { if self.header.channels <= color {
return false; return false;
} }
let marked = self.global_blocks.iter().any(|b| matches!(&b.key, b"Mtrn" | b"Mt16" | b"Mt32"));
match &self.layer_info { match &self.layer_info {
Some(li) => li.merged_alpha, Some(li) => li.merged_alpha || marked,
None => true, None => true,
} }
} }
+2 -1
View File
@@ -1,6 +1,6 @@
# File-format parity # 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 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 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 | | 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-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 | `.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) | | 2026-10-12 | minor | DDS textures open, read-only (#1327) |