webp/
lib.rs

1//! This crate provides provides functionality for encoding and decoding images from or into the webp format.
2//! It is implemented as a safe wrapper around the libwebp-sys crate.
3//! Currently only a subset of the features supported by libwebp are available.
4//! The simple encoding and decoding apis are implemented which use the default configuration of libwebp.
5
6mod animation_encoder;
7#[doc(inline)]
8pub use animation_encoder::*;
9
10mod decoder;
11#[doc(inline)]
12pub use decoder::*;
13
14mod animation_decoder;
15#[doc(inline)]
16pub use animation_decoder::*;
17
18mod encoder;
19#[doc(inline)]
20pub use encoder::*;
21pub use libwebp_sys::WebPEncodingError;
22
23mod shared;
24#[doc(inline)]
25pub use shared::*;
26
27pub use libwebp_sys::WebPConfig;
28
29#[cfg(test)]
30mod tests {
31    use std::ops::Deref;
32
33    use image::*;
34
35    use crate::*;
36
37    fn hsv_to_rgb(h: f64, s: f64, v: f64) -> [u8; 3] {
38        let h = (h - h.floor()) * 6.0;
39        let f = h - h.floor();
40        let u = (v * 255.0) as u8;
41        let p = (v * (1.0 - s) * 255.0) as u8;
42        let q = (v * (1.0 - s * f) * 255.0) as u8;
43        let t = (v * (1.0 - s * (1.0 - f)) * 255.0) as u8;
44
45        match h as u8 {
46            0 => [u, t, p],
47            1 => [q, u, p],
48            2 => [p, u, t],
49            3 => [p, q, u],
50            4 => [t, p, u],
51            5 => [u, p, q],
52            _ => unreachable!("h must be between 0.0 and 1.0"),
53        }
54    }
55
56    fn generate_color_wheel(width: u32, height: u32, background_alpha: bool) -> DynamicImage {
57        let f = |x, y| {
58            let width = width as f64;
59            let height = height as f64;
60
61            let x = x as f64 - width / 2.0;
62            let y = y as f64 - height / 2.0;
63
64            let theta = y.atan2(x);
65            let tau = 2.0 * std::f64::consts::PI;
66            let h = (theta + std::f64::consts::PI) / tau;
67            let s = (4.0 * (x * x + y * y) / width / height).sqrt();
68            let v = 1.0;
69
70            if s > 1.0 {
71                Rgba([0, 0, 0, if background_alpha { 0 } else { 255 }])
72            } else {
73                let [r, g, b] = hsv_to_rgb(h, s, v);
74                Rgba([r, g, b, 255])
75            }
76        };
77
78        DynamicImage::ImageRgba8(ImageBuffer::from_fn(width, height, f))
79    }
80
81    const SIZE: u32 = 96;
82
83    #[test]
84    fn encode_decode() {
85        let test_image_no_alpha = generate_color_wheel(SIZE, SIZE, false);
86        let encoded = Encoder::from_image(&test_image_no_alpha)
87            .unwrap()
88            .encode_lossless();
89
90        let decoded = Decoder::new(encoded.deref())
91            .decode()
92            .unwrap()
93            .to_image()
94            .to_rgb8();
95        assert_eq!(test_image_no_alpha.to_rgb8().deref(), decoded.deref());
96
97        let test_image_alpha = generate_color_wheel(SIZE, SIZE, true);
98        let encoded = Encoder::from_image(&test_image_alpha)
99            .unwrap()
100            .encode_lossless();
101
102        let decoded = Decoder::new(encoded.deref())
103            .decode()
104            .unwrap()
105            .to_image()
106            .to_rgba8();
107
108        // To achieve better compression, the webp library changes the rgb values in transparent regions
109        // This means we have to exclusively compare the opaque regions
110        // See the note for WebPEncodeLossless* at https://developers.google.com/speed/webp/docs/api#simple_encoding_api
111        fn compare(p1: &Rgba<u8>, p2: &Rgba<u8>) -> bool {
112            // two pixels are equal if they are fully transparent
113            if p1.channels()[3] == 0 && p2.channels()[3] == 0 {
114                true
115            } else {
116                // or if they otherwise equal
117                p1 == p2
118            }
119        }
120
121        for (p1, p2) in test_image_alpha.to_rgba8().pixels().zip(decoded.pixels()) {
122            assert!(compare(p1, p2))
123        }
124    }
125
126    #[test]
127    fn get_info() {
128        let test_image_no_alpha = generate_color_wheel(SIZE, SIZE, false);
129        let encoded = Encoder::from_image(&test_image_no_alpha)
130            .unwrap()
131            .encode_lossless();
132
133        let features = BitstreamFeatures::new(encoded.deref()).unwrap();
134        assert_eq!(features.width(), SIZE);
135        assert_eq!(features.height(), SIZE);
136        assert!(!features.has_alpha());
137        assert!(!features.has_animation());
138
139        let test_image_alpha = generate_color_wheel(SIZE, SIZE, true);
140        let encoded = Encoder::from_image(&test_image_alpha)
141            .unwrap()
142            .encode_lossless();
143
144        let features = BitstreamFeatures::new(encoded.deref()).unwrap();
145        assert_eq!(features.width(), SIZE);
146        assert_eq!(features.height(), SIZE);
147        assert!(features.has_alpha());
148        assert!(!features.has_animation());
149    }
150
151    #[test]
152    fn anim_encode_decode() {
153        let width = 32u32;
154        let height = 32u32;
155        let mut encode_images = vec![];
156        let mut config = WebPConfig::new().unwrap();
157        config.lossless = 1;
158        config.alpha_compression = 0;
159        config.alpha_filtering = 0;
160        config.quality = 75f32;
161
162        let mut encoder = AnimEncoder::new(width, height, &config);
163        encoder.set_bgcolor([255, 0, 0, 255]);
164        encoder.set_loop_count(3);
165
166        let v = generate_color_wheel(width, height, true);
167        encode_images.push(v);
168
169        let v = generate_color_wheel(width, height, true);
170        encode_images.push(v);
171
172        let v = generate_color_wheel(width, height, true);
173        encode_images.push(v);
174
175        let v = generate_color_wheel(width, height, true);
176        encode_images.push(v);
177
178        let mut t = 1000;
179        for v in encode_images.iter() {
180            encoder.add_frame(AnimFrame::from_image(v, t).unwrap());
181            t += 250;
182        }
183        let webp = encoder.encode();
184        let mut decode_images: Vec<DynamicImage> = vec![];
185        match AnimDecoder::new(&webp).decode() {
186            Ok(frames) => {
187                decode_images.extend((&frames).into_iter().map(|a| (&a).into()));
188            }
189            Err(mes) => {
190                println!("{}", mes);
191            }
192        }
193        let mut encode_rgba = vec![];
194        for v in encode_images.into_iter() {
195            let value = DynamicImage::ImageRgba8(v.to_rgba8());
196            encode_rgba.push(value);
197        }
198        fn compare(p1: &Rgba<u8>, p2: &Rgba<u8>) -> bool {
199            // two pixels are equal if they are fully transparent
200            if p1.channels()[3] == 0 && p2.channels()[3] == 0 {
201                true
202            } else {
203                // or if they otherwise equal
204                p1 == p2
205            }
206        }
207        for (i1, i2) in encode_rgba.iter().zip(decode_images.iter()) {
208            for (p1, p2) in i1.to_rgba8().pixels().zip(i2.to_rgba8().pixels()) {
209                assert!(compare(p1, p2))
210            }
211        }
212    }
213}