Skip to main content

figif_core/decoders/
streaming.rs

1//! Streaming GIF decoder that processes frames lazily.
2
3use crate::error::{FigifError, Result};
4use crate::traits::GifDecoder;
5use crate::types::{DecodedFrame, DisposalMethod, GifMetadata, LoopCount};
6use gif::{DecodeOptions, Decoder};
7use image::{Rgba, RgbaImage};
8use std::fs::File;
9use std::io::{BufReader, Cursor, Read};
10use std::path::Path;
11
12/// A streaming GIF decoder that processes frames on-demand.
13///
14/// This decoder is more memory-efficient for large GIFs as it doesn't
15/// need to hold all frames in memory simultaneously. However, it requires
16/// maintaining decoder state and is less suitable for random access.
17///
18/// # Example
19///
20/// ```ignore
21/// use figif_core::decoders::StreamingDecoder;
22/// use figif_core::traits::GifDecoder;
23///
24/// let decoder = StreamingDecoder::new();
25/// for frame in decoder.decode_file("large_animation.gif")? {
26///     let frame = frame?;
27///     // Process frame immediately
28/// }
29/// ```
30#[derive(Debug, Clone, Default)]
31pub struct StreamingDecoder;
32
33impl StreamingDecoder {
34    /// Create a new streaming decoder.
35    pub fn new() -> Self {
36        Self
37    }
38}
39
40/// Iterator that yields frames as they are decoded.
41pub struct StreamingFrameIter<R: Read> {
42    decoder: Decoder<R>,
43    canvas: RgbaImage,
44    previous_canvas: Option<RgbaImage>,
45    index: usize,
46    width: u32,
47    height: u32,
48}
49
50impl<R: Read> StreamingFrameIter<R> {
51    fn new(decoder: Decoder<R>) -> Self {
52        let width = decoder.width() as u32;
53        let height = decoder.height() as u32;
54        let canvas = RgbaImage::from_pixel(width, height, Rgba([0, 0, 0, 0]));
55
56        Self {
57            decoder,
58            canvas,
59            previous_canvas: None,
60            index: 0,
61            width,
62            height,
63        }
64    }
65}
66
67impl<R: Read> Iterator for StreamingFrameIter<R> {
68    type Item = Result<DecodedFrame>;
69
70    fn next(&mut self) -> Option<Self::Item> {
71        let frame = match self.decoder.read_next_frame() {
72            Ok(Some(frame)) => frame,
73            Ok(None) => return None,
74            Err(e) => return Some(Err(e.into())),
75        };
76
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            self.previous_canvas = Some(self.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 < self.width && canvas_y < self.height && pixel[3] > 0 {
106                        self.canvas.put_pixel(canvas_x, canvas_y, pixel);
107                    }
108                }
109            }
110        }
111
112        // Create the decoded frame
113        let decoded = DecodedFrame {
114            index: self.index,
115            image: self.canvas.clone(),
116            delay_centiseconds: delay,
117            disposal,
118            left,
119            top,
120        };
121
122        // Apply disposal method for next frame
123        match disposal {
124            DisposalMethod::Background => {
125                for y in 0..frame_height {
126                    for x in 0..frame_width {
127                        let canvas_x = left as u32 + x;
128                        let canvas_y = top as u32 + y;
129                        if canvas_x < self.width && canvas_y < self.height {
130                            self.canvas
131                                .put_pixel(canvas_x, canvas_y, Rgba([0, 0, 0, 0]));
132                        }
133                    }
134                }
135            }
136            DisposalMethod::Previous => {
137                if let Some(ref prev) = self.previous_canvas {
138                    self.canvas = prev.clone();
139                }
140            }
141            DisposalMethod::Keep | DisposalMethod::None => {}
142        }
143
144        self.index += 1;
145        Some(Ok(decoded))
146    }
147}
148
149/// Wrapper to handle different reader types with a common iterator type.
150pub enum StreamingIterWrapper {
151    /// Iterator reading from a file.
152    File(StreamingFrameIter<BufReader<File>>),
153    /// Iterator reading from in-memory bytes.
154    Bytes(StreamingFrameIter<Cursor<Vec<u8>>>),
155}
156
157impl Iterator for StreamingIterWrapper {
158    type Item = Result<DecodedFrame>;
159
160    fn next(&mut self) -> Option<Self::Item> {
161        match self {
162            StreamingIterWrapper::File(iter) => iter.next(),
163            StreamingIterWrapper::Bytes(iter) => iter.next(),
164        }
165    }
166}
167
168impl GifDecoder for StreamingDecoder {
169    type FrameIter = StreamingIterWrapper;
170
171    fn decode_file(&self, path: impl AsRef<Path>) -> Result<Self::FrameIter> {
172        let path = path.as_ref();
173        let file = File::open(path).map_err(|e| FigifError::FileRead {
174            path: path.to_path_buf(),
175            source: e,
176        })?;
177        let reader = BufReader::new(file);
178
179        let mut options = DecodeOptions::new();
180        options.set_color_output(gif::ColorOutput::RGBA);
181        let decoder = options.read_info(reader)?;
182
183        Ok(StreamingIterWrapper::File(StreamingFrameIter::new(decoder)))
184    }
185
186    fn decode_bytes(&self, data: &[u8]) -> Result<Self::FrameIter> {
187        if data.is_empty() {
188            return Err(FigifError::EmptyData);
189        }
190
191        let reader = Cursor::new(data.to_vec());
192
193        let mut options = DecodeOptions::new();
194        options.set_color_output(gif::ColorOutput::RGBA);
195        let decoder = options.read_info(reader)?;
196
197        Ok(StreamingIterWrapper::Bytes(StreamingFrameIter::new(
198            decoder,
199        )))
200    }
201
202    fn decode_reader<R: Read + Send>(&self, reader: R) -> Result<Self::FrameIter> {
203        // For generic readers, we need to buffer into memory
204        let mut buffer = Vec::new();
205        let mut reader = reader;
206        reader
207            .read_to_end(&mut buffer)
208            .map_err(|e| FigifError::DecodeError {
209                reason: format!("failed to read GIF data: {}", e),
210            })?;
211
212        self.decode_bytes(&buffer)
213    }
214
215    fn metadata_from_bytes(&self, data: &[u8]) -> Result<GifMetadata> {
216        if data.is_empty() {
217            return Err(FigifError::EmptyData);
218        }
219
220        let reader = Cursor::new(data);
221        let mut options = DecodeOptions::new();
222        options.set_color_output(gif::ColorOutput::RGBA);
223        let mut decoder = options.read_info(reader)?;
224
225        let width = decoder.width();
226        let height = decoder.height();
227        let global_palette = decoder.global_palette().map(|p| p.to_vec());
228
229        let mut frame_count = 0;
230        let mut total_duration_cs: u64 = 0;
231
232        while let Some(frame) = decoder.read_next_frame()? {
233            frame_count += 1;
234            total_duration_cs += frame.delay as u64;
235        }
236
237        Ok(GifMetadata {
238            width,
239            height,
240            frame_count,
241            total_duration_ms: total_duration_cs * 10,
242            has_transparency: global_palette.is_some(),
243            loop_count: LoopCount::Infinite,
244            global_palette,
245        })
246    }
247
248    fn metadata_from_file(&self, path: impl AsRef<Path>) -> Result<GifMetadata> {
249        let path = path.as_ref();
250        let data = std::fs::read(path).map_err(|e| FigifError::FileRead {
251            path: path.to_path_buf(),
252            source: e,
253        })?;
254        self.metadata_from_bytes(&data)
255    }
256
257    fn name(&self) -> &'static str {
258        "streaming"
259    }
260}