Coverage Report

Created: 2026-11-13 08:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibGfx/ImageFormats/WebPLoader.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2023, Nico Weber <thakis@chromium.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/Debug.h>
8
#include <AK/Endian.h>
9
#include <AK/Format.h>
10
#include <AK/MemoryStream.h>
11
#include <AK/Vector.h>
12
#include <LibGfx/FourCC.h>
13
#include <LibGfx/ImageFormats/WebPLoader.h>
14
#include <LibGfx/ImageFormats/WebPLoaderLossless.h>
15
#include <LibGfx/ImageFormats/WebPLoaderLossy.h>
16
#include <LibGfx/ImageFormats/WebPShared.h>
17
#include <LibGfx/Painter.h>
18
#include <LibRIFF/ChunkID.h>
19
#include <LibRIFF/RIFF.h>
20
21
// Overview: https://developers.google.com/speed/webp/docs/compression
22
// Container: https://developers.google.com/speed/webp/docs/riff_container
23
24
namespace Gfx {
25
26
namespace {
27
28
// "For a still image, the image data consists of a single frame, which is made up of:
29
//     An optional alpha subchunk.
30
//     A bitstream subchunk."
31
struct ImageData {
32
    // "This optional chunk contains encoded alpha data for this frame. A frame containing a 'VP8L' chunk SHOULD NOT contain this chunk."
33
    Optional<RIFF::Chunk> alpha_chunk; // 'ALPH'
34
    RIFF::Chunk image_data_chunk;      // Either 'VP8 ' or 'VP8L'. For 'VP8L', alpha_chunk will not have a value.
35
};
36
37
}
38
39
struct WebPLoadingContext {
40
    enum State {
41
        NotDecoded = 0,
42
        Error,
43
        HeaderDecoded,
44
        FirstChunkRead,
45
        FirstChunkDecoded,
46
        ChunksDecoded,
47
        AnimationFrameChunksDecoded,
48
        BitmapDecoded,
49
    };
50
    State state { State::NotDecoded };
51
    ReadonlyBytes data;
52
53
    ReadonlyBytes chunks_cursor;
54
55
    Optional<IntSize> size;
56
57
    RefPtr<Gfx::Bitmap> bitmap;
58
59
    // Either 'VP8 ' (simple lossy file), 'VP8L' (simple lossless file), or 'VP8X' (extended file).
60
    Optional<RIFF::Chunk> first_chunk;
61
62
    // Only valid if first_chunk->type == 'VP8X'.
63
    VP8XHeader vp8x_header;
64
65
    // If first_chunk is not a VP8X chunk, then only image_data.image_data_chunk is set and all the other Chunks are not set.
66
    // Once state is >= ChunksDecoded, for non-animated images, this will have a value, or decoding will have failed.
67
    Optional<ImageData> image_data;
68
69
    Optional<RIFF::Chunk> animation_header_chunk; // 'ANIM'
70
    Vector<RIFF::Chunk> animation_frame_chunks;   // 'ANMF'
71
72
    // These are set in state >= AnimationFrameChunksDecoded, if first_chunk.type == 'VP8X' && vp8x_header.has_animation.
73
    Optional<ANIMChunk> animation_header_chunk_data;
74
    Optional<Vector<ANMFChunk>> animation_frame_chunks_data;
75
    size_t current_frame { 0 };
76
77
    Optional<RIFF::Chunk> iccp_chunk; // 'ICCP'
78
    Optional<RIFF::Chunk> exif_chunk; // 'EXIF'
79
    Optional<RIFF::Chunk> xmp_chunk;  // 'XMP '
80
};
81
82
// https://developers.google.com/speed/webp/docs/riff_container#webp_file_header
83
static ErrorOr<void> decode_webp_header(WebPLoadingContext& context)
84
2.93k
{
85
2.93k
    if (context.state >= WebPLoadingContext::HeaderDecoded)
86
0
        return {};
87
88
2.93k
    FixedMemoryStream header_stream { context.data };
89
2.93k
    auto header = TRY(header_stream.read_value<RIFF::FileHeader>());
90
2.92k
    if (header.magic() != RIFF::riff_magic || header.subformat != "WEBP"sv)
91
41
        return Error::from_string_literal("Invalid WebP header");
92
93
    // "File Size: [...] The size of the file in bytes starting at offset 8. The maximum value of this field is 2^32 minus 10 bytes."
94
2.88k
    u32 const maximum_webp_file_size = 0xffff'ffff - 9;
95
2.88k
    if (header.file_size() > maximum_webp_file_size)
96
1
        return Error::from_string_literal("WebP header file size over maximum");
97
98
    // "The file size in the header is the total size of the chunks that follow plus 4 bytes for the 'WEBP' RIFF::ChunkID.
99
    //  The file SHOULD NOT contain any data after the data specified by File Size.
100
    //  Readers MAY parse such files, ignoring the trailing data."
101
2.88k
    if (context.data.size() - 8 < header.file_size())
102
48
        return Error::from_string_literal("WebP data too small for size in header");
103
2.83k
    if (header.file_size() < 4) // Need at least 4 bytes for 'WEBP', else we'll trim to less than the header size below.
104
11
        return Error::from_string_literal("WebP stored file size too small for header it's stored in");
105
2.82k
    if (context.data.size() - 8 > header.file_size()) {
106
438
        dbgln_if(WEBP_DEBUG, "WebP has {} bytes of data, but header needs only {}. Trimming.", context.data.size(), header.file_size() + 8);
107
438
        context.data = context.data.trim(header.file_size() + 8);
108
438
    }
109
110
2.82k
    context.state = WebPLoadingContext::HeaderDecoded;
111
2.82k
    return {};
112
2.83k
}
113
114
// https://developers.google.com/speed/webp/docs/riff_container#alpha
115
static ErrorOr<void> decode_webp_chunk_ALPH(RIFF::Chunk const& alph_chunk, Bitmap& bitmap)
116
194
{
117
194
    VERIFY(alph_chunk.id() == "ALPH"sv);
118
119
194
    if (alph_chunk.size() < 1)
120
3
        return Error::from_string_literal("WebPImageDecoderPlugin: ALPH chunk too small");
121
122
191
    u8 flags = alph_chunk[0];
123
191
    u8 preprocessing = (flags >> 4) & 3;
124
191
    u8 filtering_method = (flags >> 2) & 3;
125
191
    u8 compression_method = flags & 3;
126
127
191
    dbgln_if(WEBP_DEBUG, "ALPH: preprocessing {} filtering_method {} compression_method {}", preprocessing, filtering_method, compression_method);
128
129
191
    ReadonlyBytes alpha_data = alph_chunk.data().slice(1);
130
131
191
    size_t pixel_count = bitmap.width() * bitmap.height();
132
133
191
    auto alpha = TRY(ByteBuffer::create_uninitialized(pixel_count));
134
135
191
    if (compression_method == 0) {
136
        // "Raw data: consists of a byte sequence of length width * height, containing all the 8-bit transparency values in scan order."
137
71
        if (alpha_data.size() < pixel_count)
138
8
            return Error::from_string_literal("WebPImageDecoderPlugin: uncompressed ALPH data too small");
139
63
        memcpy(alpha.data(), alpha_data.data(), pixel_count);
140
120
    } else {
141
        // "Lossless format compression: the byte sequence is a compressed image-stream (as described in the WebP Lossless Bitstream Format)
142
        //  of implicit dimension width x height. That is, this image-stream does NOT contain any headers describing the image dimension.
143
        //  Once the image-stream is decoded into ARGB color values, following the process described in the lossless format specification,
144
        //  the transparency information must be extracted from the green channel of the ARGB quadruplet."
145
120
        VP8LHeader vp8l_header { static_cast<u16>(bitmap.width()), static_cast<u16>(bitmap.height()), /*is_alpha_used=*/false, alpha_data };
146
120
        auto lossless_bitmap = TRY(decode_webp_chunk_VP8L_contents(vp8l_header));
147
148
107
        if (pixel_count != static_cast<size_t>(lossless_bitmap->width() * lossless_bitmap->height()))
149
0
            return Error::from_string_literal("WebPImageDecoderPlugin: decompressed ALPH dimensions don't match VP8 dimensions");
150
151
3.24M
        for (size_t i = 0; i < pixel_count; ++i)
152
3.24M
            alpha[i] = (lossless_bitmap->begin()[i] & 0xff00) >> 8;
153
107
    }
154
155
    // "For each pixel, filtering is performed using the following calculations. Assume the alpha values surrounding the current X position are labeled as:
156
    //
157
    //  C | B |
158
    // ---+---+
159
    //  A | X |
160
    // [...]
161
    //
162
    // The final value is derived by adding the decompressed value X to the predictor and using modulo-256 arithmetic"
163
170
    switch (filtering_method) {
164
6
    case 0:
165
        // "Method 0: predictor = 0"
166
        // Nothing to do.
167
6
        break;
168
169
52
    case 1:
170
        // "Method 1: predictor = A"
171
        // "The top-left value at location (0, 0) uses 0 as predictor value. Otherwise,
172
        //  For horizontal or gradient filtering methods, the left-most pixels at location (0, y) are predicted using the location (0, y-1) just above."
173
20.2k
        for (int y = 1; y < bitmap.height(); ++y)
174
20.2k
            alpha[y * bitmap.width()] += alpha[(y - 1) * bitmap.width()];
175
20.3k
        for (int y = 0; y < bitmap.height(); ++y) {
176
751k
            for (int x = 1; x < bitmap.width(); ++x) {
177
730k
                u8 A = alpha[y * bitmap.width() + (x - 1)];
178
730k
                alpha[y * bitmap.width() + x] += A;
179
730k
            }
180
20.2k
        }
181
52
        break;
182
183
46
    case 2:
184
        // "Method 2: predictor = B"
185
        // "The top-left value at location (0, 0) uses 0 as predictor value. Otherwise,
186
        //  For vertical or gradient filtering methods, the top-most pixels at location (x, 0) are predicted using the location (x-1, 0) on the left."
187
18.4k
        for (int x = 1; x < bitmap.width(); ++x)
188
18.4k
            alpha[x] += alpha[x - 1];
189
12.2k
        for (int y = 1; y < bitmap.height(); ++y) {
190
987k
            for (int x = 0; x < bitmap.width(); ++x) {
191
975k
                u8 B = alpha[(y - 1) * bitmap.width() + x];
192
975k
                alpha[y * bitmap.width() + x] += B;
193
975k
            }
194
12.2k
        }
195
46
        break;
196
197
66
    case 3:
198
        // "Method 3: predictor = clip(A + B - C)"
199
        //  where clip(v) is equal to:
200
        //  * 0 if v < 0
201
        //  * 255 if v > 255
202
        //  * v otherwise"
203
        // "The top-left value at location (0, 0) uses 0 as predictor value. Otherwise,
204
        //  For horizontal or gradient filtering methods, the left-most pixels at location (0, y) are predicted using the location (0, y-1) just above.
205
        //  For vertical or gradient filtering methods, the top-most pixels at location (x, 0) are predicted using the location (x-1, 0) on the left."
206
20.2k
        for (int x = 1; x < bitmap.width(); ++x)
207
20.1k
            alpha[x] += alpha[x - 1];
208
21.6k
        for (int y = 1; y < bitmap.height(); ++y)
209
21.5k
            alpha[y * bitmap.width()] += alpha[(y - 1) * bitmap.width()];
210
21.6k
        for (int y = 1; y < bitmap.height(); ++y) {
211
819k
            for (int x = 1; x < bitmap.width(); ++x) {
212
797k
                u8 A = alpha[y * bitmap.width() + (x - 1)];
213
797k
                u8 B = alpha[(y - 1) * bitmap.width() + x];
214
797k
                u8 C = alpha[(y - 1) * bitmap.width() + (x - 1)];
215
797k
                alpha[y * bitmap.width() + x] += clamp(A + B - C, 0, 255);
216
797k
            }
217
21.5k
        }
218
66
        break;
219
220
0
    default:
221
0
        return Error::from_string_literal("WebPImageDecoderPlugin: uncompressed ALPH invalid filtering method");
222
170
    }
223
224
3.25M
    for (size_t i = 0; i < pixel_count; ++i)
225
3.25M
        bitmap.begin()[i] = alpha[i] << 24 | (bitmap.begin()[i] & 0xffffff);
226
227
170
    return {};
228
170
}
229
230
static ErrorOr<VP8XHeader> decode_webp_chunk_VP8X(RIFF::Chunk const& vp8x_chunk)
231
669
{
232
669
    VERIFY(vp8x_chunk.id() == "VP8X"sv);
233
234
    // The VP8X chunk is documented at "Extended WebP file header:" at the end of
235
    // https://developers.google.com/speed/webp/docs/riff_container#extended_file_format
236
669
    if (vp8x_chunk.size() < 10)
237
8
        return Error::from_string_literal("WebPImageDecoderPlugin: VP8X chunk too small");
238
239
    // 1 byte flags
240
    // "Reserved (Rsv): 2 bits   MUST be 0. Readers MUST ignore this field.
241
    //  ICC profile (I): 1 bit   Set if the file contains an ICC profile.
242
    //  Alpha (L): 1 bit         Set if any of the frames of the image contain transparency information ("alpha").
243
    //  Exif metadata (E): 1 bit Set if the file contains Exif metadata.
244
    //  XMP metadata (X): 1 bit  Set if the file contains XMP metadata.
245
    //  Animation (A): 1 bit     Set if this is an animated image. Data in 'ANIM' and 'ANMF' chunks should be used to control the animation.
246
    //  Reserved (R): 1 bit      MUST be 0. Readers MUST ignore this field."
247
661
    u8 flags = vp8x_chunk[0];
248
661
    bool has_icc = flags & 0x20;
249
661
    bool has_alpha = flags & 0x10;
250
661
    bool has_exif = flags & 0x8;
251
661
    bool has_xmp = flags & 0x4;
252
661
    bool has_animation = flags & 0x2;
253
254
    // 3 bytes reserved
255
    // 3 bytes width minus one
256
661
    u32 width = (vp8x_chunk[4] | (vp8x_chunk[5] << 8) | (vp8x_chunk[6] << 16)) + 1;
257
258
    // 3 bytes height minus one
259
661
    u32 height = (vp8x_chunk[7] | (vp8x_chunk[8] << 8) | (vp8x_chunk[9] << 16)) + 1;
260
261
661
    dbgln_if(WEBP_DEBUG, "VP8X: flags {:#x} --{}{}{}{}{}{}, width {}, height {}",
262
661
        flags,
263
661
        has_icc ? " icc" : "",
264
661
        has_alpha ? " alpha" : "",
265
661
        has_exif ? " exif" : "",
266
661
        has_xmp ? " xmp" : "",
267
661
        has_animation ? " anim" : "",
268
661
        (flags & 0x3e) == 0 ? " none" : "",
269
661
        width, height);
270
271
661
    return VP8XHeader { has_icc, has_alpha, has_exif, has_xmp, has_animation, width, height };
272
669
}
273
274
// https://developers.google.com/speed/webp/docs/riff_container#animation
275
static ErrorOr<ANIMChunk> decode_webp_chunk_ANIM(RIFF::Chunk const& anim_chunk)
276
255
{
277
255
    VERIFY(anim_chunk.id() == "ANIM"sv);
278
255
    if (anim_chunk.size() < 6)
279
2
        return Error::from_string_literal("WebPImageDecoderPlugin: ANIM chunk too small");
280
281
253
    u32 background_color = (u32)anim_chunk[0] | ((u32)anim_chunk[1] << 8) | ((u32)anim_chunk[2] << 16) | ((u32)anim_chunk[3] << 24);
282
253
    u16 loop_count = anim_chunk[4] | (anim_chunk[5] << 8);
283
284
253
    dbgln_if(WEBP_DEBUG, "ANIM: background_color {:x} loop_count {}", background_color, loop_count);
285
286
253
    return ANIMChunk { background_color, loop_count };
287
255
}
288
289
// https://developers.google.com/speed/webp/docs/riff_container#animation
290
static ErrorOr<ANMFChunk> decode_webp_chunk_ANMF(WebPLoadingContext& context, RIFF::Chunk const& anmf_chunk)
291
276
{
292
276
    VERIFY(anmf_chunk.id() == "ANMF"sv);
293
276
    if (anmf_chunk.size() < 16)
294
1
        return Error::from_string_literal("WebPImageDecoderPlugin: ANMF chunk too small");
295
296
    // "The X coordinate of the upper left corner of the frame is Frame X * 2."
297
275
    u32 frame_x = ((u32)anmf_chunk[0] | ((u32)anmf_chunk[1] << 8) | ((u32)anmf_chunk[2] << 16)) * 2;
298
299
    // "The Y coordinate of the upper left corner of the frame is Frame Y * 2."
300
275
    u32 frame_y = ((u32)anmf_chunk[3] | ((u32)anmf_chunk[4] << 8) | ((u32)anmf_chunk[5] << 16)) * 2;
301
302
    // "The frame width is 1 + Frame Width Minus One."
303
275
    u32 frame_width = ((u32)anmf_chunk[6] | ((u32)anmf_chunk[7] << 8) | ((u32)anmf_chunk[8] << 16)) + 1;
304
305
    // "The frame height is 1 + Frame Height Minus One."
306
275
    u32 frame_height = ((u32)anmf_chunk[9] | ((u32)anmf_chunk[10] << 8) | ((u32)anmf_chunk[11] << 16)) + 1;
307
308
    // "The time to wait before displaying the next frame, in 1 millisecond units.
309
    //  Note the interpretation of frame duration of 0 (and often <= 10) is implementation defined.
310
    //  Many tools and browsers assign a minimum duration similar to GIF."
311
275
    u32 frame_duration = (u32)anmf_chunk[12] | ((u32)anmf_chunk[13] << 8) | ((u32)anmf_chunk[14] << 16);
312
313
275
    u8 flags = anmf_chunk[15];
314
275
    auto blending_method = static_cast<ANMFChunkHeader::BlendingMethod>((flags >> 1) & 1);
315
275
    auto disposal_method = static_cast<ANMFChunkHeader::DisposalMethod>(flags & 1);
316
317
275
    dbgln_if(WEBP_DEBUG, "ANMF: frame_x {} frame_y {} frame_width {} frame_height {} frame_duration {} blending_method {} disposal_method {}",
318
275
        frame_x, frame_y, frame_width, frame_height, frame_duration, (int)blending_method, (int)disposal_method);
319
320
    // https://developers.google.com/speed/webp/docs/riff_container#assembling_the_canvas_from_frames
321
    // "assert VP8X.canvasWidth >= frame_right
322
    //  assert VP8X.canvasHeight >= frame_bottom"
323
275
    VERIFY(context.first_chunk->id() == "VP8X"sv);
324
275
    if (frame_x + frame_width > context.vp8x_header.width || frame_y + frame_height > context.vp8x_header.height)
325
44
        return Error::from_string_literal("WebPImageDecoderPlugin: ANMF dimensions out of bounds");
326
327
231
    auto header = ANMFChunkHeader { frame_x, frame_y, frame_width, frame_height, frame_duration, blending_method, disposal_method };
328
231
    ReadonlyBytes frame_data = anmf_chunk.data().slice(16);
329
231
    return ANMFChunk { header, frame_data };
330
275
}
331
332
static ErrorOr<ImageData> decode_webp_set_image_data(Optional<RIFF::Chunk> alpha, Optional<RIFF::Chunk> image_data)
333
2.50k
{
334
2.50k
    if (!image_data.has_value())
335
23
        return Error::from_string_literal("WebPImageDecoderPlugin: missing image data");
336
337
    // https://developers.google.com/speed/webp/docs/riff_container#alpha
338
    // "A frame containing a 'VP8L' chunk SHOULD NOT contain this chunk."
339
2.47k
    if (alpha.has_value() && image_data->id() == "VP8L"sv) {
340
4
        dbgln_if(WEBP_DEBUG, "WebPImageDecoderPlugin: VP8L frames should not have ALPH chunks. Ignoring ALPH chunk.");
341
4
        alpha.clear();
342
4
    }
343
344
2.47k
    return ImageData { move(alpha), image_data.value() };
345
2.50k
}
346
347
// https://developers.google.com/speed/webp/docs/riff_container#extended_file_format
348
static ErrorOr<void> decode_webp_extended(WebPLoadingContext& context, ReadonlyBytes chunks)
349
661
{
350
661
    VERIFY(context.first_chunk->id() == "VP8X"sv);
351
352
661
    Optional<RIFF::Chunk> alpha, image_data;
353
354
    // FIXME: This isn't quite to spec, which says
355
    // "All chunks SHOULD be placed in the same order as listed above.
356
    //  If a chunk appears in the wrong place, the file is invalid, but readers MAY parse the file, ignoring the chunks that are out of order."
357
2.19k
    auto store = [](auto& field, RIFF::Chunk const& chunk) {
358
2.19k
        if (!field.has_value())
359
904
            field = chunk;
360
2.19k
    };
361
12.0k
    while (!chunks.is_empty()) {
362
11.4k
        auto chunk = TRY(RIFF::Chunk::decode_and_advance(chunks));
363
364
11.3k
        if (chunk.id() == "ICCP"sv)
365
181
            store(context.iccp_chunk, chunk);
366
11.1k
        else if (chunk.id() == "ALPH"sv)
367
286
            store(alpha, chunk);
368
10.8k
        else if (chunk.id() == "ANIM"sv)
369
1.33k
            store(context.animation_header_chunk, chunk);
370
9.54k
        else if (chunk.id() == "ANMF"sv)
371
930
            TRY(context.animation_frame_chunks.try_append(chunk));
372
8.61k
        else if (chunk.id() == "EXIF"sv)
373
36
            store(context.exif_chunk, chunk);
374
8.58k
        else if (chunk.id() == "XMP "sv)
375
29
            store(context.xmp_chunk, chunk);
376
8.55k
        else if (chunk.id() == "VP8 "sv || chunk.id() == "VP8L"sv)
377
328
            store(image_data, chunk);
378
11.3k
    }
379
380
    // Validate chunks.
381
382
    // https://developers.google.com/speed/webp/docs/riff_container#animation
383
    // "ANIM Chunk: [...] This chunk MUST appear if the Animation flag in the VP8X chunk is set. If the Animation flag is not set and this chunk is present, it MUST be ignored."
384
661
    if (context.vp8x_header.has_animation && !context.animation_header_chunk.has_value())
385
16
        return Error::from_string_literal("WebPImageDecoderPlugin: Header claims animation, but no ANIM chunk");
386
528
    if (!context.vp8x_header.has_animation && context.animation_header_chunk.has_value()) {
387
27
        dbgln_if(WEBP_DEBUG, "WebPImageDecoderPlugin: Header claims no animation, but ANIM chunk present. Ignoring ANIM chunk.");
388
27
        context.animation_header_chunk.clear();
389
27
    }
390
391
    // "ANMF Chunk: [...] If the Animation flag is not set, then this chunk SHOULD NOT be present."
392
528
    if (!context.vp8x_header.has_animation && !context.animation_frame_chunks.is_empty()) {
393
6
        dbgln_if(WEBP_DEBUG, "WebPImageDecoderPlugin: Header claims no animation, but ANMF chunks present. Ignoring ANMF chunks.");
394
6
        context.animation_frame_chunks.clear();
395
6
    }
396
397
    // Image data is not optional -- but the spec doesn't explicitly say that an animated image must have more than 0 frames.
398
    // The spec also doesn't say that animated images must not contain a regular image data segment.
399
528
    if (!context.vp8x_header.has_animation || image_data.has_value())
400
276
        context.image_data = TRY(decode_webp_set_image_data(move(alpha), move(image_data)));
401
402
    // https://developers.google.com/speed/webp/docs/riff_container#color_profile
403
    // "This chunk MUST appear before the image data."
404
528
    if (context.iccp_chunk.has_value()
405
82
        && ((context.image_data.has_value()
406
5
                && (context.iccp_chunk->data().data() > context.image_data->image_data_chunk.data().data()
407
4
                    || (context.image_data->alpha_chunk.has_value() && context.iccp_chunk->data().data() > context.image_data->alpha_chunk->data().data())))
408
80
            || (!context.animation_frame_chunks.is_empty() && context.iccp_chunk->data().data() > context.animation_frame_chunks[0].data().data()))) {
409
3
        return Error::from_string_literal("WebPImageDecoderPlugin: ICCP chunk is after image data");
410
3
    }
411
412
506
    if (context.iccp_chunk.has_value() && !context.vp8x_header.has_icc)
413
3
        return Error::from_string_literal("WebPImageDecoderPlugin: ICCP chunk present, but VP8X header claims no ICC profile");
414
503
    if (!context.iccp_chunk.has_value() && context.vp8x_header.has_icc)
415
3
        return Error::from_string_literal("WebPImageDecoderPlugin: VP8X header claims ICC profile, but no ICCP chunk present");
416
417
500
    context.state = WebPLoadingContext::State::ChunksDecoded;
418
500
    return {};
419
503
}
420
421
static ErrorOr<void> read_webp_first_chunk(WebPLoadingContext& context)
422
2.82k
{
423
2.82k
    if (context.state >= WebPLoadingContext::State::FirstChunkRead)
424
0
        return {};
425
426
2.82k
    context.chunks_cursor = context.data.slice(sizeof(RIFF::FileHeader));
427
2.82k
    auto first_chunk = TRY(RIFF::Chunk::decode_and_advance(context.chunks_cursor));
428
429
2.74k
    if (first_chunk.id() != "VP8 "sv && first_chunk.id() != "VP8L"sv && first_chunk.id() != "VP8X"sv)
430
21
        return Error::from_string_literal("WebPImageDecoderPlugin: Invalid first chunk type");
431
432
2.72k
    context.first_chunk = first_chunk;
433
2.72k
    context.state = WebPLoadingContext::State::FirstChunkRead;
434
435
2.72k
    if (first_chunk.id() == "VP8 "sv || first_chunk.id() == "VP8L"sv)
436
2.05k
        context.image_data = TRY(decode_webp_set_image_data(OptionalNone {}, first_chunk));
437
438
2.72k
    return {};
439
2.72k
}
440
441
static ErrorOr<void> decode_webp_first_chunk(WebPLoadingContext& context)
442
2.82k
{
443
2.82k
    if (context.state >= WebPLoadingContext::State::FirstChunkDecoded)
444
0
        return {};
445
446
2.82k
    if (context.state < WebPLoadingContext::FirstChunkRead)
447
2.82k
        TRY(read_webp_first_chunk(context));
448
449
2.82k
    if (context.first_chunk->id() == "VP8 "sv) {
450
896
        auto vp8_header = TRY(decode_webp_chunk_VP8_header(context.first_chunk->data()));
451
826
        if (!vp8_header.keyframe_data.has_value())
452
2
            return Error::from_string_literal("WebPImageDecoderPlugin: 'VP8 ' chunk not a key frame");
453
454
824
        if (!vp8_header.show_frame)
455
2
            return Error::from_string_literal("WebPImageDecoderPlugin: 'VP8 ' chunk has invalid visibility for webp image");
456
457
822
        context.size = IntSize { vp8_header.keyframe_data->width, vp8_header.keyframe_data->height };
458
822
        context.state = WebPLoadingContext::State::FirstChunkDecoded;
459
822
        return {};
460
824
    }
461
1.82k
    if (context.first_chunk->id() == "VP8L"sv) {
462
1.15k
        auto vp8l_header = TRY(decode_webp_chunk_VP8L_header(context.first_chunk->data()));
463
1.13k
        context.size = IntSize { vp8l_header.width, vp8l_header.height };
464
1.13k
        context.state = WebPLoadingContext::State::FirstChunkDecoded;
465
1.13k
        return {};
466
1.15k
    }
467
669
    VERIFY(context.first_chunk->id() == "VP8X"sv);
468
669
    context.vp8x_header = TRY(decode_webp_chunk_VP8X(context.first_chunk.value()));
469
661
    context.size = IntSize { context.vp8x_header.width, context.vp8x_header.height };
470
661
    context.state = WebPLoadingContext::State::FirstChunkDecoded;
471
661
    return {};
472
669
}
473
474
static ErrorOr<void> decode_webp_chunks(WebPLoadingContext& context)
475
2.61k
{
476
2.61k
    if (context.state >= WebPLoadingContext::State::ChunksDecoded)
477
0
        return {};
478
479
2.61k
    VERIFY(context.state >= WebPLoadingContext::FirstChunkDecoded);
480
481
2.61k
    if (context.first_chunk->id() == "VP8X"sv)
482
661
        return decode_webp_extended(context, context.chunks_cursor);
483
484
1.95k
    context.state = WebPLoadingContext::State::ChunksDecoded;
485
1.95k
    return {};
486
2.61k
}
487
488
static ErrorOr<void> decode_webp_animation_frame_chunks(WebPLoadingContext& context)
489
255
{
490
255
    if (context.state >= WebPLoadingContext::State::AnimationFrameChunksDecoded)
491
0
        return {};
492
493
255
    VERIFY(context.state == WebPLoadingContext::State::ChunksDecoded);
494
495
255
    context.animation_header_chunk_data = TRY(decode_webp_chunk_ANIM(context.animation_header_chunk.value()));
496
497
253
    Vector<ANMFChunk> decoded_chunks;
498
253
    TRY(decoded_chunks.try_ensure_capacity(context.animation_frame_chunks.size()));
499
253
    for (auto const& chunk : context.animation_frame_chunks)
500
276
        TRY(decoded_chunks.try_append(TRY(decode_webp_chunk_ANMF(context, chunk))));
501
253
    context.animation_frame_chunks_data = move(decoded_chunks);
502
503
208
    context.state = WebPLoadingContext::State::AnimationFrameChunksDecoded;
504
208
    return {};
505
253
}
506
507
static ErrorOr<ImageData> decode_webp_animation_frame_image_data(ANMFChunk const& frame)
508
179
{
509
179
    ReadonlyBytes chunks = frame.frame_data;
510
179
    auto chunk = TRY(RIFF::Chunk::decode_and_advance(chunks));
511
512
173
    Optional<RIFF::Chunk> alpha, image_data;
513
173
    if (chunk.id() == "ALPH"sv) {
514
23
        alpha = chunk;
515
23
        chunk = TRY(RIFF::Chunk::decode_and_advance(chunks));
516
21
    }
517
173
    if (chunk.id() == "VP8 "sv || chunk.id() == "VP8L"sv)
518
167
        image_data = chunk;
519
520
171
    return decode_webp_set_image_data(move(alpha), move(image_data));
521
173
}
522
523
static ErrorOr<NonnullRefPtr<Bitmap>> decode_webp_image_data(WebPLoadingContext& context, ImageData const& image_data)
524
2.36k
{
525
2.36k
    if (image_data.image_data_chunk.id() == "VP8L"sv) {
526
1.33k
        VERIFY(!image_data.alpha_chunk.has_value());
527
1.33k
        auto vp8l_header = TRY(decode_webp_chunk_VP8L_header(image_data.image_data_chunk.data()));
528
529
        // Check that the VP8X header alpha flag matches the VP8L header alpha flag.
530
        // FIXME: For animated images, if VP8X has alpha then at least one frame should have alpha. But we currently don't check this for animations.
531
1.32k
        if (context.first_chunk->id() == "VP8X" && !context.animation_frame_chunks_data.has_value() && context.vp8x_header.has_alpha != vp8l_header.is_alpha_used)
532
1
            return Error::from_string_literal("WebPImageDecoderPlugin: VP8X header alpha flag doesn't match VP8L header");
533
534
1.32k
        return decode_webp_chunk_VP8L_contents(vp8l_header);
535
1.32k
    }
536
537
1.03k
    VERIFY(image_data.image_data_chunk.id() == "VP8 "sv);
538
1.03k
    auto vp8_header = TRY(decode_webp_chunk_VP8_header(image_data.image_data_chunk.data()));
539
1.02k
    auto bitmap = TRY(decode_webp_chunk_VP8_contents(vp8_header, image_data.alpha_chunk.has_value()));
540
541
291
    if (image_data.alpha_chunk.has_value())
542
194
        TRY(decode_webp_chunk_ALPH(image_data.alpha_chunk.value(), *bitmap));
543
544
291
    return bitmap;
545
291
}
546
547
// https://developers.google.com/speed/webp/docs/riff_container#canvas_assembly_from_frames
548
static ErrorOr<ImageFrameDescriptor> decode_webp_animation_frame(WebPLoadingContext& context, size_t frame_index)
549
208
{
550
208
    if (frame_index >= context.animation_frame_chunks_data->size())
551
0
        return Error::from_string_literal("frame_index size too high");
552
553
208
    VERIFY(context.first_chunk->id() == "VP8X"sv);
554
208
    VERIFY(context.vp8x_header.has_animation);
555
556
    // The spec says
557
    // "canvas ← new image of size VP8X.canvasWidth x VP8X.canvasHeight with
558
    //     background color ANIM.background_color."
559
    // But:
560
    // * libwebp always fills with transparent black (#00000000)
561
    // * some images (e.g. images written by Aseprite) set the background color to fully opaque white
562
    // These images then end up with a nice transparent background in libwebp-based decoders (i.e. basically everywhere)
563
    // but show a silly opaque border in ours. So don't use context.animation_header_chunk_data->background_color here.
564
208
    Color clear_color(Color::Transparent);
565
566
208
    size_t start_frame = context.current_frame + 1;
567
208
    dbgln_if(WEBP_DEBUG, "start_frame {} context.current_frame {}", start_frame, context.current_frame);
568
208
    if (context.state < WebPLoadingContext::State::BitmapDecoded) {
569
208
        start_frame = 0;
570
208
        auto format = context.vp8x_header.has_alpha ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888;
571
208
        context.bitmap = TRY(Bitmap::create(format, { context.vp8x_header.width, context.vp8x_header.height }));
572
179
        if (clear_color != Color(Color::Transparent)) // Bitmaps start out transparent, so only fill if not transparent.
573
0
            context.bitmap->fill(clear_color);
574
179
    } else if (frame_index < context.current_frame) {
575
0
        start_frame = 0;
576
0
    }
577
578
208
    Painter painter(*context.bitmap);
579
580
296
    for (size_t i = start_frame; i <= frame_index; ++i) {
581
179
        dbgln_if(WEBP_DEBUG, "drawing frame {} to produce frame {}", i, frame_index);
582
583
179
        auto const& frame = context.animation_frame_chunks_data.value()[i];
584
179
        auto const& frame_description = frame.header;
585
586
179
        if (i > 0) {
587
0
            auto const& previous_frame = context.animation_frame_chunks_data.value()[i - 1].header;
588
0
            if (previous_frame.disposal_method == ANMFChunkHeader::DisposalMethod::DisposeToBackgroundColor)
589
0
                painter.clear_rect({ previous_frame.frame_x, previous_frame.frame_y, previous_frame.frame_width, previous_frame.frame_height }, clear_color);
590
0
        }
591
592
179
        auto frame_image_data = TRY(decode_webp_animation_frame_image_data(frame));
593
167
        auto frame_bitmap = TRY(decode_webp_image_data(context, frame_image_data));
594
164
        if (static_cast<u32>(frame_bitmap->width()) != frame_description.frame_width || static_cast<u32>(frame_bitmap->height()) != frame_description.frame_height)
595
47
            return Error::from_string_literal("WebPImageDecoderPlugin: decoded frame bitmap size doesn't match frame description size");
596
597
        // FIXME: "Alpha-blending SHOULD be done in linear color space..."
598
117
        bool apply_alpha = frame_description.blending_method == ANMFChunkHeader::BlendingMethod::UseAlphaBlending;
599
117
        painter.blit({ frame_description.frame_x, frame_description.frame_y }, *frame_bitmap, { {}, frame_bitmap->size() }, /*opacity=*/1.0, apply_alpha);
600
601
117
        context.current_frame = i;
602
117
        context.state = WebPLoadingContext::State::BitmapDecoded;
603
117
    }
604
605
117
    return ImageFrameDescriptor { context.bitmap, static_cast<int>(context.animation_frame_chunks_data.value()[frame_index].header.frame_duration_in_milliseconds) };
606
179
}
607
608
WebPImageDecoderPlugin::WebPImageDecoderPlugin(ReadonlyBytes data, OwnPtr<WebPLoadingContext> context)
609
2.93k
    : m_context(move(context))
610
2.93k
{
611
2.93k
    m_context->data = data;
612
2.93k
}
613
614
2.93k
WebPImageDecoderPlugin::~WebPImageDecoderPlugin() = default;
615
616
bool WebPImageDecoderPlugin::set_error(ErrorOr<void> const& error_or)
617
308
{
618
308
    if (error_or.is_error()) {
619
50
        dbgln("WebPLoadingContext error: {}", error_or.error());
620
50
        m_context->state = WebPLoadingContext::State::Error;
621
50
        return true;
622
50
    }
623
258
    return false;
624
308
}
625
626
IntSize WebPImageDecoderPlugin::size()
627
0
{
628
0
    return m_context->size.value();
629
0
}
630
631
bool WebPImageDecoderPlugin::sniff(ReadonlyBytes data)
632
0
{
633
0
    WebPLoadingContext context;
634
0
    context.data = data;
635
636
    // Intentionally no set_error() call: We're just sniffing `data` passed to the function, not reading m_context->data.
637
0
    return !decode_webp_header(context).is_error();
638
0
}
639
640
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> WebPImageDecoderPlugin::create(ReadonlyBytes data)
641
2.93k
{
642
2.93k
    auto context = TRY(try_make<WebPLoadingContext>());
643
2.93k
    auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) WebPImageDecoderPlugin(data, move(context))));
