Compare commits

..
Author SHA1 Message Date
clydeandClaude Opus 5.5 c909b09941 [runtime_image] Build a JPEG's drawing tables once, for the decoded size
set_size() built them for the full image size before start_() picked the scale and built them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 08:52:30 +11:00
clyde ff329537bf Merge branch 's-bmpqoi' into s-direct 2026-10-23 08:50:37 +11:00
clyde 115ba5ee6c Merge branch 's-png' into s-bmpqoi 2026-10-23 08:50:37 +11:00
clyde 305a1bf3fa Merge branch 's-gray' into s-png 2026-10-23 08:50:37 +11:00
clyde d00820fae7 Merge branch 's-draw' into s-gray 2026-10-23 08:50:37 +11:00
clyde e5a51a9521 Merge branch 's-bmpqoi' into s-direct 2026-10-23 08:23:14 +11:00
clyde cfbaa907bc Merge branch 's-png' into s-bmpqoi 2026-10-23 08:23:14 +11:00
clyde 679283c28f Merge branch 's-gray' into s-png 2026-10-23 08:23:13 +11:00
clyde 4dc829e091 Merge branch 's-draw' into s-gray 2026-10-23 08:23:13 +11:00
clyde 2e62c7a70b Merge branch 's-bmpqoi' into s-direct 2026-10-23 08:01:28 +11:00
clyde ff1a9dbb86 Merge branch 's-png' into s-bmpqoi 2026-10-23 08:01:27 +11:00
clyde 95fad9aaf5 Merge branch 's-gray' into s-png 2026-10-23 08:01:27 +11:00
clyde e31de889ef Merge branch 's-draw' into s-gray 2026-10-23 08:01:27 +11:00
clyde 8cd83ec51b Merge branch 's-bmpqoi' into s-direct 2026-10-23 07:25:22 +11:00
clyde e0f3eb8677 Merge branch 's-png' into s-bmpqoi 2026-10-23 07:25:21 +11:00
clyde e84efbb934 Merge branch 's-gray' into s-png
# Conflicts:
#	esphome/components/online_image/online_image.cpp
#	tests/components/online_image/test_download_end.cpp
2026-10-23 07:25:20 +11:00
clyde f7b2bb8c19 Merge branch 's-draw' into s-gray 2026-10-23 07:24:51 +11:00
clyde 066e95077a Merge branch 's-draw' into s-gray 2026-10-23 07:24:03 +11:00
clydeandClaude Opus 5.5 9ce82f98cc [runtime_image] Work out grayscale pixels with integer maths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:52:49 +10:00
clydeandClaude Opus 5.5 a03730dd26 [runtime_image] Write any opaque RGB565 image straight into the buffer
Move the JPEG decoder's boundary tables into ImageDecoder, so draw() writes opaque RGB565 pixels for BMP, PNG and
QOI directly, and draw each QOI run once per row it covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:07:11 +11:00
clyde e0fb0d06a9 Merge branch 'image-png-time-limit' into runtime-image-bmp-qoi-time-limit 2026-10-22 22:03:59 +11:00
clyde a453dd66c6 Merge branch 'runtime-image-gray-integer' into image-png-time-limit 2026-10-22 22:03:59 +11:00
clyde 52c4c0310d Merge branch 'runtime-image-integer-draw' into runtime-image-gray-integer 2026-10-22 22:03:59 +11:00
clyde 547dbf18d4 Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 22:03:58 +11:00
clyde ba3af07a23 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 22:03:58 +11:00
clyde 9c6ae9c615 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 22:03:58 +11:00
clyde de02ef8555 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 22:03:57 +11:00
clydeandClaude Opus 5.5 38b23152f4 [runtime_image] Put the is_finished() doc back above is_finished()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:03:51 +11:00
clydeandClaude Opus 5.5 e9c6d4341b [runtime_image] Limit how long one BMP or QOI decode call runs
Count the bytes QOI uses before a chunk split across two calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 22:00:06 +11:00
clyde 0bc55bcdc6 Merge branch 'runtime-image-gray-integer' into image-png-time-limit
# Conflicts:
#	tests/components/online_image/test_download_end.cpp
2026-10-22 21:57:14 +11:00
clydeandClaude Opus 5.5 0849e4b412 [runtime_image] Work out grayscale pixels with integer maths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:56:41 +11:00
clyde 375e8bc39b Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 21:44:49 +11:00
clyde 16a91ab760 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 21:44:44 +11:00
clydeandClaude Opus 5.5 2456e8d686 [online_image] Use emplace_back in the download started test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:44:39 +11:00
clyde 0c8e1c3f1c Merge branch 'runtime-image-jpeg-scale' into runtime-image-integer-draw 2026-10-22 21:35:06 +11:00
clyde a3c575d643 Merge branch 'online-image-progress' into runtime-image-jpeg-scale 2026-10-22 21:31:12 +11:00
clydeandClaude Opus 5.5 0573c8b568 [online_image] Add on_download_started
Fires when the server has accepted the request and the image starts to
download, before the first on_download_progress; not for a 304 or an
HTTP error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:24:05 +11:00
clyde d01205fa62 Merge branch 'runtime-image-jpeg-steps' into runtime-image-jpeg-scale 2026-10-22 21:17:21 +11:00
clyde de7ea89d0b Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 21:17:19 +11:00
clyde 0077f68158 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 21:17:18 +11:00
clydeandClaude Opus 5.5 7458f8c7d8 [runtime_image] Pin JPEGDEC to 1.8.4-esphome.3, built with -O2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:17:13 +11:00
clyde 0e5e0a8ff6 Merge remote-tracking branch 'upstream/dev' into runtime-image-jpeg-steps 2026-10-22 21:15:57 +11:00
clydeandClaude Opus 5.5 752c230405 [runtime_image] Check once per draw() call whether the image is scaled
An image drawn at its own size needs no span maths at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:14:33 +11:00
clydeandClaude Opus 5.5 cfc92657a2 [runtime_image] Say the generic draw() path keeps the same pixels as the RGB565 path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:51:14 +11:00
clydeandClaude Opus 5.5 32f32f144d [runtime_image] Skip the span division for an image drawn at its own size
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:44:48 +11:00
clydeandClaude Opus 5.5 9b60e7cff2 [runtime_image] Decode a JPEG at the smallest JPEGDEC scale that covers the buffer
JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work, but
every JPEG was decoded at full size and then shrunk to the buffer. A
3596 x 2840 image shown at 281 x 222 took 7.1 s on an ESP32-S3; at
1/8 size it takes 2.0 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:19:44 +11:00
clydeandClaude Opus 5.5 02b8db1f3f [runtime_image] Draw each target pixel once, with integer maths
ImageDecoder::draw() painted every source pixel into the buffer, so a
shrunk image wrote each target pixel many times, and worked out the
spans with double multiplications and ceil() for every pixel. The S3
has no double FPU, so drawing took about 6 us a pixel: 3.7 s of a 1000
x 617 PNG's 6 s. Give each source pixel the span from ceil(start *
scale) to ceil(end * scale), worked out with integers, so neighbouring
spans meet without overlapping. The PNG now loads in 3.2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:17:27 +11:00
clyde f860c8a8d9 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 20:13:39 +11:00
clyde bb7a060138 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 20:13:38 +11:00
clydeandClaude Opus 5.5 84839507bd [runtime_image] Pin JPEGDEC to 1.8.4-esphome.2, with the ESP32-S3 SIMD under ESP-IDF
Full-size JPEG decodes are 12-19% faster on an ESP32-S3 with ESP-IDF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:13:33 +11:00
clydeandClaude Opus 5.5 1de1794574 [runtime_image] Let one PNG decode call run for up to 15 ms
Closer to the 16 ms loop time, so a large PNG needs fewer passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:10:55 +11:00
clydeandClaude Opus 5.5 84638cf2b6 [runtime_image] Use longer names in the PNG time limit test for clang-tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:59:50 +11:00
clydeandClaude Opus 5.5 4df1b53d85 [runtime_image] Feed pngle 64 bytes at a time
Flat areas of an image compress so well that 256 bytes could still
hold 160 ms of drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:54:28 +11:00
clydeandClaude Opus 5.5 bac443840c [runtime_image] Limit how long one PNG decode call runs
A few KB of compressed PNG data can hold many rows of pixels, so one
feed_data() call could hold up the main loop for hundreds of
milliseconds. Feed pngle 256 bytes at a time and return after 10 ms,
leaving the rest for the next call.

