From 5c002c13c93e5e7df819c3e7fce785a76a20d1b2 Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Fri, 5 May 2023 14:51:53 -0400 Subject: [PATCH] LibGfx/WebP: Some steps towards supporting animated webp files No observable behavior change, but when building with WEBP_DEBUG 1, this now prints frame data. --- .../LibGfx/ImageFormats/WebPLoader.cpp | 101 ++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/Userland/Libraries/LibGfx/ImageFormats/WebPLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/WebPLoader.cpp index a4b4bd837f..60cfbcbf84 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/WebPLoader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/WebPLoader.cpp @@ -87,6 +87,28 @@ struct ANIMChunk { u16 loop_count; }; +struct ANMFChunk { + u32 frame_x; + u32 frame_y; + u32 frame_width; + u32 frame_height; + u32 frame_duration_in_milliseconds; + + enum class BlendingMethod { + UseAlphaBlending = 0, + DoNotBlend = 1, + }; + BlendingMethod blending_method; + + enum class DisposalMethod { + DoNotDispose = 0, + DisposeToBackgroundColor = 1, + }; + DisposalMethod disposal_method; + + ReadonlyBytes frame_data; +}; + } struct WebPLoadingContext { @@ -97,6 +119,7 @@ struct WebPLoadingContext { FirstChunkRead, FirstChunkDecoded, ChunksDecoded, + AnimationFrameChunksDecoded, BitmapDecoded, }; State state { State::NotDecoded }; @@ -127,6 +150,10 @@ struct WebPLoadingContext { Optional animation_header_chunk; // 'ANIM' Vector animation_frame_chunks; // 'ANMF' + // These are set in state >= AnimationFrameChunksDecoded, if first_chunk.type == 'VP8X' && vp8x_header.has_animation. + Optional animation_header_chunk_data; + Optional> animation_frame_chunks_data; + Optional iccp_chunk; // 'ICCP' Optional exif_chunk; // 'EXIF' Optional xmp_chunk; // 'XMP ' @@ -1326,9 +1353,49 @@ static ErrorOr decode_webp_chunk_ANIM(WebPLoadingContext& context, Ch u32 background_color = (u32)data[0] | ((u32)data[1] << 8) | ((u32)data[2] << 16) | ((u32)data[3] << 24); u16 loop_count = data[4] | (data[5] << 8); + dbgln_if(WEBP_DEBUG, "background_color {:x} loop_count {}", background_color, loop_count); + return ANIMChunk { background_color, loop_count }; } +// https://developers.google.com/speed/webp/docs/riff_container#animation +static ErrorOr decode_webp_chunk_ANMF(WebPLoadingContext& context, Chunk const& anmf_chunk) +{ + VERIFY(anmf_chunk.type == FourCC("ANMF")); + if (anmf_chunk.data.size() < 16) + return context.error("WebPImageDecoderPlugin: ANMF chunk too small"); + + u8 const* data = anmf_chunk.data.data(); + + // "The X coordinate of the upper left corner of the frame is Frame X * 2." + u32 frame_x = ((u32)data[0] | ((u32)data[1] << 8) | ((u32)data[2] << 16)) * 2; + + // "The Y coordinate of the upper left corner of the frame is Frame Y * 2." + u32 frame_y = ((u32)data[3] | ((u32)data[4] << 8) | ((u32)data[5] << 16)) * 2; + + // "The frame width is 1 + Frame Width Minus One." + u32 frame_width = ((u32)data[6] | ((u32)data[7] << 8) | ((u32)data[8] << 16)) + 1; + + // "The frame height is 1 + Frame Height Minus One." + u32 frame_height = ((u32)data[9] | ((u32)data[10] << 8) | ((u32)data[11] << 16)) + 1; + + // "The time to wait before displaying the next frame, in 1 millisecond units. + // Note the interpretation of frame duration of 0 (and often <= 10) is implementation defined. + // Many tools and browsers assign a minimum duration similar to GIF." + u32 frame_duration = (u32)data[12] | ((u32)data[13] << 8) | ((u32)data[14] << 16); + + u8 flags = data[15]; + auto blending_method = static_cast((flags >> 1) & 1); + auto disposal_method = static_cast(flags & 1); + + ReadonlyBytes frame_data = anmf_chunk.data.slice(16); + + dbgln_if(WEBP_DEBUG, "frame_x {} frame_y {} frame_width {} frame_height {} frame_duration {} blending_method {} disposal_method {}", + frame_x, frame_y, frame_width, frame_height, frame_duration, (int)blending_method, (int)disposal_method); + + return ANMFChunk { frame_x, frame_y, frame_width, frame_height, frame_duration, blending_method, disposal_method, frame_data }; +} + // https://developers.google.com/speed/webp/docs/riff_container#extended_file_format static ErrorOr decode_webp_extended(WebPLoadingContext& context, ReadonlyBytes chunks) { @@ -1460,6 +1527,23 @@ static ErrorOr decode_webp_chunks(WebPLoadingContext& context) return {}; } +static ErrorOr decode_webp_animation_frame_chunks(WebPLoadingContext& context) +{ + if (context.state >= WebPLoadingContext::State::AnimationFrameChunksDecoded) + return {}; + + context.animation_header_chunk_data = TRY(decode_webp_chunk_ANIM(context, context.animation_header_chunk.value())); + + Vector decoded_chunks; + TRY(decoded_chunks.try_ensure_capacity(context.animation_frame_chunks.size())); + for (auto const& chunk : context.animation_frame_chunks) + TRY(decoded_chunks.try_append(TRY(decode_webp_chunk_ANMF(context, chunk)))); + context.animation_frame_chunks_data = move(decoded_chunks); + + context.state = WebPLoadingContext::State::AnimationFrameChunksDecoded; + return {}; +} + WebPImageDecoderPlugin::WebPImageDecoderPlugin(ReadonlyBytes data, OwnPtr context) : m_context(move(context)) { @@ -1530,16 +1614,12 @@ size_t WebPImageDecoderPlugin::loop_count() if (!is_animated()) return 0; - if (m_context->state < WebPLoadingContext::State::ChunksDecoded) { - if (decode_webp_chunks(*m_context).is_error()) + if (m_context->state < WebPLoadingContext::State::AnimationFrameChunksDecoded) { + if (decode_webp_animation_frame_chunks(*m_context).is_error()) return 0; } - auto anim_or_error = decode_webp_chunk_ANIM(*m_context, m_context->animation_header_chunk.value()); - if (anim_or_error.is_error()) - return 0; - - return anim_or_error.value().loop_count; + return m_context->animation_header_chunk_data->loop_count; } size_t WebPImageDecoderPlugin::frame_count() @@ -1571,8 +1651,13 @@ ErrorOr WebPImageDecoderPlugin::frame(size_t index) if (m_context->state < WebPLoadingContext::State::ChunksDecoded) TRY(decode_webp_chunks(*m_context)); - if (is_animated()) + if (is_animated()) { + if (m_context->state < WebPLoadingContext::State::AnimationFrameChunksDecoded) { + TRY(decode_webp_animation_frame_chunks(*m_context)); + } + // FIXME: Do something with the animation frames. return Error::from_string_literal("WebPImageDecoderPlugin: decoding of animated files not yet implemented"); + } if (m_context->image_data_chunk.has_value() && m_context->image_data_chunk->type == FourCC("VP8L")) { if (m_context->state < WebPLoadingContext::State::BitmapDecoded) {