engiffen/
lib.rs

1//! Engiffen is a library to convert sequences of images into animated Gifs.
2//!
3//! This library is a wrapper around the image and gif crates to convert
4//! a sequence of images into an animated Gif.
5
6#![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/// A color quantizing strategy.
38///
39/// `Naive` calculates color frequencies, picks the 256 most frequent colors
40/// to be the palette, then reassigns the less frequently occuring colors to
41/// the closest matching palette color.
42///
43/// `NeuQuant` uses the NeuQuant algorithm from the `color_quant` crate. It
44/// trains a neural network using a pseudorandom subset of pixels, then
45/// assigns each pixel its closest matching color in the palette.
46///
47/// # Usage
48///
49/// Pass this as the last argument to `engiffen` to select the quantizing
50/// strategy.
51///
52/// The `NeuQuant` strategy produces the best looking images. Its interior
53/// u32 value reduces the number of pixels that the algorithm uses to train,
54/// which can greatly reduce its workload. Specifically, for a value of N,
55/// only the pixels on every Nth column of every Nth row are considered, so
56/// a value of 1 trains using every pixel, while a value of 2 trains using
57/// 1/4 of all pixels.
58///
59/// The `Naive` strategy is fastest when you know that your input images
60/// have a limited color range, but will produce terrible banding otherwise.
61#[derive(Debug, Eq, PartialEq, Copy, Clone)]
62pub enum Quantizer {
63    Naive,
64    NeuQuant(u32),
65}
66
67/// An image, currently a wrapper around `image::DynamicImage`. If loaded from
68/// disk through the `load_image` or `load_images` functions, its path property
69/// contains the path used to read it from disk.
70pub 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/// Struct representing an animated Gif
138#[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    /// Writes the animated Gif to any output that implements Write.
165    ///
166    /// # Examples
167    ///
168    /// ```rust,no_run
169    /// use std::fs::File;
170    /// # use engiffen::{Image, engiffen, Quantizer};
171    /// # fn foo() -> Result<(), engiffen::Error> {
172    /// # let images: Vec<Image> = vec![];
173    /// let mut output = File::create("output.gif")?;
174    /// let gif = engiffen(&images, 10, Quantizer::NeuQuant(2))?;
175    /// gif.write(&mut output)?;
176    /// # Ok(())
177    /// # }
178    /// ```
179    ///
180    /// # Errors
181    ///
182    /// Returns the `std::io::Result` of the underlying `write` function calls.
183    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
201/// Loads an image from the given file path.
202///
203/// # Examples
204///
205/// ```rust,no_run
206/// # use engiffen::{load_image, Image, Error};
207/// # use std::path::PathBuf;
208/// # fn foo() -> Result<Image, Error> {
209/// let image = load_image("test/ball/ball01.bmp")?;
210/// # Ok(image)
211/// # }
212/// ```
213///
214/// # Errors
215///
216/// Returns an error if the path can't be read or if the image can't be decoded
217pub 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
233/// Loads images from a list of given paths. Errors encountered while loading files
234/// are skipped.
235///
236/// # Examples
237///
238/// ```rust,no_run
239/// # use engiffen::load_images;
240/// let paths = vec!["tests/ball/ball06.bmp", "tests/ball/ball07.bmp", "tests/ball/ball08.bmp"];
241/// let images = load_images(&paths);
242/// assert_eq!(images.len(), 2); // The last path doesn't exist. It was silently skipped.
243/// ```
244///
245/// Skips images that fail to load. If all images fail, returns an empty vector.
246pub 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
257/// Converts a sequence of images into a `Gif` at a given frame rate. The `quantizer`
258/// parameter selects the algorithm that quantizes the palette into 256-colors.
259///
260/// # Examples
261///
262/// ```rust,no_run
263/// # use engiffen::{load_images, engiffen, Gif, Error, Quantizer};
264/// # fn foo() -> Result<Gif, Error> {
265/// let paths = vec!["tests/ball/ball01.bmp", "tests/ball/ball02.bmp", "tests/ball/ball03.bmp"];
266/// let images = load_images(&paths);
267/// let gif = engiffen(&images, 10, Quantizer::NeuQuant(2), None)?;
268/// assert_eq!(gif.images.len(), 3);
269/// # Ok(gif)
270/// # }
271/// ```
272///
273/// # Errors
274///
275/// If any image dimensions differ, this function will return an Error::Mismatch
276/// containing tuples of the conflicting image dimensions.
277pub 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        // This takes a while to run when not in --release
512        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}