From f3fe25fd67d3afcda29a0ec2541ce9bbcbdb3ce9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 23:50:08 +0000 Subject: [PATCH] Import: test that an animated GIF imported from disk plays (#651) Animated GIFs came in as one still frame in 0.7.0; bfbdadd3 (after that release) probes them as movies. This end-to-end test imports a GIF file through the wired session into a comp and checks the layer shows each of its frames at its time, the path the viewer and exports share. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f --- Cargo.lock | 1 + crates/host/Cargo.toml | 1 + crates/host/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cfab570d..4f6d9c87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1350,6 +1350,7 @@ dependencies = [ "effectcraft-svg", "effectcraft-time", "exr", + "gif", "image", "rayon", "serde_json", diff --git a/crates/host/Cargo.toml b/crates/host/Cargo.toml index 13fcb87a..344d5433 100644 --- a/crates/host/Cargo.toml +++ b/crates/host/Cargo.toml @@ -31,5 +31,6 @@ effectcraft-raster = { workspace = true } effectcraft-time = { workspace = true } effectcraft-interchange = { workspace = true } image = { workspace = true } +gif = { workspace = true } exr = { workspace = true } rayon = { workspace = true } diff --git a/crates/host/src/lib.rs b/crates/host/src/lib.rs index 7f2d1a5c..cf5661a1 100644 --- a/crates/host/src/lib.rs +++ b/crates/host/src/lib.rs @@ -257,6 +257,38 @@ mod tests { assert!(red * 20 > img.data.len(), "red cube pixels: {red} of {}", img.data.len()); } + /// #651: an animated GIF imported from disk plays: the layer shows each frame at its time, + /// in the viewer's renders and in exports alike. + #[test] + fn an_imported_animated_gif_plays() { + let dir = std::env::temp_dir().join(format!("ec-gif-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("anim.gif"); + let mut bytes = Vec::new(); + { + let mut enc = gif::Encoder::new(&mut bytes, 4, 4, &[]).unwrap(); + for rgb in [[255u8, 0, 0], [0, 255, 0], [0, 0, 255]] { + let mut f = gif::Frame::from_rgb(4, 4, &rgb.repeat(16)); + f.delay = 10; + enc.write_frame(&f).unwrap(); + } + } + std::fs::write(&path, bytes).unwrap(); + let mut s = super::session(); + s.execute("comp.new", json!({"name": "G", "width": 4, "height": 4, "frameRate": 30, "duration": 1})).unwrap(); + s.execute("file.import", json!({"paths": [path.to_string_lossy()], "addToComp": true})).unwrap(); + let cid = s.active_comp_id().unwrap(); + let rgb = |t: f64| { + let img = s.render(cid, effectcraft_time::Tick::from_seconds_f64(t), effectcraft_engine::render::RenderOpts::default()); + let p = img.data[5]; + [p[0].round(), p[1].round(), p[2].round()] + }; + assert_eq!(rgb(0.05), [1.0, 0.0, 0.0]); + assert_eq!(rgb(0.15), [0.0, 1.0, 0.0]); + assert_eq!(rgb(0.25), [0.0, 0.0, 1.0]); + let _ = std::fs::remove_dir_all(dir); + } + /// The web app's path: outputs go to a sink (downloads), never to the file system. #[test] fn render_queue_exports_to_a_sink() {