diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index df0fade3d43..d7965774218 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -10,18 +10,22 @@ ESPHOME_LOG_TAG(TAG, "image"); namespace esphome::image { // Walks the clipped image range with rows (ROWS_OUTER) or columns as the outer -// loop. The coordinate selection folds at compile time, and the pixel body must -// be ESPHOME_ALWAYS_INLINE: at -Os GCC otherwise leaves it out of line and calls +// loop and hands the pixel body its coordinates plus its index in a row major +// image of the given width, with the row part hoisted out of the inner loop. +// The coordinate selection folds at compile time, and the pixel body must be +// ESPHOME_ALWAYS_INLINE: at -Os GCC otherwise leaves it out of line and calls // it once per pixel. template -static void for_each_pixel(int x_start, int x_end, int y_start, int y_end, F &&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); + f(ROWS_OUTER ? inner : outer, ROWS_OUTER ? outer : inner, + ROWS_OUTER ? outer_pos + inner : outer_pos + inner * width); } } } @@ -87,59 +91,64 @@ void Image::draw_(int x, int y, display::Display *display, Color color_on, Color // Local copies: draw_pixel_at() is an opaque virtual call, so members read // in the loop would otherwise be reloaded every pixel. const Transparency transparency = this->transparency_; + const int width = this->width_; switch (this->type_) { case IMAGE_TYPE_BINARY: - for_each_pixel(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) 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); - } - }); + for_each_pixel(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(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) 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); - } - }); + for_each_pixel(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 int width = this->width_; const uint8_t *const data = this->data_start_; - for_each_pixel(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) ESPHOME_ALWAYS_INLINE { - const uint8_t gray = progmem_read_byte(data + img_x + img_y * width); - 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); - }); + for_each_pixel(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_RGB565: - for_each_pixel(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) 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); - } - }); + for_each_pixel(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_each_pixel(x_start, x_end, y_start, y_end, [&](int img_x, int img_y) 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); - } - }); + for_each_pixel(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; } } diff --git a/tests/components/image/test_image_draw.cpp b/tests/components/image/test_image_draw.cpp index 5e3ce1fee04..2e8cab00509 100644 --- a/tests/components/image/test_image_draw.cpp +++ b/tests/components/image/test_image_draw.cpp @@ -163,4 +163,17 @@ static void expect_fallback_decodes(bool big_endian) { TEST(ImageDraw, FallbackDecodesLittleEndian) { expect_fallback_decodes(false); } TEST(ImageDraw, FallbackDecodesBigEndian) { expect_fallback_decodes(true); } +// An RGB image through the fallback must give every pixel the colour get_pixel() reads, +// which 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(W * H)); + for (const auto &pixel : display.pixels) { + EXPECT_EQ(pixel.raw, image.get_pixel(pixel.x, pixel.y).raw_32) << "at " << pixel.x << "," << pixel.y; + } +} + } // namespace esphome::image::testing