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