Coverage Report

Created: 2026-04-12 07:31

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
46.1k
    pub(crate) const fn from_fourcc(chunk_fourcc: [u8; 4]) -> Self {
190
46.1k
        match &chunk_fourcc {
191
7.60k
            b"RIFF" => Self::RIFF,
192
7.61k
            b"WEBP" => Self::WEBP,
193
2.84k
            b"VP8 " => Self::VP8,
194
4.28k
            b"VP8L" => Self::VP8L,
195
2.23k
            b"VP8X" => Self::VP8X,
196
1.28k
            b"ANIM" => Self::ANIM,
197
3.96k
            b"ANMF" => Self::ANMF,
198
2.19k
            b"ALPH" => Self::ALPH,
199
1.71k
            b"ICCP" => Self::ICCP,
200
923
            b"EXIF" => Self::EXIF,
201
535
            b"XMP " => Self::XMP,
202
10.9k
            _ => Self::Unknown(chunk_fourcc),
203
        }
204
46.1k
    }
205
206
104
    pub(crate) const fn to_fourcc(self) -> [u8; 4] {
207
104
        match self {
208
2
            Self::RIFF => *b"RIFF",
209
2
            Self::WEBP => *b"WEBP",
210
2
            Self::VP8 => *b"VP8 ",
211
2
            Self::VP8L => *b"VP8L",
212
2
            Self::VP8X => *b"VP8X",
213
2
            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
83
            Self::Unknown(fourcc) => fourcc,
220
        }
221
104
    }
