Skip to main content

gif/
common.rs

1use alloc::borrow::Cow;
2use alloc::vec::Vec;
3
4#[cfg(feature = "color_quant")]
5use alloc::collections::{BTreeMap, BTreeSet};
6
7/// Disposal method
8#[derive(Debug, Copy, Clone, PartialEq, Eq)]
9#[repr(u8)]
10pub 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
21impl DisposalMethod {
22    /// Converts `u8` to `Option<Self>`
23    #[must_use]
24    pub const fn from_u8(n: u8) -> Option<Self> {
25        match n {
26            0 => Some(Self::Any),
27            1 => Some(Self::Keep),
28            2 => Some(Self::Background),
29            3 => Some(Self::Previous),
30            _ => None,
31        }
32    }
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)]
47pub enum Block {
48    /// Image block.
49    Image = 0x2C,
50    /// Extension block.
51    Extension = 0x21,
52    /// Image trailer.
53    Trailer = 0x3B,
54}
55
56impl Block {
57    /// Converts `u8` to `Option<Self>`
58    #[must_use]
59    pub const fn from_u8(n: u8) -> Option<Self> {
60        match n {
61            0x2C => Some(Self::Image),
62            0x21 => Some(Self::Extension),
63            0x3B => Some(Self::Trailer),
64            _ => None,
65        }
66    }
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)]
78pub 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)]
86pub 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
107impl AnyExtension {
108    /// Decode the label as a known extension.
109    #[must_use]
110    pub const fn into_known(self) -> Option<Extension> {
111        Extension::from_u8(self.0)
112    }
113}
114
115impl From<Extension> for AnyExtension {
116    fn from(ext: Extension) -> Self {
117        Self(ext as u8)
118    }
119}
120
121impl Extension {
122    /// Converts `u8` to a `Extension` if it is known.
123    #[must_use]
124    pub const fn from_u8(n: u8) -> Option<Self> {
125        match n {
126            0x01 => Some(Self::Text),
127            0xF9 => Some(Self::Control),
128            0xFE => Some(Self::Comment),
129            0xFF => Some(Self::Application),
130            _ => None,
131        }
132    }
133}
134
135/// A GIF frame
136#[derive(Debug, Clone)]
137pub 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
163impl Default for Frame<'_> {
164    fn default() -> Self {
165        Frame {
166            delay: 0,
167            dispose: DisposalMethod::Keep,
168            transparent: None,
169            needs_user_input: false,
170            top: 0,
171            left: 0,
172            width: 0,
173            height: 0,
174            interlaced: false,
175            palette: None,
176            buffer: Cow::Borrowed(&[]),
177        }
178    }
179}
180
181impl 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    pub fn from_rgba(width: u16, height: u16, pixels: &mut [u8]) -> Self {
196        Frame::from_rgba_speed(width, height, pixels, 1)
197    }
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    pub fn from_rgba_speed(width: u16, height: u16, pixels: &mut [u8], speed: i32) -> Self {
216        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        assert!(
218            speed >= 1 && speed <= 30,
219            "speed needs to be in the range [1, 30]"
220        );
221        let mut transparent: Option<[u8; 4]> = None;
222        for pix in pixels.chunks_exact_mut(4) {
223            if pix[3] != 0 {
224                pix[3] = 0xFF;
225                continue;
226            }
227
228            if let Some([r, g, b, a]) = transparent {
229                pix[0] = r;
230                pix[1] = g;
231                pix[2] = b;
232                pix[3] = a;
233            } else {
234                transparent = Some([pix[0], pix[1], pix[2], pix[3]]);
235            }
236        }
237
238        // Attempt to build a palette of all colors. If we go over 256 colors,
239        // switch to the NeuQuant algorithm.
240        let mut colors: BTreeSet<(u8, u8, u8, u8)> = BTreeSet::new();
241        for pixel in pixels.chunks_exact(4) {
242            if colors.insert((pixel[0], pixel[1], pixel[2], pixel[3])) && colors.len() > 256 {
243                // > 256 colours, let's use NeuQuant.
244                let nq = color_quant::NeuQuant::new(speed, 256, pixels);
245
246                return Frame {
247                    width,
248                    height,
249                    buffer: Cow::Owned(
250                        pixels
251                            .chunks_exact(4)
252                            .map(|pix| nq.index_of(pix) as u8)
253                            .collect(),
254                    ),
255                    palette: Some(nq.color_map_rgb()),
256                    transparent: transparent.map(|t| nq.index_of(&t) as u8),
257                    ..Frame::default()
258                };
259            }
260        }
261
262        // Palette size <= 256 elements, we can build an exact palette.
263        let mut colors_vec: Vec<(u8, u8, u8, u8)> = colors.into_iter().collect();
264        colors_vec.sort_unstable();
265        let palette = colors_vec
266            .iter()
267            .flat_map(|&(r, g, b, _a)| [r, g, b])
268            .collect();
269        let colors_lookup: BTreeMap<(u8, u8, u8, u8), u8> =
270            colors_vec.into_iter().zip(0..=255).collect();
271
272        let index_of = |pixel: &[u8]| {
273            colors_lookup
274                .get(&(pixel[0], pixel[1], pixel[2], pixel[3]))
275                .copied()
276                .unwrap_or(0)
277        };
278
279        Frame {
280            width,
281            height,
282            buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()),
283            palette: Some(palette),
284            transparent: transparent.map(|t| index_of(&t)),
285            ..Frame::default()
286        }
287    }
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    pub fn from_grayscale_with_alpha(width: u16, height: u16, pixels: &[u8]) -> Self {
299        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        let mut num_transparent_pixels: u32 = 0;
304        let mut color_frequencies: [u32; 256] = [0; 256];
305        for pixel in pixels.chunks_exact(2) {
306            let color = pixel[0];
307            let alpha = pixel[1];
308            // do not count colors in fully transparent pixels
309            if alpha == 0 {
310                num_transparent_pixels += 1;
311            } else {
312                color_frequencies[color as usize] += 1;
313            }
314        }
315
316        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        if num_transparent_pixels == 0 {
321            let stripped_alpha: Vec<u8> = pixels.chunks_exact(2).map(|pixel| pixel[0]).collect();
322            return Frame {
323                width,
324                height,
325                buffer: Cow::Owned(stripped_alpha),
326                palette: Some(grayscale_palette),
327                transparent: None,
328                ..Frame::default()
329            };
330        }
331
332        // Choose the color that will be our alpha color
333        let least_used_color = color_frequencies
334            .iter()
335            .enumerate()
336            .min_by_key(|(_, &value)| value)
337            .map(|(index, _)| index as u8)
338            .expect("input slice is empty");
339
340        // pick the less used color out of the neighbours as the replacement color
341        let replacement_color = if least_used_color == 255 {
342            254
343        } else if least_used_color == 0 {
344            1
345        } else if color_frequencies[(least_used_color - 1) as usize]
346            < color_frequencies[(least_used_color + 1) as usize]
347        {
348            least_used_color - 1
349        } else {
350            least_used_color + 1
351        };
352
353        // Strip alpha and replace fully transparent pixels with the chosen color
354        let paletted: Vec<u8> = pixels
355            .chunks_exact(2)
356            .map(|pixel| {
357                let color = pixel[0];
358                let alpha = pixel[1];
359                if alpha == 0 {
360                    least_used_color
361                } else if color == least_used_color {
362                    replacement_color
363                } else {
364                    color
365                }
366            })
367            .collect();
368
369        Frame {
370            width,
371            height,
372            buffer: Cow::Owned(paletted),
373            palette: Some(grayscale_palette),
374            transparent: Some(least_used_color),
375            ..Frame::default()
376        }
377    }
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    pub fn from_palette_pixels(
386        width: u16,
387        height: u16,
388        pixels: impl Into<Vec<u8>>,
389        palette: impl Into<Vec<u8>>,
390        transparent: Option<u8>,
391    ) -> Self {
392        let pixels = pixels.into();
393        let palette = palette.into();
394        assert_eq!(
395            width as usize * height as usize,
396            pixels.len(),
397            "Too many or too little pixels for the given width and height to create a GIF Frame"
398        );
399        assert!(
400            palette.len() <= 256 * 3,
401            "Too many palette values to create a GIF Frame"
402        );
403
404        Frame {
405            width,
406            height,
407            buffer: Cow::Owned(pixels),
408            palette: Some(palette),
409            transparent,
410            ..Frame::default()
411        }
412    }
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    pub fn from_indexed_pixels(
420        width: u16,
421        height: u16,
422        pixels: impl Into<Vec<u8>>,
423        transparent: Option<u8>,
424    ) -> Self {
425        let pixels = pixels.into();
426        assert_eq!(
427            width as usize * height as usize,
428            pixels.len(),
429            "Too many or too little pixels for the given width and height to create a GIF Frame"
430        );
431
432        Frame {
433            width,
434            height,
435            buffer: Cow::Owned(pixels),
436            palette: None,
437            transparent,
438            ..Frame::default()
439        }
440    }
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    pub fn from_rgb(width: u16, height: u16, pixels: &[u8]) -> Self {
456        Frame::from_rgb_speed(width, height, pixels, 1)
457    }
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    pub fn from_rgb_speed(width: u16, height: u16, pixels: &[u8], speed: i32) -> Self {
477        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        let mut vec: Vec<u8> = Vec::new();
479        vec.try_reserve_exact(pixels.len() + width as usize * height as usize)
480            .expect("OOM");
481        for v in pixels.chunks_exact(3) {
482            vec.extend_from_slice(&[v[0], v[1], v[2], 0xFF]);
483        }
484        Frame::from_rgba_speed(width, height, &mut vec, speed)
485    }
486
487    /// Leaves empty buffer and empty palette behind
488    #[inline]
489    pub(crate) fn take(&mut self) -> Self {
490        Frame {
491            delay: self.delay,
492            dispose: self.dispose,
493            transparent: self.transparent,
494            needs_user_input: self.needs_user_input,
495            top: self.top,
496            left: self.left,
497            width: self.width,
498            height: self.height,
499            interlaced: self.interlaced,
500            palette: core::mem::take(&mut self.palette),
501            buffer: core::mem::replace(&mut self.buffer, Cow::Borrowed(&[])),
502        }
503    }
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.
511fn 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}