Skip to content

Commit

Permalink
Add animated WebP import
Browse files Browse the repository at this point in the history
Add animated WebP file loading
Add animated WebP image frames load test assertions
Add libwebpdemux for local libwebp to linuxbsd/detect.py
  • Loading branch information
Spartan322 committed Jan 23, 2025
1 parent 28eb877 commit df8d12c
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 7 deletions.
6 changes: 6 additions & 0 deletions core/io/image_frames.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "core/object/class_db.h"

ImageFramesMemLoadFunc ImageFrames::_apng_mem_loader_func = nullptr;
ImageFramesMemLoadFunc ImageFrames::_webp_mem_loader_func = nullptr;
ImageFramesMemLoadFunc ImageFrames::_gif_mem_loader_func = nullptr;

void ImageFrames::set_frame_count(int p_frames) {
Expand Down Expand Up @@ -131,6 +132,10 @@ Error ImageFrames::load_apng_from_buffer(const PackedByteArray &p_array, int p_m
return _load_from_buffer(p_array, _apng_mem_loader_func, p_max_frames);
}

Error ImageFrames::load_webp_from_buffer(const PackedByteArray &p_array, int p_max_frames) {
return _load_from_buffer(p_array, _webp_mem_loader_func, p_max_frames);
}

Error ImageFrames::load_gif_from_buffer(const PackedByteArray &p_array, int p_max_frames) {
ERR_FAIL_NULL_V_MSG(
_gif_mem_loader_func,
Expand Down Expand Up @@ -182,6 +187,7 @@ void ImageFrames::_bind_methods() {
ClassDB::bind_static_method("ImageFrames", D_METHOD("load_from_file", "path"), &ImageFrames::load_from_file);

ClassDB::bind_method(D_METHOD("load_apng_from_buffer", "buffer", "max_frames"), &ImageFrames::load_apng_from_buffer);
ClassDB::bind_method(D_METHOD("load_webp_from_buffer", "buffer", "max_frames"), &ImageFrames::load_webp_from_buffer);
ClassDB::bind_method(D_METHOD("load_gif_from_buffer", "buffer", "max_frames"), &ImageFrames::load_gif_from_buffer);

ADD_PROPERTY(PropertyInfo(Variant::INT, "frame_count", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), "set_frame_count", "get_frame_count");
Expand Down
2 changes: 2 additions & 0 deletions core/io/image_frames.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ImageFrames : public Resource {

public:
static ImageFramesMemLoadFunc _apng_mem_loader_func;
static ImageFramesMemLoadFunc _webp_mem_loader_func;
static ImageFramesMemLoadFunc _gif_mem_loader_func;

private:
Expand Down Expand Up @@ -87,6 +88,7 @@ class ImageFrames : public Resource {
static Ref<ImageFrames> load_from_file(const String &p_path);

Error load_apng_from_buffer(const PackedByteArray &p_array, int p_max_frames = 0);
Error load_webp_from_buffer(const PackedByteArray &p_array, int p_max_frames = 0);
Error load_gif_from_buffer(const PackedByteArray &p_array, int p_max_frames = 0);

void copy_internals_from(const Ref<ImageFrames> &p_frames) {
Expand Down
1 change: 1 addition & 0 deletions doc/classes/Image.xml
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
<param index="0" name="buffer" type="PackedByteArray" />
<description>
Loads an image from the binary contents of a WebP file.
If the file is animated, this function will load the first frame of the animation.
</description>
</method>
<method name="normal_map_to_xy">
Expand Down
10 changes: 9 additions & 1 deletion doc/classes/ImageFrames.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<description>
A container of [Image]s used to load and arrange a sequence of frames. Each frame can specify a delay for animated images.
Can be used to load animated image formats externally.
Supported animated image formats are [url=https://www.w3.org/Graphics/GIF/spec-gif89a.txt]GIF[/url] ([code].gif[/code]), [url=https://wiki.mozilla.org/APNG_Specification]APNG[/url] ([code].png[/code] and [code].apng[/code]), and any format exposed via a GDExtension plugin.
Supported animated image formats are [url=https://www.w3.org/Graphics/GIF/spec-gif89a.txt]GIF[/url] ([code].gif[/code]), [url=https://wiki.mozilla.org/APNG_Specification]APNG[/url] ([code].png[/code] and [code].apng[/code]), [url=https://developers.google.com/speed/webp/docs/riff_container]WepP[/url] ([code].webp[/code]), and any format exposed via a GDExtension plugin.
An [ImageTexture] is not meant to be operated from within the editor interface directly, and is mostly useful for rendering images on screen dynamically via code. If you need to generate images procedurally from within the editor, consider saving and importing images as custom texture resources implementing a new [EditorImportPlugin].
</description>
<tutorials>
Expand Down Expand Up @@ -81,6 +81,14 @@
Loads image frames from the binary contents of a GIF file.
</description>
</method>
<method name="load_webp_from_buffer">
<return type="int" enum="Error" />
<param index="0" name="buffer" type="PackedByteArray" />
<param index="1" name="max_frames" type="int" />
<description>
Loads an image from the binary contents of a WebP file.
</description>
</method>
<method name="set_frame_delay">
<return type="void" />
<param index="0" name="frame" type="int" />
Expand Down
66 changes: 66 additions & 0 deletions modules/webp/image_frames_loader_webp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**************************************************************************/
/* image_frames_loader_webp.cpp */
/**************************************************************************/
/* This file is part of: */
/* REDOT ENGINE */
/* https://redotengine.org */
/**************************************************************************/
/* Copyright (c) 2024-present Redot Engine contributors */
/* (see REDOT_AUTHORS.md) */
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "image_frames_loader_webp.h"

#include "webp_common.h"

static Ref<ImageFrames> _webp_mem_loader_func(const uint8_t *p_webp_data, int p_size, int p_max_frames) {
Ref<ImageFrames> frames;
frames.instantiate();
Error err = WebPCommon::webp_load_image_frames_from_buffer(frames.ptr(), p_webp_data, p_size, p_max_frames);
ERR_FAIL_COND_V(err, Ref<Image>());
return frames;
}

Error ImageFramesLoaderWebP::load_image_frames(Ref<ImageFrames> p_image, Ref<FileAccess> f, BitField<ImageFramesFormatLoader::LoaderFlags> p_flags, float p_scale, int p_max_frames) {
Vector<uint8_t> src_image;
uint64_t src_image_len = f->get_length();
ERR_FAIL_COND_V(src_image_len == 0, ERR_FILE_CORRUPT);
src_image.resize(src_image_len);

uint8_t *w = src_image.ptrw();

f->get_buffer(&w[0], src_image_len);

Error err = WebPCommon::webp_load_image_frames_from_buffer(p_image.ptr(), w, src_image_len, p_max_frames);

return err;
}

void ImageFramesLoaderWebP::get_recognized_extensions(List<String> *p_extensions) const {
p_extensions->push_back("webp");
}

ImageFramesLoaderWebP::ImageFramesLoaderWebP() {
ImageFrames::_webp_mem_loader_func = _webp_mem_loader_func;
}
45 changes: 45 additions & 0 deletions modules/webp/image_frames_loader_webp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**************************************************************************/
/* image_frames_loader_webp.h */
/**************************************************************************/
/* This file is part of: */
/* REDOT ENGINE */
/* https://redotengine.org */
/**************************************************************************/
/* Copyright (c) 2024-present Redot Engine contributors */
/* (see REDOT_AUTHORS.md) */
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef IMAGE_FRAMES_LOADER_WEBP_H
#define IMAGE_FRAMES_LOADER_WEBP_H

#include "core/io/image_frames_loader.h"

class ImageFramesLoaderWebP : public ImageFramesFormatLoader {
public:
virtual Error load_image_frames(Ref<ImageFrames> p_image, Ref<FileAccess> f, BitField<ImageFramesFormatLoader::LoaderFlags> p_flags, float p_scale, int p_max_frames);
virtual void get_recognized_extensions(List<String> *p_extensions) const;
ImageFramesLoaderWebP();
};

#endif // IMAGE_FRAMES_LOADER_WEBP_H
8 changes: 8 additions & 0 deletions modules/webp/register_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@

#include "register_types.h"

#include "image_frames_loader_webp.h"
#include "image_loader_webp.h"
#include "resource_saver_webp.h"

static Ref<ImageLoaderWebP> image_loader_webp;
static Ref<ImageFramesLoaderWebP> image_frames_loader_webp;
static Ref<ResourceSaverWebP> resource_saver_webp;

void initialize_webp_module(ModuleInitializationLevel p_level) {
Expand All @@ -46,6 +48,9 @@ void initialize_webp_module(ModuleInitializationLevel p_level) {
image_loader_webp.instantiate();
ImageLoader::add_image_format_loader(image_loader_webp);

image_frames_loader_webp.instantiate();
ImageFramesLoader::add_image_frames_format_loader(image_frames_loader_webp);

resource_saver_webp.instantiate();
ResourceSaver::add_resource_format_saver(resource_saver_webp);
}
Expand All @@ -58,6 +63,9 @@ void uninitialize_webp_module(ModuleInitializationLevel p_level) {
ImageLoader::remove_image_format_loader(image_loader_webp);
image_loader_webp.unref();

ImageFramesLoader::remove_image_frames_format_loader(image_frames_loader_webp);
image_frames_loader_webp.unref();

ResourceSaver::remove_resource_format_saver(resource_saver_webp);
resource_saver_webp.unref();
}
128 changes: 122 additions & 6 deletions modules/webp/webp_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
#include "core/config/project_settings.h"

#include <webp/decode.h>
#include <webp/demux.h>
#include <webp/encode.h>
#include <webp/mux_types.h>

#include <string.h>

Expand Down Expand Up @@ -167,17 +169,131 @@ Error webp_load_image_from_buffer(Image *p_image, const uint8_t *p_buffer, int p
dst_image.resize(datasize);
uint8_t *dst_w = dst_image.ptrw();

bool errdec = false;
if (features.has_alpha) {
errdec = WebPDecodeRGBAInto(p_buffer, p_buffer_len, dst_w, datasize, 4 * features.width) == nullptr;
if (!features.has_animation) {
bool errdec = false;
if (features.has_alpha) {
errdec = WebPDecodeRGBAInto(p_buffer, p_buffer_len, dst_w, datasize, 4 * features.width) == nullptr;
} else {
errdec = WebPDecodeRGBInto(p_buffer, p_buffer_len, dst_w, datasize, 3 * features.width) == nullptr;
}

ERR_FAIL_COND_V_MSG(errdec, ERR_FILE_CORRUPT, "Failed decoding WebP image.");
} else {
errdec = WebPDecodeRGBInto(p_buffer, p_buffer_len, dst_w, datasize, 3 * features.width) == nullptr;
WebPData webp_data;
WebPDataInit(&webp_data);
webp_data.bytes = p_buffer;
webp_data.size = p_buffer_len;

WebPAnimDecoder *anim_decoder = WebPAnimDecoderNew(&webp_data, nullptr);
if (anim_decoder == nullptr) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Failed decoding animated WebP image.");
}

WebPAnimInfo anim_info;
if (!WebPAnimDecoderGetInfo(anim_decoder, &anim_info)) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Failed decoding WebP animation info.");
}

uint8_t *frame_rgba;
int timestamp;

if (!WebPAnimDecoderGetNext(anim_decoder, &frame_rgba, &timestamp)) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Failed decoding animated WebP initial frame.");
}
memcpy(dst_image.ptrw(), frame_rgba, dst_image.size());

WebPAnimDecoderDelete(anim_decoder);
}

ERR_FAIL_COND_V_MSG(errdec, ERR_FILE_CORRUPT, "Failed decoding WebP image.");

p_image->set_data(features.width, features.height, false, features.has_alpha ? Image::FORMAT_RGBA8 : Image::FORMAT_RGB8, dst_image);

return OK;
}

Error webp_load_image_frames_from_buffer(ImageFrames *p_frames, const uint8_t *p_buffer, int p_buffer_len, int p_max_frames) {
ERR_FAIL_NULL_V(p_frames, ERR_INVALID_PARAMETER);

WebPBitstreamFeatures features;
if (WebPGetFeatures(p_buffer, p_buffer_len, &features) != VP8_STATUS_OK) {
ERR_FAIL_V(ERR_FILE_CORRUPT);
}

if (!features.has_animation) {
p_frames->set_frame_count(1);
Ref<Image> image;
image.instantiate();
if (webp_load_image_from_buffer(image.ptr(), p_buffer, p_buffer_len) != OK) {
return ERR_FILE_CORRUPT;
}
p_frames->set_frame_image(0, image);
return OK;
}

WebPData webp_data;
WebPDataInit(&webp_data);
webp_data.bytes = p_buffer;
webp_data.size = p_buffer_len;

#ifdef THREADS_ENABLED
const bool supports_threads = true;
#else
const bool supports_threads = false;
#endif

WebPAnimDecoderOptions anim_decoder_options;
WebPAnimDecoderOptionsInit(&anim_decoder_options);
anim_decoder_options.color_mode = MODE_RGBA;
anim_decoder_options.use_threads = supports_threads;

WebPAnimDecoder *anim_decoder = WebPAnimDecoderNew(&webp_data, &anim_decoder_options);
if (anim_decoder == nullptr) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Failed decoding animated WebP image.");
}

WebPAnimInfo anim_info;
if (!WebPAnimDecoderGetInfo(anim_decoder, &anim_info)) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, "Failed decoding WebP animation info.");
}

static const uint32_t NUM_CHANNELS = 4;
const uint64_t rgba_size = anim_info.canvas_width * NUM_CHANNELS * anim_info.canvas_height;

Vector<uint8_t> screen;
screen.resize_zeroed(rgba_size);

const uint32_t frame_count = p_max_frames > 0 ? MIN(anim_info.frame_count, (uint32_t)p_max_frames) : anim_info.frame_count;
p_frames->set_frame_count(frame_count);
p_frames->set_loop_count(anim_info.loop_count);

int previous_timestamp = 0;
for (uint32_t frame_index = 0; p_max_frames > 0 ? frame_count : WebPAnimDecoderHasMoreFrames(anim_decoder); frame_index++) {
if (frame_index >= frame_count) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_COND_V(frame_index >= frame_count, ERR_FILE_CORRUPT);
}

uint8_t *frame_rgba;
int timestamp;

if (!WebPAnimDecoderGetNext(anim_decoder, &frame_rgba, &timestamp)) {
WebPAnimDecoderDelete(anim_decoder);
ERR_FAIL_V_MSG(ERR_FILE_CORRUPT, vformat("Failed decoding WebP frame %d.", frame_index));
}
memcpy(screen.ptrw(), frame_rgba, screen.size());

Ref<Image> image = memnew(Image(anim_info.canvas_width, anim_info.canvas_height, false, Image::FORMAT_RGBA8, screen));
p_frames->set_frame_image(frame_index, image);
p_frames->set_frame_delay(frame_index, (timestamp - previous_timestamp) / 1000.0);

previous_timestamp = timestamp;
}

WebPAnimDecoderDelete(anim_decoder);
return OK;
}
} // namespace WebPCommon
3 changes: 3 additions & 0 deletions modules/webp/webp_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#define WEBP_COMMON_H

#include "core/io/image.h"
#include "core/io/image_frames.h"

namespace WebPCommon {
// Given an image, pack this data into a WebP file.
Expand All @@ -44,6 +45,8 @@ Vector<uint8_t> _webp_packer(const Ref<Image> &p_image, float p_quality, bool p_
// Given a WebP file, unpack it into an image.
Ref<Image> _webp_unpack(const Vector<uint8_t> &p_buffer);
Error webp_load_image_from_buffer(Image *p_image, const uint8_t *p_buffer, int p_buffer_len);
// Given a WebP file, unpack it into image frames.
Error webp_load_image_frames_from_buffer(ImageFrames *p_frames, const uint8_t *p_buffer, int p_buffer_len, int p_max_frames);
} //namespace WebPCommon

#endif // WEBP_COMMON_H
1 change: 1 addition & 0 deletions platform/linuxbsd/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def configure(env: "SConsEnvironment"):

if not env["builtin_libwebp"]:
env.ParseConfig("pkg-config libwebp --cflags --libs")
env.ParseConfig("pkg-config libwebpdemux --cflags --libs")

if not env["builtin_mbedtls"]:
# mbedTLS only provides a pkgconfig file since 3.6.0, but we still support 2.28.x,
Expand Down
Loading

0 comments on commit df8d12c

Please sign in to comment.