644
2.93k
    TRY(decode_webp_header(*plugin->m_context));
645
2.82k
    TRY(decode_webp_first_chunk(*plugin->m_context));
646
2.61k
    return plugin;
647
2.82k
}
648
649
bool WebPImageDecoderPlugin::is_animated()
650
5.07k
{
651
5.07k
    return m_context->first_chunk->id() == "VP8X"sv && m_context->vp8x_header.has_animation;
652
5.07k
}
653
654
size_t WebPImageDecoderPlugin::loop_count()
655
0
{
656
0
    if (!is_animated())
657
0
        return 0;
658
659
0
    if (m_context->state < WebPLoadingContext::State::ChunksDecoded) {
660
0
        if (set_error(decode_webp_chunks(*m_context)))
661
0
            return 0;
662
0
    }
663
664
0
    if (m_context->state < WebPLoadingContext::State::AnimationFrameChunksDecoded) {
665
0
        if (set_error(decode_webp_animation_frame_chunks(*m_context)))
666
0
            return 0;
667
0
    }
668
669
0
    return m_context->animation_header_chunk_data->loop_count;
670
0
}
671
672
size_t WebPImageDecoderPlugin::frame_count()
673
2.61k
{
674
2.61k
    if (!is_animated())
675
2.30k
        return 1;
676
677
308
    if (m_context->state < WebPLoadingContext::State::ChunksDecoded) {
678
308
        if (set_error(decode_webp_chunks(*m_context)))
679
50
            return 1;
680
308
    }
681
682
258
    return m_context->animation_frame_chunks.size();
683
308
}
684
685
size_t WebPImageDecoderPlugin::first_animated_frame_index()
686
0
{
687
0
    return 0;
688
0
}
689
690
ErrorOr<ImageFrameDescriptor> WebPImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
691
2.61k
{
692
2.61k
    if (index >= frame_count())
693
3
        return Error::from_string_literal("WebPImageDecoderPlugin: Invalid frame index");
694
695
2.61k
    if (m_context->state == WebPLoadingContext::State::Error)
696
50
        return Error::from_string_literal("WebPImageDecoderPlugin: Decoding failed");
697
698
    // In a lambda so that only one check to set State::Error is needed, instead of one per TRY.
699
2.56k
    auto decode_frame = [this](size_t index) -> ErrorOr<ImageFrameDescriptor> {
700
2.56k
        if (m_context->state < WebPLoadingContext::State::ChunksDecoded)
701
2.30k
            TRY(decode_webp_chunks(*m_context));
702
703
2.56k
        if (is_animated()) {
704
255
            if (m_context->state < WebPLoadingContext::State::AnimationFrameChunksDecoded)
705
255
                TRY(decode_webp_animation_frame_chunks(*m_context));
706
255
            return decode_webp_animation_frame(*m_context, index);
707
255
        }
708
709
2.19k
        if (m_context->state < WebPLoadingContext::State::BitmapDecoded) {
710
2.19k
            auto bitmap = TRY(decode_webp_image_data(*m_context, m_context->image_data.value()));
711
712
            // Check that size in VP8X chunk matches dimensions in VP8 or VP8L chunk if both are present.
713
669
            if (m_context->first_chunk->id() == "VP8X") {
714
197
                if (static_cast<u32>(bitmap->width()) != m_context->vp8x_header.width || static_cast<u32>(bitmap->height()) != m_context->vp8x_header.height)
715
193
                    return Error::from_string_literal("WebPImageDecoderPlugin: VP8X and VP8/VP8L chunks store different dimensions");
716
197
            }
717
718
476
            m_context->bitmap = move(bitmap);
719
476
            m_context->state = WebPLoadingContext::State::BitmapDecoded;
720
476
        }
721
722
476
        VERIFY(m_context->bitmap);
723
476
        return ImageFrameDescriptor { m_context->bitmap, 0 };
724
476
    };
725
726
2.56k
    auto result = decode_frame(index);
727
2.56k
    if (result.is_error()) {
728
1.97k
        m_context->state = WebPLoadingContext::State::Error;
729
1.97k
        return result.release_error();
730
1.97k
    }
731
593
    return result.release_value();
732
2.56k
}
733
734
ErrorOr<Optional<ReadonlyBytes>> WebPImageDecoderPlugin::icc_data()
735
0
{
736
0
    if (auto result = decode_webp_chunks(*m_context); result.is_error()) {
737
0
        m_context->state = WebPLoadingContext::State::Error;
738
0
        return result.release_error();
739
0
    }
740
741
    // FIXME: "If this chunk is not present, sRGB SHOULD be assumed."
742
743
0
    return m_context->iccp_chunk.map([](auto iccp_chunk) { return iccp_chunk.data(); });
744
0
}
745
746
}