Add transparency support to all image types (#4600)

This commit is contained in:
guillempages 2023-05-21 22:03:21 +02:00 committed by GitHub
parent c61a3bf431
commit 8a518f0def
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 359 additions and 110 deletions

1
.gitattributes vendored
View file

@ -1,2 +1,3 @@
# Normalize line endings to LF in the repository # Normalize line endings to LF in the repository
* text eol=lf * text eol=lf
*.png binary

View file

@ -3,6 +3,7 @@ import logging
from esphome import core from esphome import core
from esphome.components import display, font from esphome.components import display, font
import esphome.components.image as espImage import esphome.components.image as espImage
from esphome.components.image import CONF_USE_TRANSPARENCY
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE
@ -15,7 +16,28 @@ MULTI_CONF = True
Animation_ = display.display_ns.class_("Animation", espImage.Image_) 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( ANIMATION_SCHEMA = cv.Schema(
cv.All(
{ {
cv.Required(CONF_ID): cv.declare_id(Animation_), cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): cv.file_, cv.Required(CONF_FILE): cv.file_,
@ -23,8 +45,13 @@ ANIMATION_SCHEMA = cv.Schema(
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
espImage.IMAGE_TYPE, upper=True 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), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
} },
validate_cross_dependencies,
)
) )
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
@ -50,16 +77,19 @@ async def to_code(config):
else: else:
if width > 500 or height > 500: if width > 500 or height > 500:
_LOGGER.warning( _LOGGER.warning(
"The image you requested is very big. Please consider using" 'The image "%s" you requested is very big. Please consider'
" the resize parameter." " using the resize parameter.",
path,
) )
transparent = config[CONF_USE_TRANSPARENCY]
if config[CONF_TYPE] == "GRAYSCALE": if config[CONF_TYPE] == "GRAYSCALE":
data = [0 for _ in range(height * width * frames)] data = [0 for _ in range(height * width * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
frame = image.convert("L", dither=Image.NONE) frame = image.convert("LA", dither=Image.NONE)
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
pixels = list(frame.getdata()) pixels = list(frame.getdata())
@ -67,16 +97,22 @@ async def to_code(config):
raise core.EsphomeError( raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" 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 data[pos] = pix
pos += 1 pos += 1
elif config[CONF_TYPE] == "RGB24": elif config[CONF_TYPE] == "RGBA":
data = [0 for _ in range(height * width * 3 * frames)] data = [0 for _ in range(height * width * 4 * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
frame = image.convert("RGB") frame = image.convert("RGBA")
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
pixels = list(frame.getdata()) pixels = list(frame.getdata())
@ -91,13 +127,15 @@ async def to_code(config):
pos += 1 pos += 1
data[pos] = pix[2] data[pos] = pix[2]
pos += 1 pos += 1
data[pos] = pix[3]
pos += 1
elif config[CONF_TYPE] == "RGB565": elif config[CONF_TYPE] == "RGB24":
data = [0 for _ in range(height * width * 2 * frames)] data = [0 for _ in range(height * width * 3 * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
frame = image.convert("RGB") frame = image.convert("RGBA")
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
pixels = list(frame.getdata()) pixels = list(frame.getdata())
@ -105,14 +143,50 @@ async def to_code(config):
raise core.EsphomeError( raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
) )
for pix in pixels: for r, g, b, a in pixels:
R = pix[0] >> 3 if transparent:
G = pix[1] >> 2 if r == 0 and g == 0 and b == 1:
B = pix[2] >> 3 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 rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8 data[pos] = rgb >> 8
pos += 1 pos += 1
data[pos] = rgb & 255 data[pos] = rgb & 0xFF
pos += 1 pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: 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)] data = [0 for _ in range((height * width8 // 8) * frames)]
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
frame = image.convert("1", dither=Image.NONE) frame = image.convert("1", dither=Image.NONE)
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
for y in range(height): if transparent:
for x in range(width): alpha = alpha.resize([width, height])
if frame.getpixel((x, y)): 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 continue
elif frame.getpixel((x, y)):
continue
pos = x + y * width8 + (height * width8 * frameIndex) pos = x + y * width8 + (height * width8 * frameIndex)
data[pos // 8] |= 0x80 >> (pos % 8) 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] rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.new_Pvariable( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
prog_arr, prog_arr,
width, width,
@ -140,3 +226,4 @@ async def to_code(config):
frames, frames,
espImage.IMAGE_TYPE[config[CONF_TYPE]], espImage.IMAGE_TYPE[config[CONF_TYPE]],
) )
cg.add(var.set_transparency(transparent))

View file

@ -12,7 +12,7 @@ namespace display {
static const char *const TAG = "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); const Color COLOR_ON(255, 255, 255, 255);
void Rect::expand(int16_t horizontal, int16_t vertical) { 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) { void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) {
bool transparent = image->has_transparency();
switch (image->get_type()) { 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_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) { 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; break;
}
case IMAGE_TYPE_GRAYSCALE: case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) { 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)); 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_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);
}
} }
break; break;
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) { 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; break;
@ -629,14 +647,27 @@ bool Image::get_pixel(int x, int y) const {
const uint32_t pos = x + y * width_8; const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); 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 { Color Image::get_color_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_) * 3; const uint32_t pos = (x + y * this->width_) * 3;
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | 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 + 1) << 8) | progmem_read_byte(this->data_start_ + pos + 2));
(progmem_read_byte(this->data_start_ + pos + 0) << 16); if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
return Color(color32); // (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 { Color Image::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) 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 r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5; auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F; 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 { Color Image::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_); const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); 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_width() const { return this->width_; }
int Image::get_height() const { return this->height_; } 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; const uint32_t pos = x + y * width_8 + frame_index;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); 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 { Color Animation::get_color_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK; 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_)) if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index) * 3; const uint32_t pos = (x + y * this->width_ + frame_index) * 3;
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | 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 + 1) << 8) | progmem_read_byte(this->data_start_ + pos + 2));
(progmem_read_byte(this->data_start_ + pos + 0) << 16); if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
return Color(color32); // (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 { Color Animation::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) 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 r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5; auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F; 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 { Color Animation::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) 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; return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index); const uint32_t pos = (x + y * this->width_ + frame_index);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); 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) 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) {} : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {}

