diff --git a/CODEOWNERS b/CODEOWNERS index 25e6dc1b29..2aa0656343 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet +esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 7a6d25bc7d..057244e03d 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -2,97 +2,34 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS +from esphome.components import runtime_image +from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent -from esphome.components.image import ( - CONF_INVERT_ALPHA, - CONF_TRANSPARENCY, - IMAGE_SCHEMA, - Image_, - get_image_type_enum, - get_transparency_enum, - validate_settings, -) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_DITHER, - CONF_FILE, - CONF_FORMAT, CONF_ID, CONF_ON_ERROR, - CONF_RESIZE, CONF_TRIGGER_ID, - CONF_TYPE, CONF_URL, ) from esphome.core import Lambda -AUTO_LOAD = ["image"] +AUTO_LOAD = ["image", "runtime_image"] DEPENDENCIES = ["display", "http_request"] CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" -CONF_PLACEHOLDER = "placeholder" CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) online_image_ns = cg.esphome_ns.namespace("online_image") -ImageFormat = online_image_ns.enum("ImageFormat") - - -class Format: - def __init__(self, image_type): - self.image_type = image_type - - @property - def enum(self): - return getattr(ImageFormat, self.image_type) - - def actions(self): - pass - - -class BMPFormat(Format): - def __init__(self): - super().__init__("BMP") - - def actions(self): - 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", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") - - -class PNGFormat(Format): - def __init__(self): - super().__init__("PNG") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.1.0") - - -IMAGE_FORMATS = { - x.image_type: x - for x in ( - BMPFormat(), - JPEGFormat(), - 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, runtime_image.RuntimeImage +) # Actions SetUrlAction = online_image_ns.class_( @@ -111,29 +48,17 @@ DownloadErrorTrigger = online_image_ns.class_( ) -def remove_options(*options): - return { - cv.Optional(option): cv.invalid( - f"{option} is an invalid option for online_image" - ) - for option in options - } - - ONLINE_IMAGE_SCHEMA = ( - IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + runtime_image.runtime_image_schema(OnlineImage) .extend( { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), # Online Image specific options + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536), cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -162,7 +87,7 @@ CONFIG_SCHEMA = cv.Schema( rp2040_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), - validate_settings, + runtime_image.validate_runtime_image_settings, ) ) @@ -199,23 +124,21 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - image_format = IMAGE_FORMATS[config[CONF_FORMAT]] - image_format.actions() + # Use the enhanced helper function to get all runtime image parameters + settings = await runtime_image.process_runtime_image_config(config) url = config[CONF_URL] - width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = get_transparency_enum(config[CONF_TRANSPARENCY]) - var = cg.new_Pvariable( config[CONF_ID], url, - width, - height, - image_format.enum, - get_image_type_enum(config[CONF_TYPE]), - transparent, + settings.width, + settings.height, + settings.format_enum, + settings.image_type_enum, + settings.transparent, + settings.placeholder or cg.nullptr, config[CONF_BUFFER_SIZE], - config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", + settings.byte_order_big_endian, ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) @@ -227,10 +150,6 @@ async def to_code(config): else: cg.add(var.add_request_header(key, value)) - if placeholder_id := config.get(CONF_PLACEHOLDER): - placeholder = await cg.get_variable(placeholder_id) - cg.add(var.set_placeholder(placeholder)) - for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "cached")], conf) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/download_buffer.cpp similarity index 52% rename from esphome/components/online_image/image_decoder.cpp rename to esphome/components/online_image/download_buffer.cpp index 0ab7dadde3..999005df82 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/download_buffer.cpp @@ -1,29 +1,10 @@ -#include "image_decoder.h" -#include "online_image.h" - +#include "download_buffer.h" #include "esphome/core/log.h" +#include -namespace esphome { -namespace online_image { +namespace esphome::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; - 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) { - 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); - } - } -} +static const char *const TAG = "online_image.download_buffer"; DownloadBuffer::DownloadBuffer(size_t size) : size_(size) { this->buffer_ = this->allocator_.allocate(size); @@ -43,10 +24,12 @@ uint8_t *DownloadBuffer::data(size_t offset) { } size_t DownloadBuffer::read(size_t len) { - this->unread_ -= len; - if (this->unread_ > 0) { - memmove(this->data(), this->data(len), this->unread_); + if (len >= this->unread_) { + this->unread_ = 0; + return 0; } + this->unread_ -= len; + memmove(this->data(), this->data(len), this->unread_); return this->unread_; } @@ -69,5 +52,4 @@ size_t DownloadBuffer::resize(size_t size) { } } -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/online_image/download_buffer.h b/esphome/components/online_image/download_buffer.h new file mode 100644 index 0000000000..110a4b608a --- /dev/null +++ b/esphome/components/online_image/download_buffer.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::online_image { + +/** + * @brief Buffer for managing downloaded data. + * + * This class provides a buffer for downloading data with tracking of + * unread bytes and dynamic resizing capabilities. + */ +class DownloadBuffer { + public: + DownloadBuffer(size_t size); + ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + + uint8_t *data(size_t offset = 0); + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + size_t resize(size_t size); + + protected: + RAMAllocator allocator_{}; + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 4e2ecc2c77..6f5b82116d 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -1,6 +1,6 @@ #include "online_image.h" - #include "esphome/core/log.h" +#include static const char *const TAG = "online_image"; static const char *const ETAG_HEADER_NAME = "etag"; @@ -8,142 +8,82 @@ static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match"; static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified"; static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since"; -#include "image_decoder.h" +namespace esphome::online_image { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT -#include "bmp_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT -#include "jpeg_image.h" -#endif -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT -#include "png_image.h" -#endif - -namespace esphome { -namespace online_image { - -using image::ImageType; - -inline bool is_color_on(const Color &color) { - // This produces the most accurate monochrome conversion, but is slightly slower. - // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; - - // Approximation using fast integer computations; produces acceptable results - // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B - return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; -} - -OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) - : Image(nullptr, 0, 0, type, transparency), - buffer_(nullptr), - download_buffer_(download_buffer_size), - download_buffer_initial_size_(download_buffer_size), - format_(format), - fixed_width_(width), - fixed_height_(height), - is_big_endian_(is_big_endian) { +OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, + image::ImageType type, image::Transparency transparency, image::Image *placeholder, + uint32_t buffer_size, bool is_big_endian) + : RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height), + download_buffer_(buffer_size), + download_buffer_initial_size_(buffer_size) { this->set_url(url); } -void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { - if (this->data_start_) { - Image::draw(x, y, display, color_on, color_off); - } else if (this->placeholder_) { - this->placeholder_->draw(x, y, display, color_on, color_off); +bool OnlineImage::validate_url_(const std::string &url) { + if (url.empty()) { + ESP_LOGE(TAG, "URL is empty"); + return false; } -} - -void OnlineImage::release() { - if (this->buffer_) { - ESP_LOGV(TAG, "Deallocating old buffer"); - this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); - this->data_start_ = nullptr; - this->buffer_ = nullptr; - this->width_ = 0; - this->height_ = 0; - this->buffer_width_ = 0; - this->buffer_height_ = 0; - this->last_modified_ = ""; - this->etag_ = ""; - this->end_connection_(); + if (url.length() > 2048) { + ESP_LOGE(TAG, "URL is too long"); + return false; } -} - -size_t OnlineImage::resize_(int width_in, int height_in) { - int width = this->fixed_width_; - int height = this->fixed_height_; - if (this->is_auto_resize_()) { - width = width_in; - height = height_in; - if (this->width_ != width && this->height_ != height) { - this->release(); - } + if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) { + ESP_LOGE(TAG, "URL must start with http:// or https://"); + return false; } - size_t new_size = this->get_buffer_size_(width, height); - if (this->buffer_) { - // Buffer already allocated => no need to resize - return new_size; - } - ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); - this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_ == nullptr) { - ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, - this->allocator_.get_max_free_block_size()); - this->end_connection_(); - return 0; - } - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGV(TAG, "New size: (%d, %d)", width, height); - return new_size; + return true; } void OnlineImage::update() { - if (this->decoder_) { + if (this->is_decoding()) { ESP_LOGW(TAG, "Image already being updated."); return; } - ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); - std::list headers = {}; - - http_request::Header accept_header; - accept_header.name = "Accept"; - std::string accept_mime_type; - switch (this->format_) { -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - case ImageFormat::BMP: - accept_mime_type = "image/bmp"; - break; -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT - case ImageFormat::JPEG: - accept_mime_type = "image/jpeg"; - break; -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - case ImageFormat::PNG: - accept_mime_type = "image/png"; - break; -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - default: - accept_mime_type = "image/*"; + if (!this->validate_url_(this->url_)) { + ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str()); + this->download_error_callback_.call(); + return; } - accept_header.value = accept_mime_type + ",*/*;q=0.8"; + ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str()); + + std::list headers; + + // Add caching headers if we have them if (!this->etag_.empty()) { - headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_}); + headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_}); } - if (!this->last_modified_.empty()) { - headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); + headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_}); } - headers.push_back(accept_header); + // Add Accept header based on image format + const char *accept_mime_type; + switch (this->get_format()) { +#ifdef USE_RUNTIME_IMAGE_BMP + case runtime_image::BMP: + accept_mime_type = "image/bmp,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case runtime_image::JPEG: + accept_mime_type = "image/jpeg,*/*;q=0.8"; + break; +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case runtime_image::PNG: + accept_mime_type = "image/png,*/*;q=0.8"; + break; +#endif + default: + accept_mime_type = "image/*,*/*;q=0.8"; + break; + } + headers.push_back({"Accept", accept_mime_type}); + // User headers last so they can override any of the above for (auto &header : this->request_headers_) { headers.push_back(http_request::Header{header.first, header.second.value()}); } @@ -175,186 +115,117 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Starting download"); size_t total_size = this->downloader_->content_length; -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT - if (this->format_ == ImageFormat::BMP) { - ESP_LOGD(TAG, "Allocating BMP decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); + // Initialize decoder with the known format + if (!this->begin_decode(total_size)) { + ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format()); + this->end_connection_(); + this->download_error_callback_.call(); + return; } -#endif // USE_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(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - if (this->format_ == ImageFormat::PNG) { - ESP_LOGD(TAG, "Allocating PNG decoder"); - this->decoder_ = make_unique(this); - this->enable_loop(); - } -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT - if (!this->decoder_) { - ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_); - this->end_connection_(); - this->download_error_callback_.call(); - return; - } - auto prepare_result = this->decoder_->prepare(total_size); - if (prepare_result < 0) { - this->end_connection_(); - this->download_error_callback_.call(); - return; + // JPEG requires the complete image in the download buffer before decoding + if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) { + this->download_buffer_.resize(total_size); } + ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size); this->start_time_ = ::time(nullptr); + this->enable_loop(); } void OnlineImage::loop() { - if (!this->decoder_) { + if (!this->is_decoding()) { // Not decoding at the moment => nothing to do. this->disable_loop(); return; } - if (!this->downloader_ || this->decoder_->is_finished()) { - this->data_start_ = 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(), - this->width_, this->height_); - ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_)); + + if (!this->downloader_) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + // Check if download is complete — use decoder's format-specific completion check + // to handle both known content-length and chunked transfer encoding + if (this->is_decode_finished() || (this->downloader_->content_length > 0 && + this->downloader_->get_bytes_read() >= this->downloader_->content_length && + this->download_buffer_.unread() == 0)) { + // Finalize decoding + this->end_decode(); + + ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(), + (uint32_t) (::time(nullptr) - this->start_time_)); + + // Save caching headers this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME); + this->download_finished_callback_.call(false); this->end_connection_(); return; } - if (this->downloader_ == nullptr) { - ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); - return; - } + + // Download and decode more data size_t available = this->download_buffer_.free_capacity(); - 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 + if (available > 0) { + // Download in chunks to avoid blocking available = std::min(available, this->download_buffer_initial_size_); auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { this->download_buffer_.write(len); - auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); - if (fed < 0) { - ESP_LOGE(TAG, "Error when decoding image."); + + // Feed data to decoder + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + + if (consumed < 0) { + ESP_LOGE(TAG, "Error decoding image: %d", consumed); this->end_connection_(); this->download_error_callback_.call(); return; } - this->download_buffer_.read(fed); - } - } -} -void OnlineImage::map_chroma_key(Color &color) { - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (color.g == 1 && color.r == 0 && color.b == 0) { - color.g = 0; - } - if (color.w < 0x80) { - color.r = 0; - color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; - color.b = 0; - } - } -} - -void OnlineImage::draw_pixel_(int x, int y, Color color) { - 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); - return; - } - uint32_t pos = this->get_position_(x, y); - switch (this->type_) { - case ImageType::IMAGE_TYPE_BINARY: { - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - pos = x + y * width_8; - auto bitno = 0x80 >> (pos % 8u); - pos /= 8u; - auto on = is_color_on(color); - if (this->has_transparency() && color.w < 0x80) - on = false; - if (on) { - this->buffer_[pos] |= bitno; - } else { - this->buffer_[pos] &= ~bitno; + if (consumed > 0) { + this->download_buffer_.read(consumed); } - break; + } else if (len < 0) { + ESP_LOGE(TAG, "Error downloading image: %d", len); + this->end_connection_(); + this->download_error_callback_.call(); + return; } - case ImageType::IMAGE_TYPE_GRAYSCALE: { - auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { - if (gray == 1) { - gray = 0; - } - if (color.w < 0x80) { - gray = 1; - } - } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - if (color.w != 0xFF) - gray = color.w; - } - this->buffer_[pos] = gray; - break; - } - case ImageType::IMAGE_TYPE_RGB565: { - this->map_chroma_key(color); - uint16_t col565 = display::ColorUtil::color_to_565(color); - if (this->is_big_endian_) { - this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); - this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - } else { - this->buffer_[pos + 0] = static_cast(col565 & 0xFF); - this->buffer_[pos + 1] = static_cast((col565 >> 8) & 0xFF); - } - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 2] = color.w; - } - break; - } - case ImageType::IMAGE_TYPE_RGB: { - this->map_chroma_key(color); - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { - this->buffer_[pos + 3] = color.w; - } - break; + } else { + // Buffer is full, need to decode some data first + auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread()); + if (consumed > 0) { + this->download_buffer_.read(consumed); + } else if (consumed < 0) { + ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } else { + // Decoder can't process more data, might need complete image + // This is normal for JPEG which needs complete data + ESP_LOGV(TAG, "Decoder waiting for more data"); } } } void OnlineImage::end_connection_() { + // Abort any in-progress decode to free decoder resources. + // Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release(). + if (this->is_decoding()) { + RuntimeImage::release(); + } if (this->downloader_) { this->downloader_->end(); this->downloader_ = nullptr; } - this->decoder_.reset(); this->download_buffer_.reset(); -} - -bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { - ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); - return false; - } - return true; + this->disable_loop(); } void OnlineImage::add_on_finished_callback(std::function &&callback) { @@ -365,5 +236,16 @@ void OnlineImage::add_on_error_callback(std::function &&callback) { this->download_error_callback_.add(std::move(callback)); } -} // namespace online_image -} // namespace esphome +void OnlineImage::release() { + // Clear cache headers + this->etag_ = ""; + this->last_modified_ = ""; + + // End any active connection + this->end_connection_(); + + // Call parent's release to free the image buffer + RuntimeImage::release(); +} + +} // namespace esphome::online_image diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 12d409ca29..c7c80c7c66 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -1,15 +1,14 @@ #pragma once +#include "download_buffer.h" #include "esphome/components/http_request/http_request.h" -#include "esphome/components/image/image.h" +#include "esphome/components/runtime_image/runtime_image.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "image_decoder.h" - -namespace esphome { -namespace online_image { +namespace esphome::online_image { using t_http_codes = enum { HTTP_CODE_OK = 200, @@ -17,27 +16,13 @@ using t_http_codes = enum { HTTP_CODE_NOT_FOUND = 404, }; -/** - * @brief Format that the image is encoded with. - */ -enum ImageFormat { - /** Automatically detect from MIME type. Not supported yet. */ - AUTO, - /** JPEG format. */ - JPEG, - /** PNG format. */ - PNG, - /** BMP format. */ - BMP, -}; - /** * @brief Download an image from a given URL, and decode it using the specified decoder. * The image will then be stored in a buffer, so that it can be re-displayed without the * need to re-download or re-decode. */ class OnlineImage : public PollingComponent, - public image::Image, + public runtime_image::RuntimeImage, public Parented { public: /** @@ -46,17 +31,19 @@ class OnlineImage : public PollingComponent, * @param url URL to download the image from. * @param width Desired width of the target image area. * @param height Desired height of the target image area. - * @param format Format that the image is encoded in (@see ImageFormat). + * @param format Format that the image is encoded in (@see runtime_image::ImageFormat). + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. * @param buffer_size Size of the buffer used to download the image. + * @param is_big_endian Whether the image is stored in big-endian format. */ - OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); - - void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type, + image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size, + bool is_big_endian = false); void update() override; void loop() override; - void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { @@ -69,82 +56,26 @@ class OnlineImage : public PollingComponent, /** Add the request header */ template void add_request_header(const std::string &header, V value) { - this->request_headers_.push_back(std::pair >(header, value)); + this->request_headers_.push_back(std::pair>(header, value)); } - /** - * @brief Set the image that needs to be shown as long as the downloaded image - * is not available. - * - * @param placeholder Pointer to the (@link Image) to show as placeholder. - */ - void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } - /** * Release the buffer storing the image. The image will need to be downloaded again * to be able to be displayed. */ 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 &&callback); void add_on_error_callback(std::function &&callback); protected: bool validate_url_(const std::string &url); - - 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; } - - int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } - - ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } - - /** - * @brief Resize the image buffer to the requested dimensions. - * - * The buffer will be allocated if not existing. - * If the dimensions have been fixed in the yaml config, the buffer will be created - * with those dimensions and not resized, even on request. - * Otherwise, the old buffer will be deallocated and a new buffer with the requested - * allocated - * - * @param width - * @param height - * @return 0 if no memory could be allocated, the size of the new buffer otherwise. - */ - size_t resize_(int width, int height); - - /** - * @brief Draw a pixel into the buffer. - * - * This is used by the decoder to fill the buffer that will later be displayed - * by the `draw` method. This will internally convert the supplied 32 bit RGBA - * color into the requested image storage format. - * - * @param x Horizontal pixel position. - * @param y Vertical pixel position. - * @param color 32 bit color to put into the pixel. - */ - void draw_pixel_(int x, int y, Color color); - void end_connection_(); CallbackManager download_finished_callback_{}; CallbackManager download_error_callback_{}; std::shared_ptr downloader_{nullptr}; - std::unique_ptr decoder_{nullptr}; - - uint8_t *buffer_; DownloadBuffer download_buffer_; /** * This is the *initial* size of the download buffer, not the current size. @@ -153,40 +84,10 @@ class OnlineImage : public PollingComponent, */ size_t download_buffer_initial_size_; - const ImageFormat format_; - image::Image *placeholder_{nullptr}; - std::string url_{""}; - std::vector > > request_headers_; + std::vector>> request_headers_; - /** width requested on configuration, or 0 if non specified. */ - const int fixed_width_; - /** height requested on configuration, or 0 if non specified. */ - const int fixed_height_; - /** - * Whether the image is stored in big-endian format. - * This is used to determine how to store 16 bit colors in the buffer. - */ - bool is_big_endian_; - /** - * Actual width of the current image. If fixed_width_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_width()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_width_; - /** - * Actual height of the current image. If fixed_height_ is specified, - * this will be equal to it; otherwise it will be set once the decoding - * starts and the original size is known. - * This needs to be separate from "BaseImage::get_height()" because the latter - * must return 0 until the image has been decoded (to avoid showing partially - * decoded images). - */ - int buffer_height_; /** * The value of the ETag HTTP header provided in the last response. */ @@ -197,9 +98,6 @@ class OnlineImage : public PollingComponent, std::string last_modified_ = ""; 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); }; template class OnlineImageSetUrlAction : public Action { @@ -241,5 +139,4 @@ class DownloadErrorTrigger : public Trigger<> { } }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::online_image diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py new file mode 100644 index 0000000000..0773a53d91 --- /dev/null +++ b/esphome/components/runtime_image/__init__.py @@ -0,0 +1,191 @@ +from dataclasses import dataclass + +import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.image import ( + IMAGE_TYPE, + Image_, + validate_settings, + validate_transparency, + validate_type, +) +import esphome.config_validation as cv +from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE + +AUTO_LOAD = ["image"] +CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"] + +CONF_PLACEHOLDER = "placeholder" +CONF_TRANSPARENCY = "transparency" + +runtime_image_ns = cg.esphome_ns.namespace("runtime_image") + +# Base decoder classes +ImageDecoder = runtime_image_ns.class_("ImageDecoder") +BmpDecoder = runtime_image_ns.class_("BmpDecoder", ImageDecoder) +JpegDecoder = runtime_image_ns.class_("JpegDecoder", ImageDecoder) +PngDecoder = runtime_image_ns.class_("PngDecoder", ImageDecoder) + +# Runtime image class +RuntimeImage = runtime_image_ns.class_( + "RuntimeImage", cg.esphome_ns.namespace("image").class_("Image") +) + +# Image format enum +ImageFormat = runtime_image_ns.enum("ImageFormat") +IMAGE_FORMAT_AUTO = ImageFormat.AUTO +IMAGE_FORMAT_JPEG = ImageFormat.JPEG +IMAGE_FORMAT_PNG = ImageFormat.PNG +IMAGE_FORMAT_BMP = ImageFormat.BMP + +# Export enum for decode errors +DecodeError = runtime_image_ns.enum("DecodeError") +DECODE_ERROR_INVALID_TYPE = DecodeError.DECODE_ERROR_INVALID_TYPE +DECODE_ERROR_UNSUPPORTED_FORMAT = DecodeError.DECODE_ERROR_UNSUPPORTED_FORMAT +DECODE_ERROR_OUT_OF_MEMORY = DecodeError.DECODE_ERROR_OUT_OF_MEMORY + + +class Format: + """Base class for image format definitions.""" + + def __init__(self, name: str, decoder_class: cg.MockObjClass) -> None: + self.name = name + self.decoder_class = decoder_class + + def actions(self) -> None: + """Add defines and libraries needed for this format.""" + + +class BMPFormat(Format): + """BMP format decoder configuration.""" + + def __init__(self): + super().__init__("BMP", BmpDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_BMP") + + +class JPEGFormat(Format): + """JPEG format decoder configuration.""" + + def __init__(self): + super().__init__("JPEG", JpegDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_JPEG") + cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2") + + +class PNGFormat(Format): + """PNG format decoder configuration.""" + + def __init__(self): + super().__init__("PNG", PngDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_PNG") + cg.add_library("pngle", "1.1.0") + + +# Registry of available formats +IMAGE_FORMATS = { + "BMP": BMPFormat(), + "JPEG": JPEGFormat(), + "PNG": PNGFormat(), + "JPG": JPEGFormat(), # Alias for JPEG +} + + +def get_format(format_name: str) -> Format | None: + """Get a format instance by name.""" + return IMAGE_FORMATS.get(format_name.upper()) + + +def enable_format(format_name: str) -> Format | None: + """Enable a specific image format by adding its defines and libraries.""" + format_obj = get_format(format_name) + if format_obj: + format_obj.actions() + return format_obj + return None + + +# Runtime image configuration schema base - to be extended by components +def runtime_image_schema(image_class: cg.MockObjClass = RuntimeImage) -> cv.Schema: + """Create a runtime image schema with the specified image class.""" + return cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(image_class), + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), + cv.Optional(CONF_BYTE_ORDER): cv.one_of( + "BIG_ENDIAN", "LITTLE_ENDIAN", upper=True + ), + cv.Optional(CONF_TRANSPARENCY, default="OPAQUE"): validate_transparency(), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + } + ) + + +def validate_runtime_image_settings(config: dict) -> dict: + """Apply validate_settings from image component to runtime image config.""" + return validate_settings(config) + + +@dataclass +class RuntimeImageSettings: + """Processed runtime image configuration parameters.""" + + width: int + height: int + format_enum: cg.MockObj + image_type_enum: cg.MockObj + transparent: cg.MockObj + byte_order_big_endian: bool + placeholder: cg.MockObj | None + + +async def process_runtime_image_config(config: dict) -> RuntimeImageSettings: + """ + Helper function to process common runtime image configuration parameters. + Handles format enabling and returns all necessary enums and parameters. + """ + from esphome.components.image import get_image_type_enum, get_transparency_enum + + # Get resize dimensions with default (0, 0) + width, height = config.get(CONF_RESIZE, (0, 0)) + + # Handle format (required for runtime images) + format_name = config[CONF_FORMAT] + # Enable the format in the runtime_image component + enable_format(format_name) + # Map format names to enum values (handle JPG as alias for JPEG) + if format_name.upper() == "JPG": + format_name = "JPEG" + format_enum = getattr(ImageFormat, format_name.upper()) + + # Get image type enum + image_type_enum = get_image_type_enum(config[CONF_TYPE]) + + # Get transparency enum + transparent = get_transparency_enum(config.get(CONF_TRANSPARENCY, "OPAQUE")) + + # Get byte order (True for big endian, False for little endian) + byte_order_big_endian = config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN" + + # Get placeholder if specified + placeholder = None + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + + return RuntimeImageSettings( + width=width, + height=height, + format_enum=format_enum, + image_type_enum=image_type_enum, + transparent=transparent, + byte_order_big_endian=byte_order_big_endian, + placeholder=placeholder, + ) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/runtime_image/bmp_decoder.cpp similarity index 82% rename from esphome/components/online_image/bmp_image.cpp rename to esphome/components/runtime_image/bmp_decoder.cpp index 676a2efca9..1a56484c60 100644 --- a/esphome/components/online_image/bmp_image.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -1,15 +1,14 @@ -#include "bmp_image.h" +#include "bmp_decoder.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "esphome/components/display/display.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { -static const char *const TAG = "online_image.bmp"; +static const char *const TAG = "image_decoder.bmp"; int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { size_t index = 0; @@ -30,7 +29,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return DECODE_ERROR_INVALID_TYPE; } - this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + // BMP file contains its own size in the header + size_t file_size = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + if (this->expected_size_ == 0) { + this->expected_size_ = file_size; // Use file header size if not provided + } this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); this->current_index_ = 14; @@ -90,8 +93,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { while (index < size) { uint8_t current_byte = buffer[index]; for (uint8_t i = 0; i < 8; i++) { - size_t x = (this->paint_index_ % this->width_) + i; - size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + size_t x = (this->paint_index_ % static_cast(this->width_)) + i; + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; this->draw(x, y, 1, 1, c); } @@ -110,8 +113,8 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { uint8_t b = buffer[index]; uint8_t g = buffer[index + 1]; uint8_t r = buffer[index + 2]; - size_t x = this->paint_index_ % this->width_; - size_t y = (this->height_ - 1) - (this->paint_index_ / this->width_); + size_t x = this->paint_index_ % static_cast(this->width_); + size_t y = static_cast(this->height_ - 1) - (this->paint_index_ / static_cast(this->width_)); Color c = Color(r, g, b); this->draw(x, y, 1, 1, c); this->paint_index_++; @@ -133,7 +136,6 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { return size; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/runtime_image/bmp_decoder.h similarity index 52% rename from esphome/components/online_image/bmp_image.h rename to esphome/components/runtime_image/bmp_decoder.h index 916ffea1ad..37db6b4940 100644 --- a/esphome/components/online_image/bmp_image.h +++ b/esphome/components/runtime_image/bmp_decoder.h @@ -1,27 +1,32 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_RUNTIME_IMAGE_BMP #include "image_decoder.h" +#include "runtime_image.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** - * @brief Image decoder specialization for PNG images. + * @brief Image decoder specialization for BMP images. */ class BmpDecoder : public ImageDecoder { public: /** * @brief Construct a new BMP Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} + BmpDecoder(RuntimeImage *image) : ImageDecoder(image) {} int HOT decode(uint8_t *buffer, size_t size) override; + bool is_finished() const override { + // BMP is finished when we've decoded all pixel data + return this->paint_index_ >= static_cast(this->width_ * this->height_); + } + protected: size_t current_index_{0}; size_t paint_index_{0}; @@ -36,7 +41,6 @@ class BmpDecoder : public ImageDecoder { uint8_t padding_bytes_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_BMP_SUPPORT +#endif // USE_RUNTIME_IMAGE_BMP diff --git a/esphome/components/runtime_image/image_decoder.cpp b/esphome/components/runtime_image/image_decoder.cpp new file mode 100644 index 0000000000..8d3320b5d1 --- /dev/null +++ b/esphome/components/runtime_image/image_decoder.cpp @@ -0,0 +1,28 @@ +#include "image_decoder.h" +#include "runtime_image.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::runtime_image { + +static const char *const TAG = "image_decoder"; + +bool ImageDecoder::set_size(int width, int height) { + bool success = this->image_->resize(width, height) > 0; + this->x_scale_ = static_cast(this->image_->get_buffer_width()) / width; + this->y_scale_ = static_cast(this->image_->get_buffer_height()) / height; + return success; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->get_buffer_width(), static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->get_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); + } + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/runtime_image/image_decoder.h similarity index 60% rename from esphome/components/online_image/image_decoder.h rename to esphome/components/runtime_image/image_decoder.h index d11b8b46d3..926108a8a0 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/runtime_image/image_decoder.h @@ -1,8 +1,7 @@ #pragma once #include "esphome/core/color.h" -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { enum DecodeError : int { DECODE_ERROR_INVALID_TYPE = -1, @@ -10,7 +9,7 @@ enum DecodeError : int { DECODE_ERROR_OUT_OF_MEMORY = -3, }; -class OnlineImage; +class RuntimeImage; /** * @brief Class to abstract decoding different image formats. @@ -20,19 +19,19 @@ class ImageDecoder { /** * @brief Construct a new Image Decoder object * - * @param image The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - ImageDecoder(OnlineImage *image) : image_(image) {} + ImageDecoder(RuntimeImage *image) : image_(image) {} virtual ~ImageDecoder() = default; /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be downloaded for the image. + * @param expected_size Hint about the expected data size (0 if unknown). * @return int Returns 0 on success, a {@see DecodeError} value in case of an error. */ - virtual int prepare(size_t download_size) { - this->download_size_ = download_size; + virtual int prepare(size_t expected_size) { + this->expected_size_ = expected_size; return 0; } @@ -73,49 +72,26 @@ class ImageDecoder { */ void draw(int x, int y, int w, int h, const Color &color); - bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + /** + * @brief Check if the decoder has finished processing. + * + * This should be overridden by decoders that can detect completion + * based on format-specific markers rather than byte counts. + */ + virtual bool is_finished() const { + if (this->expected_size_ > 0) { + return this->decoded_bytes_ >= this->expected_size_; + } + // If size is unknown, derived classes should override this + return false; + } protected: - OnlineImage *image_; - // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". - // Will be overwritten anyway once the download size is known. - size_t download_size_ = 1; - size_t decoded_bytes_ = 0; + RuntimeImage *image_; + size_t expected_size_ = 0; // Expected data size (0 if unknown) + size_t decoded_bytes_ = 0; // Bytes processed so far double x_scale_ = 1.0; double y_scale_ = 1.0; }; -class DownloadBuffer { - public: - DownloadBuffer(size_t size); - - virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } - - uint8_t *data(size_t offset = 0); - - uint8_t *append() { return this->data(this->unread_); } - - size_t unread() const { return this->unread_; } - size_t size() const { return this->size_; } - size_t free_capacity() const { return this->size_ - this->unread_; } - - size_t read(size_t len); - size_t write(size_t len) { - this->unread_ += len; - return this->unread_; - } - - void reset() { this->unread_ = 0; } - - size_t resize(size_t size); - - protected: - RAMAllocator allocator_{}; - uint8_t *buffer_; - size_t size_; - /** Total number of downloaded bytes not yet read. */ - size_t unread_; -}; - -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image diff --git a/esphome/components/online_image/jpeg_image.cpp b/esphome/components/runtime_image/jpeg_decoder.cpp similarity index 69% rename from esphome/components/online_image/jpeg_image.cpp rename to esphome/components/runtime_image/jpeg_decoder.cpp index 10586091d5..dcaa07cd58 100644 --- a/esphome/components/online_image/jpeg_image.cpp +++ b/esphome/components/runtime_image/jpeg_decoder.cpp @@ -1,16 +1,19 @@ -#include "jpeg_image.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#include "jpeg_decoder.h" +#ifdef USE_RUNTIME_IMAGE_JPEG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "online_image.h" -static const char *const TAG = "online_image.jpeg"; +#ifdef USE_ESP_IDF +#include "esp_task_wdt.h" +#endif -namespace esphome { -namespace online_image { +static const char *const TAG = "image_decoder.jpeg"; + +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the JPEGDEC engine when a chunk @@ -22,8 +25,14 @@ 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(); + // to avoid crashing if the executing task has a watchdog enabled. +#ifdef USE_ESP_IDF + if (esp_task_wdt_status(nullptr) == ESP_OK) { +#endif + App.feed_wdt(); +#ifdef USE_ESP_IDF + } +#endif size_t position = 0; size_t height = static_cast(jpeg->iHeight); size_t width = static_cast(jpeg->iWidth); @@ -43,22 +52,23 @@ static int draw_callback(JPEGDRAW *jpeg) { return 1; } -int 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, "Download buffer resize failed!"); - return DECODE_ERROR_OUT_OF_MEMORY; - } +int JpegDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); + // JPEG decoder needs complete data before decoding return 0; } 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_); + // JPEG decoder requires complete data + // If we know the expected size, wait for it + if (this->expected_size_ > 0 && size < this->expected_size_) { + ESP_LOGV(TAG, "Download not complete. Size: %zu/%zu", size, this->expected_size_); return 0; } + // If size unknown, try to decode and see if it's valid + // The JPEGDEC library will fail gracefully if data is incomplete + if (!this->jpeg_.openRAM(buffer, size, draw_callback)) { ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError()); return DECODE_ERROR_INVALID_TYPE; @@ -88,7 +98,6 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) { return size; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/jpeg_image.h b/esphome/components/runtime_image/jpeg_decoder.h similarity index 54% rename from esphome/components/online_image/jpeg_image.h rename to esphome/components/runtime_image/jpeg_decoder.h index fd488d6138..ed2401e263 100644 --- a/esphome/components/online_image/jpeg_image.h +++ b/esphome/components/runtime_image/jpeg_decoder.h @@ -1,12 +1,12 @@ #pragma once #include "image_decoder.h" +#include "runtime_image.h" #include "esphome/core/defines.h" -#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT +#ifdef USE_RUNTIME_IMAGE_JPEG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for JPEG images. @@ -16,19 +16,18 @@ class JpegDecoder : public ImageDecoder { /** * @brief Construct a new JPEG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - JpegDecoder(OnlineImage *image) : ImageDecoder(image) {} + JpegDecoder(RuntimeImage *image) : ImageDecoder(image) {} ~JpegDecoder() override {} - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: JPEGDEC jpeg_{}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT +#endif // USE_RUNTIME_IMAGE_JPEG diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/runtime_image/png_decoder.cpp similarity index 82% rename from esphome/components/online_image/png_image.cpp rename to esphome/components/runtime_image/png_decoder.cpp index ce9d3bdc91..9fe4a9c4ff 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/runtime_image/png_decoder.cpp @@ -1,15 +1,14 @@ -#include "png_image.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_decoder.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include "esphome/components/display/display_buffer.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "online_image.png"; +static const char *const TAG = "image_decoder.png"; -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Callback method that will be called by the PNGLE engine when the basic @@ -49,7 +48,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui } } -PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { +PngDecoder::PngDecoder(RuntimeImage *image) : ImageDecoder(image) { { pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); if (!pngle) { @@ -69,8 +68,8 @@ PngDecoder::~PngDecoder() { } } -int PngDecoder::prepare(size_t download_size) { - ImageDecoder::prepare(download_size); +int PngDecoder::prepare(size_t expected_size) { + ImageDecoder::prepare(expected_size); if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; @@ -86,8 +85,9 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); return DECODE_ERROR_OUT_OF_MEMORY; } - if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { - ESP_LOGD(TAG, "Waiting for data"); + // PNG can be decoded progressively, but wait for a reasonable chunk + if (size < 256 && this->expected_size_ > 0 && size < this->expected_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for more data"); return 0; } auto fed = pngle_feed(this->pngle_, buffer, size); @@ -99,7 +99,6 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { return fed; } -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/online_image/png_image.h b/esphome/components/runtime_image/png_decoder.h similarity index 65% rename from esphome/components/online_image/png_image.h rename to esphome/components/runtime_image/png_decoder.h index 40e85dde33..b5c1e70c2a 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/runtime_image/png_decoder.h @@ -3,11 +3,11 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "image_decoder.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "runtime_image.h" +#ifdef USE_RUNTIME_IMAGE_PNG #include -namespace esphome { -namespace online_image { +namespace esphome::runtime_image { /** * @brief Image decoder specialization for PNG images. @@ -17,12 +17,12 @@ class PngDecoder : public ImageDecoder { /** * @brief Construct a new PNG Decoder object. * - * @param display The image to decode the stream into. + * @param image The RuntimeImage to decode the stream into. */ - PngDecoder(OnlineImage *image); + PngDecoder(RuntimeImage *image); ~PngDecoder() override; - int prepare(size_t download_size) override; + int prepare(size_t expected_size) override; int HOT decode(uint8_t *buffer, size_t size) override; void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } @@ -30,11 +30,10 @@ class PngDecoder : public ImageDecoder { protected: RAMAllocator allocator_; - pngle_t *pngle_; + pngle_t *pngle_{nullptr}; uint32_t pixels_decoded_{0}; }; -} // namespace online_image -} // namespace esphome +} // namespace esphome::runtime_image -#endif // USE_ONLINE_IMAGE_PNG_SUPPORT +#endif // USE_RUNTIME_IMAGE_PNG diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp new file mode 100644 index 0000000000..1d70f38d6b --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -0,0 +1,300 @@ +#include "runtime_image.h" +#include "image_decoder.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +#ifdef USE_RUNTIME_IMAGE_BMP +#include "bmp_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG +#include "jpeg_decoder.h" +#endif +#ifdef USE_RUNTIME_IMAGE_PNG +#include "png_decoder.h" +#endif + +namespace esphome::runtime_image { + +static const char *const TAG = "runtime_image"; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height) + : Image(nullptr, 0, 0, type, transparency), + format_(format), + fixed_width_(fixed_width), + fixed_height_(fixed_height), + placeholder_(placeholder), + is_big_endian_(is_big_endian) {} + +RuntimeImage::~RuntimeImage() { this->release(); } + +int RuntimeImage::resize(int width, int height) { + // Use fixed dimensions if specified (0 means auto-resize) + int target_width = this->fixed_width_ ? this->fixed_width_ : width; + int target_height = this->fixed_height_ ? this->fixed_height_ : height; + + size_t result = this->resize_buffer_(target_width, target_height); + if (result > 0 && this->progressive_display_) { + // Update display dimensions for progressive display + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + return result; +} + +void RuntimeImage::draw_pixel(int x, int y, const Color &color) { + 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); + return; + } + + switch (this->type_) { + case image::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u; + uint32_t pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; + } else { + this->buffer_[pos] &= ~bitno; + } + break; + } + case image::IMAGE_TYPE_GRAYSCALE: { + uint32_t pos = this->get_position_(x, y); + auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; + } + this->buffer_[pos] = gray; + break; + } + case image::IMAGE_TYPE_RGB565: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color); + if (this->is_big_endian_) { + this->buffer_[pos + 0] = static_cast((rgb565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(rgb565 & 0xFF); + } else { + this->buffer_[pos + 0] = static_cast(rgb565 & 0xFF); + this->buffer_[pos + 1] = static_cast((rgb565 >> 8) & 0xFF); + } + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 2] = color.w; + } + break; + } + case image::IMAGE_TYPE_RGB: { + uint32_t pos = this->get_position_(x, y); + Color mapped_color = color; + this->map_chroma_key(mapped_color); + this->buffer_[pos + 0] = mapped_color.r; + this->buffer_[pos + 1] = mapped_color.g; + this->buffer_[pos + 2] = mapped_color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } + break; + } + } +} + +void RuntimeImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + +void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + // If we have a complete image, use the base class draw method + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + // Show placeholder while the runtime image is not available + this->placeholder_->draw(x, y, display, color_on, color_off); + } + // If no image is loaded and no placeholder, nothing to draw +} + +bool RuntimeImage::begin_decode(size_t expected_size) { + if (this->decoder_) { + ESP_LOGW(TAG, "Decoding already in progress"); + return false; + } + + this->decoder_ = this->create_decoder_(); + if (!this->decoder_) { + ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_); + return false; + } + + this->total_size_ = expected_size; + this->decoded_bytes_ = 0; + + // Initialize decoder + int result = this->decoder_->prepare(expected_size); + if (result < 0) { + ESP_LOGE(TAG, "Failed to prepare decoder: %d", result); + this->decoder_ = nullptr; + return false; + } + + return true; +} + +int RuntimeImage::feed_data(uint8_t *data, size_t len) { + if (!this->decoder_) { + ESP_LOGE(TAG, "No decoder initialized"); + return -1; + } + + int consumed = this->decoder_->decode(data, len); + if (consumed > 0) { + this->decoded_bytes_ += consumed; + } + + return consumed; +} + +bool RuntimeImage::end_decode() { + if (!this->decoder_) { + return false; + } + + // Finalize the image for display + if (!this->progressive_display_) { + // Only now make the image visible + this->width_ = this->buffer_width_; + this->height_ = this->buffer_height_; + this->data_start_ = this->buffer_; + } + + // Clean up decoder + this->decoder_ = nullptr; + + ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_); + return true; +} + +bool RuntimeImage::is_decode_finished() const { + if (!this->decoder_) { + return false; + } + return this->decoder_->is_finished(); +} + +void RuntimeImage::release() { + this->release_buffer_(); + // Reset decoder separately — release() can be called from within the decoder + // (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here. + // The decoder lifecycle is managed by begin_decode()/end_decode(). + this->decoder_ = nullptr; +} + +void RuntimeImage::release_buffer_() { + if (this->buffer_) { + ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->allocator_.deallocate(this->buffer_, this->get_buffer_size_(this->buffer_width_, this->buffer_height_)); + this->buffer_ = nullptr; + this->data_start_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + } +} + +size_t RuntimeImage::resize_buffer_(int width, int height) { + size_t new_size = this->get_buffer_size_(width, height); + + if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) { + // Buffer already allocated with correct size + return new_size; + } + + // Release old buffer if dimensions changed + if (this->buffer_) { + this->release_buffer_(); + } + + ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size); + this->buffer_ = this->allocator_.allocate(new_size); + + if (!this->buffer_) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size, + this->allocator_.get_max_free_block_size()); + return 0; + } + + // Clear buffer + memset(this->buffer_, 0, new_size); + + this->buffer_width_ = width; + this->buffer_height_ = height; + + return new_size; +} + +size_t RuntimeImage::get_buffer_size_(int width, int height) const { + return (this->get_bpp() * width + 7u) / 8u * height; +} + +int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; } + +std::unique_ptr RuntimeImage::create_decoder_() { + switch (this->format_) { +#ifdef USE_RUNTIME_IMAGE_BMP + case BMP: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_JPEG + case JPEG: + return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_PNG + case PNG: + return make_unique(this); +#endif + default: + ESP_LOGE(TAG, "Unsupported image format: %d", this->format_); + return nullptr; + } +} + +} // namespace esphome::runtime_image diff --git a/esphome/components/runtime_image/runtime_image.h b/esphome/components/runtime_image/runtime_image.h new file mode 100644 index 0000000000..0a5279d86d --- /dev/null +++ b/esphome/components/runtime_image/runtime_image.h @@ -0,0 +1,214 @@ +#pragma once + +#include "esphome/components/image/image.h" +#include "esphome/core/helpers.h" + +namespace esphome::runtime_image { + +// Forward declaration +class ImageDecoder; + +/** + * @brief Image format types that can be decoded dynamically. + */ +enum ImageFormat { + /** Automatically detect from data. Not implemented yet. */ + AUTO, + /** JPEG format. */ + JPEG, + /** PNG format. */ + PNG, + /** BMP format. */ + BMP, +}; + +/** + * @brief A dynamic image that can be loaded and decoded at runtime. + * + * This class provides dynamic buffer allocation and management for images + * that are decoded at runtime, as opposed to static images compiled into + * the firmware. It serves as a base class for components that need to + * load images dynamically from various sources. + */ +class RuntimeImage : public image::Image { + public: + /** + * @brief Construct a new RuntimeImage object. + * + * @param format The image format to decode. + * @param type The pixel format for the image. + * @param transparency The transparency type for the image. + * @param placeholder Optional placeholder image to show while loading. + * @param is_big_endian Whether the image is stored in big-endian format. + * @param fixed_width Fixed width for the image (0 for auto-resize). + * @param fixed_height Fixed height for the image (0 for auto-resize). + */ + RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency, + image::Image *placeholder = nullptr, bool is_big_endian = false, int fixed_width = 0, + int fixed_height = 0); + + ~RuntimeImage(); + + // Decoder interface methods + /** + * @brief Resize the image buffer to the requested dimensions. + * + * The buffer will be allocated if not existing. + * If fixed dimensions have been specified in the constructor, the buffer will be created + * with those dimensions and not resized, even on request. + * Otherwise, the old buffer will be deallocated and a new buffer with the requested + * dimensions allocated. + * + * @param width Requested width (ignored if fixed_width_ is set) + * @param height Requested height (ignored if fixed_height_ is set) + * @return Size of the allocated buffer in bytes, or 0 if allocation failed. + */ + int resize(int width, int height); + void draw_pixel(int x, int y, const Color &color); + void map_chroma_key(Color &color); + int get_buffer_width() const { return this->buffer_width_; } + int get_buffer_height() const { return this->buffer_height_; } + + // Image drawing interface + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + /** + * @brief Begin decoding an image. + * + * @param expected_size Optional hint about the expected data size. + * @return true if decoder was successfully initialized. + */ + bool begin_decode(size_t expected_size = 0); + + /** + * @brief Feed data to the decoder. + * + * @param data Pointer to the data buffer. + * @param len Length of data to process. + * @return Number of bytes consumed by the decoder. + */ + int feed_data(uint8_t *data, size_t len); + + /** + * @brief Complete the decoding process. + * + * @return true if decoding completed successfully. + */ + bool end_decode(); + + /** + * @brief Check if decoding is currently in progress. + */ + bool is_decoding() const { return this->decoder_ != nullptr; } + + /** + * @brief Check if the decoder has finished processing all data. + * + * This delegates to the decoder's format-specific completion check, + * which handles both known-size and chunked transfer cases. + */ + bool is_decode_finished() const; + + /** + * @brief Check if an image is currently loaded. + */ + bool is_loaded() const { return this->buffer_ != nullptr; } + + /** + * @brief Get the image format. + */ + ImageFormat get_format() const { return this->format_; } + + /** + * @brief Release the image buffer and free memory. + */ + void release(); + + /** + * @brief Set whether to allow progressive display during decode. + * + * When enabled, the image can be displayed even while still decoding. + * When disabled, the image is only displayed after decoding completes. + */ + void set_progressive_display(bool progressive) { this->progressive_display_ = progressive; } + + protected: + /** + * @brief Resize the image buffer to the requested dimensions. + * + * @param width New width in pixels. + * @param height New height in pixels. + * @return Size of the allocated buffer, or 0 on failure. + */ + size_t resize_buffer_(int width, int height); + + /** + * @brief Release only the image buffer without resetting the decoder. + * + * This is safe to call from within the decoder (e.g., during resize). + */ + void release_buffer_(); + + /** + * @brief Get the buffer size in bytes for given dimensions. + */ + size_t get_buffer_size_(int width, int height) const; + + /** + * @brief Get the position in the buffer for a pixel. + */ + int get_position_(int x, int y) const; + + /** + * @brief Create decoder instance for the image's format. + */ + std::unique_ptr create_decoder_(); + + // Memory management + RAMAllocator allocator_{}; + uint8_t *buffer_{nullptr}; + + // Decoder management + std::unique_ptr decoder_{nullptr}; + /** The image format this RuntimeImage is configured to decode. */ + const ImageFormat format_; + + /** + * Actual width of the current image. + * This needs to be separate from "Image::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_width_{0}; + /** + * Actual height of the current image. + * This needs to be separate from "Image::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). When progressive_display_ is enabled, Image dimensions + * are updated during decoding to allow rendering in progress. + */ + int buffer_height_{0}; + + // Decoding state + size_t total_size_{0}; + size_t decoded_bytes_{0}; + + /** Fixed width requested on configuration, or 0 if not specified. */ + const int fixed_width_{0}; + /** Fixed height requested on configuration, or 0 if not specified. */ + const int fixed_height_{0}; + + /** Placeholder image to show when the runtime image is not available. */ + image::Image *placeholder_{nullptr}; + + // Configuration + bool progressive_display_{false}; + /** + * Whether the image is stored in big-endian format. + * This is used to determine how to store 16 bit colors in the buffer. + */ + bool is_big_endian_{false}; +}; + +} // namespace esphome::runtime_image diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bfa33e4e59..0d6c1a42e8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -148,9 +148,9 @@ #define USE_MQTT #define USE_MQTT_COVER_JSON #define USE_NETWORK -#define USE_ONLINE_IMAGE_BMP_SUPPORT -#define USE_ONLINE_IMAGE_PNG_SUPPORT -#define USE_ONLINE_IMAGE_JPEG_SUPPORT +#define USE_RUNTIME_IMAGE_BMP +#define USE_RUNTIME_IMAGE_PNG +#define USE_RUNTIME_IMAGE_JPEG #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER