Skip to main content

otf_pixels_codec_gif/
format.rs

1//! GIF structure: the header, the logical screen, blocks and extensions.
2//!
3//! GIF89a (and the near-identical GIF87a) is a stream of *blocks* introduced by
4//! a one-byte label. A decoder walks them, accumulating state from extensions
5//! until it reaches an image, which it then renders onto a persistent canvas.
6//!
7//! # Sub-blocks
8//!
9//! Almost every variable-length payload in GIF is a chain of *sub-blocks*: a
10//! length byte, that many data bytes, repeating until a zero length. The chain
11//! is how a decoder skips an extension it does not understand without knowing
12//! anything about its contents, which is what makes unknown extensions
13//! skippable rather than fatal.
14
15use otf_pixels_core::{PixelsError, Result, Source};
16
17/// The two signatures GIF has ever used.
18pub const SIGNATURE_87A: [u8; 6] = *b"GIF87a";
19/// The 89a signature, which adds extensions and transparency.
20pub const SIGNATURE_89A: [u8; 6] = *b"GIF89a";
21
22/// Block labels (§Appendix A).
23pub mod label {
24    /// Introduces an extension block.
25    pub const EXTENSION: u8 = 0x21;
26    /// Introduces an image descriptor.
27    pub const IMAGE: u8 = 0x2C;
28    /// Ends the stream.
29    pub const TRAILER: u8 = 0x3B;
30
31    /// Graphic control extension: delay, disposal, transparency.
32    pub const GRAPHIC_CONTROL: u8 = 0xF9;
33    // The remaining extension labels are listed for the reader rather than
34    // matched on: the decoder skips every extension it does not understand by
35    // walking its sub-block chain, which is exactly what §Appendix A requires
36    // and needs no knowledge of the label.
37    //
38    // 0x01 plain text, 0xFE comment, 0xFF application (the Netscape loop
39    // count lives here).
40}
41
42/// The logical screen descriptor: the canvas every frame is drawn onto.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[non_exhaustive]
45pub struct Screen {
46    /// Canvas width in pixels.
47    pub width: u16,
48    /// Canvas height in pixels.
49    pub height: u16,
50    /// Number of entries in the global colour table, or zero if absent.
51    pub global_table_size: usize,
52    /// Index into the global table used as the background, if any.
53    pub background: u8,
54}
55
56impl Screen {
57    /// Parse the seven bytes following the signature.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`PixelsError::Malformed`] if the canvas has no pixels.
62    pub fn parse(bytes: &[u8; 7]) -> Result<Self> {
63        let width = u16::from_le_bytes([bytes[0], bytes[1]]);
64        let height = u16::from_le_bytes([bytes[2], bytes[3]]);
65        let packed = bytes[4];
66        let background = bytes[5];
67        // bytes[6] is the pixel aspect ratio, which v1 ignores: it describes
68        // display geometry, not pixels, and honouring it would resample.
69
70        if width == 0 || height == 0 {
71            return Err(PixelsError::malformed(
72                "gif",
73                format!("logical screen is {width}x{height}"),
74            ));
75        }
76
77        // Bit 7 is the global colour table flag; bits 0..2 encode its size as
78        // 2^(n+1) entries.
79        let global_table_size = if packed & 0x80 != 0 {
80            1_usize << ((packed & 0x07) + 1)
81        } else {
82            0
83        };
84
85        Ok(Self {
86            width,
87            height,
88            global_table_size,
89            background,
90        })
91    }
92}
93
94/// How the canvas is treated after a frame is displayed (§23.c.iv).
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub enum Disposal {
97    /// Unspecified: treated as [`Disposal::Keep`], which is what viewers do.
98    #[default]
99    None,
100    /// Leave the frame in place; the next one draws over it.
101    Keep,
102    /// Restore the frame's rectangle to the background colour.
103    Background,
104    /// Restore the frame's rectangle to what was there before it was drawn.
105    Previous,
106}
107
108impl Disposal {
109    /// The disposal for a packed graphic-control field.
110    #[must_use]
111    pub const fn from_bits(bits: u8) -> Self {
112        match bits {
113            1 => Self::Keep,
114            2 => Self::Background,
115            3 => Self::Previous,
116            // 4..=7 are reserved. Treating them as "none" is what viewers do,
117            // and rejecting a file over a reserved disposal value would fail
118            // images that display fine everywhere else.
119            _ => Self::None,
120        }
121    }
122}
123
124/// State from a graphic control extension, applying to the next image.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126#[non_exhaustive]
127pub struct GraphicControl {
128    /// How to dispose of the frame after displaying it.
129    pub disposal: Disposal,
130    /// Delay before the next frame, in hundredths of a second.
131    pub delay_centiseconds: u16,
132    /// Palette index treated as transparent, if any.
133    pub transparent: Option<u8>,
134}
135
136impl GraphicControl {
137    /// Parse a four-byte graphic control payload.
138    #[must_use]
139    pub fn parse(payload: &[u8]) -> Self {
140        let packed = payload.first().copied().unwrap_or(0);
141        let delay = u16::from_le_bytes([
142            payload.get(1).copied().unwrap_or(0),
143            payload.get(2).copied().unwrap_or(0),
144        ]);
145        let index = payload.get(3).copied().unwrap_or(0);
146        Self {
147            disposal: Disposal::from_bits((packed >> 2) & 0x07),
148            delay_centiseconds: delay,
149            transparent: if packed & 0x01 != 0 {
150                Some(index)
151            } else {
152                None
153            },
154        }
155    }
156}
157
158/// An image descriptor: where a frame sits on the canvas and how it is coded.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[non_exhaustive]
161pub struct ImageDescriptor {
162    /// Left edge, in canvas coordinates.
163    pub left: u16,
164    /// Top edge, in canvas coordinates.
165    pub top: u16,
166    /// Frame width.
167    pub width: u16,
168    /// Frame height.
169    pub height: u16,
170    /// Entries in the local colour table, or zero if absent.
171    pub local_table_size: usize,
172    /// Whether the frame's pixels are stored interlaced.
173    pub interlaced: bool,
174}
175
176impl ImageDescriptor {
177    /// Parse the nine bytes of an image descriptor.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`PixelsError::Malformed`] if the frame has no pixels.
182    pub fn parse(bytes: &[u8; 9]) -> Result<Self> {
183        let left = u16::from_le_bytes([bytes[0], bytes[1]]);
184        let top = u16::from_le_bytes([bytes[2], bytes[3]]);
185        let width = u16::from_le_bytes([bytes[4], bytes[5]]);
186        let height = u16::from_le_bytes([bytes[6], bytes[7]]);
187        let packed = bytes[8];
188
189        if width == 0 || height == 0 {
190            return Err(PixelsError::malformed(
191                "gif",
192                format!("frame is {width}x{height}"),
193            ));
194        }
195
196        let local_table_size = if packed & 0x80 != 0 {
197            1_usize << ((packed & 0x07) + 1)
198        } else {
199            0
200        };
201
202        Ok(Self {
203            left,
204            top,
205            width,
206            height,
207            local_table_size,
208            interlaced: packed & 0x40 != 0,
209        })
210    }
211}
212
213/// GIF's four-pass interlace (§20.i), which is *not* Adam7.
214///
215/// Rows are emitted in four passes: every 8th from 0, every 8th from 4, every
216/// 4th from 2, every 2nd from 1. Unlike PNG's Adam7 this interlaces rows only,
217/// never columns, so a pass is whole scanlines and needs no scatter.
218#[must_use]
219pub fn interlaced_row(pass: usize, index: u32, height: u32) -> Option<u32> {
220    let (start, step) = match pass {
221        0 => (0, 8),
222        1 => (4, 8),
223        2 => (2, 4),
224        3 => (1, 2),
225        _ => return None,
226    };
227    let row = start + index * step;
228    if row < height { Some(row) } else { None }
229}
230
231/// How many rows pass `pass` contains for an image of `height` rows.
232#[must_use]
233pub const fn interlaced_pass_rows(pass: usize, height: u32) -> u32 {
234    let (start, step) = match pass {
235        0 => (0_u32, 8_u32),
236        1 => (4, 8),
237        2 => (2, 4),
238        3 => (1, 2),
239        _ => return 0,
240    };
241    if height > start {
242        (height - start).div_ceil(step)
243    } else {
244        0
245    }
246}
247
248/// Read a chain of sub-blocks, concatenating their payloads.
249///
250/// `limit` bounds the total, because a chain is length-prefixed per block but
251/// unbounded overall — an attacker can repeat 255-byte blocks indefinitely.
252///
253/// # Errors
254///
255/// Returns [`PixelsError::Malformed`] if the stream ends mid-chain or the
256/// total exceeds `limit`.
257pub fn read_sub_blocks<S: Source>(source: &mut S, limit: usize) -> Result<Vec<u8>> {
258    let mut out = Vec::new();
259    loop {
260        let mut length = [0_u8; 1];
261        source.read_exact(&mut length)?;
262        let length = length[0] as usize;
263        if length == 0 {
264            return Ok(out);
265        }
266        if out.len() + length > limit {
267            return Err(PixelsError::malformed(
268                "gif",
269                format!("sub-block chain exceeds the {limit} byte limit"),
270            ));
271        }
272        let at = out.len();
273        out.resize(at + length, 0);
274        let Some(slot) = out.get_mut(at..) else {
275            return Err(PixelsError::malformed("gif", "sub-block buffer is short"));
276        };
277        source.read_exact(slot)?;
278    }
279}
280
281/// Skip a chain of sub-blocks without retaining it.
282///
283/// # Errors
284///
285/// Returns [`PixelsError::Malformed`] if the stream ends mid-chain.
286pub fn skip_sub_blocks<S: Source>(source: &mut S) -> Result<()> {
287    let mut scratch = [0_u8; 255];
288    loop {
289        let mut length = [0_u8; 1];
290        source.read_exact(&mut length)?;
291        let length = length[0] as usize;
292        if length == 0 {
293            return Ok(());
294        }
295        let Some(slot) = scratch.get_mut(..length) else {
296            return Err(PixelsError::malformed("gif", "sub-block longer than 255"));
297        };
298        source.read_exact(slot)?;
299    }
300}
301
302/// Write a payload as a chain of sub-blocks, terminated by a zero byte.
303pub fn write_sub_blocks(out: &mut Vec<u8>, payload: &[u8]) {
304    for chunk in payload.chunks(255) {
305        out.push(chunk.len() as u8);
306        out.extend_from_slice(chunk);
307    }
308    out.push(0);
309}
310
311#[cfg(test)]
312#[allow(
313    clippy::unwrap_used,
314    clippy::expect_used,
315    clippy::indexing_slicing,
316    clippy::panic,
317    reason = "tests operate on known-good values and assert shapes directly"
318)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn a_screen_descriptor_decodes_its_packed_fields() {
324        // 4x3 canvas, global table present with 2^(3+1) = 16 entries.
325        let bytes = [4, 0, 3, 0, 0x80 | 0x03, 7, 0];
326        let screen = Screen::parse(&bytes).unwrap();
327        assert_eq!((screen.width, screen.height), (4, 3));
328        assert_eq!(screen.global_table_size, 16);
329        assert_eq!(screen.background, 7);
330    }
331
332    #[test]
333    fn a_screen_without_a_global_table_reports_none() {
334        let bytes = [4, 0, 3, 0, 0x07, 0, 0];
335        let screen = Screen::parse(&bytes).unwrap();
336        assert_eq!(
337            screen.global_table_size, 0,
338            "the size bits must be ignored when the flag is clear"
339        );
340    }
341
342    #[test]
343    fn a_zero_sized_screen_is_malformed() {
344        assert!(Screen::parse(&[0, 0, 3, 0, 0, 0, 0]).is_err());
345        assert!(Screen::parse(&[4, 0, 0, 0, 0, 0, 0]).is_err());
346    }
347
348    #[test]
349    fn an_image_descriptor_decodes_its_packed_fields() {
350        // At (1,2), 5x6, local table of 2^(2+1) = 8, interlaced.
351        let bytes = [1, 0, 2, 0, 5, 0, 6, 0, 0x80 | 0x40 | 0x02];
352        let image = ImageDescriptor::parse(&bytes).unwrap();
353        assert_eq!((image.left, image.top), (1, 2));
354        assert_eq!((image.width, image.height), (5, 6));
355        assert_eq!(image.local_table_size, 8);
356        assert!(image.interlaced);
357    }
358
359    #[test]
360    fn graphic_control_decodes_disposal_and_transparency() {
361        // Disposal 2 (background), delay 0x0102, transparent index 5.
362        let control = GraphicControl::parse(&[(2 << 2) | 0x01, 0x02, 0x01, 5]);
363        assert_eq!(control.disposal, Disposal::Background);
364        assert_eq!(control.delay_centiseconds, 0x0102);
365        assert_eq!(control.transparent, Some(5));
366
367        // Transparency flag clear means no transparent index, whatever the
368        // index byte says.
369        let opaque = GraphicControl::parse(&[0, 0, 0, 5]);
370        assert_eq!(opaque.transparent, None);
371    }
372
373    #[test]
374    fn reserved_disposal_values_are_treated_as_none() {
375        // Rejecting a reserved value would fail files that display fine
376        // everywhere else.
377        for bits in 4..=7 {
378            assert_eq!(Disposal::from_bits(bits), Disposal::None, "{bits}");
379        }
380    }
381
382    #[test]
383    fn the_four_interlace_passes_cover_every_row_exactly_once() {
384        // The classic interlace bug is a row emitted twice or not at all, and
385        // it is invisible on a height that happens to divide evenly.
386        for height in 1..40_u32 {
387            let mut seen = vec![0_u32; height as usize];
388            let mut total = 0;
389            for pass in 0..4 {
390                let rows = interlaced_pass_rows(pass, height);
391                for index in 0..rows {
392                    let row = interlaced_row(pass, index, height)
393                        .unwrap_or_else(|| panic!("pass {pass} row {index} of {height}"));
394                    seen[row as usize] += 1;
395                    total += 1;
396                }
397            }
398            assert_eq!(total, height, "height {height} emitted {total} rows");
399            assert!(
400                seen.iter().all(|&n| n == 1),
401                "height {height} covered rows {seen:?}"
402            );
403        }
404    }
405
406    #[test]
407    fn interlaced_rows_past_a_pass_are_none() {
408        assert_eq!(interlaced_row(0, 0, 1), Some(0));
409        assert_eq!(interlaced_row(1, 0, 1), None, "pass 1 starts at row 4");
410        assert_eq!(
411            interlaced_row(9, 0, 100),
412            None,
413            "there are only four passes"
414        );
415    }
416
417    #[test]
418    fn sub_blocks_round_trip() {
419        for length in [0_usize, 1, 254, 255, 256, 1000] {
420            let payload: Vec<u8> = (0..length).map(|i| (i % 251) as u8).collect();
421            let mut encoded = Vec::new();
422            write_sub_blocks(&mut encoded, &payload);
423            assert_eq!(
424                encoded.last().copied(),
425                Some(0),
426                "a chain must end with a zero length"
427            );
428            let mut source = &encoded[..];
429            let decoded = read_sub_blocks(&mut source, 1 << 20).unwrap();
430            assert_eq!(decoded, payload, "length {length}");
431        }
432    }
433
434    #[test]
435    fn a_sub_block_chain_can_be_skipped_without_reading_it() {
436        let mut encoded = Vec::new();
437        write_sub_blocks(&mut encoded, &vec![7_u8; 600]);
438        encoded.extend_from_slice(b"after");
439        let mut source = &encoded[..];
440        skip_sub_blocks(&mut source).unwrap();
441        assert_eq!(source, b"after", "skipping stopped in the wrong place");
442    }
443
444    #[test]
445    fn a_truncated_chain_is_an_error_not_a_hang() {
446        let mut encoded = Vec::new();
447        write_sub_blocks(&mut encoded, &vec![1_u8; 300]);
448        for cut in 0..encoded.len() {
449            let mut source = &encoded[..cut];
450            let _ = read_sub_blocks(&mut source, 1 << 20);
451        }
452    }
453
454    #[test]
455    fn an_unbounded_chain_is_refused() {
456        // A chain is length-prefixed per block but unbounded overall, so an
457        // attacker can repeat 255-byte blocks indefinitely.
458        let mut encoded = Vec::new();
459        write_sub_blocks(&mut encoded, &vec![0_u8; 100_000]);
460        let mut source = &encoded[..];
461        let error = read_sub_blocks(&mut source, 4096).unwrap_err();
462        assert!(error.to_string().contains("limit"), "{error}");
463    }
464}