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