Files
photocraft/crates/engine/src/doc_path/tests.rs
T
aiongg 0a328189aa Smart filters, data sets, video: re-read stored files only when local or on the document's share
Paths these features keep in the document go through the same resolver as
linked smart objects (relative to the document's folder; other computers,
devices and URLs refused):

- A Displace smart filter's `mapPath` and a Color Lookup smart filter's LUT
  `file`, re-read on every re-render: a path that is not followed leaves
  the pixels as they are, as for a missing map or LUT.
- Apply Data Set's Pixel Replacement images.
- Reload Frame of a video layer's footage.

Apply Data Set and Reload Frame say why a path is not followed.
2026-10-16 16:53:28 +00:00

228 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Each kind of place that follows a path stored in a document goes through [`super::resolve`]: a
//! link to another computer is never followed, and a relative link next to the document is.
use std::path::{Path, PathBuf};
use photocraft_doc::{LayerContent, LayerId, SmartSource};
use photocraft_geom::Rect;
use serde_json::json;
use crate::Session;
/// A link to a share on a TEST-NET-1 address, which never answers.
const REMOTE: &str = r"\\192.0.2.1\share\x.png";
/// An empty folder of its own (tests run in parallel).
fn dir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("photocraft-doc-path-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
/// A 32×32 PNG whose pixel at column `x` is `rgb(x)`.
fn png(path: &Path, rgb: impl Fn(u32) -> [u8; 3]) {
let mut px = Vec::new();
for _ in 0..32 {
for x in 0..32 {
px.extend(rgb(x));
}
}
let image = photocraft_codecs::Image::from_u8(32, 32, photocraft_codecs::ChannelLayout::Rgb, px).unwrap();
std::fs::write(path, photocraft_codecs::encode(&image, photocraft_codecs::Format::Png, &Default::default()).unwrap()).unwrap();
}
/// On Unix `//` followed by an absolute path names that same local file, while the resolver sees a
/// path on another computer: a place that skipped the resolver would read the file.
#[cfg(unix)]
fn network_alias(p: &Path) -> String {
format!("/{}", p.display())
}
/// A 32×32 white document saved (as far as its links know) at `d/doc.pcraft`, with `file` placed
/// linked → (session, layer id).
fn document(d: &Path, file: &Path) -> (Session, LayerId) {
let mut s = Session::new();
s.execute("file.new", json!({"width": 32, "height": 32})).unwrap();
s.active_mut().unwrap().path = Some(d.join("doc.pcraft").to_string_lossy().into_owned());
s.execute("file.placeLinked", json!({"path": file.to_string_lossy()})).unwrap();
let id = s.active().unwrap().active_layer.unwrap();
(s, id)
}
/// Point smart object `id` at `path` without re-rendering it.
fn relink(s: &mut Session, id: LayerId, path: &str) {
s.edit("Relink", |doc, _| {
if let Some(LayerContent::Smart(sm)) = doc.layer_mut(id).map(|l| &mut l.content) {
sm.source = SmartSource::Linked { path: path.into() };
}
Ok(())
})
.unwrap();
}
fn centre(s: &Session) -> [f32; 3] {
let p = photocraft_compose::render(&s.active().unwrap().doc, Rect::new(16, 16, 17, 17)).px[0];
[p[0], p[1], p[2]]
}
const RED: [f32; 3] = [1.0, 0.0, 0.0];
const BLUE: [f32; 3] = [0.0, 0.0, 1.0];
fn update(s: &mut Session) -> Result<serde_json::Value, String> {
s.execute("layer.smartObjects.updateModifiedContent", json!({})).map_err(|e| e.to_string())
}
#[test]
fn linked_smart_objects_follow_only_local_files() {
let d = dir("links");
png(&d.join("red.png"), |_| [255, 0, 0]);
png(&d.join("blue.png"), |_| [0, 0, 255]);
let (mut s, id) = document(&d, &d.join("red.png"));
assert_eq!(centre(&s), RED);
relink(&mut s, id, REMOTE);
s.start_link_scan();
assert!(s.link_scan.is_none(), "no look at a file on another computer");
assert_eq!(s.execute("layer.smartObjects.updateChanged", json!({})).unwrap(), json!({"updated": []}));
let err = update(&mut s).unwrap_err();
assert!(err.contains("is not opened: the path is on another computer"), "{err}");
assert_eq!(centre(&s), RED, "the layer keeps its rendering");
#[cfg(unix)]
{
relink(&mut s, id, &network_alias(&d.join("blue.png")));
assert!(update(&mut s).is_err());
assert_eq!(centre(&s), RED, "the file was not read");
}
// A relative link is taken from the document's folder.
relink(&mut s, id, "blue.png");
update(&mut s).unwrap();
assert_eq!(centre(&s), BLUE);
s.start_link_scan();
assert!(s.link_scan.is_some(), "a followed link is looked at");
}
#[test]
fn package_copies_only_followed_links() {
let d = dir("package");
png(&d.join("red.png"), |_| [255, 0, 0]);
let (mut s, id) = document(&d, &d.join("red.png"));
relink(&mut s, id, "red.png");
#[allow(unused_mut)]
let mut remote = vec![REMOTE.to_string()];
#[cfg(unix)]
remote.push(network_alias(&d.join("red.png")));
for path in &remote {
s.execute("file.placeLinked", json!({"path": d.join("red.png").to_string_lossy()})).unwrap();
let other = s.active().unwrap().active_layer.unwrap();
relink(&mut s, other, path);
}
let r = s.execute("file.package", json!({"dir": d.join("pkg").to_string_lossy()})).unwrap();
let mut missing: Vec<String> = serde_json::from_value(r["missing"].clone()).unwrap();
missing.sort();
remote.sort();
assert_eq!(missing, remote);
let links = r["links"].as_array().unwrap();
assert_eq!(links.len(), 1);
assert!(links[0].as_str().unwrap().ends_with("red.png"), "{links:?}");
}
#[test]
fn psd_saves_embed_only_followed_links() {
let d = dir("psd");
png(&d.join("red.png"), |_| [255, 0, 0]);
let (mut s, id) = document(&d, &d.join("red.png"));
relink(&mut s, id, "red.png");
s.execute("file.placeLinked", json!({"path": d.join("red.png").to_string_lossy()})).unwrap();
let other = s.active().unwrap().active_layer.unwrap();
relink(&mut s, other, REMOTE);
let warnings = |r: serde_json::Value| r["warnings"].as_array().unwrap().iter().map(|w| w.as_str().unwrap().to_string()).collect::<Vec<_>>();
let saved = warnings(s.execute("file.saveACopy", json!({"path": d.join("copy.psd").to_string_lossy()})).unwrap());
assert!(saved.iter().any(|w| w.contains("red.png was embedded")), "{saved:?}");
assert!(saved.iter().any(|w| w.contains("is not opened: the path is on another computer")), "{saved:?}");
// Package as PSD writes the copy of the document, whose remote link stays as it was.
let packaged = warnings(s.execute("file.package", json!({"dir": d.join("pkg").to_string_lossy(), "format": "psd"})).unwrap());
assert!(packaged.iter().any(|w| w.contains("is not opened: the path is on another computer")), "{packaged:?}");
}
/// The pixels of the active document after its smart object `id` re-renders with its first smart
/// filter's params set to `params`.
fn replayed(s: &mut Session, id: LayerId, params: serde_json::Value) -> Vec<[f32; 4]> {
s.edit("Params", |doc, _| {
if let Some(LayerContent::Smart(sm)) = doc.layer_mut(id).map(|l| &mut l.content) {
sm.smart_filters[0].params = params;
}
crate::smart_cmds::refresh(doc, id).map(|_| ())
})
.unwrap();
let doc = &s.active().unwrap().doc;
photocraft_compose::render(doc, doc.bounds()).px
}
#[test]
fn smart_filters_reread_only_followed_files() {
let d = dir("filters");
png(&d.join("ramp.png"), |x| [(x * 8) as u8, 0, 0]);
png(&d.join("map.png"), |_| [255, 255, 255]);
// An inverting look: corner (r, g, b) of the cube maps to (1-r, 1-g, 1-b), red fastest.
let corners = (0..8).map(|i| format!("{} {} {}\n", 1 - (i & 1), 1 - ((i >> 1) & 1), 1 - ((i >> 2) & 1))).collect::<String>();
std::fs::write(d.join("invert.cube"), format!("LUT_3D_SIZE 2\n{corners}")).unwrap();
let path = |name: &str| d.join(name).to_string_lossy().into_owned();
// Displace re-reads its `mapPath`.
let (mut s, id) = document(&d, &d.join("ramp.png"));
s.execute("filter.distort.displace", json!({"layer": id.0, "mapPath": path("map.png"), "horizontal": 50, "vertical": 0})).unwrap();
let displaced = replayed(&mut s, id, json!({"mapPath": path("map.png"), "horizontal": 50, "vertical": 0}));
let unfiltered = replayed(&mut s, id, json!({"mapPath": REMOTE, "horizontal": 50, "vertical": 0}));
assert_ne!(displaced, unfiltered, "the map moves the pixels");
assert_eq!(replayed(&mut s, id, json!({"mapPath": "map.png", "horizontal": 50, "vertical": 0})), displaced, "a relative map next to the document");
#[cfg(unix)]
assert_eq!(replayed(&mut s, id, json!({"mapPath": network_alias(&d.join("map.png")), "horizontal": 50, "vertical": 0})), unfiltered);
// Color Lookup re-reads its LUT `file`.
let (mut s, id) = document(&d, &d.join("ramp.png"));
s.execute("image.adjustments.colorLookup", json!({"layer": id.0, "lut": "none"})).unwrap();
let inverted = replayed(&mut s, id, json!({"file": path("invert.cube")}));
let unfiltered = replayed(&mut s, id, json!({"file": REMOTE}));
assert_ne!(inverted, unfiltered, "the look changes the pixels");
assert_eq!(replayed(&mut s, id, json!({"file": "invert.cube"})), inverted, "a relative look next to the document");
#[cfg(unix)]
assert_eq!(replayed(&mut s, id, json!({"file": network_alias(&d.join("invert.cube"))})), unfiltered);
}
#[test]
fn data_sets_and_footage_read_only_followed_files() {
let d = dir("datasets");
png(&d.join("blue.png"), |_| [0, 0, 255]);
let mut s = Session::new();
s.execute("file.new", json!({"width": 32, "height": 32, "background": "#ff0000"})).unwrap();
s.active_mut().unwrap().path = Some(d.join("doc.pcraft").to_string_lossy().into_owned());
let layer = s.active().unwrap().active_layer.unwrap();
s.execute("image.variables.define", json!({"defs": [{"name": "img", "layer": layer.0, "type": "pixelReplacement", "method": "conform"}]})).unwrap();
let set = |name: &str, path: &str| json!({"name": name, "values": [{"variable": "img", "kind": "pixels", "value": path}]});
s.execute("image.variables.dataSets", json!({"dataSets": [set("remote", REMOTE), set("relative", "blue.png")]})).unwrap();
let err = s.execute("image.applyDataSet", json!({"name": "remote"})).unwrap_err().to_string();
assert!(err.contains("is not opened: the path is on another computer"), "{err}");
assert_eq!(centre(&s), RED);
s.execute("image.applyDataSet", json!({"name": "relative"})).unwrap();
assert_eq!(centre(&s), BLUE);
// Reload Frame re-reads the video layer's footage.
s.execute("layer.videoLayers.newVideoLayerFromFile", json!({"path": d.join("blue.png").to_string_lossy()})).unwrap();
let footage = |s: &mut Session, path: &str| {
let id = s.active().unwrap().active_layer.unwrap();
s.edit("Footage", |doc, _| {
if let Some(v) = doc.layer_mut(id).and_then(|l| l.video.as_mut()) {
v.source = photocraft_doc::VideoSource::File { path: path.into() };
}
Ok(())
})
.unwrap();
s.execute("layer.videoLayers.reloadFrame", json!({})).map_err(|e| e.to_string())
};
let err = footage(&mut s, REMOTE).unwrap_err();
assert!(err.contains("is not opened: the path is on another computer"), "{err}");
footage(&mut s, "blue.png").unwrap();
}