From 983a2f0ebfa99d659af6ce76d02f372cbcfbf2cd Mon Sep 17 00:00:00 2001 From: Vikram Bhaskaran Date: Sun, 11 Oct 2026 11:26:26 +1100 Subject: [PATCH] Tests: an imported animated GIF plays its frames in a wired session (#651). Importing an animated GIF as a movie (bfbdadd) made it preview and export again, but nothing pinned it: the item must probe as a GIF movie and its rendered frames must follow the file's own per-frame delays, not hold one still. Add a host integration test that imports a red/green/blue GIF and asserts each third of its duration renders the matching colour, so a regression to still-frame import is caught. --- crates/host/Cargo.toml | 1 + crates/host/tests/gif_animates.rs | 58 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 crates/host/tests/gif_animates.rs 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/tests/gif_animates.rs b/crates/host/tests/gif_animates.rs new file mode 100644 index 00000000..3cf6a8c3 --- /dev/null +++ b/crates/host/tests/gif_animates.rs @@ -0,0 +1,58 @@ +//! Animated GIF footage through the wired session (#651): the imported item is a movie whose +//! frames follow the file's own delays in preview rendering, not one frozen still. + +use effectcraft_engine::render::RenderOpts; +use effectcraft_project::{ItemId, ItemKind}; +use effectcraft_time::Tick; +use serde_json::json; + +fn tmp(name: &str) -> String { + let d = std::env::temp_dir().join(format!("effectcraft-host-gif-{}", std::process::id())); + std::fs::create_dir_all(&d).unwrap(); + d.join(name).to_string_lossy().to_string() +} + +/// A 3-frame 16×16 GIF: red, green, blue, one tenth of a second each. +fn animated_gif() -> Vec { + let mut out = Vec::new(); + { + let mut enc = gif::Encoder::new(&mut out, 16, 16, &[]).unwrap(); + for rgb in [[255u8, 0, 0], [0, 255, 0], [0, 0, 255]] { + let mut f = gif::Frame::from_rgb(16, 16, &rgb.repeat(256)); + f.delay = 10; + enc.write_frame(&f).unwrap(); + } + } + out +} + +fn centre(s: &effectcraft_engine::Session, comp: ItemId, t: f64) -> [f32; 3] { + let img = s.render(comp, Tick::from_seconds_f64(t), RenderOpts::default()); + let p = img.data[8 * 16 + 8]; + [p[0], p[1], p[2]] +} + +#[test] +fn an_imported_animated_gif_plays_its_frames() { + let mut s = effectcraft_host::session(); + let path = tmp("anim.gif"); + std::fs::write(&path, animated_gif()).unwrap(); + s.execute("comp.new", json!({"name": "Gif", "width": 16, "height": 16, "frameRate": 10, "duration": 1})).unwrap(); + let before = s.project.items.len(); + s.execute("file.import", json!({"paths": [path.clone()], "addToComp": true})).unwrap(); + let f = match s.project.items.values().find_map(|i| match &i.kind { + ItemKind::Footage(f) if f.path == path => Some(f), + _ => None, + }) { + Some(f) => f.clone(), + None => panic!("no footage item for the GIF"), + }; + assert_eq!(s.project.items.len(), before + 1, "one footage item, not a comp"); + assert_eq!((f.codec.as_str(), f.has_video), ("GIF", true), "probed as an animated movie"); + let cid = s.active_comp_id().unwrap(); + let (r, g, b) = (centre(&s, cid, 0.05), centre(&s, cid, 0.15), centre(&s, cid, 0.25)); + let _ = std::fs::remove_file(&path); + assert!(r[0] > 0.9 && r[1] < 0.1, "frame at 0.05 s is red, got {r:?}"); + assert!(g[1] > 0.9 && g[0] < 0.1, "frame at 0.15 s is green, got {g:?}"); + assert!(b[2] > 0.9 && b[0] < 0.1, "frame at 0.25 s is blue, got {b:?}"); +}