mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-11 04:40:31 +00:00
fix(engine): Displace smart filter ignores its mapPath displacement map (#3676)
A Displace applied to a smart object is recorded as a smart filter and rendered by apply_smart_filters -> filters::apply_filter_to_surface, both when it is added and on every later re-render (replace contents, toggling a filter, transforms). That path built FilterParams with params_for alone, which leaves Displace's map as None; only run_filter resolved `mapPath` (through filters_ext::resolve, which needs the session). With no map, algo's displace returns the source unchanged, so the smart filter was a silent no-op. apply_filter_to_surface now resolves `mapPath` through the same file read and import the direct command uses (filters_ext::map_from_path, shared by resolve and the new session-free resolve_map_path). A map that can no longer be read on re-render leaves the pixels alone, as for an unknown filter; the command still refuses an unreadable map when the filter is added. `mapDocument` needs the session and stays unresolved on re-render. Regression test: smart_cmds::tests:: displace_smart_filter_reads_its_map_path_on_re_render (8/16/32-bit). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
64a02107ba
commit
2cb396d154
3 files changed
+60
-5
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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<u64>) -> Result<Option<algo::Image>> {
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user