* Actions: record File › Save and Save As steps and replay them (#2032) Save and Save As run in the desktop shell (dialogs, TIFF prompt, background save job), outside Session::execute, so they never reached the journal that action recording copies from. The shell now journals `file.save` {} for a save in place and `file.saveAs` {path, tiffLayers?} once a Save As has its path (after the dialog or the TIFF Options answer). The engine treats both as replayable shell steps; shell playback writes them synchronously without a dialog. Batch and droplets drop recorded saves, since their destination folder overrides them (Photoshop's "Override Action 'Save As' Commands"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Batch: skip recorded view steps in batch and droplets (#2752) Actions recorded in the desktop app can hold the shell's view steps (view.zoomIn, view.fitOnScreen, ...). Batch and Create Droplet checked every step against the engine registry and rejected the whole action with "unknown command". They now skip view steps, which don't change the output, alongside the recorded saves #2746 already skips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
6ff2a03f33
commit
cfe031f2a6
|
|
@ -79,7 +79,7 @@ pub fn shell_save_command(id: &str) -> bool {
|
|||
}
|
||||
|
||||
/// View-only menu commands recorded by the desktop shell. Headless playback reports
|
||||
/// them as unsupported instead of silently dropping them.
|
||||
/// them as unsupported instead of silently dropping them; Batch and droplets skip them.
|
||||
pub fn shell_view_command(id: &str) -> bool {
|
||||
matches!(id, "view.fitOnScreen" | "view.actualPixels" | "view.zoomIn" | "view.zoomOut")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,8 +292,8 @@ fn create_droplet(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let path = p.get("path").and_then(Value::as_str).filter(|v| !v.is_empty()).ok_or_else(|| bad(cmd, "missing \"path\" (where to save the droplet)"))?;
|
||||
let steps_v = p.get("steps").or_else(|| p.get("action")).ok_or_else(|| bad(cmd, "missing \"steps\" (the action to run)"))?;
|
||||
let mut steps = parse_steps(steps_v, cmd)?;
|
||||
// The droplet's output folder replaces recorded saves, as in batch.
|
||||
steps.retain(|(id, _)| !crate::actions_cmds::shell_save_command(id));
|
||||
// The droplet's output folder replaces recorded saves, and view steps don't apply, as in batch.
|
||||
steps.retain(|(id, _)| !crate::actions_cmds::shell_save_command(id) && !crate::actions_cmds::shell_view_command(id));
|
||||
check_known(&steps, cmd)?;
|
||||
let path = if path.ends_with(".pcdroplet") || path.ends_with(".json") { path.to_string() } else { format!("{path}.pcdroplet") };
|
||||
let name = p.get("name").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| stem(&path));
|
||||
|
|
|
|||
|
|
@ -113,6 +113,21 @@ fn droplets_are_written_and_run() {
|
|||
assert!(s.execute("file.automate.createDroplet", json!({"path": path, "steps": [["bogus", {}]]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_and_droplets_skip_recorded_view_steps() {
|
||||
let dir = tmp("view-steps");
|
||||
images(&dir, 1, 40, 20);
|
||||
let steps = json!([["view.zoomIn", {}], ["image.imageRotation.90cw", {}], ["view.fitOnScreen", {}]]);
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("file.automate.batch", json!({"steps": steps, "input": dir.clone(), "output": format!("{dir}/batch"), "format": "png"})).unwrap();
|
||||
assert!(r["errors"].as_array().unwrap().is_empty(), "{r}");
|
||||
let out = photocraft_codecs::decode(&std::fs::read(format!("{dir}/batch/img0.png")).unwrap()).unwrap();
|
||||
assert_eq!(out.dimensions(), (20, 40));
|
||||
let r = s.execute("file.automate.createDroplet", json!({"path": format!("{dir}/Rotate"), "steps": steps})).unwrap();
|
||||
let v: Value = serde_json::from_slice(&std::fs::read(r["path"].as_str().unwrap()).unwrap()).unwrap();
|
||||
assert_eq!(v["action"]["steps"], json!([["image.imageRotation.90cw", {}]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn statistics_makes_a_stack_mode_smart_object() {
|
||||
let dir = tmp("stats");
|
||||
|
|
|
|||
|
|
@ -775,8 +775,9 @@ fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let mut steps =
|
||||
parse_steps(p.get("steps").or_else(|| p.get("action")).ok_or_else(|| EngineError::BadParams { cmd: cmd.into(), msg: "missing \"steps\"".into() })?)?;
|
||||
// Each result goes to the destination folder, which overrides the action's recorded Save /
|
||||
// Save As steps (Photoshop's "Override Action 'Save As' Commands").
|
||||
steps.retain(|(id, _)| !crate::actions_cmds::shell_save_command(id));
|
||||
// Save As steps (Photoshop's "Override Action 'Save As' Commands"). Recorded view steps
|
||||
// (zoom, fit) don't change the output, so they are skipped (#2752).
|
||||
steps.retain(|(id, _)| !crate::actions_cmds::shell_save_command(id) && !crate::actions_cmds::shell_view_command(id));
|
||||
if let Some((id, _)) = steps.iter().find(|(id, _)| crate::commands::find(id).is_none()) {
|
||||
return Err(EngineError::BadParams { cmd: cmd.into(), msg: format!("unknown command `{id}` in the action") });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue