/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 | | } |