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 exif: Option<Vec<u8>>,
34 icc: Option<Vec<u8>>,
36 animation: Option<Animation>,
38}
39
40impl WebPDecoder {
41 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 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 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 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 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
148fn 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 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 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 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 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#[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#[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 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 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 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 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}