mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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(¶ms).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");
|
||||
|
||||
@@ -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(¶ms), 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(¶ms), 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¶ms, &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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user