From f9f1efea6ea0502cd5a0a61372fa72c7d580f4d1 Mon Sep 17 00:00:00 2001 From: MikeeI Date: Mon, 5 Oct 2026 07:24:54 +0200 Subject: [PATCH] fix(io): preserve selected real PSD masks (#130) --- crates/io/src/psd_import.rs | 14 ++++++++++---- crates/io/tests/psd_structure.rs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/io/src/psd_import.rs b/crates/io/src/psd_import.rs index c2bdc0ce3b..921c75e9c4 100644 --- a/crates/io/src/psd_import.rs +++ b/crates/io/src/psd_import.rs @@ -67,6 +67,12 @@ fn doc_mode(m: PsdMode) -> Option { }) } +// Real-mask metadata without a -3 channel still selects the synthetic -2 mask. +fn selected_real_mask(rec: &LayerRecord) -> Option { + rec.channel(CHANNEL_REAL_USER_MASK)?; + rec.layer_mask()?.real +} + impl Ctx<'_> { fn warn(&mut self, s: impl Into) { self.warnings.push(s.into()); @@ -108,8 +114,8 @@ impl Ctx<'_> { fn record_mask(&mut self, rec: &LayerRecord, name: &str) -> Option { let m = rec.layer_mask()?; - let (id, rect, default, flags) = match (rec.channel(CHANNEL_REAL_USER_MASK), m.real) { - (Some(_), Some(real)) => (CHANNEL_REAL_USER_MASK, real.rect, real.background, real.flags), + let (id, rect, default, flags) = match selected_real_mask(rec) { + Some(real) => (CHANNEL_REAL_USER_MASK, real.rect, real.background, real.flags), _ => { rec.channel(CHANNEL_USER_MASK)?; (CHANNEL_USER_MASK, m.rect, m.default_color, m.flags) @@ -272,7 +278,7 @@ impl Ctx<'_> { // Mask flag bit 3: the user mask was rendered from vector data. For // shape/text/smart layers the cached pixels already include that // coverage, so applying it again would double-mask. - if rendered && rec.layer_mask().is_some_and(|m| m.flags & 8 != 0) { + if rendered && selected_real_mask(rec).is_none() && rec.layer_mask().is_some_and(|m| m.flags & 8 != 0) { l.mask = None; } if !matches!(l.content, LayerContent::Shape(_)) { @@ -292,7 +298,7 @@ impl Ctx<'_> { vm.density = p.vector_density.map_or(1.0, |d| f32::from(d) / 255.0); vm.feather = p.vector_feather.map_or(0.0, |f| f as f32); } - if m.flags & 8 != 0 && m.real.is_none() { + if m.flags & 8 != 0 && selected_real_mask(rec).is_none() { l.mask = None; } } diff --git a/crates/io/tests/psd_structure.rs b/crates/io/tests/psd_structure.rs index f072a06b22..2d47830e48 100644 --- a/crates/io/tests/psd_structure.rs +++ b/crates/io/tests/psd_structure.rs @@ -446,6 +446,18 @@ fn vector_rendered_mask_not_doubled_on_shapes() { let l = d.layers.last().unwrap(); assert_eq!(l.content.kind_name(), "Shape"); assert!(l.mask.is_none()); + + let record = f.layers_mut().last_mut().unwrap(); + let photocraft_psd::MaskData::Mask(mask) = &mut record.mask else { panic!("fixture has a mask") }; + mask.real = Some(photocraft_psd::RealMask { flags: 0, background: 255, rect: photocraft_psd::Rect { top: 0, left: 0, bottom: 1, right: 1 } }); + record.channels.push(photocraft_psd::ChannelData { id: -3, compression: Some(Compression::Raw), data: vec![64] }); + let (imported, _) = psd_to_document(&f); + assert_eq!(imported.layers.last().unwrap().content.kind_name(), "Shape"); + let mask = imported.layers.last().unwrap().mask.as_ref().expect("selected real mask must survive synthetic cleanup"); + assert!((mask.surface.sample_channel(0, 0, 0) - 64.0 / 255.0).abs() < 1e-6); + let exported = export(&imported, "real-mask.psd", &Default::default()).unwrap(); + let reopened = import("real-mask.psd", &exported.bytes).unwrap().document; + assert_eq!(reopened.layers.last().unwrap().mask.as_ref().unwrap().surface.pixel(0, 0), mask.surface.pixel(0, 0)); } #[test]