mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 11:16:53 -04:00
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:
1 parent
7ad75b5db2
commit
ff57499774
3 files changed
+41
-3
No files matched your search
@@ -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<_>>());
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
Reference in new issue
Block a user