diff --git a/crates/io/tests/psd16_merged_transparency.rs b/crates/io/tests/psd16_merged_transparency.rs new file mode 100644 index 0000000000..3f04970626 Binary files /dev/null and b/crates/io/tests/psd16_merged_transparency.rs differ diff --git a/crates/io/tests/psd32_export.rs b/crates/io/tests/psd32_export.rs index 9ff2b4113b..b4707fadb0 100644 --- a/crates/io/tests/psd32_export.rs +++ b/crates/io/tests/psd32_export.rs @@ -226,11 +226,11 @@ fn mt32_marks_the_merged_transparency() { 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)); + file.resources.retain(|r| !matches!(r.id, 1006 | 1045)); 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()); + file.resources.push(photocraft_psd::ImageResource::new(1045, name)); 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(); diff --git a/crates/psd/src/file.rs b/crates/psd/src/file.rs index e14bc45aaf..fab262732b 100644 --- a/crates/psd/src/file.rs +++ b/crates/psd/src/file.rs @@ -354,7 +354,10 @@ impl PsdFile { /// `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). + /// layer count (psd-tools transparentbg.psd). Photoshop also names the merged transparency + /// "Transparency" and lists it first among the alpha channel names, and some of its 16-bit + /// PSBs carry no other mark (#4654): when the layers sit in a global `Lr16`/`Lr32` block, that + /// name with one name per extra channel marks it too. pub fn merged_has_alpha(&self) -> bool { let Some(color) = self.header.color_mode.color_channels() else { return false; @@ -364,11 +367,40 @@ impl PsdFile { } 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 || marked, + Some(li) => { + let in_block = matches!(self.layer_info_placement, LayerInfoPlacement::GlobalBlock { .. }); + li.merged_alpha || marked || (in_block && self.names_merged_transparency(self.header.channels - color)) + } None => true, } } + /// `true` when the alpha channel names (resource 1045, else 1006) hold exactly `extra` names + /// and the first is "Transparency", Photoshop's name for the merged transparency. + fn names_merged_transparency(&self, extra: u16) -> bool { + const NAME: &str = "Transparency"; + let mut names = Vec::new(); + if let Some(r) = self.resource(1045) { + let mut at = 0usize; + while let Some(len) = r.data.get(at..at.saturating_add(4)).and_then(|b| b.try_into().ok()).map(u32::from_be_bytes) { + at = at.saturating_add(4); + let end = (len as usize).checked_mul(2).and_then(|n| at.checked_add(n)); + let Some(units) = end.and_then(|end| r.data.get(at..end)) else { break }; + let units: Vec = units.as_chunks::<2>().0.iter().map(|c| u16::from_be_bytes(*c)).collect(); + names.push(String::from_utf16_lossy(&units).trim_end_matches('\0').to_string()); + at = at.saturating_add(units.len() * 2); + } + } else if let Some(r) = self.resource(1006) { + let mut at = 0usize; + while let Some(&len) = r.data.get(at) { + let Some(name) = r.data.get(at + 1..at + 1 + usize::from(len)) else { break }; + names.push(String::from_utf8_lossy(name).into_owned()); + at += 1 + usize::from(len); + } + } + names.len() == usize::from(extra) && names.first().is_some_and(|n| n == NAME) + } + /// Checks structural invariants that the writer relies on. pub fn validate(&self) -> Result<()> { self.header.validate()?;