1use otf_pixels_core::{
9 Animation, Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation,
10 PixelFormat, PixelsError, Result, Source,
11};
12
13const MAX_COMPRESSED: usize = 256 * 1024 * 1024;
19
20#[derive(Debug)]
22pub struct WebPDecoder {
23 descriptor: ImageDescriptor,
24 bytes: Vec<u8>,
26 pixels: Option<Vec<u8>>,
28 row: u32,
30 orientation: Orientation,
32 icc: Option<Vec<u8>>,
34 animation: Option<Animation>,
36}
37
38impl WebPDecoder {
39 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 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 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 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 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
142fn 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 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 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 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#[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#[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 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 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 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 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}