Skip to main content

image_webp/
encoder.rs

1//! Encoding of WebP images.
2use std::io::{self, Write};
3
4use quick_error::quick_error;
5
6use crate::{
7    lossless::{encode_frame_lossless, EncoderParams},
8    lossy::encoder::encode_frame_lossy,
9};
10
11/// Color type of the image.
12///
13/// Note that the WebP format doesn't have a concept of color type. All images are encoded as RGBA
14/// and some decoders may treat them as such. This enum is used to indicate the color type of the
15/// input data provided to the encoder, which can help improve compression ratio.
16#[derive(Copy, Clone, Debug, PartialEq, Eq)]
17pub enum ColorType {
18    /// Opaque image with a single luminance byte per pixel.
19    L8,
20    /// Image with a luminance and alpha byte per pixel.
21    La8,
22    /// Opaque image with a red, green, and blue byte per pixel.
23    Rgb8,
24    /// Image with a red, green, blue, and alpha byte per pixel.
25    Rgba8,
26}
27
28impl ColorType {
29    fn has_alpha(self) -> bool {
30        self == ColorType::La8 || self == ColorType::Rgba8
31    }
32}
33
34quick_error! {
35    /// Error that can occur during encoding.
36    #[derive(Debug)]
37    #[non_exhaustive]
38    pub enum EncodingError {
39        /// An IO error occurred.
40        IoError(err: io::Error) {
41            from()
42            display("IO error: {}", err)
43            source(err)
44        }
45
46        /// The image dimensions are not allowed by the WebP format.
47        InvalidDimensions {
48            display("Invalid dimensions")
49        }
50    }
51}
52
53/// Encodes the alpha part of the image data losslessly.
54/// Used for lossy images that include transparency.
55///
56/// # Panics
57///
58/// Panics if the image data is not of the indicated dimensions.
59fn encode_alpha_lossless<W: Write>(
60    mut writer: W,
61    data: &[u8],
62    width: u32,
63    height: u32,
64    color: ColorType,
65) -> Result<(), EncodingError> {
66    let bytes_per_pixel = match color {
67        ColorType::La8 => 2,
68        ColorType::Rgba8 => 4,
69        _ => unreachable!(),
70    };
71    if width == 0 || width > 16384 || height == 0 || height > 16384 {
72        return Err(EncodingError::InvalidDimensions);
73    }
74
75    let preprocessing = 0u8;
76    let filtering_method = 0u8;
77    // 0 is raw alpha data
78    // 1 is using the lossless format to encode alpha data
79    let compression_method = 1u8;
80
81    let initial_byte = preprocessing << 4 | filtering_method << 2 | compression_method;
82
83    writer.write_all(&[initial_byte])?;
84
85    // uncompressed raw alpha data
86    let alpha_data: Vec<u8> = data
87        .iter()
88        .skip(bytes_per_pixel - 1)
89        .step_by(bytes_per_pixel)
90        .copied()
91        .collect();
92
93    debug_assert_eq!(alpha_data.len(), (width * height) as usize);
94
95    encode_frame_lossless(
96        writer,
97        &alpha_data,
98        width,
99        height,
100        ColorType::L8,
101        EncoderParams::default(),
102        true,
103    )?;
104
105    Ok(())
106}
107
108const fn chunk_size(inner_bytes: usize) -> u32 {
109    if inner_bytes % 2 == 1 {
110        (inner_bytes + 1) as u32 + 8
111    } else {
112        inner_bytes as u32 + 8
113    }
114}
115
116fn write_chunk<W: Write>(mut w: W, name: &[u8], data: &[u8]) -> io::Result<()> {
117    debug_assert!(name.len() == 4);
118
119    w.write_all(name)?;
120    w.write_all(&(data.len() as u32).to_le_bytes())?;
121    w.write_all(data)?;
122    if data.len() % 2 == 1 {
123        w.write_all(&[0])?;
124    }
125    Ok(())
126}
127
128/// WebP Encoder.
129pub struct WebPEncoder<W> {
130    writer: W,
131    icc_profile: Vec<u8>,
132    exif_metadata: Vec<u8>,
133    xmp_metadata: Vec<u8>,
134    params: EncoderParams,
135}
136
137impl<W: Write> WebPEncoder<W> {
138    /// Create a new encoder that writes its output to `w`.
139    ///
140    /// Defaults to "VP8L" lossless encoding. Set
141    /// [`EncoderParams::use_lossy`] (with [`EncoderParams::lossy_quality`])
142    /// via [`WebPEncoder::set_params`] to emit a lossy "VP8 " bitstream
143    /// instead.
144    pub fn new(w: W) -> Self {
145        Self {
146            writer: w,
147            icc_profile: Vec::new(),
148            exif_metadata: Vec::new(),
149            xmp_metadata: Vec::new(),
150            params: EncoderParams::default(),
151        }
152    }
153
154    /// Set the ICC profile to use for the image.
155    pub fn set_icc_profile(&mut self, icc_profile: Vec<u8>) {
156        self.icc_profile = icc_profile;
157    }
158
159    /// Set the EXIF metadata to use for the image.
160    pub fn set_exif_metadata(&mut self, exif_metadata: Vec<u8>) {
161        self.exif_metadata = exif_metadata;
162    }
163
164    /// Set the XMP metadata to use for the image.
165    pub fn set_xmp_metadata(&mut self, xmp_metadata: Vec<u8>) {
166        self.xmp_metadata = xmp_metadata;
167    }
168
169    /// Set the `EncoderParams` to use.
170    pub fn set_params(&mut self, params: EncoderParams) {
171        self.params = params;
172    }
173
174    /// Encode image data with the indicated color type.
175    ///
176    /// # Panics
177    ///
178    /// Panics if the image data is not of the indicated dimensions.
179    pub fn encode(
180        mut self,
181        data: &[u8],
182        width: u32,
183        height: u32,
184        color: ColorType,
185    ) -> Result<(), EncodingError> {
186        let mut frame = Vec::new();
187
188        let lossy_with_alpha = self.params.use_lossy && color.has_alpha();
189
190        let frame_chunk = if self.params.use_lossy {
191            encode_frame_lossy(
192                &mut frame,
193                data,
194                width,
195                height,
196                color,
197                self.params.lossy_quality,
198            )?;
199            b"VP8 "
200        } else {
201            encode_frame_lossless(&mut frame, data, width, height, color, self.params, false)?;
202            b"VP8L"
203        };
204
205        // If the image has no metadata and isn't lossy with alpha,
206        // it can be encoded with the "simple" WebP container format.
207        let use_simple_container = self.icc_profile.is_empty()
208            && self.exif_metadata.is_empty()
209            && self.xmp_metadata.is_empty()
210            && !lossy_with_alpha;
211
212        if use_simple_container {
213            self.writer.write_all(b"RIFF")?;
214            self.writer
215                .write_all(&(chunk_size(frame.len()) + 4).to_le_bytes())?;
216            self.writer.write_all(b"WEBP")?;
217            write_chunk(&mut self.writer, frame_chunk, &frame)?;
218        } else {
219            let mut total_bytes = 22 + chunk_size(frame.len());
220            if !self.icc_profile.is_empty() {
221                total_bytes += chunk_size(self.icc_profile.len());
222            }
223            if !self.exif_metadata.is_empty() {
224                total_bytes += chunk_size(self.exif_metadata.len());
225            }
226            if !self.xmp_metadata.is_empty() {
227                total_bytes += chunk_size(self.xmp_metadata.len());
228            }
229
230            let alpha_chunk_data = if lossy_with_alpha {
231                let mut alpha_chunk = Vec::new();
232                encode_alpha_lossless(&mut alpha_chunk, data, width, height, color)?;
233
234                total_bytes += chunk_size(alpha_chunk.len());
235                Some(alpha_chunk)
236            } else {
237                None
238            };
239
240            let mut flags = 0;
241            if !self.xmp_metadata.is_empty() {
242                flags |= 1 << 2;
243            }
244            if !self.exif_metadata.is_empty() {
245                flags |= 1 << 3;
246            }
247            if color.has_alpha() {
248                flags |= 1 << 4;
249            }
250            if !self.icc_profile.is_empty() {
251                flags |= 1 << 5;
252            }
253
254            self.writer.write_all(b"RIFF")?;
255            self.writer.write_all(&total_bytes.to_le_bytes())?;
256            self.writer.write_all(b"WEBP")?;
257
258            let mut vp8x = Vec::new();
259            vp8x.write_all(&[flags])?; // flags
260            vp8x.write_all(&[0; 3])?; // reserved
261            vp8x.write_all(&(width - 1).to_le_bytes()[..3])?; // canvas width
262            vp8x.write_all(&(height - 1).to_le_bytes()[..3])?; // canvas height
263            write_chunk(&mut self.writer, b"VP8X", &vp8x)?;
264
265            if !self.icc_profile.is_empty() {
266                write_chunk(&mut self.writer, b"ICCP", &self.icc_profile)?;
267            }
268
269            if let Some(alpha_chunk) = alpha_chunk_data {
270                write_chunk(&mut self.writer, b"ALPH", &alpha_chunk)?;
271            }
272
273            write_chunk(&mut self.writer, frame_chunk, &frame)?;
274
275            if !self.exif_metadata.is_empty() {
276                write_chunk(&mut self.writer, b"EXIF", &self.exif_metadata)?;
277            }
278
279            if !self.xmp_metadata.is_empty() {
280                write_chunk(&mut self.writer, b"XMP ", &self.xmp_metadata)?;
281            }
282        }
283
284        Ok(())
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use rand::RngCore;
291
292    use super::*;
293
294    #[test]
295    fn write_webp() {
296        let mut img = vec![0; 256 * 256 * 4];
297        rand::thread_rng().fill_bytes(&mut img);
298
299        let mut output = Vec::new();
300        WebPEncoder::new(&mut output)
301            .encode(&img, 256, 256, crate::ColorType::Rgba8)
302            .unwrap();
303
304        let mut decoder = crate::WebPDecoder::new(std::io::Cursor::new(output)).unwrap();
305        let mut img2 = vec![0; 256 * 256 * 4];
306        decoder.read_image(&mut img2).unwrap();
307        assert_eq!(img, img2);
308    }
309
310    #[test]
311    fn write_webp_exif() {
312        let mut img = vec![0; 256 * 256 * 3];
313        rand::thread_rng().fill_bytes(&mut img);
314
315        let mut exif = vec![0; 10];
316        rand::thread_rng().fill_bytes(&mut exif);
317
318        let mut output = Vec::new();
319        let mut encoder = WebPEncoder::new(&mut output);
320        encoder.set_exif_metadata(exif.clone());
321        encoder
322            .encode(&img, 256, 256, crate::ColorType::Rgb8)
323            .unwrap();
324
325        let mut decoder = crate::WebPDecoder::new(std::io::Cursor::new(output)).unwrap();
326
327        let mut img2 = vec![0; 256 * 256 * 3];
328        decoder.read_image(&mut img2).unwrap();
329        assert_eq!(img, img2);
330
331        let exif2 = decoder.exif_metadata().unwrap();
332        assert_eq!(Some(exif), exif2);
333    }
334
335    /// Exercises the VP8 lossy encoder end to end - including adaptive
336    /// quantisation (segmentation) - decoded by libwebp, the same
337    /// correctness gate `examples/rd_eval.rs` and `examples/ssimu2_eval.rs`
338    /// rely on. Nothing else in `cargo test` reaches the lossy path at all
339    /// (`EncoderParams::default()` has `use_lossy: false`), so without this
340    /// a segment/quantiser inconsistency that still produces a
341    /// self-consistent bitstream - decodable, just to the wrong pixels -
342    /// would only ever show up as an unexplained quality regression in a
343    /// harness nobody runs by default.
344    ///
345    /// The source image deliberately mixes a smooth gradient with a
346    /// checkerboard so macroblocks land across more than one activity
347    /// quartile, which is what actually exercises `classify_segments`
348    /// picking more than one segment.
349    #[test]
350    fn roundtrip_libwebp_lossy_segmented() {
351        let (w, h): (u32, u32) = (200, 150);
352        let mut img = vec![0u8; (w * h * 3) as usize];
353        for y in 0..h {
354            for x in 0..w {
355                let i = ((y * w + x) * 3) as usize;
356                let (r, g, b) = if x < w / 2 {
357                    // Smooth gradient: low local activity.
358                    let v = ((x * 255) / w.max(1)) as u8;
359                    (v, v.wrapping_add((y % 255) as u8), 128)
360                } else {
361                    // Checkerboard: high local activity.
362                    let v = if (x / 4 + y / 4) % 2 == 0 { 20 } else { 235 };
363                    (v, v, v)
364                };
365                img[i] = r;
366                img[i + 1] = g;
367                img[i + 2] = b;
368            }
369        }
370
371        for lossy_quality in [1u8, 40, 75, 95, 100] {
372            let mut output = Vec::new();
373            let mut encoder = WebPEncoder::new(&mut output);
374            encoder.set_params(EncoderParams {
375                use_lossy: true,
376                lossy_quality,
377                ..Default::default()
378            });
379            encoder
380                .encode(&img, w, h, crate::ColorType::Rgb8)
381                .unwrap_or_else(|e| panic!("encode failed at lossy_quality={lossy_quality}: {e}"));
382
383            let decoded = webp::Decoder::new(&output).decode().unwrap_or_else(|| {
384                panic!("libwebp failed to decode our bitstream at lossy_quality={lossy_quality}")
385            });
386            assert_eq!(decoded.width(), w);
387            assert_eq!(decoded.height(), h);
388        }
389    }
390
391    #[test]
392    fn roundtrip_libwebp() {
393        roundtrip_libwebp_params(EncoderParams::default());
394        roundtrip_libwebp_params(EncoderParams {
395            use_predictor_transform: false,
396            ..Default::default()
397        });
398    }
399
400    fn roundtrip_libwebp_params(params: EncoderParams) {
401        println!("Testing {params:?}");
402
403        let mut img = vec![0; 256 * 256 * 4];
404        rand::thread_rng().fill_bytes(&mut img);
405
406        let mut output = Vec::new();
407        let mut encoder = WebPEncoder::new(&mut output);
408        encoder.set_params(params.clone());
409        encoder
410            .encode(&img[..256 * 256 * 3], 256, 256, crate::ColorType::Rgb8)
411            .unwrap();
412        let decoded = webp::Decoder::new(&output).decode().unwrap();
413        assert_eq!(img[..256 * 256 * 3], *decoded);
414
415        let mut output = Vec::new();
416        let mut encoder = WebPEncoder::new(&mut output);
417        encoder.set_params(params.clone());
418        encoder
419            .encode(&img, 256, 256, crate::ColorType::Rgba8)
420            .unwrap();
421        let decoded = webp::Decoder::new(&output).decode().unwrap();
422        assert_eq!(img, *decoded);
423
424        let mut output = Vec::new();
425        let mut encoder = WebPEncoder::new(&mut output);
426        encoder.set_params(params.clone());
427        encoder.set_icc_profile(vec![0; 10]);
428        encoder
429            .encode(&img, 256, 256, crate::ColorType::Rgba8)
430            .unwrap();
431        let decoded = webp::Decoder::new(&output).decode().unwrap();
432        assert_eq!(img, *decoded);
433
434        let mut output = Vec::new();
435        let mut encoder = WebPEncoder::new(&mut output);
436        encoder.set_params(params.clone());
437        encoder.set_exif_metadata(vec![0; 10]);
438        encoder
439            .encode(&img, 256, 256, crate::ColorType::Rgba8)
440            .unwrap();
441        let decoded = webp::Decoder::new(&output).decode().unwrap();
442        assert_eq!(img, *decoded);
443
444        let mut output = Vec::new();
445        let mut encoder = WebPEncoder::new(&mut output);
446        encoder.set_params(params);
447        encoder.set_xmp_metadata(vec![0; 7]);
448        encoder.set_icc_profile(vec![0; 8]);
449        encoder.set_icc_profile(vec![0; 9]);
450        encoder
451            .encode(&img, 256, 256, crate::ColorType::Rgba8)
452            .unwrap();
453        let decoded = webp::Decoder::new(&output).decode().unwrap();
454        assert_eq!(img, *decoded);
455    }
456}