Skip to main content

otf_pixels_codec_webp/
decoder.rs

1//! The WebP decoder.
2//!
3//! The container is parsed here (`riff`); the image decodes through the owned
4//! VP8L (`vp8l`) or VP8 (`vp8`) decoder, with `ALPH` alpha (`alpha`) and
5//! libwebp's YUV-to-RGB conversion (`yuv`). An animation decodes to its first
6//! frame, placed on its canvas as libwebp's animation decoder does.
7
8use otf_pixels_core::{
9    Animation, Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation,
10    PixelFormat, PixelsError, Result, Source,
11};
12
13/// The most compressed bytes read before a file is called hostile.
14///
15/// The whole file is held in memory, and nothing about the image bounds how
16/// much chunk data may follow a small header. `max_pixels` bounds the
17/// output; this bounds the input.
18const MAX_COMPRESSED: usize = 256 * 1024 * 1024;
19
20/// Decodes a WebP stream.
21#[derive(Debug)]
22pub struct WebPDecoder {
23    descriptor: ImageDescriptor,
24    /// The whole file, until the first row is asked for.
25    bytes: Vec<u8>,
26    /// The decoded image, interleaved, once the first row is asked for.
27    pixels: Option<Vec<u8>>,
28    /// Rows already served.
29    row: u32,
30    /// From the `EXIF` chunk, if there is one.
31    orientation: Orientation,
32    /// The `EXIF` chunk's block.
33    exif: Option<Vec<u8>>,
34    /// The `ICCP` chunk, if there is one.
35    icc: Option<Vec<u8>>,
36    /// The animation's frames and timing, for an animated file.
37    animation: Option<Animation>,
38}
39
40impl WebPDecoder {
41    /// Read the file and parse its container; the pixels decode when the
42    /// first row is read.
43    ///
44    /// The whole file is read here: chunks may come in any order the
45    /// container allows, so the size, alpha, orientation and ICC profile are
46    /// only known once every chunk has been seen. Decoding the bitstream is
47    /// what costs, and it waits, so opening a WebP to ask its size or
48    /// metadata is a header parse. The image then decodes in one piece:
49    /// neither bitstream yields finished rows from a prefix of the file
50    /// without the decoder holding its whole working state.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`PixelsError::Malformed`] for a container or bitstream
55    /// header it rejects, [`PixelsError::Unsupported`] for a WebP feature it
56    /// does not implement, or [`PixelsError::LimitExceeded`] if the image
57    /// exceeds `limits`. A bitstream that is damaged past its header is
58    /// reported by the first [`Decoder::read_row`].
59    pub fn new<S: Source>(mut source: S, limits: Limits) -> Result<Self> {
60        let mut bytes = Vec::new();
61        let mut chunk = [0_u8; 64 * 1024];
62        loop {
63            if bytes.len() > MAX_COMPRESSED {
64                return Err(PixelsError::malformed(
65                    "webp",
66                    format!("stream exceeds {MAX_COMPRESSED} bytes"),
67                ));
68            }
69            match source.read(&mut chunk)? {
70                0 => break,
71                read => {
72                    let Some(filled) = chunk.get(..read) else {
73                        break;
74                    };
75                    bytes.extend_from_slice(filled);
76                }
77            }
78        }
79
80        let container = crate::riff::parse(&bytes)?;
81        // An unreadable EXIF block is metadata lost, not an image refused.
82        let orientation = container
83            .exif
84            .and_then(Orientation::from_exif_block)
85            .unwrap_or_default();
86        let pixel = if container.has_alpha {
87            PixelFormat::Rgba8
88        } else {
89            PixelFormat::Rgb8
90        };
91        // Enforced before any pixel buffer exists (SPEC §Safety).
92        let descriptor =
93            ImageDescriptor::with_limits(container.width, container.height, pixel, &limits)?;
94        let icc = container.icc.map(<[u8]>::to_vec);
95        let exif = container
96            .exif
97            .map(|block| block.strip_prefix(b"Exif\0\0").unwrap_or(block).to_vec());
98        let animation = Animation::new(container.frame_durations_ms.clone(), container.loop_count);
99        Ok(Self {
100            descriptor,
101            bytes,
102            pixels: None,
103            row: 0,
104            orientation,
105            exif,
106            icc,
107            animation,
108        })
109    }
110
111    /// Decode the image onto its canvas, in the output pixel format.
112    fn decode_canvas(&self) -> Result<Vec<u8>> {
113        let container = crate::riff::parse(&self.bytes)?;
114        let frame = container.frame;
115        let rgba = decode_rgba(
116            container.bitstream,
117            frame.width as usize,
118            frame.height as usize,
119        )?;
120
121        // The frame onto its canvas: a still fills it; an animation's first
122        // frame is written into a transparent-black canvas without blending,
123        // as libwebp's animation decoder starts every key frame.
124        let channels = self.descriptor.pixel.channels();
125        let (canvas_width, frame_width) = (container.width as usize, frame.width as usize);
126        let mut pixels =
127            vec![0_u8; container.width as usize * container.height as usize * channels];
128        for (y, source) in rgba.chunks_exact(frame_width * 4).enumerate() {
129            let row = (frame.y as usize + y) * canvas_width + frame.x as usize;
130            let Some(target) = pixels.get_mut(row * channels..(row + frame_width) * channels)
131            else {
132                return Err(PixelsError::malformed(
133                    "webp",
134                    "a frame overruns its canvas",
135                ));
136            };
137            for (out, sample) in target
138                .chunks_exact_mut(channels)
139                .zip(source.chunks_exact(4))
140            {
141                out.copy_from_slice(sample.get(..channels).unwrap_or(&[]));
142            }
143        }
144        Ok(pixels)
145    }
146}
147
148/// Decode one coded image of `width` x `height` to RGBA.
149fn decode_rgba(
150    bitstream: crate::riff::Bitstream<'_>,
151    width: usize,
152    height: usize,
153) -> Result<Vec<u8>> {
154    match bitstream {
155        crate::riff::Bitstream::Lossless(stream) => {
156            let argb = crate::vp8l::decode(stream, width, height)?;
157            Ok(argb
158                .into_iter()
159                .flat_map(|p| {
160                    let [blue, green, red, alpha] = p.to_le_bytes();
161                    [red, green, blue, alpha]
162                })
163                .collect())
164        }
165        crate::riff::Bitstream::Lossy { vp8, alpha } => {
166            let frame = crate::vp8::decode(vp8)?;
167            if (frame.width, frame.height) != (width, height) {
168                return Err(PixelsError::malformed(
169                    "webp",
170                    "the VP8 frame's size differs from the container's",
171                ));
172            }
173            // No ALPH chunk means opaque, as libwebp reports it.
174            let alpha = match alpha {
175                Some(chunk) => crate::alpha::decode(chunk, width, height)?,
176                None => vec![255; width * height],
177            };
178            Ok(crate::yuv::to_rgb(
179                &frame.y,
180                frame.y_stride,
181                &frame.u,
182                &frame.v,
183                frame.uv_stride,
184                width,
185                height,
186                Some(&alpha),
187            ))
188        }
189    }
190}
191
192impl Decoder for WebPDecoder {
193    fn descriptor(&self) -> ImageDescriptor {
194        self.descriptor
195    }
196
197    fn orientation(&self) -> Orientation {
198        self.orientation
199    }
200
201    fn icc_profile(&self) -> Option<&[u8]> {
202        self.icc.as_deref()
203    }
204
205    /// The `EXIF` chunk's block.
206    fn exif(&self) -> Option<&[u8]> {
207        self.exif.as_deref()
208    }
209
210    fn animation(&self) -> Option<Animation> {
211        self.animation.clone()
212    }
213
214    fn capability(&self) -> DecodeCapability {
215        // The image is already in memory, but `Sequential` is what the row
216        // contract describes; claiming `Regions` would promise a
217        // `read_region` this does not implement.
218        DecodeCapability::Sequential
219    }
220
221    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
222        if self.row >= self.descriptor.height {
223            return Err(PixelsError::invalid_argument(
224                "out",
225                format!("all {} rows have already been read", self.descriptor.height),
226            ));
227        }
228        let row_bytes = self.descriptor.row_bytes();
229        if out.len() != row_bytes {
230            return Err(PixelsError::invalid_argument(
231                "out",
232                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
233            ));
234        }
235        if self.pixels.is_none() {
236            self.pixels = Some(self.decode_canvas()?);
237            // Decoded, the compressed file is no longer needed.
238            self.bytes = Vec::new();
239        }
240        let start = self.row as usize * row_bytes;
241        let row = self
242            .pixels
243            .as_deref()
244            .unwrap_or(&[])
245            .get(start..)
246            .and_then(|rest| rest.get(..row_bytes))
247            .ok_or_else(|| PixelsError::malformed("webp", "decoded image is short"))?;
248        out.copy_from_slice(row);
249        self.row += 1;
250        Ok(())
251    }
252}
253
254/// Whether `prefix` starts with a WebP signature.
255///
256/// Detection is by magic bytes only (SPEC §Formats). `RIFF` alone names a
257/// container family that also holds WAV and AVI, so the form type at offset 8
258/// is what actually identifies a WebP.
259#[must_use]
260pub fn probe(prefix: &[u8]) -> bool {
261    prefix.get(..4) == Some(&crate::SIGNATURE_RIFF[..])
262        && prefix.get(8..12) == Some(&crate::SIGNATURE_WEBP[..])
263}
264
265/// The WebP entry in a sniffing registry.
266#[derive(Debug, Clone, Copy, Default)]
267pub struct WebPCodec;
268
269impl Codec for WebPCodec {
270    fn format(&self) -> Format {
271        Format::WebP
272    }
273
274    fn magic_len(&self) -> usize {
275        12
276    }
277
278    fn probe(&self, prefix: &[u8]) -> bool {
279        probe(prefix)
280    }
281}
282
283#[cfg(test)]
284#[allow(
285    clippy::unwrap_used,
286    clippy::indexing_slicing,
287    reason = "tests operate on known-good values and assert shapes directly"
288)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn probe_needs_the_form_type_not_just_riff() {
294        let mut header = Vec::from(*b"RIFF");
295        header.extend_from_slice(&[0, 0, 0, 0]);
296        header.extend_from_slice(b"WEBP");
297        assert!(probe(&header));
298
299        // A WAV file is also RIFF, and must not be claimed.
300        let mut wav = Vec::from(*b"RIFF");
301        wav.extend_from_slice(&[0, 0, 0, 0]);
302        wav.extend_from_slice(b"WAVE");
303        assert!(!probe(&wav));
304
305        // Short prefixes are declined, never indexed past.
306        assert!(!probe(b"RIFF"));
307        assert!(!probe(b""));
308        assert!(!probe(b"\x89PNG\r\n\x1a\n"));
309    }
310
311    const LOSSY: &[u8] = include_bytes!("../tests/fixtures/lossy/alpha_blocks_q70.webp");
312
313    #[test]
314    fn opening_parses_the_header_and_leaves_the_pixels_for_the_first_row() {
315        let mut decoder = WebPDecoder::new(LOSSY, Limits::default()).unwrap();
316        // Everything metadata needs is known; nothing has been decoded.
317        assert_eq!(decoder.descriptor().pixel, PixelFormat::Rgba8);
318        assert!(decoder.pixels.is_none());
319
320        let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
321        decoder.read_row(&mut row).unwrap();
322        assert!(decoder.pixels.is_some());
323        assert!(decoder.bytes.is_empty(), "the compressed file is released");
324    }
325
326    #[test]
327    fn a_bitstream_damaged_past_its_header_fails_on_the_first_row() {
328        // The lossless bitstream starts at byte 20 (RIFF, WEBP, VP8L chunk
329        // header) with a 5-byte header; everything after it is wrecked, so
330        // the container and the image header still parse.
331        let mut bytes = include_bytes!("../tests/fixtures/lossless/blocks_m6.webp").to_vec();
332        for byte in bytes.iter_mut().skip(25) {
333            *byte = 0xFF;
334        }
335        let mut decoder = WebPDecoder::new(&bytes[..], Limits::default()).unwrap();
336        let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
337        let error = decoder.read_row(&mut row).unwrap_err();
338        assert_eq!(
339            error.code(),
340            otf_pixels_core::ErrorCode::Malformed,
341            "{error}"
342        );
343    }
344
345    #[test]
346    fn a_stream_that_is_not_a_webp_is_rejected() {
347        let error = WebPDecoder::new(&b"not a webp at all"[..], Limits::default()).unwrap_err();
348        assert_eq!(
349            error.code(),
350            otf_pixels_core::ErrorCode::Malformed,
351            "{error}"
352        );
353    }
354}