diff --git a/crates/engine/src/filters.rs b/crates/engine/src/filters.rs index 2ccc999b40..40b51189e2 100644 --- a/crates/engine/src/filters.rs +++ b/crates/engine/src/filters.rs @@ -185,7 +185,10 @@ pub fn apply_filter_to_surface( if id == crate::plugin_cmds::RUN { return crate::plugin_cmds::apply_to_surface(id, params, surf, selection, canvas); } - let fp = params_for(id, params)?; + let mut fp = params_for(id, params)?; + // A map that can no longer be read (moved or deleted since) leaves the pixels alone, like an + // unknown filter; the command itself already refused an unreadable map when it was added. + crate::filters_ext::resolve_map_path(&mut fp, params).ok()?; let sel_bounds = selection.map(photocraft_raster::Surface::content_bounds); let content = surf.content_bounds(); let area = algo::output_area(&fp, content, bounds, sel_bounds); diff --git a/crates/engine/src/filters_ext.rs b/crates/engine/src/filters_ext.rs index bf1656cae7..f1787fd1e3 100644 --- a/crates/engine/src/filters_ext.rs +++ b/crates/engine/src/filters_ext.rs @@ -435,10 +435,8 @@ pub(crate) fn resolve(s: &Session, fp: &mut FilterParams, p: &Value) -> Result<( return Ok(()); } let layer = p.get("mapLayer").and_then(Value::as_u64); - let img = if let Some(path) = p.get("mapPath").and_then(Value::as_str).filter(|p| !p.is_empty()) { - let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("cannot read displacement map `{path}`: {e}")))?; - let imported = crate::file_cmds::import(path, &bytes).map_err(|e| EngineError::Other(format!("cannot open displacement map `{path}`: {e}")))?; - map_from_document(&imported, layer) + let img = if let Some(path) = map_path(p) { + map_from_path(path, layer)? } else if let Some(d) = p.get("mapDocument") { let docs = s.documents(); let found = match d { @@ -454,6 +452,30 @@ pub(crate) fn resolve(s: &Session, fp: &mut FilterParams, p: &Value) -> Result<( Ok(()) } +fn map_path(p: &Value) -> Option<&str> { + p.get("mapPath").and_then(Value::as_str).filter(|p| !p.is_empty()) +} + +/// Displace's map read from a file (flattened, or its `mapLayer`). +fn map_from_path(path: &str, layer: Option) -> Result> { + let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("cannot read displacement map `{path}`: {e}")))?; + let imported = crate::file_cmds::import(path, &bytes).map_err(|e| EngineError::Other(format!("cannot open displacement map `{path}`: {e}")))?; + Ok(map_from_document(&imported, layer)) +} + +/// The session-free part of [`resolve`]: Displace's `mapPath`. Smart filters record params, not +/// the map, so their re-render reads the map again here; without it the filter passed the pixels +/// through. `mapDocument` needs the session and is left unresolved. +pub(crate) fn resolve_map_path(fp: &mut FilterParams, p: &Value) -> Result<()> { + if let FilterParams::Displace { map, .. } = fp + && map.is_none() + && let Some(path) = map_path(p) + { + *map = map_from_path(path, p.get("mapLayer").and_then(Value::as_u64))?.map(Arc::new); + } + Ok(()) +} + /// Resolves inputs read from the target layer itself: the layer mask as Lens Blur depth. pub(crate) fn resolve_in_layer(fp: &mut FilterParams, layer: &Layer, bounds: Rect) { if let FilterParams::LensBlur { depth: DepthSource::LayerMask, depth_map, .. } = fp diff --git a/crates/engine/src/smart_cmds/tests.rs b/crates/engine/src/smart_cmds/tests.rs index 2ee54322bf..90bb325a2c 100644 --- a/crates/engine/src/smart_cmds/tests.rs +++ b/crates/engine/src/smart_cmds/tests.rs @@ -977,3 +977,33 @@ fn inspect_reports_the_smart_source() { assert_eq!(source(&s)["kind"], "embedded"); assert!(source(&s)["fileName"].is_string()); } + +#[test] +fn displace_smart_filter_reads_its_map_path_on_re_render() { + let dir = std::env::temp_dir().join(format!("pcraft-displace-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + // A white map displaces by the full scale; mid-grey would not move anything. + let white = Document::with_background( + "map", + Size::new(W as u32, H as u32), + photocraft_color::ColorMode::Rgb, + photocraft_color::SampleType::U8, + photocraft_color::Color::rgba(1.0, 1.0, 1.0, 1.0), + ); + let map = dir.join("white.png"); + std::fs::write(&map, photocraft_io::export(&white, "png", &Default::default()).unwrap().bytes).unwrap(); + for depth in DEPTHS { + let mut s = session(depth); + paint(&mut s); + convert(&mut s); + let before = flat(&s); + s.execute("filter.distort.displace", json!({"horizontal": 8, "vertical": 8, "mapPath": map.to_str().unwrap()})).unwrap(); + assert!(max_diff(&before, &flat(&s)) > 0.1, "the smart filter displaced the pixels at {depth}-bit"); + // A later re-render (here: hide and show the filter) reads the map again. + s.execute("layer.smartFilter.setVisible", json!({"index": 0, "visible": false})).unwrap(); + assert_eq!(max_diff(&before, &flat(&s)), 0.0, "hidden at {depth}-bit"); + s.execute("layer.smartFilter.setVisible", json!({"index": 0, "visible": true})).unwrap(); + assert!(max_diff(&before, &flat(&s)) > 0.1, "shown again at {depth}-bit"); + } + std::fs::remove_dir_all(&dir).ok(); +}