online_image now passes data the decoder has not used to it again
when nothing new arrives, instead of treating it as data the decoder
could not use once the download ends.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:48:22 +11:00
clyde ef54eb14d8 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 16:29:02 +11:00
clydeandClaude Opus 5.5 60aa3e24f1 [online_image] Brace the whole-file fallback for clang-tidy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:29:01 +11:00
clyde 63566999bd Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 16:28:45 +11:00
clydeandClaude Opus 5.5 1728d3326b [runtime_image] Log a failed open from a source at debug level, as the caller may recover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:28:44 +11:00
clyde 2b99c56754 Merge branch 'online-image-jpeg-stream' into online-image-progress 2026-10-22 16:21:09 +11:00
clydeandClaude Opus 5.5 d79cd9a7eb [online_image] Download the whole JPEG when its headers do not fit the buffer
Before JPEG images were decoded as they download, the buffer grew to
hold the whole file, so an image with large EXIF or ICC data decoded
with any buffer_size. Keep that working: when the headers do not fit,
grow the buffer to the file size, keeping what has arrived, and decode
it at the end as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:21:08 +11:00
clyde fa18994fab Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 16:19:20 +11:00
clydeandClaude Opus 5.5 1090706216 [runtime_image] Count the bytes of a stepped decode, and say when begin_source() can be called
Report any failed read of the source as DECODE_ERROR_READ from one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:19:01 +11:00
clydeandClaude Opus 5.5 ced497995d [online_image] Add on_download_progress, to show how far a download has got
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:35:37 +11:00
clydeandClaude Opus 5.5 228c553f60 [online_image] Also give the buffer_size hint when unreadable headers end in a read error
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:34:15 +11:00
clydeandClaude Opus 5.5 563e197c6d [online_image] Open a streamed JPEG in a pass of its own, and only blame the buffer for unreadable headers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:29:07 +11:00
clyde 9d07f1f400 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 14:27:47 +11:00
clydeandClaude Opus 5.5 846fc70bdf [runtime_image] Report a progressive JPEG as an unsupported format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:27:46 +11:00
clyde 74212d8d04 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:41:53 +11:00
clydeandClaude Opus 5.5 101c9cd840 [runtime_image] Take JPEGDEC from esphome-libs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:41:48 +11:00
clyde 8152dcf224 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:37:13 +11:00
clydeandClaude Opus 5.5 f2d50b4aa7 [runtime_image] Drop the trailing underscore from the static JPEGDEC callbacks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:37:13 +11:00
clydeandClaude Opus 5.5 f75b3d6ded [online_image] Test decode and read errors part way through a streamed JPEG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:17:00 +11:00
clyde 3cf29bf4fb Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:14:54 +11:00
clydeandClaude Opus 5.5 f752e3b150 [runtime_image] Report a failed seek to the start as a read error
Also limit the JPEGDEC pin comment to the fixes ESPHome uses: it never
crops, and refuses progressive images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:14:51 +11:00
clyde a5bc7070d1 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 13:06:41 +11:00
clydeandClaude Opus 5.5 b2eebfb010 [runtime_image] Pin JPEGDEC to the 1.8.4-esphome.1 release
Adds the upstream fix that skips building AC Huffman tables for
progressive images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:06:37 +11:00
clyde a596d1d3c8 Merge branch 'runtime-image-jpeg-steps' into online-image-jpeg-stream 2026-10-22 09:09:13 +11:00
clydeandClaude Opus 5.5 6701aedae2 [runtime_image] Pin JPEGDEC with the grayscale, crop and progressive fixes
A grayscale JPEG decoded for any image type but direct RGB565 came out garbled:
JPEGDEC wrote RGB565 words where it was asked for RGB8888. The new pin fixes
that, along with cropping and a crash on progressive images with skipped blocks,
which ESPHome does not use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 09:09:09 +11:00
clydeandClaude Opus 5.5 6fddbd1d48 [online_image] Decode a JPEG as it downloads
A JPEG had to be downloaded whole, into a buffer grown to the file size, and was
then decoded in one call. When the server gives the size, the JPEG decoder now
reads the download from the buffer itself and decodes a row of MCUs at a time,
within a time budget per loop pass. The buffer only has to hold the headers, so
buffer_size bounds the RAM used, and a slow link makes the decoder wait for the
rest rather than decode past what has arrived. Downloads without a size keep the
old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:23:28 +11:00
clydeandClaude Opus 5.5 03aaa00d75 [runtime_image] Refuse a source decode without a usable image size
JPEGDEC needs the file size when it opens a source, and holds it in an int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:21:14 +11:00
clydeandClaude Opus 5.5 b612eb2929 [runtime_image] Let a source-reading decoder wait for data that has not arrived
A source that is still arriving, such as a download, may have nothing to give
when the decoder asks for more. The pinned JPEGDEC now pauses before an MCU when
less input is buffered than it needs and the source has no more yet, and
decode_step() reports that as DECODE_STEP_NEED_DATA; the next call carries on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 07:04:54 +11:00
clydeandClaude Opus 5.5 63c041a95b [runtime_image] Pin JPEGDEC with the ARM NEON clipped MCU fix
Upstream's fix for the NEON path writing past the end of a row on clipped MCUs,
which overflows a framebuffer whose width is not a multiple of 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:36:44 +11:00
clydeandClaude Opus 5.5 f8d5096093 [runtime_image] Pin JPEGDEC with the ARM NEON colour order fix
The NEON 4:2:0 RGB8888 path swapped red and blue (JPEGDEC PR 121), which shows on
host builds on 64-bit ARM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:26:39 +11:00
clydeandClaude Opus 5.5 f835600fa6 [runtime_image] Let a decoder read its input itself and decode JPEG a row at a time
JPEGDEC decodes a whole image in one call, from a copy of the whole file in RAM.
A JPEGDEC that can stop between rows (1.8.4 plus decodeBegin() and decodeRows(),
pinned from a fork for now) lets the JPEG decoder read a DecodeSource itself,
through JPEGDEC's own 2 KB buffer, and decode one row of MCUs per decode_step().
A component that can read its input on demand, such as a file, no longer needs a
buffer for the whole file or a main loop pass long enough to decode the image.

