1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-23 21:52:23 +01:00

added webp animation support to online_image

This commit is contained in:
Ian Foster
2025-02-19 20:28:11 -08:00
parent 3020083564
commit c5fcbb9404
10 changed files with 231 additions and 24 deletions

View File

@@ -11,6 +11,9 @@ from esphome.components.image import (
get_image_type_enum, get_image_type_enum,
get_transparency_enum, get_transparency_enum,
) )
from esphome.components.animation import (
Animation_,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
@@ -25,7 +28,7 @@ from esphome.const import (
CONF_URL, CONF_URL,
) )
AUTO_LOAD = ["image"] AUTO_LOAD = ["image", "animation"]
DEPENDENCIES = ["display", "http_request"] DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"] CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True MULTI_CONF = True
@@ -68,6 +71,13 @@ class JPEGFormat(Format):
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT") cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") 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): class PNGFormat(Format):
def __init__(self): def __init__(self):
@@ -83,12 +93,13 @@ IMAGE_FORMATS = {
for x in ( for x in (
BMPFormat(), BMPFormat(),
JPEGFormat(), JPEGFormat(),
WEBPFormat(),
PNGFormat(), PNGFormat(),
) )
} }
IMAGE_FORMATS.update({"JPG": IMAGE_FORMATS["JPEG"]}) 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 # Actions
SetUrlAction = online_image_ns.class_( SetUrlAction = online_image_ns.class_(

View File

@@ -8,19 +8,19 @@ namespace online_image {
static const char *const TAG = "online_image.decoder"; static const char *const TAG = "online_image.decoder";
bool ImageDecoder::set_size(int width, int height) { bool ImageDecoder::set_size(int width, int height, int frames) {
bool success = this->image_->resize_(width, height) > 0; bool success = this->image_->resize_(width, height, frames) > 0;
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width; this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height; this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
return success; 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<int>(std::ceil((x + w) * this->x_scale_))); auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_)));
auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_))); auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_)));
for (int i = x * this->x_scale_; i < width; i++) { for (int i = x * this->x_scale_; i < width; i++) {
for (int j = y * this->y_scale_; j < height; j++) { 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);
} }
} }
} }

View File

@@ -55,9 +55,10 @@ class ImageDecoder {
* *
* @param width The image's width. * @param width The image's width.
* @param height The image's height. * @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. * @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. * @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 w The width of the rectangle.
* @param h The height of the rectangle. * @param h The height of the rectangle.
* @param color The fill color * @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_; } bool is_finished() const { return this->decoded_bytes_ == this->download_size_; }

View File

