Automation: exported layers keep linked smart objects as pixels in workspace sessions

`Headless::export_layers` calls `export_layers_with` outside `Session::execute`, so no session gate was installed when `encode` chose whether a PSD/PSB embeds a linked smart object's file. `export_layers_with` enters the session's gate itself, as `Session::execute` does, and a workspace session's layer export writes such a layer as pixels like `doc.save`.
This commit is contained in:
aiongg
2026-10-22 18:41:32 +00:00
parent a89554eb83
commit 993e3046df
2 changed files with 40 additions and 0 deletions
+37
View File
@@ -935,6 +935,43 @@ mod tests {
std::fs::remove_dir_all(base).unwrap();
}
#[test]
fn workspace_layer_exports_keep_linked_smart_objects_as_pixels() {
use photocraft_engine::doc::{LayerContent, SmartSource};
const MARKER: &[u8] = b"PHOTOCRAFT-LINKED-0002";
let has_marker = |bytes: &[u8]| bytes.windows(MARKER.len()).any(|w| w == MARKER);
let base = std::env::temp_dir().join(format!("pc-export-layers-linked-{}", std::process::id()));
let root = base.join("workspace");
std::fs::create_dir_all(&root).unwrap();
let outside = base.join("outside.txt");
std::fs::write(&outside, MARKER).unwrap();
// A document whose smart object names a file outside the roots as its source.
let mut session = photocraft_engine::Session::new();
session.execute("file.new", json!({"width": 8, "height": 8, "background": "white"})).unwrap();
session.execute("layer.smartObjects.convertToSmartObject", json!({})).unwrap();
let state = session.active_mut().unwrap();
let LayerContent::Smart(smart) = &mut std::sync::Arc::make_mut(&mut state.doc).layers[0].content else { panic!("smart object") };
smart.source = SmartSource::Linked { path: outside.to_string_lossy().into_owned() };
std::fs::write(root.join("in.pcraft"), photocraft_engine::smart_cmds::encode_source(&state.doc).unwrap()).unwrap();
let mut h = Headless::with_workspace(crate::AuthorizedWorkspace::new(Some(&root), Some(&root)).unwrap());
h.handle("doc.open", json!({"path": "in.pcraft"})).unwrap();
let out = root.join("layers");
for format in ["psd", "psb"] {
let exported = h.handle("doc.exportLayers", json!({"dir": out.to_string_lossy(), "format": format})).unwrap();
assert_eq!(exported["count"], 1, "{exported}");
}
let files: Vec<_> = std::fs::read_dir(&out).unwrap().map(|e| e.unwrap().path()).collect();
assert_eq!(files.len(), 2, "{files:?}");
for file in files {
assert!(!has_marker(&std::fs::read(&file).unwrap()), "{} embeds no file outside the roots", file.display());
}
// Release directory capabilities before cleanup on Windows.
drop(h);
std::fs::remove_dir_all(base).unwrap();
}
#[test]
fn trusted_local_render_keeps_full_size_behavior() {
let mut h = Headless::trusted_local();
+3
View File
@@ -1681,6 +1681,9 @@ pub fn export_layers_with(s: &Session, p: &Value, mut write: impl FnMut(&str, &[
let cmd = "file.export.layersToFiles";
let format = p.get("format").and_then(Value::as_str).unwrap_or("png").trim_start_matches('.').to_ascii_lowercase();
let visible_only = p.get("visibleOnly").and_then(Value::as_bool).unwrap_or(true);
// Callers outside `Session::execute` (headless workspace exports) carry the session's gate
// into `encode`, so a PSD embeds no file a linked smart object names.
let _authorization = crate::jobs::render_authorization_scope(s.authorize);
let save = SaveOpts::from_params(s, p, cmd)?;
let d = s.active().ok_or(EngineError::NoDocument)?;
let prefix = p.get("prefix").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| stem(&d.doc.name));