test(engine): a Displace smart filter keeps its embedded map; an outside new map is refused before it is read (#7423)

This commit is contained in:
Korg1ch
2026-10-21 18:26:00 +03:00
parent 3c7ee9eded
commit 66ff3bf7eb
5 changed files with 194 additions and 0 deletions
@@ -257,3 +257,26 @@ fn an_ambient_map_or_lut_never_renders_unfiltered_pixels_into_the_stack_cache()
assert_eq!(bytes, smart_cmds::stack_cache_bytes(), "a denied render cached no stage");
}
}
/// Choosing another Displace map through `setParams` embeds it (#7423), so the new map is read
/// before the edit: a path outside the workspace is refused before that read, and the filter
/// keeps the map it had.
#[test]
fn a_new_displace_map_outside_the_workspace_is_refused_before_it_is_read() {
let f = Fixture::new();
let (mut h, layer) = f.setup(false);
let params = |h: &Headless| {
let d = h.session.active().unwrap();
let LayerContent::Smart(sm) = &d.doc.layer(layer).unwrap().content else { panic!("smart object") };
sm.smart_filters[0].params.clone()
};
let before = params(&h);
let map = photocraft_io::smart_map::displace_embedded_map(&before).expect("the open document's map, embedded");
assert!(map.data.iter().all(|v| *v < 0.9), "the grey map");
let doc = h.session.active().unwrap().doc.clone();
let edit = json!({"layer": layer.0, "params": {"mapPath": f.outside("white.png")}});
assert_denied(h.session.execute("layer.smartFilter.setParams", edit.clone()));
assert_denied(h.handle("engine.execute", json!({"command": "layer.smartFilter.setParams", "params": edit})));
assert!(Arc::ptr_eq(&doc, &h.session.active().unwrap().doc));
assert_eq!(params(&h), before, "no white map was read into the filter");
}
+30
View File
@@ -191,6 +191,36 @@ fn smart_filters_reread_only_followed_files() {
assert_eq!(replayed(&mut s, id, json!({"file": network_alias(&d.join("invert.cube"))})), unfiltered);
}
/// #7423 (review on #7799): Displace on a Smart Object keeps a `mapPath` as a path, as before
/// (only a map from an open document is embedded). A path relative to the saved document, set
/// through the filter's own edit, displaces as the absolute one did, and the dialog's "None"
/// (`mapDocument: -1`) names no map instead of failing.
#[test]
fn smart_displace_keeps_a_relative_map_path() {
let d = dir("displace-relative");
png(&d.join("ramp.png"), |x| [(x * 8) as u8, 0, 0]);
png(&d.join("map.png"), |_| [255, 255, 255]);
let (mut s, id) = document(&d, &d.join("ramp.png"));
let path = d.join("map.png").to_string_lossy().into_owned();
let unfiltered = photocraft_compose::render(&s.active().unwrap().doc, s.active().unwrap().doc.bounds()).px;
s.execute("filter.distort.displace", json!({"layer": id.0, "mapPath": path, "horizontal": 50, "vertical": 0})).unwrap();
let state = |s: &Session| {
let doc = &s.active().unwrap().doc;
let Some(LayerContent::Smart(sm)) = doc.layer(id).map(|l| &l.content) else { panic!("a smart object") };
(sm.smart_filters[0].params.clone(), photocraft_compose::render(doc, doc.bounds()).px)
};
let (params, displaced) = state(&s);
assert_eq!(params["mapPath"], json!(path));
assert!(photocraft_io::smart_map::displace_embedded_map(&params).is_none(), "a path, not an embedded map");
assert_ne!(displaced, unfiltered, "the map moves the pixels");
s.execute("layer.smartFilter.setParams", json!({"layer": id.0, "index": 0, "params": {"mapPath": "map.png"}})).unwrap();
let (params, relative) = state(&s);
assert_eq!(params["mapPath"], json!("map.png"));
assert_eq!(relative, displaced, "a relative map next to the document");
s.execute("layer.smartFilter.setParams", json!({"layer": id.0, "index": 0, "params": {"mapDocument": -1}})).unwrap();
assert_eq!(state(&s).1, displaced, "\"None\" changes nothing");
}
#[test]
fn data_sets_and_footage_read_only_followed_files() {
let d = dir("datasets");
+61
View File
@@ -952,4 +952,65 @@ mod tests {
assert!(calls.load(std::sync::atomic::Ordering::SeqCst) > 2);
assert!(new.tiles().all(|(c, _)| got.tile(*c).is_some()));
}
/// #7423: Displace on a Smart Object keeps its map (read from an open document) in the smart
/// filter, as Photoshop embeds it, so the filter displaces and keeps displacing when the
/// object re-renders. A map chosen again replaces it.
#[test]
fn displace_on_a_smart_object_embeds_its_map() {
let mut s = Session::new();
// Documents 0 and 1: a white and a black displacement map.
s.execute("file.new", json!({"width": 8, "height": 8, "background": "white"})).unwrap();
s.execute("file.new", json!({"width": 8, "height": 8, "background": "black"})).unwrap();
s.execute("file.new", json!({"width": 48, "height": 32, "background": "transparent"})).unwrap();
s.edit("square", |doc, active| {
let fmt = doc.pixel_format();
let surf = doc.layer_mut(active.unwrap()).unwrap().surface_mut().unwrap();
surf.fill_rect(photocraft_geom::Rect::new(10, 10, 20, 12), &photocraft_raster::from_rgba(&fmt, [1.0, 0.0, 0.0, 1.0]));
Ok(())
})
.unwrap();
s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap();
let original = {
let d = s.active().unwrap();
let LayerContent::Smart(sm) = &d.doc.layer(d.active_layer.unwrap()).unwrap().content else { panic!("not a smart object") };
sm.cache.as_ref().unwrap().read_region(d.doc.bounds())
};
let smart = |s: &Session| {
let d = s.active().unwrap();
let LayerContent::Smart(sm) = &d.doc.layer(d.active_layer.unwrap()).unwrap().content else { panic!("not a smart object") };
(sm.smart_filters[0].params.clone(), sm.cache.as_ref().unwrap().read_region(d.doc.bounds()))
};
let first_value = |params: &Value| photocraft_io::smart_map::displace_embedded_map(params).expect("an embedded map").data[0];
s.execute("filter.distort.displace", json!({"horizontal": 10, "vertical": 0, "mapDocument": 0})).unwrap();
let (params, displaced) = smart(&s);
assert!(params.get("mapDocument").is_none(), "{params}");
assert_eq!(first_value(&params), 1.0, "the white map");
assert_ne!(displaced, original, "the smart object is displaced");
// Re-rendered from its params alone (the filter hidden and shown again): still displaced.
s.execute("layer.smartFilter.setVisible", json!({"index": 0, "visible": false})).unwrap();
assert_eq!(smart(&s).1, original);
s.execute("layer.smartFilter.setVisible", json!({"index": 0, "visible": true})).unwrap();
assert_eq!(smart(&s).1, displaced);
// Another map chosen for the filter replaces the embedded one.
s.execute("layer.smartFilter.setParams", json!({"index": 0, "params": {"mapDocument": 1}})).unwrap();
let (params, again) = smart(&s);
assert!(params.get("mapDocument").is_none(), "{params}");
assert_eq!(first_value(&params), 0.0, "the black map");
assert_ne!(again, displaced, "displaced the other way");
// The stored params passed back whole (the old embedded map included) with another map
// named: the named map wins, for the filter's own edit and for a new Displace alike.
let mut back = params.as_object().unwrap().clone();
back.insert("mapDocument".into(), json!(0));
s.execute("layer.smartFilter.setParams", json!({"index": 0, "params": back.clone()})).unwrap();
assert_eq!(first_value(&smart(&s).0), 1.0, "the white map, not the embedded black one");
back.insert("mapDocument".into(), json!(1));
s.execute("filter.distort.displace", Value::Object(back)).unwrap();
let d = s.active().unwrap();
let LayerContent::Smart(sm) = &d.doc.layer(d.active_layer.unwrap()).unwrap().content else { panic!("not a smart object") };
assert_eq!(first_value(&sm.smart_filters[1].params), 0.0, "the black map, not the passed-back white one");
}
}
+34
View File
@@ -407,4 +407,38 @@ mod tests {
}
assert!(embedded_map(&json!({TEMPLATE_KEY: "0".repeat(MAX_TEMPLATE_BYTES * 2 + 2)})).is_none());
}
/// #7423: a map from a file or an open document, embedded for a smart filter, is read back as
/// the filter's map and written to PSD like an imported one.
#[test]
fn an_embedded_map_reads_back_and_exports() {
let rect = Rect::new(0, 0, 3, 2);
// RGBA: red is the horizontal map, green the vertical one.
let rgba = Image { rect, ch: 4, data: (0..6).flat_map(|i| [i as f32 / 5.0, 1.0 - i as f32 / 5.0, 0.0, 1.0]).collect() };
let params = json!({"horizontal": 20, "vertical": -5, "fit": "tile", "undefinedAreas": "wrap", "mapDocument": 0, "mapLayer": 3});
let embedded = embed_map(&params, &rgba).unwrap();
for key in ["mapPath", "mapDocument", "mapLayer"] {
assert!(embedded.get(key).is_none(), "{key}");
}
assert_eq!(embedded["horizontal"], json!(20));
let map = embedded_map(&embedded).unwrap();
assert_eq!((map.rect, map.ch), (rect, 3));
let h: Vec<f32> = map.data.chunks(3).map(|p| p[0]).collect();
let v: Vec<f32> = map.data.chunks(3).map(|p| p[1]).collect();
assert_eq!(h, [0.0, 51.0 / 255.0, 102.0 / 255.0, 153.0 / 255.0, 204.0 / 255.0, 1.0]);
assert_eq!(v, [1.0, 204.0 / 255.0, 153.0 / 255.0, 102.0 / 255.0, 51.0 / 255.0, 0.0]);
// Written to PSD with the params' own settings.
let f = export_item(&embedded).unwrap();
let fltr = get_desc(&f, "Fltr").unwrap();
assert_eq!(num(fltr.get("HrzS")), Some(20.0));
assert_eq!(enum_of(fltr, "DspM"), Some(&b"Tile"[..]));
// A grey map displaces the same both ways.
let grey = Image { rect, ch: 1, data: vec![0.25; 6] };
let map = embedded_map(&embed_map(&json!({}), &grey).unwrap()).unwrap();
assert_eq!(map.ch, 1);
// Nothing to embed.
assert!(embed_map(&json!({}), &Image { rect: Rect::new(0, 0, 0, 0), ch: 4, data: vec![] }).is_none());
assert!(embed_map(&json!({}), &Image { rect, ch: 4, data: vec![0.0; 5] }).is_none(), "short data");
assert!(embed_map(&json!(3), &grey).is_none(), "params not an object");
}
}
+46
View File
@@ -1779,6 +1779,52 @@ mod tests {
assert_eq!(label("wavelengthMin"), "Wavelength Min");
}
/// #7423 (review on #7799): re-editing a Displace smart filter and picking another open
/// document as its map previews with that document, as OK applies it. The preview session held
/// only the edited document, and the map's `mapDocument` sits inside the filter's `params`.
#[test]
fn smart_displace_preview_uses_the_map_document() {
let mut s = photocraft_engine::Session::new();
// Documents 0 and 1: a white and a black map, opened before the edited one.
s.execute("file.new", json!({"width": 8, "height": 8, "background": "white"})).unwrap();
s.execute("file.new", json!({"width": 8, "height": 8, "background": "black"})).unwrap();
s.execute("file.new", json!({"width": 48, "height": 32, "background": "transparent"})).unwrap();
s.edit("square", |doc, active| {
let fmt = doc.pixel_format();
let surf = doc.layer_mut(active.unwrap()).unwrap().surface_mut().unwrap();
surf.fill_rect(photocraft_geom::Rect::new(10, 10, 20, 12), &photocraft_raster::from_rgba(&fmt, [1.0, 0.0, 0.0, 1.0]));
Ok(())
})
.unwrap();
s.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap();
s.execute("filter.distort.displace", json!({"horizontal": 10, "vertical": 0, "mapDocument": 0})).unwrap();
let doc = s.active().unwrap().doc.clone();
let layer = s.active().unwrap().active_layer;
let documents: Vec<_> = s.documents().iter().map(|d| d.doc.clone()).collect();
for map in [1, 0] {
let p = json!({"index": 0, "params": {"mapDocument": map}});
let mut applied = photocraft_engine::Session::new();
for d in &documents {
applied.add_document((**d).clone(), None);
}
applied.execute(SMART_FILTER_SET_PARAMS, p.clone()).unwrap();
let want = photocraft_compose::render(&applied.active().unwrap().doc, doc.bounds()).px;
let preview = preview_document_for_target(
&doc,
layer,
SMART_FILTER_SET_PARAMS,
&p,
1,
None,
photocraft_engine::channel_cmds::ChannelTarget::Composite,
&documents,
&ColorSettings::default(),
)
.unwrap_or_else(|| panic!("map {map}: no preview"));
assert!(photocraft_compose::render(&preview, doc.bounds()).px == want, "map {map}: the preview differs from what OK applies");
}
}
/// Match Color's preview reads the Source document from the open documents, as OK does: the
/// preview session held only the edited document, so a Source showed no change (#4851).
#[test]