Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
bflatastic
2026-10-30 03:39:42 +07:00
11 changed files with 163 additions and 17 deletions
+1 -1
View File
@@ -746,7 +746,7 @@ pub fn specs() -> Vec<CommandSpec> {
"File...",
["File", "Import"],
Some("Cmd+I"),
"{paths: [string] (files or folders), sequence?: bool (the Sequence checkbox: true imports numbered stills of one run as one image sequence, one picked file bringing its whole run and several picked files that range; default: a numbered still brings its run, except dropped files (drag), which import as stills as in After Effects; folders' runs import as sequences; false imports stills and a folder's files individually), alphabetical?: bool (Force Alphabetical Order: every image of that type in the folder, by name), frameRate?: fps (image sequences; default Settings ▸ Import ▸ Sequence Footage), importAs?: footage|composition|compositionLayerSizes (Photoshop, PDF, Illustrator and EPS files), layer?: name|index (footage of one Photoshop layer), page?: number from 1 (PDF / Illustrator page), drag?: bool (dropped files: Settings ▸ Import ▸ Default Drag Import As; files import as stills unless sequence), addToComp?: bool (also add them to the active comp, at time?, index?, position? as in layer.addItem), background?: bool (probe the files in a background job, jobs.list / jobs.wait; returns {job})}",
"{paths: [string] (files or folders), sequence?: bool (the Sequence checkbox: true imports numbered stills of one run as one image sequence, one picked file bringing its whole run and several picked files that range; default false: picked or dropped files import as stills, as in After Effects; folders' runs import as sequences; false imports stills and a folder's files individually), alphabetical?: bool (Force Alphabetical Order: every image of that type in the folder, by name), frameRate?: fps (image sequences; default Settings ▸ Import ▸ Sequence Footage), importAs?: footage|composition|compositionLayerSizes (Photoshop, PDF, Illustrator and EPS files), layer?: name|index (footage of one Photoshop layer), page?: number from 1 (PDF / Illustrator page), drag?: bool (dropped files: Settings ▸ Import ▸ Default Drag Import As; files import as stills unless sequence), addToComp?: bool (also add them to the active comp, at time?, index?, position? as in layer.addItem), background?: bool (probe the files in a background job, jobs.list / jobs.wait; returns {job})}",
always,
import_cmd
),
+3 -4
View File
@@ -45,14 +45,13 @@ impl SequenceOptions {
/// `file.import`'s options. Dropped files (`drag: true`) import as stills, as in After
/// Effects, while a folder's numbered stills import as sequences unless `sequence: false`
/// (Alt-drag of a folder). The Import dialog passes its `"<format> Sequence"` checkbox (off
/// unless ticked) as `sequence`. Without `sequence` or `drag` (scripts, agents) a numbered
/// still brings its run, as it always has.
/// unless ticked) as `sequence`. Without `sequence` (scripts, agents) a numbered still imports
/// as itself, as `ImportOptions.sequence` defaults to false in After Effects (#2018).
pub fn for_import(p: &serde_json::Value) -> SequenceOptions {
let flag = |k: &str| p.get(k).and_then(serde_json::Value::as_bool);
let (sequence, alphabetical) = (flag("sequence"), flag("alphabetical").unwrap_or(false));
let dropped = flag("drag").unwrap_or(false);
// Force Alphabetical Order is part of the Sequence option.
SequenceOptions { sequence: sequence.unwrap_or(alphabetical || !dropped), folders: sequence.unwrap_or(true), alphabetical }
SequenceOptions { sequence: sequence.unwrap_or(alphabetical), folders: sequence.unwrap_or(true), alphabetical }
}
}
+31 -1
View File
@@ -50,8 +50,20 @@ pub trait Services: Send + Sync {
pub struct FsServices;
impl Services for FsServices {
/// Refuses what isn't a regular file: a project can name `/dev/zero` or a FIFO, and reading
/// those never ends (#2134).
fn read_file(&self, path: &str) -> std::io::Result<Vec<u8>> {
std::fs::read(path)
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let meta = file.metadata()?;
if !meta.is_file() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{path} is not a file")));
}
let mut bytes = Vec::new();
// A sparse file can claim far more than memory: an error, not an abort.
bytes.try_reserve_exact(usize::try_from(meta.len()).unwrap_or(usize::MAX)).map_err(|e| std::io::Error::new(std::io::ErrorKind::OutOfMemory, e))?;
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
fn write_file(&self, path: &str, data: &[u8]) -> std::io::Result<()> {
config::atomic_write(std::path::Path::new(path), data)
@@ -70,3 +82,21 @@ pub trait Importer: Send + Sync {
/// media layer reads from memory (the browser) register them; file-based ones need nothing.
fn add_frames(&self, _paths: &[String]) {}
}
#[cfg(test)]
mod tests {
use super::{FsServices, Services};
/// #2134: a project can name a folder, `/dev/zero` or a FIFO; reading those is an error.
#[test]
fn fs_services_read_only_regular_files() {
let dir = std::env::temp_dir();
assert!(FsServices.read_file(&dir.to_string_lossy()).is_err());
#[cfg(unix)]
assert!(FsServices.read_file("/dev/zero").is_err());
let f = dir.join(format!("ec-services-read-{}.txt", std::process::id()));
std::fs::write(&f, b"data").unwrap();
assert_eq!(FsServices.read_file(&f.to_string_lossy()).unwrap(), b"data");
let _ = std::fs::remove_file(f);
}
}
+5 -4
View File
@@ -104,10 +104,11 @@ fn numbered_stills_import_as_stills_by_default() {
let r = s.execute("file.import", json!({"paths": [dir.clone()], "drag": true})).unwrap();
let (name, f) = footage(&s, &r["items"][0]);
assert_eq!((name.as_str(), f.kind), ("shot_[0001-0005].png", FootageKind::Sequence));
// Scripts and agents naming one frame without `sequence` still get its run.
let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0003.png")]})).unwrap();
let (_, f) = footage(&s, &r["items"][0]);
assert_eq!(f.kind, FootageKind::Sequence);
// #2018: scripts and agents naming one frame without `sequence` get that still, as
// `ImportOptions.sequence` defaults to false in After Effects.
let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0005.png")]})).unwrap();
let (name, f) = footage(&s, &r["items"][0]);
assert_eq!((name.as_str(), f.kind), ("shot_0005.png", FootageKind::Still));
}
#[test]