ESPHome 2025.9.0-dev
Loading...
Searching...
No Matches
online_image.cpp
Go to the documentation of this file.
1#include "online_image.h"
2
3#include "esphome/core/log.h"
4
5static const char *const TAG = "online_image";
6static const char *const ETAG_HEADER_NAME = "etag";
7static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match";
8static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified";
9static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
10
11#include "image_decoder.h"
12
13#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
14#include "bmp_image.h"
15#endif
16#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
17#include "jpeg_image.h"
18#endif
19#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
20#include "png_image.h"
21#endif
22
23namespace esphome {
24namespace online_image {
25
27
28inline bool is_color_on(const Color &color) {
29 // This produces the most accurate monochrome conversion, but is slightly slower.
30 // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
31
32 // Approximation using fast integer computations; produces acceptable results
33 // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
34 return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
35}
36
37OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
38 image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian)
39 : Image(nullptr, 0, 0, type, transparency),
40 buffer_(nullptr),
41 download_buffer_(download_buffer_size),
42 download_buffer_initial_size_(download_buffer_size),
43 format_(format),
44 fixed_width_(width),
45 fixed_height_(height),
46 is_big_endian_(is_big_endian) {
47 this->set_url(url);
48}
49
50void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
51 if (this->data_start_) {
52 Image::draw(x, y, display, color_on, color_off);
53 } else if (this->placeholder_) {
54 this->placeholder_->draw(x, y, display, color_on, color_off);
55 }
56}
57
59 if (this->buffer_) {
60 ESP_LOGV(TAG, "Deallocating old buffer");
61 this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
62 this->data_start_ = nullptr;
63 this->buffer_ = nullptr;
64 this->width_ = 0;
65 this->height_ = 0;
66 this->buffer_width_ = 0;
67 this->buffer_height_ = 0;
68 this->last_modified_ = "";
69 this->etag_ = "";
70 this->end_connection_();
71 }
72}
73
74size_t OnlineImage::resize_(int width_in, int height_in) {
75 int width = this->fixed_width_;
76 int height = this->fixed_height_;
77 if (this->is_auto_resize_()) {
78 width = width_in;
79 height = height_in;
80 if (this->width_ != width && this->height_ != height) {
81 this->release();
82 }
83 }
84 size_t new_size = this->get_buffer_size_(width, height);
85 if (this->buffer_) {
86 // Buffer already allocated => no need to resize
87 return new_size;
88 }
89 ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
90 this->buffer_ = this->allocator_.allocate(new_size);
91 if (this->buffer_ == nullptr) {
92 ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
94 this->end_connection_();
95 return 0;
96 }
97 this->buffer_width_ = width;
98 this->buffer_height_ = height;
99 this->width_ = width;
100 ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
101 return new_size;
102}
103
105 if (this->decoder_) {
106 ESP_LOGW(TAG, "Image already being updated.");
107 return;
108 }
109 ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
110
111 std::list<http_request::Header> headers = {};
112
113 http_request::Header accept_header;
114 accept_header.name = "Accept";
115 std::string accept_mime_type;
116 switch (this->format_) {
117#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
118 case ImageFormat::BMP:
119 accept_mime_type = "image/bmp";
120 break;
121#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
122#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
124 accept_mime_type = "image/jpeg";
125 break;
126#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
127#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
128 case ImageFormat::PNG:
129 accept_mime_type = "image/png";
130 break;
131#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
132 default:
133 accept_mime_type = "image/*";
134 }
135 accept_header.value = accept_mime_type + ",*/*;q=0.8";
136
137 if (!this->etag_.empty()) {
138 headers.push_back(http_request::Header{IF_NONE_MATCH_HEADER_NAME, this->etag_});
139 }
140
141 if (!this->last_modified_.empty()) {
142 headers.push_back(http_request::Header{IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
143 }
144
145 headers.push_back(accept_header);
146
147 for (auto &header : this->request_headers_) {
148 headers.push_back(http_request::Header{header.first, header.second.value()});
149 }
150
151 this->downloader_ = this->parent_->get(this->url_, headers, {ETAG_HEADER_NAME, LAST_MODIFIED_HEADER_NAME});
152
153 if (this->downloader_ == nullptr) {
154 ESP_LOGE(TAG, "Download failed.");
155 this->end_connection_();
156 this->download_error_callback_.call();
157 return;
158 }
159
160 int http_code = this->downloader_->status_code;
161 if (http_code == HTTP_CODE_NOT_MODIFIED) {
162 // Image hasn't changed on server. Skip download.
163 ESP_LOGI(TAG, "Server returned HTTP 304 (Not Modified). Download skipped.");
164 this->end_connection_();
165 this->download_finished_callback_.call(true);
166 return;
167 }
168 if (http_code != HTTP_CODE_OK) {
169 ESP_LOGE(TAG, "HTTP result: %d", http_code);
170 this->end_connection_();
171 this->download_error_callback_.call();
172 return;
173 }
174
175 ESP_LOGD(TAG, "Starting download");
176 size_t total_size = this->downloader_->content_length;
177
178#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
179 if (this->format_ == ImageFormat::BMP) {
180 ESP_LOGD(TAG, "Allocating BMP decoder");
181 this->decoder_ = make_unique<BmpDecoder>(this);
182 this->enable_loop();
183 }
184#endif // USE_ONLINE_IMAGE_BMP_SUPPORT
185#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
186 if (this->format_ == ImageFormat::JPEG) {
187 ESP_LOGD(TAG, "Allocating JPEG decoder");
188 this->decoder_ = esphome::make_unique<JpegDecoder>(this);
189 this->enable_loop();
190 }
191#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
192#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
193 if (this->format_ == ImageFormat::PNG) {
194 ESP_LOGD(TAG, "Allocating PNG decoder");
195 this->decoder_ = make_unique<PngDecoder>(this);
196 this->enable_loop();
197 }
198#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
199
200 if (!this->decoder_) {
201 ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported: %d", this->format_);
202 this->end_connection_();
203 this->download_error_callback_.call();
204 return;
205 }
206 auto prepare_result = this->decoder_->prepare(total_size);
207 if (prepare_result < 0) {
208 this->end_connection_();
209 this->download_error_callback_.call();
210 return;
211 }
212 ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
213 this->start_time_ = ::time(nullptr);
214}
215
217 if (!this->decoder_) {
218 // Not decoding at the moment => nothing to do.
219 this->disable_loop();
220 return;
221 }
222 if (!this->downloader_ || this->decoder_->is_finished()) {
223 this->data_start_ = buffer_;
224 this->width_ = buffer_width_;
225 this->height_ = buffer_height_;
226 ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
227 this->width_, this->height_);
228 ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_));
229 this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
230 this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME);
231 this->download_finished_callback_.call(false);
232 this->end_connection_();
233 return;
234 }
235 if (this->downloader_ == nullptr) {
236 ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
237 return;
238 }
239 size_t available = this->download_buffer_.free_capacity();
240 if (available) {
241 // Some decoders need to fully download the image before downloading.
242 // In case of huge images, don't wait blocking until the whole image has been downloaded,
243 // use smaller chunks
244 available = std::min(available, this->download_buffer_initial_size_);
245 auto len = this->downloader_->read(this->download_buffer_.append(), available);
246 if (len > 0) {
248 auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread());
249 if (fed < 0) {
250 ESP_LOGE(TAG, "Error when decoding image.");
251 this->end_connection_();
252 this->download_error_callback_.call();
253 return;
254 }
255 this->download_buffer_.read(fed);
256 }
257 }
258}
259
262 if (color.g == 1 && color.r == 0 && color.b == 0) {
263 color.g = 0;
264 }
265 if (color.w < 0x80) {
266 color.r = 0;
267 color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
268 color.b = 0;
269 }
270 }
271}
272
273void OnlineImage::draw_pixel_(int x, int y, Color color) {
274 if (!this->buffer_) {
275 ESP_LOGE(TAG, "Buffer not allocated!");
276 return;
277 }
278 if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
279 ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
280 return;
281 }
282 uint32_t pos = this->get_position_(x, y);
283 switch (this->type_) {
285 const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
286 pos = x + y * width_8;
287 auto bitno = 0x80 >> (pos % 8u);
288 pos /= 8u;
289 auto on = is_color_on(color);
290 if (this->has_transparency() && color.w < 0x80)
291 on = false;
292 if (on) {
293 this->buffer_[pos] |= bitno;
294 } else {
295 this->buffer_[pos] &= ~bitno;
296 }
297 break;
298 }
300 auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
302 if (gray == 1) {
303 gray = 0;
304 }
305 if (color.w < 0x80) {
306 gray = 1;
307 }
309 if (color.w != 0xFF)
310 gray = color.w;
311 }
312 this->buffer_[pos] = gray;
313 break;
314 }
316 this->map_chroma_key(color);
317 uint16_t col565 = display::ColorUtil::color_to_565(color);
318 if (this->is_big_endian_) {
319 this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
320 this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
321 } else {
322 this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF);
323 this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
324 }
326 this->buffer_[pos + 2] = color.w;
327 }
328 break;
329 }
331 this->map_chroma_key(color);
332 this->buffer_[pos + 0] = color.r;
333 this->buffer_[pos + 1] = color.g;
334 this->buffer_[pos + 2] = color.b;
336 this->buffer_[pos + 3] = color.w;
337 }
338 break;
339 }
340 }
341}
342
344 if (this->downloader_) {
345 this->downloader_->end();
346 this->downloader_ = nullptr;
347 }
348 this->decoder_.reset();
349 this->download_buffer_.reset();
350}
351
352bool OnlineImage::validate_url_(const std::string &url) {
353 if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) {
354 ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
355 return false;
356 }
357 return true;
358}
359
360void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) {
361 this->download_finished_callback_.add(std::move(callback));
362}
363
364void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
365 this->download_error_callback_.add(std::move(callback));
366}
367
368} // namespace online_image
369} // namespace esphome
void enable_loop()
Enable this component's loop.
void disable_loop()
Disable this component's loop.
esphome::http_request::HttpRequestComponent * parent_
Definition helpers.h:667
void deallocate(T *p, size_t n)
Definition helpers.h:876
size_t get_max_free_block_size() const
Return the maximum size block this allocator could allocate.
Definition helpers.h:904
T * allocate(size_t n)
Definition helpers.h:838
static uint16_t color_to_565(Color color, ColorOrder color_order=ColorOrder::COLOR_ORDER_RGB)
std::shared_ptr< HttpContainer > get(const std::string &url)
const uint8_t * data_start_
Definition image.h:55
bool has_transparency() const
Definition image.h:41
ImageType type_
Definition image.h:54
Transparency transparency_
Definition image.h:56
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override
Definition image.cpp:9
uint8_t * data(size_t offset=0)
std::string etag_
The value of the ETag HTTP header provided in the last response.
size_t resize_(int width, int height)
Resize the image buffer to the requested dimensions.
bool validate_url_(const std::string &url)
int get_position_(int x, int y) const
CallbackManager< void(bool)> download_finished_callback_
void draw_pixel_(int x, int y, Color color)
Draw a pixel into the buffer.
bool is_big_endian_
Whether the image is stored in big-endian format.
std::vector< std::pair< std::string, TemplatableValue< std::string > > > request_headers_
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)
Construct a new OnlineImage object.
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color)
void add_on_error_callback(std::function< void()> &&callback)
std::unique_ptr< ImageDecoder > decoder_
RAMAllocator< uint8_t > allocator_
void add_on_finished_callback(std::function< void(bool)> &&callback)
int buffer_width_
Actual width of the current image.
ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const
void set_url(const std::string &url)
Set the URL to download the image from.
std::shared_ptr< http_request::HttpContainer > downloader_
const int fixed_width_
width requested on configuration, or 0 if non specified.
CallbackManager< void()> download_error_callback_
const int fixed_height_
height requested on configuration, or 0 if non specified.
int buffer_height_
Actual height of the current image.
size_t download_buffer_initial_size_
This is the initial size of the download buffer, not the current size.
std::string last_modified_
The value of the Last-Modified HTTP header provided in the last response.
void release()
Release the buffer storing the image.
uint8_t type
@ TRANSPARENCY_ALPHA_CHANNEL
Definition image.h:22
@ TRANSPARENCY_CHROMA_KEY
Definition image.h:21
@ IMAGE_TYPE_GRAYSCALE
Definition image.h:14
@ IMAGE_TYPE_BINARY
Definition image.h:13
@ IMAGE_TYPE_RGB565
Definition image.h:16
@ IMAGE_TYPE_RGB
Definition image.h:15
bool is_color_on(const Color &color)
ImageFormat
Format that the image is encoded with.
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:279
uint8_t w
Definition color.h:33
uint8_t g
Definition color.h:25
uint8_t b
Definition color.h:29
uint8_t r
Definition color.h:21
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6