222
223
18.0k
    pub(crate) const fn is_unknown(self) -> bool {
224
18.0k
        matches!(self, Self::Unknown(_))
225
18.0k
    }
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
8.35k
    fn default() -> Self {
252
8.35k
        Self {
253
8.35k
            next_frame: 0,
254
8.35k
            next_frame_start: 0,
255
8.35k
            dispose_next_frame: true,
256
8.35k
            previous_frame_width: 0,
257
8.35k
            previous_frame_height: 0,
258
8.35k
            previous_frame_x_offset: 0,
259
8.35k
            previous_frame_y_offset: 0,
260
8.35k
            canvas: None,
261
8.35k
        }
262
8.35k
    }
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.59k
    fn default() -> Self {
286
7.59k
        Self {
287
7.59k
            lossy_upsampling: UpsamplingMethod::Bilinear,
288
7.59k
        }
289
7.59k
    }
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.59k
    pub fn new(r: R) -> Result<Self, DecodingError> {
337
7.59k
        Self::new_with_options(r, WebPDecodeOptions::default())
338
7.59k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::new
Line
Count
Source
336
7.59k
    pub fn new(r: R) -> Result<Self, DecodingError> {
337
7.59k
        Self::new_with_options(r, WebPDecodeOptions::default())
338
7.59k
    }
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.59k
    pub fn new_with_options(
343
7.59k
        r: R,
344
7.59k
        webp_decode_options: WebPDecodeOptions,
345
7.59k
    ) -> Result<Self, DecodingError> {
346
7.59k
        let mut decoder = Self {
347
7.59k
            r,
348
7.59k
            width: 0,
349
7.59k
            height: 0,
350
7.59k
            num_frames: 0,
351
7.59k
            kind: ImageKind::Lossy,
352
7.59k
            chunks: HashMap::new(),
353
7.59k
            animation: Default::default(),
354
7.59k
            memory_limit: usize::MAX,
355
7.59k
            is_lossy: false,
356
7.59k
            has_alpha: false,
357
7.59k
            loop_count: LoopCount::Times(NonZeroU16::new(1).unwrap()),
358
7.59k
            loop_duration: 0,
359
7.59k
            webp_decode_options,
360
7.59k
        };
361
7.59k
        decoder.read_data()?;
362
7.22k
        Ok(decoder)
363
7.59k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::new_with_options
Line
Count
Source
342
7.59k
    pub fn new_with_options(
343
7.59k
        r: R,
344
7.59k
        webp_decode_options: WebPDecodeOptions,
345
7.59k
    ) -> Result<Self, DecodingError> {
346
7.59k
        let mut decoder = Self {
347
7.59k
            r,
348
7.59k
            width: 0,
349
7.59k
            height: 0,
350
7.59k
            num_frames: 0,
351
7.59k
            kind: ImageKind::Lossy,
352
7.59k
            chunks: HashMap::new(),
353
7.59k
            animation: Default::default(),
354
7.59k
            memory_limit: usize::MAX,
355
7.59k
            is_lossy: false,
356
7.59k
            has_alpha: false,
357
7.59k
            loop_count: LoopCount::Times(NonZeroU16::new(1).unwrap()),
358
7.59k
            loop_duration: 0,
359
7.59k
            webp_decode_options,
360
7.59k
        };
361
7.59k
        decoder.read_data()?;
362
7.22k
        Ok(decoder)
363
7.59k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::new_with_options
364
365
7.59k
    fn read_data(&mut self) -> Result<(), DecodingError> {
366
7.59k
        let (WebPRiffChunk::RIFF, riff_size, _) = read_chunk_header(&mut self.r)? else {
367
2
            return Err(DecodingError::ChunkHeaderInvalid(*b"RIFF"));
368
        };
369
370
7.59k
        match &read_fourcc(&mut self.r)? {
371
7.59k
            WebPRiffChunk::WEBP => {}
372
0
            fourcc => return Err(DecodingError::WebpSignatureInvalid(fourcc.to_fourcc())),
373
        }
374
375
7.59k
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
376
7.59k
        let start = self.r.stream_position()?;
377
378
7.59k
        match chunk {
379
            WebPRiffChunk::VP8 => {
380
2.02k
                let tag = self.r.read_u24::<LittleEndian>()?;
381
382
2.01k
                let keyframe = tag & 1 == 0;
383
2.01k
                if !keyframe {
384
1
                    return Err(DecodingError::UnsupportedFeature(
385
1
                        "Non-keyframe frames".to_owned(),
386
1
                    ));
387
2.01k
                }
388
389
2.01k
                let mut tag = [0u8; 3];
390
2.01k
                self.r.read_exact(&mut tag)?;
391
2.01k
                if tag != [0x9d, 0x01, 0x2a] {
392
1
                    return Err(DecodingError::Vp8MagicInvalid(tag));
393
2.01k
                }
394
395
2.01k
                let w = self.r.read_u16::<LittleEndian>()?;
396
2.01k
                let h = self.r.read_u16::<LittleEndian>()?;
397
398
2.01k
                self.width = u32::from(w & 0x3FFF);
399
2.01k
                self.height = u32::from(h & 0x3FFF);
400
2.01k
                if self.width == 0 || self.height == 0 {
401
2
                    return Err(DecodingError::InconsistentImageSizes);
402
2.01k
                }
403
404
2.01k
                self.chunks
405
2.01k
                    .insert(WebPRiffChunk::VP8, start..start + chunk_size);
406
2.01k
                self.kind = ImageKind::Lossy;
407
2.01k
                self.is_lossy = true;
408
            }
409
            WebPRiffChunk::VP8L => {
410
3.90k
                let signature = self.r.read_u8()?;
411
3.90k
                if signature != 0x2f {
412
7
                    return Err(DecodingError::LosslessSignatureInvalid(signature));
413
3.89k
                }
414
415
3.89k
                let header = self.r.read_u32::<LittleEndian>()?;
416
3.89k
                let version = header >> 29;
417
3.89k
                if version != 0 {
418
6
                    return Err(DecodingError::VersionNumberInvalid(version as u8));
419
3.88k
                }
420
421
3.88k
                self.width = (1 + header) & 0x3FFF;
422
3.88k
                self.height = (1 + (header >> 14)) & 0x3FFF;
423
3.88k
                self.chunks
424
3.88k
                    .insert(WebPRiffChunk::VP8L, start..start + chunk_size);
425
3.88k
                self.kind = ImageKind::Lossless;
426
3.88k
                self.has_alpha = (header >> 28) & 1 != 0;
427
            }
428
            WebPRiffChunk::VP8X => {
429
1.62k
                let mut info = extended::read_extended_header(&mut self.r)?;
430
1.61k
                self.width = info.canvas_width;
431
1.61k
                self.height = info.canvas_height;
432
433
1.61k
                let mut position = start + chunk_size_rounded;
434
1.61k
                let max_position = position + riff_size.saturating_sub(12);
435
1.61k
                self.r.seek(io::SeekFrom::Start(position))?;
436
437
19.6k
                while position < max_position {
438
18.9k
                    match read_chunk_header(&mut self.r) {
439
18.0k
                        Ok((chunk, chunk_size, chunk_size_rounded)) => {
440
18.0k
                            let range = position + 8..position + 8 + chunk_size;
441
18.0k
                            position += 8 + chunk_size_rounded;
442
443
18.0k
                            if !chunk.is_unknown() {
444
9.51k
                                self.chunks.entry(chunk).or_insert(range);
445
9.51k
                            }
446
447
18.0k
                            if chunk == WebPRiffChunk::ANMF {
448
3.20k
                                self.num_frames += 1;
449
3.20k
                                if chunk_size < 24 {
450
1
                                    return Err(DecodingError::InvalidChunkSize);
451
3.20k
                                }
452
453
3.20k
                                self.r.seek_relative(12)?;
454
3.20k
                                let duration = self.r.read_u32::<LittleEndian>()? & 0xffffff;
455
3.19k
                                self.loop_duration =
456
3.19k
                                    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.19k
                                if !self.is_lossy {
464
2.38k
                                    let (subchunk, ..) = read_chunk_header(&mut self.r)?;
465
2.37k
                                    if let WebPRiffChunk::VP8 | WebPRiffChunk::ALPH = subchunk {
466
598
                                        self.is_lossy = true;
467
1.77k
                                    }
468
2.37k
                                    self.r.seek_relative(chunk_size_rounded as i64 - 24)?;
469
                                } else {
470
817
                                    self.r.seek_relative(chunk_size_rounded as i64 - 16)?;
471
                                }
472
473
3.19k
                                continue;
474
14.8k
                            }
475
476
14.8k
                            self.r.seek_relative(chunk_size_rounded as i64)?;
477
                        }
478
851
                        Err(DecodingError::IoError(e))
479
851
                            if e.kind() == io::ErrorKind::UnexpectedEof =>
480
                        {
481
851
                            break;
482
                        }
483
0
                        Err(e) => return Err(e),
484
                    }
485
                }
486
1.60k
                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.60k
                if info.animation
491
891
                    && (!self.chunks.contains_key(&WebPRiffChunk::ANIM)
492
811
                        || !self.chunks.contains_key(&WebPRiffChunk::ANMF))
493
1.49k
                    || info.exif_metadata && !self.chunks.contains_key(&WebPRiffChunk::EXIF)
494
1.47k
                    || info.xmp_metadata && !self.chunks.contains_key(&WebPRiffChunk::XMP)
495
1.46k
                    || !info.animation
496
682
                        && self.chunks.contains_key(&WebPRiffChunk::VP8)
497
682
                            == self.chunks.contains_key(&WebPRiffChunk::VP8L)
498
                {
499
260
                    return Err(DecodingError::ChunkMissing);
500
1.34k
                }
501
502
                // Decode ANIM chunk.
503
1.34k
                if info.animation {
504
779
                    match self.read_chunk(WebPRiffChunk::ANIM, 6) {
505
778
                        Ok(Some(chunk)) => {
506
778
                            let mut cursor = Cursor::new(chunk);
507
778
                            cursor.read_exact(&mut info.background_color_hint)?;
508
777
                            self.loop_count = match cursor.read_u16::<LittleEndian>()? {
509
313
                                0 => LoopCount::Forever,
510
463
                                n => LoopCount::Times(NonZeroU16::new(n).unwrap()),
511
                            };
512
776
                            self.animation.next_frame_start =
513
776
                                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
565
                }
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.34k
                if let Some(range) = self.chunks.get(&WebPRiffChunk::ANMF).cloned() {
527
778
                    let mut position = range.start + 16;
528
778
                    self.r.seek(io::SeekFrom::Start(position))?;
529
1.48k
                    for _ in 0..2 {
530
1.36k
                        let (subchunk, subchunk_size, subchunk_size_rounded) =
531
1.37k
                            read_chunk_header(&mut self.r)?;
532
1.36k
                        let subrange = position + 8..position + 8 + subchunk_size;
533
1.36k
                        self.chunks.entry(subchunk).or_insert(subrange.clone());
534
535
1.36k
                        position += 8 + subchunk_size_rounded;
536
1.36k
                        if position + 8 > range.end {
537
661
                            break;
538
705
                        }
539
                    }
540
563
                }
541
542
1.32k
                self.has_alpha = info.alpha;
543
1.32k
                self.kind = ImageKind::Extended(info);
544
            }
545
44
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
546
        };
547
548
7.22k
        Ok(())
549
7.59k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_data
Line
Count
Source
365
7.59k
    fn read_data(&mut self) -> Result<(), DecodingError> {
366
7.59k
        let (WebPRiffChunk::RIFF, riff_size, _) = read_chunk_header(&mut self.r)? else {
367
2
            return Err(DecodingError::ChunkHeaderInvalid(*b"RIFF"));
368
        };
369
370
7.59k
        match &read_fourcc(&mut self.r)? {
371
7.59k
            WebPRiffChunk::WEBP => {}
372
0
            fourcc => return Err(DecodingError::WebpSignatureInvalid(fourcc.to_fourcc())),
373
        }
374
375
7.59k
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
376
7.59k
        let start = self.r.stream_position()?;
377
378
7.59k
        match chunk {
379
            WebPRiffChunk::VP8 => {
380
2.02k
                let tag = self.r.read_u24::<LittleEndian>()?;
381
382
2.01k
                let keyframe = tag & 1 == 0;
383
2.01k
                if !keyframe {
384
1
                    return Err(DecodingError::UnsupportedFeature(
385
1
                        "Non-keyframe frames".to_owned(),
386
1
                    ));
387
2.01k
                }
388
389
2.01k
                let mut tag = [0u8; 3];
390
2.01k
                self.r.read_exact(&mut tag)?;
391
2.01k
                if tag != [0x9d, 0x01, 0x2a] {
392
1
                    return Err(DecodingError::Vp8MagicInvalid(tag));
393
2.01k
                }
394
395
2.01k
                let w = self.r.read_u16::<LittleEndian>()?;
396
2.01k
                let h = self.r.read_u16::<LittleEndian>()?;
397
398
2.01k
                self.width = u32::from(w & 0x3FFF);
399
2.01k
                self.height = u32::from(h & 0x3FFF);
400
2.01k
                if self.width == 0 || self.height == 0 {
401
2
                    return Err(DecodingError::InconsistentImageSizes);
402
2.01k
                }
403
404
2.01k
                self.chunks
405
2.01k
                    .insert(WebPRiffChunk::VP8, start..start + chunk_size);
406
2.01k
                self.kind = ImageKind::Lossy;
407
2.01k
                self.is_lossy = true;
408
            }
409
            WebPRiffChunk::VP8L => {
410
3.90k
                let signature = self.r.read_u8()?;
411
3.90k
                if signature != 0x2f {
412
7
                    return Err(DecodingError::LosslessSignatureInvalid(signature));
413
3.89k
                }
414
415
3.89k
                let header = self.r.read_u32::<LittleEndian>()?;
416
3.89k
                let version = header >> 29;
417
3.89k
                if version != 0 {
418
6
                    return Err(DecodingError::VersionNumberInvalid(version as u8));
419
3.88k
                }
420
421
3.88k
                self.width = (1 + header) & 0x3FFF;
422
3.88k
                self.height = (1 + (header >> 14)) & 0x3FFF;
423
3.88k
                self.chunks
424
3.88k
                    .insert(WebPRiffChunk::VP8L, start..start + chunk_size);
425
3.88k
                self.kind = ImageKind::Lossless;
426
3.88k
                self.has_alpha = (header >> 28) & 1 != 0;
427
            }
428
            WebPRiffChunk::VP8X => {
429
1.62k
                let mut info = extended::read_extended_header(&mut self.r)?;
430
1.61k
                self.width = info.canvas_width;
431
1.61k
                self.height = info.canvas_height;
432
433
1.61k
                let mut position = start + chunk_size_rounded;
434
1.61k
                let max_position = position + riff_size.saturating_sub(12);
435
1.61k
                self.r.seek(io::SeekFrom::Start(position))?;
436
437
19.6k
                while position < max_position {
438
18.9k
                    match read_chunk_header(&mut self.r) {
439
18.0k
                        Ok((chunk, chunk_size, chunk_size_rounded)) => {
440
18.0k
                            let range = position + 8..position + 8 + chunk_size;
441
18.0k
                            position += 8 + chunk_size_rounded;
442
443
18.0k
                            if !chunk.is_unknown() {
444
9.51k
                                self.chunks.entry(chunk).or_insert(range);
445
9.51k
                            }
446
447
18.0k
                            if chunk == WebPRiffChunk::ANMF {
448
3.20k
                                self.num_frames += 1;
449
3.20k
                                if chunk_size < 24 {
450
1
                                    return Err(DecodingError::InvalidChunkSize);
451
3.20k
                                }
452
453
3.20k
                                self.r.seek_relative(12)?;
454
3.20k
                                let duration = self.r.read_u32::<LittleEndian>()? & 0xffffff;
455
3.19k
                                self.loop_duration =
456
3.19k
                                    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.19k
                                if !self.is_lossy {
464
2.38k
                                    let (subchunk, ..) = read_chunk_header(&mut self.r)?;
465
2.37k
                                    if let WebPRiffChunk::VP8 | WebPRiffChunk::ALPH = subchunk {
466
598
                                        self.is_lossy = true;
467
1.77k
                                    }
468
2.37k
                                    self.r.seek_relative(chunk_size_rounded as i64 - 24)?;
469
                                } else {
470
817
                                    self.r.seek_relative(chunk_size_rounded as i64 - 16)?;
471
                                }
472
473
3.19k
                                continue;
474
14.8k
                            }
475
476
14.8k
                            self.r.seek_relative(chunk_size_rounded as i64)?;
477
                        }
478
851
                        Err(DecodingError::IoError(e))
479
851
                            if e.kind() == io::ErrorKind::UnexpectedEof =>
480
                        {
481
851
                            break;
482
                        }
483
0
                        Err(e) => return Err(e),
484
                    }
485
                }
486
1.60k
                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.60k
                if info.animation
491
891
                    && (!self.chunks.contains_key(&WebPRiffChunk::ANIM)
492
811
                        || !self.chunks.contains_key(&WebPRiffChunk::ANMF))
493
1.49k
                    || info.exif_metadata && !self.chunks.contains_key(&WebPRiffChunk::EXIF)
494
1.47k
                    || info.xmp_metadata && !self.chunks.contains_key(&WebPRiffChunk::XMP)
495
1.46k
                    || !info.animation
496
682
                        && self.chunks.contains_key(&WebPRiffChunk::VP8)
497
682
                            == self.chunks.contains_key(&WebPRiffChunk::VP8L)
498
                {
499
260
                    return Err(DecodingError::ChunkMissing);
500
1.34k
                }
501
502
                // Decode ANIM chunk.
503
1.34k
                if info.animation {
504
779
                    match self.read_chunk(WebPRiffChunk::ANIM, 6) {
505
778
                        Ok(Some(chunk)) => {
506
778
                            let mut cursor = Cursor::new(chunk);
507
778
                            cursor.read_exact(&mut info.background_color_hint)?;
508
777
                            self.loop_count = match cursor.read_u16::<LittleEndian>()? {
509
313
                                0 => LoopCount::Forever,
510
463
                                n => LoopCount::Times(NonZeroU16::new(n).unwrap()),
511
                            };
512
776
                            self.animation.next_frame_start =
513
776
                                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
565
                }
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.34k
                if let Some(range) = self.chunks.get(&WebPRiffChunk::ANMF).cloned() {
527
778
                    let mut position = range.start + 16;
528
778
                    self.r.seek(io::SeekFrom::Start(position))?;
529
1.48k
                    for _ in 0..2 {
530
1.36k
                        let (subchunk, subchunk_size, subchunk_size_rounded) =
531
1.37k
                            read_chunk_header(&mut self.r)?;
532
1.36k
                        let subrange = position + 8..position + 8 + subchunk_size;
533
1.36k
                        self.chunks.entry(subchunk).or_insert(subrange.clone());
534
535
1.36k
                        position += 8 + subchunk_size_rounded;
536
1.36k
                        if position + 8 > range.end {
537
661
                            break;
538
705
                        }
539
                    }
540
563
                }
541
542
1.32k
                self.has_alpha = info.alpha;
543
1.32k
                self.kind = ImageKind::Extended(info);
544
            }
545
44
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
546
        };
547
548
7.22k
        Ok(())
549
7.59k
    }
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
36.1k
    pub fn dimensions(&self) -> (u32, u32) {
581
36.1k
        (self.width, self.height)
582
36.1k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::dimensions
Line
Count
Source
580
36.1k
    pub fn dimensions(&self) -> (u32, u32) {
581
36.1k
        (self.width, self.height)
582
36.1k
    }
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
38.0k
    pub fn has_alpha(&self) -> bool {
587
38.0k
        self.has_alpha
588
38.0k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::has_alpha
Line
Count
Source
586
38.0k
    pub fn has_alpha(&self) -> bool {
587
38.0k
        self.has_alpha
588
38.0k
    }
Unexecuted instantiation: <image_webp::decoder::WebPDecoder<_>>::has_alpha
589
590
    /// Returns true if the image is animated.
591
7.97k
    pub fn is_animated(&self) -> bool {
592
7.97k
        match &self.kind {
593
5.89k
            ImageKind::Lossy | ImageKind::Lossless => false,
594
2.07k
            ImageKind::Extended(extended) => extended.animation,
595
        }
596
7.97k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::is_animated
Line
Count
Source
591
7.97k
    pub fn is_animated(&self) -> bool {
592
7.97k
        match &self.kind {
593
5.89k
            ImageKind::Lossy | ImageKind::Lossless => false,
594
2.07k
            ImageKind::Extended(extended) => extended.animation,
595
        }
596
7.97k
    }
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
779
    fn read_chunk(
623
779
        &mut self,
624
779
        chunk: WebPRiffChunk,
625
779
        max_size: usize,
626
779
    ) -> Result<Option<Vec<u8>>, DecodingError> {
627
779
        match self.chunks.get(&chunk) {
628
779
            Some(range) => {
629
779
                if range.end - range.start > max_size as u64 {
630
1
                    return Err(DecodingError::MemoryLimitExceeded);
631
778
                }
632
633
778
                self.r.seek(io::SeekFrom::Start(range.start))?;
634
778
                let mut data = vec![0; (range.end - range.start) as usize];
635
778
                self.r.read_exact(&mut data)?;
636
778
                Ok(Some(data))
637
            }
638
0
            None => Ok(None),
639
        }
640
779
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_chunk
Line
Count
Source
622
779
    fn read_chunk(
623
779
        &mut self,
624
779
        chunk: WebPRiffChunk,
625
779
        max_size: usize,
626
779
    ) -> Result<Option<Vec<u8>>, DecodingError> {
627
779
        match self.chunks.get(&chunk) {
628
779
            Some(range) => {
629
779
                if range.end - range.start > max_size as u64 {
630
1
                    return Err(DecodingError::MemoryLimitExceeded);
631
778
                }
632
633
778
                self.r.seek(io::SeekFrom::Start(range.start))?;
634
778
                let mut data = vec![0; (range.end - range.start) as usize];
635
778
                self.r.read_exact(&mut data)?;
636
778
                Ok(Some(data))
637
            }
638
0
            None => Ok(None),
639
        }
640
779
    }
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.97k
    pub fn output_buffer_size(&self) -> Option<usize> {
660
7.97k
        let bytes_per_pixel = if self.has_alpha() { 4 } else { 3 };
661
7.97k
        (self.width as usize)
662
7.97k
            .checked_mul(self.height as usize)?
663
7.97k
            .checked_mul(bytes_per_pixel)
664
7.97k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::output_buffer_size
Line
Count
Source
659
7.97k
    pub fn output_buffer_size(&self) -> Option<usize> {
660
7.97k
        let bytes_per_pixel = if self.has_alpha() { 4 } else { 3 };
661
7.97k
        (self.width as usize)
662
7.97k
            .checked_mul(self.height as usize)?
663
7.97k
            .checked_mul(bytes_per_pixel)
664
7.97k
    }
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
7.21k
    pub fn read_image(&mut self, buf: &mut [u8]) -> Result<(), DecodingError> {
670
7.21k
        if Some(buf.len()) != self.output_buffer_size() {
671
0
            return Err(DecodingError::ImageTooLarge);
672
7.21k
        }
673
674
7.21k
        if self.is_animated() {
675
756
            let saved = std::mem::take(&mut self.animation);
676
756
            self.animation.next_frame_start =
677
756
                self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
678
756
            let result = self.read_frame(buf);
679
756
            self.animation = saved;
680
756
            result?;
681
6.46k
        } else if let Some(range) = self.chunks.get(&WebPRiffChunk::VP8L) {
682
3.89k
            let mut decoder = LosslessDecoder::new(range_reader(&mut self.r, range.clone())?);
683
684
3.89k
            if self.has_alpha {
685
2.48k
                decoder.decode_frame(self.width, self.height, false, buf)?;
686
            } else {
687
1.40k
                let mut data = vec![0; self.width as usize * self.height as usize * 4];
688
1.40k
                decoder.decode_frame(self.width, self.height, false, &mut data)?;
689
1.21G
                for (rgba_val, chunk) in data.chunks_exact(4).zip(buf.chunks_exact_mut(3)) {
690
1.21G
                    chunk.copy_from_slice(&rgba_val[..3]);
691
1.21G
                }
692
            }
693
        } else {
694
2.56k
            let range = self
695
2.56k
                .chunks
696
2.56k
                .get(&WebPRiffChunk::VP8)
697
2.56k
                .ok_or(DecodingError::ChunkMissing)?;
698
2.56k
            let reader = range_reader(&mut self.r, range.start..range.end)?;
699
2.56k
            let frame = Vp8Decoder::decode_frame(reader)?;
700
1.06k
            if u32::from(frame.width) != self.width || u32::from(frame.height) != self.height {
701
19
                return Err(DecodingError::InconsistentImageSizes);
702
1.04k
            }
703
704
1.04k
            if self.has_alpha() {
705
450
                frame.fill_rgba(buf, self.webp_decode_options.lossy_upsampling);
706
707
450
                let range = self
708
450
                    .chunks
709
450
                    .get(&WebPRiffChunk::ALPH)
710
450
                    .ok_or(DecodingError::ChunkMissing)?
711
442
                    .clone();
712
442
                let alpha_chunk = read_alpha_chunk(
713
442
                    &mut range_reader(&mut self.r, range)?,
714
442
                    self.width as u16,
715
442
                    self.height as u16,
716
378
                )?;
717
718
7.75k
                for y in 0..frame.height {
719
1.81M
                    for x in 0..frame.width {
720
1.81M
                        let predictor: u8 = get_alpha_predictor(
721
1.81M
                            x.into(),
722
1.81M
                            y.into(),
723
1.81M
                            frame.width.into(),
724
1.81M
                            alpha_chunk.filtering_method,
725
1.81M
                            buf,
726
1.81M
                        );
727
1.81M
728
1.81M
                        let alpha_index =
729
1.81M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
730
1.81M
                        let buffer_index = alpha_index * 4 + 3;
731
1.81M
732
1.81M
                        buf[buffer_index] = predictor.wrapping_add(alpha_chunk.data[alpha_index]);
733
1.81M
                    }
734
                }
735
592
            } else {
736
592
                frame.fill_rgb(buf, self.webp_decode_options.lossy_upsampling);
737
592
            }
738
        }
739
740
3.48k
        Ok(())
741
7.21k
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_image
Line
Count
Source
669
7.21k
    pub fn read_image(&mut self, buf: &mut [u8]) -> Result<(), DecodingError> {
670
7.21k
        if Some(buf.len()) != self.output_buffer_size() {
671
0
            return Err(DecodingError::ImageTooLarge);
672
7.21k
        }
673
674
7.21k
        if self.is_animated() {
675
756
            let saved = std::mem::take(&mut self.animation);
676
756
            self.animation.next_frame_start =
677
756
                self.chunks.get(&WebPRiffChunk::ANMF).unwrap().start - 8;
678
756
            let result = self.read_frame(buf);
679
756
            self.animation = saved;
680
756
            result?;
681
6.46k
        } else if let Some(range) = self.chunks.get(&WebPRiffChunk::VP8L) {
682
3.89k
            let mut decoder = LosslessDecoder::new(range_reader(&mut self.r, range.clone())?);
683
684
3.89k
            if self.has_alpha {
685
2.48k
                decoder.decode_frame(self.width, self.height, false, buf)?;
686
            } else {
687
1.40k
                let mut data = vec![0; self.width as usize * self.height as usize * 4];
688
1.40k
                decoder.decode_frame(self.width, self.height, false, &mut data)?;
689
1.21G
                for (rgba_val, chunk) in data.chunks_exact(4).zip(buf.chunks_exact_mut(3)) {
690
1.21G
                    chunk.copy_from_slice(&rgba_val[..3]);
691
1.21G
                }
692
            }
693
        } else {
694
2.56k
            let range = self
695
2.56k
                .chunks
696
2.56k
                .get(&WebPRiffChunk::VP8)
697
2.56k
                .ok_or(DecodingError::ChunkMissing)?;
698
2.56k
            let reader = range_reader(&mut self.r, range.start..range.end)?;
699
2.56k
            let frame = Vp8Decoder::decode_frame(reader)?;
700
1.06k
            if u32::from(frame.width) != self.width || u32::from(frame.height) != self.height {
701
19
                return Err(DecodingError::InconsistentImageSizes);
702
1.04k
            }
703
704
1.04k
            if self.has_alpha() {
705
450
                frame.fill_rgba(buf, self.webp_decode_options.lossy_upsampling);
706
707
450
                let range = self
708
450
                    .chunks
709
450
                    .get(&WebPRiffChunk::ALPH)
710
450
                    .ok_or(DecodingError::ChunkMissing)?
711
442
                    .clone();
712
442
                let alpha_chunk = read_alpha_chunk(
713
442
                    &mut range_reader(&mut self.r, range)?,
714
442
                    self.width as u16,
715
442
                    self.height as u16,
716
378
                )?;
717
718
7.75k
                for y in 0..frame.height {
719
1.81M
                    for x in 0..frame.width {
720
1.81M
                        let predictor: u8 = get_alpha_predictor(
721
1.81M
                            x.into(),
722
1.81M
                            y.into(),
723
1.81M
                            frame.width.into(),
724
1.81M
                            alpha_chunk.filtering_method,
725
1.81M
                            buf,
726
1.81M
                        );
727
1.81M
728
1.81M
                        let alpha_index =
729
1.81M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
730
1.81M
                        let buffer_index = alpha_index * 4 + 3;
731
1.81M
732
1.81M
                        buf[buffer_index] = predictor.wrapping_add(alpha_chunk.data[alpha_index]);
733
1.81M
                    }
734
                }
735
592
            } else {
736
592
                frame.fill_rgb(buf, self.webp_decode_options.lossy_upsampling);
737
592
            }
738
        }
739
740
3.48k
        Ok(())
741
7.21k
    }
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
756
    pub fn read_frame(&mut self, buf: &mut [u8]) -> Result<u32, DecodingError> {
753
756
        assert!(self.is_animated());
754
756
        assert_eq!(Some(buf.len()), self.output_buffer_size());
755
756
756
        if self.animation.next_frame == self.num_frames {
757
0
            return Err(DecodingError::NoMoreFrames);
758
756
        }
759
760
756
        let ImageKind::Extended(info) = &self.kind else {
761
0
            unreachable!()
762
        };
763
764
756
        self.r
765
756
            .seek(io::SeekFrom::Start(self.animation.next_frame_start))?;
766
767
756
        let anmf_size = match read_chunk_header(&mut self.r)? {
768
756
            (WebPRiffChunk::ANMF, size, _) if size >= 32 => size,
769
28
            _ => return Err(DecodingError::ChunkHeaderInvalid(*b"ANMF")),
770
        };
771
772
        // Read ANMF chunk
773
728
        let frame_x = extended::read_3_bytes(&mut self.r)? * 2;
774
728
        let frame_y = extended::read_3_bytes(&mut self.r)? * 2;
775
728
        let frame_width = extended::read_3_bytes(&mut self.r)? + 1;
776
728
        let frame_height = extended::read_3_bytes(&mut self.r)? + 1;
777
728
        if frame_width > 16384 || frame_height > 16384 {
778
34
            return Err(DecodingError::ImageTooLarge);
779
694
        }
780
694
        if frame_x + frame_width > self.width || frame_y + frame_height > self.height {
781
40
            return Err(DecodingError::FrameOutsideImage);
782
654
        }
783
654
        let duration = extended::read_3_bytes(&mut self.r)?;
784
654
        let frame_info = self.r.read_u8()?;
785
654
        let use_alpha_blending = frame_info & 0b00000010 == 0;
786
654
        let dispose = frame_info & 0b00000001 != 0;
787
788
654
        let clear_color = if self.animation.dispose_next_frame {
789
654
            info.background_color
790
        } else {
791
0
            None
792
        };
793
794
        // Read normal bitstream now
795
654
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
796
654
        if chunk_size_rounded + 24 > anmf_size {
797
38
            return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
798
616
        }
799
800
616
        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
67
                let reader = (&mut self.r).take(chunk_size);
815
67
                let mut lossless_decoder = LosslessDecoder::new(reader);
816
67
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
817
67
                lossless_decoder.decode_frame(frame_width, frame_height, false, &mut rgba_frame)?;
818
61
                (rgba_frame, true)
819
            }
820
            WebPRiffChunk::ALPH => {
821
524
                if chunk_size_rounded + 32 > anmf_size {
822
1
                    return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
823
523
                }
824
825
                // read alpha
826
523
                let next_chunk_start = self.r.stream_position()? + chunk_size_rounded;
827
523
                let mut reader = (&mut self.r).take(chunk_size);
828
146
                let alpha_chunk =
829
523
                    read_alpha_chunk(&mut reader, frame_width as u16, frame_height as u16)?;
830
831
                // read opaque
832
146
                self.r.seek(io::SeekFrom::Start(next_chunk_start))?;
833
146
                let (next_chunk, next_chunk_size, _) = read_chunk_header(&mut self.r)?;
834
132
                if chunk_size + next_chunk_size + 32 > anmf_size {
835
16
                    return Err(DecodingError::ChunkHeaderInvalid(next_chunk.to_fourcc()));
836
116
                }
837
838
116
                let frame = Vp8Decoder::decode_frame((&mut self.r).take(next_chunk_size))?;
839
840
111
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
841
111
                frame.fill_rgba(&mut rgba_frame, self.webp_decode_options.lossy_upsampling);
842
843
11.3k
                for y in 0..frame.height {
844
1.93M
                    for x in 0..frame.width {
845
1.93M
                        let predictor: u8 = get_alpha_predictor(
846
1.93M
                            x.into(),
847
1.93M
                            y.into(),
848
1.93M
                            frame.width.into(),
849
1.93M
                            alpha_chunk.filtering_method,
850
1.93M
                            &rgba_frame,
851
1.93M
                        );
852
1.93M
853
1.93M
                        let alpha_index =
854
1.93M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
855
1.93M
                        let buffer_index = alpha_index * 4 + 3;
856
1.93M
857
1.93M
                        rgba_frame[buffer_index] =
858
1.93M
                            predictor.wrapping_add(alpha_chunk.data[alpha_index]);
859
1.93M
                    }
860
                }
861
862
111
                (rgba_frame, true)
863
            }
864
5
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
865
        };
866
867
        // fill starting canvas with clear color
868
186
        if self.animation.canvas.is_none() {
869
186
            self.animation.canvas = {
870
186
                let mut canvas = vec![0; (self.width * self.height * 4) as usize];
871
186
                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
186
                }
876
186
                Some(canvas)
877
            }
878
0
        }
879
186
        extended::composite_frame(
880
186
            self.animation.canvas.as_mut().unwrap(),
881
186
            self.width,
882
186
            self.height,
883
186
            clear_color,
884
186
            &frame,
885
186
            frame_x,
886
186
            frame_y,
887
186
            frame_width,
888
186
            frame_height,
889
186
            frame_has_alpha,
890
186
            use_alpha_blending,
891
186
            self.animation.previous_frame_width,
892
186
            self.animation.previous_frame_height,
893
186
            self.animation.previous_frame_x_offset,
894
186
            self.animation.previous_frame_y_offset,
895
        );
896
897
186
        self.animation.previous_frame_width = frame_width;
898
186
        self.animation.previous_frame_height = frame_height;
899
186
        self.animation.previous_frame_x_offset = frame_x;
900
186
        self.animation.previous_frame_y_offset = frame_y;
901
902
186
        self.animation.dispose_next_frame = dispose;
903
186
        self.animation.next_frame_start += anmf_size + 8;
904
186
        self.animation.next_frame += 1;
905
906
186
        if self.has_alpha() {
907
164
            buf.copy_from_slice(self.animation.canvas.as_ref().unwrap());
908
164
        } else {
909
833M
            for (b, c) in buf
910
22
                .chunks_exact_mut(3)
911
22
                .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
186
        Ok(duration)
918
756
    }
<image_webp::decoder::WebPDecoder<std::io::cursor::Cursor<&[u8]>>>::read_frame
Line
Count
Source
752
756
    pub fn read_frame(&mut self, buf: &mut [u8]) -> Result<u32, DecodingError> {
753
756
        assert!(self.is_animated());
754
756
        assert_eq!(Some(buf.len()), self.output_buffer_size());
755
756
756
        if self.animation.next_frame == self.num_frames {
757
0
            return Err(DecodingError::NoMoreFrames);
758
756
        }
759
760
756
        let ImageKind::Extended(info) = &self.kind else {
761
0
            unreachable!()
762
        };
763
764
756
        self.r
765
756
            .seek(io::SeekFrom::Start(self.animation.next_frame_start))?;
766
767
756
        let anmf_size = match read_chunk_header(&mut self.r)? {
768
756
            (WebPRiffChunk::ANMF, size, _) if size >= 32 => size,
769
28
            _ => return Err(DecodingError::ChunkHeaderInvalid(*b"ANMF")),
770
        };
771
772
        // Read ANMF chunk
773
728
        let frame_x = extended::read_3_bytes(&mut self.r)? * 2;
774
728
        let frame_y = extended::read_3_bytes(&mut self.r)? * 2;
775
728
        let frame_width = extended::read_3_bytes(&mut self.r)? + 1;
776
728
        let frame_height = extended::read_3_bytes(&mut self.r)? + 1;
777
728
        if frame_width > 16384 || frame_height > 16384 {
778
34
            return Err(DecodingError::ImageTooLarge);
779
694
        }
780
694
        if frame_x + frame_width > self.width || frame_y + frame_height > self.height {
781
40
            return Err(DecodingError::FrameOutsideImage);
782
654
        }
783
654
        let duration = extended::read_3_bytes(&mut self.r)?;
784
654
        let frame_info = self.r.read_u8()?;
785
654
        let use_alpha_blending = frame_info & 0b00000010 == 0;
786
654
        let dispose = frame_info & 0b00000001 != 0;
787
788
654
        let clear_color = if self.animation.dispose_next_frame {
789
654
            info.background_color
790
        } else {
791
0
            None
792
        };
793
794
        // Read normal bitstream now
795
654
        let (chunk, chunk_size, chunk_size_rounded) = read_chunk_header(&mut self.r)?;
796
654
        if chunk_size_rounded + 24 > anmf_size {
797
38
            return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
798
616
        }
799
800
616
        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
67
                let reader = (&mut self.r).take(chunk_size);
815
67
                let mut lossless_decoder = LosslessDecoder::new(reader);
816
67
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
817
67
                lossless_decoder.decode_frame(frame_width, frame_height, false, &mut rgba_frame)?;
818
61
                (rgba_frame, true)
819
            }
820
            WebPRiffChunk::ALPH => {
821
524
                if chunk_size_rounded + 32 > anmf_size {
822
1
                    return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc()));
823
523
                }
824
825
                // read alpha
826
523
                let next_chunk_start = self.r.stream_position()? + chunk_size_rounded;
827
523
                let mut reader = (&mut self.r).take(chunk_size);
828
146
                let alpha_chunk =
829
523
                    read_alpha_chunk(&mut reader, frame_width as u16, frame_height as u16)?;
830
831
                // read opaque
832
146
                self.r.seek(io::SeekFrom::Start(next_chunk_start))?;
833
146
                let (next_chunk, next_chunk_size, _) = read_chunk_header(&mut self.r)?;
834
132
                if chunk_size + next_chunk_size + 32 > anmf_size {
835
16
                    return Err(DecodingError::ChunkHeaderInvalid(next_chunk.to_fourcc()));
836
116
                }
837
838
116
                let frame = Vp8Decoder::decode_frame((&mut self.r).take(next_chunk_size))?;
839
840
111
                let mut rgba_frame = vec![0; frame_width as usize * frame_height as usize * 4];
841
111
                frame.fill_rgba(&mut rgba_frame, self.webp_decode_options.lossy_upsampling);
842
843
11.3k
                for y in 0..frame.height {
844
1.93M
                    for x in 0..frame.width {
845
1.93M
                        let predictor: u8 = get_alpha_predictor(
846
1.93M
                            x.into(),
847
1.93M
                            y.into(),
848
1.93M
                            frame.width.into(),
849
1.93M
                            alpha_chunk.filtering_method,
850
1.93M
                            &rgba_frame,
851
1.93M
                        );
852
1.93M
853
1.93M
                        let alpha_index =
854
1.93M
                            usize::from(y) * usize::from(frame.width) + usize::from(x);
855
1.93M
                        let buffer_index = alpha_index * 4 + 3;
856
1.93M
857
1.93M
                        rgba_frame[buffer_index] =
858
1.93M
                            predictor.wrapping_add(alpha_chunk.data[alpha_index]);
859
1.93M
                    }
860
                }
861
862
111
                (rgba_frame, true)
863
            }
864
5
            _ => return Err(DecodingError::ChunkHeaderInvalid(chunk.to_fourcc())),
865
        };
866
867
        // fill starting canvas with clear color
868
186
        if self.animation.canvas.is_none() {
869
186
            self.animation.canvas = {
870
186
                let mut canvas = vec![0; (self.width * self.height * 4) as usize];
871
186
                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
186
                }
876
186
                Some(canvas)
877
            }
878
0
        }
879
186
        extended::composite_frame(
880
186
            self.animation.canvas.as_mut().unwrap(),
881
186
            self.width,
882
186
            self.height,
883
186
            clear_color,
884
186
            &frame,
885
186
            frame_x,
886
186
            frame_y,
887
186
            frame_width,
888
186
            frame_height,
889
186
            frame_has_alpha,
890
186
            use_alpha_blending,
891
186
            self.animation.previous_frame_width,
892
186
            self.animation.previous_frame_height,
893
186
            self.animation.previous_frame_x_offset,
894
186
            self.animation.previous_frame_y_offset,
895
        );
896
897
186
        self.animation.previous_frame_width = frame_width;
898
186
        self.animation.previous_frame_height = frame_height;
899
186
        self.animation.previous_frame_x_offset = frame_x;
900
186
        self.animation.previous_frame_y_offset = frame_y;
901
902
186
        self.animation.dispose_next_frame = dispose;
903
186
        self.animation.next_frame_start += anmf_size + 8;
904
186
        self.animation.next_frame += 1;
905
906
186
        if self.has_alpha() {
907
164
            buf.copy_from_slice(self.animation.canvas.as_ref().unwrap());
908
164
        } else {
909
833M
            for (b, c) in buf
910
22
                .chunks_exact_mut(3)
911
22
                .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
186
        Ok(duration)
918
756
    }
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.90k
pub(crate) fn range_reader<R: BufRead + Seek>(
940
6.90k
    mut r: R,
941
6.90k
    range: Range<u64>,
942
6.90k
) -> Result<impl BufRead, DecodingError> {
943
6.90k
    r.seek(io::SeekFrom::Start(range.start))?;
944
6.90k
    Ok(r.take(range.end - range.start))
945
6.90k
}
image_webp::decoder::range_reader::<&mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
939
6.90k
pub(crate) fn range_reader<R: BufRead + Seek>(
940
6.90k
    mut r: R,
941
6.90k
    range: Range<u64>,
942
6.90k
) -> Result<impl BufRead, DecodingError> {
943
6.90k
    r.seek(io::SeekFrom::Start(range.start))?;
944
6.90k
    Ok(r.take(range.end - range.start))
945
6.90k
}
Unexecuted instantiation: image_webp::decoder::range_reader::<_>
946
947
47.0k
pub(crate) fn read_fourcc<R: BufRead>(mut r: R) -> Result<WebPRiffChunk, DecodingError> {
948
47.0k
    let mut chunk_fourcc = [0; 4];
949
47.0k
    r.read_exact(&mut chunk_fourcc)?;
950
46.1k
    Ok(WebPRiffChunk::from_fourcc(chunk_fourcc))
951
47.0k
}
image_webp::decoder::read_fourcc::<&mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
947
7.59k
pub(crate) fn read_fourcc<R: BufRead>(mut r: R) -> Result<WebPRiffChunk, DecodingError> {
948
7.59k
    let mut chunk_fourcc = [0; 4];
949
7.59k
    r.read_exact(&mut chunk_fourcc)?;
950
7.59k
    Ok(WebPRiffChunk::from_fourcc(chunk_fourcc))
951
7.59k
}
image_webp::decoder::read_fourcc::<&mut &mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
947
39.4k
pub(crate) fn read_fourcc<R: BufRead>(mut r: R) -> Result<WebPRiffChunk, DecodingError> {
948
39.4k
    let mut chunk_fourcc = [0; 4];
949
39.4k
    r.read_exact(&mut chunk_fourcc)?;
950
38.5k
    Ok(WebPRiffChunk::from_fourcc(chunk_fourcc))
951
39.4k
}
Unexecuted instantiation: image_webp::decoder::read_fourcc::<_>
952
953
39.4k
pub(crate) fn read_chunk_header<R: BufRead>(
954
39.4k
    mut r: R,
955
39.4k
) -> Result<(WebPRiffChunk, u64, u64), DecodingError> {
956
39.4k
    let chunk = read_fourcc(&mut r)?;
957
38.5k
    let chunk_size = r.read_u32::<LittleEndian>()?;
958
38.5k
    let chunk_size_rounded = chunk_size.saturating_add(chunk_size & 1);
959
38.5k
    Ok((chunk, chunk_size.into(), chunk_size_rounded.into()))
960
39.4k
}
image_webp::decoder::read_chunk_header::<&mut std::io::cursor::Cursor<&[u8]>>
Line
Count
Source
953
39.4k
pub(crate) fn read_chunk_header<R: BufRead>(
954
39.4k
    mut r: R,
955
39.4k
) -> Result<(WebPRiffChunk, u64, u64), DecodingError> {
956
39.4k
    let chunk = read_fourcc(&mut r)?;
957
38.5k
    let chunk_size = r.read_u32::<LittleEndian>()?;
958
38.5k
    let chunk_size_rounded = chunk_size.saturating_add(chunk_size & 1);
959
38.5k
    Ok((chunk, chunk_size.into(), chunk_size_rounded.into()))
960
39.4k
}
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
}