2023-02-24 00:37:08 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2023, Nico Weber <thakis@chromium.org>
|
2024-07-12 05:08:39 -03:00
|
|
|
* Copyright (c) 2024, doctortheemh <doctortheemh@gmail.com>
|
2023-02-24 00:37:08 -03:00
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
#include <AK/Error.h>
|
2023-03-21 15:58:06 -03:00
|
|
|
#include <LibGfx/ImageFormats/WebPLoader.h>
|
2023-02-24 00:37:08 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
#include <webp/decode.h>
|
|
|
|
|
#include <webp/demux.h>
|
|
|
|
|
#include <webp/mux.h>
|
2023-02-24 00:37:08 -03:00
|
|
|
|
|
|
|
|
namespace Gfx {
|
|
|
|
|
|
|
|
|
|
struct WebPLoadingContext {
|
|
|
|
|
enum State {
|
|
|
|
|
NotDecoded = 0,
|
|
|
|
|
Error,
|
|
|
|
|
HeaderDecoded,
|
|
|
|
|
BitmapDecoded,
|
|
|
|
|
};
|
2024-07-12 05:08:39 -03:00
|
|
|
|
2023-02-24 00:37:08 -03:00
|
|
|
State state { State::NotDecoded };
|
|
|
|
|
ReadonlyBytes data;
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
// Image properties
|
|
|
|
|
IntSize size;
|
|
|
|
|
bool has_alpha;
|
|
|
|
|
bool has_animation;
|
|
|
|
|
size_t frame_count;
|
|
|
|
|
size_t loop_count;
|
|
|
|
|
ByteBuffer icc_data;
|
2026-02-13 10:59:27 -03:00
|
|
|
Vector<int> frame_durations;
|
2023-02-25 18:14:05 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
Vector<ImageFrameDescriptor> frame_descriptors;
|
2026-02-13 10:59:37 -03:00
|
|
|
|
|
|
|
|
// Incremental animation decoder state.
|
|
|
|
|
WebPAnimDecoder* anim_decoder { nullptr };
|
|
|
|
|
int anim_old_timestamp { 0 };
|
|
|
|
|
size_t anim_frames_decoded { 0 };
|
|
|
|
|
|
|
|
|
|
~WebPLoadingContext()
|
|
|
|
|
{
|
|
|
|
|
if (anim_decoder)
|
|
|
|
|
WebPAnimDecoderDelete(anim_decoder);
|
|
|
|
|
}
|
2023-02-24 00:37:08 -03:00
|
|
|
};
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
WebPImageDecoderPlugin::WebPImageDecoderPlugin(ReadonlyBytes data, OwnPtr<WebPLoadingContext> context)
|
|
|
|
|
: m_context(move(context))
|
2023-05-05 15:51:53 -03:00
|
|
|
{
|
2024-07-12 05:08:39 -03:00
|
|
|
m_context->data = data;
|
2023-05-05 15:51:53 -03:00
|
|
|
}
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
WebPImageDecoderPlugin::~WebPImageDecoderPlugin() = default;
|
2023-05-07 17:27:45 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
IntSize WebPImageDecoderPlugin::size()
|
2023-02-24 00:37:08 -03:00
|
|
|
{
|
2024-07-12 05:08:39 -03:00
|
|
|
return m_context->size;
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
static ErrorOr<void> decode_webp_header(WebPLoadingContext& context)
|
2023-02-24 00:37:08 -03:00
|
|
|
{
|
2024-07-12 05:08:39 -03:00
|
|
|
if (context.state >= WebPLoadingContext::HeaderDecoded)
|
2023-02-24 00:37:08 -03:00
|
|
|
return {};
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
int width = 0;
|
|
|
|
|
int height = 0;
|
|
|
|
|
int getinfo_result = WebPGetInfo(context.data.data(), context.data.size(), &width, &height);
|
|
|
|
|
if (getinfo_result != 1)
|
|
|
|
|
return Error::from_string_literal("Failed to decode webp image data");
|
2023-02-25 18:14:05 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
WebPBitstreamFeatures webp_bitstream_features {};
|
|
|
|
|
VP8StatusCode vp8_result = WebPGetFeatures(context.data.data(), context.data.size(), &webp_bitstream_features);
|
|
|
|
|
if (vp8_result != VP8_STATUS_OK)
|
|
|
|
|
return Error::from_string_literal("Failed to get WebP bitstream features");
|
2023-02-25 18:14:05 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
// Image header now decoded, save some results for fast access in other parts of the plugin.
|
|
|
|
|
context.size = IntSize { width, height };
|
|
|
|
|
context.has_animation = webp_bitstream_features.has_animation;
|
|
|
|
|
context.has_alpha = webp_bitstream_features.has_alpha;
|
|
|
|
|
context.frame_count = 1;
|
|
|
|
|
context.loop_count = 0;
|
2023-02-24 00:37:08 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
if (webp_bitstream_features.has_animation == 1) {
|
|
|
|
|
WebPAnimDecoderOptions anim_decoder_options {};
|
|
|
|
|
WebPAnimDecoderOptionsInit(&anim_decoder_options);
|
|
|
|
|
anim_decoder_options.color_mode = MODE_BGRA;
|
|
|
|
|
anim_decoder_options.use_threads = 1;
|
2023-02-25 18:14:05 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
WebPData webp_data { .bytes = context.data.data(), .size = context.data.size() };
|
|
|
|
|
auto* anim_decoder = WebPAnimDecoderNew(&webp_data, &anim_decoder_options);
|
|
|
|
|
if (anim_decoder == nullptr)
|
|
|
|
|
return Error::from_string_literal("Failed to allocate WebPAnimDecoderNew failed");
|
|
|
|
|
ScopeGuard guard { [=]() { WebPAnimDecoderDelete(anim_decoder); } };
|
2023-02-25 18:14:05 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
WebPAnimInfo anim_info {};
|
|
|
|
|
int anim_getinfo_result = WebPAnimDecoderGetInfo(anim_decoder, &anim_info);
|
|
|
|
|
if (anim_getinfo_result != 1)
|
|
|
|
|
return Error::from_string_literal("Failed to get WebP animation info");
|
2023-02-25 18:14:05 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
context.frame_count = anim_info.frame_count;
|
|
|
|
|
context.loop_count = anim_info.loop_count;
|
2026-02-13 10:59:27 -03:00
|
|
|
|
|
|
|
|
// Extract frame durations from the demuxer without decoding pixels.
|
|
|
|
|
auto* demux = WebPDemux(&webp_data);
|
|
|
|
|
if (demux) {
|
|
|
|
|
WebPIterator iter {};
|
|
|
|
|
if (WebPDemuxGetFrame(demux, 1, &iter)) {
|
|
|
|
|
do {
|
|
|
|
|
context.frame_durations.append(iter.duration);
|
|
|
|
|
} while (WebPDemuxNextFrame(&iter));
|
|
|
|
|
WebPDemuxReleaseIterator(&iter);
|
|
|
|
|
}
|
|
|
|
|
WebPDemuxDelete(demux);
|
|
|
|
|
}
|
2023-02-25 18:14:05 -03:00
|
|
|
}
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
WebPData webp_data { .bytes = context.data.data(), .size = context.data.size() };
|
2026-06-22 11:50:46 -03:00
|
|
|
if (WebPMux* mux = WebPMuxCreate(&webp_data, 0)) {
|
|
|
|
|
ScopeGuard guard { [=]() { WebPMuxDelete(mux); } };
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
}
|
2023-05-06 07:04:11 -03:00
|
|
|
}
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
context.state = WebPLoadingContext::State::HeaderDecoded;
|
|
|
|
|
return {};
|
2023-05-06 07:04:11 -03:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
static ErrorOr<void> ensure_anim_decoder(WebPLoadingContext& context)
|
2023-05-06 17:15:51 -03:00
|
|
|
{
|
2026-02-13 10:59:37 -03:00
|
|
|
if (context.anim_decoder)
|
|
|
|
|
return {};
|
2024-05-07 19:00:33 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
WebPAnimDecoderOptions options {};
|
|
|
|
|
WebPAnimDecoderOptionsInit(&options);
|
|
|
|
|
options.color_mode = MODE_BGRA;
|
|
|
|
|
options.use_threads = 1;
|
2023-05-06 17:15:51 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
WebPData webp_data { .bytes = context.data.data(), .size = context.data.size() };
|
|
|
|
|
context.anim_decoder = WebPAnimDecoderNew(&webp_data, &options);
|
|
|
|
|
if (!context.anim_decoder)
|
|
|
|
|
return Error::from_string_literal("Failed to create WebPAnimDecoder");
|
|
|
|
|
|
|
|
|
|
context.anim_old_timestamp = 0;
|
|
|
|
|
context.anim_frames_decoded = 0;
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2023-05-06 17:34:45 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
static ErrorOr<ImageFrameDescriptor> decode_next_webp_animation_frame(WebPLoadingContext& context)
|
|
|
|
|
{
|
|
|
|
|
TRY(ensure_anim_decoder(context));
|
2023-05-06 07:04:11 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
if (!WebPAnimDecoderHasMoreFrames(context.anim_decoder))
|
|
|
|
|
return Error::from_string_literal("No more frames to decode");
|
2023-05-06 07:04:11 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
uint8_t* frame_data = nullptr;
|
|
|
|
|
int timestamp = 0;
|
|
|
|
|
if (!WebPAnimDecoderGetNext(context.anim_decoder, &frame_data, ×tamp))
|
|
|
|
|
return Error::from_string_literal("Failed to decode animated frame");
|
2023-05-06 07:04:11 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
auto bitmap_format = context.has_alpha ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888;
|
|
|
|
|
auto bitmap = TRY(Bitmap::create(bitmap_format, Gfx::AlphaType::Unpremultiplied, context.size));
|
|
|
|
|
memcpy(bitmap->scanline_u8(0), frame_data, context.size.width() * context.size.height() * 4);
|
2023-05-06 07:04:11 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
auto duration = timestamp - context.anim_old_timestamp;
|
|
|
|
|
context.anim_old_timestamp = timestamp;
|
|
|
|
|
++context.anim_frames_decoded;
|
2023-05-06 07:04:11 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
return ImageFrameDescriptor { bitmap, duration };
|
|
|
|
|
}
|
2023-02-24 00:37:08 -03:00
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
static ErrorOr<void> decode_webp_image(WebPLoadingContext& context)
|
|
|
|
|
{
|
|
|
|
|
VERIFY(context.state >= WebPLoadingContext::State::HeaderDecoded);
|
|
|
|
|
VERIFY(!context.has_animation);
|
|
|
|
|
|
|
|
|
|
auto bitmap_format = context.has_alpha ? BitmapFormat::BGRA8888 : BitmapFormat::BGRx8888;
|
|
|
|
|
auto bitmap = TRY(Bitmap::create(bitmap_format, Gfx::AlphaType::Unpremultiplied, context.size));
|
|
|
|
|
|
|
|
|
|
auto image_data = WebPDecodeBGRAInto(context.data.data(), context.data.size(), bitmap->scanline_u8(0), bitmap->data_size(), bitmap->pitch());
|
|
|
|
|
if (image_data == nullptr)
|
|
|
|
|
return Error::from_string_literal("Failed to decode webp image into bitmap");
|
|
|
|
|
|
|
|
|
|
context.frame_descriptors.append(ImageFrameDescriptor { bitmap, 0 });
|
2023-05-07 21:38:25 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
return {};
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
2023-02-26 15:02:50 -03:00
|
|
|
bool WebPImageDecoderPlugin::sniff(ReadonlyBytes data)
|
2023-02-24 00:37:08 -03:00
|
|
|
{
|
|
|
|
|
WebPLoadingContext context;
|
|
|
|
|
context.data = data;
|
2023-02-26 14:52:56 -03:00
|
|
|
return !decode_webp_header(context).is_error();
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> WebPImageDecoderPlugin::create(ReadonlyBytes data)
|
|
|
|
|
{
|
|
|
|
|
auto context = TRY(try_make<WebPLoadingContext>());
|
2023-07-14 19:33:14 -03:00
|
|
|
auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) WebPImageDecoderPlugin(data, move(context))));
|
|
|
|
|
TRY(decode_webp_header(*plugin->m_context));
|
|
|
|
|
return plugin;
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool WebPImageDecoderPlugin::is_animated()
|
|
|
|
|
{
|
2024-07-12 05:08:39 -03:00
|
|
|
return m_context->has_animation;
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t WebPImageDecoderPlugin::loop_count()
|
|
|
|
|
{
|
2023-02-26 11:13:27 -03:00
|
|
|
if (!is_animated())
|
|
|
|
|
return 0;
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
return m_context->loop_count;
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t WebPImageDecoderPlugin::frame_count()
|
|
|
|
|
{
|
2023-02-26 00:15:34 -03:00
|
|
|
if (!is_animated())
|
|
|
|
|
return 1;
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
return m_context->frame_count;
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
2023-04-07 23:41:22 -03:00
|
|
|
size_t WebPImageDecoderPlugin::first_animated_frame_index()
|
|
|
|
|
{
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-02 18:20:06 -03:00
|
|
|
ErrorOr<ImageFrameDescriptor> WebPImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
|
2023-02-24 00:37:08 -03:00
|
|
|
{
|
|
|
|
|
if (index >= frame_count())
|
|
|
|
|
return Error::from_string_literal("WebPImageDecoderPlugin: Invalid frame index");
|
|
|
|
|
|
2023-03-31 12:09:58 -03:00
|
|
|
if (m_context->state == WebPLoadingContext::State::Error)
|
|
|
|
|
return Error::from_string_literal("WebPImageDecoderPlugin: Decoding failed");
|
|
|
|
|
|
2026-02-13 10:59:37 -03:00
|
|
|
if (m_context->has_animation) {
|
|
|
|
|
// Reset the decoder if requesting a frame we've already passed.
|
|
|
|
|
if (index < m_context->anim_frames_decoded) {
|
|
|
|
|
if (m_context->anim_decoder) {
|
|
|
|
|
WebPAnimDecoderReset(m_context->anim_decoder);
|
|
|
|
|
m_context->anim_frames_decoded = 0;
|
|
|
|
|
m_context->anim_old_timestamp = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip frames before the requested index.
|
|
|
|
|
while (m_context->anim_frames_decoded < index)
|
|
|
|
|
(void)TRY(decode_next_webp_animation_frame(*m_context));
|
|
|
|
|
|
|
|
|
|
// Decode and return the requested frame without caching it.
|
|
|
|
|
return TRY(decode_next_webp_animation_frame(*m_context));
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
if (m_context->state < WebPLoadingContext::State::BitmapDecoded) {
|
|
|
|
|
TRY(decode_webp_image(*m_context));
|
|
|
|
|
m_context->state = WebPLoadingContext::State::BitmapDecoded;
|
2023-05-07 21:38:25 -03:00
|
|
|
}
|
2024-07-12 05:08:39 -03:00
|
|
|
|
|
|
|
|
if (index >= m_context->frame_descriptors.size())
|
|
|
|
|
return Error::from_string_literal("WebPImageDecoderPlugin: Invalid frame index");
|
|
|
|
|
return m_context->frame_descriptors[index];
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
2026-02-13 10:59:27 -03:00
|
|
|
int WebPImageDecoderPlugin::frame_duration(size_t index)
|
|
|
|
|
{
|
|
|
|
|
if (index < m_context->frame_durations.size())
|
|
|
|
|
return m_context->frame_durations[index];
|
|
|
|
|
if (index < m_context->frame_descriptors.size())
|
|
|
|
|
return m_context->frame_descriptors[index].duration;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-24 00:37:08 -03:00
|
|
|
ErrorOr<Optional<ReadonlyBytes>> WebPImageDecoderPlugin::icc_data()
|
|
|
|
|
{
|
2024-07-12 05:08:39 -03:00
|
|
|
if (m_context->state < WebPLoadingContext::State::HeaderDecoded)
|
|
|
|
|
(void)frame(0);
|
2023-02-24 18:16:50 -03:00
|
|
|
|
2024-07-12 05:08:39 -03:00
|
|
|
if (!m_context->icc_data.is_empty())
|
|
|
|
|
return m_context->icc_data;
|
|
|
|
|
return OptionalNone {};
|
2023-02-24 00:37:08 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|