ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
runtime_image.cpp
Go to the documentation of this file.
1#include "runtime_image.h"
2#include "image_decoder.h"
3#include "esphome/core/log.h"
4#include <algorithm>
5#include <cstdint>
6#include <cstring>
7#include <limits>
8
9#ifdef USE_RUNTIME_IMAGE_BMP
10#include "bmp_decoder.h"
11#endif
12#ifdef USE_RUNTIME_IMAGE_JPEG
13#include "jpeg_decoder.h"
14#endif
15#ifdef USE_RUNTIME_IMAGE_PNG
16#include "png_decoder.h"
17#endif
18#ifdef USE_RUNTIME_IMAGE_QOI
19#include "qoi_decoder.h"
20#endif
21
22namespace esphome::runtime_image {
23
24ESPHOME_LOG_TAG(TAG, "runtime_image");
25
26// Widest supported format is 4 bytes/pixel, so 32767 * 32767 * 4 still fits a 32-bit size_t
27static constexpr int MAX_IMAGE_DIMENSION = 32767;
28static constexpr int MAX_IMAGE_BPP = 32;
29static_assert((static_cast<uint64_t>(MAX_IMAGE_BPP) * MAX_IMAGE_DIMENSION + 7) / 8 * MAX_IMAGE_DIMENSION <=
30 std::numeric_limits<size_t>::max(),
31 "MAX_IMAGE_DIMENSION must keep the worst-case buffer size within size_t");
32
33inline bool is_color_on(const Color &color) {
34 // This produces the most accurate monochrome conversion, but is slightly slower.
35 // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
36
37 // Approximation using fast integer computations; produces acceptable results
38 // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
39 return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
40}
41
43 image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
44 : Image(nullptr, 0, 0, type, transparency, is_big_endian),
45 format_(format),
46 fixed_width_(fixed_width),
47 fixed_height_(fixed_height),
48 placeholder_(placeholder) {}
49
51
52int RuntimeImage::resize(int width, int height) {
53 // Use fixed dimensions if specified (0 means auto-resize)
54 int target_width = this->fixed_width_ ? this->fixed_width_ : width;
55 int target_height = this->fixed_height_ ? this->fixed_height_ : height;
56
57 // When both fixed dimensions are set, scale uniformly to preserve aspect ratio
58 if (this->fixed_width_ && this->fixed_height_ && width > 0 && height > 0) {
59 float scale =
60 std::min(static_cast<float>(this->fixed_width_) / width, static_cast<float>(this->fixed_height_) / height);
61 target_width = static_cast<int>(width * scale);
62 target_height = static_cast<int>(height * scale);
63 }
64
65 size_t result = this->resize_buffer_(target_width, target_height);
66 if (result > 0 && this->progressive_display_) {
67 // Update display dimensions for progressive display
68 this->width_ = this->buffer_width_;
69 this->height_ = this->buffer_height_;
70 this->data_start_ = this->buffer_;
71 }
72 return result;
73}
74
75void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
76 if (!this->buffer_) {
77 ESP_LOGE(TAG, "Buffer not allocated!");
78 return;
79 }
80 if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
81 ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
82 return;
83 }
84
85 switch (this->type_) {
87 const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u;
88 uint32_t pos = x + y * width_8;
89 auto bitno = 0x80 >> (pos % 8u);
90 pos /= 8u;
91 auto on = is_color_on(color);
92 if (this->has_transparency() && color.w < 0x80)
93 on = false;
94 if (on) {
95 this->buffer_[pos] |= bitno;
96 } else {
97 this->buffer_[pos] &= ~bitno;
98 }
99 break;
100 }
102 uint8_t bits = (color.r >> 7) << 2 | (color.g >> 7) << 1 | (color.b >> 7);
103 if (this->bpp_ == 4)
104 bits = bits << 1 | (color.w >> 7);
105 const uint32_t stride = (this->buffer_width_ * this->bpp_ + 7u) / 8u;
106 const uint32_t bitpos = y * stride * 8 + x * this->bpp_;
107 uint8_t *byte = this->buffer_ + bitpos / 8;
108 // Place the pixel in a 16-bit window so it may straddle two bytes.
109 const uint8_t shift = 16 - this->bpp_ - bitpos % 8;
110 const uint16_t mask = ((1u << this->bpp_) - 1) << shift;
111 const uint16_t value = bits << shift;
112 byte[0] = (byte[0] & ~(mask >> 8)) | (value >> 8);
113 if (mask & 0xFF)
114 byte[1] = (byte[1] & ~(mask & 0xFF)) | (value & 0xFF);
115 break;
116 }
118 uint32_t pos = this->get_position_(x, y);
119 auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
121 if (gray == 1) {
122 gray = 0;
123 }
124 if (color.w < 0x80) {
125 gray = 1;
126 }
128 if (color.w != 0xFF)
129 gray = color.w;
130 }
131 this->buffer_[pos] = gray;
132 break;
133 }
135 const size_t pos = (x + y * this->buffer_width_) * 2;
136 Color mapped_color = color;
137 this->map_chroma_key(mapped_color);
138 uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
139 if (this->big_endian_) {
140 this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
141 this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
142 } else {
143 this->buffer_[pos + 0] = static_cast<uint8_t>(rgb565 & 0xFF);
144 this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
145 }
147 const size_t alpha_pos = pos / 2 + this->buffer_width_ * this->buffer_height_ * 2;
148 this->buffer_[alpha_pos] = color.w;
149 }
150 break;
151 }
153 uint32_t pos = this->get_position_(x, y);
154 Color mapped_color = color;
155 this->map_chroma_key(mapped_color);
156 this->buffer_[pos + 0] = mapped_color.b;
157 this->buffer_[pos + 1] = mapped_color.g;
158 this->buffer_[pos + 2] = mapped_color.r;
160 this->buffer_[pos + 3] = color.w;
161 }
162 break;
163 }
164 }
165}
166
169 if (color.g == 1 && color.r == 0 && color.b == 0) {
170 color.g = 0;
171 }
172 if (color.w < 0x80) {
173 color.r = 0;
174 color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1;
175 color.b = 0;
176 }
177 }
178}
179
180void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
181 if (this->data_start_) {
182 // If we have a complete image, use the base class draw method
183 Image::draw(x, y, display, color_on, color_off);
184 } else if (this->placeholder_) {
185 // Show placeholder while the runtime image is not available
186 this->placeholder_->draw(x, y, display, color_on, color_off);
187 }
188 // If no image is loaded and no placeholder, nothing to draw
189}
190
191bool RuntimeImage::begin_decode(size_t expected_size, ImageFormat format) {
192 if (this->is_decoding()) {
193 ESP_LOGW(TAG, "Decoding already in progress");
194 return false;
195 }
196
197 if (format == AUTO && this->format_ != AUTO) {
198 // Fall back to the configured format before the reuse check below
199 format = this->format_;
200 }
201
202 // An idle decoder for a different format cannot be reused
203 if (this->decoder_ != nullptr && this->decoder_->get_format() != format) {
204 ESP_LOGD(TAG, "Decoder format mismatch: current: %s, new: %s",
205 LOG_STR_ARG(get_format_name(this->decoder_->get_format())), LOG_STR_ARG(get_format_name(format)));
206 this->decoder_ = nullptr;
207 }
208
209 if (!this->decoder_) {
210 this->decoder_ = this->create_decoder_(format);
211 if (!this->decoder_) {
212 ESP_LOGE(TAG, "Failed to create decoder for format %s", LOG_STR_ARG(get_format_name(format)));
213 return false;
214 }
215 }
216
217 this->decoded_bytes_ = 0;
218
219 int result = this->decoder_->prepare(expected_size);
220 if (result < 0) {
221 ESP_LOGE(TAG, "Failed to prepare decoder: %d", result);
222 this->decoder_ = nullptr; // If prepare fails, a full reset is needed
223 return false;
224 }
225 return true;
226}
227
228int RuntimeImage::feed_data(uint8_t *data, size_t len) {
229 if (!this->is_decoding()) {
230 ESP_LOGE(TAG, "No decoder initialized");
231 return -1;
232 }
233
234 int consumed = this->decoder_->decode(data, len);
235 if (consumed > 0) {
236 this->decoded_bytes_ += consumed;
237 }
238
239 return consumed;
240}
241
243 if (!this->is_decoding()) {
244 return false;
245 }
246
247 // Finalize the image for display
248 if (!this->progressive_display_) {
249 // Only now make the image visible
250 this->width_ = this->buffer_width_;
251 this->height_ = this->buffer_height_;
252 this->data_start_ = this->buffer_;
253 }
254
255 // End the session; the decoder object stays warm so the next decode can
256 // reuse it (and its buffers) without churning the heap.
257 this->decoder_->reset();
258
259 ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_);
260 return true;
261}
262
263bool RuntimeImage::is_decode_finished() const { return this->is_decoding() && this->decoder_->is_finished(); }
264
266 this->release_buffer_();
267 // End any active decode session; decoders free the format-specific working buffers
268 // they can (PNG), while the decoder object itself is kept warm for the next decode.
269 if (this->decoder_) {
270 this->decoder_->reset();
271 }
272}
273
275 if (this->buffer_) {
276 if (this->external_buffer_) {
277 // The caller owns this memory and goes on using it after the image lets go of it.
278 ESP_LOGV(TAG, "Letting go of the external %dx%d buffer", this->buffer_width_, this->buffer_height_);
279 this->external_buffer_ = false;
280 } else {
281 ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size(this->buffer_width_, this->buffer_height_));
282 RAMAllocator<uint8_t> allocator;
283 allocator.deallocate(this->buffer_, this->get_buffer_size(this->buffer_width_, this->buffer_height_));
284 }
285 this->buffer_ = nullptr;
286 this->data_start_ = nullptr;
287 this->width_ = 0;
288 this->height_ = 0;
289 this->buffer_width_ = 0;
290 this->buffer_height_ = 0;
291#ifdef USE_LVGL
292 memset(&this->dsc_, 0, sizeof(this->dsc_));
293#endif
294 }
295}
296
297bool RuntimeImage::set_external_buffer(uint8_t *buffer, int width, int height) {
298 this->release_buffer_();
299 if (buffer == nullptr || this->get_buffer_size(width, height) == 0) {
300 // Keep the released state rather than remembering a buffer that cannot be decoded into: an
301 // external buffer that is never handed back would otherwise block every later allocation.
302 ESP_LOGE(TAG, "Refusing an invalid external buffer for %dx%d", width, height);
303 return false;
304 }
305 this->buffer_ = buffer;
306 this->external_buffer_ = true;
307 this->buffer_width_ = width;
308 this->buffer_height_ = height;
309 return true;
310}
311
312size_t RuntimeImage::resize_buffer_(int width, int height) {
313 size_t new_size = this->get_buffer_size(width, height);
314
315 // A buffer only ever exists with dimensions the image can decode at, so a match here means
316 // new_size is non-zero. Checking it before the invalid dimension case below lets the external
317 // buffer be let go of for every decode it cannot serve, not just for valid other dimensions.
318 if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) {
319 // Buffer already allocated with correct size
320 return new_size;
321 }
322
323 if (this->external_buffer_) {
324 ESP_LOGE(TAG, "Image decoded to %dx%d, but the external buffer is %dx%d", width, height, this->buffer_width_,
325 this->buffer_height_);
326 // Let the buffer go rather than free memory that belongs to the caller. Dropping it also stops
327 // a decoder that ignores this failure from publishing a picture it never painted.
328 this->release_buffer_();
329 return 0;
330 }
331
332 if (new_size == 0) {
333 ESP_LOGE(TAG, "Refusing to allocate buffer for invalid image dimensions %dx%d", width, height);
334 return 0;
335 }
336
337 // Release old buffer if dimensions changed
338 if (this->buffer_) {
339 this->release_buffer_();
340 }
341
342 ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size);
343 RAMAllocator<uint8_t> allocator;
344 this->buffer_ = allocator.allocate(new_size);
345
346 if (!this->buffer_) {
347 ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size,
348 allocator.get_max_free_block_size());
349 return 0;
350 }
351
352 // Clear buffer
353 memset(this->buffer_, 0, new_size);
354
355 this->buffer_width_ = width;
356 this->buffer_height_ = height;
357
358 return new_size;
359}
360
361size_t RuntimeImage::get_buffer_size(int width, int height) const {
362 // Dimensions come from a remote image header; reject absurd values so the size math cannot overflow
363 if (width <= 0 || height <= 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
364 return 0;
365 }
367 // Add extra alpha channel for RGB565 with alpha
368 return static_cast<size_t>(width) * height * 3;
369 }
370 return (static_cast<size_t>(this->get_bpp()) * width + 7u) / 8u * height;
371}
372
373int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
374
375std::unique_ptr<ImageDecoder> RuntimeImage::create_decoder_(ImageFormat format) {
376 ESP_LOGV(TAG, "Creating decoder for format %s", LOG_STR_ARG(get_format_name(format)));
377 switch (format) {
378#ifdef USE_RUNTIME_IMAGE_BMP
379 case BMP:
380 return make_unique<BmpDecoder>(this);
381#endif
382#ifdef USE_RUNTIME_IMAGE_JPEG
383 case JPEG:
384 return make_unique<JpegDecoder>(this);
385#endif
386#ifdef USE_RUNTIME_IMAGE_PNG
387 case PNG:
388 return make_unique<PngDecoder>(this);
389#endif
390#ifdef USE_RUNTIME_IMAGE_QOI
391 case QOI:
392 return make_unique<QoiDecoder>(this);
393#endif
394 case AUTO:
395 ESP_LOGE(TAG, "Image format could not be determined; set `format:` explicitly in the configuration");
396 return nullptr;
397 default:
398 ESP_LOGE(TAG, "Unsupported image format: %s", LOG_STR_ARG(get_format_name(format)));
399 return nullptr;
400 }
401}
402
403} // namespace esphome::runtime_image
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2222
void deallocate(T *p, size_t n)
Definition helpers.h:2279
size_t get_max_free_block_size() const
Return the maximum size block this allocator could allocate.
Definition helpers.h:2327
T * allocate(size_t n)
Definition helpers.h:2249
const uint8_t * data_start_
Definition image.h:62
int get_bpp() const
Definition image.h:36
bool has_transparency() const
Definition image.h:43
ImageType type_
Definition image.h:61
ImageType get_type() const
Definition image.cpp:293
Transparency transparency_
Definition image.h:63
lv_img_dsc_t dsc_
Definition image.h:67
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override
Definition image.cpp:30
bool begin_decode(size_t expected_size=0, ImageFormat format=AUTO)
Begin decoding an image.
bool is_decoding() const
Check if decoding is currently in progress.
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override
size_t get_buffer_size(int width, int height) const
Get the buffer size in bytes needed for a picture of the given dimensions.
int buffer_width_
Actual width of the current image.
bool end_decode()
Complete the decoding process.
int get_position_(int x, int y) const
Get the position in the buffer for a pixel.
const int fixed_height_
Fixed height requested on configuration, or 0 if not specified.
void release_buffer_()
Release only the image buffer without resetting the decoder.
int resize(int width, int height)
Resize the image buffer to the requested dimensions.
int feed_data(uint8_t *data, size_t len)
Feed data to the decoder.
const int fixed_width_
Fixed width requested on configuration, or 0 if not specified.
bool is_decode_finished() const
Check if the decoder has finished processing all data.
void release()
Release the image buffer and free its memory, ending any decode session.
image::Image * placeholder_
Placeholder image to show when the runtime image is not available.
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)
Construct a new RuntimeImage object.
const ImageFormat format_
The image format this RuntimeImage is configured to decode.
size_t resize_buffer_(int width, int height)
Resize the image buffer to the requested dimensions.
std::unique_ptr< ImageDecoder > decoder_
void draw_pixel(int x, int y, const Color &color)
std::unique_ptr< ImageDecoder > create_decoder_(ImageFormat format)
Create decoder instance for the requested format.
int buffer_height_
Actual height of the current image.
bool set_external_buffer(uint8_t *buffer, int width, int height)
Decode into a buffer the caller owns, instead of one allocated here.
bool external_buffer_
Whether buffer_ belongs to the caller, so it must not be freed or resized here.
uint8_t type
uint32_t buffer
Definition gif.cpp:50
const char * format
@ TRANSPARENCY_ALPHA_CHANNEL
Definition image.h:22
@ TRANSPARENCY_CHROMA_KEY
Definition image.h:21
@ IMAGE_TYPE_GRAYSCALE
Definition image.h:13
@ IMAGE_TYPE_BINARY
Definition image.h:12
@ IMAGE_TYPE_RGB565
Definition image.h:15
@ IMAGE_TYPE_RGB111
Definition image.h:16
@ IMAGE_TYPE_RGB
Definition image.h:14
bool is_color_on(const Color &color)
ESPHOME_LOG_TAG(TAG, "image_decoder.bmp")
ImageFormat
Image format types that can be decoded dynamically.
@ AUTO
Format is supplied per decode, e.g.
const LogString * get_format_name(ImageFormat format)
Human-readable name for a format; "UNKNOWN" if not recognized.
const void size_t len
Definition hal.h:68
size_t size_t pos
Definition helpers.h:1179
static void uint32_t
uint8_t w
Definition color.h:42
uint8_t g
Definition color.h:34
uint8_t b
Definition color.h:38
uint8_t r
Definition color.h:30
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6