@@ -12,6 +12,9 @@ static const char *const TAG = "online_image";
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
#include "jpeg_image.h" #include "jpeg_image.h"
#endif #endif
#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT
#include "webp_image.h"
#endif
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "png_image.h" #include "png_image.h"
#endif #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, OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
image::Transparency transparency, uint32_t download_buffer_size) image::Transparency transparency, uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type, transparency), : Animation(nullptr, 0, 0, 1, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size), download_buffer_initial_size_(download_buffer_size),
@@ -60,11 +63,12 @@ void OnlineImage::release() {
this->height_ = 0; this->height_ = 0;
this->buffer_width_ = 0; this->buffer_width_ = 0;
this->buffer_height_ = 0; this->buffer_height_ = 0;
this->buffer_frame_size_ = 0;
this->end_connection_(); 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 width = this->fixed_width_;
int height = this->fixed_height_; int height = this->fixed_height_;
if (this->is_auto_resize_()) { if (this->is_auto_resize_()) {
@@ -74,7 +78,7 @@ size_t OnlineImage::resize_(int width_in, int height_in) {
this->release(); 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_) { if (this->buffer_) {
// Buffer already allocated => no need to resize // Buffer already allocated => no need to resize
return new_size; return new_size;
@@ -90,7 +94,10 @@ size_t OnlineImage::resize_(int width_in, int height_in) {
this->buffer_width_ = width; this->buffer_width_ = width;
this->buffer_height_ = height; this->buffer_height_ = height;
this->width_ = width; 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; return new_size;
} }
@@ -117,6 +124,11 @@ void OnlineImage::update() {
accept_mime_type = "image/jpeg"; accept_mime_type = "image/jpeg";
break; break;
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #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 #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
case ImageFormat::PNG: case ImageFormat::PNG:
accept_mime_type = "image/png"; accept_mime_type = "image/png";
@@ -166,6 +178,12 @@ void OnlineImage::update() {
this->decoder_ = esphome::make_unique<JpegDecoder>(this); this->decoder_ = esphome::make_unique<JpegDecoder>(this);
} }
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #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<WebpDecoder>(this);
}
#endif // USE_ONLINE_IMAGE_WEBP_SUPPORT
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
if (this->format_ == ImageFormat::PNG) { if (this->format_ == ImageFormat::PNG) {
ESP_LOGD(TAG, "Allocating PNG decoder"); ESP_LOGD(TAG, "Allocating PNG decoder");
@@ -196,6 +214,7 @@ void OnlineImage::loop() {
} }
if (!this->downloader_ || this->decoder_->is_finished()) { if (!this->downloader_ || this->decoder_->is_finished()) {
this->data_start_ = buffer_; this->data_start_ = buffer_;
this->animation_data_start_ = this->buffer_;
this->width_ = buffer_width_; this->width_ = buffer_width_;
this->height_ = buffer_height_; this->height_ = buffer_height_;
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), 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_) { if (!this->buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!"); ESP_LOGE(TAG, "Buffer not allocated!");
return; return;
} }
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { 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) outside the image!", x, y); ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d,%d) outside the image!", x, y, frame);
return; return;
} }
uint32_t pos = this->get_position_(x, y); uint32_t pos = this->get_position_(x, y, frame);
switch (this->type_) { switch (this->type_) {
case ImageType::IMAGE_TYPE_BINARY: { case ImageType::IMAGE_TYPE_BINARY: {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;

View File

@@ -2,6 +2,7 @@
#include "esphome/components/http_request/http_request.h" #include "esphome/components/http_request/http_request.h"
#include "esphome/components/image/image.h" #include "esphome/components/image/image.h"
#include "esphome/components/animation/animation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -25,6 +26,8 @@ enum ImageFormat {
AUTO, AUTO,
/** JPEG format. */ /** JPEG format. */
JPEG, JPEG,
/** WEBP format. */
WEBP,
/** PNG format. */ /** PNG format. */
PNG, PNG,
/** BMP format. */ /** BMP format. */
@@ -37,7 +40,7 @@ enum ImageFormat {
* need to re-download or re-decode. * need to re-download or re-decode.
*/ */
class OnlineImage : public PollingComponent, class OnlineImage : public PollingComponent,
public image::Image, public animation::Animation,
public Parented<esphome::http_request::HttpRequestComponent> { public Parented<esphome::http_request::HttpRequestComponent> {
public: public:
/** /**
@@ -94,10 +97,13 @@ class OnlineImage : public PollingComponent,
RAMAllocator<uint8_t> allocator_{}; RAMAllocator<uint8_t> allocator_{};
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_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) const { return (this->get_bpp() * width + 7u) / 8u * height; } 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; } 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 width
* @param height * @param height
* @param frames
* @return 0 if no memory could be allocated, the size of the new buffer otherwise. * @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. * @brief Draw a pixel into the buffer.
@@ -126,8 +133,9 @@ class OnlineImage : public PollingComponent,
* @param x Horizontal pixel position. * @param x Horizontal pixel position.
* @param y Vertical pixel position. * @param y Vertical pixel position.
* @param color 32 bit color to put into the pixel. * @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_(); void end_connection_();
@@ -173,11 +181,13 @@ class OnlineImage : public PollingComponent,
* decoded images). * decoded images).
*/ */
int buffer_height_; 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_; time_t start_time_;
friend bool ImageDecoder::set_size(int width, int height); 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); friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color, int frame);
}; };
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> { template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {

View File

@@ -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, &timestamp)) {
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

View File

@@ -0,0 +1,35 @@
#pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_WEBP_SUPPORT
#include <webp/demux.h>
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

View File

@@ -68,6 +68,7 @@
#define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_BMP_SUPPORT
#define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT
#define USE_ONLINE_IMAGE_JPEG_SUPPORT #define USE_ONLINE_IMAGE_JPEG_SUPPORT
#define USE_ONLINE_IMAGE_WEBP_SUPPORT
#define USE_OTA #define USE_OTA
#define USE_OTA_PASSWORD #define USE_OTA_PASSWORD
#define USE_OTA_STATE_CALLBACK #define USE_OTA_STATE_CALLBACK

View File

@@ -43,6 +43,7 @@ lib_deps =
kikuchan98/pngle@1.0.2 ; online_image kikuchan98/pngle@1.0.2 ; online_image
; Using the repository directly, otherwise ESP-IDF can't use the library ; Using the repository directly, otherwise ESP-IDF can't use the library
https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image 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 ; 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 https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
lvgl/lvgl@8.4.0 ; lvgl lvgl/lvgl@8.4.0 ; lvgl

View File

@@ -38,6 +38,10 @@ online_image:
url: http://www.faqs.org/images/library.jpg url: http://www.faqs.org/images/library.jpg
format: JPG format: JPG
type: RGB565 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 # Check the set_url action
esphome: esphome: