From a96c80788a548ee727e4412956f012a6682bc325 Mon Sep 17 00:00:00 2001 From: redonion23 <199118131+redonion23@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:32:48 +0200 Subject: [PATCH] Scripting: CompItem.saveFrameToPng writes the frame as a PNG (#816) After Effects scripts save a comp frame with saveFrameToPng(time, file); EffectCraft only had the comp.saveFrameAs command, so the method was undefined. It now runs comp.saveFrameAs at the given comp time, after the same file-gate check as other script writes (Allow Scripts to Write Files). Co-Authored-By: Claude Opus 5.5 --- crates/script/src/prelude.js | 8 ++++++++ crates/script/src/tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 9daac6ab..7eb1b30d 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -1600,6 +1600,14 @@ CompItem.prototype.constructor = CompItem; __call("essential.rename", { comp: this.__id, control: c[i - 1].id, name: String(name) }); return String(name); }; + // After Effects' CompItem.saveFrameToPng(time, file): Composition ▸ Save Frame As ▸ File at + // `time` (comp seconds), behind the scripting file gate like other writes (#816). + P.saveFrameToPng = function (time, file) { + var f = __str(file); + if (!f) throw __err("saveFrameToPng: missing file"); + __file("write", f, ""); + __call("comp.saveFrameAs", { comp: this.__id, path: f, time: __num(time, "time") }); + }; P.openInEssentialGraphics = function () { __call("comp.openInEssentialGraphics", { comp: this.__id }); }; P.exportAsMotionGraphicsTemplate = function (overwrite, path) { var f = __str(path); diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 303bd820..84c2760f 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -362,6 +362,29 @@ fn security_gate_for_files() { let _ = std::fs::remove_dir_all(&dir); } +/// #816: CompItem.saveFrameToPng writes the frame as a PNG, behind the file-write preference. +#[test] +fn save_frame_to_png_writes_behind_the_file_gate() { + let dir = std::env::temp_dir().join(format!("ec-save-frame-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let png = dir.join("frame.png"); + let mut s = session(); + let code = format!( + "var c = app.project.items.addComp('F', 40, 30, 1, 2, 25); c.layers.addSolid([1, 0, 0], 'S', 40, 30, 1); c.saveFrameToPng(1, new File({}))", + serde_json::to_string(&png.to_string_lossy()).unwrap() + ); + let o = run_code(&mut s, &code, "frame.jsx"); + assert!(o.error.expect("refused").message.contains("Allow Scripts to Write Files")); + assert!(!png.exists() || std::fs::metadata(&png).unwrap().len() == 0); + s.prefs.scripting.allow_scripts_write_files = true; + ok(&mut s, &code); + let bytes = std::fs::read(&png).unwrap(); + assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n"); + // IHDR width and height. + assert_eq!((u32::from_be_bytes(bytes[16..20].try_into().unwrap()), u32::from_be_bytes(bytes[20..24].try_into().unwrap())), (40, 30)); + let _ = std::fs::remove_dir_all(&dir); +} + /// #590: a script can't give itself file and network access (or turn off the warning before /// executing files) by changing Settings ▸ Scripting & Expressions; the user still can. #[test]