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}