mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add transparency support to all image types (#4600)
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | ||||
| # Normalize line endings to LF in the repository | ||||
| * text eol=lf | ||||
| *.png binary | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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) {} | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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 = [] | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								tests/pnglogo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/pnglogo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 685 B | 
| @@ -659,6 +659,27 @@ interval: | ||||
|  | ||||
| display: | ||||
|  | ||||
| image: | ||||
|   - id: binary_image | ||||
|     file: pnglogo.png | ||||
|     type: BINARY | ||||
|     dither: FloydSteinberg | ||||
|   - id: transparent_transparent_image | ||||
|     file: pnglogo.png | ||||
|     type: TRANSPARENT_BINARY | ||||
|   - id: rgba_image | ||||
|     file: pnglogo.png | ||||
|     type: RGBA | ||||
|     resize: 50x50 | ||||
|   - id: rgb24_image | ||||
|     file: pnglogo.png | ||||
|     type: RGB24 | ||||
|     use_transparency: yes | ||||
|   - id: rgb565_image | ||||
|     file: pnglogo.png | ||||
|     type: RGB565 | ||||
|     use_transparency: no | ||||
|  | ||||
| cap1188: | ||||
|   id: cap1188_component | ||||
|   address: 0x29 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user