diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index ca4eefea6f..fdb49fc493 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -61,8 +61,22 @@ class PNGFormat(Format): cg.add_library("pngle", "1.0.2") +class BMPFormat(Format): + def __init__(self): + super().__init__("BMP") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") + + # New formats can be added here. -IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)} +IMAGE_FORMATS = { + x.image_type: x + for x in ( + PNGFormat(), + BMPFormat(), + ) +} OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp new file mode 100644 index 0000000000..af9019a4d2 --- /dev/null +++ b/esphome/components/online_image/bmp_image.cpp @@ -0,0 +1,101 @@ +#include "bmp_image.h" + +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT + +#include "esphome/components/display/display.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace online_image { + +static const char *const TAG = "online_image.bmp"; + +int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { + size_t index = 0; + if (this->current_index_ == 0 && index == 0 && size > 14) { + /** + * BMP file format: + * 0-1: Signature (BM) + * 2-5: File size + * 6-9: Reserved + * 10-13: Pixel data offset + * + * Integer values are stored in little-endian format. + */ + + // Check if the file is a BMP image + if (buffer[0] != 'B' || buffer[1] != 'M') { + ESP_LOGE(TAG, "Not a BMP file"); + return DECODE_ERROR_INVALID_TYPE; + } + + this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); + + this->current_index_ = 14; + index = 14; + } + if (this->current_index_ == 14 && index == 14 && size > this->data_offset_) { + /** + * BMP DIB header: + * 14-17: DIB header size + * 18-21: Image width + * 22-25: Image height + * 26-27: Number of color planes + * 28-29: Bits per pixel + * 30-33: Compression method + * 34-37: Image data size + * 38-41: Horizontal resolution + * 42-45: Vertical resolution + * 46-49: Number of colors in the color table + */ + + this->width_ = encode_uint32(buffer[21], buffer[20], buffer[19], buffer[18]); + this->height_ = encode_uint32(buffer[25], buffer[24], buffer[23], buffer[22]); + this->bits_per_pixel_ = encode_uint16(buffer[29], buffer[28]); + this->compression_method_ = encode_uint32(buffer[33], buffer[32], buffer[31], buffer[30]); + this->image_data_size_ = encode_uint32(buffer[37], buffer[36], buffer[35], buffer[34]); + this->color_table_entries_ = encode_uint32(buffer[49], buffer[48], buffer[47], buffer[46]); + + switch (this->bits_per_pixel_) { + case 1: + this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); + break; + default: + ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + if (this->compression_method_ != 0) { + ESP_LOGE(TAG, "Unsupported compression method: %d", this->compression_method_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + if (!this->set_size(this->width_, this->height_)) { + return DECODE_ERROR_OUT_OF_MEMORY; + } + this->current_index_ = this->data_offset_; + index = this->data_offset_; + } + while (index < size) { + size_t paint_index = this->current_index_ - this->data_offset_; + + uint8_t current_byte = buffer[index]; + for (uint8_t i = 0; i < 8; i++) { + size_t x = (paint_index * 8) % this->width_ + i; + size_t y = (this->height_ - 1) - (paint_index / this->width_bytes_); + Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; + this->draw(x, y, 1, 1, c); + } + this->current_index_++; + index++; + } + this->decoded_bytes_ += size; + return size; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_BMP_SUPPORT diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/online_image/bmp_image.h new file mode 100644 index 0000000000..61192f6a46 --- /dev/null +++ b/esphome/components/online_image/bmp_image.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT + +#include "image_decoder.h" + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for PNG images. + */ +class BmpDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new BMP Decoder object. + * + * @param display The image to decode the stream into. + */ + BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} + + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + size_t current_index_{0}; + ssize_t width_{0}; + ssize_t height_{0}; + uint16_t bits_per_pixel_{0}; + uint32_t compression_method_{0}; + uint32_t image_data_size_{0}; + uint32_t color_table_entries_{0}; + size_t width_bytes_{0}; + size_t data_offset_{0}; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_BMP_SUPPORT diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp index 50ec39dfcc..d0d0495ba6 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/image_decoder.cpp @@ -8,10 +8,11 @@ namespace online_image { static const char *const TAG = "online_image.decoder"; -void ImageDecoder::set_size(int width, int height) { - this->image_->resize_(width, height); +bool ImageDecoder::set_size(int width, int height) { + bool resized = this->image_->resize_(width, height); this->x_scale_ = static_cast(this->image_->buffer_width_) / width; this->y_scale_ = static_cast(this->image_->buffer_height_) / height; + return resized; } void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 7c5175d72d..3b04824bb9 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -4,6 +4,12 @@ namespace esphome { namespace online_image { +enum DecodeError : int { + DECODE_ERROR_INVALID_TYPE = -1, + DECODE_ERROR_UNSUPPORTED_FORMAT = -2, + DECODE_ERROR_OUT_OF_MEMORY = -3, +}; + class OnlineImage; /** @@ -45,8 +51,9 @@ class ImageDecoder { * * @param width The image's width. * @param height The image's height. + * @return true if the image was resized, false otherwise. */ - void set_size(int width, int height); + bool set_size(int width, int height); /** * @brief Fill a rectangle on the display_buffer using the defined color. diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 93d070c6a9..23fe61b534 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -10,6 +10,10 @@ static const char *const TAG = "online_image"; #include "png_image.h" #endif +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#include "bmp_image.h" +#endif + namespace esphome { namespace online_image { @@ -120,9 +124,14 @@ void OnlineImage::update() { #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT if (this->format_ == ImageFormat::PNG) { - this->decoder_ = esphome::make_unique(this); + this->decoder_ = make_unique(this); } #endif // ONLINE_IMAGE_PNG_SUPPORT +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT + if (this->format_ == ImageFormat::BMP) { + this->decoder_ = make_unique(this); + } +#endif // ONLINE_IMAGE_BMP_SUPPORT if (!this->decoder_) { ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index bafd8ba67e..849f860ad5 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -1,10 +1,10 @@ #pragma once +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/image/image.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "esphome/components/http_request/http_request.h" -#include "esphome/components/image/image.h" #include "image_decoder.h" @@ -27,6 +27,8 @@ enum ImageFormat { JPEG, /** PNG format. */ PNG, + /** BMP format. */ + BMP, }; /** @@ -146,7 +148,7 @@ class OnlineImage : public PollingComponent, */ int buffer_height_; - friend void 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); }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 96a05435ed..074b19809f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -60,6 +60,7 @@ #define USE_NETWORK #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER +#define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 6c161e4f20..d75900adf9 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -26,6 +26,10 @@ online_image: format: PNG type: RGB transparency: chroma_key + - id: online_binary_bmp + url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp + format: BMP + type: BINARY # Check the set_url action esphome: