ladybird/Libraries/LibGfx/ImageFormats/GIFLoader.cpp
Tim Ledbetter 3424c08b27 LibImageDecoders: Decode GIFs using wuffs
Replace the homegrown GIF parser and LZW decompressor with the wuffs
GIF decoder, which is memory-safe by construction and already used in
other engines via Skia.

One behavior change is that `loop_count()` now reports the correct
value, since the raw value stored in the file does not include the
first frane and should be incremented by 1 to be compatible with what
callers expect.
2026-06-10 20:29:11 +02:00

292 lines
10 KiB
C++

/*
* Copyright (c) 2018-2021, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Memory.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/Optional.h>
#include <AK/Vector.h>
#include <LibGfx/ImageFormats/GIFLoader.h>
#include <LibGfx/Painter.h>
#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 <wuffs/wuffs-v0.3.c>
namespace Gfx {
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 {
explicit GIFLoadingContext(ReadonlyBytes data)
: data(data)
{
}
ReadonlyBytes data;
IntSize size;
// Frame metadata, gathered without decoding pixel data.
Optional<bool> metadata_scan_succeeded;
Vector<int> frame_durations;
u32 loop_count { 0 };
OwnPtr<wuffs_gif__decoder> decoder;
wuffs_base__io_buffer io_buffer {};
wuffs_base__pixel_buffer pixel_buffer {};
RefPtr<Bitmap> canvas;
RefPtr<Bitmap> 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;
};
static ErrorOr<NonnullOwnPtr<wuffs_gif__decoder>> create_decoder(ReadonlyBytes data, wuffs_base__io_buffer& io_buffer, wuffs_base__image_config& out_image_config)
{
auto decoder = TRY(adopt_nonnull_own_or_enomem(new (nothrow) wuffs_gif__decoder()));
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);
wuffs_gif__decoder__set_quirk_enabled(decoder.ptr(), WUFFS_GIF__QUIRK_IGNORE_TOO_MUCH_PIXEL_DATA, true);
io_buffer = wuffs_base__ptr_u8__reader(const_cast<u8*>(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 ErrorOr<void> scan_frame_metadata(GIFLoadingContext& 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));
for (;;) {
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;
// 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;
}
int duration_ms = static_cast<int>(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));
}
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 {};
}
static ErrorOr<void> 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<void> 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<void> 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<float>(), 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<int>(frame_bounds.min_incl_x),
static_cast<int>(frame_bounds.min_incl_y),
static_cast<int>(wuffs_base__rect_ie_u32__width(&frame_bounds)),
static_cast<int>(wuffs_base__rect_ie_u32__height(&frame_bounds)),
};
context.current_frame_disposal = disposal;
return {};
}
GIFImageDecoderPlugin::GIFImageDecoderPlugin(ReadonlyBytes data)
: m_context(make<GIFLoadingContext>(data))
{
}
GIFImageDecoderPlugin::~GIFImageDecoderPlugin() = default;
IntSize GIFImageDecoderPlugin::size()
{
return m_context->size;
}
bool GIFImageDecoderPlugin::sniff(ReadonlyBytes data)
{
return data.starts_with("GIF87a"sv.bytes()) || data.starts_with("GIF89a"sv.bytes());
}
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> GIFImageDecoderPlugin::create(ReadonlyBytes data)
{
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<int>(wuffs_base__pixel_config__width(&image_config.pixcfg)),
static_cast<int>(wuffs_base__pixel_config__height(&image_config.pixcfg)),
};
return plugin;
}
bool GIFImageDecoderPlugin::is_animated()
{
if (ensure_frame_metadata(*m_context).is_error())
return false;
return m_context->frame_durations.size() > 1;
}
size_t GIFImageDecoderPlugin::loop_count()
{
if (ensure_frame_metadata(*m_context).is_error())
return 0;
return m_context->loop_count;
}
size_t GIFImageDecoderPlugin::frame_count()
{
if (ensure_frame_metadata(*m_context).is_error())
return 1;
return m_context->frame_durations.size();
}
size_t GIFImageDecoderPlugin::first_animated_frame_index()
{
return 0;
}
int GIFImageDecoderPlugin::frame_duration(size_t index)
{
if (ensure_frame_metadata(*m_context).is_error() || index >= m_context->frame_durations.size())
return 0;
return m_context->frame_durations[index];
}
ErrorOr<ImageFrameDescriptor> GIFImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
{
auto& context = *m_context;
TRY(ensure_frame_metadata(context));
if (index >= context.frame_durations.size())
return Error::from_string_literal("GIFImageDecoderPlugin: Invalid frame index");
if (!context.canvas || index + 1 < context.frames_decoded)
TRY(restart_pixel_decoding(context));
while (context.frames_decoded <= index)
TRY(decode_next_frame(context));
return ImageFrameDescriptor { TRY(context.canvas->clone()), context.frame_durations[index] };
}
}