From 8a518f0def0a9d306eea608064547f0266beb8d7 Mon Sep 17 00:00:00 2001 From: guillempages Date: Sun, 21 May 2023 22:03:21 +0200 Subject: [PATCH] Add transparency support to all image types (#4600) --- .gitattributes | 1 + esphome/components/animation/__init__.py | 147 ++++++++++++---- esphome/components/display/display_buffer.cpp | 127 ++++++++++---- esphome/components/display/display_buffer.h | 10 +- esphome/components/image/__init__.py | 160 +++++++++++++----- script/ci-custom.py | 3 +- tests/pnglogo.png | Bin 0 -> 685 bytes tests/test2.yaml | 21 +++ 8 files changed, 359 insertions(+), 110 deletions(-) create mode 100644 tests/pnglogo.png diff --git a/.gitattributes b/.gitattributes index dad0966222..1b3fd332b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Normalize line endings to LF in the repository * text eol=lf +*.png binary diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 68c3eee132..1b804bd527 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -3,6 +3,7 @@ import logging from esphome import core from esphome.components import display, font import esphome.components.image as espImage +from esphome.components.image import CONF_USE_TRANSPARENCY import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE @@ -15,16 +16,42 @@ MULTI_CONF = True Animation_ = display.display_ns.class_("Animation", espImage.Image_) + +def validate_cross_dependencies(config): + """ + Validate fields whose possible values depend on other fields. + For example, validate that explicitly transparent image types + have "use_transparency" set to True. + Also set the default value for those kind of dependent fields. + """ + image_type = config[CONF_TYPE] + is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] + # If the use_transparency option was not specified, set the default depending on the image type + if CONF_USE_TRANSPARENCY not in config: + config[CONF_USE_TRANSPARENCY] = is_transparent_type + + if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: + raise cv.Invalid(f"Image type {image_type} must always be transparent.") + + return config + + ANIMATION_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Required(CONF_FILE): cv.file_, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( - espImage.IMAGE_TYPE, upper=True - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - } + cv.All( + { + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( + espImage.IMAGE_TYPE, upper=True + ), + # Not setting default here on purpose; the default depends on the image type, + # and thus will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + }, + validate_cross_dependencies, + ) ) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) @@ -50,16 +77,19 @@ async def to_code(config): else: if width > 500 or height > 500: _LOGGER.warning( - "The image you requested is very big. Please consider using" - " the resize parameter." + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, ) + transparent = config[CONF_USE_TRANSPARENCY] + if config[CONF_TYPE] == "GRAYSCALE": data = [0 for _ in range(height * width * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("L", dither=Image.NONE) + frame = image.convert("LA", dither=Image.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) @@ -67,16 +97,22 @@ async def to_code(config): raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) - for pix in pixels: + for pix, a in pixels: + if transparent: + if pix == 1: + pix = 0 + if a < 0x80: + pix = 1 + data[pos] = pix pos += 1 - elif config[CONF_TYPE] == "RGB24": - data = [0 for _ in range(height * width * 3 * frames)] + elif config[CONF_TYPE] == "RGBA": + data = [0 for _ in range(height * width * 4 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("RGB") + frame = image.convert("RGBA") if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) @@ -91,13 +127,15 @@ async def to_code(config): pos += 1 data[pos] = pix[2] pos += 1 + data[pos] = pix[3] + pos += 1 - elif config[CONF_TYPE] == "RGB565": - data = [0 for _ in range(height * width * 2 * frames)] + elif config[CONF_TYPE] == "RGB24": + data = [0 for _ in range(height * width * 3 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("RGB") + frame = image.convert("RGBA") if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) @@ -105,14 +143,50 @@ async def to_code(config): raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) - for pix in pixels: - R = pix[0] >> 3 - G = pix[1] >> 2 - B = pix[2] >> 3 + for r, g, b, a in pixels: + if transparent: + if r == 0 and g == 0 and b == 1: + b = 0 + if a < 0x80: + r = 0 + g = 0 + b = 1 + + data[pos] = r + pos += 1 + data[pos] = g + pos += 1 + data[pos] = b + pos += 1 + + elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: + data = [0 for _ in range(height * width * 2 * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("RGBA") + if CONF_RESIZE in config: + frame = frame.resize([width, height]) + pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + ) + for r, g, b, a in pixels: + R = r >> 3 + G = g >> 2 + B = b >> 3 rgb = (R << 11) | (G << 5) | B + + if transparent: + if rgb == 0x0020: + rgb = 0 + if a < 0x80: + rgb = 0x0020 + data[pos] = rgb >> 8 pos += 1 - data[pos] = rgb & 255 + data[pos] = rgb & 0xFF pos += 1 elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: @@ -120,19 +194,31 @@ async def to_code(config): data = [0 for _ in range((height * width8 // 8) * frames)] for frameIndex in range(frames): image.seek(frameIndex) + if transparent: + alpha = image.split()[-1] + has_alpha = alpha.getextrema()[0] < 0xFF frame = image.convert("1", dither=Image.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) - for y in range(height): - for x in range(width): - if frame.getpixel((x, y)): + if transparent: + alpha = alpha.resize([width, height]) + for x, y in [(i, j) for i in range(width) for j in range(height)]: + if transparent and has_alpha: + if not alpha.getpixel((x, y)): continue - pos = x + y * width8 + (height * width8 * frameIndex) - data[pos // 8] |= 0x80 >> (pos % 8) + elif frame.getpixel((x, y)): + continue + + pos = x + y * width8 + (height * width8 * frameIndex) + data[pos // 8] |= 0x80 >> (pos % 8) + else: + raise core.EsphomeError( + f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." + ) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable( + var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, @@ -140,3 +226,4 @@ async def to_code(config): frames, espImage.IMAGE_TYPE[config[CONF_TYPE]], ) + cg.add(var.set_transparency(transparent)) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index f4e7785b5e..35e55bc1ba 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -12,7 +12,7 @@ namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); +const Color COLOR_OFF(0, 0, 0, 255); const Color COLOR_ON(255, 255, 255, 255); void Rect::expand(int16_t horizontal, int16_t vertical) { @@ -307,40 +307,58 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al } void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { + bool transparent = image->has_transparency(); + switch (image->get_type()) { - case IMAGE_TYPE_BINARY: + case IMAGE_TYPE_BINARY: { for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); + if (image->get_pixel(img_x, img_y)) { + this->draw_pixel_at(x + img_x, y + img_y, color_on); + } else if (!transparent) { + this->draw_pixel_at(x + img_x, y + img_y, color_off); + } } } break; + } case IMAGE_TYPE_GRAYSCALE: for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); - } - } - break; - case IMAGE_TYPE_RGB24: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); - } - } - break; - case IMAGE_TYPE_TRANSPARENT_BINARY: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - if (image->get_pixel(img_x, img_y)) - this->draw_pixel_at(x + img_x, y + img_y, color_on); + auto color = image->get_grayscale_pixel(img_x, img_y); + if (color.w >= 0x80) { + this->draw_pixel_at(x + img_x, y + img_y, color); + } } } break; case IMAGE_TYPE_RGB565: for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y)); + auto color = image->get_rgb565_pixel(img_x, img_y); + if (color.w >= 0x80) { + this->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + auto color = image->get_color_pixel(img_x, img_y); + if (color.w >= 0x80) { + this->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGBA: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + auto color = image->get_rgba_pixel(img_x, img_y); + if (color.w >= 0x80) { + this->draw_pixel_at(x + img_x, y + img_y, color); + } } } break; @@ -629,14 +647,27 @@ bool Image::get_pixel(int x, int y) const { const uint32_t pos = x + y * width_8; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } +Color Image::get_rgba_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_) * 4; + return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); +} Color Image::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; const uint32_t pos = (x + y * this->width_) * 3; - const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | - (progmem_read_byte(this->data_start_ + pos + 1) << 8) | - (progmem_read_byte(this->data_start_ + pos + 0) << 16); - return Color(color32); + Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2)); + if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { + // (0, 0, 1) has been defined as transparent color for non-alpha images. + // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) + color.w = 0; + } else { + color.w = 0xFF; + } + return color; } Color Image::get_rgb565_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) @@ -647,14 +678,22 @@ Color Image::get_rgb565_pixel(int x, int y) const { auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; - return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); + Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); + if (rgb565 == 0x0020 && transparent_) { + // darkest green has been defined as transparent color for transparent RGB565 images. + color.w = 0; + } else { + color.w = 0xFF; + } + return color; } Color Image::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - return Color(gray | gray << 8 | gray << 16 | gray << 24); + uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; + return Color(gray, gray, gray, alpha); } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } @@ -673,6 +712,16 @@ bool Animation::get_pixel(int x, int y) const { const uint32_t pos = x + y * width_8 + frame_index; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } +Color Animation::get_rgba_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_ + frame_index) * 4; + return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); +} Color Animation::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; @@ -680,10 +729,16 @@ Color Animation::get_color_pixel(int x, int y) const { if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) return Color::BLACK; const uint32_t pos = (x + y * this->width_ + frame_index) * 3; - const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | - (progmem_read_byte(this->data_start_ + pos + 1) << 8) | - (progmem_read_byte(this->data_start_ + pos + 0) << 16); - return Color(color32); + Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2)); + if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { + // (0, 0, 1) has been defined as transparent color for non-alpha images. + // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) + color.w = 0; + } else { + color.w = 0xFF; + } + return color; } Color Animation::get_rgb565_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) @@ -697,7 +752,14 @@ Color Animation::get_rgb565_pixel(int x, int y) const { auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; - return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); + Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); + if (rgb565 == 0x0020 && transparent_) { + // darkest green has been defined as transparent color for transparent RGB565 images. + color.w = 0; + } else { + color.w = 0xFF; + } + return color; } Color Animation::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) @@ -707,7 +769,8 @@ Color Animation::get_grayscale_pixel(int x, int y) const { return Color::BLACK; const uint32_t pos = (x + y * this->width_ + frame_index); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - return Color(gray | gray << 8 | gray << 16 | gray << 24); + uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; + return Color(gray, gray, gray, alpha); } Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {} diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 4477685e1b..a8ec0e588f 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -82,8 +82,8 @@ enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2, - IMAGE_TYPE_TRANSPARENT_BINARY = 3, - IMAGE_TYPE_RGB565 = 4, + IMAGE_TYPE_RGB565 = 3, + IMAGE_TYPE_RGBA = 4, }; enum DisplayType { @@ -540,6 +540,7 @@ class Image { Image(const uint8_t *data_start, int width, int height, ImageType type); virtual bool get_pixel(int x, int y) const; virtual Color get_color_pixel(int x, int y) const; + virtual Color get_rgba_pixel(int x, int y) const; virtual Color get_rgb565_pixel(int x, int y) const; virtual Color get_grayscale_pixel(int x, int y) const; int get_width() const; @@ -548,11 +549,15 @@ class Image { virtual int get_current_frame() const; + void set_transparency(bool transparent) { transparent_ = transparent; } + bool has_transparency() const { return transparent_; } + protected: int width_; int height_; ImageType type_; const uint8_t *data_start_; + bool transparent_; }; class Animation : public Image { @@ -560,6 +565,7 @@ class Animation : public Image { Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); bool get_pixel(int x, int y) const override; Color get_color_pixel(int x, int y) const override; + Color get_rgba_pixel(int x, int y) const override; Color get_rgb565_pixel(int x, int y) const override; Color get_grayscale_pixel(int x, int y) const override; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 88c625961b..4260636fa9 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -22,26 +22,55 @@ MULTI_CONF = True ImageType = display.display_ns.enum("ImageType") IMAGE_TYPE = { "BINARY": ImageType.IMAGE_TYPE_BINARY, + "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, - "RGB24": ImageType.IMAGE_TYPE_RGB24, - "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, "RGB565": ImageType.IMAGE_TYPE_RGB565, - "TRANSPARENT_IMAGE": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, + "RGB24": ImageType.IMAGE_TYPE_RGB24, + "RGBA": ImageType.IMAGE_TYPE_RGBA, } +CONF_USE_TRANSPARENCY = "use_transparency" + Image_ = display.display_ns.class_("Image") + +def validate_cross_dependencies(config): + """ + Validate fields whose possible values depend on other fields. + For example, validate that explicitly transparent image types + have "use_transparency" set to True. + Also set the default value for those kind of dependent fields. + """ + image_type = config[CONF_TYPE] + is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] + + # If the use_transparency option was not specified, set the default depending on the image type + if CONF_USE_TRANSPARENCY not in config: + config[CONF_USE_TRANSPARENCY] = is_transparent_type + + if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: + raise cv.Invalid(f"Image type {image_type} must always be transparent.") + + return config + + IMAGE_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.file_, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), - cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( - "NONE", "FLOYDSTEINBERG", upper=True - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - } + cv.All( + { + cv.Required(CONF_ID): cv.declare_id(Image_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the image type, + # and thus will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, + cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( + "NONE", "FLOYDSTEINBERG", upper=True + ), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + }, + validate_cross_dependencies, + ) ) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) @@ -64,72 +93,113 @@ async def to_code(config): else: if width > 500 or height > 500: _LOGGER.warning( - "The image you requested is very big. Please consider using" - " the resize parameter." + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, ) + transparent = config[CONF_USE_TRANSPARENCY] + dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG if config[CONF_TYPE] == "GRAYSCALE": - image = image.convert("L", dither=dither) + image = image.convert("LA", dither=dither) pixels = list(image.getdata()) data = [0 for _ in range(height * width)] pos = 0 - for pix in pixels: - data[pos] = pix + for g, a in pixels: + if transparent: + if g == 1: + g = 0 + if a < 0x80: + g = 1 + + data[pos] = g + pos += 1 + + elif config[CONF_TYPE] == "RGBA": + image = image.convert("RGBA") + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 4)] + pos = 0 + for r, g, b, a in pixels: + data[pos] = r + pos += 1 + data[pos] = g + pos += 1 + data[pos] = b + pos += 1 + data[pos] = a pos += 1 elif config[CONF_TYPE] == "RGB24": - image = image.convert("RGB") + image = image.convert("RGBA") pixels = list(image.getdata()) data = [0 for _ in range(height * width * 3)] pos = 0 - for pix in pixels: - data[pos] = pix[0] + for r, g, b, a in pixels: + if transparent: + if r == 0 and g == 0 and b == 1: + b = 0 + if a < 0x80: + r = 0 + g = 0 + b = 1 + + data[pos] = r pos += 1 - data[pos] = pix[1] + data[pos] = g pos += 1 - data[pos] = pix[2] + data[pos] = b pos += 1 - elif config[CONF_TYPE] == "RGB565": - image = image.convert("RGB") + elif config[CONF_TYPE] in ["RGB565"]: + image = image.convert("RGBA") pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 3)] + data = [0 for _ in range(height * width * 2)] pos = 0 - for pix in pixels: - R = pix[0] >> 3 - G = pix[1] >> 2 - B = pix[2] >> 3 + for r, g, b, a in pixels: + R = r >> 3 + G = g >> 2 + B = b >> 3 rgb = (R << 11) | (G << 5) | B + + if transparent: + if rgb == 0x0020: + rgb = 0 + if a < 0x80: + rgb = 0x0020 + data[pos] = rgb >> 8 pos += 1 - data[pos] = rgb & 255 + data[pos] = rgb & 0xFF pos += 1 - elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"): + elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: + if transparent: + alpha = image.split()[-1] + has_alpha = alpha.getextrema()[0] < 0xFF + _LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha) image = image.convert("1", dither=dither) width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] for y in range(height): for x in range(width): - if image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) - - elif config[CONF_TYPE] == "TRANSPARENT_IMAGE": - image = image.convert("RGBA") - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if not image.getpixel((x, y))[3]: + if transparent and has_alpha: + a = alpha.getpixel((x, y)) + if not a: + continue + elif image.getpixel((x, y)): continue pos = x + y * width8 data[pos // 8] |= 0x80 >> (pos % 8) + else: + raise core.EsphomeError( + f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}." + ) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable( + var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] ) + cg.add(var.set_transparency(transparent)) diff --git a/script/ci-custom.py b/script/ci-custom.py index 20f607f987..44ed83f392 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -66,6 +66,7 @@ file_types = ( ".txt", ".ico", ".svg", + ".png", ".py", ".html", ".js", @@ -80,7 +81,7 @@ file_types = ( "", ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") -ignore_types = (".ico", ".woff", ".woff2", "") +ignore_types = (".ico", ".png", ".woff", ".woff2", "") LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] diff --git a/tests/pnglogo.png b/tests/pnglogo.png new file mode 100644 index 0000000000000000000000000000000000000000..bd2fd547833635ab465a796c9a936dcfdf4b3086 GIT binary patch literal 685 zcmV;e0#f~nP)L}=x{JFG>CwJ5D;t#m}DrRq)32-m{@Rt7*HUfm=IXNV4$eL@SH$^Fd$%z zXjoi`ptN`(B!GB`_=uRmD2yPGoKSFNfG~g{NN8A)6cAu&aBz5tz+h+?v>2eMc$h38 zh?J<9ps=W{IB0lqsBAEBj1W*HOPAS56LM;hJ$00DtXL_t(2&#jVOQxicDMK?xJC#$kXC61eE z6`~?!1rcMW6~E$=i|7CU9v?PBQe~Ad_RHLwI(2VV_bvFx6#oSfDDRv@7RX2~?N{N(R%R&zd#e0wv(eX6D$m5o=GAMo^6d3z%Q42eiWHL=s66(FHVl1vh@ zdrbP{5G*WCPwlS1d)wa8-m~yf*L6s!a$=Nc`=WEBx z8I|OWAmdKXc)1oaghWmtI0r$31bGE0NI~q2eOHhcQXqwmK5$jDb*=T%ioeZobIrF8 T1OmU^00000NkvXXu0mjf