Coverage Report

Created: 2026-09-01 07:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/gif-0.14.2/src/common.rs
Line
Count
Source
1
use alloc::borrow::Cow;
2
use alloc::vec::Vec;
3
4
#[cfg(feature = "color_quant")]
5
use alloc::collections::{BTreeMap, BTreeSet};
6
7
/// Disposal method
8
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
9
#[repr(u8)]
10
pub enum DisposalMethod {
11
    /// `StreamingDecoder` is not required to take any action.
12
    Any = 0,
13
    /// Do not dispose.
14
    Keep = 1,
15
    /// Restore to background color.
16
    Background = 2,
17
    /// Restore to previous.
18
    Previous = 3,
19
}
20
21
impl DisposalMethod {
22
    /// Converts `u8` to `Option<Self>`
23
    #[must_use]
24
1.65k
    pub const fn from_u8(n: u8) -> Option<Self> {
25
1.65k
        match n {
26
727
            0 => Some(Self::Any),
27
107
            1 => Some(Self::Keep),
28
376
            2 => Some(Self::Background),
29
263
            3 => Some(Self::Previous),
30
178
            _ => None,
31
        }
32
1.65k
    }
33
}
34
35
/// Known GIF block labels.
36
///
37
/// Note that the block uniquely specifies the layout of bytes that follow and how they are
38
/// framed. For example, the header always has a fixed length but is followed by a variable amount
39
/// of additional data. An image descriptor may be followed by a local color table depending on
40
/// information read in it. Therefore, it doesn't make sense to continue parsing after encountering
41
/// an unknown block as the semantics of following bytes are unclear.
42
///
43
/// The extension block provides a common framing for an arbitrary amount of application specific
44
/// data which may be ignored.
45
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
46
#[repr(u8)]
47
pub enum Block {
48
    /// Image block.
49
    Image = 0x2C,
50
    /// Extension block.
51
    Extension = 0x21,
52
    /// Image trailer.
53
    Trailer = 0x3B,
54
}
55
56
impl Block {
57
    /// Converts `u8` to `Option<Self>`
58
    #[must_use]
59
128k
    pub const fn from_u8(n: u8) -> Option<Self> {
60
128k
        match n {
61
5.51k
            0x2C => Some(Self::Image),
62
122k
            0x21 => Some(Self::Extension),
63
5
            0x3B => Some(Self::Trailer),
64
263
            _ => None,
65
        }
66
128k
    }
67
}
68
69
/// A newtype wrapper around an arbitrary extension ID.
70
///
71
/// An extension is some amount of byte data organized in sub-blocks so that one can skip over it
72
/// without knowing the semantics. Though technically you likely want to use a `Application`
73
/// extension, the library tries to stay flexible here.
74
///
75
/// This allows us to customize the set of impls compared to a raw `u8`. It also clarifies the
76
/// intent and gives some inherent methods for interoperability with known extension types.
77
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78
pub struct AnyExtension(pub u8);
79
80
/// Known GIF extension labels.
81
///
82
/// These are extensions which may be interpreted by the library and to which a specification with
83
/// the internal data layout is known.
84
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
85
#[repr(u8)]
86
pub enum Extension {
87
    /// Plain Text extension.
88
    ///
89
    /// This instructs the decoder to render a text as characters in a grid of cells, in a
90
    /// mono-spaced font of its choosing. This is seldom actually implemented and ignored by
91
    /// ImageMagick. The color is always taken from the global table which further complicates any
92
    /// use. No real information on the frame sequencing of this block is available in the
93
    /// standard.
94
    Text = 0x01,
95
    /// Control extension.
96
    Control = 0xF9,
97
    /// Comment extension.
98
    Comment = 0xFE,
99
    /// Application extension.
100
    ///
101
    /// See [ImageMagick] for an idea of commonly recognized extensions.
102
    ///
103
    /// [ImageMagick]: https://github.com/ImageMagick/ImageMagick/blob/b0b58c6303195928060f55f9c3ca8233ab7f7733/coders/gif.c#L1128
104
    Application = 0xFF,
105
}
106
107
impl AnyExtension {
108
    /// Decode the label as a known extension.
109
    #[must_use]
110
244k
    pub const fn into_known(self) -> Option<Extension> {
111
244k
        Extension::from_u8(self.0)
112
244k
    }
113
}
114
115
impl From<Extension> for AnyExtension {
116
0
    fn from(ext: Extension) -> Self {
117
0
        Self(ext as u8)
118
0
    }
119
}
120
121
impl Extension {
122
    /// Converts `u8` to a `Extension` if it is known.
123
    #[must_use]
124
244k
    pub const fn from_u8(n: u8) -> Option<Self> {
125
244k
        match n {
126
1.92k
            0x01 => Some(Self::Text),
127
3.35k
            0xF9 => Some(Self::Control),
128
658
            0xFE => Some(Self::Comment),
129
238k
            0xFF => Some(Self::Application),
130
15
            _ => None,
131
        }
132
244k
    }
133
}
134
135
/// A GIF frame
136
#[derive(Debug, Clone)]
137
pub struct Frame<'a> {
138
    /// Frame delay in units of 10 ms.
