Skip to main content

figif_core/decoders/
buffered.rs

1//! Buffered GIF decoder that loads all frames into memory.
2
3use crate::error::{FigifError, Result};
4use crate::traits::{BufferedGifDecoder, GifDecoder};
5use crate::types::{DecodedFrame, DisposalMethod, GifMetadata, LoopCount};
6use gif::DecodeOptions;
7use image::{Rgba, RgbaImage};
8use std::fs::File;
9use std::io::{BufReader, Cursor, Read};
10use std::path::Path;
11
12/// A buffered GIF decoder that loads and composites all frames.
13///
14/// This decoder properly handles GIF disposal methods and transparency,
15/// producing fully composited RGBA frames suitable for analysis.
16///
17/// # Example
18///
19/// ```ignore
20/// use figif_core::decoders::BufferedDecoder;
21/// use figif_core::traits::GifDecoder;
22///
23/// let decoder = BufferedDecoder::new();
24/// let frames: Vec<_> = decoder.decode_file("animation.gif")?.collect::<Result<Vec<_>, _>>()?;
25/// ```
26#[derive(Debug, Clone, Default)]
27pub struct BufferedDecoder {
28    /// Memory limit for decoding (0 = no limit).
29    memory_limit: usize,
30}
31
32impl BufferedDecoder {
33    /// Create a new buffered decoder with default settings.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Set a memory limit for decoding.
39    ///
40    /// If the GIF would require more memory than this limit,
41    /// decoding will fail with an error.
42    pub fn with_memory_limit(mut self, limit: usize) -> Self {
43        self.memory_limit = limit;
44        self
45    }
46
47    /// Decode all frames from a reader into a vector.
48    fn decode_all_frames<R: Read>(&self, reader: R) -> Result<Vec<DecodedFrame>> {
49        let mut options = DecodeOptions::new();
50        options.set_color_output(gif::ColorOutput::RGBA);
51
52        let mut decoder = options.read_info(reader)?;
53
54        let width = decoder.width() as u32;
55        let height = decoder.height() as u32;
56
57        // Check memory limit
58        if self.memory_limit > 0 {
59            let frame_size = (width * height * 4) as usize;
60            // Rough estimate: we need at least 2 frames worth of memory
61            if frame_size * 2 > self.memory_limit {
62                return Err(FigifError::InvalidConfig {
63                    message: format!("GIF dimensions {}x{} exceed memory limit", width, height),
64                });
65            }
66        }
67
68        // Canvas for compositing frames
69        let mut canvas = RgbaImage::from_pixel(width, height, Rgba([0, 0, 0, 0]));
70        let mut frames = Vec::new();
71        let mut index = 0;
72
73        // Previous frame for "Previous" disposal method
74        let mut previous_canvas: Option<RgbaImage> = None;
75
76        while let Some(frame) = decoder.read_next_frame()? {
77            let delay = frame.delay;
78            let disposal = DisposalMethod::from(frame.dispose);
79            let left = frame.left;
80            let top = frame.top;
81            let frame_width = frame.width as u32;
82            let frame_height = frame.height as u32;
83
84            // Save canvas before applying this frame (for Previous disposal)
85            if matches!(disposal, DisposalMethod::Previous) {
86                previous_canvas = Some(canvas.clone());
87            }
88
89            // Composite frame onto canvas
90            let frame_buffer = &frame.buffer;
91            for y in 0..frame_height {
92                for x in 0..frame_width {
93                    let src_idx = ((y * frame_width + x) * 4) as usize;
94                    if src_idx + 3 < frame_buffer.len() {
95                        let pixel = Rgba([
96                            frame_buffer[src_idx],
97                            frame_buffer[src_idx + 1],
98                            frame_buffer[src_idx + 2],
99                            frame_buffer[src_idx + 3],
100                        ]);
101
102                        let canvas_x = left as u32 + x;
103                        let canvas_y = top as u32 + y;
104
105                        if canvas_x < width && canvas_y < height {
106                            // Only draw non-transparent pixels
107                            if pixel[3] > 0 {
108                                canvas.put_pixel(canvas_x, canvas_y, pixel);
109                            }
110                        }
111                    }
112                }
113            }
114
115            // Store the composited frame
116            frames.push(DecodedFrame {
117                index,
118                image: canvas.clone(),
119                delay_centiseconds: delay,
120                disposal,
121                left,
122                top,
123            });
124
125            // Apply disposal method for next frame
126            match disposal {
127                DisposalMethod::Background => {
128                    // Clear the frame area to background/transparent
129                    for y in 0..frame_height {
130                        for x in 0..frame_width {
131                            let canvas_x = left as u32 + x;
132                            let canvas_y = top as u32 + y;
133                            if canvas_x < width && canvas_y < height {
134                                canvas.put_pixel(canvas_x, canvas_y, Rgba([0, 0, 0, 0]));
135                            }
136                        }
137                    }
138                }
139                DisposalMethod::Previous => {
140                    // Restore previous canvas
141                    if let Some(ref prev) = previous_canvas {
142                        canvas = prev.clone();
143                    }
144                }
145                DisposalMethod::Keep | DisposalMethod::None => {
146                    // Keep canvas as-is
147                }
148            }
149
150            index += 1;
151        }
152
153        if frames.is_empty() {
154            return Err(FigifError::NoFrames);
155        }
156
157        Ok(frames)
158    }
159
160    /// Extract metadata from a decoder.
161    fn extract_metadata<R: Read>(&self, reader: R) -> Result<GifMetadata> {
162        let mut options = DecodeOptions::new();
163        options.set_color_output(gif::ColorOutput::RGBA);
164
165        let mut decoder = options.read_info(reader)?;
166
167        let width = decoder.width();
168        let height = decoder.height();
169        let global_palette = decoder.global_palette().map(|p| p.to_vec());
170        let has_transparency = global_palette
171            .as_ref()
172            .is_some_and(|_| decoder.bg_color().is_some());
173
174        // Count frames and total duration
175        let mut frame_count = 0;
176        let mut total_duration_cs: u64 = 0;
177
178        while let Some(frame) = decoder.read_next_frame()? {
179            frame_count += 1;
180            total_duration_cs += frame.delay as u64;
181        }
182
183        // Get repeat info (need to check extensions)
184        let loop_count = LoopCount::Infinite; // Default, would need extension parsing
185
186        Ok(GifMetadata {
187            width,
188            height,
189            frame_count,
190            total_duration_ms: total_duration_cs * 10,
191            has_transparency,
192            loop_count,
193            global_palette,
194        })
195    }
196}
197
198/// Iterator adapter for buffered frames.
199pub struct BufferedFrameIter {
200    frames: std::vec::IntoIter<DecodedFrame>,
201}
202
203impl Iterator for BufferedFrameIter {
204    type Item = Result<DecodedFrame>;
205
206    fn next(&mut self) -> Option<Self::Item> {
207        self.frames.next().map(Ok)
208    }
209
210    fn size_hint(&self) -> (usize, Option<usize>) {
211        self.frames.size_hint()
212    }
213}
214
215impl ExactSizeIterator for BufferedFrameIter {}
216
217impl GifDecoder for BufferedDecoder {
218    type FrameIter = BufferedFrameIter;
219
220    fn decode_file(&self, path: impl AsRef<Path>) -> Result<Self::FrameIter> {
221        let path = path.as_ref();
222        let file = File::open(path).map_err(|e| FigifError::FileRead {
223            path: path.to_path_buf(),
224            source: e,
225        })?;
226        let reader = BufReader::new(file);
227        let frames = self.decode_all_frames(reader)?;
228        Ok(BufferedFrameIter {
229            frames: frames.into_iter(),
230        })
231    }
232
233    fn decode_bytes(&self, data: &[u8]) -> Result<Self::FrameIter> {
234        if data.is_empty() {
235            return Err(FigifError::EmptyData);
236        }
237        let reader = Cursor::new(data);
238        let frames = self.decode_all_frames(reader)?;
239        Ok(BufferedFrameIter {
240            frames: frames.into_iter(),
241        })
242    }
243
244    fn decode_reader<R: Read + Send>(&self, reader: R) -> Result<Self::FrameIter> {
245        let frames = self.decode_all_frames(reader)?;
246        Ok(BufferedFrameIter {
247            frames: frames.into_iter(),
248        })
249    }
250
251    fn metadata_from_bytes(&self, data: &[u8]) -> Result<GifMetadata> {
252        if data.is_empty() {
253            return Err(FigifError::EmptyData);
254        }
255        let reader = Cursor::new(data);
256        self.extract_metadata(reader)
257    }
258
259    fn metadata_from_file(&self, path: impl AsRef<Path>) -> Result<GifMetadata> {
260        let path = path.as_ref();
261        let file = File::open(path).map_err(|e| FigifError::FileRead {
262            path: path.to_path_buf(),
263            source: e,
264        })?;
265        let reader = BufReader::new(file);
266        self.extract_metadata(reader)
267    }
268
269    fn name(&self) -> &'static str {
270        "buffered"
271    }
272}
273
274impl BufferedGifDecoder for BufferedDecoder {}