diff --git a/Libraries/LibGfx/ImageFormats/GIFLoader.cpp b/Libraries/LibGfx/ImageFormats/GIFLoader.cpp index f7b4d2491e..ab7d9749c4 100644 --- a/Libraries/LibGfx/ImageFormats/GIFLoader.cpp +++ b/Libraries/LibGfx/ImageFormats/GIFLoader.cpp @@ -1,467 +1,262 @@ /* * Copyright (c) 2018-2021, Andreas Kling * Copyright (c) 2022, the SerenityOS developers. + * Copyright (c) 2026-present, the Ladybird developers. * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include -#include +#include #include -#include #include -#include -#include -#include +#include +#include +#include #include #include +#define WUFFS_IMPLEMENTATION + +#define WUFFS_CONFIG__STATIC_FUNCTIONS +#define WUFFS_CONFIG__MODULES +#define WUFFS_CONFIG__MODULE__BASE__CORE +#define WUFFS_CONFIG__MODULE__BASE__INTERFACES +#define WUFFS_CONFIG__MODULE__BASE__PIXCONV +#define WUFFS_CONFIG__MODULE__GIF +#define WUFFS_CONFIG__MODULE__LZW + +#include + namespace Gfx { -// Row strides and offsets for each interlace pass. -static constexpr Array INTERLACE_ROW_STRIDES = { 8, 8, 4, 2 }; -static constexpr Array INTERLACE_ROW_OFFSETS = { 0, 4, 2, 1 }; - -struct GIFImageDescriptor { - u16 x { 0 }; - u16 y { 0 }; - u16 width { 0 }; - u16 height { 0 }; - bool use_global_color_map { true }; - bool interlaced { false }; - Color color_map[256]; - u8 lzw_min_code_size { 0 }; - ByteBuffer lzw_encoded_bytes; - - // Fields from optional graphic control extension block - enum DisposalMethod : u8 { - None = 0, - InPlace = 1, - RestoreBackground = 2, - RestorePrevious = 3, - }; - DisposalMethod disposal_method { None }; - u8 transparency_index { 0 }; - u16 duration { 0 }; - bool transparent { false }; - bool user_input { false }; - - IntRect rect() const - { - return { this->x, this->y, this->width, this->height }; - } -}; - -struct LogicalScreen { - u16 width; - u16 height; - Color color_map[256]; -}; +static Error error_from_wuffs_status(wuffs_base__status const& status) +{ + auto const* message = wuffs_base__status__message(&status); + return Error::from_string_view(StringView { message, strlen(message) }); +} struct GIFLoadingContext { - GIFLoadingContext(FixedMemoryStream stream) - : stream(move(stream)) + explicit GIFLoadingContext(ReadonlyBytes data) + : data(data) { } - enum State { - NotDecoded = 0, - FrameDescriptorsLoaded, - FrameComplete, - }; - State state { NotDecoded }; - enum ErrorState { - NoError = 0, - FailedToDecodeAllFrames, - FailedToDecodeAnyFrame, - FailedToLoadFrameDescriptors, - }; - ErrorState error_state { NoError }; + ReadonlyBytes data; + IntSize size; - FixedMemoryStream stream; + // Frame metadata, gathered without decoding pixel data. + Optional metadata_scan_succeeded; + Vector frame_durations; + u32 loop_count { 0 }; - LogicalScreen logical_screen {}; - u8 background_color_index { 0 }; - Vector> images {}; - size_t loops { 1 }; - RefPtr frame_buffer; - size_t current_frame { 0 }; - RefPtr prev_frame_buffer; + OwnPtr decoder; + wuffs_base__io_buffer io_buffer {}; + wuffs_base__pixel_buffer pixel_buffer {}; + RefPtr canvas; + RefPtr saved_canvas; + ByteBuffer work_buffer; + size_t frames_decoded { 0 }; + wuffs_base__animation_disposal current_frame_disposal { WUFFS_BASE__ANIMATION_DISPOSAL__NONE }; + IntRect current_frame_rect; }; -enum class GIFFormat { - GIF87a, - GIF89a, -}; - -static ErrorOr decode_gif_header(Stream& stream) +static ErrorOr> create_decoder(ReadonlyBytes data, wuffs_base__io_buffer& io_buffer, wuffs_base__image_config& out_image_config) { - static auto valid_header_87 = "GIF87a"sv; - static auto valid_header_89 = "GIF89a"sv; + auto decoder = TRY(adopt_nonnull_own_or_enomem(new (nothrow) wuffs_gif__decoder())); - Array header; - TRY(stream.read_until_filled(header)); + auto status = wuffs_gif__decoder__initialize(decoder.ptr(), sizeof__wuffs_gif__decoder(), WUFFS_VERSION, WUFFS_INITIALIZE__ALREADY_ZEROED); + if (!wuffs_base__status__is_ok(&status)) + return error_from_wuffs_status(status); - if (header.span() == valid_header_87.bytes()) - return GIFFormat::GIF87a; - if (header.span() == valid_header_89.bytes()) - return GIFFormat::GIF89a; + wuffs_gif__decoder__set_quirk_enabled(decoder.ptr(), WUFFS_GIF__QUIRK_IGNORE_TOO_MUCH_PIXEL_DATA, true); - return Error::from_string_literal("GIF header unknown"); + io_buffer = wuffs_base__ptr_u8__reader(const_cast(data.data()), data.size(), true); + + status = wuffs_gif__decoder__decode_image_config(decoder.ptr(), &out_image_config, &io_buffer); + if (!wuffs_base__status__is_ok(&status)) + return error_from_wuffs_status(status); + + return decoder; } -static void copy_frame_buffer(Bitmap& dest, Bitmap const& src) +static ErrorOr scan_frame_metadata(GIFLoadingContext& context) { - VERIFY(dest.size_in_bytes() == src.size_in_bytes()); - memcpy(dest.scanline(0), src.scanline(0), dest.size_in_bytes()); -} + wuffs_base__io_buffer io_buffer {}; + auto image_config = wuffs_base__null_image_config(); + auto decoder = TRY(create_decoder(context.data, io_buffer, image_config)); -static void clear_rect(Bitmap& bitmap, IntRect const& rect, Color color) -{ - auto intersection_rect = rect.intersected(bitmap.rect()); - if (intersection_rect.is_empty()) - return; - - RawPixel* dst = bitmap.scanline(intersection_rect.top()) + intersection_rect.left(); - size_t const dst_skip = bitmap.pitch() / sizeof(RawPixel); - - auto const value = color.value(); - auto const width = intersection_rect.width(); - for (int i = intersection_rect.height() - 1; i >= 0; --i) { - for (RawPixel* p = dst; p < (dst + width); ++p) { - *p = value; - } - dst += dst_skip; - } -} - -static ErrorOr decode_frame(GIFLoadingContext& context, size_t frame_index) -{ - if (frame_index >= context.images.size()) { - return Error::from_string_literal("frame_index size too high"); - } - - if (context.state >= GIFLoadingContext::State::FrameComplete && frame_index == context.current_frame) { - return {}; - } - - size_t start_frame = context.current_frame + 1; - if (context.state < GIFLoadingContext::State::FrameComplete) { - start_frame = 0; - context.frame_buffer = TRY(Bitmap::create(BitmapFormat::BGRA8888, { context.logical_screen.width, context.logical_screen.height })); - context.prev_frame_buffer = TRY(Bitmap::create(BitmapFormat::BGRA8888, { context.logical_screen.width, context.logical_screen.height })); - - } else if (frame_index < context.current_frame) { - start_frame = 0; - } - - for (size_t i = start_frame; i <= frame_index; ++i) { - auto& image = context.images.at(i); - - auto const previous_image_disposal_method = i > 0 ? context.images.at(i - 1)->disposal_method : GIFImageDescriptor::DisposalMethod::None; - - if (i == 0) { - auto painter = Gfx::Painter::create(*context.frame_buffer); - painter->clear_rect(context.frame_buffer->rect().to_type(), Color::Transparent); - } else if (i > 0 && image->disposal_method == GIFImageDescriptor::DisposalMethod::RestorePrevious - && previous_image_disposal_method != GIFImageDescriptor::DisposalMethod::RestorePrevious) { - // This marks the start of a run of frames that once disposed should be restored to the - // previous underlying image contents. Therefore we make a copy of the current frame - // buffer so that it can be restored later. - copy_frame_buffer(*context.prev_frame_buffer, *context.frame_buffer); - } - - if (previous_image_disposal_method == GIFImageDescriptor::DisposalMethod::RestoreBackground) { - // Note: RestoreBackground could be interpreted either as restoring the underlying - // background of the entire image (e.g. container element's background-color), or the - // background color of the GIF itself. It appears that all major browsers and most other - // GIF decoders adhere to the former interpretation, therefore we will do the same by - // clearing the entire frame buffer to transparent. - clear_rect(*context.frame_buffer, context.images[i - 1]->rect(), Color::Transparent); - } else if (i > 0 && previous_image_disposal_method == GIFImageDescriptor::DisposalMethod::RestorePrevious) { - // Previous frame indicated that once disposed, it should be restored to *its* previous - // underlying image contents, therefore we restore the saved previous frame buffer. - copy_frame_buffer(*context.frame_buffer, *context.prev_frame_buffer); - } - - if (image->lzw_min_code_size > 8) - return Error::from_string_literal("LZW minimum code size is greater than 8"); - - ByteBuffer decoded_stream; - if (!image->lzw_encoded_bytes.is_empty()) { - decoded_stream = TRY(Compress::LzwDecompressor::decompress_all(image->lzw_encoded_bytes, image->lzw_min_code_size)); - } - - auto const& color_map = image->use_global_color_map ? context.logical_screen.color_map : image->color_map; - - int pixel_index = 0; - int row = 0; - int interlace_pass = 0; - - if (!image->width) - continue; - - for (auto const& color : decoded_stream.bytes()) { - auto c = color_map[color]; - - int x = pixel_index % image->width + image->x; - int y = row + image->y; - - if (context.frame_buffer->rect().contains(x, y) && (!image->transparent || color != image->transparency_index)) { - context.frame_buffer->set_pixel(x, y, c); - } - - ++pixel_index; - if (pixel_index % image->width == 0) { - if (image->interlaced) { - if (interlace_pass < 4) { - if (row + INTERLACE_ROW_STRIDES[interlace_pass] >= image->height) { - ++interlace_pass; - if (interlace_pass < 4) - row = INTERLACE_ROW_OFFSETS[interlace_pass]; - } else { - row += INTERLACE_ROW_STRIDES[interlace_pass]; - } - } - } else { - ++row; - } - } - } - - context.current_frame = i; - context.state = GIFLoadingContext::State::FrameComplete; - } - - return {}; -} - -static ErrorOr load_header_and_logical_screen(GIFLoadingContext& context) -{ - if (TRY(context.stream.size()) < 26) - return Error::from_string_literal("Size too short for minimal GIF structure"); - - TRY(decode_gif_header(context.stream)); - - context.logical_screen.width = TRY(context.stream.read_value>()); - context.logical_screen.height = TRY(context.stream.read_value>()); - - auto packed_fields = TRY(context.stream.read_value()); - context.background_color_index = TRY(context.stream.read_value()); - [[maybe_unused]] auto pixel_aspect_ratio = TRY(context.stream.read_value()); - - // Global Color Table; if the flag is set, the Global Color Table will - // immediately follow the Logical Screen Descriptor. - bool global_color_table_flag = packed_fields & 0x80; - - if (global_color_table_flag) { - u8 bits_per_pixel = (packed_fields & 7) + 1; - size_t color_map_entry_count = 1 << bits_per_pixel; - - for (size_t i = 0; i < color_map_entry_count; ++i) { - u8 r = TRY(context.stream.read_value()); - u8 g = TRY(context.stream.read_value()); - u8 b = TRY(context.stream.read_value()); - context.logical_screen.color_map[i] = { r, g, b }; - } - } - - return {}; -} - -static ErrorOr load_gif_frame_descriptors(GIFLoadingContext& context) -{ - NonnullOwnPtr current_image = make(); for (;;) { - u8 sentinel = TRY(context.stream.read_value()); + auto frame_config = wuffs_base__null_frame_config(); + auto status = wuffs_gif__decoder__decode_frame_config(decoder.ptr(), &frame_config, &io_buffer); + if (status.repr == wuffs_base__note__end_of_data) + break; - if (sentinel == '!') { - u8 extension_type = TRY(context.stream.read_value()); - - u8 sub_block_length = 0; - - Vector sub_block {}; - for (;;) { - sub_block_length = TRY(context.stream.read_value()); - if (sub_block_length == 0) - break; - - TRY(sub_block.try_resize(sub_block.size() + sub_block_length)); - TRY(context.stream.read_until_filled(sub_block.span().slice_from_end(sub_block_length))); - } - - if (extension_type == 0xF9) { - if (sub_block.size() != 4) { - dbgln_if(GIF_DEBUG, "Unexpected graphic control size"); - continue; - } - - u8 disposal_method = (sub_block[0] & 0x1C) >> 2; - current_image->disposal_method = (GIFImageDescriptor::DisposalMethod)disposal_method; - - u8 user_input = (sub_block[0] & 0x2) >> 1; - current_image->user_input = user_input == 1; - - u8 transparent = sub_block[0] & 1; - current_image->transparent = transparent == 1; - - u16 duration = sub_block[1] + ((u16)sub_block[2] << 8); - current_image->duration = duration; - - current_image->transparency_index = sub_block[3]; - - dbgln_if(GIF_DEBUG, "Graphic control: disposal_method={}, user_input={}, transparent={}, duration={}", (int)current_image->disposal_method, current_image->user_input, current_image->transparent, current_image->duration); - } - - if (extension_type == 0xFF) { - if (sub_block.size() != 14) { - dbgln_if(GIF_DEBUG, "Unexpected application extension size: {}", sub_block.size()); - continue; - } - - if (sub_block[11] != 1) { - dbgln_if(GIF_DEBUG, "Unexpected application extension format"); - continue; - } - - u16 loops = sub_block[12] + (sub_block[13] << 8); - context.loops = loops; - - dbgln_if(GIF_DEBUG, "Application extension: loops={}", context.loops); - } - - continue; - } - - if (sentinel == ',') { - context.images.append(move(current_image)); - auto& image = context.images.last(); - - image->x = TRY(context.stream.read_value>()); - image->y = TRY(context.stream.read_value>()); - image->width = TRY(context.stream.read_value>()); - image->height = TRY(context.stream.read_value>()); - - auto packed_fields = TRY(context.stream.read_value()); - - image->use_global_color_map = !(packed_fields & 0x80); - image->interlaced = (packed_fields & 0x40) != 0; - - dbgln_if(GIF_DEBUG, "Image descriptor: x={}, y={}, width={}, height={}, use_global_color_map={}, local_map_size_exponent={}, interlaced={}", image->x, image->y, image->width, image->height, image->use_global_color_map, (packed_fields & 7) + 1, image->interlaced); - - if (!image->use_global_color_map) { - size_t local_color_table_size = AK::exp2((packed_fields & 7) + 1); - - for (size_t i = 0; i < local_color_table_size; ++i) { - u8 r = TRY(context.stream.read_value()); - u8 g = TRY(context.stream.read_value()); - u8 b = TRY(context.stream.read_value()); - image->color_map[i] = { r, g, b }; - } - } - - image->lzw_min_code_size = TRY(context.stream.read_value()); - - for (;;) { - auto const lzw_encoded_bytes_expected = TRY(context.stream.read_value()); - - // Block terminator - if (lzw_encoded_bytes_expected == 0) - break; - - auto const lzw_subblock = TRY(image->lzw_encoded_bytes.get_bytes_for_writing(lzw_encoded_bytes_expected)); - TRY(context.stream.read_until_filled(lzw_subblock)); - } - - current_image = make(); - continue; - } - - if (sentinel == ';') { + // Tolerate corrupted or truncated data after at least one frame has been seen. + if (!wuffs_base__status__is_ok(&status)) { + if (context.frame_durations.is_empty()) + return error_from_wuffs_status(status); break; } - return Error::from_string_literal("Unexpected sentinel"); + int duration_ms = static_cast(wuffs_base__frame_config__duration(&frame_config) / WUFFS_BASE__FLICKS_PER_MILLISECOND); + if (duration_ms <= 10) + duration_ms = 100; + TRY(context.frame_durations.try_append(duration_ms)); } - context.state = GIFLoadingContext::State::FrameDescriptorsLoaded; + if (context.frame_durations.is_empty()) + return Error::from_string_literal("GIFImageDecoderPlugin: No frames could be decoded"); + + context.loop_count = wuffs_gif__decoder__num_animation_loops(decoder.ptr()); + return {}; } -GIFImageDecoderPlugin::GIFImageDecoderPlugin(FixedMemoryStream stream) +static ErrorOr ensure_frame_metadata(GIFLoadingContext& context) +{ + if (!context.metadata_scan_succeeded.has_value()) + context.metadata_scan_succeeded = !scan_frame_metadata(context).is_error(); + + if (!*context.metadata_scan_succeeded) + return Error::from_string_literal("GIFImageDecoderPlugin: Decoding failed"); + + return {}; +} + +static ErrorOr restart_pixel_decoding(GIFLoadingContext& context) +{ + auto image_config = wuffs_base__null_image_config(); + context.decoder = TRY(create_decoder(context.data, context.io_buffer, image_config)); + + if (context.canvas) { + memset(context.canvas->scanline_u8(0), 0, context.canvas->size_in_bytes()); + } else { + context.canvas = TRY(Bitmap::create(BitmapFormat::BGRA8888, AlphaType::Unpremultiplied, context.size)); + + auto pixel_config = wuffs_base__null_pixel_config(); + wuffs_base__pixel_config__set(&pixel_config, WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL, WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, context.canvas->width(), context.canvas->height()); + + auto status = wuffs_base__pixel_buffer__set_from_slice(&context.pixel_buffer, &pixel_config, wuffs_base__make_slice_u8(context.canvas->scanline_u8(0), context.canvas->size_in_bytes())); + if (!wuffs_base__status__is_ok(&status)) + return error_from_wuffs_status(status); + + auto work_buffer_length = wuffs_gif__decoder__workbuf_len(context.decoder.ptr()).max_incl; + context.work_buffer = TRY(ByteBuffer::create_uninitialized(work_buffer_length)); + } + + context.frames_decoded = 0; + context.current_frame_disposal = WUFFS_BASE__ANIMATION_DISPOSAL__NONE; + + return {}; +} + +static ErrorOr decode_next_frame(GIFLoadingContext& context) +{ + if (context.frames_decoded > 0) { + switch (context.current_frame_disposal) { + case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND: + Painter::create(*context.canvas)->clear_rect(context.current_frame_rect.to_type(), Color::Transparent); + break; + case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS: + memcpy(context.canvas->scanline_u8(0), context.saved_canvas->scanline_u8(0), context.canvas->size_in_bytes()); + break; + default: + break; + } + } + + auto frame_config = wuffs_base__null_frame_config(); + auto status = wuffs_gif__decoder__decode_frame_config(context.decoder.ptr(), &frame_config, &context.io_buffer); + if (!wuffs_base__status__is_ok(&status)) + return error_from_wuffs_status(status); + + auto disposal = wuffs_base__frame_config__disposal(&frame_config); + if (disposal == WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS && context.current_frame_disposal != WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS) { + if (context.saved_canvas) { + memcpy(context.saved_canvas->scanline_u8(0), context.canvas->scanline_u8(0), context.canvas->size_in_bytes()); + } else { + context.saved_canvas = TRY(context.canvas->clone()); + } + } + + auto blend = wuffs_base__frame_config__overwrite_instead_of_blend(&frame_config) + ? WUFFS_BASE__PIXEL_BLEND__SRC + : WUFFS_BASE__PIXEL_BLEND__SRC_OVER; + status = wuffs_gif__decoder__decode_frame(context.decoder.ptr(), &context.pixel_buffer, &context.io_buffer, blend, wuffs_base__make_slice_u8(context.work_buffer.data(), context.work_buffer.size()), nullptr); + + if (!wuffs_base__status__is_ok(&status) && context.frames_decoded > 0) + return error_from_wuffs_status(status); + + ++context.frames_decoded; + + auto frame_bounds = wuffs_base__frame_config__bounds(&frame_config); + context.current_frame_rect = { + static_cast(frame_bounds.min_incl_x), + static_cast(frame_bounds.min_incl_y), + static_cast(wuffs_base__rect_ie_u32__width(&frame_bounds)), + static_cast(wuffs_base__rect_ie_u32__height(&frame_bounds)), + }; + context.current_frame_disposal = disposal; + + return {}; +} + +GIFImageDecoderPlugin::GIFImageDecoderPlugin(ReadonlyBytes data) + : m_context(make(data)) { - m_context = make(move(stream)); } GIFImageDecoderPlugin::~GIFImageDecoderPlugin() = default; IntSize GIFImageDecoderPlugin::size() { - return { m_context->logical_screen.width, m_context->logical_screen.height }; + return m_context->size; } bool GIFImageDecoderPlugin::sniff(ReadonlyBytes data) { - FixedMemoryStream stream { data }; - return !decode_gif_header(stream).is_error(); + return data.starts_with("GIF87a"sv.bytes()) || data.starts_with("GIF89a"sv.bytes()); } ErrorOr> GIFImageDecoderPlugin::create(ReadonlyBytes data) { - FixedMemoryStream stream { data }; - auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) GIFImageDecoderPlugin(move(stream)))); - TRY(load_header_and_logical_screen(*plugin->m_context)); + auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) GIFImageDecoderPlugin(data))); + auto& context = *plugin->m_context; + + wuffs_base__io_buffer io_buffer {}; + auto image_config = wuffs_base__null_image_config(); + auto decoder = TRY(create_decoder(context.data, io_buffer, image_config)); + + context.size = { + static_cast(wuffs_base__pixel_config__width(&image_config.pixcfg)), + static_cast(wuffs_base__pixel_config__height(&image_config.pixcfg)), + }; + return plugin; } bool GIFImageDecoderPlugin::is_animated() { - if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) { + if (ensure_frame_metadata(*m_context).is_error()) return false; - } - if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { - if (load_gif_frame_descriptors(*m_context).is_error()) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; - return false; - } - } - - return m_context->images.size() > 1; + return m_context->frame_durations.size() > 1; } size_t GIFImageDecoderPlugin::loop_count() { - if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) { + if (ensure_frame_metadata(*m_context).is_error()) return 0; - } - if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { - if (load_gif_frame_descriptors(*m_context).is_error()) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; - return 0; - } - } - - return m_context->loops; + return m_context->loop_count; } size_t GIFImageDecoderPlugin::frame_count() { - if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) { + if (ensure_frame_metadata(*m_context).is_error()) return 1; - } - if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { - if (load_gif_frame_descriptors(*m_context).is_error()) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; - return 1; - } - } - - return m_context->images.size(); + return m_context->frame_durations.size(); } size_t GIFImageDecoderPlugin::first_animated_frame_index() @@ -471,63 +266,27 @@ size_t GIFImageDecoderPlugin::first_animated_frame_index() int GIFImageDecoderPlugin::frame_duration(size_t index) { - if (m_context->error_state != GIFLoadingContext::ErrorState::NoError) + if (ensure_frame_metadata(*m_context).is_error() || index >= m_context->frame_durations.size()) return 0; - if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { - if (load_gif_frame_descriptors(*m_context).is_error()) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; - return 0; - } - } - - if (index >= m_context->images.size()) - return 0; - - int duration = m_context->images[index]->duration * 10; - if (duration <= 10) - duration = 100; - return duration; + return m_context->frame_durations[index]; } ErrorOr GIFImageDecoderPlugin::frame(size_t index, Optional) { - if (m_context->error_state == GIFLoadingContext::ErrorState::FailedToDecodeAnyFrame) { - return Error::from_string_literal("GIFImageDecoderPlugin: Decoding failed"); - } + auto& context = *m_context; + TRY(ensure_frame_metadata(context)); - if (m_context->state < GIFLoadingContext::State::FrameDescriptorsLoaded) { - if (auto result = load_gif_frame_descriptors(*m_context); result.is_error()) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToLoadFrameDescriptors; + if (index >= context.frame_durations.size()) + return Error::from_string_literal("GIFImageDecoderPlugin: Invalid frame index"); - // If we failed to load frame descriptors but we have some images, we can still try to decode them. - if (m_context->images.is_empty()) { - return result.release_error(); - } - } - } + if (!context.canvas || index + 1 < context.frames_decoded) + TRY(restart_pixel_decoding(context)); - if (auto result = decode_frame(*m_context, index); result.is_error()) { - if (m_context->state < GIFLoadingContext::State::FrameComplete) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToDecodeAnyFrame; - return result.release_error(); - } - if (result = decode_frame(*m_context, 0); result.is_error()) { - m_context->error_state = GIFLoadingContext::ErrorState::FailedToDecodeAnyFrame; - return result.release_error(); - } - m_context->error_state = GIFLoadingContext::ErrorState::FailedToDecodeAllFrames; - } + while (context.frames_decoded <= index) + TRY(decode_next_frame(context)); - ImageFrameDescriptor frame { - .image = TRY(m_context->frame_buffer->clone()), - .duration = m_context->images[index]->duration * 10, - }; - - if (frame.duration <= 10) - frame.duration = 100; - - return frame; + return ImageFrameDescriptor { TRY(context.canvas->clone()), context.frame_durations[index] }; } } diff --git a/Libraries/LibGfx/ImageFormats/GIFLoader.h b/Libraries/LibGfx/ImageFormats/GIFLoader.h index 34e6d81408..e602d3cef6 100644 --- a/Libraries/LibGfx/ImageFormats/GIFLoader.h +++ b/Libraries/LibGfx/ImageFormats/GIFLoader.h @@ -6,7 +6,6 @@ #pragma once -#include #include namespace Gfx { @@ -32,7 +31,7 @@ public: virtual int frame_duration(size_t index) override; private: - GIFImageDecoderPlugin(FixedMemoryStream); + explicit GIFImageDecoderPlugin(ReadonlyBytes); OwnPtr m_context; }; diff --git a/Libraries/LibImageDecoders/CMakeLists.txt b/Libraries/LibImageDecoders/CMakeLists.txt index f642eabbed..8d1c5b4afb 100644 --- a/Libraries/LibImageDecoders/CMakeLists.txt +++ b/Libraries/LibImageDecoders/CMakeLists.txt @@ -35,6 +35,8 @@ target_link_libraries(LibImageDecoders PRIVATE avif ) +target_include_directories(LibImageDecoders PRIVATE ${WUFFS_INCLUDE_DIR}) + set(generated_sources TIFFMetadata.h TIFFTagHandler.cpp) list(TRANSFORM generated_sources PREPEND "${CMAKE_BINARY_DIR}/Libraries/LibGfx/ImageFormats/") diff --git a/Meta/CMake/check_for_dependencies.cmake b/Meta/CMake/check_for_dependencies.cmake index fd4ccf1119..f30fe34d2f 100644 --- a/Meta/CMake/check_for_dependencies.cmake +++ b/Meta/CMake/check_for_dependencies.cmake @@ -21,6 +21,7 @@ find_package(JPEG REQUIRED) find_package(LIBAVIF REQUIRED) find_package(PNG REQUIRED) find_package(WebP REQUIRED) +find_path(WUFFS_INCLUDE_DIR NAMES wuffs/wuffs-v0.3.c REQUIRED) pkg_check_modules(WOFF2 REQUIRED IMPORTED_TARGET libwoff2dec) diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 0b53ca5b34..95776a6b30 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -169,6 +169,33 @@ TEST_CASE(test_gif) EXPECT(frame.duration == 400); } +TEST_CASE(test_gif_loop_count_from_netscape_extension) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("gif/loop-count-2.gif"sv))); + EXPECT(Gfx::GIFImageDecoderPlugin::sniff(file->bytes())); + auto plugin_decoder = TRY_OR_FAIL(Gfx::GIFImageDecoderPlugin::create(file->bytes())); + + EXPECT_EQ(plugin_decoder->frame_count(), 2u); + EXPECT(plugin_decoder->is_animated()); + + EXPECT_EQ(plugin_decoder->loop_count(), 3u); +} + +TEST_CASE(test_gif_corrupt_second_frame) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("gif/corrupt-second-frame.gif"sv))); + EXPECT(Gfx::GIFImageDecoderPlugin::sniff(file->bytes())); + auto plugin_decoder = TRY_OR_FAIL(Gfx::GIFImageDecoderPlugin::create(file->bytes())); + + EXPECT_EQ(plugin_decoder->frame_count(), 2u); + + auto first_frame = TRY_OR_FAIL(plugin_decoder->frame(0)); + EXPECT_EQ(first_frame.image->get_pixel(0, 0), Gfx::Color::NamedColor::Red); + + EXPECT(plugin_decoder->frame(1).is_error()); + EXPECT(!plugin_decoder->frame(0).is_error()); +} + TEST_CASE(test_corrupted_gif) { auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("gif/corrupted.gif"sv))); diff --git a/Tests/LibGfx/test-inputs/gif/corrupt-second-frame.gif b/Tests/LibGfx/test-inputs/gif/corrupt-second-frame.gif new file mode 100644 index 0000000000..c7da504a51 Binary files /dev/null and b/Tests/LibGfx/test-inputs/gif/corrupt-second-frame.gif differ diff --git a/Tests/LibGfx/test-inputs/gif/loop-count-2.gif b/Tests/LibGfx/test-inputs/gif/loop-count-2.gif new file mode 100644 index 0000000000..044666ab52 Binary files /dev/null and b/Tests/LibGfx/test-inputs/gif/loop-count-2.gif differ