139
    pub delay: u16,
140
    /// Disposal method.
141
    pub dispose: DisposalMethod,
142
    /// Transparent index (if available).
143
    pub transparent: Option<u8>,
144
    /// True if the frame needs user input to be displayed.
145
    pub needs_user_input: bool,
146
    /// Offset from the top border of the canvas.
147
    pub top: u16,
148
    /// Offset from the left border of the canvas.
149
    pub left: u16,
150
    /// Width of the frame.
151
    pub width: u16,
152
    /// Height of the frame.
153
    pub height: u16,
154
    /// True if the image is interlaced.
155
    pub interlaced: bool,
156
    /// Frame local color palette if available.
157
    pub palette: Option<Vec<u8>>,
158
    /// Buffer containing the image data.
159
    /// Only indices unless configured differently.
160
    pub buffer: Cow<'a, [u8]>,
161
}
162
163
impl Default for Frame<'_> {
164
12.6k
    fn default() -> Self {
165
12.6k
        Frame {
166
12.6k
            delay: 0,
167
12.6k
            dispose: DisposalMethod::Keep,
168
12.6k
            transparent: None,
169
12.6k
            needs_user_input: false,
170
12.6k
            top: 0,
171
12.6k
            left: 0,
172
12.6k
            width: 0,
173
12.6k
            height: 0,
174
12.6k
            interlaced: false,
175
12.6k
            palette: None,
176
12.6k
            buffer: Cow::Borrowed(&[]),
177
12.6k
        }
178
12.6k
    }
179
}
180
181
impl Frame<'static> {
182
    /// Creates a frame from pixels in RGBA format.
183
    ///
184
    /// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit
185
    /// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque
186
    /// pixel. Additionally, only 256 colors can appear in a single frame. The palette will be
187
    /// reduced by the NeuQuant algorithm if necessary. Different frames have independent palettes.
188
    ///
189
    /// *Note: This method is not optimized for speed.*
190
    ///
191
    /// # Panics:
192
    /// *   If the length of pixels does not equal `width * height * 4`.
193
    #[cfg(feature = "color_quant")]
194
    #[track_caller]
195
0
    pub fn from_rgba(width: u16, height: u16, pixels: &mut [u8]) -> Self {
196
0
        Frame::from_rgba_speed(width, height, pixels, 1)
197
0
    }
198
199
    /// Creates a frame from pixels in RGBA format.
200
    ///
201
    /// `speed` is a value in the range [1, 30].
202
    /// The higher the value the faster it runs at the cost of image quality.
203
    /// A `speed` of 10 is a good compromise between speed and quality.
204
    ///
205
    /// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit
206
    /// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque
207
    /// pixel. Additionally, only 256 colors can appear in a single frame. The palette will be
208
    /// reduced by the NeuQuant algorithm if necessary. Different frames have independent palettes.
209
    ///
210
    /// # Panics:
211
    /// *   If the length of pixels does not equal `width * height * 4`.
212
    /// *   If `speed < 1` or `speed > 30`
213
    #[cfg(feature = "color_quant")]
214
    #[track_caller]