Decoders that cannot read a source say so, and the fed decode() path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:08:01 +11:00
14 changed files with 758 additions and 93 deletions
@@ -231,30 +231,47 @@ void OnlineImage::loop() {
this->end_connection_();
this->download_error_callback_.call();
return;
} else if (this->downloader_->is_read_complete()) {
// The response has ended, so data left in the buffer can never be completed
size_t unread = this->download_buffer_.unread();
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
if (this->is_decode_finished() || !this->decoder_knows_end()) {
this->finish_download_();
} else {
// Nothing new arrived. The decoder works for a limited time per call, so it may still have data to use.
if (this->download_buffer_.unread() != 0) {
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
if (consumed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", esphome::runtime_image::decode_error_to_string(consumed));
this->end_connection_();
this->download_error_callback_.call();
return;
}
if (consumed > 0) {
this->download_buffer_.read(consumed);
this->last_data_time_ = App.get_loop_component_start_time();
return;
}
ESP_LOGE(TAG, "Download ended before the image was complete");
} else if (unread == 0) {
ESP_LOGE(TAG, "Server sent an empty response");
} else {
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
}
this->end_connection_();
this->download_error_callback_.call();
return;
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
// ESP-IDF keeps returning 0 after the server closes the connection early
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->end_connection_();
this->download_error_callback_.call();
return;
if (this->downloader_->is_read_complete()) {
// The response has ended, so data left in the buffer can never be completed
size_t unread = this->download_buffer_.unread();
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
if (this->is_decode_finished() || !this->decoder_knows_end()) {
this->finish_download_();
return;
}
ESP_LOGE(TAG, "Download ended before the image was complete");
} else if (unread == 0) {
ESP_LOGE(TAG, "Server sent an empty response");
} else {
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
}
this->end_connection_();
this->download_error_callback_.call();
return;
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
// ESP-IDF keeps returning 0 after the server closes the connection early
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->end_connection_();
this->download_error_callback_.call();
return;
}
}
} else {
// Buffer is full, need to decode some data first
@@ -3,6 +3,7 @@
#ifdef USE_RUNTIME_IMAGE_BMP
#include "esphome/components/display/display.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -142,6 +143,12 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
this->current_index_ = this->data_offset_;
index = this->data_offset_;
}
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
const uint32_t start = millis();
uint32_t steps = 0;
auto out_of_time = [start, &steps]() {
return ++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS;
};
switch (this->bits_per_pixel_) {
case 1: {
size_t width = static_cast<size_t>(this->width_);
@@ -152,7 +159,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
uint8_t pixels_in_byte = std::min<size_t>(remaining_in_row, 8);
bool end_of_row = remaining_in_row <= 8;
size_t needed = 1 + (end_of_row ? this->padding_bytes_ : 0);
if (index + needed > size) {
if (index + needed > size || out_of_time()) {
this->decoded_bytes_ += index;
return index;
}
@@ -179,7 +186,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
size_t x = this->paint_index_ % width;
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
size_t needed = 1 + ((x == last_col) ? this->padding_bytes_ : 0);
if (index + needed > size) {
if (index + needed > size || out_of_time()) {
this->decoded_bytes_ += index;
return index;
}
@@ -212,7 +219,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
size_t x = this->paint_index_ % width;
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
size_t needed = 3 + ((x == last_col) ? this->padding_bytes_ : 0);
if (index + needed > size) {
if (index + needed > size || out_of_time()) {
this->decoded_bytes_ += index;
return index;
}
@@ -1,5 +1,6 @@
#include "image_decoder.h"
#include "runtime_image.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include <algorithm>
@@ -8,11 +9,65 @@ namespace esphome::runtime_image {
ESPHOME_LOG_TAG(TAG, "image_decoder");
bool ImageDecoder::set_size(int width, int height) {
bool success = this->image_->resize(width, height) > 0;
this->size_valid_ = success;
return this->resize_image_(width, height) && this->prepare_draw_(width, height);
}
bool ImageDecoder::resize_image_(int width, int height) {
this->size_valid_ = this->image_->resize(width, height) > 0;
return this->size_valid_;
}
bool ImageDecoder::prepare_draw_(int width, int height) {
this->draw_width_ = width;
this->draw_height_ = height;
return success;
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
if (!this->direct_rgb565_)
return true;
if ((this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) ||
(this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1))) {
this->direct_rgb565_ = false;
this->size_valid_ = false;
return false;
}
this->x_boundaries_.clear();
this->y_boundaries_.clear();
const uint32_t buffer_width = this->image_->get_buffer_width();
const uint32_t buffer_height = this->image_->get_buffer_height();
// The same rounding as scaled_x_() and scaled_y_(), so both paths paint the same pixels
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
this->x_boundaries_.push_back(static_cast<uint16_t>((x * buffer_width + width - 1) / width));
}
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
this->y_boundaries_.push_back(static_cast<uint16_t>((y * buffer_height + height - 1) / height));
}
return true;
}
void ImageDecoder::draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565) {
const int x_end = std::min(x + w, this->draw_width_);
const int y_end = std::min(y + h, this->draw_height_);
x = std::max(x, 0);
y = std::max(y, 0);
if (x >= x_end || y >= y_end)
return;
// A block of source pixels lands on one block of the buffer
const uint16_t x1 = this->x_boundaries_[x];
const uint16_t x2 = this->x_boundaries_[x_end];
const uint16_t y1 = this->y_boundaries_[y];
const uint16_t y2 = this->y_boundaries_[y_end];
const bool big_endian = this->image_is_big_endian_();
const uint8_t first = big_endian ? rgb565 >> 8 : rgb565 & 0xFF;
const uint8_t second = big_endian ? rgb565 & 0xFF : rgb565 >> 8;
uint8_t *const buffer = this->image_buffer_();
const uint32_t buffer_width = this->image_->get_buffer_width();
for (uint32_t row = y1; row < y2; row++) {
uint8_t *destination = buffer + (row * buffer_width + x1) * 2;
for (uint32_t col = x1; col < x2; col++) {
destination[0] = first;
destination[1] = second;
destination += 2;
}
}
}
int ImageDecoder::scaled_x_(int x) const {
@@ -33,6 +88,10 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
if (!this->size_valid_) {
return;
}
if (this->direct_rgb565_) {
this->draw_rgb565_(x, y, w, h, display::ColorUtil::color_to_565(color));
return;
}
// A source pixel paints the target pixels from ceil(start * scale) up to ceil(end * scale). The spans of neighbouring
// pixels meet without overlapping, so when an image is shrunk each target pixel is painted once, by the first
// source pixel that reaches it, rather than by every pixel that lands on it. Integer maths: the S3 has no double FPU.
@@ -2,6 +2,7 @@
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
#include "image_format.h"
namespace esphome::runtime_image {
@@ -38,6 +39,11 @@ constexpr const char *decode_error_to_string(int error) {
}
}
// The longest one decode() call works before it returns, leaving the rest of the data for the next call
static constexpr uint32_t DECODE_TIME_LIMIT_MS = 15;
// Pixel loops read the clock once per this many steps
static constexpr uint32_t DECODE_TIME_CHECK_STEPS = 64;
class RuntimeImage;
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
@@ -85,6 +91,7 @@ class ImageDecoder {
this->size_valid_ = true;
this->draw_width_ = 0;
this->draw_height_ = 0;
this->direct_rgb565_ = false;
}
/**
@@ -179,7 +186,20 @@ class ImageDecoder {
/// Whether is_finished() can tell a complete image from a cut-off one
virtual bool knows_end() const { return this->expected_size_ > 0; }
/// The image is opaque RGB565, so draw() writes it straight into the buffer
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
protected:
/// Resize the image for a width x height source, without preparing to draw. prepare_draw_() must follow.
bool resize_image_(int width, int height);
/**
* Draw a source of width x height over the whole buffer from now on. For an opaque RGB565 image this builds the
* tables of where each source column and row lands. Returns false when they cannot be allocated.
*/
bool prepare_draw_(int width, int height);
/// Paint a block of source pixels in one color straight into the buffer. Only while uses_direct_rgb565().
void draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565);
/// The image's pixel buffer, for decoders that write their output straight into it
uint8_t *image_buffer_() const;
bool image_is_big_endian_() const;
@@ -191,12 +211,16 @@ class ImageDecoder {
// Size of the image as the decoder draws it, which draw() spreads over the buffer
int draw_width_ = 0;
int draw_height_ = 0;
// Boundary n is the first buffer column, or row, that source column, or row, n paints
FixedVector<uint16_t> x_boundaries_{};
FixedVector<uint16_t> y_boundaries_{};
RuntimeImage *image_;
size_t expected_size_ = 0; // Expected data size (0 if unknown)
size_t decoded_bytes_ = 0; // Bytes processed so far
const ImageFormat format_;
bool active_ = false; // A decoding session is in progress
bool size_valid_ = true; // Last set_size() result; draw() no-ops while false
bool direct_rgb565_ = false;
};
} // namespace esphome::runtime_image
@@ -16,32 +16,6 @@ ESPHOME_LOG_TAG(TAG, "image_decoder.jpeg");
namespace esphome::runtime_image {
bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
if (this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) {
return false;
}
if (this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1)) {
return false;
}
this->source_width_ = width;
this->source_height_ = height;
this->x_boundaries_.clear();
this->y_boundaries_.clear();
const uint32_t target_width = this->image_->get_buffer_width();
const uint32_t target_height = this->image_->get_buffer_height();
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6,
// as the generic draw() path does
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
}
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
}
return true;
}
void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels) {
if (pixels == nullptr) {
return;
@@ -52,14 +26,14 @@ void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels)
const uint8_t first = this->image_is_big_endian_() ? 1 : 0;
for (int source_y = 0; source_y < h; source_y++) {
const int absolute_y = y + source_y;
if (absolute_y < 0 || absolute_y >= this->source_height_) {
if (absolute_y < 0 || absolute_y >= this->draw_height_) {
continue;
}
const uint16_t y1 = this->y_boundaries_[absolute_y];
const uint16_t y2 = this->y_boundaries_[absolute_y + 1];
for (int source_x = 0; source_x < w; source_x++) {
const int absolute_x = x + source_x;
if (absolute_x < 0 || absolute_x >= this->source_width_) {
if (absolute_x < 0 || absolute_x >= this->draw_width_) {
continue;
}
const uint16_t x1 = this->x_boundaries_[absolute_x];
@@ -157,17 +131,10 @@ int JpegDecoder::start_() {
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
this->jpeg_.setUserPointer(this);
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
if (this->direct_rgb565_) {
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
} else {
this->jpeg_.setPixelType(RGB8888);
}
const int width = this->jpeg_.getWidth();
const int height = this->jpeg_.getHeight();
if (!this->set_size(width, height)) {
// The tables for drawing are only built once the decoded size is known, below
if (!this->resize_image_(width, height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
@@ -181,14 +148,19 @@ int JpegDecoder::start_() {
}
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
this->draw_width_ = decoded_width;
this->draw_height_ = decoded_height;
if (this->scale_ != 1) {
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
}
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
if (!this->prepare_draw_(decoded_width, decoded_height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (this->direct_rgb565_) {
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
} else {
this->jpeg_.setPixelType(RGB8888);
}
return 0;
}
@@ -30,13 +30,11 @@ class JpegDecoder : public ImageDecoder {
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
int HOT decode_step() override;
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
int get_scale() const { return this->scale_; }
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
protected:
bool prepare_rgb565_scaling_(int width, int height);
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
int start_();
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
@@ -49,13 +47,8 @@ class JpegDecoder : public ImageDecoder {
static void source_close(void *handle) {}
JPEGDEC jpeg_{};
FixedVector<uint16_t> x_boundaries_{};
FixedVector<uint16_t> y_boundaries_{};
int source_width_{0};
int source_height_{0};
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
bool direct_rgb565_{false};
bool source_failed_{false};
};
@@ -6,10 +6,15 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
ESPHOME_LOG_TAG(TAG, "image_decoder.png");
namespace esphome::runtime_image {
// Compressed bytes handed to pngle at a time
static constexpr size_t FEED_PIECE_SIZE = 64;
/**
* @brief Callback method that will be called by the PNGLE engine when the basic
* data of the image is received (i.e. width and height);
@@ -93,16 +98,25 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
ESP_LOGD(TAG, "Waiting for more data");
return 0;
}
auto fed = pngle_feed(this->pngle_, buffer, size);
if (fed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
} else if (!this->size_valid_) {
return DECODE_ERROR_OUT_OF_MEMORY;
} else {
// A few compressed bytes can hold many rows of pixels, so feed small pieces and stop once the call has run for
// DECODE_TIME_LIMIT_MS; the caller passes the rest again later
const uint32_t start = millis();
size_t total = 0;
while (total < size) {
const size_t piece = std::min(FEED_PIECE_SIZE, size - total);
const int fed = pngle_feed(this->pngle_, buffer + total, piece);
if (fed < 0) {
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
}
if (!this->size_valid_)
return DECODE_ERROR_OUT_OF_MEMORY;
this->decoded_bytes_ += fed;
total += fed;
if (static_cast<size_t>(fed) < piece || millis() - start >= DECODE_TIME_LIMIT_MS)
break;
}
return fed;
return total;
}
} // namespace esphome::runtime_image
@@ -3,9 +3,11 @@
#ifdef USE_RUNTIME_IMAGE_QOI
#include "esphome/components/display/display.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cinttypes>
namespace esphome::runtime_image {
@@ -102,12 +104,17 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
Color color;
const size_t total_pixels = this->width_ * this->height_;
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
const uint32_t start = millis();
uint32_t steps = 0;
while (index < size && this->paint_index_ < total_pixels) {
if (++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS)
break;
color = this->last_pixel_;
uint8_t byte = buffer[index];
if (byte == QOI_OP_RGB) {
if (size < index + QOI_RGB_CHUNK_SIZE) {
return index; // Need more data for RGB chunk
break; // Need more data for RGB chunk
}
index++;
color.r = buffer[index++];
@@ -117,7 +124,7 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
this->paint_index_++;
} else if (byte == QOI_OP_RGBA) {
if (size < index + QOI_RGBA_CHUNK_SIZE) {
return index; // Need more data for RGBA chunk
break; // Need more data for RGBA chunk
}
index++;
color.r = buffer[index++];
@@ -129,15 +136,18 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
} else if ((byte & QOI_MASK_OP) == QOI_OP_RUN) {
// QOI run chunk
size_t run_length = (byte & QOI_MASK_VALUE) + 1; // run length is encoded in the lower 6 bits, plus one
for (size_t i = 0; i < run_length; i++) {
// TODO: optimize by drawing runs of pixels at once instead of one by one
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
this->paint_index_++;
// Draw the part of the run in each row it covers at once
while (run_length > 0) {
const size_t x = this->paint_index_ % this->width_;
const size_t count = std::min(run_length, this->width_ - x);
this->draw(x, this->paint_index_ / this->width_, count, 1, color);
this->paint_index_ += count;
run_length -= count;
}
index++;
} else if ((byte & QOI_MASK_OP) == QOI_OP_LUMA) {
if (size < index + QOI_LUMA_CHUNK_SIZE) {
return index; // Need more data for LUMA chunk
break; // Need more data for LUMA chunk
}
index++;
uint8_t byte2 = buffer[index++];
@@ -165,8 +175,10 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
this->last_pixel_ = color;
this->color_table_[qoi_color_table_index(color)] = color;
}
this->decoded_bytes_ += size;
return size;
if (this->paint_index_ >= total_pixels)
index = size; // All that follows the last pixel is the end marker
this->decoded_bytes_ += index;
return index;
}
} // namespace esphome::runtime_image
@@ -116,7 +116,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
}
case image::IMAGE_TYPE_GRAYSCALE: {
uint32_t pos = this->get_position_(x, y);
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
// 0.2125, 0.7154 and 0.0721 in 1/256ths, summing to 256 so white stays 255. Integer maths: the S3 has no
// double FPU
auto gray = static_cast<uint8_t>((54 * color.r + 183 * color.g + 19 * color.b) >> 8);
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) {
gray = 0;
@@ -135,6 +135,132 @@ static constexpr std::array<uint8_t, 69> PNG_1X1{
0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9,
0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
// 800x600 RGB PNG of a single color, 2312 bytes: the decoder needs several calls to draw it
static const uint8_t PNG_SOLID[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};
// 8x8 progressive JPEG, 521 bytes
static const uint8_t JPEG_PROGRESSIVE[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
@@ -458,6 +584,29 @@ TEST_F(OnlineImageDownloadEnd, JpegBufferAllocationFailureReportsError) {
EXPECT_EQ(this->finished_, 0);
}
// The whole PNG arrives in one read, but the decoder stops after a short time per call: the rest is decoded on
// later passes, not reported as data the decoder could not use
TEST_F(OnlineImageDownloadEnd, PngDecodedOverSeveralPassesAfterTheDownloadEnds) {
for (bool chunked : {false, true}) {
this->finished_ = 0;
this->errors_ = 0;
auto image = this->make_image_(runtime_image::PNG, 4096);
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID)),
sizeof(PNG_SOLID), chunked);
this->start_(*image, container);
int passes = 0;
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++)
image->loop();
EXPECT_EQ(this->finished_, 1) << (chunked ? "chunked" : "with Content-Length");
EXPECT_EQ(this->errors_, 0);
EXPECT_GT(passes, 2);
EXPECT_EQ(image->get_width(), 800);
EXPECT_EQ(image->get_pixel(400, 300).b, 200);
}
}
// The JPEG decoded with all of it in memory at once, to compare the downloaded images with
static std::vector<uint8_t> reference_jpeg_pixels() {
runtime_image::RuntimeImage ref(runtime_image::JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
@@ -0,0 +1,125 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <utility>
#include <vector>
#include "esphome/components/runtime_image/qoi_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
namespace esphome::runtime_image::testing {
// Big enough that decoding it takes well over DECODE_TIME_LIMIT_MS, little enough to build quickly
static constexpr uint32_t WIDTH = 4096;
static constexpr uint32_t HEIGHT = 2048;
static void put_le32(std::vector<uint8_t> &data, size_t offset, uint32_t value) {
for (size_t i = 0; i < 4; i++)
data[offset + i] = (value >> (8 * i)) & 0xFF;
}
static void put_be32(std::vector<uint8_t> &data, size_t offset, uint32_t value) {
for (size_t i = 0; i < 4; i++)
data[offset + i] = (value >> (24 - 8 * i)) & 0xFF;
}
// A 1-bit BMP with the left half of every row set: eight pixels for each byte read
static std::vector<uint8_t> make_mono_bmp() {
constexpr size_t data_offset = 14 + 40 + 2 * 4;
constexpr size_t row_bytes = WIDTH / 8;
std::vector<uint8_t> bmp(data_offset + row_bytes * HEIGHT, 0);
bmp[0] = 'B';
bmp[1] = 'M';
put_le32(bmp, 2, bmp.size());
put_le32(bmp, 10, data_offset);
put_le32(bmp, 14, 40); // DIB header size
put_le32(bmp, 18, WIDTH);
put_le32(bmp, 22, HEIGHT);
bmp[26] = 1; // color planes
bmp[28] = 1; // bits per pixel
for (size_t row = 0; row < HEIGHT; row++) {
for (size_t i = 0; i < row_bytes / 2; i++)
bmp[data_offset + row * row_bytes + i] = 0xFF;
}
return bmp;
}
// A QOI image of one color: an RGB chunk, then runs of 62 pixels, each run a single byte
static std::vector<uint8_t> make_solid_qoi() {
std::vector<uint8_t> qoi(14, 0);
qoi[0] = 'q';
qoi[1] = 'o';
qoi[2] = 'i';
qoi[3] = 'f';
put_be32(qoi, 4, WIDTH);
put_be32(qoi, 8, HEIGHT);
qoi[12] = 3; // channels
qoi.insert(qoi.end(), {0xFE, 30, 120, 200});
for (size_t left = WIDTH * HEIGHT - 1; left > 0;) {
const size_t run = std::min<size_t>(left, 62);
qoi.push_back(0xC0 | (run - 1));
left -= run;
}
qoi.insert(qoi.end(), {0, 0, 0, 0, 0, 0, 0, 1});
return qoi;
}
// Feed the whole image as often as it takes, and return how many calls that was
static int decode_in_calls(RuntimeImage &img, std::vector<uint8_t> &data) {
EXPECT_TRUE(img.begin_decode(data.size()));
size_t offset = 0;
int calls = 0;
while (offset < data.size() && calls < 10000) {
const int consumed = img.feed_data(data.data() + offset, data.size() - offset);
EXPECT_GT(consumed, 0);
if (consumed <= 0)
break;
offset += consumed;
calls++;
}
EXPECT_EQ(offset, data.size());
EXPECT_TRUE(img.end_decode());
return calls;
}
TEST(DecodeTimeLimit, BmpIsDecodedOverSeveralCalls) {
RuntimeImage img(BMP, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE);
auto data = make_mono_bmp();
EXPECT_GT(decode_in_calls(img, data), 1) << "the whole image was decoded in one call";
ASSERT_EQ(img.get_width(), static_cast<int>(WIDTH));
ASSERT_EQ(img.get_height(), static_cast<int>(HEIGHT));
for (auto [col, row] : {std::pair{0, 0}, std::pair{1000, 1000}, std::pair{4095, 2047}}) {
const uint8_t expected = col < static_cast<int>(WIDTH / 2) ? 255 : 0;
EXPECT_EQ(img.get_pixel(col, row).r, expected) << col << "," << row;
}
}
TEST(DecodeTimeLimit, QoiIsDecodedOverSeveralCalls) {
RuntimeImage img(QOI, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
auto data = make_solid_qoi();
EXPECT_GT(decode_in_calls(img, data), 1) << "the whole image was decoded in one call";
for (auto [col, row] : {std::pair{0, 0}, std::pair{2000, 1000}, std::pair{4095, 2047}}) {
const Color pixel = img.get_pixel(col, row);
EXPECT_EQ(pixel.r, 30);
EXPECT_EQ(pixel.g, 120);
EXPECT_EQ(pixel.b, 200);
}
}
// A chunk split across two calls: the bytes used before the split count towards the decoded total
TEST(DecodeTimeLimit, QoiCountsTheBytesBeforeASplitChunk) {
RuntimeImage img(QOI, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
std::vector<uint8_t> data = {'q', 'o', 'i', 'f', 0, 0, 0, 2, 0, 0, 0, 1, 3, 0};
data.insert(data.end(), {0xFE, 10, 20, 30, 0xFE, 40, 50, 60, 0, 0, 0, 0, 0, 0, 0, 1});
QoiDecoder decoder(&img);
ASSERT_EQ(decoder.prepare(data.size()), 0);
// The header and the first chunk, and half of the second
EXPECT_EQ(decoder.decode(data.data(), 20), 18);
EXPECT_EQ(decoder.get_decoded_bytes(), 18u);
EXPECT_EQ(decoder.decode(data.data() + 18, data.size() - 18), static_cast<int>(data.size() - 18));
EXPECT_EQ(decoder.get_decoded_bytes(), data.size());
EXPECT_TRUE(decoder.is_finished());
}
} // namespace esphome::runtime_image::testing
@@ -4,9 +4,11 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <tuple>
#include <utility>
#include <vector>
#include "esphome/components/display/display_color_utils.h"
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/components/runtime_image/jpeg_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
@@ -477,7 +479,7 @@ TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
}
// Target pixel index each source pixel paints when `source` pixels map onto `target`, the way
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
// prepare_draw_ rounds: source n covers targets ceil(n * target / source) up to the next start
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
// JPEGDEC decodes at the smallest of full, 1/2, 1/4 and 1/8 size that still covers the buffer
@@ -573,4 +575,92 @@ TEST(RuntimeImageDecoder, SessionFlagsTrackLifecycle) {
EXPECT_FALSE(img.is_decode_finished()) << "no session means nothing is 'finished'";
}
// 5x3 QOI whose runs carry on from one row into the next
static const uint8_t QOI_RUNS[] = {
'q', 'o', 'i', 'f', 0, 0, 0, 5, 0, 0, 0, 3, 3, 0, // header
0xFE, 10, 20, 30, // 1 pixel
0xC5, // a run of 6
0xFE, 200, 100, 50, // 1 pixel
0xC6, // a run of 7
0, 0, 0, 0, 0, 0, 0, 1, // end marker
};
/// Decode a whole image into img
static void decode_into(RuntimeImage &img, ImageFormat format, const uint8_t *data, size_t len) {
std::vector<uint8_t> buffer(data, data + len);
ASSERT_TRUE(img.begin_decode(len, format));
size_t offset = 0;
while (offset < len) {
const int consumed = img.feed_data(buffer.data() + offset, len - offset);
ASSERT_GT(consumed, 0);
offset += consumed;
}
ASSERT_TRUE(img.end_decode());
}
// An opaque RGB565 image is written straight into the buffer. It must hold the same pixels, in the same places, as an
// RGB image filled through draw_pixel(), at the image's own size and scaled.
TEST(RuntimeImageDecoder, DirectRgb565MatchesTheGenericPath) {
const std::tuple<ImageFormat, const uint8_t *, size_t> images[] = {
{BMP, BMP_24BPP, sizeof(BMP_24BPP)},
{BMP, BMP_8BPP, sizeof(BMP_8BPP)},
{PNG, PNG_RGB, sizeof(PNG_RGB)},
{QOI, QOI_RUNS, sizeof(QOI_RUNS)},
};
for (auto [format, data, len] : images) {
for (auto [width, height] : {std::pair{0, 0}, std::pair{7, 5}, std::pair{2, 2}, std::pair{9, 2}}) {
RuntimeImage reference(format, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, nullptr, false, width, height);
decode_into(reference, format, data, len);
for (bool big_endian : {false, true}) {
RuntimeImage img(format, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE, nullptr, big_endian, width,
height);
decode_into(img, format, data, len);
ASSERT_EQ(img.get_width(), reference.get_width());
ASSERT_EQ(img.get_height(), reference.get_height());
const uint8_t *pixels = img.get_data_start();
for (int i = 0; i < img.get_width() * img.get_height(); i++) {
const Color color = reference.get_pixel(i % img.get_width(), i / img.get_width());
const uint16_t expected = display::ColorUtil::color_to_565(color);
const uint16_t actual =
big_endian ? (pixels[i * 2] << 8 | pixels[i * 2 + 1]) : (pixels[i * 2] | pixels[i * 2 + 1] << 8);
EXPECT_EQ(actual, expected) << "image of " << len << " bytes into " << width << "x" << height
<< (big_endian ? " big-endian" : " little-endian") << ", pixel " << i;
}
}
}
}
}
// The runs fill the rows they carry on into
TEST(RuntimeImageDecoder, QoiRunsCrossRows) {
TestableRuntimeImage img(QOI);
ASSERT_TRUE(decode_all(img, QOI_RUNS, sizeof(QOI_RUNS)));
for (int i = 0; i < 15; i++) {
const Color pixel = img.get_pixel(i % 5, i / 5);
const bool second = i >= 7;
EXPECT_EQ(pixel.r, second ? 200 : 10) << "pixel " << i;
EXPECT_EQ(pixel.g, second ? 100 : 20) << "pixel " << i;
EXPECT_EQ(pixel.b, second ? 50 : 30) << "pixel " << i;
}
}
/// Reads a decoder's protected boundary tables
struct BoundaryTables : ImageDecoder {
static size_t x_capacity(const ImageDecoder &decoder) {
return (decoder.*(&BoundaryTables::x_boundaries_)).capacity();
}
static size_t y_capacity(const ImageDecoder &decoder) {
return (decoder.*(&BoundaryTables::y_boundaries_)).capacity();
}
};
// A JPEG decoded at reduced size builds its tables once, for the decoded size, not first for the full size
TEST(RuntimeImageDecoder, JpegTablesAreBuiltForTheDecodedSize) {
TestableRuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, false, 1, 1);
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
ASSERT_EQ(static_cast<JpegDecoder *>(img.decoder())->get_scale(), 8);
EXPECT_EQ(BoundaryTables::x_capacity(*img.decoder()), 2u);
EXPECT_EQ(BoundaryTables::y_capacity(*img.decoder()), 2u);
}
} // namespace esphome::runtime_image::testing
@@ -0,0 +1,35 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstdlib>
#include "esphome/components/runtime_image/runtime_image.h"
namespace esphome::runtime_image::testing {
class GrayscaleImage : public RuntimeImage {
public:
GrayscaleImage() : RuntimeImage(BMP, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE) {}
uint8_t pixel(int x) const { return this->buffer_[x]; }
};
// The integer weights stay within one step of the floating point formula they replace, and keep white at 255
TEST(GrayscalePixel, MatchesTheFloatingPointWeights) {
GrayscaleImage img;
ASSERT_GT(img.resize(256, 1), 0);
for (int r = 0; r < 256; r += 5) {
for (int g = 0; g < 256; g += 5) {
for (int b = 0; b < 256; b += 5) {
img.draw_pixel(0, 0, Color(r, g, b));
const int expected = static_cast<int>(0.2125 * r + 0.7154 * g + 0.0721 * b);
ASSERT_LE(std::abs(img.pixel(0) - expected), 1) << "r=" << r << " g=" << g << " b=" << b;
}
}
}
img.draw_pixel(1, 0, Color(255, 255, 255));
EXPECT_EQ(img.pixel(1), 255);
img.draw_pixel(2, 0, Color(0, 0, 0));
EXPECT_EQ(img.pixel(2), 0);
}
} // namespace esphome::runtime_image::testing
@@ -0,0 +1,166 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <utility>
#include <vector>
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
namespace esphome::runtime_image::testing {
// 800x600 RGB PNG of a single color (30, 120, 200), 2312 bytes: little data, but many pixels to draw
static const uint8_t PNG_SOLID[] = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};
// One feed_data() call stops after a short time, so a large image is decoded over several calls
TEST(PngTimeLimit, LargeImageIsDecodedOverSeveralCalls) {
RuntimeImage img(PNG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
std::vector<uint8_t> data(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID));
ASSERT_TRUE(img.begin_decode(data.size()));
size_t offset = 0;
int calls = 0;
while (offset < data.size() && calls < 1000) {
const int consumed = img.feed_data(data.data() + offset, data.size() - offset);
ASSERT_GT(consumed, 0);
offset += consumed;
calls++;
}
EXPECT_GT(calls, 1) << "the whole image was decoded in one call";
ASSERT_EQ(offset, data.size());
ASSERT_TRUE(img.end_decode());
ASSERT_EQ(img.get_width(), 800);
ASSERT_EQ(img.get_height(), 600);
for (auto [col, row] : {std::pair{0, 0}, std::pair{400, 300}, std::pair{799, 599}}) {
const Color pixel = img.get_pixel(col, row);
EXPECT_EQ(pixel.r, 30);
EXPECT_EQ(pixel.g, 120);
EXPECT_EQ(pixel.b, 200);
}
}
} // namespace esphome::runtime_image::testing