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