diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml index 093d0c1c..10c635eb 100644 --- a/crates/script/Cargo.toml +++ b/crates/script/Cargo.toml @@ -26,3 +26,4 @@ boa_engine = { workspace = true, features = ["js"] } [dev-dependencies] effectcraft-expr = { workspace = true } +effectcraft-psd = { workspace = true } diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index 367d43cc..d32cb6ca 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -86,6 +86,9 @@ var MaskMode = __enum("MaskMode", ["NONE", "ADD", "SUBTRACT", "INTERSECT", "LIGH var RQItemStatus = __enum("RQItemStatus", ["WILL_CONTINUE", "NEEDS_OUTPUT", "UNQUEUED", "QUEUED", "RENDERING", "USER_STOPPED", "ERR_STOPPED", "DONE"], 3012); var CloseOptions = __enum("CloseOptions", ["DO_NOT_SAVE_CHANGES", "PROMPT_TO_SAVE_CHANGES", "SAVE_CHANGES"], 1212); var ImportAsType = __enum("ImportAsType", ["COMP_CROPPED_LAYERS", "FOOTAGE", "COMP", "PROJECT"], 3812); +// Accept the longer names used by some ExtendScript hosts. +ImportAsType.COMPOSITION_CROPPED_LAYERS = ImportAsType.COMP_CROPPED_LAYERS; +ImportAsType.COMPOSITION = ImportAsType.COMP; var TimeDisplayType = __enum("TimeDisplayType", ["FRAMES", "TIMECODE"], 2012); var LogType = __enum("LogType", ["ERRORS_ONLY", "ERRORS_AND_SETTINGS", "ERRORS_AND_PER_FRAME_INFO"], 3212); var KeyframeLabel = __enum("KeyframeLabel", ["NONE"], 0); @@ -1768,7 +1771,11 @@ function ImportOptions(file) { this.rangeStart = 0; this.rangeEnd = 0; } -ImportOptions.prototype.canImportAs = function (t) { return t === ImportAsType.FOOTAGE; }; +ImportOptions.prototype.canImportAs = function (t) { + if (t === ImportAsType.FOOTAGE) return true; + return (t === ImportAsType.COMP || t === ImportAsType.COMP_CROPPED_LAYERS) && + /\.(psd|psb|pdf|ai|eps)$/i.test(__str(this.file)); +}; function Project() {} Project.prototype = { @@ -1822,10 +1829,19 @@ Project.prototype = { if (!f) throw __err("importFile(): no file"); var args = { paths: [__str(f)] }; // After Effects imports just the picked file unless ImportOptions.sequence is set. - if (opts instanceof ImportOptions) args.sequence = !!opts.sequence; + if (opts instanceof ImportOptions) { + args.sequence = !!opts.sequence; + switch (opts.importAs) { + case ImportAsType.FOOTAGE: args.importAs = "footage"; break; + case ImportAsType.COMP: args.importAs = "composition"; break; + case ImportAsType.COMP_CROPPED_LAYERS: args.importAs = "compositionLayerSizes"; break; + default: throw __err("importFile(): unsupported ImportOptions.importAs value " + opts.importAs); + } + } var r = __call("file.import", args); if (r.errors && r.errors.length) throw __err(r.errors.join("; ")); - return __item(r.items[0]); + // Composition imports create several footage items, but callers need the comp. + return __item(r.comps && r.comps.length ? r.comps[0] : r.items[0]); }, importFileWithDialog: function () { return null; }, importPlaceholder: function (name, w, h, rate, dur) { diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index e918da7c..681c093c 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -901,3 +901,32 @@ fn extendscript_substr_keeps_start_and_length_semantics() { ); assert_eq!(o.result, json!(["ab", "cde", "ef", "ab", "", "", "cd", "def", "", "ab", ""])); } + +#[test] +fn import_options_preserve_layered_psd_compositions() { + use effectcraft_psd::Rect; + use effectcraft_psd::write::{WDoc, WLayer, write}; + + let dir = std::env::temp_dir().join(format!("ec-script-import-as-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let source = dir.join("layers.psd"); + let mut doc = WDoc::new(12, 12); + doc.layers.push(WLayer::solid("Background", Rect::new(0, 0, 12, 12), [1.0, 1.0, 1.0, 1.0])); + doc.layers.push(WLayer::solid("Accent", Rect::new(2, 2, 4, 4), [0.0, 0.4, 1.0, 1.0])); + std::fs::write(&source, write(&doc)).unwrap(); + let file = serde_json::to_string(&source.to_string_lossy()).unwrap(); + let mut s = session(); + let o = ok( + &mut s, + &format!( + "var io = new ImportOptions(new File({file})); io.importAs = ImportAsType.COMPOSITION_CROPPED_LAYERS; \ + [io.canImportAs(io.importAs), app.project.importFile(io).typeName]" + ), + ); + assert_eq!(o.result, json!([true, "Composition"])); + let imported = s.project.comps().next().unwrap().1; + assert_eq!(imported.layers.len(), 2); + let bad = ok(&mut s, &format!("new ImportOptions(new File({file})).canImportAs(ImportAsType.PROJECT)")); + assert_eq!(bad.result, json!(false)); + let _ = std::fs::remove_dir_all(&dir); +}