| 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 = 16; |
| 368 | let pixel_data: Vec<u8> = (0..=255).map(|a| vec![a, a, a]).flatten().collect(); |
| 369 | let _ = Frame::from_rgb(side, side, &pixel_data); |
| 370 | } |
| 371 | |