diff --git a/core/io/image_frames.cpp b/core/io/image_frames.cpp index 544638c8370..200330518d4 100644 --- a/core/io/image_frames.cpp +++ b/core/io/image_frames.cpp @@ -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) { @@ -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, @@ -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"); diff --git a/core/io/image_frames.h b/core/io/image_frames.h index 42870c09185..447961e16e6 100644 --- a/core/io/image_frames.h +++ b/core/io/image_frames.h @@ -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: @@ -87,6 +88,7 @@ class ImageFrames : public Resource { static Ref 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 &p_frames) { diff --git a/doc/classes/Image.xml b/doc/classes/Image.xml index 61b0c8d0981..a214da8eab3 100644 --- a/doc/classes/Image.xml +++ b/doc/classes/Image.xml @@ -416,6 +416,7 @@ 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. diff --git a/doc/classes/ImageFrames.xml b/doc/classes/ImageFrames.xml index 7dfea6e7497..fc6950a2344 100644 --- a/doc/classes/ImageFrames.xml +++ b/doc/classes/ImageFrames.xml @@ -6,7 +6,7 @@ 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]. @@ -81,6 +81,14 @@ Loads image frames from the binary contents of a GIF file. + + + + + + Loads an image from the binary contents of a WebP file. + + diff --git a/modules/webp/image_frames_loader_webp.cpp b/modules/webp/image_frames_loader_webp.cpp new file mode 100644 index 00000000000..c6074f5c0f7 --- /dev/null +++ b/modules/webp/image_frames_loader_webp.cpp @@ -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 _webp_mem_loader_func(const uint8_t *p_webp_data, int p_size, int p_max_frames) { + Ref 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()); + return frames; +} + +Error ImageFramesLoaderWebP::load_image_frames(Ref p_image, Ref f, BitField p_flags, float p_scale, int p_max_frames) { + Vector 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 *p_extensions) const { + p_extensions->push_back("webp"); +} + +ImageFramesLoaderWebP::ImageFramesLoaderWebP() { + ImageFrames::_webp_mem_loader_func = _webp_mem_loader_func; +} diff --git a/modules/webp/image_frames_loader_webp.h b/modules/webp/image_frames_loader_webp.h new file mode 100644 index 00000000000..f2130ed3fe4 --- /dev/null +++ b/modules/webp/image_frames_loader_webp.h @@ -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 p_image, Ref f, BitField p_flags, float p_scale, int p_max_frames); + virtual void get_recognized_extensions(List *p_extensions) const; + ImageFramesLoaderWebP(); +}; + +#endif // IMAGE_FRAMES_LOADER_WEBP_H diff --git a/modules/webp/register_types.cpp b/modules/webp/register_types.cpp index af7d3b7ddc4..d1e6041d173 100644 --- a/modules/webp/register_types.cpp +++ b/modules/webp/register_types.cpp @@ -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 image_loader_webp; +static Ref image_frames_loader_webp; static Ref resource_saver_webp; void initialize_webp_module(ModuleInitializationLevel p_level) { @@ -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); } @@ -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(); } diff --git a/modules/webp/webp_common.cpp b/modules/webp/webp_common.cpp index 72ec6fcc10a..ecf28b52884 100644 --- a/modules/webp/webp_common.cpp +++ b/modules/webp/webp_common.cpp @@ -35,7 +35,9 @@ #include "core/config/project_settings.h" #include +#include #include +#include #include @@ -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, ×tamp)) { + 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.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 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, ×tamp)) { + 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 = 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 diff --git a/modules/webp/webp_common.h b/modules/webp/webp_common.h index c6743c87296..e7af4fd4944 100644 --- a/modules/webp/webp_common.h +++ b/modules/webp/webp_common.h @@ -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. @@ -44,6 +45,8 @@ Vector _webp_packer(const Ref &p_image, float p_quality, bool p_ // Given a WebP file, unpack it into an image. Ref _webp_unpack(const Vector &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 diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 0e5782e200c..54a773a1eaa 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -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, diff --git a/tests/core/io/test_image_frames.h b/tests/core/io/test_image_frames.h index a046054f8f4..ef619808456 100644 --- a/tests/core/io/test_image_frames.h +++ b/tests/core/io/test_image_frames.h @@ -100,6 +100,19 @@ TEST_CASE("[ImageFrames] Loading") { "The GIF image frame should load successfully."); #endif +#ifdef MODULE_WEBP_ENABLED + // Load WebP + Ref image_frames_webp = memnew(ImageFrames()); + Ref f_webp = FileAccess::open(TestUtils::get_data_path("image_frames/icon.webp"), FileAccess::READ, &err); + REQUIRE(f_webp.is_valid()); + PackedByteArray data_webp; + data_webp.resize(f_webp->get_length() + 1); + f_webp->get_buffer(data_webp.ptrw(), f_webp->get_length()); + CHECK_MESSAGE( + image_frames_webp->load_webp_from_buffer(data_webp) == OK, + "The WebP image should load successfully."); +#endif // MODULE_WEBP_ENABLED + // Load APNG Ref image_frames_apng = memnew(ImageFrames()); Ref f_apng = FileAccess::open(TestUtils::get_data_path("image_frames/icon.apng"), FileAccess::READ, &err); diff --git a/tests/data/image_frames/icon.webp b/tests/data/image_frames/icon.webp new file mode 100644 index 00000000000..d8b6f1205fb Binary files /dev/null and b/tests/data/image_frames/icon.webp differ