1 | use std::borrow::Cow; |
2 | #[cfg (feature = "color_quant" )] |
3 | use std::collections::{HashMap, HashSet}; |
4 | |
5 | /// Disposal method |
6 | #[derive (Debug, Copy, Clone, PartialEq, Eq)] |
7 | #[repr (u8)] |
8 | pub 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 | |
19 | impl DisposalMethod { |
20 | /// Converts `u8` to `Option<Self>` |
21 | #[must_use ] |
22 | pub fn from_u8(n: u8) -> Option<DisposalMethod> { |
23 | match n { |
24 | 0 => Some(DisposalMethod::Any), |
25 | 1 => Some(DisposalMethod::Keep), |
26 | 2 => Some(DisposalMethod::Background), |
27 | 3 => Some(DisposalMethod::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)] |
45 | pub enum Block { |
46 | /// Image block. |
47 | Image = 0x2C, |
48 | /// Extension block. |
49 | Extension = 0x21, |
50 | /// Image trailer. |
51 | Trailer = 0x3B, |
52 | } |
53 | |
54 | impl Block { |
55 | /// Converts `u8` to `Option<Self>` |
56 | #[must_use ] |
57 | pub fn from_u8(n: u8) -> Option<Block> { |
58 | match n { |
59 | 0x2C => Some(Block::Image), |
60 | 0x21 => Some(Block::Extension), |
61 | 0x3B => Some(Block::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)] |
76 | pub 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)] |
84 | pub 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 | |
105 | impl AnyExtension { |
106 | /// Decode the label as a known extension. |
107 | #[must_use ] |
108 | pub fn into_known(self) -> Option<Extension> { |
109 | Extension::from_u8(self.0) |
110 | } |
111 | } |
112 | |
113 | impl From<Extension> for AnyExtension { |
114 | fn from(ext: Extension) -> Self { |
115 | AnyExtension(ext as u8) |
116 | } |
117 | } |
118 | |
119 | impl Extension { |
120 | /// Converts `u8` to a `Extension` if it is known. |
121 | #[must_use ] |
122 | pub fn from_u8(n: u8) -> Option<Extension> { |
123 | match n { |
124 | 0x01 => Some(Extension::Text), |
125 | 0xF9 => Some(Extension::Control), |
126 | 0xFE => Some(Extension::Comment), |
127 | 0xFF => Some(Extension::Application), |
128 | _ => None, |
129 | } |
130 | } |
131 | } |
132 | |
133 | /// A GIF frame |
134 | #[derive (Debug, Clone)] |
135 | pub 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 | |
161 | impl<'a> Default for Frame<'a> { |
162 | fn default() -> Frame<'a> { |
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 | |
179 | impl 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 | pub fn from_rgba(width: u16, height: u16, pixels: &mut [u8]) -> Frame<'static> { |
193 | Frame::from_rgba_speed(width, height, pixels, 1) |
194 | } |
195 | |
196 | /// Creates a frame from pixels in RGBA format. |
197 | /// |
198 | /// `speed` is a value in the range [1, 30]. |
199 | /// The higher the value the faster it runs at the cost of image quality. |
200 | /// A `speed` of 10 is a good compromise between speed and quality. |
201 | /// |
202 | /// This is a lossy method. The `gif` format does not support arbitrary alpha but only a 1-bit |
203 | /// transparency mask per pixel. Any non-zero alpha value will be interpreted as a fully opaque |
204 | /// pixel. Additionally, only 256 colors can appear in a single frame. The palette will be |
205 | /// reduced by the NeuQuant algorithm if necessary. Different frames have independent palettes. |
206 | /// |
207 | /// # Panics: |
208 | /// * If the length of pixels does not equal `width * height * 4`. |
209 | /// * If `speed < 1` or `speed > 30` |
210 | #[cfg (feature = "color_quant" )] |
211 | pub fn from_rgba_speed(width: u16, height: u16, pixels: &mut [u8], speed: i32) -> Frame<'static> { |
212 | 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" ); |
213 | assert!(speed >= 1 && speed <= 30, "speed needs to be in the range [1, 30]" ); |
214 | let mut transparent = None; |
215 | for pix in pixels.chunks_exact_mut(4) { |
216 | if pix[3] != 0 { |
217 | pix[3] = 0xFF; |
218 | } else { |
219 | transparent = Some([pix[0], pix[1], pix[2], pix[3]]); |
220 | } |
221 | } |
222 | |
223 | // Attempt to build a palette of all colors. If we go over 256 colors, |
224 | // switch to the NeuQuant algorithm. |
225 | let mut colors: HashSet<(u8, u8, u8, u8)> = HashSet::new(); |
226 | for pixel in pixels.chunks_exact(4) { |
227 | if colors.insert((pixel[0], pixel[1], pixel[2], pixel[3])) && colors.len() > 256 { |
228 | // > 256 colours, let's use NeuQuant. |
229 | let nq = color_quant::NeuQuant::new(speed, 256, pixels); |
230 | |
231 | return Frame { |
232 | width, |
233 | height, |
234 | buffer: Cow::Owned(pixels.chunks_exact(4).map(|pix| nq.index_of(pix) as u8).collect()), |
235 | palette: Some(nq.color_map_rgb()), |
236 | transparent: transparent.map(|t| nq.index_of(&t) as u8), |
237 | ..Frame::default() |
238 | }; |
239 | } |
240 | } |
241 | |
242 | // Palette size <= 256 elements, we can build an exact palette. |
243 | let mut colors_vec: Vec<(u8, u8, u8, u8)> = colors.into_iter().collect(); |
244 | colors_vec.sort_unstable(); |
245 | let palette = colors_vec.iter().flat_map(|&(r, g, b, _a)| [r, g, b]).collect(); |
246 | let colors_lookup: HashMap<(u8, u8, u8, u8), u8> = colors_vec.into_iter().zip(0..=255).collect(); |
247 | |
248 | let index_of = | pixel: &[u8] | |
249 | colors_lookup.get(&(pixel[0], pixel[1], pixel[2], pixel[3])).copied().unwrap_or(0); |
250 | |
251 | return Frame { |
252 | width, |
253 | height, |
254 | buffer: Cow::Owned(pixels.chunks_exact(4).map(index_of).collect()), |
255 | palette: Some(palette), |
256 | transparent: transparent.map(|t| index_of(&t)), |
257 | ..Frame::default() |
258 | }; |
259 | } |
260 | |
261 | /// Creates a frame from a palette and indexed pixels. |
262 | /// |
263 | /// # Panics: |
264 | /// * If the length of pixels does not equal `width * height`. |
265 | /// * If the length of palette > `256 * 3`. |
266 | pub fn from_palette_pixels(width: u16, height: u16, pixels: impl Into<Vec<u8>>, palette: impl Into<Vec<u8>>, transparent: Option<u8>) -> Frame<'static> { |
267 | let pixels = pixels.into(); |
268 | let palette = palette.into(); |
269 | 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" ); |
270 | assert!(palette.len() <= 256*3, "Too many palette values to create a GIF Frame" ); |
271 | |
272 | Frame { |
273 | width, |
274 | height, |
275 | buffer: Cow::Owned(pixels), |
276 | palette: Some(palette), |
277 | transparent, |
278 | ..Frame::default() |
279 | } |
280 | } |
281 | |
282 | /// Creates a frame from indexed pixels in the global palette. |
283 | /// |
284 | /// # Panics: |
285 | /// * If the length of pixels does not equal `width * height`. |
286 | pub fn from_indexed_pixels(width: u16, height: u16, pixels: impl Into<Vec<u8>>, transparent: Option<u8>) -> Frame<'static> { |
287 | let pixels = pixels.into(); |
288 | 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" ); |
289 | |
290 | Frame { |
291 | width, |
292 | height, |
293 | buffer: Cow::Owned(pixels.clone()), |
294 | palette: None, |
295 | transparent, |
296 | ..Frame::default() |
297 | } |
298 | } |
299 | |
300 | /// Creates a frame from pixels in RGB format. |
301 | /// |
302 | /// This is a lossy method. In the `gif` format only 256 colors can appear in a single frame. |
303 | /// The palette will be reduced by the NeuQuant algorithm if necessary. Different frames have |
304 | /// independent palettes. |
305 | /// |
306 | /// *Note: This method is not optimized for speed.* |
307 | /// |
308 | /// # Panics: |
309 | /// * If the length of pixels does not equal `width * height * 3`. |
310 | #[cfg (feature = "color_quant" )] |
311 | #[must_use ] |
312 | pub fn from_rgb(width: u16, height: u16, pixels: &[u8]) -> Frame<'static> { |
313 | Frame::from_rgb_speed(width, height, pixels, 1) |
314 | } |
315 | |
316 | /// Creates a frame from pixels in RGB format. |
317 | /// |
318 | /// `speed` is a value in the range [1, 30]. |
319 | /// |
320 | /// This is a lossy method. In the `gif` format only 256 colors can appear in a single frame. |
321 | /// The palette will be reduced by the NeuQuant algorithm if necessary. Different frames have |
322 | /// independent palettes. |
323 | /// |
324 | /// The higher the value the faster it runs at the cost of image quality. |
325 | /// A `speed` of 10 is a good compromise between speed and quality. |
326 | /// |
327 | /// # Panics: |
328 | /// * If the length of pixels does not equal `width * height * 3`. |
329 | /// * If `speed < 1` or `speed > 30` |
330 | #[cfg (feature = "color_quant" )] |
331 | #[must_use ] |
332 | pub fn from_rgb_speed(width: u16, height: u16, pixels: &[u8], speed: i32) -> Frame<'static> { |
333 | 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" ); |
334 | let mut vec: Vec<u8> = Vec::new(); |
335 | vec.try_reserve_exact(pixels.len() + width as usize * height as usize).expect("OOM" ); |
336 | for v in pixels.chunks_exact(3) { |
337 | vec.extend_from_slice(&[v[0], v[1], v[2], 0xFF]); |
338 | } |
339 | Frame::from_rgba_speed(width, height, &mut vec, speed) |
340 | } |
341 | |
342 | /// Leaves empty buffer and empty palette behind |
343 | #[inline ] |
344 | pub(crate) fn take(&mut self) -> Self { |
345 | Frame { |
346 | delay: self.delay, |
347 | dispose: self.dispose, |
348 | transparent: self.transparent, |
349 | needs_user_input: self.needs_user_input, |
350 | top: self.top, |
351 | left: self.left, |
352 | width: self.width, |
353 | height: self.height, |
354 | interlaced: self.interlaced, |
355 | palette: std::mem::take(&mut self.palette), |
356 | buffer: std::mem::replace(&mut self.buffer, Cow::Borrowed(&[])), |
357 | } |
358 | } |
359 | } |
360 | |
361 | #[test ] |
362 | #[cfg (feature = "color_quant" )] |
363 | // Creating the `colors_lookup` hashmap in Frame::from_rgba_speed panics due to |
364 | // overflow while bypassing NeuQuant and zipping a RangeFrom with 256 colors. |
365 | // Changing .zip(0_u8..) to .zip(0_u8..=255) fixes this issue. |
366 | fn rgba_speed_avoid_panic_256_colors() { |
367 | let side: u16 = 16; |
368 | let pixel_data: Vec<u8> = (0..=255).map(|a: u8| vec![a, a, a]).flatten().collect(); |
369 | let _ = Frame::from_rgb(width:side, height:side, &pixel_data); |
370 | } |
371 | |