Files
photocraft/crates/io/tests/gif_frames.rs
f14817a5d2 Animated GIF: every frame loads into video layers (#572) (#2999)
* feat(codecs): decode every frame of an animated GIF (#572)

decode_gif_frames returns each frame composited onto the logical screen
(disposal methods applied) with its delay in milliseconds. decode still
keeps the first frame only. A damaged later frame keeps the frames before
it and marks the first with DecodeWarning::Truncated; dimensions, the
total decoded bytes and a frame cap are checked against the limits.

* feat(io): import_gif_frames hands each GIF frame over as a document (#572)

A frame at a time becomes a single-layer document, so a caller can
flatten and drop it instead of holding every frame twice.

* feat(engine): animated GIFs load every frame into video layers (#572)

New Video Layer from File, Replace Footage, Reload Frame and Video Frames
to Layers take all frames of a GIF instead of the first. The timeline rate
comes from the GIF's delays (the most common one, delays under 20 ms shown
at 100 ms as browsers do); an explicit fps still wins.

* docs: animated GIF frames in video layers (#572)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:26:37 -04:00

40 lines
1.4 KiB
Rust

//! `import_gif_frames`: each GIF frame as a single-layer document, in order, with its delay.
use photocraft_codecs::web::{GifFrame, encode_gif_animated};
use photocraft_io::{IoError, import_gif_frames};
fn gif(colours: &[([u8; 3], u16)]) -> Vec<u8> {
let frames: Vec<GifFrame> = colours.iter().map(|&(c, delay_cs)| GifFrame { indices: vec![0; 6], palette: vec![c], transparent: None, delay_cs }).collect();
encode_gif_animated(3, 2, &frames, true).unwrap()
}
#[test]
fn frames_arrive_in_order_as_documents_with_their_delays() {
let bytes = gif(&[([255, 0, 0], 3), ([0, 0, 255], 7)]);
let mut seen = Vec::new();
let n = import_gif_frames("a.gif", &bytes, &mut |doc, delay_ms| {
seen.push((doc.layers.len(), doc.bounds().width(), doc.bounds().height(), delay_ms));
Ok(())
})
.unwrap();
assert_eq!(n, 2);
assert_eq!(seen, [(1, 3, 2, 30), (1, 3, 2, 70)]);
}
#[test]
fn an_error_from_the_callback_stops_the_import() {
let bytes = gif(&[([255, 0, 0], 3), ([0, 0, 255], 7), ([0, 255, 0], 7)]);
let mut calls = 0;
let r = import_gif_frames("a.gif", &bytes, &mut |_, _| {
calls += 1;
Err(IoError::Unsupported("stop".into()))
});
assert!(matches!(r, Err(IoError::Unsupported(_))));
assert_eq!(calls, 1);
}
#[test]
fn garbage_is_an_error() {
assert!(import_gif_frames("a.gif", b"not a gif", &mut |_, _| Ok(())).is_err());
}