1
0
mirror of https://github.com/esphome/esphome.git synced 2025-01-31 02:00:55 +00:00

[online_image] Add JPEG support to online_image (#8127)

Co-authored-by: Jimmy Hedman <jimmy.hedman@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Rodrigo Martín <contact@rodrigomartin.dev>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
guillempages 2025-01-29 00:35:43 +01:00 committed by GitHub
parent f7f8bf4da4
commit 7dab1a6082
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 199 additions and 6 deletions

View File

@ -60,6 +60,15 @@ class BMPFormat(Format):
cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT")
class JPEGFormat(Format):
def __init__(self):
super().__init__("JPEG")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
cg.add_library("JPEGDEC", "1.6.2", "https://github.com/bitbank2/JPEGDEC")
class PNGFormat(Format): class PNGFormat(Format):
def __init__(self): def __init__(self):
super().__init__("PNG") super().__init__("PNG")
@ -69,14 +78,15 @@ class PNGFormat(Format):
cg.add_library("pngle", "1.0.2") cg.add_library("pngle", "1.0.2")
# New formats can be added here.
IMAGE_FORMATS = { IMAGE_FORMATS = {
x.image_type: x x.image_type: x
for x in ( for x in (
BMPFormat(), BMPFormat(),
JPEGFormat(),
PNGFormat(), 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_)
@ -116,7 +126,7 @@ ONLINE_IMAGE_SCHEMA = (
cv.Required(CONF_URL): cv.url, cv.Required(CONF_URL): cv.url,
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(

View File

@ -41,5 +41,20 @@ size_t DownloadBuffer::read(size_t len) {
return this->unread_; return this->unread_;
} }
size_t DownloadBuffer::resize(size_t size) {
if (this->size_ == size) {
return size;
}
this->allocator_.deallocate(this->buffer_, this->size_);
this->size_ = size;
this->buffer_ = this->allocator_.allocate(size);
this->reset();
if (this->buffer_) {
return size;
} else {
return 0;
}
}
} // namespace online_image } // namespace online_image
} // namespace esphome } // namespace esphome

View File

@ -106,6 +106,8 @@ class DownloadBuffer {
void reset() { this->unread_ = 0; } void reset() { this->unread_ = 0; }
size_t resize(size_t size);
protected: protected:
RAMAllocator<uint8_t> allocator_{}; RAMAllocator<uint8_t> allocator_{};
uint8_t *buffer_; uint8_t *buffer_;

View File

@ -0,0 +1,89 @@
#include "jpeg_image.h"
#ifdef USE_ONLINE_IMAGE_JPEG_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.jpeg";
namespace esphome {
namespace online_image {
/**
* @brief Callback method that will be called by the JPEGDEC engine when a chunk
* of the image is decoded.
*
* @param jpeg The JPEGDRAW object, including the context data.
*/
static int draw_callback(JPEGDRAW *jpeg) {
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
// Some very big images take too long to decode, so feed the watchdog on each callback
// to avoid crashing.
App.feed_wdt();
size_t position = 0;
for (size_t y = 0; y < jpeg->iHeight; y++) {
for (size_t x = 0; x < jpeg->iWidth; x++) {
auto rg = decode_value(jpeg->pPixels[position++]);
auto ba = decode_value(jpeg->pPixels[position++]);
Color color(rg[1], rg[0], ba[1], ba[0]);
if (!decoder) {
ESP_LOGE(TAG, "Decoder pointer is null!");
return 0;
}
decoder->draw(jpeg->x + x, jpeg->y + y, 1, 1, color);
}
}
return 1;
}
void JpegDecoder::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, "Resize failed!");
// TODO: return an error code;
}
}
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
if (size < this->download_size_) {
ESP_LOGV(TAG, "Download not complete. Size: %d/%d", size, this->download_size_);
return 0;
}
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
ESP_LOGE(TAG, "Could not open image for decoding.");
return DECODE_ERROR_INVALID_TYPE;
}
auto jpeg_type = this->jpeg_.getJPEGType();
if (jpeg_type == JPEG_MODE_INVALID) {
ESP_LOGE(TAG, "Unsupported JPEG image");
return DECODE_ERROR_INVALID_TYPE;
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
ESP_LOGE(TAG, "Progressive JPEG images not supported");
return DECODE_ERROR_INVALID_TYPE;
}
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
this->jpeg_.setUserPointer(this);
this->jpeg_.setPixelType(RGB8888);
this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight());
if (!this->jpeg_.decode(0, 0, 0)) {
ESP_LOGE(TAG, "Error while decoding.");
this->jpeg_.close();
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
this->decoded_bytes_ = size;
this->jpeg_.close();
return size;
}
} // namespace online_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT

View File

@ -0,0 +1,34 @@
#pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
#include <JPEGDEC.h>
namespace esphome {
namespace online_image {
/**
* @brief Image decoder specialization for JPEG images.
*/
class JpegDecoder : public ImageDecoder {
public:
/**
* @brief Construct a new JPEG Decoder object.
*
* @param display The image to decode the stream into.
*/
JpegDecoder(OnlineImage *image) : ImageDecoder(image) {}
~JpegDecoder() override {}
void prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
protected:
JPEGDEC jpeg_{};
};
} // namespace online_image
} // namespace esphome
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT

View File

