mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 03:32:50 +00:00
[image] Draw rows in the outer loop and hand opaque RGB565 and RGB blocks to the display (#20653)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
clydebarrow
Claude Opus 5.5
parent
51b1fdcc8b
commit
802c2633ed
@@ -5,8 +5,8 @@
|
||||
namespace esphome::animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type, image::Transparency transparent)
|
||||
: Image(data_start, width, height, type, transparent),
|
||||
image::ImageType type, image::Transparency transparent, bool big_endian)
|
||||
: Image(data_start, width, height, type, transparent, big_endian),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::animation {
|
||||
class Animation final : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
|
||||
image::Transparency transparent);
|
||||
image::Transparency transparent, bool big_endian = false);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_LOOP
|
||||
from esphome.components.const import CONF_BYTE_ORDER, CONF_LOOP
|
||||
from esphome.components.file import image as file_image
|
||||
from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
@@ -90,6 +90,7 @@ async def setup_animation(config: ConfigType) -> None:
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
|
||||
)
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
|
||||
@@ -254,7 +254,13 @@ async def new_image(config: ConfigType) -> MockObj:
|
||||
"""
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
config[CONF_ID],
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
image_type,
|
||||
trans_value,
|
||||
config.get(CONF_BYTE_ORDER) == "BIG_ENDIAN",
|
||||
)
|
||||
add_metadata(
|
||||
config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "image.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -7,92 +9,137 @@ ESPHOME_LOG_TAG(TAG, "image");
|
||||
|
||||
namespace esphome::image {
|
||||
|
||||
// Walks the clipped range with rows (ROWS_OUTER) or columns outer and hands the
|
||||
// body its coordinates and row major index. The body must be ESPHOME_ALWAYS_INLINE,
|
||||
// at -Os GCC otherwise calls it once per pixel.
|
||||
template<bool ROWS_OUTER, typename F>
|
||||
static void for_each_pixel(int x_start, int x_end, int y_start, int y_end, int width, F &&f) {
|
||||
const int outer_start = ROWS_OUTER ? y_start : x_start;
|
||||
const int outer_end = ROWS_OUTER ? y_end : x_end;
|
||||
const int inner_start = ROWS_OUTER ? x_start : y_start;
|
||||
const int inner_end = ROWS_OUTER ? x_end : y_end;
|
||||
for (int outer = outer_start; outer < outer_end; outer++) {
|
||||
const int outer_pos = ROWS_OUTER ? outer * width : outer;
|
||||
for (int inner = inner_start; inner < inner_end; inner++) {
|
||||
f(ROWS_OUTER ? inner : outer, ROWS_OUTER ? outer : inner,
|
||||
ROWS_OUTER ? outer_pos + inner : outer_pos + inner * width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||
int img_x0 = 0;
|
||||
int img_y0 = 0;
|
||||
int w = width_;
|
||||
int h = height_;
|
||||
int x_start = 0;
|
||||
int y_start = 0;
|
||||
int x_end = width_;
|
||||
int y_end = height_;
|
||||
|
||||
auto clipping = display->get_clipping();
|
||||
if (clipping.is_set()) {
|
||||
if (clipping.x > x)
|
||||
img_x0 += clipping.x - x;
|
||||
x_start += clipping.x - x;
|
||||
if (clipping.y > y)
|
||||
img_y0 += clipping.y - y;
|
||||
if (w > clipping.x2() - x)
|
||||
w = clipping.x2() - x;
|
||||
if (h > clipping.y2() - y)
|
||||
h = clipping.y2() - y;
|
||||
y_start += clipping.y - y;
|
||||
if (x_end > clipping.x2() - x)
|
||||
x_end = clipping.x2() - x;
|
||||
if (y_end > clipping.y2() - y)
|
||||
y_end = clipping.y2() - y;
|
||||
}
|
||||
|
||||
switch (type_) {
|
||||
case IMAGE_TYPE_BINARY: {
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparency_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifndef USE_ESP8266
|
||||
// Opaque RGB565 and RGB are already laid out for draw_pixels_at(), so the display
|
||||
// gets the whole window. Not on the ESP8266, whose image bytes need progmem_read_byte().
|
||||
if (this->transparency_ == TRANSPARENCY_OPAQUE &&
|
||||
(this->type_ == IMAGE_TYPE_RGB565 || this->type_ == IMAGE_TYPE_RGB)) {
|
||||
// Keep the block on screen
|
||||
x_start = std::max(x_start, -x);
|
||||
y_start = std::max(y_start, -y);
|
||||
x_end = std::min(x_end, display->get_width() - x);
|
||||
y_end = std::min(y_end, display->get_height() - y);
|
||||
if (x_end <= x_start || y_end <= y_start)
|
||||
return;
|
||||
const bool rgb565 = this->type_ == IMAGE_TYPE_RGB565;
|
||||
display->draw_pixels_at(x + x_start, y + y_start, x_end - x_start, y_end - y_start, this->data_start_,
|
||||
display::COLOR_ORDER_RGB, rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_888,
|
||||
this->big_endian_, x_start, y_start, this->width_ - x_end);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Rows keep the read and the write sequential; when the display swaps the axes
|
||||
// columns keep the write sequential instead, and a strided write costs more.
|
||||
#ifdef USE_PSRAM
|
||||
if (display->pixel_axes_swapped()) {
|
||||
this->draw_<false>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Without PSRAM a strided write is free, so one copy of the loops is enough
|
||||
this->draw_<true>(x, y, display, color_on, color_off, x_start, x_end, y_start, y_end);
|
||||
}
|
||||
|
||||
template<bool ROWS_OUTER>
|
||||
void Image::draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
|
||||
int y_start, int y_end) {
|
||||
// Local copies, or the virtual draw_pixel_at() forces a reload every pixel
|
||||
const Transparency transparency = this->transparency_;
|
||||
const int width = this->width_;
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
|
||||
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (transparency == TRANSPARENCY_OPAQUE) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case IMAGE_TYPE_RGB111:
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
|
||||
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
|
||||
auto color = this->get_rgb111_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case IMAGE_TYPE_GRAYSCALE: {
|
||||
const uint8_t *const data = this->data_start_;
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
|
||||
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
|
||||
const uint8_t gray = progmem_read_byte(data + pos);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
if (transparency == TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
return; // transparent pixel, skip drawing
|
||||
}
|
||||
} else if (transparency == TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
// gray is the alpha: blend from color_off to color_on, drawn opaque
|
||||
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
|
||||
Color::blend_channel(color_off.g, color_on.g, gray),
|
||||
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_RGB111: {
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
auto color = this->get_rgb111_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
const uint32_t pos = (img_x + img_y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
Color color = Color(gray, gray, gray, 0xFF);
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_CHROMA_KEY:
|
||||
if (gray == 1) {
|
||||
continue; // skip drawing
|
||||
}
|
||||
break;
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
// gray is the alpha: blend from color_off to color_on, drawn opaque
|
||||
color = Color(Color::blend_channel(color_off.r, color_on.r, gray),
|
||||
Color::blend_channel(color_off.g, color_on.g, gray),
|
||||
Color::blend_channel(color_off.b, color_on.b, gray), 0xFF);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
auto color = this->get_rgb565_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
|
||||
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
|
||||
auto color = this->get_rgb565_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case IMAGE_TYPE_RGB:
|
||||
for (int img_x = img_x0; img_x < w; img_x++) {
|
||||
for (int img_y = img_y0; img_y < h; img_y++) {
|
||||
auto color = this->get_rgb_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
for_each_pixel<ROWS_OUTER>(x_start, x_end, y_start, y_end, width,
|
||||
[&](int img_x, int img_y, int pos) ESPHOME_ALWAYS_INLINE {
|
||||
auto color = this->get_rgb_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -185,7 +232,7 @@ Color Image::get_rgb111_pixel_(int x, int y) const {
|
||||
return Color(rgb & 4 ? 0xFF : 0x00, rgb & 2 ? 0xFF : 0x00, rgb & 1 ? 0xFF : 0x00, opaque ? 0xFF : 0x00);
|
||||
}
|
||||
Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
|
||||
const uint32_t pos = (x + y * this->width_) * static_cast<size_t>(this->bpp_) / 8;
|
||||
Color color = Color(progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 1),
|
||||
progmem_read_byte(this->data_start_ + pos + 0), 0xFF);
|
||||
|
||||
@@ -205,8 +252,10 @@ Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
return color;
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * static_cast<size_t>(this->bpp_) / 8;
|
||||
// High byte first when big endian
|
||||
const uint8_t high = this->big_endian_ ? 0 : 1;
|
||||
const uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + high), progmem_read_byte(pos + 1 - high));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
@@ -242,8 +291,14 @@ Color Image::get_grayscale_pixel_(int x, int y) const {
|
||||
int Image::get_width() const { return this->width_; }
|
||||
int Image::get_height() const { return this->height_; }
|
||||
ImageType Image::get_type() const { return this->type_; }
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency,
|
||||
bool big_endian)
|
||||
: width_(width),
|
||||
height_(height),
|
||||
type_(type),
|
||||
data_start_(data_start),
|
||||
transparency_(transparency),
|
||||
big_endian_(big_endian) {
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
this->bpp_ = 1;
|
||||
|
||||
@@ -24,7 +24,9 @@ enum Transparency {
|
||||
|
||||
class Image : public display::BaseImage {
|
||||
public:
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
|
||||
/// big_endian is the RGB565 byte order; codegen writes little endian unless configured otherwise.
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency,
|
||||
bool big_endian = false);
|
||||
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
|
||||
int get_width() const override;
|
||||
int get_height() const override;
|
||||
@@ -44,6 +46,10 @@ class Image : public display::BaseImage {
|
||||
lv_image_dsc_t *get_lv_image_dsc();
|
||||
#endif
|
||||
protected:
|
||||
/// Per-pixel draw of the clipped range, rows (true) or columns (false) outer.
|
||||
template<bool ROWS_OUTER>
|
||||
void draw_(int x, int y, display::Display *display, Color color_on, Color color_off, int x_start, int x_end,
|
||||
int y_start, int y_end);
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgb111_pixel_(int x, int y) const;
|
||||
Color get_rgb_pixel_(int x, int y) const;
|
||||
@@ -55,7 +61,8 @@ class Image : public display::BaseImage {
|
||||
ImageType type_;
|
||||
const uint8_t *data_start_;
|
||||
Transparency transparency_;
|
||||
size_t bpp_{};
|
||||
bool big_endian_;
|
||||
uint8_t bpp_{};
|
||||
#ifdef USE_LVGL
|
||||
lv_img_dsc_t dsc_{};
|
||||
#endif
|
||||
|
||||
@@ -30,6 +30,6 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
}
|
||||
|
||||
uint8_t *ImageDecoder::image_buffer_() const { return this->image_->buffer_; }
|
||||
bool ImageDecoder::image_is_big_endian_() const { return this->image_->is_big_endian_; }
|
||||
bool ImageDecoder::image_is_big_endian_() const { return this->image_->big_endian_; }
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -41,12 +41,11 @@ inline bool is_color_on(const Color &color) {
|
||||
|
||||
RuntimeImage::RuntimeImage(ImageFormat format, image::ImageType type, image::Transparency transparency,
|
||||
image::Image *placeholder, bool is_big_endian, int fixed_width, int fixed_height)
|
||||
: Image(nullptr, 0, 0, type, transparency),
|
||||
: Image(nullptr, 0, 0, type, transparency, is_big_endian),
|
||||
format_(format),
|
||||
fixed_width_(fixed_width),
|
||||
fixed_height_(fixed_height),
|
||||
placeholder_(placeholder),
|
||||
is_big_endian_(is_big_endian) {}
|
||||
placeholder_(placeholder) {}
|
||||
|
||||
RuntimeImage::~RuntimeImage() { this->release(); }
|
||||
|
||||
@@ -137,7 +136,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
uint16_t rgb565 = display::ColorUtil::color_to_565(mapped_color);
|
||||
if (this->is_big_endian_) {
|
||||
if (this->big_endian_) {
|
||||
this->buffer_[pos + 0] = static_cast<uint8_t>((rgb565 >> 8) & 0xFF);
|
||||
this->buffer_[pos + 1] = static_cast<uint8_t>(rgb565 & 0xFF);
|
||||
} else {
|
||||
|
||||
@@ -227,11 +227,6 @@ class RuntimeImage : public image::Image {
|
||||
|
||||
// Configuration
|
||||
bool progressive_display_{false};
|
||||
/**
|
||||
* Whether the image is stored in big-endian format.
|
||||
* This is used to determine how to store 16 bit colors in the buffer.
|
||||
*/
|
||||
bool is_big_endian_{false};
|
||||
/** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */
|
||||
bool external_buffer_{false};
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
settings.height,
|
||||
settings.image_type_enum,
|
||||
settings.transparent,
|
||||
settings.byte_order_big_endian,
|
||||
)
|
||||
add_metadata(
|
||||
view_id,
|
||||
|
||||
@@ -745,7 +745,7 @@ def test_image_generation(
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
"new(cat_img) image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
||||
"new(cat_img) image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE, false);"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
spi:
|
||||
clk_pin: 18
|
||||
mosi_pin: 23
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
model: ili9342
|
||||
cs_pin: 22
|
||||
dc_pin: 21
|
||||
invert_colors: false
|
||||
|
||||
sendspin:
|
||||
|
||||
image:
|
||||
- platform: sendspin
|
||||
id: album_slot
|
||||
format: JPEG
|
||||
type: RGB565
|
||||
byte_order: BIG_ENDIAN
|
||||
resize: 240x240
|
||||
current_image:
|
||||
id: album_art
|
||||
transition_image:
|
||||
id: album_art_transition
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
spi:
|
||||
clk_pin: 18
|
||||
mosi_pin: 23
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
model: ili9342
|
||||
cs_pin: 22
|
||||
dc_pin: 21
|
||||
invert_colors: false
|
||||
|
||||
sendspin:
|
||||
|
||||
image:
|
||||
- platform: sendspin
|
||||
id: album_slot
|
||||
format: JPEG
|
||||
type: RGB565
|
||||
byte_order: LITTLE_ENDIAN
|
||||
resize: 240x240
|
||||
current_image:
|
||||
id: album_art
|
||||
transition_image:
|
||||
id: album_art_transition
|
||||
@@ -4,6 +4,8 @@ These cover the rejection branches, which a compile test cannot reach: a
|
||||
`test*.yaml` can only assert that a configuration is accepted.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -120,3 +122,24 @@ def test_display_offset_accepted(
|
||||
config = CONFIG_SCHEMA(_slot_config(display_offset=display_offset))
|
||||
|
||||
assert config["display_offset"].total_milliseconds == expected_ms
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
("image_byte_order_big_endian.yaml", "true"),
|
||||
("image_byte_order_little_endian.yaml", "false"),
|
||||
],
|
||||
)
|
||||
def test_views_get_the_byte_order(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Both views draw the frames the slot decodes, so they must read them in its byte order."""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
views = [line for line in main_cpp.splitlines() if "ArtworkImageView(" in line]
|
||||
assert len(views) == 2
|
||||
assert all(line.endswith(f", {expected});") for line in views), views
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
namespace esphome::image::testing {
|
||||
|
||||
class TestDisplay : public display::Display {
|
||||
public:
|
||||
void update() override {}
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
int get_width_internal() override { return 64; }
|
||||
int get_height_internal() override { return 48; }
|
||||
};
|
||||
|
||||
struct Blit {
|
||||
int x, y, w, h;
|
||||
const uint8_t *ptr;
|
||||
display::ColorBitness bitness;
|
||||
bool big_endian;
|
||||
int x_offset, y_offset, x_pad;
|
||||
};
|
||||
|
||||
// Records the bulk calls and counts the per pixel calls Image::draw() makes.
|
||||
class RecordingDisplay : public TestDisplay {
|
||||
public:
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
|
||||
this->blits.push_back({x_start, y_start, w, h, ptr, bitness, big_endian, x_offset, y_offset, x_pad});
|
||||
}
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels++; }
|
||||
|
||||
std::vector<Blit> blits;
|
||||
int pixels{0};
|
||||
};
|
||||
|
||||
// Lets the bulk call fall through to the base class and keeps the pixels it produces.
|
||||
class FallbackDisplay : public TestDisplay {
|
||||
public:
|
||||
void draw_pixel_at(int x, int y, Color color) override { this->pixels.push_back({x, y, color.raw_32}); }
|
||||
|
||||
struct Pixel {
|
||||
int x, y;
|
||||
uint32_t raw;
|
||||
};
|
||||
std::vector<Pixel> pixels;
|
||||
};
|
||||
|
||||
static constexpr int W = 8, H = 6;
|
||||
|
||||
static std::vector<uint8_t> make_data(int bytes_per_pixel) {
|
||||
std::vector<uint8_t> data(W * H * bytes_per_pixel);
|
||||
uint8_t value = 1;
|
||||
for (auto &byte : data)
|
||||
byte = value++;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Draws an opaque RGB565 image at (x, y) and returns the single bulk call it made.
|
||||
static Blit draw_one(int x, int y, const std::vector<uint8_t> &data, RecordingDisplay &display,
|
||||
bool big_endian = false) {
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, big_endian);
|
||||
image.draw(x, y, &display, Color::WHITE, Color::BLACK);
|
||||
EXPECT_EQ(display.blits.size(), 1u);
|
||||
EXPECT_EQ(display.pixels, 0);
|
||||
return display.blits.at(0);
|
||||
}
|
||||
|
||||
static void expect_window(const Blit &blit, int x, int y, int w, int h, int x_offset, int y_offset, int x_pad) {
|
||||
EXPECT_EQ(blit.x, x);
|
||||
EXPECT_EQ(blit.y, y);
|
||||
EXPECT_EQ(blit.w, w);
|
||||
EXPECT_EQ(blit.h, h);
|
||||
EXPECT_EQ(blit.x_offset, x_offset);
|
||||
EXPECT_EQ(blit.y_offset, y_offset);
|
||||
EXPECT_EQ(blit.x_pad, x_pad);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, OpaqueRgb565IsOneBulkCall) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
const Blit blit = draw_one(10, 20, data, display);
|
||||
expect_window(blit, 10, 20, W, H, 0, 0, 0);
|
||||
EXPECT_EQ(blit.ptr, data.data());
|
||||
EXPECT_EQ(blit.bitness, display::COLOR_BITNESS_565);
|
||||
EXPECT_FALSE(blit.big_endian);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, BigEndianRgb565IsPassedThrough) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
EXPECT_TRUE(draw_one(0, 0, data, display, true).big_endian);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, OpaqueRgbIsOneBulkCall) {
|
||||
const auto data = make_data(3);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE);
|
||||
RecordingDisplay display;
|
||||
image.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
ASSERT_EQ(display.blits.size(), 1u);
|
||||
EXPECT_EQ(display.blits[0].bitness, display::COLOR_BITNESS_888);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, ClippingShrinksTheWindow) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
display.start_clipping(12, 22, 15, 24); // right and bottom are exclusive
|
||||
expect_window(draw_one(10, 20, data, display), 12, 22, 3, 2, 2, 2, W - 5);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, PartlyOffTheTopLeftIsClamped) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
expect_window(draw_one(-3, -2, data, display), 0, 0, W - 3, H - 2, 3, 2, 0);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, PartlyOffTheBottomRightIsClamped) {
|
||||
const auto data = make_data(2);
|
||||
RecordingDisplay display;
|
||||
expect_window(draw_one(60, 44, data, display), 60, 44, 4, 4, 0, 0, W - 4);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, FullyOffScreenDrawsNothing) {
|
||||
const auto data = make_data(2);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE);
|
||||
RecordingDisplay display;
|
||||
image.draw(64, 0, &display, Color::WHITE, Color::BLACK);
|
||||
image.draw(0, -H, &display, Color::WHITE, Color::BLACK);
|
||||
EXPECT_TRUE(display.blits.empty());
|
||||
EXPECT_EQ(display.pixels, 0);
|
||||
}
|
||||
|
||||
TEST(ImageDraw, TransparentAndOtherFormatsStayPerPixel) {
|
||||
const auto rgb565 = make_data(2);
|
||||
const auto gray = make_data(1);
|
||||
Image keyed(rgb565.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY);
|
||||
Image grayscale(gray.data(), W, H, IMAGE_TYPE_GRAYSCALE, TRANSPARENCY_OPAQUE);
|
||||
RecordingDisplay display;
|
||||
keyed.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
grayscale.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
EXPECT_TRUE(display.blits.empty());
|
||||
EXPECT_GT(display.pixels, 0);
|
||||
}
|
||||
|
||||
// The base class fallback reads pixels in the image's byte order.
|
||||
static void expect_fallback_decodes(bool big_endian) {
|
||||
const auto data = make_data(2);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, big_endian);
|
||||
FallbackDisplay display;
|
||||
image.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
ASSERT_EQ(display.pixels.size(), static_cast<size_t>(W * H));
|
||||
for (const auto &pixel : display.pixels) {
|
||||
const uint32_t rgb565 =
|
||||
display::ColorUtil::read_packed<display::COLOR_BITNESS_565>(data.data(), pixel.x + pixel.y * W, big_endian);
|
||||
const Color expected = display::ColorUtil::to_color(rgb565, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565);
|
||||
EXPECT_EQ(pixel.raw, expected.raw_32) << "at " << pixel.x << "," << pixel.y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageDraw, FallbackDecodesLittleEndian) { expect_fallback_decodes(false); }
|
||||
TEST(ImageDraw, FallbackDecodesBigEndian) { expect_fallback_decodes(true); }
|
||||
|
||||
// The per pixel reader behind get_pixel() reads RGB565 in the image's byte order.
|
||||
static void expect_reader_decodes(bool big_endian) {
|
||||
const auto data = make_data(2);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_OPAQUE, big_endian);
|
||||
for (int y = 0; y < H; y++) {
|
||||
for (int x = 0; x < W; x++) {
|
||||
const uint32_t rgb565 =
|
||||
display::ColorUtil::read_packed<display::COLOR_BITNESS_565>(data.data(), x + y * W, big_endian);
|
||||
const Color expected(((rgb565 >> 11) << 3) | ((rgb565 >> 11) >> 2),
|
||||
(((rgb565 >> 5) & 0x3F) << 2) | (((rgb565 >> 5) & 0x3F) >> 4),
|
||||
((rgb565 & 0x1F) << 3) | ((rgb565 & 0x1F) >> 2), 0xFF);
|
||||
EXPECT_EQ(image.get_pixel(x, y).raw_32, expected.raw_32) << "at " << x << "," << y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageDraw, ReaderDecodesLittleEndian) { expect_reader_decodes(false); }
|
||||
TEST(ImageDraw, ReaderDecodesBigEndian) { expect_reader_decodes(true); }
|
||||
|
||||
// A chroma keyed RGB565 draw stays per pixel and must match get_pixel().
|
||||
static void expect_keyed_draw_matches_reader(bool big_endian) {
|
||||
const auto data = make_data(2);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB565, TRANSPARENCY_CHROMA_KEY, big_endian);
|
||||
FallbackDisplay display;
|
||||
image.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
ASSERT_GT(display.pixels.size(), 0u);
|
||||
for (const auto &pixel : display.pixels) {
|
||||
EXPECT_EQ(pixel.raw, image.get_pixel(pixel.x, pixel.y).raw_32) << "at " << pixel.x << "," << pixel.y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ImageDraw, KeyedDrawMatchesReaderLittleEndian) { expect_keyed_draw_matches_reader(false); }
|
||||
TEST(ImageDraw, KeyedDrawMatchesReaderBigEndian) { expect_keyed_draw_matches_reader(true); }
|
||||
|
||||
// The fallback keeps red and blue in the order the image stores them.
|
||||
TEST(ImageDraw, FallbackKeepsRgbChannelOrder) {
|
||||
const auto data = make_data(3);
|
||||
Image image(data.data(), W, H, IMAGE_TYPE_RGB, TRANSPARENCY_OPAQUE);
|
||||
FallbackDisplay display;
|
||||
image.draw(0, 0, &display, Color::WHITE, Color::BLACK);
|
||||
ASSERT_EQ(display.pixels.size(), static_cast<size_t>(W * H));
|
||||
for (const auto &pixel : display.pixels) {
|
||||
// get_pixel() reports an opaque pixel with alpha set; the fallback decodes colour only
|
||||
const Color expected = image.get_pixel(pixel.x, pixel.y);
|
||||
EXPECT_EQ(pixel.raw, Color(expected.r, expected.g, expected.b).raw_32) << "at " << pixel.x << "," << pixel.y;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::image::testing
|
||||
Reference in New Issue
Block a user