1#![doc(html_root_url = "https://docs.rs/engiffen/0.8.1")]
7
8extern crate color_quant;
9extern crate fnv;
10extern crate gif;
11extern crate image;
12extern crate lab;
13extern crate rayon;
14
15use color_quant::NeuQuant;
16use fnv::FnvHashMap;
17use gif::{Encoder, Frame, Repeat};
18use image::GenericImageView;
19use lab::Lab;
20use rayon::prelude::*;
21use std::borrow::Cow;
22use std::io;
23use std::path::Path;
24use std::{error, f32, fmt};
25
26#[cfg(feature = "debug-stderr")]
27use std::time::Instant;
28
29#[cfg(feature = "debug-stderr")]
30fn ms(duration: Instant) -> u64 {
31 let duration = duration.elapsed();
32 duration.as_secs() * 1000 + duration.subsec_nanos() as u64 / 1000000
33}
34
35type Rgba = [u8; 4];
36
37#[derive(Debug, Eq, PartialEq, Copy, Clone)]
62pub enum Quantizer {
63 Naive,
64 NeuQuant(u32),
65}
66
67pub struct Image {
71 pub pixels: Vec<Rgba>,
72 pub width: u32,
73 pub height: u32,
74}
75
76impl fmt::Debug for Image {
77 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78 write!(
79 f,
80 "Image {{ dimensions: {} x {} }}",
81 self.width, self.height
82 )
83 }
84}
85
86#[derive(Debug)]
87pub enum Error {
88 NoImages,
89 Mismatch((u32, u32), (u32, u32)),
90 ImageLoad(image::ImageError),
91 ImageWrite(io::Error),
92 Encode(gif::EncodingError),
93}
94
95impl From<image::ImageError> for Error {
96 fn from(err: image::ImageError) -> Error {
97 Error::ImageLoad(err)
98 }
99}
100
101impl From<io::Error> for Error {
102 fn from(err: io::Error) -> Error {
103 Error::ImageWrite(err)
104 }
105}
106
107impl From<gif::EncodingError> for Error {
108 fn from(err: gif::EncodingError) -> Error {
109 Error::Encode(err)
110 }
111}
112
113impl fmt::Display for Error {
114 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115 match *self {
116 Error::NoImages => write!(f, "No frames sent for engiffening"),
117 Error::Mismatch(_, _) => write!(f, "Frames don't have the same dimensions"),
118 Error::ImageLoad(ref e) => write!(f, "Image load error: {}", e),
119 Error::ImageWrite(ref e) => write!(f, "Image write error: {}", e),
120 Error::Encode(ref e) => write!(f, "Gif encoding error: {}", e),
121 }
122 }
123}
124
125impl error::Error for Error {
126 fn description(&self) -> &str {
127 match *self {
128 Error::NoImages => "No frames sent for engiffening",
129 Error::Mismatch(_, _) => "Frames don't have the same dimensions",
130 Error::ImageLoad(_) => "Unable to load image",
131 Error::ImageWrite(_) => "Unable to write image",
132 Error::Encode(_) => "Unable to encode gif",
133 }
134 }
135}
136
137#[derive(Eq, PartialEq, Clone, Hash)]
139pub struct Gif {
140 pub palette: Vec<u8>,
141 pub transparency: Option<u8>,
142 pub width: u16,
143 pub height: u16,
144 pub images: Vec<Vec<u8>>,
145 pub loops: Option<u16>,
146 pub delay: u16,
147}
148
149impl fmt::Debug for Gif {
150 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151 write!(f, "Gif {{ palette: Vec<u8 x {:?}>, transparency: {:?}, width: {:?}, height: {:?}, images: Vec<Vec<u8> x {:?}>, delay: {:?}, loops: {:?} }}",
152 self.palette.len(),
153 self.transparency,
154 self.width,
155 self.height,
156 self.images.len(),
157 self.delay,
158 self.loops
159 )
160 }
161}
162
163impl Gif {
164 pub fn write<W: io::Write>(&self, mut out: &mut W) -> Result<(), Error> {
184 let mut encoder = Encoder::new(&mut out, self.width, self.height, &self.palette)?;
185 encoder.set_repeat(self.loops.map_or(Repeat::Infinite, Repeat::Finite))?;
186 for img in &self.images {
187 let frame = Frame::<'_> {
188 delay: self.delay / 10,
189 width: self.width,
190 height: self.height,
191 buffer: Cow::Borrowed(img),
192 transparent: self.transparency,
193 ..Default::default()
194 };
195 encoder.write_frame(&frame)?;
196 }
197 Ok(())
198 }
199}
200
201pub fn load_image<P>(path: P) -> Result<Image, Error>
218where
219 P: AsRef<Path>,
220{
221 let img = image::open(&path)?;
222 let mut pixels: Vec<Rgba> = Vec::with_capacity(0);
223 for (_, _, px) in img.pixels() {
224 pixels.push(px.0);
225 }
226 Ok(Image {
227 pixels,
228 width: img.width(),
229 height: img.height(),
230 })
231}
232
233pub fn load_images<P>(paths: &[P]) -> Vec<Image>
247where
248 P: AsRef<Path>,
249{
250 paths
251 .iter()
252 .map(load_image)
253 .filter_map(|img| img.ok())
254 .collect()
255}
256
257pub fn engiffen(
278 imgs: &[Image],
279 fps: usize,
280 quantizer: Quantizer,
281 loops: Option<u16>,
282) -> Result<Gif, Error> {
283 if imgs.is_empty() {
284 return Err(Error::NoImages);
285 }
286 #[cfg(feature = "debug-stderr")]
287 eprintln!("Engiffening {} images", imgs.len());
288
289 let (width, height) = {
290 let first = &imgs[0];
291 let first_dimensions = (first.width, first.height);
292 for img in imgs.iter() {
293 let other_dimensions = (img.width, img.height);
294 if first_dimensions != other_dimensions {
295 return Err(Error::Mismatch(first_dimensions, other_dimensions));
296 }
297 }
298 first_dimensions
299 };
300
301 let (palette, palettized_imgs, transparency) = match quantizer {
302 Quantizer::NeuQuant(sample_rate) => neuquant_palettize(imgs, sample_rate, width, height),
303 Quantizer::Naive => naive_palettize(imgs),
304 };
305
306 let delay = (1000 / fps) as u16;
307
308 Ok(Gif {
309 palette,
310 transparency,
311 width: width as u16,
312 height: height as u16,
313 images: palettized_imgs,
314 loops,
315 delay,
316 })
317}
318
319fn neuquant_palettize(
320 imgs: &[Image],
321 sample_rate: u32,
322 width: u32,
323 height: u32,
324) -> (Vec<u8>, Vec<Vec<u8>>, Option<u8>) {
325 let image_len = (width * height * 4 / sample_rate / sample_rate) as usize;
326 let width = width as usize;
327 let sample_rate = sample_rate as usize;
328 let transparent_black = [0u8; 4];
329 #[cfg(feature = "debug-stderr")]
330 let time_push = Instant::now();
331 let colors: Vec<u8> = imgs
332 .par_iter()
333 .map(|img| {
334 let mut temp: Vec<_> = Vec::with_capacity(image_len);
335 for (n, px) in img.pixels.iter().enumerate() {
336 if sample_rate > 1 && n % sample_rate != 0 || (n / width) % sample_rate != 0 {
337 continue;
338 }
339 if px[3] == 0 {
340 temp.extend_from_slice(&transparent_black);
341 } else {
342 temp.extend_from_slice(&px[..3]);
343 temp.push(255);
344 }
345 }
346 temp
347 })
348 .reduce(
349 || Vec::with_capacity(image_len * imgs.len()),
350 |mut acc, img| {
351 acc.extend_from_slice(&img);
352 acc
353 },
354 );
355 #[cfg(feature = "debug-stderr")]
356 eprintln!(
357 "Neuquant: Concatenated {} bytes in {} ms.",
358 colors.len(),
359 ms(time_push)
360 );
361
362 #[cfg(feature = "debug-stderr")]
363 let time_quant = Instant::now();
364 let quant = NeuQuant::new(10, 256, &colors);
365 #[cfg(feature = "debug-stderr")]
366 eprintln!("Neuquant: Computed palette in {} ms.", ms(time_quant));
367
368 #[cfg(feature = "debug-stderr")]
369 let time_map = Instant::now();
370 let mut transparency = None;
371 let mut cache: FnvHashMap<Rgba, u8> = FnvHashMap::default();
372 let palettized_imgs: Vec<Vec<u8>> = imgs
373 .iter()
374 .map(|img| {
375 img.pixels
376 .iter()
377 .map(|px| {
378 *cache.entry(*px).or_insert_with(|| {
379 let idx = quant.index_of(px) as u8;
380 if transparency.is_none() && px[3] == 0 {
381 transparency = Some(idx);
382 }
383 idx
384 })
385 })
386 .collect()
387 })
388 .collect();
389 #[cfg(feature = "debug-stderr")]
390 eprintln!("Neuquant: Mapped pixels to palette in {} ms.", ms(time_map));
391
392 (quant.color_map_rgb(), palettized_imgs, transparency)
393}
394
395fn naive_palettize(imgs: &[Image]) -> (Vec<u8>, Vec<Vec<u8>>, Option<u8>) {
396 #[cfg(feature = "debug-stderr")]
397 let time_count = Instant::now();
398 let frequencies: FnvHashMap<Rgba, usize> = imgs
399 .par_iter()
400 .map(|img| {
401 let mut fr: FnvHashMap<Rgba, usize> = FnvHashMap::default();
402 for pixel in img.pixels.iter() {
403 let num = fr.entry(*pixel).or_insert(0);
404 *num += 1;
405 }
406 fr
407 })
408 .reduce(FnvHashMap::default, |mut acc, fr| {
409 for (color, count) in fr {
410 let num = acc.entry(color).or_insert(0);
411 *num += count;
412 }
413 acc
414 });
415 #[cfg(feature = "debug-stderr")]
416 eprintln!("Naive: Counted color frequencies in {} ms", ms(time_count));
417 #[cfg(feature = "debug-stderr")]
418 let time_palette = Instant::now();
419 let mut sorted_frequencies = frequencies.into_iter().collect::<Vec<_>>();
420 sorted_frequencies.sort_by(|a, b| b.1.cmp(&a.1));
421 let sorted = sorted_frequencies
422 .into_iter()
423 .map(|c| (c.0, Lab::from_rgba(&c.0)))
424 .collect::<Vec<_>>();
425
426 let (palette, rest) = if sorted.len() > 256 {
427 (&sorted[..256], &sorted[256..])
428 } else {
429 (&sorted[..], &[] as &[_])
430 };
431
432 let mut map: FnvHashMap<Rgba, u8> = FnvHashMap::default();
433 for (i, color) in palette.iter().enumerate() {
434 map.insert(color.0, i as u8);
435 }
436 for color in rest {
437 let closest_index = palette
438 .iter()
439 .enumerate()
440 .fold((0, f32::INFINITY), |closest, (idx, p)| {
441 let dist = p.1.squared_distance(&color.1);
442 if closest.1 < dist {
443 closest
444 } else {
445 (idx, dist)
446 }
447 })
448 .0;
449 let closest_rgb = palette[closest_index].0;
450 let index = *map.get(&closest_rgb).expect(
451 "A color we assigned to the palette is somehow missing from the palette index map.",
452 );
453 map.insert(color.0, index);
454 }
455 #[cfg(feature = "debug-stderr")]
456 eprintln!("Naive: Computed palette in {} ms.", ms(time_palette));
457
458 #[cfg(feature = "debug-stderr")]
459 let time_index = Instant::now();
460 let palettized_imgs: Vec<Vec<u8>> = imgs
461 .par_iter()
462 .map(|img| {
463 img.pixels
464 .iter()
465 .map(|px| {
466 *map.get(px)
467 .expect("A color in an image was not added to the palette map.")
468 })
469 .collect()
470 })
471 .collect();
472 #[cfg(feature = "debug-stderr")]
473 eprintln!("Naive: Mapped pixels to palette in {} ms", ms(time_index));
474
475 let mut palette_as_bytes = Vec::with_capacity(palette.len() * 3);
476 for color in palette {
477 palette_as_bytes.extend_from_slice(&color.0[0..3]);
478 }
479
480 (palette_as_bytes, palettized_imgs, None)
481}
482
483#[cfg(test)]
484#[allow(unused_must_use)]
485mod tests {
486 use super::{engiffen, load_image, Error, Quantizer};
487 use std::fs::{read_dir, File};
488
489 #[test]
490 fn test_error_on_size_mismatch() {
491 let imgs: Vec<_> = read_dir("tests/mismatched_size")
492 .unwrap()
493 .map(|e| e.unwrap().path())
494 .map(|path| load_image(&path).unwrap())
495 .collect();
496
497 let res = engiffen(&imgs, 30, Quantizer::NeuQuant(1), None);
498
499 assert!(res.is_err());
500 match res {
501 Err(Error::Mismatch(one, another)) => {
502 assert_eq!((one, another), ((100, 100), (50, 50)));
503 }
504 _ => unreachable!(),
505 }
506 }
507
508 #[test]
509 #[ignore]
510 fn test_compress_palette() {
511 let imgs: Vec<_> = read_dir("tests/ball")
513 .unwrap()
514 .map(|e| e.unwrap().path())
515 .filter(|path| match path.extension() {
516 Some(ext) if ext == "bmp" => true,
517 _ => false,
518 })
519 .map(|path| load_image(&path).unwrap())
520 .collect();
521
522 let mut out = File::create("tests/ball.gif").unwrap();
523 let gif = engiffen(&imgs, 10, Quantizer::NeuQuant(2), None);
524 match gif {
525 Ok(gif) => gif.write(&mut out),
526 Err(_) => panic!("Test should have successfully made a gif."),
527 };
528 }
529
530 #[test]
531 #[ignore]
532 fn test_simple_paletted_gif() {
533 let imgs: Vec<_> = read_dir("tests/shrug")
534 .unwrap()
535 .map(|e| e.unwrap().path())
536 .filter(|path| match path.extension() {
537 Some(ext) if ext == "tga" => true,
538 _ => false,
539 })
540 .map(|path| load_image(&path).unwrap())
541 .collect();
542
543 let mut out = File::create("tests/shrug.gif").unwrap();
544 let gif = engiffen(&imgs, 30, Quantizer::NeuQuant(2), None);
545 match gif {
546 Ok(gif) => gif.write(&mut out),
547 Err(_) => panic!("Test should have successfully made a gif."),
548 };
549 }
550}