View file

@ -82,8 +82,8 @@ enum ImageType {
IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2, IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_TRANSPARENT_BINARY = 3, IMAGE_TYPE_RGB565 = 3,
IMAGE_TYPE_RGB565 = 4, IMAGE_TYPE_RGBA = 4,
}; };
enum DisplayType { enum DisplayType {
@ -540,6 +540,7 @@ class Image {
Image(const uint8_t *data_start, int width, int height, ImageType type); Image(const uint8_t *data_start, int width, int height, ImageType type);
virtual bool get_pixel(int x, int y) const; virtual bool get_pixel(int x, int y) const;
virtual Color get_color_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_rgb565_pixel(int x, int y) const;
virtual Color get_grayscale_pixel(int x, int y) const; virtual Color get_grayscale_pixel(int x, int y) const;
int get_width() const; int get_width() const;
@ -548,11 +549,15 @@ class Image {
virtual int get_current_frame() const; virtual int get_current_frame() const;
void set_transparency(bool transparent) { transparent_ = transparent; }
bool has_transparency() const { return transparent_; }
protected: protected:
int width_; int width_;
int height_; int height_;
ImageType type_; ImageType type_;
const uint8_t *data_start_; const uint8_t *data_start_;
bool transparent_;
}; };
class Animation : public Image { 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); 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; bool get_pixel(int x, int y) const override;
Color get_color_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_rgb565_pixel(int x, int y) const override;
Color get_grayscale_pixel(int x, int y) const override; Color get_grayscale_pixel(int x, int y) const override;

View file

@ -22,26 +22,55 @@ MULTI_CONF = True
ImageType = display.display_ns.enum("ImageType") ImageType = display.display_ns.enum("ImageType")
IMAGE_TYPE = { IMAGE_TYPE = {
"BINARY": ImageType.IMAGE_TYPE_BINARY, "BINARY": ImageType.IMAGE_TYPE_BINARY,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
"RGB24": ImageType.IMAGE_TYPE_RGB24,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
"RGB565": ImageType.IMAGE_TYPE_RGB565, "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") 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( IMAGE_SCHEMA = cv.Schema(
cv.All(
{ {
cv.Required(CONF_ID): cv.declare_id(Image_), cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.file_, cv.Required(CONF_FILE): cv.file_,
cv.Optional(CONF_RESIZE): cv.dimensions, cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), 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( cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True "NONE", "FLOYDSTEINBERG", upper=True
), ),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
} },
validate_cross_dependencies,
)
) )
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
@ -64,72 +93,113 @@ async def to_code(config):
else: else:
if width > 500 or height > 500: if width > 500 or height > 500:
_LOGGER.warning( _LOGGER.warning(
"The image you requested is very big. Please consider using" 'The image "%s" you requested is very big. Please consider'
" the resize parameter." " using the resize parameter.",
path,
) )
transparent = config[CONF_USE_TRANSPARENCY]
dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG
if config[CONF_TYPE] == "GRAYSCALE": if config[CONF_TYPE] == "GRAYSCALE":
image = image.convert("L", dither=dither) image = image.convert("LA", dither=dither)
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width)] data = [0 for _ in range(height * width)]
pos = 0 pos = 0
for pix in pixels: for g, a in pixels:
data[pos] = pix 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 pos += 1
elif config[CONF_TYPE] == "RGB24": elif config[CONF_TYPE] == "RGB24":
image = image.convert("RGB") image = image.convert("RGBA")
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)] data = [0 for _ in range(height * width * 3)]
pos = 0 pos = 0
for pix in pixels: for r, g, b, a in pixels:
data[pos] = pix[0] 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 pos += 1
data[pos] = pix[1] data[pos] = g
pos += 1 pos += 1
data[pos] = pix[2] data[pos] = b
pos += 1 pos += 1
elif config[CONF_TYPE] == "RGB565": elif config[CONF_TYPE] in ["RGB565"]:
image = image.convert("RGB") image = image.convert("RGBA")
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)] data = [0 for _ in range(height * width * 2)]
pos = 0 pos = 0
for pix in pixels: for r, g, b, a in pixels:
R = pix[0] >> 3 R = r >> 3
G = pix[1] >> 2 G = g >> 2
B = pix[2] >> 3 B = b >> 3
rgb = (R << 11) | (G << 5) | B rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8 data[pos] = rgb >> 8
pos += 1 pos += 1
data[pos] = rgb & 255 data[pos] = rgb & 0xFF
pos += 1 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) image = image.convert("1", dither=dither)
width8 = ((width + 7) // 8) * 8 width8 = ((width + 7) // 8) * 8
data = [0 for _ in range(height * width8 // 8)] data = [0 for _ in range(height * width8 // 8)]
for y in range(height): for y in range(height):
for x in range(width): for x in range(width):
if image.getpixel((x, y)): if transparent and has_alpha:
continue a = alpha.getpixel((x, y))
pos = x + y * width8 if not a:
data[pos // 8] |= 0x80 >> (pos % 8) continue
elif image.getpixel((x, y)):
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]:
continue continue
pos = x + y * width8 pos = x + y * width8
data[pos // 8] |= 0x80 >> (pos % 8) 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] rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) 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]] config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
) )
cg.add(var.set_transparency(transparent))

View file

@ -66,6 +66,7 @@ file_types = (
".txt", ".txt",
".ico", ".ico",
".svg", ".svg",
".png",
".py", ".py",
".html", ".html",
".js", ".js",
@ -80,7 +81,7 @@ file_types = (
"", "",
) )
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
ignore_types = (".ico", ".woff", ".woff2", "") ignore_types = (".ico", ".png", ".woff", ".woff2", "")
LINT_FILE_CHECKS = [] LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = [] LINT_CONTENT_CHECKS = []

BIN
tests/pnglogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

View file

@ -659,6 +659,27 @@ interval:
display: 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: cap1188:
id: cap1188_component id: cap1188_component
address: 0x29 address: 0x29