215
0
    pub fn from_rgba_speed(width: u16, height: u16, pixels: &mut [u8], speed: i32) -> Self {
216
0
        assert_eq!(width as usize * height as usize * 4, pixels.len(), "Too much or too little pixel data for the given width and height to create a GIF Frame");
217
0
        assert!(
218
0
            speed >= 1 && speed <= 30,
219
0
            "speed needs to be in the range [1, 30]"
220
        );
221
0
        let mut transparent: Option<[u8; 4]> = None;
222
0
        for pix in pixels.chunks_exact_mut(4) {
223
0
            if pix[3] != 0 {
224
0
                pix[3] = 0xFF;
225
0
                continue;
226
0
            }
227
228
0
            if let Some([r, g, b, a]) = transparent {
229
0
                pix[0] = r;
230
0
                pix[1] = g;
231
0
                pix[2] = b;
232
0
                pix[3] = a;
233
0
            } else {
234
0
                transparent = Some([pix[0], pix[1], pix[2], pix[3]]);
235
0
            }
236
        }
237
238
        // Attempt to build a palette of all colors. If we go over 256 colors,
239
        // switch to the NeuQuant algorithm.
240
0
        let mut colors: BTreeSet<(u8, u8, u8, u8)> = BTreeSet::new();
241
0
        for pixel in pixels.chunks_exact(4) {
242
0
            if colors.insert((pixel[0], pixel[1], pixel[2], pixel[3])) && colors.len() > 256 {
243
                // > 256 colours, let's use NeuQuant.
244
0
                let nq = color_quant::NeuQuant::new(speed, 256, pixels);
245
246
                return Frame {
247
0
                    width,
248
0
                    height,
249
                    buffer: Cow::Owned(
250
0
                        pixels
251
0
                            .chunks_exact(4)
252
0
                            .map(|pix| nq.index_of(pix) as u8)
253
0
                            .collect(),
254
                    ),
255
0
                    palette: Some(nq.color_map_rgb()),
256
0
                    transparent: transparent.map(|t| nq.index_of(&t) as u8),
257
0
                    ..Frame::default()
258
                };
259
0
            }
260
        }
261
262
        // Palette size <= 256 elements, we can build an exact palette.
263
0
        let mut colors_vec: Vec<(u8, u8, u8, u8)> = colors.into_iter().collect();
264
0
        colors_vec.sort_unstable();
265
0
        let palette = colors_vec
266
0
            .iter()
267
0
            .flat_map(|&(r, g, b, _a)| [r, g, b])
268
0
            .collect();
269
0
        let colors_lookup: BTreeMap<(u8, u8, u8, u8), u8> =
270
0
            colors_vec.into_iter().zip(0..=255).collect();
271
272
0
        let index_of = |pixel: &[u8]| {
273
0
            colors_lookup
274
0
                .get(&(pixel[0], pixel[1], pixel[2], pixel[3]))
275
0
                .copied()
276
0
                .unwrap_or(0)
277
0
        };
278
279
        Frame {
280
0
            width,
281
0
            height,
282
0
            buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()),
283
0
            palette: Some(palette),
284
0
            transparent: transparent.map(|t| index_of(&t)),
285
0
            ..Frame::default()
286
        }
287
0
    }
288
289
    /// Creates a frame from pixels in LumaAlpha format (grayscale pixels with transparency).
290
    ///
291
    /// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit
292
    /// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque
293
    /// pixel. The least used color will be used to indicate alpha and replace with the closest other color
294
    /// in the image. Different frames have independent palettes.
295
    ///
296
    /// # Panics:
297
    /// *   If the length of pixels does not equal `width * height * 2`.
