ESPHome 2026.8.0b4
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"
5#include <algorithm>
6#include <cstdint>
7#include <cstring>
8#include <limits>
9
10#ifdef USE_RUNTIME_IMAGE_BMP
11#include "bmp_decoder.h"
12#endif
13#ifdef USE_RUNTIME_IMAGE_JPEG
14#include "jpeg_decoder.h"
15#endif
16#ifdef USE_RUNTIME_IMAGE_PNG
17#include "png_decoder.h"
18#endif
19
20namespace esphome::runtime_image {
21
22static const char *const TAG = "runtime_image";
23
24// Widest supported format is 4 bytes/pixel, so 32767 * 32767 * 4 still fits a 32-bit size_t
25static constexpr int MAX_IMAGE_DIMENSION = 32767;
26static constexpr int MAX_IMAGE_BPP = 32;
27static_assert((static_cast<uint64_t>(MAX_IMAGE_BPP) * MAX_IMAGE_DIMENSION + 7) / 8 * MAX_IMAGE_DIMENSION <=
28 std::numeric_limits<size_t>::max(),
29 "MAX_IMAGE_DIMENSION must keep the worst-case buffer size within size_t");
30
31inline bool is_color_on(const Color &color) {
32 // This produces the most accurate monochrome conversion, but is slightly slower.
33 // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
34
35 // Approximation using fast integer computations; produces acceptable results
36 // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
37 return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
38}
39
41 image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
42 : Image(nullptr, 0, 0, type, transparency),
43 format_(format),
44 fixed_width_(fixed_width),
45 fixed_height_(fixed_height),
46 placeholder_(placeholder),
47 is_big_endian_(is_big_endian) {}
48
50
51int RuntimeImage::resize(int width, int height) {
52 // Use fixed dimensions if specified (0 means auto-resize)
53 int target_width = this->fixed_width_ ? this->fixed_width_ : width;
54 int target_height = this->fixed_height_ ? this->fixed_height_ : height;
55
56 // When both fixed dimensions are set, scale uniformly to preserve aspect ratio
57 if (this->fixed_width_ && this->fixed_height_ && width > 0 && height > 0) {
58 float scale =
59 std::min(static_cast<float>(this->fixed_width_) / width, static_cast<float>(this->fixed_height_) / height);
60 target_width = static_cast<int>(width * scale);
61 target_height = static_cast<int>(height * scale);
62 }
63
64 size_t result = this->resize_buffer_(target_width, target_height);
65 if (result > 0 && this->progressive_display_) {
66 // Update display dimensions for progressive display
67 this->width_ = this->buffer_width_;
68 this->height_ = this->buffer_height_;
69 this->data_start_ = this->buffer_;
70 }
71 return result;
72}
73
74void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
75 if (!this->buffer_) {
76 ESP_LOGE(TAG, "Buffer not allocated!");
77 return;
78 }
79 if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
80 ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
81 return;
82 }
83
84 switch (this->type_) {
86 const uint32_t width_8 = ((this->buffer_width_ + 7u) / 8u) * 8u;
87 uint32_t pos = x + y * width_8;
88 auto bitno = 0x80 >> (pos % 8u);
89 pos /= 8u;
90 auto on = is_color_on(color);
91 if (this->has_transparency() && color.w < 0x80)
92 on = false;
93 if (on) {
94 this->buffer_[pos] |= bitno;
95 } else {
96 this->buffer_[pos] &= ~bitno;
97 }
98 break;
99 }
101 uint32_t pos = this->get_position_(x, y);
102 auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
104 if (gray == 1) {
105 gray = 0;
106 }
107 if (color.w < 0x80) {
108 gray = 1;
109 }
111 if (color.w != 0xFF)
112 gray = color.w;
113 }
114 this->buffer_[pos] = gray;
115 break;
116 }
118 const size_t pos = (x + y * this->buffer_width_) * 2;
119 Color mapped_color = color;
120 this->map_chroma_key(mapped_color);
121 uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
122 if (this->is_big_endian_) {
123 this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
124 this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
125 } else {
126 this->buffer_[pos + 0] = static_cast<uint8_t>(rgb565 & 0xFF);
127 this->buffer_[pos + 1] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
128 }
130 const size_t alpha_pos = pos / 2 + this->buffer_width_ * this->buffer_height_ * 2;
131 this->buffer_[alpha_pos] = color.w;
132 }
133 break;
134 }
136 uint32_t pos = this->get_position_(x, y);
137 Color mapped_color = color;
138 this->map_chroma_key(mapped_color);
139 this->buffer_[pos + 0] = mapped_color.b;
140 this->buffer_[pos + 1] = mapped_color.g;
141 this->buffer_[pos + 2] = mapped_color.r;
143 this->buffer_[pos + 3] = color.w;
144 }
145 break;
146 }
147 }
148}
149
152 if (color.g == 1 && color.r == 0 && color.b == 0) {
153 color.g = 0;
154 }
155 if (color.w < 0x80) {
156 color.r = 0;
157 color.g = this->type_ == image::IMAGE_TYPE_RGB565 ? 4 : 1;
158 color.b = 0;
159 }
160 }
161}
162
163void RuntimeImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
164 if (this->data_start_) {
165 // If we have a complete image, use the base class draw method
166 Image::draw(x, y, display, color_on, color_off);
167 } else if (this->placeholder_) {
168 // Show placeholder while the runtime image is not available
169 this->placeholder_->draw(x, y, display, color_on, color_off);
170 }
171 // If no image is loaded and no placeholder, nothing to draw
172}
173
174bool RuntimeImage::begin_decode(size_t expected_size) {
175 if (this->decoder_) {
176 ESP_LOGW(TAG, "Decoding already in progress");
177 return false;
178 }
179
180 this->decoder_ = this->create_decoder_();
181 if (!this->decoder_) {
182 ESP_LOGE(TAG, "Failed to create decoder for format %d", this->format_);
183 return false;
184 }
185
186 this->total_size_ = expected_size;
187 this->decoded_bytes_ = 0;
188
189 // Initialize decoder
190 int result = this->decoder_->prepare(expected_size);
191 if (result < 0) {
192 ESP_LOGE(TAG, "Failed to prepare decoder: %d", result);
193 this->decoder_ = nullptr;
194 return false;
195 }
196
197 return true;
198}
199
200int RuntimeImage::feed_data(uint8_t *data, size_t len) {
201 if (!this->decoder_) {
202 ESP_LOGE(TAG, "No decoder initialized");
203 return -1;
204 }
205
206 int consumed = this->decoder_->decode(data, len);
207 if (consumed > 0) {
208 this->decoded_bytes_ += consumed;
209 }
210
211 return consumed;
212}
213
215 if (!this->decoder_) {
216 return false;
217 }
218
219 // Finalize the image for display
220 if (!this->progressive_display_) {
221 // Only now make the image visible
222 this->width_ = this->buffer_width_;
223 this->height_ = this->buffer_height_;
224 this->data_start_ = this->buffer_;
225 }
226
227 // Clean up decoder
228 this->decoder_ = nullptr;
229
230 ESP_LOGD(TAG, "Decoding complete: %dx%d, %zu bytes", this->width_, this->height_, this->decoded_bytes_);
231 return true;
232}
233
235 if (!this->decoder_) {
236 return false;
237 }
238 return this->decoder_->is_finished();
239}
240
242 this->release_buffer_();
243 // Reset decoder separately — release() can be called from within the decoder
244 // (via set_size -> resize -> resize_buffer_), so we must not destroy the decoder here.
245 // The decoder lifecycle is managed by begin_decode()/end_decode().
246 this->decoder_ = nullptr;
247}
248
250 if (this->buffer_) {
251 if (this->external_buffer_) {
252 // The caller owns this memory and goes on using it after the image lets go of it.
253 ESP_LOGV(TAG, "Letting go of the external %dx%d buffer", this->buffer_width_, this->buffer_height_);
254 this->external_buffer_ = false;
255 } else {
256 ESP_LOGV(TAG, "Releasing buffer of size %zu", this->get_buffer_size(this->buffer_width_, this->buffer_height_));
257 RAMAllocator<uint8_t> allocator;
258 allocator.deallocate(this->buffer_, this->get_buffer_size(this->buffer_width_, this->buffer_height_));
259 }
260 this->buffer_ = nullptr;
261 this->data_start_ = nullptr;
262 this->width_ = 0;
263 this->height_ = 0;
264 this->buffer_width_ = 0;
265 this->buffer_height_ = 0;
266#ifdef USE_LVGL
267 memset(&this->dsc_, 0, sizeof(this->dsc_));
268#endif
269 }
270}
271
272bool RuntimeImage::set_external_buffer(uint8_t *buffer, int width, int height) {
273 this->release_buffer_();
274 if (buffer == nullptr || this->get_buffer_size(width, height) == 0) {
275 // Keep the released state rather than remembering a buffer that cannot be decoded into: an
276 // external buffer that is never handed back would otherwise block every later allocation.
277 ESP_LOGE(TAG, "Refusing an invalid external buffer for %dx%d", width, height);
278 return false;
279 }
280 this->buffer_ = buffer;
281 this->external_buffer_ = true;
282 this->buffer_width_ = width;
283 this->buffer_height_ = height;
284 return true;
285}
286
287size_t RuntimeImage::resize_buffer_(int width, int height) {
288 size_t new_size = this->get_buffer_size(width, height);
289
290 // A buffer only ever exists with dimensions the image can decode at, so a match here means
291 // new_size is non-zero. Checking it before the invalid dimension case below lets the external
292 // buffer be let go of for every decode it cannot serve, not just for valid other dimensions.
293 if (this->buffer_ && this->buffer_width_ == width && this->buffer_height_ == height) {
294 // Buffer already allocated with correct size
295 return new_size;
296 }
297
298 if (this->external_buffer_) {
299 ESP_LOGE(TAG, "Image decoded to %dx%d, but the external buffer is %dx%d", width, height, this->buffer_width_,
300 this->buffer_height_);
301 // Let the buffer go rather than free memory that belongs to the caller. Dropping it also stops
302 // a decoder that ignores this failure from publishing a picture it never painted.
303 this->release_buffer_();
304 return 0;
305 }
306
307 if (new_size == 0) {
308 ESP_LOGE(TAG, "Refusing to allocate buffer for invalid image dimensions %dx%d", width, height);
309 return 0;
310 }
311
312 // Release old buffer if dimensions changed
313 if (this->buffer_) {
314 this->release_buffer_();
315 }
316
317 ESP_LOGD(TAG, "Allocating buffer: %dx%d, %zu bytes", width, height, new_size);
318 RAMAllocator<uint8_t> allocator;
319 this->buffer_ = allocator.allocate(new_size);
320
321 if (!this->buffer_) {
322 ESP_LOGE(TAG, "Failed to allocate %zu bytes. Largest free block: %zu", new_size,
323 allocator.get_max_free_block_size());
324 return 0;
325 }
326
327 // Clear buffer
328 memset(this->buffer_, 0, new_size);
329
330 this->buffer_width_ = width;
331 this->buffer_height_ = height;
332
333 return new_size;
334}
335
336size_t RuntimeImage::get_buffer_size(int width, int height) const {
337 // Dimensions come from a remote image header; reject absurd values so the size math cannot overflow
338 if (width <= 0 || height <= 0 || width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
339 return 0;
340 }
342 // Add extra alpha channel for RGB565 with alpha
343 return static_cast<size_t>(width) * height * 3;
344 }
345 return (static_cast<size_t>(this->get_bpp()) * width + 7u) / 8u * height;
346}
347
348int RuntimeImage::get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
349
350std::unique_ptr<ImageDecoder> RuntimeImage::create_decoder_() {
351 switch (this->format_) {
352#ifdef USE_RUNTIME_IMAGE_BMP
353 case BMP:
354 return make_unique<BmpDecoder>(this);
355#endif
356#ifdef USE_RUNTIME_IMAGE_JPEG
357 case JPEG:
358 return make_unique<JpegDecoder>(this);
359#endif
360#ifdef USE_RUNTIME_IMAGE_PNG
361 case PNG:
362 return make_unique<PngDecoder>(this);
363#endif
364 default:
365 ESP_LOGE(TAG, "Unsupported image format: %d", this->format_);
366 return nullptr;
367 }
368}
369
370} // namespace esphome::runtime_image
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2099
void deallocate(T *p, size_t n)
Definition helpers.h:2156
size_t get_max_free_block_size() const
Return the maximum size block this allocator could allocate.
Definition helpers.h:2184
T * allocate(size_t n)
Definition helpers.h:2126
static uint16_t color_to_565(Color color, ColorOrder color_order=ColorOrder::COLOR_ORDER_RGB)
const uint8_t * data_start_
Definition image.h:54
int get_bpp() const
Definition image.h:33
bool has_transparency() const
Definition image.h:40
ImageType type_
Definition image.h:53
ImageType get_type() const
Definition image.cpp:214
Transparency transparency_
Definition image.h:55
lv_img_dsc_t dsc_
Definition image.h:59
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override
Definition image.cpp:8
bool is_big_endian_
Whether the image is stored in big-endian format.
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 memory.
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_
std::unique_ptr< ImageDecoder > create_decoder_()
Create decoder instance for the image's format.
void draw_pixel(int x, int y, const Color &color)
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 begin_decode(size_t expected_size=0)
Begin decoding an image.
bool external_buffer_
Whether buffer_ belongs to the caller, so it must not be freed or resized here.
uint16_t type
@ TRANSPARENCY_ALPHA_CHANNEL
Definition image.h:21
@ TRANSPARENCY_CHROMA_KEY
Definition image.h:20
@ IMAGE_TYPE_GRAYSCALE
Definition image.h:13
@ IMAGE_TYPE_BINARY
Definition image.h:12
@ IMAGE_TYPE_RGB565
Definition image.h:15
@ IMAGE_TYPE_RGB
Definition image.h:14
bool is_color_on(const Color &color)
ImageFormat
Image format types that can be decoded dynamically.
const char int const __FlashStringHelper * format
Definition log.h:74
const void size_t len
Definition hal.h:64
size_t size_t pos
Definition helpers.h:1062
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