mirror of
https://github.com/esphome/esphome.git
synced 2025-09-23 13:42:27 +01:00
added webp animation support to online_image
This commit is contained in:
@@ -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_(
|
||||
|
@@ -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<double>(this->image_->buffer_width_) / width;
|
||||
this->y_scale_ = static_cast<double>(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<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_)));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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_; }
|
||||
|
||||
|
@@ -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<JpegDecoder>(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<WebpDecoder>(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;
|
||||
|
@@ -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<esphome::http_request::HttpRequestComponent> {
|
||||
public:
|
||||
/**
|
||||
@@ -94,10 +97,13 @@ class OnlineImage : public PollingComponent,
|
||||
|
||||
RAMAllocator<uint8_t> 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<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||
|
124
esphome/components/online_image/webp_image.cpp
Normal file
124
esphome/components/online_image/webp_image.cpp
Normal 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, ×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
|
35
esphome/components/online_image/webp_image.h
Normal file
35
esphome/components/online_image/webp_image.h
Normal 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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user