298
0
    pub fn from_grayscale_with_alpha(width: u16, height: u16, pixels: &[u8]) -> Self {
299
0
        assert_eq!(width as usize * height as usize * 2, pixels.len(), "Too much or too little pixel data for the given width and height to create a GIF Frame");
300
301
        // Input is in LumaA format.
302
        // Count the occurrences of all the colors, then pick the least common color as alpha.
303
0
        let mut num_transparent_pixels: u32 = 0;
304
0
        let mut color_frequencies: [u32; 256] = [0; 256];
305
0
        for pixel in pixels.chunks_exact(2) {
306
0
            let color = pixel[0];
307
0
            let alpha = pixel[1];
308
            // do not count colors in fully transparent pixels
309
0
            if alpha == 0 {
310
0
                num_transparent_pixels += 1;
311
0
            } else {
312
0
                color_frequencies[color as usize] += 1;
313
0
            }
314
        }
315
316
0
        let grayscale_palette: Vec<u8> = (0..=255).flat_map(|i| [i, i, i]).collect();
317
318
        // If there were no fully transparent pixels, do not allocate a color to transparency in the GIF
319
        // and return immediately with the generic grayscale palette
320
0
        if num_transparent_pixels == 0 {
321
0
            let stripped_alpha: Vec<u8> = pixels.chunks_exact(2).map(|pixel| pixel[0]).collect();
322
0
            return Frame {
323
0
                width,
324
0
                height,
325
0
                buffer: Cow::Owned(stripped_alpha),
326
0
                palette: Some(grayscale_palette),
327
0
                transparent: None,
328
0
                ..Frame::default()
329
0
            };
330
0
        }
331
332
        // Choose the color that will be our alpha color
333
0
        let least_used_color = color_frequencies
334
0
            .iter()
335
0
            .enumerate()
336
0
            .min_by_key(|(_, &value)| value)
337
0
            .map(|(index, _)| index as u8)
338
0
            .expect("input slice is empty");
339
340
        // pick the less used color out of the neighbours as the replacement color
341
0
        let replacement_color = if least_used_color == 255 {
342
0
            254
343
0
        } else if least_used_color == 0 {
344
0
            1
345
0
        } else if color_frequencies[(least_used_color - 1) as usize]
346
0
            < color_frequencies[(least_used_color + 1) as usize]
347
        {
348
0
            least_used_color - 1
349
        } else {
350
0
            least_used_color + 1
351
        };
352
353
        // Strip alpha and replace fully transparent pixels with the chosen color
354
0
        let paletted: Vec<u8> = pixels
355
0
            .chunks_exact(2)
356
0
            .map(|pixel| {
357
0
                let color = pixel[0];
358
0
                let alpha = pixel[1];
359
0
                if alpha == 0 {
360
0
                    least_used_color
361
0
                } else if color == least_used_color {
362
0
                    replacement_color
363
                } else {
364
0
                    color
365
                }
366
0
            })
367
0
            .collect();
368
369
0
        Frame {
370
0
            width,
371
0
            height,
372
0
            buffer: Cow::Owned(paletted),
373
0
            palette: Some(grayscale_palette),
374
0
            transparent: Some(least_used_color),
375
0
            ..Frame::default()
376
0
        }
377
0
    }
378
379
    /// Creates a frame from a palette and indexed pixels.
380
    ///
381
    /// # Panics:
382
    /// *   If the length of pixels does not equal `width * height`.
383
    /// *   If the length of palette > `256 * 3`.
384
    #[track_caller]
385
0
    pub fn from_palette_pixels(
386
0
        width: u16,
387
0
        height: u16,
388
0
        pixels: impl Into<Vec<u8>>,
389
0
        palette: impl Into<Vec<u8>>,
390
0
        transparent: Option<u8>,
391
0
    ) -> Self {
392
0
        let pixels = pixels.into();
393
0
        let palette = palette.into();
394
0
        assert_eq!(
395
0
            width as usize * height as usize,
396
0
            pixels.len(),
397
0
            "Too many or too little pixels for the given width and height to create a GIF Frame"
398
        );
399
0
        assert!(
400
0
            palette.len() <= 256 * 3,
401
0
            "Too many palette values to create a GIF Frame"
402
        );
403
404
0
        Frame {
405
0
            width,
406
0
            height,
407
0
            buffer: Cow::Owned(pixels),
408
0
            palette: Some(palette),
409
0
            transparent,
410
0
            ..Frame::default()
411
0
        }
412
0
    }
Unexecuted instantiation: <gif::common::Frame>::from_palette_pixels::<_, _>
Unexecuted instantiation: <gif::common::Frame>::from_palette_pixels::<&[u8], alloc::vec::Vec<u8>>
413
414
    /// Creates a frame from indexed pixels in the global palette.
415
    ///
416
    /// # Panics:
417
    /// *   If the length of pixels does not equal `width * height`.
418
    #[track_caller]
419
0
    pub fn from_indexed_pixels(
420
0
        width: u16,
421
0
        height: u16,
422
0
        pixels: impl Into<Vec<u8>>,
423
0
        transparent: Option<u8>,
424
0
    ) -> Self {
425
0
        let pixels = pixels.into();
426
0
        assert_eq!(
427
0
            width as usize * height as usize,
428
0
            pixels.len(),
429
0
            "Too many or too little pixels for the given width and height to create a GIF Frame"
430
        );
431
432
0
        Frame {
433
0
            width,
434
0
            height,
435
0
            buffer: Cow::Owned(pixels),
436
0
            palette: None,
437
0
            transparent,
438
0
            ..Frame::default()
439
0
        }
440
0
    }
441
442
    /// Creates a frame from pixels in RGB format.
443
    ///
444
    /// This is a lossy method. In the `gif` format only 256 colors can appear in a single frame.
