use std::io::BufReader;
use std::path::Path;
use image::Rgba;
use image::RgbaImage;
#[derive(Debug, Clone)]
pub enum ImageError {
NotFound,
InvalidFormat(String),
}
impl std::fmt::Display for ImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageError::NotFound => write!(f, "Image not found"),
ImageError::InvalidFormat(s) => write!(f, "Invalid format: {}", s),
}
}
}
impl std::error::Error for ImageError {}
pub struct ImageCache;
#[derive(Clone)]
pub struct GifFrame {
pub image: image::DynamicImage,
pub delay_ms: u32,
}
impl ImageCache {
fn composite_gif_frame(canvas: &mut RgbaImage, frame: &gif::Frame, width: u32, height: u32) {
let frame_buffer = &frame.buffer;
let frame_width = frame.width as u32;
let frame_height = frame.height as u32;
let left = frame.left as u32;
let top = frame.top as u32;
for y in 0..frame_height {
for x in 0..frame_width {
let src_idx = ((y * frame_width + x) * 4) as usize;
if src_idx + 3 < frame_buffer.len() {
let pixel = Rgba([
frame_buffer[src_idx],
frame_buffer[src_idx + 1],
frame_buffer[src_idx + 2],
frame_buffer[src_idx + 3],
]);
let canvas_x = left + x;
let canvas_y = top + y;
if canvas_x < width && canvas_y < height && pixel[3] > 0 {
canvas.put_pixel(canvas_x, canvas_y, pixel);
}
}
}
}
}
pub fn extract_all_frames(path: &Path) -> Result<Vec<GifFrame>, ImageError> {
use std::fs::File;
let file = File::open(path).map_err(|_| ImageError::NotFound)?;
let reader = BufReader::new(file);
let mut options = gif::DecodeOptions::new();
options.set_color_output(gif::ColorOutput::RGBA);
match options.read_info(reader) {
Ok(mut decoder) => {
let width = decoder.width() as u32;
let height = decoder.height() as u32;
let mut frames = Vec::new();
let mut canvas = RgbaImage::from_pixel(width, height, Rgba([0, 0, 0, 0]));
while let Ok(Some(frame)) = decoder.read_next_frame() {
Self::composite_gif_frame(&mut canvas, frame, width, height);
let delay_ms = (frame.delay as u32) * 10;
frames.push(GifFrame {
image: image::DynamicImage::ImageRgba8(canvas.clone()),
delay_ms: delay_ms.max(20), });
}
if frames.is_empty() {
Self::load_static_image(path)
} else {
Ok(frames)
}
}
Err(_) => Self::load_static_image(path),
}
}
fn load_static_image(path: &Path) -> Result<Vec<GifFrame>, ImageError> {
image::ImageReader::open(path)
.ok()
.and_then(|r| r.with_guessed_format().ok())
.and_then(|r| r.decode().ok())
.map(|img| {
vec![GifFrame {
image: img,
delay_ms: 0,
}]
})
.ok_or_else(|| ImageError::InvalidFormat("Unsupported image format".to_string()))
}
pub fn extract_first_frame(path: &Path) -> Result<image::DynamicImage, ImageError> {
use std::fs::File;
let file = File::open(path).map_err(|_| ImageError::NotFound)?;
let reader = BufReader::new(file);
let mut options = gif::DecodeOptions::new();
options.set_color_output(gif::ColorOutput::RGBA);
match options.read_info(reader) {
Ok(mut decoder) => {
let width = decoder.width() as u32;
let height = decoder.height() as u32;
if let Ok(Some(frame)) = decoder.read_next_frame() {
let mut canvas = RgbaImage::from_pixel(width, height, Rgba([0, 0, 0, 0]));
Self::composite_gif_frame(&mut canvas, frame, width, height);
Ok(image::DynamicImage::ImageRgba8(canvas))
} else {
image::ImageReader::open(path)
.ok()
.and_then(|r| r.with_guessed_format().ok())
.and_then(|r| r.decode().ok())
.ok_or_else(|| {
ImageError::InvalidFormat("Failed to decode image".to_string())
})
}
}
Err(_) => image::ImageReader::open(path)
.ok()
.and_then(|r| r.with_guessed_format().ok())
.and_then(|r| r.decode().ok())
.ok_or_else(|| ImageError::InvalidFormat("Unsupported format".to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gif_frame_struct() {
let img = image::DynamicImage::new_rgba8(10, 10);
let frame = GifFrame {
image: img,
delay_ms: 100,
};
assert_eq!(frame.delay_ms, 100);
}
}