diff --git a/crates/engine/src/actions_cmds.rs b/crates/engine/src/actions_cmds.rs index e454d31c..098d3087 100644 --- a/crates/engine/src/actions_cmds.rs +++ b/crates/engine/src/actions_cmds.rs @@ -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") } diff --git a/crates/engine/src/automate_cmds.rs b/crates/engine/src/automate_cmds.rs index 36c68d15..f26ba1ad 100644 --- a/crates/engine/src/automate_cmds.rs +++ b/crates/engine/src/automate_cmds.rs @@ -292,8 +292,8 @@ fn create_droplet(s: &mut Session, p: &Value) -> Result { 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)); diff --git a/crates/engine/src/automate_cmds/tests.rs b/crates/engine/src/automate_cmds/tests.rs index 33ef1328..73fb157c 100644 --- a/crates/engine/src/automate_cmds/tests.rs +++ b/crates/engine/src/automate_cmds/tests.rs @@ -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"); diff --git a/crates/engine/src/file_cmds.rs b/crates/engine/src/file_cmds.rs index dcb6bd2b..c44373f4 100644 --- a/crates/engine/src/file_cmds.rs +++ b/crates/engine/src/file_cmds.rs @@ -775,8 +775,9 @@ fn batch(s: &mut Session, p: &Value) -> Result { 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") }); }