445
    /// The palette will be reduced by the NeuQuant algorithm if necessary. Different frames have
446
    /// independent palettes.
447
    ///
448
    /// *Note: This method is not optimized for speed.*
449
    ///
450
    /// # Panics:
451
    /// *   If the length of pixels does not equal `width * height * 3`.
452
    #[cfg(feature = "color_quant")]
453
    #[must_use]
454
    #[track_caller]
455
0
    pub fn from_rgb(width: u16, height: u16, pixels: &[u8]) -> Self {
456
0
        Frame::from_rgb_speed(width, height, pixels, 1)
457
0
    }
458
459
    /// Creates a frame from pixels in RGB format.
460
    ///
461
    /// `speed` is a value in the range [1, 30].
462
    ///
463
    /// This is a lossy method. In the `gif` format only 256 colors can appear in a single frame.
464
    /// The palette will be reduced by the NeuQuant algorithm if necessary. Different frames have
465
    /// independent palettes.
466
    ///
467
    /// The higher the value the faster it runs at the cost of image quality.
468
    /// A `speed` of 10 is a good compromise between speed and quality.
469
    ///
470
    /// # Panics:
471
    /// *   If the length of pixels does not equal `width * height * 3`.
472
    /// *   If `speed < 1` or `speed > 30`
473
    #[cfg(feature = "color_quant")]
474
    #[must_use]
475
    #[track_caller]
476
0
    pub fn from_rgb_speed(width: u16, height: u16, pixels: &[u8], speed: i32) -> Self {
477
0
        assert_eq!(width as usize * height as usize * 3, pixels.len(), "Too much or too little pixel data for the given width and height to create a GIF Frame");
478
0
        let mut vec: Vec<u8> = Vec::new();
479
0
        vec.try_reserve_exact(pixels.len() + width as usize * height as usize)
480
0
            .expect("OOM");
481
0
        for v in pixels.chunks_exact(3) {
482
0
            vec.extend_from_slice(&[v[0], v[1], v[2], 0xFF]);
483
0
        }
484
0
        Frame::from_rgba_speed(width, height, &mut vec, speed)
485
0
    }
486
487
    /// Leaves empty buffer and empty palette behind
488
    #[inline]
489
5.43k
    pub(crate) fn take(&mut self) -> Self {
490
5.43k
        Frame {
491
5.43k
            delay: self.delay,
492
5.43k
            dispose: self.dispose,
493
5.43k
            transparent: self.transparent,
494
5.43k
            needs_user_input: self.needs_user_input,
495
5.43k
            top: self.top,
496
5.43k
            left: self.left,
497
5.43k
            width: self.width,
498
5.43k
            height: self.height,
499
5.43k
            interlaced: self.interlaced,
500
5.43k
            palette: core::mem::take(&mut self.palette),
501
5.43k
            buffer: core::mem::replace(&mut self.buffer, Cow::Borrowed(&[])),
502
5.43k
        }
503
5.43k
    }
<gif::common::Frame>::take
Line
Count
Source
489
5.43k
    pub(crate) fn take(&mut self) -> Self {
490
5.43k
        Frame {
491
5.43k
            delay: self.delay,
492
5.43k
            dispose: self.dispose,
493
5.43k
            transparent: self.transparent,
494
5.43k
            needs_user_input: self.needs_user_input,
495
5.43k
            top: self.top,
496
5.43k
            left: self.left,
497
5.43k
            width: self.width,
498
5.43k
            height: self.height,
499
5.43k
            interlaced: self.interlaced,
500
5.43k
            palette: core::mem::take(&mut self.palette),
501
5.43k
            buffer: core::mem::replace(&mut self.buffer, Cow::Borrowed(&[])),
502
5.43k
        }
503
5.43k
    }
Unexecuted instantiation: <gif::common::Frame>::take
504
}
505
506
#[test]
507
#[cfg(feature = "color_quant")]
508
// Creating the `colors_lookup` hashmap in Frame::from_rgba_speed panics due to
509
// overflow while bypassing NeuQuant and zipping a RangeFrom with 256 colors.
510
// Changing .zip(0_u8..) to .zip(0_u8..=255) fixes this issue.
511
fn rgba_speed_avoid_panic_256_colors() {
512
    let side = 16;
513
    let pixel_data: Vec<u8> = (0..=255).flat_map(|a| [a, a, a]).collect();
514
    let _ = Frame::from_rgb(side, side, &pixel_data);
515
}