Skip to main content

otf_pixels_codec_gif/
decoder.rs

1//! The GIF decoder.
2//!
3//! # Canvas, not image
4//!
5//! A GIF frame is not the image: it is a rectangle drawn onto a persistent
6//! canvas, whose size comes from the logical screen descriptor. A frame can be
7//! smaller than the canvas, offset within it, and partially transparent, and
8//! the canvas retains whatever earlier frames left behind — subject to the
9//! disposal method each declared.
10//!
11//! Getting that wrong produces images that look right on the first frame and
12//! accumulate garbage after it, which is why disposal is modelled explicitly
13//! rather than being treated as "clear between frames".
14//!
15//! # Memory
16//!
17//! GIF decode is **internally buffered**, at one canvas. Frames are
18//! LZW-compressed as a unit and composited onto shared state, so there is no
19//! row at which the canvas is final until the frame is complete. SPEC
20//! §Formats says "yes (per frame)", and this is what that means: the canvas is
21//! bounded by the image, not by the number of frames, and an animation of a
22//! thousand frames costs the same as one.
23
24use otf_pixels_compress::LzwDecoder;
25use otf_pixels_core::{
26    Animation, Background, Codec, Comment, DecodeCapability, Decoder, Format,
27    ImageDescriptor as CoreDescriptor, Limits, PixelFormat, PixelsError, Result, Source,
28    SourceInfo,
29};
30
31use crate::format::{
32    Disposal, GraphicControl, ImageDescriptor, SIGNATURE_87A, SIGNATURE_89A, Screen,
33    interlaced_pass_rows, interlaced_row, label, read_sub_blocks, skip_sub_blocks,
34};
35
36/// The largest sub-block chain accepted for one frame's pixel data.
37///
38/// LZW output is bounded separately by the frame's pixel count; this bounds
39/// the *compressed* side, which a length-prefixed chain does not bound itself.
40const MAX_COMPRESSED: usize = 64 * 1024 * 1024;
41
42/// The largest GIF stream accepted. The stream is buffered to count its
43/// frames, so this bounds that buffer; a real GIF this size is a very long
44/// animation, and anything larger is refused rather than read into memory.
45const MAX_STREAM: usize = 512 * 1024 * 1024;
46
47/// The largest extension payload retained. Comments and application blocks are
48/// skipped rather than kept, so this only bounds the ones we read.
49const MAX_EXTENSION: usize = 4096;
50
51/// One decoded frame, with the animation metadata that came with it.
52#[derive(Debug, Clone)]
53#[non_exhaustive]
54pub struct Frame {
55    /// The whole canvas after this frame is composited, as RGBA8.
56    pub pixels: Vec<u8>,
57    /// Canvas width.
58    pub width: u32,
59    /// Canvas height.
60    pub height: u32,
61    /// Delay before the next frame, in hundredths of a second.
62    pub delay_centiseconds: u16,
63    /// How this frame is disposed of before the next is drawn.
64    pub disposal: Disposal,
65}
66
67/// Decodes a GIF stream.
68///
69/// [`Decoder`] presents the first frame; [`GifDecoder::next_frame`] walks the
70/// rest. See the crate docs for why the split is where it is.
71#[derive(Debug)]
72pub struct GifDecoder<S: Source> {
73    descriptor: CoreDescriptor,
74    screen: Screen,
75    /// The stream after the global colour table, read whole at construction
76    /// so its frames can be counted before the first is decoded.
77    source: Option<Bytes>,
78    /// Frame count and timing, when there is more than one frame.
79    animation: Option<Animation>,
80    /// The caller's source type, consumed in [`GifDecoder::new`].
81    _source: std::marker::PhantomData<fn() -> S>,
82    /// The global colour table, if the stream carries one.
83    global: Vec<[u8; 3]>,
84    /// The canvas, RGBA8, persisting across frames.
85    canvas: Vec<u8>,
86    /// Whether the first frame has been composited yet.
87    started: bool,
88    /// Rows of the first frame already served through [`Decoder::read_row`].
89    row: u32,
90    /// Set when the trailer is reached, so `next_frame` stops.
91    finished: bool,
92    /// Pending disposal from the frame just drawn.
93    pending: Option<Pending>,
94    /// The RGBA the canvas reverts to under `Disposal::Background`.
95    background: [u8; 4],
96    /// Palette, interlace, background and comments.
97    info: SourceInfo,
98}
99
100/// What the frame just drawn asked to happen before the next one.
101#[derive(Debug)]
102struct Pending {
103    disposal: Disposal,
104    area: ImageDescriptor,
105    /// The rectangle as it was before the frame was drawn, for `Previous`.
106    saved: Vec<u8>,
107    /// Whether that frame declared a transparent index, which changes what
108    /// `Background` means.
109    had_transparency: bool,
110}
111
112impl<S: Source> GifDecoder<S> {
113    /// Parse the header and logical screen descriptor, reading nothing more.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`PixelsError::Malformed`] for a bad signature or screen
118    /// descriptor, or [`PixelsError::LimitExceeded`] if the canvas exceeds
119    /// `limits`.
120    pub fn new(mut source: S, limits: Limits) -> Result<Self> {
121        let mut header = [0_u8; 13];
122        source.read_exact(&mut header)?;
123
124        let signature = header.get(..6).unwrap_or(&[]);
125        if signature != SIGNATURE_87A && signature != SIGNATURE_89A {
126            return Err(PixelsError::malformed(
127                "gif",
128                "signature is neither GIF87a nor GIF89a",
129            ));
130        }
131        let mut screen_bytes = [0_u8; 7];
132        screen_bytes.copy_from_slice(header.get(6..13).unwrap_or(&[0; 7]));
133        let screen = Screen::parse(&screen_bytes)?;
134
135        let width = u32::from(screen.width);
136        let height = u32::from(screen.height);
137        // Enforced before any buffer exists (SPEC §Safety).
138        let descriptor = CoreDescriptor::with_limits(width, height, PixelFormat::Rgba8, &limits)?;
139
140        let mut global = Vec::new();
141        if screen.global_table_size > 0 {
142            global = read_table(&mut source, screen.global_table_size)?;
143        }
144
145        // The rest of the stream, buffered: a GIF's compressed data is small
146        // beside its RGBA canvas, and counting frames means reading past the
147        // first, which a forward-only source cannot give back.
148        let mut rest = Vec::new();
149        let mut chunk = [0_u8; 64 * 1024];
150        loop {
151            match source.read(&mut chunk)? {
152                0 => break,
153                n => rest.extend_from_slice(chunk.get(..n).unwrap_or_default()),
154            }
155            if rest.len() > MAX_STREAM {
156                return Err(PixelsError::unsupported(format!(
157                    "gif: streams over {} MiB are not read",
158                    MAX_STREAM >> 20
159                )));
160            }
161        }
162        let (animation, comments, interlaced) = scan_animation(&rest);
163
164        let canvas_len = descriptor
165            .byte_len()
166            .ok_or_else(|| PixelsError::malformed("gif", "canvas size overflows"))?;
167
168        // "Restore to background colour" means the entry the logical screen
169        // descriptor names, opaque. A stream with no global table has no
170        // background colour to restore to, so transparent is the only
171        // available answer.
172        let background = global
173            .get(screen.background as usize)
174            .map_or([0, 0, 0, 0], |c| [c[0], c[1], c[2], 255]);
175        let mut info = SourceInfo::default();
176        info.palette = true;
177        info.progressive = interlaced;
178        info.bits_per_sample = Some(8);
179        info.comments = comments;
180        info.background = global
181            .get(screen.background as usize)
182            .map(|c| Background::Rgb(c[0].into(), c[1].into(), c[2].into()));
183
184        Ok(Self {
185            descriptor,
186            screen,
187            source: Some(std::io::Cursor::new(rest)),
188            animation,
189            _source: std::marker::PhantomData,
190            global,
191            // A GIF canvas begins fully transparent, which is what makes a
192            // first frame smaller than the canvas render correctly.
193            canvas: vec![0_u8; canvas_len],
194            started: false,
195            row: 0,
196            finished: false,
197            pending: None,
198            background,
199            info,
200        })
201    }
202
203    /// The logical screen descriptor.
204    #[must_use]
205    pub const fn screen(&self) -> Screen {
206        self.screen
207    }
208
209    /// Decode the next frame, compositing it onto the canvas.
210    ///
211    /// Returns `None` once the stream's trailer is reached. Each frame carries
212    /// the whole canvas, because that is what a viewer draws — a frame's own
213    /// rectangle is meaningless without what it was composited onto.
214    ///
215    /// # Errors
216    ///
217    /// Returns [`PixelsError::Malformed`] for a malformed block or a frame
218    /// that does not fit its canvas, or [`PixelsError::Io`] on source failure.
219    pub fn next_frame(&mut self) -> Result<Option<Frame>> {
220        if self.finished {
221            return Ok(None);
222        }
223        let Some(mut source) = self.source.take() else {
224            return Ok(None);
225        };
226        let result = self.decode_next(&mut source);
227        self.source = Some(source);
228        result
229    }
230
231    /// The body of [`GifDecoder::next_frame`], with the source borrowed out.
232    fn decode_next(&mut self, source: &mut Bytes) -> Result<Option<Frame>> {
233        // Apply the previous frame's disposal before drawing this one. It
234        // happens here rather than after drawing because `Previous` needs the
235        // saved rectangle, and saving it is only worth doing if a later frame
236        // actually arrives.
237        self.apply_disposal();
238
239        let mut control = GraphicControl::default();
240        loop {
241            let mut marker = [0_u8; 1];
242            source.read_exact(&mut marker)?;
243            match marker[0] {
244                label::TRAILER => {
245                    self.finished = true;
246                    return Ok(None);
247                }
248                label::EXTENSION => {
249                    let mut kind = [0_u8; 1];
250                    source.read_exact(&mut kind)?;
251                    match kind[0] {
252                        label::GRAPHIC_CONTROL => {
253                            let payload = read_sub_blocks(source, MAX_EXTENSION)?;
254                            control = GraphicControl::parse(&payload);
255                        }
256                        // Comments, plain text and application blocks carry no
257                        // pixels. Skipping them is required, not optional: an
258                        // unknown extension must not be an error (§Appendix A).
259                        _ => skip_sub_blocks(source)?,
260                    }
261                }
262                label::IMAGE => {
263                    let frame = self.decode_image(source, control)?;
264                    return Ok(Some(frame));
265                }
266                other => {
267                    return Err(PixelsError::malformed(
268                        "gif",
269                        format!("unknown block label {other:#04x}"),
270                    ));
271                }
272            }
273        }
274    }
275
276    /// Undo the previous frame according to its declared disposal.
277    fn apply_disposal(&mut self) {
278        let Some(pending) = self.pending.take() else {
279            return;
280        };
281        match pending.disposal {
282            // `None` is "unspecified", which viewers treat as leaving the
283            // frame in place. Clearing here would break the overwhelmingly
284            // common case of an optimised animation drawing only what changed.
285            Disposal::None | Disposal::Keep => {}
286            Disposal::Background => {
287                // §23.c.iv says "restore to background colour", and libgif
288                // does exactly that. Browsers instead restore to transparent,
289                // which is what an animation with a transparent index is
290                // authored against — so the frame's own transparency decides
291                // which reading applies. Picking one unconditionally makes
292                // one large class of real animations render wrongly.
293                let fill = if pending.had_transparency {
294                    [0, 0, 0, 0]
295                } else {
296                    self.background
297                };
298                self.fill_area(pending.area, &fill);
299            }
300            Disposal::Previous => {
301                self.restore_area(pending.area, &pending.saved);
302            }
303        }
304    }
305
306    /// Fill a frame's rectangle with one RGBA value.
307    fn fill_area(&mut self, area: ImageDescriptor, value: &[u8; 4]) {
308        let width = self.descriptor.width;
309        let height = self.descriptor.height;
310        for y in u32::from(area.top)..u32::from(area.top) + u32::from(area.height) {
311            if y >= height {
312                break;
313            }
314            for x in u32::from(area.left)..u32::from(area.left) + u32::from(area.width) {
315                if x >= width {
316                    break;
317                }
318                let at = ((y * width + x) * 4) as usize;
319                if let Some(slot) = self.canvas.get_mut(at..at + 4) {
320                    slot.copy_from_slice(value);
321                }
322            }
323        }
324    }
325
326    /// Restore a frame's rectangle from a saved copy.
327    fn restore_area(&mut self, area: ImageDescriptor, saved: &[u8]) {
328        let width = self.descriptor.width;
329        let height = self.descriptor.height;
330        let area_width = u32::from(area.width) as usize;
331        for row in 0..u32::from(area.height) {
332            let y = u32::from(area.top) + row;
333            if y >= height {
334                break;
335            }
336            for column in 0..u32::from(area.width) {
337                let x = u32::from(area.left) + column;
338                if x >= width {
339                    break;
340                }
341                let from = ((row as usize * area_width) + column as usize) * 4;
342                let to = ((y * width + x) * 4) as usize;
343                let (Some(source), Some(target)) =
344                    (saved.get(from..from + 4), self.canvas.get_mut(to..to + 4))
345                else {
346                    continue;
347                };
348                target.copy_from_slice(source);
349            }
350        }
351    }
352
353    /// Decode one image block and composite it onto the canvas.
354    fn decode_image(&mut self, source: &mut Bytes, control: GraphicControl) -> Result<Frame> {
355        let mut bytes = [0_u8; 9];
356        source.read_exact(&mut bytes)?;
357        let image = ImageDescriptor::parse(&bytes)?;
358
359        // A frame must lie within the canvas. Some encoders emit frames that
360        // overhang, and viewers clip; rejecting would fail files that display
361        // fine, so the compositing loops clip instead.
362        let local = if image.local_table_size > 0 {
363            read_table(source, image.local_table_size)?
364        } else {
365            Vec::new()
366        };
367        // Cloned rather than borrowed: compositing takes `&mut self`, and a
368        // 256-entry table is 768 bytes once per frame.
369        let palette: Vec<[u8; 3]> = if local.is_empty() {
370            self.global.clone()
371        } else {
372            local
373        };
374        if palette.is_empty() {
375            return Err(PixelsError::malformed(
376                "gif",
377                "frame has neither a local nor a global colour table",
378            ));
379        }
380
381        let mut minimum_width = [0_u8; 1];
382        source.read_exact(&mut minimum_width)?;
383        let compressed = read_sub_blocks(source, MAX_COMPRESSED)?;
384
385        let pixels = u32::from(image.width) as usize * u32::from(image.height) as usize;
386        let decoder =
387            LzwDecoder::gif(u32::from(minimum_width[0])).map_err(crate::compress_error)?;
388        // The limit is the frame's exact pixel count, which is what makes an
389        // LZW bomb a malformed-input error rather than an allocation.
390        let indices = decoder
391            .decode(&compressed, pixels)
392            .map_err(crate::compress_error)?;
393
394        // Save the rectangle *before* drawing, for a later `Previous`.
395        let saved = if control.disposal == Disposal::Previous {
396            self.save_area(image)
397        } else {
398            Vec::new()
399        };
400
401        self.composite(image, &indices, &palette, control.transparent);
402        self.pending = Some(Pending {
403            disposal: control.disposal,
404            area: image,
405            saved,
406            had_transparency: control.transparent.is_some(),
407        });
408        self.started = true;
409
410        Ok(Frame {
411            pixels: self.canvas.clone(),
412            width: self.descriptor.width,
413            height: self.descriptor.height,
414            delay_centiseconds: control.delay_centiseconds,
415            disposal: control.disposal,
416        })
417    }
418
419    /// Copy a rectangle of the canvas, for `Disposal::Previous`.
420    fn save_area(&self, area: ImageDescriptor) -> Vec<u8> {
421        let width = self.descriptor.width;
422        let height = self.descriptor.height;
423        let mut out =
424            vec![0_u8; u32::from(area.width) as usize * u32::from(area.height) as usize * 4];
425        let area_width = u32::from(area.width) as usize;
426        for row in 0..u32::from(area.height) {
427            let y = u32::from(area.top) + row;
428            if y >= height {
429                break;
430            }
431            for column in 0..u32::from(area.width) {
432                let x = u32::from(area.left) + column;
433                if x >= width {
434                    break;
435                }
436                let from = ((y * width + x) * 4) as usize;
437                let to = ((row as usize * area_width) + column as usize) * 4;
438                let (Some(source), Some(target)) =
439                    (self.canvas.get(from..from + 4), out.get_mut(to..to + 4))
440                else {
441                    continue;
442                };
443                target.copy_from_slice(source);
444            }
445        }
446        out
447    }
448
449    /// Draw palette indices onto the canvas, honouring transparency.
450    fn composite(
451        &mut self,
452        image: ImageDescriptor,
453        indices: &[u8],
454        palette: &[[u8; 3]],
455        transparent: Option<u8>,
456    ) {
457        let canvas_width = self.descriptor.width;
458        let canvas_height = self.descriptor.height;
459        let frame_width = u32::from(image.width);
460        let frame_height = u32::from(image.height);
461
462        for source_row in 0..frame_height {
463            // Interlaced frames store rows out of order; the mapping is GIF's
464            // four-pass row interlace, which is not PNG's Adam7.
465            let target_row = if image.interlaced {
466                match deinterlace(source_row, frame_height) {
467                    Some(row) => row,
468                    None => continue,
469                }
470            } else {
471                source_row
472            };
473            let y = u32::from(image.top) + target_row;
474            if y >= canvas_height {
475                continue;
476            }
477
478            for column in 0..frame_width {
479                let x = u32::from(image.left) + column;
480                if x >= canvas_width {
481                    continue;
482                }
483                let index = (source_row * frame_width + column) as usize;
484                let Some(&entry) = indices.get(index) else {
485                    // A short LZW stream leaves the rest of the frame
486                    // untouched, which is what viewers show. Truncating to an
487                    // error would reject files that display.
488                    continue;
489                };
490                if Some(entry) == transparent {
491                    // Transparent pixels let the canvas show through — the
492                    // whole point of frame-to-frame optimisation.
493                    continue;
494                }
495                let colour = palette.get(entry as usize).copied().unwrap_or([0, 0, 0]);
496                let at = ((y * canvas_width + x) * 4) as usize;
497                if let Some(slot) = self.canvas.get_mut(at..at + 4) {
498                    slot.copy_from_slice(&[colour[0], colour[1], colour[2], 255]);
499                }
500            }
501        }
502    }
503
504    /// Ensure the first frame has been decoded, for the [`Decoder`] path.
505    fn ensure_started(&mut self) -> Result<()> {
506        if self.started {
507            return Ok(());
508        }
509        match self.next_frame()? {
510            Some(_) => Ok(()),
511            None => Err(PixelsError::malformed("gif", "stream contains no frames")),
512        }
513    }
514}
515
516/// The buffered stream the frames are decoded from.
517type Bytes = std::io::Cursor<Vec<u8>>;
518
519/// Count the frames in `stream` (everything after the global colour table)
520/// and collect their delays and the `NETSCAPE2.0` loop count, skipping the
521/// pixel data. `None` for a still image. A stream that breaks off counts the
522/// frames before the break: that is a decode error, reported when pixels are
523/// read, not a reason to misreport what came before. Comment extensions are
524/// collected on the way, and whether the first frame is interlaced.
525fn scan_animation(stream: &[u8]) -> (Option<Animation>, Vec<Comment>, bool) {
526    let mut comments = Vec::new();
527    let mut interlaced = None;
528    let mut source = stream;
529    let mut delays = Vec::new();
530    let mut delay = 0_u32;
531    // No NETSCAPE2.0 block: the animation plays once.
532    let mut loop_count = 1;
533    let mut byte = [0_u8; 1];
534    while source.read_exact(&mut byte).is_ok() {
535        match byte[0] {
536            label::EXTENSION => {
537                if source.read_exact(&mut byte).is_err() {
538                    break;
539                }
540                let Ok(payload) = read_sub_blocks(&mut source, MAX_EXTENSION) else {
541                    break;
542                };
543                match byte[0] {
544                    label::GRAPHIC_CONTROL => {
545                        delay = u32::from(GraphicControl::parse(&payload).delay_centiseconds) * 10;
546                    }
547                    0xFE => comments.push(Comment {
548                        keyword: String::new(),
549                        text: String::from_utf8(payload.clone())
550                            .unwrap_or_else(|_| payload.iter().map(|&c| char::from(c)).collect()),
551                    }),
552                    // The application block's identifier is its first
553                    // sub-block; the loop count is sub-block 1 of the data.
554                    0xFF if payload.starts_with(b"NETSCAPE2.0")
555                        || payload.starts_with(b"ANIMEXTS1.0") =>
556                    {
557                        if let [1, lo, hi, ..] = payload.get(11..).unwrap_or_default() {
558                            loop_count = u32::from(u16::from_le_bytes([*lo, *hi]));
559                        }
560                    }
561                    _ => {}
562                }
563            }
564            label::IMAGE => {
565                let mut bytes = [0_u8; 9];
566                if source.read_exact(&mut bytes).is_err() {
567                    break;
568                }
569                let Ok(image) = ImageDescriptor::parse(&bytes) else {
570                    break;
571                };
572                interlaced = interlaced.or(Some(image.interlaced));
573                let table = image.local_table_size * 3;
574                let mut code_size = [0_u8; 1];
575                if source.len() < table + 1 {
576                    break;
577                }
578                source = source.get(table..).unwrap_or_default();
579                if source.read_exact(&mut code_size).is_err()
580                    || skip_sub_blocks(&mut source).is_err()
581                {
582                    break;
583                }
584                delays.push(delay);
585                delay = 0;
586            }
587            _ => break,
588        }
589    }
590    (
591        Animation::new(delays, loop_count),
592        comments,
593        interlaced.unwrap_or(false),
594    )
595}
596
597/// Map a stored row index to its position in an interlaced frame.
598fn deinterlace(stored: u32, height: u32) -> Option<u32> {
599    let mut seen = 0;
600    for pass in 0..4 {
601        let rows = interlaced_pass_rows(pass, height);
602        if stored < seen + rows {
603            return interlaced_row(pass, stored - seen, height);
604        }
605        seen += rows;
606    }
607    None
608}
609
610/// Read a colour table of `entries` RGB triples.
611fn read_table<S: Source>(source: &mut S, entries: usize) -> Result<Vec<[u8; 3]>> {
612    let mut bytes = vec![0_u8; entries * 3];
613    source.read_exact(&mut bytes)?;
614    Ok(bytes
615        .chunks_exact(3)
616        .map(|rgb| {
617            [
618                rgb.first().copied().unwrap_or(0),
619                rgb.get(1).copied().unwrap_or(0),
620                rgb.get(2).copied().unwrap_or(0),
621            ]
622        })
623        .collect())
624}
625
626impl<S: Source + std::fmt::Debug> Decoder for GifDecoder<S> {
627    fn descriptor(&self) -> CoreDescriptor {
628        self.descriptor
629    }
630
631    fn capability(&self) -> DecodeCapability {
632        DecodeCapability::Sequential
633    }
634
635    fn animation(&self) -> Option<Animation> {
636        self.animation.clone()
637    }
638
639    /// Palette, the first frame's interlace, the screen's background colour
640    /// and comment extensions. XMP is not read.
641    fn source_info(&self) -> SourceInfo {
642        self.info.clone()
643    }
644
645    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
646        self.ensure_started()?;
647        if self.row >= self.descriptor.height {
648            return Err(PixelsError::invalid_argument(
649                "out",
650                format!("all {} rows have already been read", self.descriptor.height),
651            ));
652        }
653        let row_bytes = self.descriptor.row_bytes();
654        if out.len() != row_bytes {
655            return Err(PixelsError::invalid_argument(
656                "out",
657                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
658            ));
659        }
660        let start = self.row as usize * row_bytes;
661        let row = self
662            .canvas
663            .get(start..start + row_bytes)
664            .ok_or_else(|| PixelsError::malformed("gif", "canvas is short"))?;
665        out.copy_from_slice(row);
666        self.row += 1;
667        Ok(())
668    }
669}
670
671/// Whether `prefix` starts with a GIF signature.
672///
673/// Detection is by magic bytes only (SPEC §Formats).
674#[must_use]
675pub fn probe(prefix: &[u8]) -> bool {
676    let head = prefix.get(..6);
677    head == Some(&SIGNATURE_87A[..]) || head == Some(&SIGNATURE_89A[..])
678}
679
680/// The GIF entry in a sniffing registry.
681#[derive(Debug, Clone, Copy, Default)]
682pub struct GifCodec;
683
684impl Codec for GifCodec {
685    fn format(&self) -> Format {
686        Format::Gif
687    }
688
689    fn magic_len(&self) -> usize {
690        6
691    }
692
693    fn probe(&self, prefix: &[u8]) -> bool {
694        probe(prefix)
695    }
696}