From 8c06273dbf617cd1adda0e26f44f699670b39a52 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Mon, 22 Jun 2026 16:50:46 +0200 Subject: [PATCH] LibGfx: Tolerate missing optional WebP ICC chunks Some WebP files advertise ICC metadata in their VP8X feature flags even though the ICCP chunk is absent or malformed. We accepted the bitstream header and could decode the pixels, but the follow-up mux metadata lookup failed during header decoding and made sniffing reject the image entirely. Treat mux and ICC metadata extraction as best-effort after libwebp accepts the header. Images without readable optional ICC metadata now decode normally and simply report no ICC profile. --- Libraries/LibGfx/ImageFormats/WebPLoader.cpp | 22 +++----- Tests/LibGfx/TestImageDecoder.cpp | 56 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/Libraries/LibGfx/ImageFormats/WebPLoader.cpp b/Libraries/LibGfx/ImageFormats/WebPLoader.cpp index 141216242b..54f767909b 100644 --- a/Libraries/LibGfx/ImageFormats/WebPLoader.cpp +++ b/Libraries/LibGfx/ImageFormats/WebPLoader.cpp @@ -119,21 +119,15 @@ static ErrorOr decode_webp_header(WebPLoadingContext& context) } WebPData webp_data { .bytes = context.data.data(), .size = context.data.size() }; - WebPMux* mux = WebPMuxCreate(&webp_data, 0); - ScopeGuard guard { [=]() { WebPMuxDelete(mux); } }; + if (WebPMux* mux = WebPMuxCreate(&webp_data, 0)) { + ScopeGuard guard { [=]() { WebPMuxDelete(mux); } }; - uint32_t flag = 0; - WebPMuxError err = WebPMuxGetFeatures(mux, &flag); - if (err != WEBP_MUX_OK) - return Error::from_string_literal("Failed to get webp features"); - - if (flag & ICCP_FLAG) { - WebPData icc_profile; - err = WebPMuxGetChunk(mux, "ICCP", &icc_profile); - if (err != WEBP_MUX_OK) - return Error::from_string_literal("Failed to get ICCP chunk of webp"); - - context.icc_data = TRY(context.icc_data.copy(icc_profile.bytes, icc_profile.size)); + uint32_t flag = 0; + if (WebPMuxGetFeatures(mux, &flag) == WEBP_MUX_OK && (flag & ICCP_FLAG)) { + WebPData icc_profile {}; + if (WebPMuxGetChunk(mux, "ICCP", &icc_profile) == WEBP_MUX_OK) + context.icc_data = TRY(context.icc_data.copy(icc_profile.bytes, icc_profile.size)); + } } context.state = WebPLoadingContext::State::HeaderDecoded; diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index a8b2390823..ef52be1caa 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -42,6 +42,48 @@ static ErrorOr expect_single_frame_of_size(Gfx::Image return frame; } +static ErrorOr make_webp_with_declared_but_missing_iccp_chunk(ReadonlyBytes simple_webp) +{ + VERIFY(simple_webp.size() >= 20); + VERIFY(simple_webp.slice(0, 4) == "RIFF"sv.bytes()); + VERIFY(simple_webp.slice(8, 4) == "WEBP"sv.bytes()); + VERIFY(simple_webp.slice(12, 4) == "VP8 "sv.bytes()); + + auto vp8_chunk = simple_webp.slice(12); + + ByteBuffer extended_webp; + auto append_u32_le = [&](u32 value) -> ErrorOr { + TRY(extended_webp.try_append(static_cast(value))); + TRY(extended_webp.try_append(static_cast(value >> 8))); + TRY(extended_webp.try_append(static_cast(value >> 16))); + TRY(extended_webp.try_append(static_cast(value >> 24))); + return {}; + }; + auto append_u24_le = [&](u32 value) -> ErrorOr { + VERIFY(value <= 0xffffff); + TRY(extended_webp.try_append(static_cast(value))); + TRY(extended_webp.try_append(static_cast(value >> 8))); + TRY(extended_webp.try_append(static_cast(value >> 16))); + return {}; + }; + + TRY(extended_webp.try_append("RIFF"sv.bytes())); + TRY(append_u32_le(4 + 18 + vp8_chunk.size())); + TRY(extended_webp.try_append("WEBP"sv.bytes())); + + TRY(extended_webp.try_append("VP8X"sv.bytes())); + TRY(append_u32_le(10)); + TRY(extended_webp.try_append(0x20)); // ICCP flag, without a corresponding ICCP chunk. + TRY(extended_webp.try_append(0)); + TRY(extended_webp.try_append(0)); + TRY(extended_webp.try_append(0)); + TRY(append_u24_le(239)); // simple-vp8.webp is 240x240, encoded as dimension - 1. + TRY(append_u24_le(239)); + + TRY(extended_webp.try_append(vp8_chunk)); + return extended_webp; +} + TEST_CASE(test_bmp) { auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("bmp/rgba32-1.bmp"sv))); @@ -637,6 +679,20 @@ TEST_CASE(test_webp_simple_lossy) EXPECT_EQ(frame.image->get_pixel(198, 202), Gfx::Color(0x7a, 0xaa, 0xd5, 255)); } +TEST_CASE(test_webp_extended_missing_declared_icc_chunk) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("webp/simple-vp8.webp"sv))); + auto malformed_webp = TRY_OR_FAIL(make_webp_with_declared_but_missing_iccp_chunk(file->bytes())); + + EXPECT(Gfx::WebPImageDecoderPlugin::sniff(malformed_webp)); + auto plugin_decoder = TRY_OR_FAIL(Gfx::WebPImageDecoderPlugin::create(malformed_webp)); + EXPECT(!TRY_OR_FAIL(plugin_decoder->icc_data()).has_value()); + + auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 240, 240 })); + EXPECT_EQ(frame.image->get_pixel(120, 232), Gfx::Color(0xf1, 0xef, 0xf0, 255)); + EXPECT_EQ(frame.image->get_pixel(198, 202), Gfx::Color(0x7a, 0xaa, 0xd5, 255)); +} + TEST_CASE(test_webp_simple_lossless) { auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("webp/simple-vp8l.webp"sv)));