mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
PSD import: a pattern-filled shape opens as a pattern fill layer (#2059)
Since #1917 the exporter writes pattern fill pixels; a vector-masked pattern fill then reopened as a shape layer covering the canvas, failing the corpus round-trip floor (adjustment-fillers.psd). Pattern-filled plain shapes now import as pattern fill layers with their vector mask. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b8fe7dd5ee
commit
9b0c6c4d74
2 files changed
+37
-8
No files matched your search
@@ -84,8 +84,8 @@ fn selected_real_mask(rec: &LayerRecord) -> Option<photocraft_psd::RealMask> {
|
||||
|
||||
/// 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<photocraft_doc::Fill> {
|
||||
/// 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)));
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user