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