From c5fcbb9404f45424da1a3245ea37afe0da3a86a7 Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Wed, 19 Feb 2025 20:28:11 -0800 Subject: [PATCH] added webp animation support to online_image --- esphome/components/online_image/__init__.py | 15 ++- .../components/online_image/image_decoder.cpp | 8 +- .../components/online_image/image_decoder.h | 6 +- .../components/online_image/online_image.cpp | 35 +++-- .../components/online_image/online_image.h | 26 ++-- .../components/online_image/webp_image.cpp | 124 ++++++++++++++++++ esphome/components/online_image/webp_image.h | 35 +++++ esphome/core/defines.h | 1 + platformio.ini | 1 + tests/components/online_image/common.yaml | 4 + 10 files changed, 231 insertions(+), 24 deletions(-) create mode 100644 esphome/components/online_image/webp_image.cpp create mode 100644 esphome/components/online_image/webp_image.h diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 6b69bc240b..de534f3b05 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -11,6 +11,9 @@ from esphome.components.image import ( get_image_type_enum, get_transparency_enum, ) +from esphome.components.animation import ( + Animation_, +) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -25,7 +28,7 @@ from esphome.const import ( CONF_URL, ) -AUTO_LOAD = ["image"] +AUTO_LOAD = ["image", "animation"] DEPENDENCIES = ["display", "http_request"] CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True @@ -68,6 +71,13 @@ class JPEGFormat(Format): cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT") cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") +class WEBPFormat(Format): + def __init__(self): + super().__init__("WEBP") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_WEBP_SUPPORT") + cg.add_library("libwebp", None, "https://github.com/acvigue/libwebp#26b0c4b") class PNGFormat(Format): def __init__(self): @@ -83,12 +93,13 @@ IMAGE_FORMATS = { for x in ( BMPFormat(), JPEGFormat(), + WEBPFormat(), PNGFormat(), ) } IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]}) -OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) +OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_, Animation_) # Actions SetUrlAction = online_image_ns.class_( diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp index 0ab7dadde3..7e071fcd0d 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/image_decoder.cpp @@ -8,19 +8,19 @@ namespace online_image { static const char *const TAG = "online_image.decoder"; -bool ImageDecoder::set_size(int width, int height) { - bool success = this->image_->resize_(width, height) > 0; +bool ImageDecoder::set_size(int width, int height, int frames) { + bool success = this->image_->resize_(width, height, frames) > 0; this->x_scale_ = static_cast(this->image_->buffer_width_) / width; this->y_scale_ = static_cast(this->image_->buffer_height_) / height; return success; } -void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color, int frame) { auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); for (int i = x * this->x_scale_; i < width; i++) { for (int j = y * this->y_scale_; j < height; j++) { - this->image_->draw_pixel_(i, j, color); + this->image_->draw_pixel_(i, j, color, frame); } } } diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index d11b8b46d3..929f855319 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -55,9 +55,10 @@ class ImageDecoder { * * @param width The image's width. * @param height The image's height. + * @param frames The number of frames in an image if animated. * @return true if the image was resized, false otherwise. */ - bool set_size(int width, int height); + bool set_size(int width, int height, int frames = 1); /** * @brief Fill a rectangle on the display_buffer using the defined color. @@ -70,8 +71,9 @@ class ImageDecoder { * @param w The width of the rectangle. * @param h The height of the rectangle. * @param color The fill color + * @param frame The frame to write to */ - void draw(int x, int y, int w, int h, const Color &color); + void draw(int x, int y, int w, int h, const Color &color, int frame = 0); bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 3411018901..5d2b3bfc79 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -12,6 +12,9 @@ static const char *const TAG = "online_image"; #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT #include "jpeg_image.h" #endif +#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT +#include "webp_image.h" +#endif #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "png_image.h" #endif @@ -32,7 +35,7 @@ inline bool is_color_on(const Color &color) { OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, image::Transparency transparency, uint32_t download_buffer_size) - : Image(nullptr, 0, 0, type, transparency), + : Animation(nullptr, 0, 0, 1, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), download_buffer_initial_size_(download_buffer_size), @@ -60,11 +63,12 @@ void OnlineImage::release() { this->height_ = 0; this->buffer_width_ = 0; this->buffer_height_ = 0; + this->buffer_frame_size_ = 0; this->end_connection_(); } } -size_t OnlineImage::resize_(int width_in, int height_in) { +size_t OnlineImage::resize_(int width_in, int height_in, int frames) { int width = this->fixed_width_; int height = this->fixed_height_; if (this->is_auto_resize_()) { @@ -74,7 +78,7 @@ size_t OnlineImage::resize_(int width_in, int height_in) { this->release(); } } - size_t new_size = this->get_buffer_size_(width, height); + size_t new_size = this->get_buffer_size_(width, height, frames); if (this->buffer_) { // Buffer already allocated => no need to resize return new_size; @@ -90,7 +94,10 @@ size_t OnlineImage::resize_(int width_in, int height_in) { this->buffer_width_ = width; this->buffer_height_ = height; this->width_ = width; - ESP_LOGV(TAG, "New size: (%d, %d)", width, height); + this->animation_frame_count_ = frames; + this->buffer_frame_size_ = new_size / frames; + this->current_frame_ = 0; + ESP_LOGV(TAG, "New size: (%d, %d, %d)", width, height, frames); return new_size; } @@ -117,6 +124,11 @@ void OnlineImage::update() { accept_mime_type = "image/jpeg"; break; #endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT + case ImageFormat::WEBP: + accept_mime_type = "image/webp"; + break; +#endif // USE_ONLINE_IMAGE_WEBP_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT case ImageFormat::PNG: accept_mime_type = "image/png"; @@ -166,6 +178,12 @@ void OnlineImage::update() { this->decoder_ = esphome::make_unique(this); } #endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT + if (this->format_ == ImageFormat::WEBP) { + ESP_LOGD(TAG, "Allocating WEBP decoder"); + this->decoder_ = esphome::make_unique(this); + } +#endif // USE_ONLINE_IMAGE_WEBP_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT if (this->format_ == ImageFormat::PNG) { ESP_LOGD(TAG, "Allocating PNG decoder"); @@ -196,6 +214,7 @@ void OnlineImage::loop() { } if (!this->downloader_ || this->decoder_->is_finished()) { this->data_start_ = buffer_; + this->animation_data_start_ = this->buffer_; this->width_ = buffer_width_; this->height_ = buffer_height_; ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), @@ -243,16 +262,16 @@ void OnlineImage::map_chroma_key(Color &color) { } } -void OnlineImage::draw_pixel_(int x, int y, Color color) { +void OnlineImage::draw_pixel_(int x, int y, Color color, int frame) { if (!this->buffer_) { ESP_LOGE(TAG, "Buffer not allocated!"); return; } - if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { - ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + if (x < 0 || y < 0 || frame < 0 || x >= this->buffer_width_ || y >= this->buffer_height_ || frame >= this->animation_frame_count_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d,%d) outside the image!", x, y, frame); return; } - uint32_t pos = this->get_position_(x, y); + uint32_t pos = this->get_position_(x, y, frame); switch (this->type_) { case ImageType::IMAGE_TYPE_BINARY: { const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 2d10e528b1..4322a4b73c 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -2,6 +2,7 @@ #include "esphome/components/http_request/http_request.h" #include "esphome/components/image/image.h" +#include "esphome/components/animation/animation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -25,6 +26,8 @@ enum ImageFormat { AUTO, /** JPEG format. */ JPEG, + /** WEBP format. */ + WEBP, /** PNG format. */ PNG, /** BMP format. */ @@ -37,7 +40,7 @@ enum ImageFormat { * need to re-download or re-decode. */ class OnlineImage : public PollingComponent, - public image::Image, + public animation::Animation, public Parented { public: /** @@ -94,10 +97,13 @@ class OnlineImage : public PollingComponent, RAMAllocator allocator_{}; - uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } - int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; } + uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_, this->animation_frame_count_); } + int get_buffer_size_(int width, int height, int frames) const { return frames * ((this->get_bpp() * width + 7u) / 8u * height); } - int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } + int get_position_(int x, int y, int frame = 0) const { + int frame_offset = this->buffer_frame_size_ * frame; + return ((x + y * this->buffer_width_) * this->get_bpp() / 8) + frame_offset; + } ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } @@ -112,9 +118,10 @@ class OnlineImage : public PollingComponent, * * @param width * @param height + * @param frames * @return 0 if no memory could be allocated, the size of the new buffer otherwise. */ - size_t resize_(int width, int height); + size_t resize_(int width, int height, int frames = 1); /** * @brief Draw a pixel into the buffer. @@ -126,8 +133,9 @@ class OnlineImage : public PollingComponent, * @param x Horizontal pixel position. * @param y Vertical pixel position. * @param color 32 bit color to put into the pixel. + * @param frame the frame to draw the image buffer to if animated */ - void draw_pixel_(int x, int y, Color color); + void draw_pixel_(int x, int y, Color color, int frame = 0); void end_connection_(); @@ -173,11 +181,13 @@ class OnlineImage : public PollingComponent, * decoded images). */ int buffer_height_; + /** The calculated size of a single frame for the given width and height in the buffer */ + int buffer_frame_size_; time_t start_time_; - friend bool ImageDecoder::set_size(int width, int height); - friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); + friend bool ImageDecoder::set_size(int width, int height, int frames); + friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color, int frame); }; template class OnlineImageSetUrlAction : public Action { diff --git a/esphome/components/online_image/webp_image.cpp b/esphome/components/online_image/webp_image.cpp new file mode 100644 index 0000000000..af8e499f84 --- /dev/null +++ b/esphome/components/online_image/webp_image.cpp @@ -0,0 +1,124 @@ +#include "webp_image.h" + +#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT + +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "online_image.h" +static const char *const TAG = "online_image.webp"; + +namespace esphome { +namespace online_image { + +/** + * @brief method that will be called to draw each frame to the image buffer. + * + * @param decoder The ImageDecoder to draw to + * @param pix Buffer of pixels for the given frame + * @param width Image width + * @param height Image Height + * @param frame The frame to draw the image to + */ +void draw_frame(ImageDecoder *decoder, uint8_t *pix, uint32_t width, uint32_t height, int frame) { + static int ixR = 0; + static int ixG = 1; + static int ixB = 2; + static int ixA = 3; + static int channels = 4; + + for (unsigned int y = 0; y < height; y++) { + for (unsigned int x = 0; x < width; x++) { + const uint8_t *p = &pix[(y * width + x) * channels]; + uint8_t r = p[ixR]; + uint8_t g = p[ixG]; + uint8_t b = p[ixB]; + Color color(r, g, b, p[ixA]); + decoder->draw(x, y, 1, 1, color, frame); + } + } +} + +int WebpDecoder::prepare(size_t download_size) { + ImageDecoder::prepare(download_size); + auto size = this->image_->resize_download_buffer(download_size); + if (size < download_size) { + ESP_LOGE(TAG, "Download buffer resize failed!"); + return DECODE_ERROR_OUT_OF_MEMORY; + } + return 0; +} + +int HOT WebpDecoder::decode(uint8_t *buffer, size_t size) { + ESP_LOGD(TAG, "decode size: %d", size); + if (size < this->download_size_) { + ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_); + return 0; + } + + // Set up WebP decoder + WebPData webpData; + WebPDataInit(&webpData); + webpData.bytes = buffer; + webpData.size = size; + + WebPAnimDecoderOptions decoderOptions; + WebPAnimDecoderOptionsInit(&decoderOptions); + decoderOptions.color_mode = MODE_RGBA; + + this->decoder_ = WebPAnimDecoderNew(&webpData, &decoderOptions); + if (this->decoder_ == NULL) { + ESP_LOGE(TAG, "Could not create WebP decoder"); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + if (!WebPAnimDecoderGetInfo(this->decoder_, &this->animation_)) { + ESP_LOGE(TAG, "Could not get WebP animation"); + WebPAnimDecoderDelete(this->decoder_); + this->decoder_ = NULL; + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + ESP_LOGD(TAG, "WebPAnimDecode size: (%dx%d), loops: %d, frames: %d, bgcolor: #%X", animation_.canvas_width, animation_.canvas_height, animation_.loop_count, animation_.frame_count, animation_.bgcolor); + + if (!this->set_size(animation_.canvas_width, animation_.canvas_height, animation_.frame_count)) { + ESP_LOGE(TAG,"could not allocate enough memory"); + WebPAnimDecoderDelete(this->decoder_); + this->decoder_ = NULL; + return DECODE_ERROR_OUT_OF_MEMORY; + } + + if (animation_.frame_count == 0) { + ESP_LOGE(TAG, "Animation has 0 frames"); + WebPAnimDecoderDelete(this->decoder_); + this->decoder_ = NULL; + return 0; + } + + // iterate over all frames + for (uint frame = 0; frame < animation_.frame_count; frame++) { + uint8_t *pix; + int timestamp; + if (!WebPAnimDecoderGetNext(this->decoder_, &pix, ×tamp)) { + ESP_LOGE(TAG,"error parsing webp frame %u/%u", frame, animation_.frame_count); + WebPAnimDecoderDelete(this->decoder_); + this->decoder_ = NULL; + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + draw_frame(this, pix, this->animation_.canvas_width, this->animation_.canvas_height, frame); + } + + WebPAnimDecoderDelete(this->decoder_); + this->decoder_ = NULL; + + this->decoded_bytes_ = size; + return size; +} + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_WEBP_SUPPORT diff --git a/esphome/components/online_image/webp_image.h b/esphome/components/online_image/webp_image.h new file mode 100644 index 0000000000..470c5774a6 --- /dev/null +++ b/esphome/components/online_image/webp_image.h @@ -0,0 +1,35 @@ +#pragma once + +#include "image_decoder.h" +#include "esphome/core/defines.h" +#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT +#include + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for WEBP images. + */ +class WebpDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new WEBP Decoder object. + * + * @param display The image to decode the stream into. + */ + WebpDecoder(OnlineImage *image) : ImageDecoder(image) {} + ~WebpDecoder() override {} + + int prepare(size_t download_size) override; + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + WebPAnimInfo animation_; + WebPAnimDecoder *decoder_; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_WEBP_SUPPORT diff --git a/esphome/core/defines.h b/esphome/core/defines.h index dc0ac3c1e8..236af70736 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -68,6 +68,7 @@ #define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_ONLINE_IMAGE_JPEG_SUPPORT +#define USE_ONLINE_IMAGE_WEBP_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK diff --git a/platformio.ini b/platformio.ini index 4153310480..455b4bab7a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -43,6 +43,7 @@ lib_deps = kikuchan98/pngle@1.0.2 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image + https://github.com/acvigue/libwebp#26b0c4b ; online_image ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 69daa915c5..73587d5f34 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -38,6 +38,10 @@ online_image: url: http://www.faqs.org/images/library.jpg format: JPG type: RGB565 + - id: online_webp_image + url: https://samples-files.com/samples/images/webp/480-360-sample.webp + format: WEBP + type: RGB # Check the set_url action esphome: