gif/
common.rs

1use std::borrow::Cow;
2#[cfg(feature = "color_quant")]
3use std::collections::{HashMap, HashSet};
4
5/// Disposal method
6#[derive(Debug, Copy, Clone, PartialEq, Eq)]
7#[repr(u8)]
8pub enum DisposalMethod {
9    /// `StreamingDecoder` is not required to take any action.
10    Any = 0,
11    /// Do not dispose.
12    Keep = 1,
13    /// Restore to background color.
14    Background = 2,
15    /// Restore to previous.
16    Previous = 3,
17}
18
19impl DisposalMethod {
20    /// Converts `u8` to `Option<Self>`
21    #[must_use]
22    pub const fn from_u8(n: u8) -> Option<Self> {
23        match n {
24            0 => Some(Self::Any),
25            1 => Some(Self::Keep),
26            2 => Some(Self::Background),
27            3 => Some(Self::Previous),
28            _ => None,
29        }
30    }
31}
32
33/// Known GIF block labels.
34///
35/// Note that the block uniquely specifies the layout of bytes that follow and how they are
36/// framed. For example, the header always has a fixed length but is followed by a variable amount
37/// of additional data. An image descriptor may be followed by a local color table depending on
38/// information read in it. Therefore, it doesn't make sense to continue parsing after encountering
39/// an unknown block as the semantics of following bytes are unclear.
40///
41/// The extension block provides a common framing for an arbitrary amount of application specific
42/// data which may be ignored.
43#[derive(Debug, Copy, Clone, PartialEq, Eq)]
44#[repr(u8)]
45pub enum Block {
46    /// Image block.
47    Image = 0x2C,
48    /// Extension block.
49    Extension = 0x21,
50    /// Image trailer.
51    Trailer = 0x3B,
52}
53
54impl Block {
55    /// Converts `u8` to `Option<Self>`
56    #[must_use]
57    pub const fn from_u8(n: u8) -> Option<Self> {
58        match n {
59            0x2C => Some(Self::Image),
60            0x21 => Some(Self::Extension),
61            0x3B => Some(Self::Trailer),
62            _ => None,
63        }
64    }
65}
66
67/// A newtype wrapper around an arbitrary extension ID.
68///
69/// An extension is some amount of byte data organized in sub-blocks so that one can skip over it
70/// without knowing the semantics. Though technically you likely want to use a `Application`
71/// extension, the library tries to stay flexible here.
72///
73/// This allows us to customize the set of impls compared to a raw `u8`. It also clarifies the
74/// intent and gives some inherent methods for interoperability with known extension types.
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
76pub struct AnyExtension(pub u8);
77
78/// Known GIF extension labels.
79///
80/// These are extensions which may be interpreted by the library and to which a specification with
81/// the internal data layout is known.
82#[derive(Debug, Copy, Clone, PartialEq, Eq)]
83#[repr(u8)]
84pub enum Extension {
85    /// Plain Text extension.
86    ///
87    /// This instructs the decoder to render a text as characters in a grid of cells, in a
88    /// mono-spaced font of its choosing. This is seldom actually implemented and ignored by
89    /// ImageMagick. The color is always taken from the global table which further complicates any
90    /// use. No real information on the frame sequencing of this block is available in the
91    /// standard.
92    Text = 0x01,
93    /// Control extension.
94    Control = 0xF9,
95    /// Comment extension.
96    Comment = 0xFE,
97    /// Application extension.
98    ///
99    /// See [ImageMagick] for an idea of commonly recognized extensions.
100    ///
101    /// [ImageMagick]: https://github.com/ImageMagick/ImageMagick/blob/b0b58c6303195928060f55f9c3ca8233ab7f7733/coders/gif.c#L1128
102    Application = 0xFF,
103}
104
105impl AnyExtension {
106    /// Decode the label as a known extension.
107    #[must_use]
108    pub const fn into_known(self) -> Option<Extension> {
109        Extension::from_u8(self.0)
110    }
111}
112
113impl From<Extension> for AnyExtension {
114    fn from(ext: Extension) -> Self {
115        Self(ext as u8)
116    }
117}
118
119impl Extension {
120    /// Converts `u8` to a `Extension` if it is known.
121    #[must_use]
122    pub const fn from_u8(n: u8) -> Option<Self> {
123        match n {
124            0x01 => Some(Self::Text),
125            0xF9 => Some(Self::Control),
126            0xFE => Some(Self::Comment),
127            0xFF => Some(Self::Application),
128            _ => None,
129        }
130    }
131}
132
133/// A GIF frame
134#[derive(Debug, Clone)]
135pub struct Frame<'a> {
136    /// Frame delay in units of 10 ms.
137    pub delay: u16,
138    /// Disposal method.
139    pub dispose: DisposalMethod,
140    /// Transparent index (if available).
141    pub transparent: Option<u8>,
142    /// True if the frame needs user input to be displayed.
143    pub needs_user_input: bool,
144    /// Offset from the top border of the canvas.
145    pub top: u16,
146    /// Offset from the left border of the canvas.
147    pub left: u16,
148    /// Width of the frame.
149    pub width: u16,
150    /// Height of the frame.
151    pub height: u16,
152    /// True if the image is interlaced.
153    pub interlaced: bool,
154    /// Frame local color palette if available.
155    pub palette: Option<Vec<u8>>,
156    /// Buffer containing the image data.
157    /// Only indices unless configured differently.
158    pub buffer: Cow<'a, [u8]>,
159}
160
161impl Default for Frame<'_> {
162    fn default() -> Self {
163        Frame {
164            delay: 0,
165            dispose: DisposalMethod::Keep,
166            transparent: None,
167            needs_user_input: false,
168            top: 0,
169            left: 0,
170            width: 0,
171            height: 0,
172            interlaced: false,
173            palette: None,
174            buffer: Cow::Borrowed(&[]),
175        }
176    }
177}
178
179impl Frame<'static> {
180    /// Creates a frame from pixels in RGBA format.
181    ///
182    /// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit
183    /// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque
184    /// pixel. Additionally, only 256 colors can appear in a single frame. The palette will be
185    /// reduced by the NeuQuant algorithm if necessary. Different frames have independent palettes.
186    ///
187    /// *Note: This method is not optimized for speed.*
188    ///
189    /// # Panics:
190    /// *   If the length of pixels does not equal `width * height * 4`.
191    #[cfg(feature = "color_quant")]
192    #[track_caller]
193    pub fn from_rgba(width: u16, height: u16, pixels: &mut [u8]) -> Self {
194        Frame::from_rgba_speed(width, height, pixels, 1)
195    }
196
197    /// Creates a frame from pixels in RGBA format.
198    ///
199    /// `speed` is a value in the range [1, 30].
200    /// The higher the value the faster it runs at the cost of image quality.
201    /// A `speed` of 10 is a good compromise between speed and quality.
202    ///
203    /// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit
204    /// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque
205    /// pixel. Additionally, only 256 colors can appear in a single frame. The palette will be
206    /// reduced by the NeuQuant algorithm if necessary. Different frames have independent palettes.
207    ///
208    /// # Panics:
209    /// *   If the length of pixels does not equal `width * height * 4`.
210    /// *   If `speed < 1` or `speed > 30`
211    #[cfg(feature = "color_quant")]
212    #[track_caller]
213    pub fn from_rgba_speed(width: u16, height: u16, pixels: &mut [u8], speed: i32) -> Self {
214        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");
215        assert!(speed >= 1 && speed <= 30, "speed needs to be in the range [1, 30]");
216        let mut transparent = None;
217        for pix in pixels.chunks_exact_mut(4) {
218            if pix[3] != 0 {
219                pix[3] = 0xFF;
220            } else {
221                transparent = Some([pix[0], pix[1], pix[2], pix[3]]);
222            }
223        }
224
225        // Attempt to build a palette of all colors. If we go over 256 colors,
226        // switch to the NeuQuant algorithm.
227        let mut colors: HashSet<(u8, u8, u8, u8)> = HashSet::new();
228        for pixel in pixels.chunks_exact(4) {
229            if colors.insert((pixel[0], pixel[1], pixel[2], pixel[3])) && colors.len() > 256 {
230                // > 256 colours, let's use NeuQuant.
231                let nq = color_quant::NeuQuant::new(speed, 256, pixels);
232
233                return Frame {
234                    width,
235                    height,
236                    buffer: Cow::Owned(pixels.chunks_exact(4).map(|pix| nq.index_of(pix) as u8).collect()),
237                    palette: Some(nq.color_map_rgb()),
238                    transparent: transparent.map(|t| nq.index_of(&t) as u8),
239                    ..Frame::default()
240                };
241            }
242        }
243
244        // Palette size <= 256 elements, we can build an exact palette.
245        let mut colors_vec: Vec<(u8, u8, u8, u8)> = colors.into_iter().collect();
246        colors_vec.sort_unstable();
247        let palette = colors_vec.iter().flat_map(|&(r, g, b, _a)| [r, g, b]).collect();
248        let colors_lookup: HashMap<(u8, u8, u8, u8), u8> = colors_vec.into_iter().zip(0..=255).collect();
249
250        let index_of = | pixel: &[u8] |
251            colors_lookup.get(&(pixel[0], pixel[1], pixel[2], pixel[3])).copied().unwrap_or(0);
252
253        Frame {
254            width,
255            height,
256            buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()),
257            palette: Some(palette),
258            transparent: transparent.map(|t| index_of(&t)),
259            ..Frame::default()
260        }
261    }
262
263    /// Creates a frame from a palette and indexed pixels.
264    ///
265    /// # Panics:
266    /// *   If the length of pixels does not equal `width * height`.
267    /// *   If the length of palette > `256 * 3`.
268    #[track_caller]
269    pub fn from_palette_pixels(width: u16, height: u16, pixels: impl Into<Vec<u8>>, palette: impl Into<Vec<u8>>, transparent: Option<u8>) -> Self {
270        let pixels = pixels.into();
271        let palette = palette.into();
272        assert_eq!(width as usize * height as usize, pixels.len(), "Too many or too little pixels for the given width and height to create a GIF Frame");
273        assert!(palette.len() <= 256*3, "Too many palette values to create a GIF Frame");
274
275        Frame {
276            width,
277            height,
278            buffer: Cow::Owned(pixels),
279            palette: Some(palette),
280            transparent,
281            ..Frame::default()
282        }
283    }
284
285    /// Creates a frame from indexed pixels in the global palette.
286    ///
287    /// # Panics:
288    /// *   If the length of pixels does not equal `width * height`.
289    #[track_caller]
290    pub fn from_indexed_pixels(width: u16, height: u16, pixels: impl Into<Vec<u8>>, transparent: Option<u8>) -> Self {
291        let pixels = pixels.into();
292        assert_eq!(width as usize * height as usize, pixels.len(), "Too many or too little pixels for the given width and height to create a GIF Frame");
293
294        Frame {
295            width,
296            height,
297            buffer: Cow::Owned(pixels),
298            palette: None,
299            transparent,
300            ..Frame::default()
301        }
302    }
303
304    /// Creates a frame from pixels in RGB format.
305    ///
306    /// This is a lossy method. In the `gif` format only 256 colors can appear in a single frame.
307    /// The palette will be reduced by the NeuQuant algorithm if necessary. Different frames have
308    /// independent palettes.
309    ///
310    /// *Note: This method is not optimized for speed.*
311    ///
312    /// # Panics:
313    /// *   If the length of pixels does not equal `width * height * 3`.
314    #[cfg(feature = "color_quant")]
315    #[must_use]
316    #[track_caller]
317    pub fn from_rgb(width: u16, height: u16, pixels: &[u8]) -> Self {
318        Frame::from_rgb_speed(width, height, pixels, 1)
319    }
320
321    /// Creates a frame from pixels in RGB format.
322    ///
323    /// `speed` is a value in the range [1, 30].
324    ///
325    /// This is a lossy method. In the `gif` format only 256 colors can appear in a single frame.
326    /// The palette will be reduced by the NeuQuant algorithm if necessary. Different frames have
327    /// independent palettes.
328    ///
329    /// The higher the value the faster it runs at the cost of image quality.
330    /// A `speed` of 10 is a good compromise between speed and quality.
331    ///
332    /// # Panics:
333    /// *   If the length of pixels does not equal `width * height * 3`.
334    /// *   If `speed < 1` or `speed > 30`
335    #[cfg(feature = "color_quant")]
336    #[must_use]
337    #[track_caller]
338    pub fn from_rgb_speed(width: u16, height: u16, pixels: &[u8], speed: i32) -> Self {
339        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");
340        let mut vec: Vec<u8> = Vec::new();
341        vec.try_reserve_exact(pixels.len() + width as usize * height as usize).expect("OOM");
342        for v in pixels.chunks_exact(3) {
343            vec.extend_from_slice(&[v[0], v[1], v[2], 0xFF]);
344        }
345        Frame::from_rgba_speed(width, height, &mut vec, speed)
346    }
347
348    /// Leaves empty buffer and empty palette behind
349    #[inline]
350    pub(crate) fn take(&mut self) -> Self {
351        Frame {
352            delay: self.delay,
353            dispose: self.dispose,
354            transparent: self.transparent,
355            needs_user_input: self.needs_user_input,
356            top: self.top,
357            left: self.left,
358            width: self.width,
359            height: self.height,
360            interlaced: self.interlaced,
361            palette: std::mem::take(&mut self.palette),
362            buffer: std::mem::replace(&mut self.buffer, Cow::Borrowed(&[])),
363        }
364    }
365}
366
367#[test]
368#[cfg(feature = "color_quant")]
369// Creating the `colors_lookup` hashmap in Frame::from_rgba_speed panics due to
370// overflow while bypassing NeuQuant and zipping a RangeFrom with 256 colors.
371// Changing .zip(0_u8..) to .zip(0_u8..=255) fixes this issue.
372fn rgba_speed_avoid_panic_256_colors() {
373    let side = 16;
374    let pixel_data: Vec<u8> = (0..=255).flat_map(|a| [a, a, a]).collect();
375    let _ = Frame::from_rgb(side, side, &pixel_data);
376}