Coverage Report

Created: 2026-03-20 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/image-webp-0.2.4/src/decoder.rs
Line
Count
Source
1
use byteorder_lite::{LittleEndian, ReadBytesExt};
2
use quick_error::quick_error;
3
4
use std::collections::HashMap;
5
use std::io::{self, BufRead, Cursor, Read, Seek};
6
use std::num::NonZeroU16;
7
use std::ops::Range;
8
9
use crate::extended::{self, get_alpha_predictor, read_alpha_chunk, WebPExtendedInfo};
10
11
use super::lossless::LosslessDecoder;
12
use super::vp8::Vp8Decoder;
13
14
quick_error! {
15
    /// Errors that can occur when attempting to decode a WebP image
16
    #[derive(Debug)]
17
    #[non_exhaustive]
18
    pub enum DecodingError {
19
        /// An IO error occurred while reading the file
20
        IoError(err: io::Error) {
21
            from()
22
            display("IO Error: {}", err)
23
            source(err)
24
        }
25
26
        /// RIFF's "RIFF" signature not found or invalid
27
        RiffSignatureInvalid(err: [u8; 4]) {
28
            display("Invalid RIFF signature: {err:x?}")
29
        }
30
31
        /// WebP's "WEBP" signature not found or invalid
32
        WebpSignatureInvalid(err: [u8; 4]) {
33
            display("Invalid WebP signature: {err:x?}")
34
        }
35
36
        /// An expected chunk was missing
37
        ChunkMissing {
38
            display("An expected chunk was missing")
39
        }
40
41
        /// Chunk Header was incorrect or invalid in its usage
42
        ChunkHeaderInvalid(err: [u8; 4]) {
43
            display("Invalid Chunk header: {err:x?}")
44
        }
45
46
        #[allow(deprecated)]
47
        #[deprecated]
48
        /// Some bits were invalid
49
        ReservedBitSet {
50
            display("Reserved bits set")
51
        }
52
53
        /// The ALPH chunk preprocessing info flag was invalid
54
        InvalidAlphaPreprocessing {
55
            display("Alpha chunk preprocessing flag invalid")
56
        }
57
58
        /// Invalid compression method
59
        InvalidCompressionMethod {
60
            display("Invalid compression method")
61
        }
62
63
        /// Alpha chunk doesn't match the frame's size
64
        AlphaChunkSizeMismatch {
65
            display("Alpha chunk size mismatch")
66
        }
67
68
        /// Image is too large, either for the platform's pointer size or generally
69
        ImageTooLarge {
70
            display("Image too large")
71
        }
72
73
        /// Frame would go out of the canvas
74
        FrameOutsideImage {
75
            display("Frame outside image")
76
        }
77
78
        /// Signature of 0x2f not found
79
        LosslessSignatureInvalid(err: u8) {
80
            display("Invalid lossless signature: {err:x?}")
81
        }
82
83
        /// Version Number was not zero
84
        VersionNumberInvalid(err: u8) {
85
            display("Invalid lossless version number: {err}")
86
        }
87
88
        /// Invalid color cache bits
89
        InvalidColorCacheBits(err: u8) {
90
            display("Invalid color cache bits: {err}")
91
        }
92
93
        /// An invalid Huffman code was encountered
94
        HuffmanError {
95
            display("Invalid Huffman code")
96
        }
97
98
        /// The bitstream was somehow corrupt
99
        BitStreamError {
100
            display("Corrupt bitstream")
101
        }
102
103
        /// The transforms specified were invalid
104
        TransformError {
105
            display("Invalid transform")
106
        }
107
108
        /// VP8's `[0x9D, 0x01, 0x2A]` magic not found or invalid
109
        Vp8MagicInvalid(err: [u8; 3]) {
110
            display("Invalid VP8 magic: {err:x?}")
111
        }
112
113
        /// VP8 Decoder initialisation wasn't provided with enough data
114
        NotEnoughInitData {
115
            display("Not enough VP8 init data")
116
        }
117
118
        /// At time of writing, only the YUV colour-space encoded as `0` is specified
119
        ColorSpaceInvalid(err: u8) {
120
            display("Invalid VP8 color space: {err}")
121
        }
122
123
        /// LUMA prediction mode was not recognised
124
        LumaPredictionModeInvalid(err: i8) {
125
            display("Invalid VP8 luma prediction mode: {err}")
126
        }
127
128
        /// Intra-prediction mode was not recognised
129
        IntraPredictionModeInvalid(err: i8) {
130
            display("Invalid VP8 intra prediction mode: {err}")
131
        }
132
133
        /// Chroma prediction mode was not recognised
134
        ChromaPredictionModeInvalid(err: i8) {
135
            display("Invalid VP8 chroma prediction mode: {err}")
136
        }
137
138
        /// Inconsistent image sizes
139
        InconsistentImageSizes {
140
            display("Inconsistent image sizes")
141
        }
142
143
        /// The file may be valid, but this crate doesn't support decoding it.
144
        UnsupportedFeature(err: String) {
145
            display("Unsupported feature: {err}")
146
        }
147
148
        /// Invalid function call or parameter
149
        InvalidParameter(err: String) {
150
            display("Invalid parameter: {err}")
151
        }
152
153
        /// Memory limit exceeded
154
        MemoryLimitExceeded {
155
            display("Memory limit exceeded")
156
        }
157
158
        /// Invalid chunk size
159
        InvalidChunkSize {
160
            display("Invalid chunk size")
161
        }
162
163
        /// No more frames in image
164
        NoMoreFrames {
165
            display("No more frames")
166
        }
167
    }
168
}
169
170
/// All possible RIFF chunks in a WebP image file
171
#[allow(clippy::upper_case_acronyms)]
172
#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)]
173
pub(crate) enum WebPRiffChunk {
174
    RIFF,
175
    WEBP,
176
    VP8,
177
    VP8L,
178
    VP8X,
179
    ANIM,
180
    ANMF,
181
    ALPH,
182
    ICCP,
183
    EXIF,
184
    XMP,
185
    Unknown([u8; 4]),
186
}
187
188
impl WebPRiffChunk {
189
44.2k
    pub(crate) const fn from_fourcc(chunk_fourcc: [u8; 4]) -> Self {
190
44.2k
        match &chunk_fourcc {
191
7.15k
            b"RIFF" => Self::RIFF,
192
7.16k
            b"WEBP" => Self::WEBP,
193
2.76k
            b"VP8 " => Self::VP8,
194
3.94k
            b"VP8L" => Self::VP8L,
195
2.05k
            b"VP8X" => Self::VP8X,
196
1.59k
            b"ANIM" => Self::ANIM,
197
3.95k
            b"ANMF" => Self::ANMF,
198
2.10k
            b"ALPH" => Self::ALPH,
199
1.56k
            b"ICCP" => Self::ICCP,
200
927
            b"EXIF" => Self::EXIF,
201
534
            b"XMP " => Self::XMP,
202
10.5k
            _ => Self::Unknown(chunk_fourcc),
203
        }
204
44.2k
    }
205
206
92
    pub(crate) const fn to_fourcc(self) -> [u8; 4] {
207
92
        match self {
208
2
            Self::RIFF => *b"RIFF",
209
1
            Self::WEBP => *b"WEBP",
210
2
            Self::VP8 => *b"VP8 ",
211
2
            Self::VP8L => *b"VP8L",
212
1
            Self::VP8X => *b"VP8X",
213
1
            Self::ANIM => *b"ANIM",
214
2
            Self::ANMF => *b"ANMF",
215
4
            Self::ALPH => *b"ALPH",
216
1
            Self::ICCP => *b"ICCP",
217
1
            Self::EXIF => *b"EXIF",
218
1
            Self::XMP => *b"XMP ",
219
74
            Self::Unknown(fourcc) => fourcc,
220
        }
221
92
    }
222
223
17.7k
    pub(crate) const fn is_unknown(self) -> bool {
224
17.7k
        matches!(self, Self::Unknown(_))
225
17.7k
    }
226
}
227
228
// enum WebPImage {
229
//     Lossy(VP8Frame),
230
//     Lossless(LosslessFrame),
231
//     Extended(ExtendedImage),
232
// }
233
234
enum ImageKind {
235
    Lossy,
236
    Lossless,
237
    Extended(WebPExtendedInfo),
238
}
239
240
struct AnimationState {
241
    next_frame: u32,
242
    next_frame_start: u64,
243
    dispose_next_frame: bool,
244
    previous_frame_width: u32,
245
    previous_frame_height: u32,
246
    previous_frame_x_offset: u32,
247
    previous_frame_y_offset: u32,
248
    canvas: Option<Vec<u8>>,
249
}
250
impl Default for AnimationState {
251
7.86k
    fn default() -> Self {
252
7.86k
        Self {
253
7.86k
            next_frame: 0,
254
7.86k
            next_frame_start: 0,
255
7.86k
            dispose_next_frame: true,
256
7.86k
            previous_frame_width: 0,
257
7.86k
            previous_frame_height: 0,
258
7.86k
            previous_frame_x_offset: 0,
259
7.86k
            previous_frame_y_offset: 0,
260
7.86k
            canvas: None,
261
7.86k
        }
262
7.86k
    }
263
}
264
265
/// Number of times that an animation loops.
266
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
267
pub enum LoopCount {
268
    /// The animation loops forever.
269
    Forever,
270
    /// Each frame of the animation is displayed the specified number of times.
271
    Times(NonZeroU16),
272
}
273
274
/// WebP decoder configuration options
275
#[derive(Clone)]
276
#[non_exhaustive]
277
pub struct WebPDecodeOptions {
278
    /// The upsampling method used in conversion from lossy yuv to rgb
279
    ///
280
    /// Defaults to `Bilinear`.
281
    pub lossy_upsampling: UpsamplingMethod,
282
}
283
284
impl Default for WebPDecodeOptions {
285
7.14k
    fn default() -> Self {
286
7.14k
        Self {
287
7.14k
            lossy_upsampling: UpsamplingMethod::Bilinear,
288
7.14k
        }
289
7.14k
    }
290
}
291
292
/// Methods for upsampling the chroma values in lossy decoding
293
///
294
/// The chroma red and blue planes are encoded in VP8 as half the size of the luma plane
295
/// Therefore we need to upsample these values up to fit each pixel in the image.
296
#[derive(Clone, Copy, Default)]
297
pub enum UpsamplingMethod {
298
    /// Fancy upsampling
299
    ///
300
    /// Does bilinear interpolation using the 4 values nearest to the pixel, weighting based on the distance
301
    /// from the pixel.
302
    #[default]
303
    Bilinear,
304
    /// Simple upsampling, just uses the closest u/v value to the pixel when upsampling
305
    ///
306
    /// Matches the -nofancy option in dwebp.
307
    /// Should be faster but may lead to slightly jagged edges.
308
    Simple,
309
}
310
311
/// WebP image format decoder.
312
pub struct WebPDecoder<R> {
313
    r: R,
314
    memory_limit: usize,
315
316
    width: u32,
317
    height: u32,
318
319
    kind: ImageKind,
320
    animation: AnimationState,
321
322
    is_lossy: bool,
323
    has_alpha: bool,
324
    num_frames: u32,
325
    loop_count: LoopCount,
326
    loop_duration: u64,
327
328
    chunks: HashMap<WebPRiffChunk, Range<u64>>,
329
330
    webp_decode_options: WebPDecodeOptions,
331
}
332
333
impl<R: BufRead + Seek> WebPDecoder<R> {
334
    /// Create a new `WebPDecoder` from the reader `r`. The decoder performs many small reads, so the
335
    /// reader should be buffered.
336
7.14k
    pub fn new(r: R) -> Result<Self, DecodingError> {
337
7.14k
        Self::new_with_options(r, WebPDecodeOptions::default())
338
7.14k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::new
Line
Count
Source
336
7.14k
    pub fn new(r: R) -> Result<Self, DecodingError> {
337
7.14k
        Self::new_with_options(r, WebPDecodeOptions::default())
338
7.14k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::new
339
340
    /// Create a new `WebPDecoder` from the reader `r` with the options `WebPDecodeOptions`. The decoder
341
    /// performs many small reads, so the reader should be buffered.
342
7.14k
    pub fn new_with_options(
343
7.14k
        r: R,
344
7.14k
        webp_decode_options: WebPDecodeOptions,
345
7.14k
    ) -> Result<Self, DecodingError> {
346
7.14k
        let mut decoder = Self {
347
7.14k
            r,
348
7.14k
            width: 0,
349
7.14k
            height: 0,
350
7.14k
            num_frames: 0,
351
7.14k
            kind: ImageKind::Lossy,
352
7.14k
            chunks: HashMap::new(),
353
7.14k
            animation: Default::default(),
354
7.14k
            memory_limit: usize::MAX,
355
7.14k
            is_lossy: false,
356
7.14k
            has_alpha: false,
357
7.14k
            loop_count: LoopCount::Times(NonZeroU16::new(1).unwrap()),
358
7.14k
            loop_duration: 0,
359
7.14k
            webp_decode_options,
360
7.14k
        };
361
7.14k
        decoder.read_data()?;
362
6.80k
        Ok(decoder)
363
7.14k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::new_with_options
Line
Count
Source
342
7.14k
    pub fn new_with_options(
343
7.14k
        r: R,
344
7.14k
        webp_decode_options: WebPDecodeOptions,
345
7.14k
    ) -> Result<Self, DecodingError> {
346
7.14k
        let mut decoder = Self {
347
7.14k
            r,
348
7.14k
            width: 0,
349
7.14k
            height: 0,
350
7.14k
            num_frames: 0,
351
7.14k
            kind: ImageKind::Lossy,
352
7.14k
            chunks: HashMap::new(),
353
7.14k
            animation: Default::default(),
354
7.14k
            memory_limit: usize::MAX,
355
7.14k
            is_lossy: false,
356
7.14k
            has_alpha: false,
357
7.14k
            loop_count: LoopCount::Times(NonZeroU16::new(1).unwrap()),
358
7.14k
            loop_duration: 0,
359
7.14k
            webp_decode_options,
360
7.14k
        };
361
7.14k
        decoder.read_data()?;
362
6.80k
        Ok(decoder)
363
7.14k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::new_with_options
364
365
7.14k
    fn read_data(&mut self) -> Result<(), DecodingError> {
366
7.14k
        let (WebPRiffChunk::RIFF, riff_size, _) = read_chunk_header(&mut self.r)? else {
367
2
            return Err(DecodingError::ChunkHeaderInvalid(*b"RIFF"));
368
        };
369
370
7.14k
        match &read_fourcc(&mut self.r)? {
371
7.14k
            WebPRiffChunk::WEBP => {}
372
0
            fourcc => return Err(DecodingError::WebpSignatureInvalid(fourcc.to_fourcc())),
373
        }
374
375
7.14k
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
376
7.13k
        let start = self.r.stream_position()?;
377
378
7.13k
        match chunk {
379
            WebPRiffChunk::VP8 => {
380
1.99k
                let tag = self.r.read_u24::<LittleEndian>()?;
381
382
1.99k
                let keyframe = tag & 1 == 0;
383
1.99k
                if !keyframe {
384
1
                    return Err(DecodingError::UnsupportedFeature(
385
1
                        "Non-keyframe frames".to_owned(),
386
1
                    ));
387
1.99k
                }
388
389
1.99k
                let mut tag = [0u8; 3];
390
1.99k
                self.r.read_exact(&mut tag)?;
391
1.99k
                if tag != [0x9d, 0x01, 0x2a] {
392
1
                    return Err(DecodingError::Vp8MagicInvalid(tag));
393
1.99k
                }
394
395
1.99k
                let w = self.r.read_u16::<LittleEndian>()?;
396
1.99k
                let h = self.r.read_u16::<LittleEndian>()?;
397
398
1.99k
                self.width = u32::from(w & 0x3FFF);
399
1.99k
                self.height = u32::from(h & 0x3FFF);
400
1.99k
                if self.width == 0 || self.height == 0 {
401
2
                    return Err(DecodingError::InconsistentImageSizes);
402
1.98k
                }
403
404
1.98k
                self.chunks
405
1.98k
                    .insert(WebPRiffChunk::VP8, start..start + chunk_size);
406
1.98k
                self.kind = ImageKind::Lossy;
407
1.98k
                self.is_lossy = true;
408
            }
409
            WebPRiffChunk::VP8L => {
410
3.57k
                let signature = self.r.read_u8()?;
411
3.57k
                if signature != 0x2f {
412
8
                    return Err(DecodingError::LosslessSignatureInvalid(signature));
413
3.56k
                }
414
415
3.56k
                let header = self.r.read_u32::<LittleEndian>()?;
416
3.56k
                let version = header >> 29;
417
3.56k
                if version != 0 {
418
6
                    return Err(DecodingError::VersionNumberInvalid(version as u8));
419
3.55k
                }
420
421
3.55k
                self.width = (1 + header) & 0x3FFF;
422
3.55k
                self.height = (1 + (header >> 14)) & 0x3FFF;
423
3.55k
                self.chunks
424
3.55k
                    .insert(WebPRiffChunk::VP8L, start..start + chunk_size);
425
3.55k
                self.kind = ImageKind::Lossless;
426
3.55k
                self.has_alpha = (header >> 28) & 1 != 0;
427
            }
428
            WebPRiffChunk::VP8X => {
429
1.52k
                let mut info = extended::read_extended_header(&mut self.r)?;
430
1.52k
                self.width = info.canvas_width;
431
1.52k
                self.height = info.canvas_height;
432
433
1.52k
                let mut position = start + chunk_size_rounded;
434
1.52k
                let max_position = position + riff_size.saturating_sub(12);
435
1.52k
                self.r.seek(io::SeekFrom::Start(position))?;
436
437
19.2k
                while position < max_position {
438
18.5k
                    match read_chunk_header(&mut self.r) {
439
17.7k
                        Ok((chunk, chunk_size, chunk_size_rounded)) => {
440
17.7k
                            let range = position + 8..position + 8 + chunk_size;
441
17.7k
                            position += 8 + chunk_size_rounded;
442
443
17.7k
                            if !chunk.is_unknown() {
444
9.56k
                                self.chunks.entry(chunk).or_insert(range);
445
9.56k
                            }
446
447
17.7k
                            if chunk == WebPRiffChunk::ANMF {
448
3.22k
                                self.num_frames += 1;
449
3.22k
                                if chunk_size < 24 {
450
1
                                    return Err(DecodingError::InvalidChunkSize);
451
3.22k
                                }
452
453
3.22k
                                self.r.seek_relative(12)?;
454
3.22k
                                let duration = self.r.read_u32::<LittleEndian>()? & 0xffffff;
455
3.21k
                                self.loop_duration =
456
3.21k
                                    self.loop_duration.wrapping_add(u64::from(duration));
457
458
                                // If the image is animated, the image data chunk will be inside the
459
                                // ANMF chunks, so we must inspect them to determine whether the
460
                                // image contains any lossy image data. VP8 chunks store lossy data
461
                                // and the spec says that lossless images SHOULD NOT contain ALPH
462
                                // chunks, so we treat both as indicators of lossy images.
463
3.21k
                                if !self.is_lossy {
464
2.33k
                                    let (subchunk, ..) = read_chunk_header(&mut self.r)?;
465
2.33k
                                    if let WebPRiffChunk::VP8 | WebPRiffChunk::ALPH = subchunk {
466
560
                                        self.is_lossy = true;
467
1.77k
                                    }
468
2.33k
                                    self.r.seek_relative(chunk_size_rounded as i64 - 24)?;
469
                                } else {
470
882
                                    self.r.seek_relative(chunk_size_rounded as i64 - 16)?;
471
                                }
472
473
3.21k
                                continue;
474
14.4k
                            }
475
476
14.4k
                            self.r.seek_relative(chunk_size_rounded as i64)?;
477
                        }
478
826
                        Err(DecodingError::IoError(e))
479
826
                            if e.kind() == io::ErrorKind::UnexpectedEof =>
480
                        {
481
826
                            break;
482
                        }
483
0
                        Err(e) => return Err(e),
484
                    }
485
                }
486
1.51k
                self.is_lossy = self.is_lossy || self.chunks.contains_key(&WebPRiffChunk::VP8);
487
488
                // NOTE: We allow malformed images that have `info.icc_profile` set without a ICCP chunk,
489
                // because this is relatively common.
490
1.51k
                if info.animation
491
840
                    && (!self.chunks.contains_key(&WebPRiffChunk::ANIM)
492
764
                        || !self.chunks.contains_key(&WebPRiffChunk::ANMF))
493
1.41k
                    || info.exif_metadata && !self.chunks.contains_key(&WebPRiffChunk::EXIF)
494
1.38k
                    || info.xmp_metadata && !self.chunks.contains_key(&WebPRiffChunk::XMP)
495
1.37k
                    || !info.animation
496
643
                        && self.chunks.contains_key(&WebPRiffChunk::VP8)
497
643
                            == self.chunks.contains_key(&WebPRiffChunk::VP8L)
498
                {
499
240
                    return Err(DecodingError::ChunkMissing);
500
1.27k
                }
501
502
                // Decode ANIM chunk.
503
1.27k
                if info.animation {
504
735
                    match self.read_chunk(WebPRiffChunk::ANIM, 6) {
505
734
                        Ok(Some(chunk)) => {
506
734
                            let mut cursor = Cursor::new(chunk);
507
734
                            cursor.read_exact(&mut info.background_color_hint)?;
508
733
                            self.loop_count = match cursor.read_u16::<LittleEndian>()? {
509
307
                                0 => LoopCount::Forever,
510
425
                                n => LoopCount::Times(NonZeroU16::new(n).unwrap()),
511
                            };
512
732
                            self.animation.next_frame_start =
513
732
                                self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
514
                        }
515
0
                        Ok(None) => return Err(DecodingError::ChunkMissing),
516
                        Err(DecodingError::MemoryLimitExceeded) => {
517
1
                            return Err(DecodingError::InvalidChunkSize)
518
                        }
519
0
                        Err(e) => return Err(e),
520
                    }
521
535
                }
522
523
                // If the image is animated, the image data chunk will be inside the ANMF chunks. We
524
                // store the ALPH, VP8, and VP8L chunks (as applicable) of the first frame in the
525
                // hashmap so that we can read them later.
526
1.26k
                if let Some(range) = self.chunks.get(&WebPRiffChunk::ANMF).cloned() {
527
734
                    let mut position = range.start + 16;
528
734
                    self.r.seek(io::SeekFrom::Start(position))?;
529
1.38k
                    for _ in 0..2 {
530
1.29k
                        let (subchunk, subchunk_size, subchunk_size_rounded) =
531
1.30k
                            read_chunk_header(&mut self.r)?;
532
1.29k
                        let subrange = position + 8..position + 8 + subchunk_size;
533
1.29k
                        self.chunks.entry(subchunk).or_insert(subrange.clone());
534
535
1.29k
                        position += 8 + subchunk_size_rounded;
536
1.29k
                        if position + 8 > range.end {
537
643
                            break;
538
651
                        }
539
                    }
540
533
                }
541
542
1.25k
                self.has_alpha = info.alpha;
543
1.25k
                self.kind = ImageKind::Extended(info);
544
            }
545
41
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
546
        };
547
548
6.80k
        Ok(())
549
7.14k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_data
Line
Count
Source
365
7.14k
    fn read_data(&mut self) -> Result<(), DecodingError> {
366
7.14k
        let (WebPRiffChunk::RIFF, riff_size, _) = read_chunk_header(&mut self.r)? else {
367
2
            return Err(DecodingError::ChunkHeaderInvalid(*b"RIFF"));
368
        };
369
370
7.14k
        match &read_fourcc(&mut self.r)? {
371
7.14k
            WebPRiffChunk::WEBP => {}
372
0
            fourcc => return Err(DecodingError::WebpSignatureInvalid(fourcc.to_fourcc())),
373
        }
374
375
7.14k
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
376
7.13k
        let start = self.r.stream_position()?;
377
378
7.13k
        match chunk {
379
            WebPRiffChunk::VP8 => {
380
1.99k
                let tag = self.r.read_u24::<LittleEndian>()?;
381
382
1.99k
                let keyframe = tag & 1 == 0;
383
1.99k
                if !keyframe {
384
1
                    return Err(DecodingError::UnsupportedFeature(
385
1
                        "Non-keyframe frames".to_owned(),
386
1
                    ));
387
1.99k
                }
388
389
1.99k
                let mut tag = [0u8; 3];
390
1.99k
                self.r.read_exact(&mut tag)?;
391
1.99k
                if tag != [0x9d, 0x01, 0x2a] {
392
1
                    return Err(DecodingError::Vp8MagicInvalid(tag));
393
1.99k
                }
394
395
1.99k
                let w = self.r.read_u16::<LittleEndian>()?;
396
1.99k
                let h = self.r.read_u16::<LittleEndian>()?;
397
398
1.99k
                self.width = u32::from(w & 0x3FFF);
399
1.99k
                self.height = u32::from(h & 0x3FFF);
400
1.99k
                if self.width == 0 || self.height == 0 {
401
2
                    return Err(DecodingError::InconsistentImageSizes);
402
1.98k
                }
403
404
1.98k
                self.chunks
405
1.98k
                    .insert(WebPRiffChunk::VP8, start..start + chunk_size);
406
1.98k
                self.kind = ImageKind::Lossy;
407
1.98k
                self.is_lossy = true;
408
            }
409
            WebPRiffChunk::VP8L => {
410
3.57k
                let signature = self.r.read_u8()?;
411
3.57k
                if signature != 0x2f {
412
8
                    return Err(DecodingError::LosslessSignatureInvalid(signature));
413
3.56k
                }
414
415
3.56k
                let header = self.r.read_u32::<LittleEndian>()?;
416
3.56k
                let version = header >> 29;
417
3.56k
                if version != 0 {
418
6
                    return Err(DecodingError::VersionNumberInvalid(version as u8));
419
3.55k
                }
420
421
3.55k
                self.width = (1 + header) & 0x3FFF;
422
3.55k
                self.height = (1 + (header >> 14)) & 0x3FFF;
423
3.55k
                self.chunks
424
3.55k
                    .insert(WebPRiffChunk::VP8L, start..start + chunk_size);
425
3.55k
                self.kind = ImageKind::Lossless;
426
3.55k
                self.has_alpha = (header >> 28) & 1 != 0;
427
            }
428
            WebPRiffChunk::VP8X => {
429
1.52k
                let mut info = extended::read_extended_header(&mut self.r)?;
430
1.52k
                self.width = info.canvas_width;
431
1.52k
                self.height = info.canvas_height;
432
433
1.52k
                let mut position = start + chunk_size_rounded;
434
1.52k
                let max_position = position + riff_size.saturating_sub(12);
435
1.52k
                self.r.seek(io::SeekFrom::Start(position))?;
436
437
19.2k
                while position < max_position {
438
18.5k
                    match read_chunk_header(&mut self.r) {
439
17.7k
                        Ok((chunk, chunk_size, chunk_size_rounded)) => {
440
17.7k
                            let range = position + 8..position + 8 + chunk_size;
441
17.7k
                            position += 8 + chunk_size_rounded;
442
443
17.7k
                            if !chunk.is_unknown() {
444
9.56k
                                self.chunks.entry(chunk).or_insert(range);
445
9.56k
                            }
446
447
17.7k
                            if chunk == WebPRiffChunk::ANMF {
448
3.22k
                                self.num_frames += 1;
449
3.22k
                                if chunk_size < 24 {
450
1
                                    return Err(DecodingError::InvalidChunkSize);
451
3.22k
                                }
452
453
3.22k
                                self.r.seek_relative(12)?;
454
3.22k
                                let duration = self.r.read_u32::<LittleEndian>()? & 0xffffff;
455
3.21k
                                self.loop_duration =
456
3.21k
                                    self.loop_duration.wrapping_add(u64::from(duration));
457
458
                                // If the image is animated, the image data chunk will be inside the
459
                                // ANMF chunks, so we must inspect them to determine whether the
460
                                // image contains any lossy image data. VP8 chunks store lossy data
461
                                // and the spec says that lossless images SHOULD NOT contain ALPH
462
                                // chunks, so we treat both as indicators of lossy images.
463
3.21k
                                if !self.is_lossy {
464
2.33k
                                    let (subchunk, ..) = read_chunk_header(&mut self.r)?;
465
2.33k
                                    if let WebPRiffChunk::VP8 | WebPRiffChunk::ALPH = subchunk {
466
560
                                        self.is_lossy = true;
467
1.77k
                                    }
468
2.33k
                                    self.r.seek_relative(chunk_size_rounded as i64 - 24)?;
469
                                } else {
470
882
                                    self.r.seek_relative(chunk_size_rounded as i64 - 16)?;
471
                                }
472
473
3.21k
                                continue;
474
14.4k
                            }
475
476
14.4k
                            self.r.seek_relative(chunk_size_rounded as i64)?;
477
                        }
478
826
                        Err(DecodingError::IoError(e))
479
826
                            if e.kind() == io::ErrorKind::UnexpectedEof =>
480
                        {
481
826
                            break;
482
                        }
483
0
                        Err(e) => return Err(e),
484
                    }
485
                }
486
1.51k
                self.is_lossy = self.is_lossy || self.chunks.contains_key(&WebPRiffChunk::VP8);
487
488
                // NOTE: We allow malformed images that have `info.icc_profile` set without a ICCP chunk,
489
                // because this is relatively common.
490
1.51k
                if info.animation
491
840
                    && (!self.chunks.contains_key(&WebPRiffChunk::ANIM)
492
764
                        || !self.chunks.contains_key(&WebPRiffChunk::ANMF))
493
1.41k
                    || info.exif_metadata && !self.chunks.contains_key(&WebPRiffChunk::EXIF)
494
1.38k
                    || info.xmp_metadata && !self.chunks.contains_key(&WebPRiffChunk::XMP)
495
1.37k
                    || !info.animation
496
643
                        && self.chunks.contains_key(&WebPRiffChunk::VP8)
497
643
                            == self.chunks.contains_key(&WebPRiffChunk::VP8L)
498
                {
499
240
                    return Err(DecodingError::ChunkMissing);
500
1.27k
                }
501
502
                // Decode ANIM chunk.
503
1.27k
                if info.animation {
504
735
                    match self.read_chunk(WebPRiffChunk::ANIM, 6) {
505
734
                        Ok(Some(chunk)) => {
506
734
                            let mut cursor = Cursor::new(chunk);
507
734
                            cursor.read_exact(&mut info.background_color_hint)?;
508
733
                            self.loop_count = match cursor.read_u16::<LittleEndian>()? {
509
307
                                0 => LoopCount::Forever,
510
425
                                n => LoopCount::Times(NonZeroU16::new(n).unwrap()),
511
                            };
512
732
                            self.animation.next_frame_start =
513
732
                                self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
514
                        }
515
0
                        Ok(None) => return Err(DecodingError::ChunkMissing),
516
                        Err(DecodingError::MemoryLimitExceeded) => {
517
1
                            return Err(DecodingError::InvalidChunkSize)
518
                        }
519
0
                        Err(e) => return Err(e),
520
                    }
521
535
                }
522
523
                // If the image is animated, the image data chunk will be inside the ANMF chunks. We
524
                // store the ALPH, VP8, and VP8L chunks (as applicable) of the first frame in the
525
                // hashmap so that we can read them later.
526
1.26k
                if let Some(range) = self.chunks.get(&WebPRiffChunk::ANMF).cloned() {
527
734
                    let mut position = range.start + 16;
528
734
                    self.r.seek(io::SeekFrom::Start(position))?;
529
1.38k
                    for _ in 0..2 {
530
1.29k
                        let (subchunk, subchunk_size, subchunk_size_rounded) =
531
1.30k
                            read_chunk_header(&mut self.r)?;
532
1.29k
                        let subrange = position + 8..position + 8 + subchunk_size;
533
1.29k
                        self.chunks.entry(subchunk).or_insert(subrange.clone());
534
535
1.29k
                        position += 8 + subchunk_size_rounded;
536
1.29k
                        if position + 8 > range.end {
537
643
                            break;
538
651
                        }
539
                    }
540
533
                }
541
542
1.25k
                self.has_alpha = info.alpha;
543
1.25k
                self.kind = ImageKind::Extended(info);
544
            }
545
41
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
546
        };
547
548
6.80k
        Ok(())
549
7.14k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::read_data
550
551
    /// Sets the maximum amount of memory that the decoder is allowed to allocate at once.
552
    ///
553
    /// TODO: Some allocations currently ignore this limit.
554
0
    pub fn set_memory_limit(&mut self, limit: usize) {
555
0
        self.memory_limit = limit;
556
0
    }
557
558
    /// Get the background color specified in the image file if the image is extended and animated webp.
559
0
    pub fn background_color_hint(&self) -> Option<[u8; 4]> {
560
0
        if let ImageKind::Extended(info) = &self.kind {
561
0
            Some(info.background_color_hint)
562
        } else {
563
0
            None
564
        }
565
0
    }
566
567
    /// Sets the background color if the image is an extended and animated webp.
568
0
    pub fn set_background_color(&mut self, color: [u8; 4]) -> Result<(), DecodingError> {
569
0
        if let ImageKind::Extended(info) = &mut self.kind {
570
0
            info.background_color = Some(color);
571
0
            Ok(())
572
        } else {
573
0
            Err(DecodingError::InvalidParameter(
574
0
                "Background color can only be set on animated webp".to_owned(),
575
0
            ))
576
        }
577
0
    }
578
579
    /// Returns the (width, height) of the image in pixels.
580
33.9k
    pub fn dimensions(&self) -> (u32, u32) {
581
33.9k
        (self.width, self.height)
582
33.9k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::dimensions
Line
Count
Source
580
33.9k
    pub fn dimensions(&self) -> (u32, u32) {
581
33.9k
        (self.width, self.height)
582
33.9k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::dimensions
583
584
    /// Returns whether the image has an alpha channel. If so, the pixel format is Rgba8 and
585
    /// otherwise Rgb8.
586
35.8k
    pub fn has_alpha(&self) -> bool {
587
35.8k
        self.has_alpha
588
35.8k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::has_alpha
Line
Count
Source
586
35.8k
    pub fn has_alpha(&self) -> bool {
587
35.8k
        self.has_alpha
588
35.8k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::has_alpha
589
590
    /// Returns true if the image is animated.
591
7.50k
    pub fn is_animated(&self) -> bool {
592
7.50k
        match &self.kind {
593
5.53k
            ImageKind::Lossy | ImageKind::Lossless => false,
594
1.97k
            ImageKind::Extended(extended) => extended.animation,
595
        }
596
7.50k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::is_animated
Line
Count
Source
591
7.50k
    pub fn is_animated(&self) -> bool {
592
7.50k
        match &self.kind {
593
5.53k
            ImageKind::Lossy | ImageKind::Lossless => false,
594
1.97k
            ImageKind::Extended(extended) => extended.animation,
595
        }
596
7.50k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::is_animated
597
598
    /// Returns whether the image is lossy. For animated images, this is true if any frame is lossy.
599
0
    pub fn is_lossy(&mut self) -> bool {
600
0
        self.is_lossy
601
0
    }
602
603
    /// Returns the number of frames of a single loop of the animation, or zero if the image is not
604
    /// animated.
605
0
    pub fn num_frames(&self) -> u32 {
606
0
        self.num_frames
607
0
    }
608
609
    /// Returns the number of times the animation should loop.
610
0
    pub fn loop_count(&self) -> LoopCount {
611
0
        self.loop_count
612
0
    }
613
614
    /// Returns the total duration of one loop through the animation in milliseconds, or zero if the
615
    /// image is not animated.
616
    ///
617
    /// This is the sum of the durations of all individual frames of the image.
618
0
    pub fn loop_duration(&self) -> u64 {
619
0
        self.loop_duration
620
0
    }
621
622
735
    fn read_chunk(
623
735
        &mut self,
624
735
        chunk: WebPRiffChunk,
625
735
        max_size: usize,
626
735
    ) -> Result<Option<Vec<u8>>, DecodingError> {
627
735
        match self.chunks.get(&chunk) {
628
735
            Some(range) => {
629
735
                if range.end - range.start > max_size as u64 {
630
1
                    return Err(DecodingError::MemoryLimitExceeded);
631
734
                }
632
633
734
                self.r.seek(io::SeekFrom::Start(range.start))?;
634
734
                let mut data = vec![0; (range.end - range.start) as usize];
635
734
                self.r.read_exact(&mut data)?;
636
734
                Ok(Some(data))
637
            }
638
0
            None => Ok(None),
639
        }
640
735
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_chunk
Line
Count
Source
622
735
    fn read_chunk(
623
735
        &mut self,
624
735
        chunk: WebPRiffChunk,
625
735
        max_size: usize,
626
735
    ) -> Result<Option<Vec<u8>>, DecodingError> {
627
735
        match self.chunks.get(&chunk) {
628
735
            Some(range) => {
629
735
                if range.end - range.start > max_size as u64 {
630
1
                    return Err(DecodingError::MemoryLimitExceeded);
631
734
                }
632
633
734
                self.r.seek(io::SeekFrom::Start(range.start))?;
634
734
                let mut data = vec![0; (range.end - range.start) as usize];
635
734
                self.r.read_exact(&mut data)?;
636
734
                Ok(Some(data))
637
            }
638
0
            None => Ok(None),
639
        }
640
735
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::read_chunk
641
642
    /// Returns the raw bytes of the ICC profile, or None if there is no ICC profile.
643
0
    pub fn icc_profile(&mut self) -> Result<Option<Vec<u8>>, DecodingError> {
644
0
        self.read_chunk(WebPRiffChunk::ICCP, self.memory_limit)
645
0
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::icc_profile
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::icc_profile
646
647
    /// Returns the raw bytes of the EXIF metadata, or None if there is no EXIF metadata.
648
0
    pub fn exif_metadata(&mut self) -> Result<Option<Vec<u8>>, DecodingError> {
649
0
        self.read_chunk(WebPRiffChunk::EXIF, self.memory_limit)
650
0
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::exif_metadata
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::exif_metadata
651
652
    /// Returns the raw bytes of the XMP metadata, or None if there is no XMP metadata.
653
0
    pub fn xmp_metadata(&mut self) -> Result<Option<Vec<u8>>, DecodingError> {
654
0
        self.read_chunk(WebPRiffChunk::XMP, self.memory_limit)
655
0
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::xmp_metadata
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::xmp_metadata
656
657
    /// Returns the number of bytes required to store the image or a single frame, or None if that
658
    /// would take more than `usize::MAX` bytes.
659
7.50k
    pub fn output_buffer_size(&self) -> Option<usize> {
660
7.50k
        let bytes_per_pixel = if self.has_alpha() { 4 } else { 3 };
661
7.50k
        (self.width as usize)
662
7.50k
            .checked_mul(self.height as usize)?
663
7.50k
            .checked_mul(bytes_per_pixel)
664
7.50k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::output_buffer_size
Line
Count
Source
659
7.50k
    pub fn output_buffer_size(&self) -> Option<usize> {
660
7.50k
        let bytes_per_pixel = if self.has_alpha() { 4 } else { 3 };
661
7.50k
        (self.width as usize)
662
7.50k
            .checked_mul(self.height as usize)?
663
7.50k
            .checked_mul(bytes_per_pixel)
664
7.50k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::output_buffer_size
665
666
    /// Returns the raw bytes of the image. For animated images, this is the first frame.
667
    ///
668
    /// Fails with `ImageTooLarge` if `buf` has length different than `output_buffer_size()`
669
6.79k
    pub fn read_image(&mut self, buf: &mut [u8]) -> Result<(), DecodingError> {
670
6.79k
        if Some(buf.len()) != self.output_buffer_size() {
671
0
            return Err(DecodingError::ImageTooLarge);
672
6.79k
        }
673
674
6.79k
        if self.is_animated() {
675
718
            let saved = std::mem::take(&mut self.animation);
676
718
            self.animation.next_frame_start =
677
718
                self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
678
718
            let result = self.read_frame(buf);
679
718
            self.animation = saved;
680
718
            result?;
681
6.07k
        } else if let Some(range) = self.chunks.get(&WebPRiffChunk::VP8L) {
682
3.55k
            let mut decoder = LosslessDecoder::new(range_reader(&mut self.r, range.clone())?);
683
684
3.55k
            if self.has_alpha {
685
2.23k
                decoder.decode_frame(self.width, self.height, false, buf)?;
686
            } else {
687
1.32k
                let mut data = vec![0; self.width as usize * self.height as usize * 4];
688
1.32k
                decoder.decode_frame(self.width, self.height, false, &mut data)?;
689
1.28G
                for (rgba_val, chunk) in data.chunks_exact(4).zip(buf.chunks_exact_mut(3)) {
690
1.28G
                    chunk.copy_from_slice(&rgba_val[..3]);
691
1.28G
                }
692
            }
693
        } else {
694
2.51k
            let range = self
695
2.51k
                .chunks
696
2.51k
                .get(&WebPRiffChunk::VP8)
697
2.51k
                .ok_or(DecodingError::ChunkMissing)?;
698
2.51k
            let reader = range_reader(&mut self.r, range.start..range.end)?;
699
2.51k
            let frame = Vp8Decoder::decode_frame(reader)?;
700
1.05k
            if u32::from(frame.width) != self.width || u32::from(frame.height) != self.height {
701
22
                return Err(DecodingError::InconsistentImageSizes);
702
1.02k
            }
703
704
1.02k
            if self.has_alpha() {
705
422
                frame.fill_rgba(buf, self.webp_decode_options.lossy_upsampling);
706
707
422
                let range = self
708
422
                    .chunks
709
422
                    .get(&WebPRiffChunk::ALPH)
710
422
                    .ok_or(DecodingError::ChunkMissing)?
711
414
                    .clone();
712
414
                let alpha_chunk = read_alpha_chunk(
713
414
                    &mut range_reader(&mut self.r, range)?,
714
414
                    self.width as u16,
715
414
                    self.height as u16,
716
349
                )?;
717
718
7.53k
                for y in 0..frame.height {
719
1.70M
                    for x in 0..frame.width {
720
1.70M
                        let predictor: u8 = get_alpha_predictor(
721
1.70M
                            x.into(),
722
1.70M
                            y.into(),
723
1.70M
                            frame.width.into(),
724
1.70M
                            alpha_chunk.filtering_method,
725
1.70M
                            buf,
726
1.70M
                        );
727
1.70M
728
1.70M
                        let alpha_index =
729
1.70M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
730
1.70M
                        let buffer_index = alpha_index * 4 + 3;
731
1.70M
732
1.70M
                        buf[buffer_index] = predictor.wrapping_add(alpha_chunk.data[alpha_index]);
733
1.70M
                    }
734
                }
735
607
            } else {
736
607
                frame.fill_rgb(buf, self.webp_decode_options.lossy_upsampling);
737
607
            }
738
        }
739
740
3.23k
        Ok(())
741
6.79k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_image
Line
Count
Source
669
6.79k
    pub fn read_image(&mut self, buf: &mut [u8]) -> Result<(), DecodingError> {
670
6.79k
        if Some(buf.len()) != self.output_buffer_size() {
671
0
            return Err(DecodingError::ImageTooLarge);
672
6.79k
        }
673
674
6.79k
        if self.is_animated() {
675
718
            let saved = std::mem::take(&mut self.animation);
676
718
            self.animation.next_frame_start =
677
718
                self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
678
718
            let result = self.read_frame(buf);
679
718
            self.animation = saved;
680
718
            result?;
681
6.07k
        } else if let Some(range) = self.chunks.get(&WebPRiffChunk::VP8L) {
682
3.55k
            let mut decoder = LosslessDecoder::new(range_reader(&mut self.r, range.clone())?);
683
684
3.55k
            if self.has_alpha {
685
2.23k
                decoder.decode_frame(self.width, self.height, false, buf)?;
686
            } else {
687
1.32k
                let mut data = vec![0; self.width as usize * self.height as usize * 4];
688
1.32k
                decoder.decode_frame(self.width, self.height, false, &mut data)?;
689
1.28G
                for (rgba_val, chunk) in data.chunks_exact(4).zip(buf.chunks_exact_mut(3)) {
690
1.28G
                    chunk.copy_from_slice(&rgba_val[..3]);
691
1.28G
                }
692
            }
693
        } else {
694
2.51k
            let range = self
695
2.51k
                .chunks
696
2.51k
                .get(&WebPRiffChunk::VP8)
697
2.51k
                .ok_or(DecodingError::ChunkMissing)?;
698
2.51k
            let reader = range_reader(&mut self.r, range.start..range.end)?;
699
2.51k
            let frame = Vp8Decoder::decode_frame(reader)?;
700
1.05k
            if u32::from(frame.width) != self.width || u32::from(frame.height) != self.height {
701
22
                return Err(DecodingError::InconsistentImageSizes);
702
1.02k
            }
703
704
1.02k
            if self.has_alpha() {
705
422
                frame.fill_rgba(buf, self.webp_decode_options.lossy_upsampling);
706
707
422
                let range = self
708
422
                    .chunks
709
422
                    .get(&WebPRiffChunk::ALPH)
710
422
                    .ok_or(DecodingError::ChunkMissing)?
711
414
                    .clone();
712
414
                let alpha_chunk = read_alpha_chunk(
713
414
                    &mut range_reader(&mut self.r, range)?,
714
414
                    self.width as u16,
715
414
                    self.height as u16,
716
349
                )?;
717
718
7.53k
                for y in 0..frame.height {
719
1.70M
                    for x in 0..frame.width {
720
1.70M
                        let predictor: u8 = get_alpha_predictor(
721
1.70M
                            x.into(),
722
1.70M
                            y.into(),
723
1.70M
                            frame.width.into(),
724
1.70M
                            alpha_chunk.filtering_method,
725
1.70M
                            buf,
726
1.70M
                        );
727
1.70M
728
1.70M
                        let alpha_index =
729
1.70M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
730
1.70M
                        let buffer_index = alpha_index * 4 + 3;
731
1.70M
732
1.70M
                        buf[buffer_index] = predictor.wrapping_add(alpha_chunk.data[alpha_index]);
733
1.70M
                    }
734
                }
735
607
            } else {
736
607
                frame.fill_rgb(buf, self.webp_decode_options.lossy_upsampling);
737
607
            }
738
        }
739
740
3.23k
        Ok(())
741
6.79k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::read_image
742
743
    /// Reads the next frame of the animation.
744
    ///
745
    /// The frame contents are written into `buf` and the method returns the duration of the frame
746
    /// in milliseconds. If there are no more frames, the method returns
747
    /// `DecodingError::NoMoreFrames` and `buf` is left unchanged.
748
    ///
749
    /// # Panics
750
    ///
751
    /// Panics if the image is not animated.
752
718
    pub fn read_frame(&mut self, buf: &mut [u8]) -> Result<u32, DecodingError> {
753
718
        assert!(self.is_animated());
754
718
        assert_eq!(Some(buf.len()), self.output_buffer_size());
755
756
718
        if self.animation.next_frame == self.num_frames {
757
0
            return Err(DecodingError::NoMoreFrames);
758
718
        }
759
760
718
        let ImageKind::Extended(info) = &self.kind else {
761
0
            unreachable!()
762
        };
763
764
718
        self.r
765
718
            .seek(io::SeekFrom::Start(self.animation.next_frame_start))?;
766
767
718
        let anmf_size = match read_chunk_header(&mut self.r)? {
768
718
            (WebPRiffChunk::ANMF, size, _) if size >= 32 => size,
769
16
            _ => return Err(DecodingError::ChunkHeaderInvalid(*b"ANMF")),
770
        };
771
772
        // Read ANMF chunk
773
702
        let frame_x = extended::read_3_bytes(&mut self.r)? * 2;
774
702
        let frame_y = extended::read_3_bytes(&mut self.r)? * 2;
775
702
        let frame_width = extended::read_3_bytes(&mut self.r)? + 1;
776
702
        let frame_height = extended::read_3_bytes(&mut self.r)? + 1;
777
702
        if frame_width > 16384 || frame_height > 16384 {
778
30
            return Err(DecodingError::ImageTooLarge);
779
672
        }
780
672
        if frame_x + frame_width > self.width || frame_y + frame_height > self.height {
781
47
            return Err(DecodingError::FrameOutsideImage);
782
625
        }
783
625
        let duration = extended::read_3_bytes(&mut self.r)?;
784
625
        let frame_info = self.r.read_u8()?;
785
625
        let use_alpha_blending = frame_info & 0b00000010 == 0;
786
625
        let dispose = frame_info & 0b00000001 != 0;
787
788
625
        let clear_color = if self.animation.dispose_next_frame {
789
625
            info.background_color
790
        } else {
791
0
            None
792
        };
793
794
        // Read normal bitstream now
795
625
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
796
625
        if chunk_size_rounded + 24 > anmf_size {
797
35
            return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
798
590
        }
799
800
590
        let (frame, frame_has_alpha): (Vec<u8>, bool) = match chunk {
801
            WebPRiffChunk::VP8 => {
802
20
                let reader = (&mut self.r).take(chunk_size);
803
20
                let raw_frame = Vp8Decoder::decode_frame(reader)?;
804
19
                if u32::from(raw_frame.width) != frame_width
805
16
                    || u32::from(raw_frame.height) != frame_height
806
                {
807
5
                    return Err(DecodingError::InconsistentImageSizes);
808
14
                }
809
14
                let mut rgb_frame = vec![0; frame_width as usize * frame_height as usize * 3];
810
14
                raw_frame.fill_rgb(&mut rgb_frame, self.webp_decode_options.lossy_upsampling);
811
14
                (rgb_frame, false)
812
            }
813
            WebPRiffChunk::VP8L => {
814
64
                let reader = (&mut self.r).take(chunk_size);
815
64
                let mut lossless_decoder = LosslessDecoder::new(reader);
816
64
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
817
64
                lossless_decoder.decode_frame(frame_width, frame_height, false, &mut rgba_frame)?;
818
60
                (rgba_frame, true)
819
            }
820
            WebPRiffChunk::ALPH => {
821
500
                if chunk_size_rounded + 32 > anmf_size {
822
1
                    return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
823
499
                }
824
825
                // read alpha
826
499
                let next_chunk_start = self.r.stream_position()? + chunk_size_rounded;
827
499
                let mut reader = (&mut self.r).take(chunk_size);
828
129
                let alpha_chunk =
829
499
                    read_alpha_chunk(&mut reader, frame_width as u16, frame_height as u16)?;
830
831
                // read opaque
832
129
                self.r.seek(io::SeekFrom::Start(next_chunk_start))?;
833
129
                let (next_chunk, next_chunk_size, _) = read_chunk_header(&mut self.r)?;
834
119
                if chunk_size + next_chunk_size + 32 > anmf_size {
835
9
                    return Err(DecodingError::ChunkHeaderInvalid(next_chunk.to_fourcc()));
836
110
                }
837
838
110
                let frame = Vp8Decoder::decode_frame((&mut self.r).take(next_chunk_size))?;
839
840
106
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
841
106
                frame.fill_rgba(&mut rgba_frame, self.webp_decode_options.lossy_upsampling);
842
843
11.0k
                for y in 0..frame.height {
844
1.89M
                    for x in 0..frame.width {
845
1.89M
                        let predictor: u8 = get_alpha_predictor(
846
1.89M
                            x.into(),
847
1.89M
                            y.into(),
848
1.89M
                            frame.width.into(),
849
1.89M
                            alpha_chunk.filtering_method,
850
1.89M
                            &rgba_frame,
851
1.89M
                        );
852
1.89M
853
1.89M
                        let alpha_index =
854
1.89M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
855
1.89M
                        let buffer_index = alpha_index * 4 + 3;
856
1.89M
857
1.89M
                        rgba_frame[buffer_index] =
858
1.89M
                            predictor.wrapping_add(alpha_chunk.data[alpha_index]);
859
1.89M
                    }
860
                }
861
862
106
                (rgba_frame, true)
863
            }
864
6
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
865
        };
866
867
        // fill starting canvas with clear color
868
180
        if self.animation.canvas.is_none() {
869
180
            self.animation.canvas = {
870
180
                let mut canvas = vec![0; (self.width * self.height * 4) as usize];
871
180
                if let Some(color) = info.background_color.as_ref() {
872
0
                    canvas
873
0
                        .chunks_exact_mut(4)
874
0
                        .for_each(|c| c.copy_from_slice(color))
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_frame::{closure#0}
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::read_frame::{closure#0}
875
180
                }
876
180
                Some(canvas)
877
            }
878
0
        }
879
180
        extended::composite_frame(
880
180
            self.animation.canvas.as_mut().unwrap(),
881
180
            self.width,
882
180
            self.height,
883
180
            clear_color,
884
180
            &frame,
885
180
            frame_x,
886
180
            frame_y,
887
180
            frame_width,
888
180
            frame_height,
889
180
            frame_has_alpha,
890
180
            use_alpha_blending,
891
180
            self.animation.previous_frame_width,
892
180
            self.animation.previous_frame_height,
893
180
            self.animation.previous_frame_x_offset,
894
180
            self.animation.previous_frame_y_offset,
895
        );
896
897
180
        self.animation.previous_frame_width = frame_width;
898
180
        self.animation.previous_frame_height = frame_height;
899
180
        self.animation.previous_frame_x_offset = frame_x;
900
180
        self.animation.previous_frame_y_offset = frame_y;
901
902
180
        self.animation.dispose_next_frame = dispose;
903
180
        self.animation.next_frame_start += anmf_size + 8;
904
180
        self.animation.next_frame += 1;
905
906
180
        if self.has_alpha() {
907
159
            buf.copy_from_slice(self.animation.canvas.as_ref().unwrap());
908
159
        } else {
909
833M
            for (b, c) in buf
910
21
                .chunks_exact_mut(3)
911
21
                .zip(self.animation.canvas.as_ref().unwrap().chunks_exact(4))
912
833M
            {
913
833M
                b.copy_from_slice(&c[..3]);
914
833M
            }
915
        }
916
917
180
        Ok(duration)
918
718
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_frame
Line
Count
Source
752
718
    pub fn read_frame(&mut self, buf: &mut [u8]) -> Result<u32, DecodingError> {
753
718
        assert!(self.is_animated());
754
718
        assert_eq!(Some(buf.len()), self.output_buffer_size());
755
756
718
        if self.animation.next_frame == self.num_frames {
757
0
            return Err(DecodingError::NoMoreFrames);
758
718
        }
759
760
718
        let ImageKind::Extended(info) = &self.kind else {
761
0
            unreachable!()
762
        };
763
764
718
        self.r
765
718
            .seek(io::SeekFrom::Start(self.animation.next_frame_start))?;
766
767
718
        let anmf_size = match read_chunk_header(&mut self.r)? {
768
718
            (WebPRiffChunk::ANMF, size, _) if size >= 32 => size,
769
16
            _ => return Err(DecodingError::ChunkHeaderInvalid(*b"ANMF")),
770
        };
771
772
        // Read ANMF chunk
773
702
        let frame_x = extended::read_3_bytes(&mut self.r)? * 2;
774
702
        let frame_y = extended::read_3_bytes(&mut self.r)? * 2;
775
702
        let frame_width = extended::read_3_bytes(&mut self.r)? + 1;
776
702
        let frame_height = extended::read_3_bytes(&mut self.r)? + 1;
777
702
        if frame_width > 16384 || frame_height > 16384 {
778
30
            return Err(DecodingError::ImageTooLarge);
779
672
        }
780
672
        if frame_x + frame_width > self.width || frame_y + frame_height > self.height {
781
47
            return Err(DecodingError::FrameOutsideImage);
782
625
        }
783
625
        let duration = extended::read_3_bytes(&mut self.r)?;
784
625
        let frame_info = self.r.read_u8()?;
785
625
        let use_alpha_blending = frame_info & 0b00000010 == 0;
786
625
        let dispose = frame_info & 0b00000001 != 0;
787
788
625
        let clear_color = if self.animation.dispose_next_frame {
789
625
            info.background_color
790
        } else {
791
0
            None
792
        };
793
794
        // Read normal bitstream now
795
625
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
796
625
        if chunk_size_rounded + 24 > anmf_size {
797
35
            return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
798
590
        }
799
800
590
        let (frame, frame_has_alpha): (Vec<u8>, bool) = match chunk {
801
            WebPRiffChunk::VP8 => {
802
20
                let reader = (&mut self.r).take(chunk_size);
803
20
                let raw_frame = Vp8Decoder::decode_frame(reader)?;
804
19
                if u32::from(raw_frame.width) != frame_width
805
16
                    || u32::from(raw_frame.height) != frame_height
806
                {
807
5
                    return Err(DecodingError::InconsistentImageSizes);
808
14
                }
809
14
                let mut rgb_frame = vec![0; frame_width as usize * frame_height as usize * 3];
810
14
                raw_frame.fill_rgb(&mut rgb_frame, self.webp_decode_options.lossy_upsampling);
811
14
                (rgb_frame, false)
812
            }
813
            WebPRiffChunk::VP8L => {
814
64
                let reader = (&mut self.r).take(chunk_size);
815
64
                let mut lossless_decoder = LosslessDecoder::new(reader);
816
64
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
817
64
                lossless_decoder.decode_frame(frame_width, frame_height, false, &mut rgba_frame)?;
818
60
                (rgba_frame, true)
819
            }
820
            WebPRiffChunk::ALPH => {
821
500
                if chunk_size_rounded + 32 > anmf_size {
822
1
                    return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
823
499
                }
824
825
                // read alpha
826
499
                let next_chunk_start = self.r.stream_position()? + chunk_size_rounded;
827
499
                let mut reader = (&mut self.r).take(chunk_size);
828
129
                let alpha_chunk =
829
499
                    read_alpha_chunk(&mut reader, frame_width as u16, frame_height as u16)?;
830
831
                // read opaque
832
129
                self.r.seek(io::SeekFrom::Start(next_chunk_start))?;
833
129
                let (next_chunk, next_chunk_size, _) = read_chunk_header(&mut self.r)?;
834
119
                if chunk_size + next_chunk_size + 32 > anmf_size {
835
9
                    return Err(DecodingError::ChunkHeaderInvalid(next_chunk.to_fourcc()));
836
110
                }
837
838
110
                let frame = Vp8Decoder::decode_frame((&mut self.r).take(next_chunk_size))?;
839
840
106
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
841
106
                frame.fill_rgba(&mut rgba_frame, self.webp_decode_options.lossy_upsampling);
842
843
11.0k
                for y in 0..frame.height {
844
1.89M
                    for x in 0..frame.width {
845
1.89M
                        let predictor: u8 = get_alpha_predictor(
846
1.89M
                            x.into(),
847
1.89M
                            y.into(),
848
1.89M
                            frame.width.into(),
849
1.89M
                            alpha_chunk.filtering_method,
850
1.89M
                            &rgba_frame,
851
1.89M
                        );
852
1.89M
853
1.89M
                        let alpha_index =
854
1.89M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
855
1.89M
                        let buffer_index = alpha_index * 4 + 3;
856
1.89M
857
1.89M
                        rgba_frame[buffer_index] =
858
1.89M
                            predictor.wrapping_add(alpha_chunk.data[alpha_index]);
859
1.89M
                    }
860
                }
861
862
106
                (rgba_frame, true)
863
            }
864
6
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
865
        };
866
867
        // fill starting canvas with clear color
868
180
        if self.animation.canvas.is_none() {
869
180
            self.animation.canvas = {
870
180
                let mut canvas = vec![0; (self.width * self.height * 4) as usize];
871
180
                if let Some(color) = info.background_color.as_ref() {
872
0
                    canvas
873
0
                        .chunks_exact_mut(4)
874
0
                        .for_each(|c| c.copy_from_slice(color))
875
180
                }
876
180
                Some(canvas)
877
            }
878
0
        }
879
180
        extended::composite_frame(
880
180
            self.animation.canvas.as_mut().unwrap(),
881
180
            self.width,
882
180
            self.height,
883
180
            clear_color,
884
180
            &frame,
885
180
            frame_x,
886
180
            frame_y,
887
180
            frame_width,
888
180
            frame_height,
889
180
            frame_has_alpha,
890
180
            use_alpha_blending,
891
180
            self.animation.previous_frame_width,
892
180
            self.animation.previous_frame_height,
893
180
            self.animation.previous_frame_x_offset,
894
180
            self.animation.previous_frame_y_offset,
895
        );
896
897
180
        self.animation.previous_frame_width = frame_width;
898
180
        self.animation.previous_frame_height = frame_height;
899
180
        self.animation.previous_frame_x_offset = frame_x;
900
180
        self.animation.previous_frame_y_offset = frame_y;
901
902
180
        self.animation.dispose_next_frame = dispose;
903
180
        self.animation.next_frame_start += anmf_size + 8;
904
180
        self.animation.next_frame += 1;
905
906
180
        if self.has_alpha() {
907
159
            buf.copy_from_slice(self.animation.canvas.as_ref().unwrap());
908
159
        } else {
909
833M
            for (b, c) in buf
910
21
                .chunks_exact_mut(3)
911
21
                .zip(self.animation.canvas.as_ref().unwrap().chunks_exact(4))
912
833M
            {
913
833M
                b.copy_from_slice(&c[..3]);
914
833M
            }
915
        }
916
917
180
        Ok(duration)
918
718
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::read_frame
919
920
    /// Resets the animation to the first frame.
921
    ///
922
    /// # Panics
923
    ///
924
    /// Panics if the image is not animated.
925
0
    pub fn reset_animation(&mut self) {
926
0
        assert!(self.is_animated());
927
928
0
        self.animation.next_frame = 0;
929
0
        self.animation.next_frame_start = self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
930
0
        self.animation.dispose_next_frame = true;
931
0
    }
932
933
    /// Sets the upsampling method that is used in lossy decoding
934
0
    pub fn set_lossy_upsampling(&mut self, upsampling_method: UpsamplingMethod) {
935
0
        self.webp_decode_options.lossy_upsampling = upsampling_method;
936
0
    }
937
}
938
939
6.48k
pub(crate) fn range_reader<R: BufRead + Seek>(
940
6.48k
    mut r: R,
941
6.48k
    range: Range<u64>,
942
6.48k
) -> Result<impl BufRead, DecodingError> {
943
6.48k
    r.seek(io::SeekFrom::Start(range.start))?;
944
6.48k
    Ok(r.take(range.end - range.start))
945
6.48k
}
image_webp::decoder::range_reader::<&mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
939
6.48k
pub(crate) fn range_reader<R: BufRead + Seek>(
940
6.48k
    mut r: R,
941
6.48k
    range: Range<u64>,
942
6.48k
) -> Result<impl BufRead, DecodingError> {
943
6.48k
    r.seek(io::SeekFrom::Start(range.start))?;
944
6.48k
    Ok(r.take(range.end - range.start))
945
6.48k
}
Unexecuted instantiation: image_webp::decoder::range_reader::<_>
946
947
45.0k
pub(crate) fn read_fourcc<R: BufRead>(mut r: R) -> Result<WebPRiffChunk, DecodingError> {
948
45.0k
    let mut chunk_fourcc = [0; 4];
949
45.0k
    r.read_exact(&mut chunk_fourcc)?;
950
44.2k
    Ok(WebPRiffChunk::from_fourcc(chunk_fourcc))
951
45.0k
}
image_webp::decoder::read_fourcc::<&mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
947
7.14k
pub(crate) fn read_fourcc<R: BufRead>(mut r: R) -> Result<WebPRiffChunk, DecodingError> {
948
7.14k
    let mut chunk_fourcc = [0; 4];
949
7.14k
    r.read_exact(&mut chunk_fourcc)?;
950
7.14k
    Ok(WebPRiffChunk::from_fourcc(chunk_fourcc))
951
7.14k
}
image_webp::decoder::read_fourcc::<&mut &mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
947
37.9k
pub(crate) fn read_fourcc<R: BufRead>(mut r: R) -> Result<WebPRiffChunk, DecodingError> {
948
37.9k
    let mut chunk_fourcc = [0; 4];
949
37.9k
    r.read_exact(&mut chunk_fourcc)?;
950
37.1k
    Ok(WebPRiffChunk::from_fourcc(chunk_fourcc))
951
37.9k
}
Unexecuted instantiation: image_webp::decoder::read_fourcc::<_>
952
953
37.9k
pub(crate) fn read_chunk_header<R: BufRead>(
954
37.9k
    mut r: R,
955
37.9k
) -> Result<(WebPRiffChunk, u64, u64), DecodingError> {
956
37.9k
    let chunk = read_fourcc(&mut r)?;
957
37.1k
    let chunk_size = r.read_u32::<LittleEndian>()?;
958
37.0k
    let chunk_size_rounded = chunk_size.saturating_add(chunk_size & 1);
959
37.0k
    Ok((chunk, chunk_size.into(), chunk_size_rounded.into()))
960
37.9k
}
image_webp::decoder::read_chunk_header::<&mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
953
37.9k
pub(crate) fn read_chunk_header<R: BufRead>(
954
37.9k
    mut r: R,
955
37.9k
) -> Result<(WebPRiffChunk, u64, u64), DecodingError> {
956
37.9k
    let chunk = read_fourcc(&mut r)?;
957
37.1k
    let chunk_size = r.read_u32::<LittleEndian>()?;
958
37.0k
    let chunk_size_rounded = chunk_size.saturating_add(chunk_size & 1);
959
37.0k
    Ok((chunk, chunk_size.into(), chunk_size_rounded.into()))
960
37.9k
}
Unexecuted instantiation: image_webp::decoder::read_chunk_header::<_>
961
962
#[cfg(test)]
963
mod tests {
964
    use super::*;
965
    const RGB_BPP: usize = 3;
966
967
    #[test]
968
    fn add_with_overflow_size() {
969
        let bytes = vec![
970
            0x52, 0x49, 0x46, 0x46, 0xaf, 0x37, 0x80, 0x47, 0x57, 0x45, 0x42, 0x50, 0x6c, 0x64,
971
            0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xfb, 0x7e, 0x73, 0x00, 0x06, 0x00, 0x00, 0x00,
972
            0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65,
973
            0x40, 0xfb, 0xff, 0xff, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65,
974
            0x00, 0x00, 0x00, 0x00, 0x62, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49,
975
            0x49, 0x54, 0x55, 0x50, 0x4c, 0x54, 0x59, 0x50, 0x45, 0x33, 0x37, 0x44, 0x4d, 0x46,
976
        ];
977
978
        let data = std::io::Cursor::new(bytes);
979
980
        let _ = WebPDecoder::new(data);
981
    }
982
983
    #[test]
984
    fn decode_2x2_single_color_image() {
985
        // Image data created from imagemagick and output of xxd:
986
        // $ convert -size 2x2 xc:#f00 red.webp
987
        // $ xxd -g 1 red.webp | head
988
989
        const NUM_PIXELS: usize = 2 * 2 * RGB_BPP;
990
        // 2x2 red pixel image
991
        let bytes = [
992
            0x52, 0x49, 0x46, 0x46, 0x3c, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50,
993
            0x38, 0x20, 0x30, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x02, 0x00,
994
            0x02, 0x00, 0x02, 0x00, 0x34, 0x25, 0xa0, 0x02, 0x74, 0xba, 0x01, 0xf8, 0x00, 0x03,
995
            0xb0, 0x00, 0xfe, 0xf0, 0xc4, 0x0b, 0xff, 0x20, 0xb9, 0x61, 0x75, 0xc8, 0xd7, 0xff,
996
            0x20, 0x3f, 0xe4, 0x07, 0xfc, 0x80, 0xff, 0xf8, 0xf2, 0x00, 0x00, 0x00,
997
        ];
998
999
        let mut data = [0; NUM_PIXELS];
1000
        let mut decoder = WebPDecoder::new(std::io::Cursor::new(bytes)).unwrap();
1001
        decoder.read_image(&mut data).unwrap();
1002
1003
        // All pixels are the same value
1004
        let first_pixel = &data[..RGB_BPP];
1005
        assert!(data.chunks_exact(3).all(|ch| ch.iter().eq(first_pixel)));
1006
    }
1007
1008
    #[test]
1009
    fn decode_3x3_single_color_image() {
1010
        // Test that any odd pixel "tail" is decoded properly
1011
1012
        const NUM_PIXELS: usize = 3 * 3 * RGB_BPP;
1013
        // 3x3 red pixel image
1014
        let bytes = [
1015
            0x52, 0x49, 0x46, 0x46, 0x3c, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50,
1016
            0x38, 0x20, 0x30, 0x00, 0x00, 0x00, 0xd0, 0x01, 0x00, 0x9d, 0x01, 0x2a, 0x03, 0x00,
1017
            0x03, 0x00, 0x02, 0x00, 0x34, 0x25, 0xa0, 0x02, 0x74, 0xba, 0x01, 0xf8, 0x00, 0x03,
1018
            0xb0, 0x00, 0xfe, 0xf0, 0xc4, 0x0b, 0xff, 0x20, 0xb9, 0x61, 0x75, 0xc8, 0xd7, 0xff,
1019
            0x20, 0x3f, 0xe4, 0x07, 0xfc, 0x80, 0xff, 0xf8, 0xf2, 0x00, 0x00, 0x00,
1020
        ];
1021
1022
        let mut data = [0; NUM_PIXELS];
1023
        let mut decoder = WebPDecoder::new(std::io::Cursor::new(bytes)).unwrap();
1024
        decoder.read_image(&mut data).unwrap();
1025
1026
        // All pixels are the same value
1027
        let first_pixel = &data[..RGB_BPP];
1028
        assert!(data.chunks_exact(3).all(|ch| ch.iter().eq(first_pixel)));
1029
    }
1030
}