Files
figcraft/crates/render/src/gif_tests.rs
T
BFlat 5b321a779c M6: animated GIF fills and TIFF images (#116)
Animated GIF image fills keep their frames (first frame on the canvas and in exports; the presenter and preview play them, capped frames and memory, idle when not playing). TIFF images open, import, place and drop (pure-Rust decoder already in the tree); SVG exports embed TIFF and BMP as PNG. HEIC and video stay out (non-Rust decoders).
2026-10-11 11:51:37 +07:00

101 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Animated GIFs: frames and their timing, the caps on decoding, junk input, and the first frame
//! everywhere but the presenter.
use std::sync::Arc;
use figcraft_color::{ColorProfile, ImageFilters, ImageScaleMode, Paint};
use figcraft_doc::{Document, ImageData, NodeKind};
use image::codecs::gif::{GifEncoder, Repeat};
use image::{Delay, Frame, RgbaImage};
use crate::images::{self, MAX_FRAMES};
const RED: [u8; 4] = [255, 0, 0, 255];
const GREEN: [u8; 4] = [0, 255, 0, 255];
const BLUE: [u8; 4] = [0, 0, 255, 255];
/// A synthetic animated GIF: one solid `w`×`h` frame per (colour, delay in ms).
fn gif(frames: &[([u8; 4], u32)], w: u32, h: u32) -> Vec<u8> {
let mut out = Vec::new();
{
let mut enc = GifEncoder::new(&mut out);
enc.set_repeat(Repeat::Infinite).unwrap();
let frames =
frames.iter().map(|(c, ms)| Frame::from_parts(RgbaImage::from_pixel(w, h, image::Rgba(*c)), 0, 0, Delay::from_numer_denom_ms(*ms, 1)));
enc.encode_frames(frames).unwrap();
}
out
}
fn data(bytes: Vec<u8>) -> ImageData {
ImageData { bytes: Arc::new(bytes), width: 4, height: 4 }
}
#[test]
fn frames_keep_their_delays_and_loop() {
let bytes = gif(&[(RED, 100), (GREEN, 200), (BLUE, 0)], 4, 4);
let frames = images::decode_frames(&bytes).unwrap();
let secs: Vec<f64> = frames.iter().map(|f| f.1).collect();
assert_eq!(secs, vec![0.1, 0.2, 0.1], "a 0 ms delay plays as 100 ms");
let a = images::animation("gif-timing", Some(&data(bytes)), &ImageFilters::default(), ColorProfile::Srgb).unwrap();
assert_eq!(a.len(), 3);
let at = |t: f64| {
let (i, next) = a.at(t);
(i, (next * 1000.0).round())
};
assert_eq!(at(0.0), (0, 100.0));
assert_eq!(at(0.15), (1, 150.0));
assert_eq!(at(0.35), (2, 50.0));
assert_eq!(at(0.45), (0, 50.0), "loops after 400 ms");
assert_eq!(at(f64::NAN), (0, 100.0));
}
#[test]
fn stills_and_junk_never_animate() {
let still = gif(&[(RED, 100)], 4, 4);
assert!(images::decode_frames(&still).is_none(), "one frame is a still");
let png = crate::encode_png(&tiny_skia::Pixmap::new(2, 2).unwrap()).unwrap();
assert!(images::decode_frames(&png).is_none());
let moving = gif(&[(RED, 100), (GREEN, 100)], 8, 8);
for n in 0..moving.len() {
// Every truncation decodes to something or nothing, never a panic.
let _ = images::decode_frames(&moving[..n]);
let _ = images::decode(&moving[..n]);
}
let mut junk = b"GIF89a".to_vec();
junk.extend((0..200u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8));
assert!(images::decode_frames(&junk).is_none());
}
#[test]
fn decoding_is_capped() {
let many: Vec<([u8; 4], u32)> = (0..MAX_FRAMES + 5).map(|i| (if i % 2 == 0 { RED } else { GREEN }, 100)).collect();
assert_eq!(images::decode_frames(&gif(&many, 1, 1)).unwrap().len(), MAX_FRAMES);
// A canvas so big that one frame is past the byte cap doesn't play (its first frame is still
// the image, where the renderer can decode it).
let mut huge = gif(&[(RED, 100), (GREEN, 100)], 1, 1);
huge[6..10].copy_from_slice(&[0x28, 0x23, 0x28, 0x23]);
assert!(images::decode_frames(&huge).is_none());
}
#[test]
fn the_first_frame_draws_unless_time_moves_on() {
let mut d = Document::new("t");
let page = d.add_page("Page 1");
let id = d.create(page, NodeKind::Rectangle, "GIF", 0.0, 0.0, 4.0, 4.0).unwrap();
let bytes = gif(&[(RED, 100), (GREEN, 100)], 4, 4);
let hash = "gif-first-frame".to_string();
d.images.insert(hash.clone(), data(bytes));
d.get_mut(id).unwrap().fills = vec![Paint::image(hash, ImageScaleMode::Fill)];
let centre = |p: tiny_skia::Pixmap| {
let c = p.pixel(2, 2).unwrap();
[c.red(), c.green(), c.blue()]
};
assert_eq!(centre(crate::render_node(&d, id, 1.0).unwrap()), [255, 0, 0], "exports and the canvas: the first frame");
assert_eq!(centre(crate::render_node_at(&d, id, 1.0, 0.15).unwrap()), [0, 255, 0], "the presenter, 150 ms in");
let (frame, next) = images::animation_clock(&d, page, 0.15).unwrap();
assert!(frame != images::animation_clock(&d, page, 0.05).unwrap().0 && (next - 0.05).abs() < 1e-9);
d.get_mut(id).unwrap().visible = false;
assert!(images::animation_clock(&d, page, 0.15).is_none(), "hidden layers don't keep the presenter awake");
}