diff --git a/crates/engine/src/commands/file.rs b/crates/engine/src/commands/file.rs index 55b2c2b6..6f3d2e70 100644 --- a/crates/engine/src/commands/file.rs +++ b/crates/engine/src/commands/file.rs @@ -43,7 +43,8 @@ pub(crate) fn open(s: &mut Session, p: &Value) -> Result { let text = String::from_utf8(bytes).map_err(|_| EngineError::Other("not a text project file".into()))?; // XML copies (File ▸ Save a Copy As XML…) open like the JSON project. let text = if crate::xml_project::is_xml(&text) { crate::xml_project::from_xml(&text).map_err(EngineError::Other)? } else { text }; - let proj = Project::from_json(&text).map_err(|e| EngineError::Other(format!("{path} is not a project EffectCraft can open: {e}")))?; + let mut proj = Project::from_json(&text).map_err(|e| EngineError::Other(format!("{path} is not a project EffectCraft can open: {e}")))?; + resolve_relative_footage(&mut proj, path, s.services.as_ref()); s.replace_project(proj, Some(path.to_string())); s.note_project_path(path); // Written by a newer EffectCraft: what this version doesn't know would be lost on saving. @@ -62,6 +63,28 @@ pub(crate) fn open(s: &mut Session, p: &Value) -> Result { Ok(json!({"path": path, "savedBy": saved_by})) } +/// Footage saved with a relative path (imported by the CLI or an agent as `clip.mp4`, or written +/// by hand) is found next to the project file, as After Effects looks for footage relative to +/// the project. It used to be looked up from the working directory: the CLI started in the +/// project's folder found it, the desktop app (started from the Finder, Explorer or a launcher, +/// in another folder) showed and rendered nothing (#652). A path that exists from the working +/// directory is kept. +fn resolve_relative_footage(p: &mut Project, project_path: &str, services: &dyn crate::Services) { + use std::path::Path; + let Some(dir) = Path::new(project_path).parent().filter(|d| !d.as_os_str().is_empty()) else { return }; + let beside = |f: &str| dir.join(f).to_string_lossy().to_string(); + for item in p.items.values_mut() { + let ItemKind::Footage(f) = &mut item.kind else { continue }; + if f.path.is_empty() || !Path::new(&f.path).is_relative() || services.exists(&f.path) || !services.exists(&beside(&f.path)) { + continue; + } + f.path = beside(&f.path); + for frame in f.sequence.iter_mut().filter(|q| Path::new(q.as_str()).is_relative()) { + *frame = beside(frame); + } + } +} + /// The project file's text for `path`: `.ecproj` JSON, or XML for `.ecprojx` / `.xml` (a /// project opened from an XML copy saves as XML again). Fails, writing nothing, when the project /// wouldn't read back. diff --git a/crates/engine/src/footage_check.rs b/crates/engine/src/footage_check.rs index c3765f1e..f5db284e 100644 --- a/crates/engine/src/footage_check.rs +++ b/crates/engine/src/footage_check.rs @@ -193,6 +193,30 @@ mod tests { let _ = std::fs::remove_dir_all(dir); } + /// #652: footage saved with a relative path is found next to the project, wherever the + /// app was started (the desktop app doesn't start in the project's folder, the CLI often + /// does). + #[test] + fn relative_footage_is_found_next_to_the_project() { + let dir = std::env::temp_dir().join(format!("ec-relative-{}", std::process::id())); + std::fs::create_dir_all(dir.join("media")).unwrap(); + let clip = dir.join("media").join("ec-relative-clip.png"); + std::fs::write(&clip, b"x").unwrap(); + let mut s = session_with(&["media/ec-relative-clip.png", "/nonexistent/ec/abs.png", "media/ec-relative-gone.png"]); + let path = dir.join("p.ecproj"); + s.execute("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap(); + let mut o = Session::default(); + o.execute("file.open", json!({"path": path.to_string_lossy()})).unwrap(); + let mut paths: Vec = + o.project.items.values().filter_map(|i| if let ItemKind::Footage(f) = &i.kind { Some(f.path.clone()) } else { None }).collect(); + paths.sort(); + assert_eq!(paths, ["/nonexistent/ec/abs.png", clip.to_str().unwrap(), "media/ec-relative-gone.png"]); + assert!(!o.is_dirty(), "finding the files is no edit"); + let r = o.execute("footage.check", json!({"wait": true})).unwrap(); + assert_eq!(r["missing"], 2); + let _ = std::fs::remove_dir_all(dir); + } + #[test] fn open_starts_the_check_when_asked() { let mut s = session_with(&["/nonexistent/ec/a.mov"]);