@ -9,6 +9,9 @@ static const char *const TAG = "online_image";
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
#include "bmp_image.h" #include "bmp_image.h"
#endif #endif
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
#include "jpeg_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,6 +35,7 @@ OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFor
: Image(nullptr, 0, 0, type, transparency), : Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
download_buffer_initial_size_(download_buffer_size),
format_(format), format_(format),
fixed_width_(width), fixed_width_(width),
fixed_height_(height) { fixed_height_(height) {
@ -123,23 +127,32 @@ void OnlineImage::update() {
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
if (this->format_ == ImageFormat::BMP) { if (this->format_ == ImageFormat::BMP) {
ESP_LOGD(TAG, "Allocating BMP decoder");
this->decoder_ = make_unique<BmpDecoder>(this); this->decoder_ = make_unique<BmpDecoder>(this);
} }
#endif // ONLINE_IMAGE_BMP_SUPPORT #endif // ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
if (this->format_ == ImageFormat::JPEG) {
ESP_LOGD(TAG, "Allocating JPEG decoder");
this->decoder_ = esphome::make_unique<JpegDecoder>(this);
}
#endif // USE_ONLINE_IMAGE_JPEG_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");
this->decoder_ = make_unique<PngDecoder>(this); this->decoder_ = make_unique<PngDecoder>(this);
} }
#endif // ONLINE_IMAGE_PNG_SUPPORT #endif // ONLINE_IMAGE_PNG_SUPPORT
if (!this->decoder_) { if (!this->decoder_) {
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
this->end_connection_(); this->end_connection_();
this->download_error_callback_.call(); this->download_error_callback_.call();
return; return;
} }
this->decoder_->prepare(total_size); this->decoder_->prepare(total_size);
ESP_LOGI(TAG, "Downloading image"); ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size);
this->start_time_ = ::time(nullptr);
} }
void OnlineImage::loop() { void OnlineImage::loop() {
@ -153,6 +166,7 @@ void OnlineImage::loop() {
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(),
this->width_, this->height_); this->width_, this->height_);
ESP_LOGD(TAG, "Total time: %lds", ::time(nullptr) - this->start_time_);
this->end_connection_(); this->end_connection_();
this->download_finished_callback_.call(); this->download_finished_callback_.call();
return; return;
@ -163,6 +177,10 @@ void OnlineImage::loop() {
} }
size_t available = this->download_buffer_.free_capacity(); size_t available = this->download_buffer_.free_capacity();
if (available) { if (available) {
// Some decoders need to fully download the image before downloading.
// In case of huge images, don't wait blocking until the whole image has been downloaded,
// use smaller chunks
available = std::min(available, this->download_buffer_initial_size_);
auto len = this->downloader_->read(this->download_buffer_.append(), available); auto len = this->downloader_->read(this->download_buffer_.append(), available);
if (len > 0) { if (len > 0) {
this->download_buffer_.write(len); this->download_buffer_.write(len);

View File

@ -23,7 +23,7 @@ using t_http_codes = enum {
enum ImageFormat { enum ImageFormat {
/** Automatically detect from MIME type. Not supported yet. */ /** Automatically detect from MIME type. Not supported yet. */
AUTO, AUTO,
/** JPEG format. Not supported yet. */ /** JPEG format. */
JPEG, JPEG,
/** PNG format. */ /** PNG format. */
PNG, PNG,
@ -79,6 +79,13 @@ class OnlineImage : public PollingComponent,
*/ */
void release(); void release();
/**
* Resize the download buffer
*
* @param size The new size for the download buffer.
*/
size_t resize_download_buffer(size_t size) { return this->download_buffer_.resize(size); }
void add_on_finished_callback(std::function<void()> &&callback); void add_on_finished_callback(std::function<void()> &&callback);
void add_on_error_callback(std::function<void()> &&callback); void add_on_error_callback(std::function<void()> &&callback);
@ -119,6 +126,12 @@ class OnlineImage : public PollingComponent,
uint8_t *buffer_; uint8_t *buffer_;
DownloadBuffer download_buffer_; DownloadBuffer download_buffer_;
/**
* This is the *initial* size of the download buffer, not the current size.
* The download buffer can be resized at runtime; the download_buffer_initial_size_
* will *not* change even if the download buffer has been resized.
*/
size_t download_buffer_initial_size_;
const ImageFormat format_; const ImageFormat format_;
image::Image *placeholder_{nullptr}; image::Image *placeholder_{nullptr};
@ -148,6 +161,8 @@ class OnlineImage : public PollingComponent,
*/ */
int buffer_height_; int buffer_height_;
time_t start_time_;
friend bool ImageDecoder::set_size(int width, int height); 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 void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
}; };

View File

@ -2,7 +2,6 @@
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_buffer.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"

View File

@ -62,6 +62,7 @@
#define USE_NUMBER #define USE_NUMBER
#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_OTA #define USE_OTA
#define USE_OTA_PASSWORD #define USE_OTA_PASSWORD
#define USE_OTA_STATE_CALLBACK #define USE_OTA_STATE_CALLBACK

View File

@ -41,6 +41,8 @@ lib_deps =
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
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
https://github.com/bitbank2/JPEGDEC.git#1.6.2 ; 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

@ -30,6 +30,14 @@ online_image:
url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp
format: BMP format: BMP
type: BINARY type: BINARY
- id: online_jpeg_image
url: http://www.faqs.org/images/library.jpg
format: JPEG
type: RGB
- id: online_jpg_image
url: http://www.faqs.org/images/library.jpg
format: JPG
type: RGB565
# Check the set_url action # Check the set_url action
esphome: esphome: