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:?}");
}
}
// 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)
}
/// `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,
}
}
+2 -1
View File
@@ -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) |