mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 03:47:00 -06:00
* 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>
40 lines
1.4 KiB
Rust
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());
|
|
}
|