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.
This commit is contained in:
Aliaksandr Kalenik 2026-06-22 16:50:46 +02:00 committed by Jelle Raaijmakers
parent b024e45c2c
commit 8c06273dbf
2 changed files with 64 additions and 14 deletions

View file

@ -119,21 +119,15 @@ static ErrorOr<void> 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;

View file

@ -42,6 +42,48 @@ static ErrorOr<Gfx::ImageFrameDescriptor> expect_single_frame_of_size(Gfx::Image
return frame;
}
static ErrorOr<ByteBuffer> 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<void> {
TRY(extended_webp.try_append(static_cast<u8>(value)));
TRY(extended_webp.try_append(static_cast<u8>(value >> 8)));
TRY(extended_webp.try_append(static_cast<u8>(value >> 16)));
TRY(extended_webp.try_append(static_cast<u8>(value >> 24)));
return {};
};
auto append_u24_le = [&](u32 value) -> ErrorOr<void> {
VERIFY(value <= 0xffffff);
TRY(extended_webp.try_append(static_cast<u8>(value)));
TRY(extended_webp.try_append(static_cast<u8>(value >> 8)));
TRY(extended_webp.try_append(static_cast<u8>(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)));