From cd75fa78efe47736d90542006f52e922eaeef416 Mon Sep 17 00:00:00 2001 From: Higgs32584 Date: Fri, 16 Oct 2026 17:38:42 -0400 Subject: [PATCH] PSD/PSB: a 16/32-bit file's "Transparency" channel stays the merged transparency on re-save (#4654) Photoshop's 16-bit PSBs can mark the merged transparency only by naming it "Transparency" in the alpha channel names (positive Lr16 layer count, no Mt16). The importer read it as a user alpha channel, and the next save wrote a second one. merged_has_alpha now also accepts that name when the layers sit in a global Lr16/Lr32 block and the names cover every extra channel. Co-Authored-By: Claude Opus 5.5 --- crates/io/tests/psd16_merged_transparency.rs | Bin 0 -> 3511 bytes crates/io/tests/psd32_export.rs | 4 +-- crates/psd/src/file.rs | 36 +++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 crates/io/tests/psd16_merged_transparency.rs diff --git a/crates/io/tests/psd16_merged_transparency.rs b/crates/io/tests/psd16_merged_transparency.rs new file mode 100644 index 0000000000000000000000000000000000000000..3f04970626e7f21e776f84241be0ee3a46d66a99 GIT binary patch literal 3511 zcmb7HZExC05Wdg+iV2lc(^3OTiYm9X(!EqV>8e&q@4n=av)D^)#oqOK*G;&P|9;Qx z+CWIs>QJP>!o2LvGtV;~jSlE@B~`92NvM=+`O1iI?h}2KRLP2RL#3#%aI96aqQ$B=LV(;FQlgY}C1qvEW|ERjC?yL@ zbfawKj{0ABJoPuffJPd8>Z6V@3jd1DE8LfSRV zVNFIR8h5ohQVO1(OkbZ2LiXc)ZElS~lc~G(m$RQKSI%24D_vX4grTKw>Mooa!mZ0M zN1#HikdM0>+gJj|3Y}cR42ftZ_p;||4e5SGLOR_v2c5XCwUy`x z;$mQ#;>ISmSlT$zg|4IM_80#DNvDW)d9#v4C9ITPlY-6EJ8aI&1?B=+%4r zOD6BmYz?5lr`sOkRY*%`x|BgGtQgSDK|SoEm=M^hgF(2=RT`}8HPvCB(ro6#hoz|8 z>0upuds(3Px0ol})QZDJk&@TJfKb#fT!}(XEgR_*BQ?qPF~Upy_*ro z2@9#<)8C+Tt6-rI*)j-=mj0k!z63el?jdpzAnj{V6~smd2&9U4=FjH z%;$qa2qYE&PB7R;Ms32XFLFgs@i0RZBgj2&~uBIdzF5T>};6JWec zfSX<{bxotPA?Zj zf3ZirE=z_h{DxJ2Ba7IlwA)dR&)++SG`_i>nS#E_L?sa(UWTB$#QGd2L&D$dA)O$T z<015>k-OdOt^_hLh~5r)2R4WNLwUoj9ULO`&=q$`hrhYA&y~%!GEtPurDzJkZxG_w z48*>->JLIUKihMNoss)h6$Tn)WBJcPz?p=Z6sfE+2~Ovr4<~5iE%)j!0x1_P9xor$ zP7bqegWzxxUdh#v7X44QR()E#cav%N?)a#`k0Rf~gWv!W z+BBf^u}dWY_%`js;*s%+#5`aiHMEj;9}ip>CX1Fd-!NyMj~h(X*!@k8=OL4C-0BK| zf;-$({Ga+EmMREf>?n`Lrw|tGQw5GrVce1Xhr66D1O0ke(#>V3*isZy} zZ?sX4tqaG;I|~HpDT;nC#icxL1-Ttts50P)3OrTjA61#r%JyxUx8S|Eay}RrF1*G@ ziyd@EIhIBYKJy?(9uc`_juW=fb88{Mu)(4?0v16R?V`h#>|Pvz7f#>Vm>cWkZHB$u z72Hw)@k>I!2ZJ7J#0>L@*0J{vfb2cYP3%^hJ>l)YSyNu~?_CH7y4-ilBCU)l^FlU?8_sJ9cI~3V%dQ5jR<;ROBo-7`l$~K6Ta^@`V zjR_9h<2~$mExBjZ^%r|3FNJHhrWaVcW_t@e_Zd$&*a@($LBn?VgVLrd@R-+k&q S&+Y|+i0|IO?>(); 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()?;