diff --git a/crates/io/src/psd_import.rs b/crates/io/src/psd_import.rs index d9a507a8b6..6490471f33 100644 --- a/crates/io/src/psd_import.rs +++ b/crates/io/src/psd_import.rs @@ -84,8 +84,8 @@ fn selected_real_mask(rec: &LayerRecord) -> Option { /// The fill of a plain shape layer (fill block + vector path, no stroke) to import as a fill layer /// with a vector mask: when its mask parameters give the vector mask a density below 100 % or a -/// feather, or when it stores no pixels and fills with a pattern. -fn soft_shape_fill(rec: &LayerRecord, has_vector: bool, fill_key: Option<&[u8; 4]>) -> Option { +/// feather (`.1` true), or when it fills with a pattern. +fn soft_shape_fill(rec: &LayerRecord, has_vector: bool, fill_key: Option<&[u8; 4]>) -> Option<(photocraft_doc::Fill, bool)> { if !has_vector || rec.block(b"vstk").is_some() || rec.block(b"vscg").is_some() { return None; } @@ -93,10 +93,12 @@ fn soft_shape_fill(rec: &LayerRecord, has_vector: bool, fill_key: Option<&[u8; 4 let fill = rec.block(k).and_then(|b| blocks::parse_fill(k, &b.data))?; let p = rec.layer_mask().and_then(|m| m.parameters); let soft = p.is_some_and(|p| p.vector_density.is_some_and(|d| d < 255) || p.vector_feather.is_some_and(|f| f > 0.0)); - // Without stored pixels a pattern-filled shape cannot be rasterized on its own (the - // compositor resolves the document's patterns for fill layers). - let unrendered_pattern = (rec.rect.is_empty() || rec.rect.size().is_err()) && matches!(fill, photocraft_doc::Fill::Pattern { .. }); - (soft || unrendered_pattern).then_some(fill) + // A pattern-filled shape can't be rasterized on its own (the compositor resolves the + // document's patterns for fill layers), and its stored pixels may be the unclipped fill + // (our own export writes fill layers so, #1907): it is a pattern fill layer seen through + // its vector mask. + let pattern = matches!(fill, photocraft_doc::Fill::Pattern { .. }); + (soft || pattern).then_some((fill, soft)) } impl Ctx<'_> { @@ -335,10 +337,14 @@ impl Ctx<'_> { // Distort / Perspective: the fourth corner (the affine `transform` drops it). perspective: rec.block(k).and_then(|b| blocks::parse_smart_perspective(k, &b.data)), }) - } else if let Some(f) = soft_shape_fill(rec, vector_key.is_some(), fill_key) { + } else if let Some((f, soft)) = soft_shape_fill(rec, vector_key.is_some(), fill_key) { // A shape whose vector mask has a density or feather is a fill layer seen through a // soft vector mask: the fill shows beyond the path, which the stored pixels (the - // shape alone) lack. Import it as exactly that. + // shape alone) lack. Import it as exactly that. A pattern fill keeps the stored + // pixels as its rendering (a soft mask's pixels lack the fill beyond the path). + if !soft && matches!(f, photocraft_doc::Fill::Pattern { .. }) && !rec.rect.is_empty() { + fill_cache = Some(FillCache { fill: f.clone(), surface: self.record_surface(rec, &name) }); + } LayerContent::Fill(f) } else if (vector_key.is_some() && (fill_key.is_some() || rec.block(b"vstk").is_some())) || rec.block(b"vscg").is_some() { let fill = fill_key.and_then(|k| rec.block(k).and_then(|b| blocks::parse_fill(k, &b.data))); diff --git a/crates/io/tests/pattern_fill_psd.rs b/crates/io/tests/pattern_fill_psd.rs index 5ca00152b6..8c9b8ab611 100644 --- a/crates/io/tests/pattern_fill_psd.rs +++ b/crates/io/tests/pattern_fill_psd.rs @@ -59,3 +59,26 @@ fn pattern_fill_survives_psd_and_layered_tiff_in_every_mode() { } } } + +/// A pattern fill seen through a vector mask (Photoshop's pattern-filled shape) stays a pattern +/// fill layer after a save and reopen: its stored pixels are the unclipped fill, which must not +/// turn it into a shape layer drawing the pattern over the whole canvas (psd-tools +/// adjustment-fillers.psd). +#[test] +fn vector_masked_pattern_fill_survives_psd() { + for mode in [ColorMode::Rgb, ColorMode::Cmyk] { + let mut d = doc(mode, SampleType::U8); + let fmt = d.pixel_format(); + let mut white = Layer::raster("Background", fmt); + white.surface_mut().expect("raster").fill_rect(Rect::new(0, 0, 40, 24), &photocraft_raster::from_rgba(&fmt, [1.0, 1.0, 1.0, 1.0])); + d.layers.insert(0, white); + d.layers[1].vector_mask = Some(photocraft_doc::VectorMask::new(photocraft_vector::shapes::rect(8.0, 4.0, 16.0, 12.0))); + let r = export(&d, "x.psd", &ExportOptions::default()).expect("export"); + let back = import("x.psd", &r.bytes).expect("import").document; + let l = back.layers.iter().find(|l| l.name == "Pattern Fill 1").expect("layer"); + assert!(matches!(l.content, LayerContent::Fill(Fill::Pattern { .. })), "{mode:?}: reopened as a pattern fill"); + assert!(l.vector_mask.is_some(), "{mode:?}: keeps its vector mask"); + let diff = max_diff(&d, &back); + assert!(diff < 0.02, "{mode:?}: the reopened layer differs by {diff}"); + } +}