use crate::output::OutputError;
use image::codecs::gif::{GifEncoder, Repeat};
use image::{Frame, RgbaImage};
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
pub fn render_gif(
frames: &[RgbaImage],
duration_ms: u32,
loop_anim: bool,
path: &Path,
) -> Result<(), OutputError> {
if frames.is_empty() {
return Ok(());
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
let file = File::create(path)?;
let writer = BufWriter::new(file);
let mut encoder = GifEncoder::new(writer);
let repeat = if loop_anim { Repeat::Infinite } else { Repeat::Finite(0) };
encoder.set_repeat(repeat)?;
let delay_cs = (duration_ms / 10).max(1) as u16;
for rgba_image in frames {
let delay = image::Delay::from_numer_denom_ms(delay_cs as u32 * 10, 1);
let frame = Frame::from_parts(rgba_image.clone(), 0, 0, delay);
encoder.encode_frame(frame)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use image::Rgba;
use tempfile::tempdir;
fn create_test_frame(width: u32, height: u32, color: Rgba<u8>) -> RgbaImage {
let mut img = RgbaImage::new(width, height);
for pixel in img.pixels_mut() {
*pixel = color;
}
img
}
#[test]
fn test_render_gif_creates_valid_file() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.gif");
let frames = vec![
create_test_frame(2, 2, Rgba([255, 0, 0, 255])), create_test_frame(2, 2, Rgba([0, 255, 0, 255])), ];
let result = render_gif(&frames, 100, true, &path);
assert!(result.is_ok());
assert!(path.exists());
let img = image::open(&path);
assert!(img.is_ok());
}
#[test]
fn test_render_gif_frame_duration() {
let dir = tempdir().unwrap();
let path = dir.path().join("duration_test.gif");
let frames = vec![
create_test_frame(4, 4, Rgba([255, 255, 0, 255])), create_test_frame(4, 4, Rgba([0, 255, 255, 255])), ];
let result = render_gif(&frames, 500, true, &path);
assert!(result.is_ok());
assert!(path.exists());
let img = image::open(&path);
assert!(img.is_ok());
}
#[test]
fn test_render_gif_loop_setting() {
let dir = tempdir().unwrap();
let loop_path = dir.path().join("loop.gif");
let frames = vec![
create_test_frame(2, 2, Rgba([255, 0, 0, 255])),
create_test_frame(2, 2, Rgba([0, 0, 255, 255])),
];
let result = render_gif(&frames, 100, true, &loop_path);
assert!(result.is_ok());
assert!(loop_path.exists());
let no_loop_path = dir.path().join("no_loop.gif");
let result = render_gif(&frames, 100, false, &no_loop_path);
assert!(result.is_ok());
assert!(no_loop_path.exists());
assert!(image::open(&loop_path).is_ok());
assert!(image::open(&no_loop_path).is_ok());
}
#[test]
fn test_render_gif_empty_frames() {
let dir = tempdir().unwrap();
let path = dir.path().join("empty.gif");
let frames: Vec<RgbaImage> = vec![];
let result = render_gif(&frames, 100, true, &path);
assert!(result.is_ok());
assert!(!path.exists());
}
#[test]
fn test_render_gif_single_frame() {
let dir = tempdir().unwrap();
let path = dir.path().join("single.gif");
let frames = vec![create_test_frame(8, 8, Rgba([128, 128, 128, 255]))];
let result = render_gif(&frames, 100, true, &path);
assert!(result.is_ok());
assert!(path.exists());
}
#[test]
fn test_render_gif_creates_parent_dirs() {
let dir = tempdir().unwrap();
let path = dir.path().join("nested/dirs/test.gif");
let frames = vec![create_test_frame(2, 2, Rgba([255, 0, 0, 255]))];
let result = render_gif(&frames, 100, true, &path);
assert!(result.is_ok());
assert!(path.exists());
}
#[test]
fn test_render_gif_minimum_delay() {
let dir = tempdir().unwrap();
let path = dir.path().join("min_delay.gif");
let frames = vec![
create_test_frame(2, 2, Rgba([255, 0, 0, 255])),
create_test_frame(2, 2, Rgba([0, 255, 0, 255])),
];
let result = render_gif(&frames, 5, true, &path);
assert!(result.is_ok());
assert!(path.exists());
}
}