mirror of
https://github.com/esphome/esphome.git
synced 2024-11-23 07:28:10 +01:00
Add transparency support to all image types (#4600)
This commit is contained in:
parent
c61a3bf431
commit
8a518f0def
8 changed files with 359 additions and 110 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -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
|
||||||
|
|
|
@ -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,16 +16,42 @@ 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_FILE): cv.file_,
|
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
cv.Required(CONF_FILE): cv.file_,
|
||||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
espImage.IMAGE_TYPE, upper=True
|
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
||||||
),
|
espImage.IMAGE_TYPE, upper=True
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
),
|
||||||
}
|
# 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)
|
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
|
||||||
pos = x + y * width8 + (height * width8 * frameIndex)
|
elif frame.getpixel((x, y)):
|
||||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
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]
|
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))
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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_FILE): cv.file_,
|
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
cv.Required(CONF_FILE): cv.file_,
|
||||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
||||||
"NONE", "FLOYDSTEINBERG", 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.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
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)
|
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))
|
||||||
|
|
|
@ -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
BIN
tests/pnglogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 685 B |
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue