1
0
mirror of https://github.com/esphome/esphome.git synced 2025-01-18 20:10:55 +00:00

[image] Transparency changes; code refactor (#7908)

This commit is contained in:
Clyde Stubbs 2025-01-13 14:21:42 +11:00 committed by GitHub
parent aa87c60717
commit f1c0570e3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 845 additions and 787 deletions

View File

@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core

View File

@ -1,28 +1,10 @@
import logging import logging
from esphome import automation, core from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.components.image as espImage import esphome.components.image as espImage
from esphome.components.image import (
CONF_USE_TRANSPARENCY,
LOCAL_SCHEMA,
SOURCE_LOCAL,
SOURCE_WEB,
WEB_SCHEMA,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_ID, CONF_REPEAT
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_REPEAT,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"] CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_LOOP = "loop" CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame" CONF_START_FRAME = "start_frame"
@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
) )
TYPED_FILE_SCHEMA = cv.typed_schema( CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
{ {
SOURCE_LOCAL: LOCAL_SCHEMA, cv.Required(CONF_ID): cv.declare_id(Animation_),
SOURCE_WEB: WEB_SCHEMA, cv.Optional(CONF_LOOP): cv.All(
},
key=CONF_SOURCE,
)
def _file_schema(value):
if isinstance(value, str):
return validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
FILE_SCHEMA = cv.Schema(_file_schema)
def validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA(
{ {
CONF_SOURCE: SOURCE_WEB, cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
CONF_URL: value, cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
} }
) ),
return FILE_SCHEMA( },
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
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.All(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Required(CONF_FILE): FILE_SCHEMA,
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.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
},
validate_cross_dependencies,
)
) )
CONFIG_SCHEMA = ANIMATION_SCHEMA
NEXT_FRAME_SCHEMA = automation.maybe_simple_id( NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{ {
@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args):
async def to_code(config): async def to_code(config):
from PIL import Image (
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await espImage.write_image(config, all_frames=True)
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = espImage.compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
try:
image = Image.open(path)
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}")
width, height = image.size
frames = image.n_frames
if CONF_RESIZE in config:
new_width_max, new_height_max = config[CONF_RESIZE]
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
elif width > 500 or height > 500:
_LOGGER.warning(
'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("LA", dither=Image.Dither.NONE)
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 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] == "RGBA":
data = [0 for _ in range(height * width * 4 * 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 pix in pixels:
data[pos] = pix[0]
pos += 1
data[pos] = pix[1]
pos += 1
data[pos] = pix[2]
pos += 1
data[pos] = pix[3]
pos += 1
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("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:
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"]:
bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel * 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
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 0xFF
pos += 1
if transparent:
data[pos] = a
pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
width8 = ((width + 7) // 8) * 8
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
else:
has_alpha = False
frame = image.convert("1", dither=Image.Dither.NONE)
if CONF_RESIZE in config:
frame = frame.resize([width, height])
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
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)
var = cg.new_Pvariable( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
prog_arr, prog_arr,
width, width,
height, height,
frames, frame_count,
espImage.IMAGE_TYPE[config[CONF_TYPE]], image_type,
trans_value,
) )
cg.add(var.set_transparency(transparent))
if loop_config := config.get(CONF_LOOP): if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME] start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frames) end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1) count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count)) cg.add(var.set_loop(start, end, count))

View File

@ -6,8 +6,8 @@ namespace esphome {
namespace animation { namespace animation {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
image::ImageType type) image::ImageType type, image::Transparency transparent)
: Image(data_start, width, height, type), : Image(data_start, width, height, type, transparent),
animation_data_start_(data_start), animation_data_start_(data_start),
current_frame_(0), current_frame_(0),
animation_frame_count_(animation_frame_count), animation_frame_count_(animation_frame_count),

View File

@ -8,7 +8,8 @@ namespace animation {
class Animation : public image::Image { class Animation : public image::Image {
public: public:
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
image::Transparency transparent);
uint32_t get_animation_frame_count() const; uint32_t get_animation_frame_count() const;
int get_current_frame() const; int get_current_frame() const;

View File

@ -6,7 +6,7 @@ import logging
from pathlib import Path from pathlib import Path
import re import re
import puremagic from PIL import Image, UnidentifiedImageError
from esphome import core, external_files from esphome import core, external_files
import esphome.codegen as cg import esphome.codegen as cg
@ -29,21 +29,236 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "image" DOMAIN = "image"
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
image_ns = cg.esphome_ns.namespace("image") image_ns = cg.esphome_ns.namespace("image")
ImageType = image_ns.enum("ImageType") ImageType = image_ns.enum("ImageType")
CONF_OPAQUE = "opaque"
CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
CONF_CHROMA_KEY,
CONF_ALPHA_CHANNEL,
)
def get_image_type_enum(type):
return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}")
def get_transparency_enum(transparency):
return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}")
class ImageEncoder:
"""
Superclass of image type encoders
"""
# Control which transparency options are available for a given type
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
# All imageencoder types are valid
@staticmethod
def validate(value):
return value
def __init__(self, width, height, transparency, dither, invert_alpha):
"""
:param width: The image width in pixels
:param height: The image height in pixels
:param transparency: Transparency type
:param dither: Dither method
:param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours.
"""
self.transparency = transparency
self.width = width
self.height = height
self.data = [0 for _ in range(width * height)]
self.dither = dither
self.index = 0
self.invert_alpha = invert_alpha
def convert(self, image):
"""
Convert the image format
:param image: Input image
:return: converted image
"""
return image
def encode(self, pixel):
"""
Encode a single pixel
"""
def end_row(self):
"""
Marks the end of a pixel row
:return:
"""
class ImageBinary(ImageEncoder):
allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY}
def __init__(self, width, height, transparency, dither, invert_alpha):
self.width8 = (width + 7) // 8
super().__init__(self.width8, height, transparency, dither, invert_alpha)
self.bitno = 0
def convert(self, image):
return image.convert("1", dither=self.dither)
def encode(self, pixel):
if self.invert_alpha:
pixel = not pixel
if pixel:
self.data[self.index] |= 0x80 >> (self.bitno % 8)
self.bitno += 1
if self.bitno == 8:
self.bitno = 0
self.index += 1
def end_row(self):
"""
Pad rows to a byte boundary
"""
if self.bitno != 0:
self.bitno = 0
self.index += 1
class ImageGrayscale(ImageEncoder):
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE}
def convert(self, image):
return image.convert("LA")
def encode(self, pixel):
b, a = pixel
if self.transparency == CONF_CHROMA_KEY:
if b == 1:
b = 0
if a != 0xFF:
b = 1
if self.invert_alpha:
b ^= 0xFF
if self.transparency == CONF_ALPHA_CHANNEL:
if a != 0xFF:
b = a
self.data[self.index] = b
self.index += 1
class ImageRGB565(ImageEncoder):
def __init__(self, width, height, transparency, dither, invert_alpha):
stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2
super().__init__(
width * stride,
height,
transparency,
dither,
invert_alpha,
)
def convert(self, image):
return image.convert("RGBA")
def encode(self, pixel):
r, g, b, a = pixel
r = r >> 3
g = g >> 2
b = b >> 3
if self.transparency == CONF_CHROMA_KEY:
if r == 0 and g == 1 and b == 0:
g = 0
elif a < 128:
r = 0
g = 1
b = 0
rgb = (r << 11) | (g << 5) | b
self.data[self.index] = rgb >> 8
self.index += 1
self.data[self.index] = rgb & 0xFF
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
a ^= 0xFF
self.data[self.index] = a
self.index += 1
class ImageRGB(ImageEncoder):
def __init__(self, width, height, transparency, dither, invert_alpha):
stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3
super().__init__(
width * stride,
height,
transparency,
dither,
invert_alpha,
)
def convert(self, image):
return image.convert("RGBA")
def encode(self, pixel):
r, g, b, a = pixel
if self.transparency == CONF_CHROMA_KEY:
if r == 0 and g == 1 and b == 0:
g = 0
elif a < 128:
r = 0
g = 1
b = 0
self.data[self.index] = r
self.index += 1
self.data[self.index] = g
self.index += 1
self.data[self.index] = b
self.index += 1
if self.transparency == CONF_ALPHA_CHANNEL:
if self.invert_alpha:
a ^= 0xFF
self.data[self.index] = a
self.index += 1
class ReplaceWith:
"""
Placeholder class to provide feedback on deprecated features
"""
allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE}
def __init__(self, replace_with):
self.replace_with = replace_with
def validate(self, value):
raise cv.Invalid(
f"Image type {value} is removed; replace with {self.replace_with}"
)
IMAGE_TYPE = { IMAGE_TYPE = {
"BINARY": ImageType.IMAGE_TYPE_BINARY, "BINARY": ImageBinary,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, "GRAYSCALE": ImageGrayscale,
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, "RGB565": ImageRGB565,
"RGB565": ImageType.IMAGE_TYPE_RGB565, "RGB": ImageRGB,
"RGB24": ImageType.IMAGE_TYPE_RGB24, "TRANSPARENT_BINARY": ReplaceWith(
"RGBA": ImageType.IMAGE_TYPE_RGBA, "'type: BINARY' and 'use_transparency: chroma_key'"
),
"RGB24": ReplaceWith("'type: RGB'"),
"RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"),
} }
TransparencyType = image_ns.enum("TransparencyType")
CONF_USE_TRANSPARENCY = "use_transparency" CONF_USE_TRANSPARENCY = "use_transparency"
# If the MDI file cannot be downloaded within this time, abort. # If the MDI file cannot be downloaded within this time, abort.
@ -53,17 +268,11 @@ SOURCE_LOCAL = "local"
SOURCE_MDI = "mdi" SOURCE_MDI = "mdi"
SOURCE_WEB = "web" SOURCE_WEB = "web"
Image_ = image_ns.class_("Image") Image_ = image_ns.class_("Image")
def _compute_local_icon_path(value: dict) -> Path: def compute_local_image_path(value) -> Path:
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" url = value[CONF_URL] if isinstance(value, dict) else value
return base_dir / f"{value[CONF_ICON]}.svg"
def compute_local_image_path(value: dict) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256") h = hashlib.new("sha256")
h.update(url.encode()) h.update(url.encode())
key = h.hexdigest()[:8] key = h.hexdigest()[:8]
@ -71,30 +280,38 @@ def compute_local_image_path(value: dict) -> Path:
return base_dir / key return base_dir / key
def download_mdi(value): def local_path(value):
validate_cairosvg_installed(value) value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
mdi_id = value[CONF_ICON]
path = _compute_local_icon_path(value) def download_file(url, path):
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return str(path)
def download_mdi(value):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi"
path = base_dir / f"{mdi_id}.svg"
url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg"
return download_file(url, path)
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
def download_image(value): def download_image(value):
url = value[CONF_URL] value = value[CONF_URL] if isinstance(value, dict) else value
path = compute_local_image_path(value) return download_file(value, compute_local_image_path(value))
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return value
def validate_cairosvg_installed(value): def is_svg_file(file):
"""Validate that cairosvg is installed""" if not file:
return False
with open(file, "rb") as f:
return "<svg " in str(f.read(1024))
def validate_cairosvg_installed():
try: try:
import cairosvg import cairosvg
except ImportError as err: except ImportError as err:
@ -110,73 +327,28 @@ def validate_cairosvg_installed(value):
"(pip install -U cairosvg)" "(pip install -U cairosvg)"
) )
return value
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.
"""
is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI
if CONF_TYPE not in config:
if is_mdi:
config[CONF_TYPE] = "TRANSPARENT_BINARY"
else:
config[CONF_TYPE] = "BINARY"
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.")
if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]:
raise cv.Invalid("MDI images must be binary images.")
return config
def validate_file_shorthand(value): def validate_file_shorthand(value):
value = cv.string_strict(value) value = cv.string_strict(value)
if value.startswith("mdi:"): if value.startswith("mdi:"):
validate_cairosvg_installed(value)
match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value)
if match is None: if match is None:
raise cv.Invalid("Could not parse mdi icon name.") raise cv.Invalid("Could not parse mdi icon name.")
icon = match.group(1) icon = match.group(1)
return FILE_SCHEMA( return download_mdi(icon)
{
CONF_SOURCE: SOURCE_MDI,
CONF_ICON: icon,
}
)
if value.startswith("http://") or value.startswith("https://"): if value.startswith("http://") or value.startswith("https://"):
return FILE_SCHEMA( return download_image(value)
{
CONF_SOURCE: SOURCE_WEB, value = cv.file_(value)
CONF_URL: value, return local_path(value)
}
)
return FILE_SCHEMA(
{
CONF_SOURCE: SOURCE_LOCAL,
CONF_PATH: value,
}
)
LOCAL_SCHEMA = cv.Schema( LOCAL_SCHEMA = cv.All(
{ {
cv.Required(CONF_PATH): cv.file_, cv.Required(CONF_PATH): cv.file_,
} },
local_path,
) )
MDI_SCHEMA = cv.All( MDI_SCHEMA = cv.All(
@ -203,205 +375,202 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
) )
def _file_schema(value): def validate_transparency(choices=TRANSPARENCY_TYPES):
if isinstance(value, str): def validate(value):
return validate_file_shorthand(value) if isinstance(value, bool):
return TYPED_FILE_SCHEMA(value) value = str(value)
return cv.one_of(*choices, lower=True)(value)
return validate
FILE_SCHEMA = cv.Schema(_file_schema) def validate_type(image_types):
def validate(value):
value = cv.one_of(*image_types, upper=True)(value)
return IMAGE_TYPE[value].validate(value)
IMAGE_SCHEMA = cv.Schema( return validate
cv.All(
{
cv.Required(CONF_ID): cv.declare_id(Image_), def validate_settings(value):
cv.Required(CONF_FILE): FILE_SCHEMA, type = value[CONF_TYPE]
cv.Optional(CONF_RESIZE): cv.dimensions, transparency = value[CONF_USE_TRANSPARENCY].lower()
# Not setting default here on purpose; the default depends on the source type allow_config = IMAGE_TYPE[type].allow_config
# (file or mdi), and will be set in the "validate_cross_dependencies" validator. if transparency not in allow_config:
cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True), raise cv.Invalid(
# Not setting default here on purpose; the default depends on the image type, f"Image format '{type}' cannot have transparency: {transparency}"
# and thus will be set in the "validate_cross_dependencies" validator. )
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, invert_alpha = value.get(CONF_INVERT_ALPHA, False)
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( if (
"NONE", "FLOYDSTEINBERG", upper=True invert_alpha
), and transparency != CONF_ALPHA_CHANNEL
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), and CONF_INVERT_ALPHA not in allow_config
}, ):
validate_cross_dependencies, raise cv.Invalid("No alpha channel to invert")
) if file := value.get(CONF_FILE):
file = Path(file)
if is_svg_file(file):
validate_cairosvg_installed()
else:
try:
Image.open(file)
except UnidentifiedImageError as exc:
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
return value
BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
).add_extra(validate_settings)
IMAGE_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
cv.Optional(
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
): validate_transparency(),
}
) )
CONFIG_SCHEMA = IMAGE_SCHEMA
def typed_image_schema(image_type):
"""
Construct a schema for a specific image type, allowing transparency options
"""
return cv.Any(
cv.Schema(
{
cv.Optional(t.lower()): cv.ensure_list(
BASE_SCHEMA.extend(
{
cv.Optional(
CONF_USE_TRANSPARENCY, default=t
): validate_transparency((t,)),
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
)
)
for t in IMAGE_TYPE[image_type].allow_config.intersection(
TRANSPARENCY_TYPES
)
}
),
# Allow a default configuration with no transparency preselected
cv.ensure_list(
BASE_SCHEMA.extend(
{
cv.Optional(
CONF_USE_TRANSPARENCY, default=CONF_OPAQUE
): validate_transparency(),
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
)
),
)
def load_svg_image(file: bytes, resize: tuple[int, int]): # The config schema can be a (possibly empty) single list of images,
# Local imports only to allow "validate_pillow_installed" to run *before* importing it # or a dictionary of image types each with a list of images
# cairosvg is only needed in case of SVG images; adding it CONFIG_SCHEMA = cv.Any(
# to the top would force configurations not using SVG to also have it cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
# installed for no reason. cv.ensure_list(IMAGE_SCHEMA),
from cairosvg import svg2png )
from PIL import Image
if resize:
req_width, req_height = resize
svg_image = svg2png(
file,
output_width=req_width,
output_height=req_height,
)
else:
svg_image = svg2png(file)
return Image.open(io.BytesIO(svg_image))
async def to_code(config): async def write_image(config, all_frames=False):
# Local import only to allow "validate_pillow_installed" to run *before* importing it path = Path(config[CONF_FILE])
from PIL import Image if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
conf_file = config[CONF_FILE]
if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif conf_file[CONF_SOURCE] == SOURCE_MDI:
path = _compute_local_icon_path(conf_file).as_posix()
elif conf_file[CONF_SOURCE] == SOURCE_WEB:
path = compute_local_image_path(conf_file).as_posix()
else:
raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}")
try:
with open(path, "rb") as f:
file_contents = f.read()
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}")
file_type = puremagic.from_string(file_contents, mime=True)
resize = config.get(CONF_RESIZE) resize = config.get(CONF_RESIZE)
if "svg" in file_type: if is_svg_file(path):
image = load_svg_image(file_contents, resize) # Local import so use of non-SVG files needn't require cairosvg installed
from cairosvg import svg2png
if not resize:
resize = (None, None)
with open(path, "rb") as file:
image = svg2png(
file_obj=file,
output_width=resize[0],
output_height=resize[1],
)
image = Image.open(io.BytesIO(image))
width, height = image.size
else: else:
image = Image.open(io.BytesIO(file_contents)) image = Image.open(path)
width, height = image.size
if resize: if resize:
image.thumbnail(resize) # Preserve aspect ratio
new_width_max = min(width, resize[0])
new_height_max = min(height, resize[1])
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
width, height = image.size if not resize and (width > 500 or height > 500):
if CONF_RESIZE not in config and (width > 500 or height > 500):
_LOGGER.warning( _LOGGER.warning(
'The image "%s" you requested is very big. Please consider' 'The image "%s" you requested is very big. Please consider'
" using the resize parameter.", " using the resize parameter.",
path, path,
) )
transparent = config[CONF_USE_TRANSPARENCY]
dither = ( dither = (
Image.Dither.NONE Image.Dither.NONE
if config[CONF_DITHER] == "NONE" if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG else Image.Dither.FLOYDSTEINBERG
) )
if config[CONF_TYPE] == "GRAYSCALE": type = config[CONF_TYPE]
image = image.convert("LA", dither=dither) transparency = config[CONF_USE_TRANSPARENCY]
pixels = list(image.getdata()) invert_alpha = config[CONF_INVERT_ALPHA]
data = [0 for _ in range(height * width)] frame_count = 1
pos = 0 if all_frames:
for g, a in pixels: try:
if transparent: frame_count = image.n_frames
if g == 1: except AttributeError:
g = 0 pass
if a < 0x80: if frame_count <= 1:
g = 1 _LOGGER.warning("Image file %s has no animation frames", path)
data[pos] = g total_rows = height * frame_count
pos += 1 encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
for frame_index in range(frame_count):
image.seek(frame_index)
pixels = encoder.convert(image.resize((width, height))).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
elif config[CONF_TYPE] == "RGBA": rhs = [HexInt(x) for x in encoder.data]
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("RGBA")
pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)]
pos = 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] = g
pos += 1
data[pos] = b
pos += 1
elif config[CONF_TYPE] in ["RGB565"]:
image = image.convert("RGBA")
pixels = list(image.getdata())
bytes_per_pixel = 3 if transparent else 2
data = [0 for _ in range(height * width * bytes_per_pixel)]
pos = 0
for r, g, b, a in pixels:
R = r >> 3
G = g >> 2
B = b >> 3
rgb = (R << 11) | (G << 5) | B
data[pos] = rgb >> 8
pos += 1
data[pos] = rgb & 0xFF
pos += 1
if transparent:
data[pos] = a
pos += 1
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 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) prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
var = cg.new_Pvariable( image_type = get_image_type_enum(type)
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] trans_value = get_transparency_enum(transparency)
)
cg.add(var.set_transparency(transparent)) return prog_arr, width, height, image_type, trans_value, frame_count
async def to_code(config):
if isinstance(config, list):
for entry in config:
await to_code(entry)
elif CONF_ID not in config:
for entry in config.values():
await to_code(entry)
else:
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
cg.new_Pvariable(
config[CONF_ID], prog_arr, width, height, image_type, trans_value
)

View File

@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = 0; img_y < height_; img_y++) {
if (this->get_binary_pixel_(img_x, img_y)) { if (this->get_binary_pixel_(img_x, img_y)) {
display->draw_pixel_at(x + img_x, y + img_y, color_on); display->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!this->transparent_) { } else if (!this->transparency_) {
display->draw_pixel_at(x + img_x, y + img_y, color_off); display->draw_pixel_at(x + img_x, y + img_y, color_off);
} }
} }
@ -39,20 +39,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
} }
} }
break; break;
case IMAGE_TYPE_RGB24: case IMAGE_TYPE_RGB:
for (int img_x = 0; img_x < width_; img_x++) { for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) { for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_pixel_(img_x, img_y); auto color = this->get_rgb_pixel_(img_x, img_y);
if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGBA:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgba_pixel_(img_x, img_y);
if (color.w >= 0x80) { if (color.w >= 0x80) {
display->draw_pixel_at(x + img_x, y + img_y, color); display->draw_pixel_at(x + img_x, y + img_y, color);
} }
@ -61,20 +51,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
break; break;
} }
} }
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) 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_off; return color_off;
switch (this->type_) { switch (this->type_) {
case IMAGE_TYPE_BINARY: case IMAGE_TYPE_BINARY:
return this->get_binary_pixel_(x, y) ? color_on : color_off; if (this->get_binary_pixel_(x, y))
return color_on;
return color_off;
case IMAGE_TYPE_GRAYSCALE: case IMAGE_TYPE_GRAYSCALE:
return this->get_grayscale_pixel_(x, y); return this->get_grayscale_pixel_(x, y);
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
return this->get_rgb565_pixel_(x, y); return this->get_rgb565_pixel_(x, y);
case IMAGE_TYPE_RGB24: case IMAGE_TYPE_RGB:
return this->get_rgb24_pixel_(x, y); return this->get_rgb_pixel_(x, y);
case IMAGE_TYPE_RGBA:
return this->get_rgba_pixel_(x, y);
default: default:
return color_off; return color_off;
} }
@ -98,23 +88,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT;
break; break;
case IMAGE_TYPE_RGB24: case IMAGE_TYPE_RGB:
this->dsc_.header.cf = LV_IMG_CF_RGB888; #if LV_COLOR_DEPTH == 32
switch (this->transparent_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
case TRANSPARENCY_CHROMA_KEY:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
break;
default:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
break;
}
#else
this->dsc_.header.cf =
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888;
#endif
break; break;
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
#if LV_COLOR_DEPTH == 16 #if LV_COLOR_DEPTH == 16
this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR; switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
case TRANSPARENCY_CHROMA_KEY:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED;
break;
default:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
break;
}
#else #else
this->dsc_.header.cf = LV_IMG_CF_RGB565; this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
#endif
break;
case IMAGE_TYPE_RGBA:
#if LV_COLOR_DEPTH == 32
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR;
#else
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
#endif #endif
break; break;
} }
@ -128,51 +135,73 @@ bool Image::get_binary_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 { Color Image::get_rgb_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 4; const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
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_rgb24_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 3;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), 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)); progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
// (0, 0, 1) has been defined as transparent color for non-alpha images. switch (this->transparency_) {
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) case TRANSPARENCY_CHROMA_KEY:
color.w = 0; if (color.g == 1 && color.r == 0 && color.b == 0) {
} else { // (0, 1, 0) has been defined as transparent color for non-alpha images.
color.w = 0xFF; color.w = 0;
}
break;
case TRANSPARENCY_ALPHA_CHANNEL:
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
break;
default:
break;
} }
return color; return color;
} }
Color Image::get_rgb565_pixel_(int x, int y) const { Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_; const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
if (this->transparent_) {
pos += (x + y * this->width_) * 3;
} else {
pos += (x + y * this->width_) * 2;
}
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
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;
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF; auto a = 0xFF;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); switch (this->transparency_) {
return color; case TRANSPARENCY_ALPHA_CHANNEL:
a = progmem_read_byte(pos + 2);
break;
case TRANSPARENCY_CHROMA_KEY:
if (rgb565 == 0x0020)
a = 0;
break;
default:
break;
}
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
} }
Color Image::get_grayscale_pixel_(int x, int y) const { Color Image::get_grayscale_pixel_(int x, int y) const {
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);
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF;
return Color(gray, gray, gray, alpha); 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_; }
ImageType Image::get_type() const { return this->type_; } ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type) Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency)
: width_(width), height_(height), type_(type), data_start_(data_start) {} : width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) {
switch (this->type_) {
case IMAGE_TYPE_BINARY:
this->bpp_ = 1;
break;
case IMAGE_TYPE_GRAYSCALE:
this->bpp_ = 8;
break;
case IMAGE_TYPE_RGB565:
this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16;
break;
case IMAGE_TYPE_RGB:
this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24;
break;
}
}
} // namespace image } // namespace image
} // namespace esphome } // namespace esphome

View File

@ -12,51 +12,40 @@ namespace image {
enum ImageType { enum ImageType {
IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2, IMAGE_TYPE_RGB = 2,
IMAGE_TYPE_RGB565 = 3, IMAGE_TYPE_RGB565 = 3,
IMAGE_TYPE_RGBA = 4, };
enum Transparency {
TRANSPARENCY_OPAQUE = 0,
TRANSPARENCY_CHROMA_KEY = 1,
TRANSPARENCY_ALPHA_CHANNEL = 2,
}; };
class Image : public display::BaseImage { class Image : public display::BaseImage {
public: public:
Image(const uint8_t *data_start, int width, int height, ImageType type); Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency);
Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const;
int get_width() const override; int get_width() const override;
int get_height() const override; int get_height() const override;
const uint8_t *get_data_start() const { return this->data_start_; } const uint8_t *get_data_start() const { return this->data_start_; }
ImageType get_type() const; ImageType get_type() const;
int get_bpp() const { int get_bpp() const { return this->bpp_; }
switch (this->type_) {
case IMAGE_TYPE_BINARY:
return 1;
case IMAGE_TYPE_GRAYSCALE:
return 8;
case IMAGE_TYPE_RGB565:
return this->transparent_ ? 24 : 16;
case IMAGE_TYPE_RGB24:
return 24;
case IMAGE_TYPE_RGBA:
return 32;
}
return 0;
}
/// Return the stride of the image in bytes, that is, the distance in bytes /// Return the stride of the image in bytes, that is, the distance in bytes
/// between two consecutive rows of pixels. /// between two consecutive rows of pixels.
uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; }
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void set_transparency(bool transparent) { transparent_ = transparent; } bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
bool has_transparency() const { return transparent_; }
#ifdef USE_LVGL #ifdef USE_LVGL
lv_img_dsc_t *get_lv_img_dsc(); lv_img_dsc_t *get_lv_img_dsc();
#endif #endif
protected: protected:
bool get_binary_pixel_(int x, int y) const; bool get_binary_pixel_(int x, int y) const;
Color get_rgb24_pixel_(int x, int y) const; Color get_rgb_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_rgb565_pixel_(int x, int y) const; Color get_rgb565_pixel_(int x, int y) const;
Color get_grayscale_pixel_(int x, int y) const; Color get_grayscale_pixel_(int x, int y) const;
@ -64,7 +53,9 @@ class Image : public display::BaseImage {
int height_; int height_;
ImageType type_; ImageType type_;
const uint8_t *data_start_; const uint8_t *data_start_;
bool transparent_; Transparency transparency_;
size_t bpp_{};
size_t stride_{};
#ifdef USE_LVGL #ifdef USE_LVGL
lv_img_dsc_t dsc_{}; lv_img_dsc_t dsc_{};
#endif #endif

View File

@ -4,14 +4,18 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import ( from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_USE_TRANSPARENCY, CONF_USE_TRANSPARENCY,
IMAGE_TYPE, IMAGE_SCHEMA,
Image_, Image_,
validate_cross_dependencies, get_image_type_enum,
get_transparency_enum,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUFFER_SIZE, CONF_BUFFER_SIZE,
CONF_DITHER,
CONF_FILE,
CONF_FORMAT, CONF_FORMAT,
CONF_ID, CONF_ID,
CONF_ON_ERROR, CONF_ON_ERROR,
@ -23,7 +27,7 @@ from esphome.const import (
AUTO_LOAD = ["image"] AUTO_LOAD = ["image"]
DEPENDENCIES = ["display", "http_request"] DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages"] CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
@ -35,9 +39,30 @@ online_image_ns = cg.esphome_ns.namespace("online_image")
ImageFormat = online_image_ns.enum("ImageFormat") ImageFormat = online_image_ns.enum("ImageFormat")
FORMAT_PNG = "PNG"
IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here class Format:
def __init__(self, image_type):
self.image_type = image_type
@property
def enum(self):
return getattr(ImageFormat, self.image_type)
def actions(self):
pass
class PNGFormat(Format):
def __init__(self):
super().__init__("PNG")
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
# New formats can be added here.
IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)}
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_(
"DownloadErrorTrigger", automation.Trigger.template() "DownloadErrorTrigger", automation.Trigger.template()
) )
ONLINE_IMAGE_SCHEMA = cv.Schema(
{ def remove_options(*options):
cv.Required(CONF_ID): cv.declare_id(OnlineImage), return {
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), cv.Optional(option): cv.invalid(
# f"{option} is an invalid option for online_image"
# Common image options )
# for option in options
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,
#
# Online Image specific options
#
cv.Required(CONF_URL): cv.url,
cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger),
}
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
}
),
} }
).extend(cv.polling_component_schema("never"))
ONLINE_IMAGE_SCHEMA = (
IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER))
.extend(
{
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
# Online Image specific options
cv.Required(CONF_URL): cv.url,
cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True),
cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_),
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DownloadFinishedTrigger
),
}
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
}
),
}
)
.extend(cv.polling_component_schema("never"))
)
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
cv.All( cv.All(
ONLINE_IMAGE_SCHEMA, ONLINE_IMAGE_SCHEMA,
validate_cross_dependencies,
cv.require_framework_version( cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0), # esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0), esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0), esp_idf=cv.Version(4, 0, 0),
rp2040_arduino=cv.Version(0, 0, 0), rp2040_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
), ),
) )
) )
@ -132,29 +163,26 @@ async def online_image_action_to_code(config, action_id, template_arg, args):
async def to_code(config): async def to_code(config):
format = config[CONF_FORMAT] image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
if format in [FORMAT_PNG]: image_format.actions()
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
url = config[CONF_URL] url = config[CONF_URL]
width, height = config.get(CONF_RESIZE, (0, 0)) width, height = config.get(CONF_RESIZE, (0, 0))
transparent = config[CONF_USE_TRANSPARENCY] transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY])
var = cg.new_Pvariable( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
url, url,
width, width,
height, height,
format, image_format.enum,
config[CONF_TYPE], get_image_type_enum(config[CONF_TYPE]),
transparent,
config[CONF_BUFFER_SIZE], config[CONF_BUFFER_SIZE],
) )
await cg.register_component(var, config) await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_transparency(transparent))
if placeholder_id := config.get(CONF_PLACEHOLDER): if placeholder_id := config.get(CONF_PLACEHOLDER):
placeholder = await cg.get_variable(placeholder_id) placeholder = await cg.get_variable(placeholder_id)
cg.add(var.set_placeholder(placeholder)) cg.add(var.set_placeholder(placeholder))

View File

@ -1,5 +1,4 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#include "esphome/core/color.h" #include "esphome/core/color.h"
namespace esphome { namespace esphome {
@ -23,7 +22,7 @@ class ImageDecoder {
/** /**
* @brief Initialize the decoder. * @brief Initialize the decoder.
* *
* @param download_size The total number of bytes that need to be download for the image. * @param download_size The total number of bytes that need to be downloaded for the image.
*/ */
virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; }
@ -38,7 +37,7 @@ class ImageDecoder {
* @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully * @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully
* decode anything, or negative in case of a decoding error. * decode anything, or negative in case of a decoding error.
*/ */
virtual int decode(uint8_t *buffer, size_t size); virtual int decode(uint8_t *buffer, size_t size) = 0;
/** /**
* @brief Request the image to be resized once the actual dimensions are known. * @brief Request the image to be resized once the actual dimensions are known.
@ -50,7 +49,7 @@ class ImageDecoder {
void set_size(int width, int height); void set_size(int width, int height);
/** /**
* @brief Draw a rectangle on the display_buffer using the defined color. * @brief Fill a rectangle on the display_buffer using the defined color.
* Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly.
* In case of binary displays, the color will be converted to binary as well. * In case of binary displays, the color will be converted to binary as well.
* Called by the callback functions, to be able to access the parent Image class. * Called by the callback functions, to be able to access the parent Image class.
@ -59,7 +58,7 @@ class ImageDecoder {
* @param y The top-most coordinate of the rectangle. * @param y The top-most coordinate of the rectangle.
* @param w The width of the rectangle. * @param w The width of the rectangle.
* @param h The height of the rectangle. * @param h The height of the rectangle.
* @param color The color to draw the rectangle with. * @param color The fill color
*/ */
void draw(int x, int y, int w, int h, const Color &color); void draw(int x, int y, int w, int h, const Color &color);
@ -67,7 +66,7 @@ class ImageDecoder {
protected: protected:
OnlineImage *image_; OnlineImage *image_;
// Initializing to 1, to ensure it is different than initial "decoded_bytes_". // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_".
// Will be overwritten anyway once the download size is known. // Will be overwritten anyway once the download size is known.
uint32_t download_size_ = 1; uint32_t download_size_ = 1;
uint32_t decoded_bytes_ = 0; uint32_t decoded_bytes_ = 0;

View File

@ -25,8 +25,8 @@ inline bool is_color_on(const Color &color) {
} }
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
uint32_t download_buffer_size) image::Transparency transparency, uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type), : Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr), buffer_(nullptr),
download_buffer_(download_buffer_size), download_buffer_(download_buffer_size),
format_(format), format_(format),
@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on,
void OnlineImage::release() { void OnlineImage::release() {
if (this->buffer_) { if (this->buffer_) {
ESP_LOGD(TAG, "Deallocating old buffer..."); ESP_LOGV(TAG, "Deallocating old buffer...");
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
this->data_start_ = nullptr; this->data_start_ = nullptr;
this->buffer_ = nullptr; this->buffer_ = nullptr;
@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) {
if (this->buffer_) { if (this->buffer_) {
return false; return false;
} }
auto new_size = this->get_buffer_size_(width, height); size_t new_size = this->get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
delay_microseconds_safe(2000);
this->buffer_ = this->allocator_.allocate(new_size); this->buffer_ = this->allocator_.allocate(new_size);
if (this->buffer_) { if (this->buffer_ == nullptr) {
this->buffer_width_ = width; ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
this->buffer_height_ = height; this->allocator_.get_max_free_block_size());
this->width_ = width;
ESP_LOGD(TAG, "New size: (%d, %d)", width, height);
} else {
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %zu Bytes", this->allocator_.get_max_free_block_size());
this->end_connection_(); this->end_connection_();
return false; return false;
} }
this->buffer_width_ = width;
this->buffer_height_ = height;
this->width_ = width;
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
return true; return true;
} }
@ -91,9 +90,8 @@ void OnlineImage::update() {
if (this->decoder_) { if (this->decoder_) {
ESP_LOGW(TAG, "Image already being updated."); ESP_LOGW(TAG, "Image already being updated.");
return; return;
} else {
ESP_LOGI(TAG, "Updating image");
} }
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
this->downloader_ = this->parent_->get(this->url_); this->downloader_ = this->parent_->get(this->url_);
@ -142,10 +140,11 @@ void OnlineImage::loop() {
return; return;
} }
if (!this->downloader_ || this->decoder_->is_finished()) { if (!this->downloader_ || this->decoder_->is_finished()) {
ESP_LOGD(TAG, "Image fully downloaded");
this->data_start_ = buffer_; this->data_start_ = buffer_;
this->width_ = buffer_width_; this->width_ = buffer_width_;
this->height_ = buffer_height_; this->height_ = buffer_height_;
ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(),
this->width_, this->height_);
this->end_connection_(); this->end_connection_();
this->download_finished_callback_.call(); this->download_finished_callback_.call();
return; return;
@ -171,6 +170,19 @@ void OnlineImage::loop() {
} }
} }
void OnlineImage::map_chroma_key(Color &color) {
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (color.g == 1 && color.r == 0 && color.b == 0) {
color.g = 0;
}
if (color.w < 0x80) {
color.r = 0;
color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1;
color.b = 0;
}
}
}
void OnlineImage::draw_pixel_(int x, int y, Color color) { void OnlineImage::draw_pixel_(int x, int y, Color color) {
if (!this->buffer_) { if (!this->buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!"); ESP_LOGE(TAG, "Buffer not allocated!");
@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
switch (this->type_) { switch (this->type_) {
case ImageType::IMAGE_TYPE_BINARY: { case ImageType::IMAGE_TYPE_BINARY: {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8; pos = x + y * width_8;
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { auto bitno = 0x80 >> (pos % 8u);
this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); pos /= 8u;
auto on = is_color_on(color);
if (this->has_transparency() && color.w < 0x80)
on = false;
if (on) {
this->buffer_[pos] |= bitno;
} else { } else {
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); this->buffer_[pos] &= ~bitno;
} }
break; break;
} }
case ImageType::IMAGE_TYPE_GRAYSCALE: { case ImageType::IMAGE_TYPE_GRAYSCALE: {
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
if (this->has_transparency()) { if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
if (gray == 1) { if (gray == 1) {
gray = 0; gray = 0;
} }
if (color.w < 0x80) { if (color.w < 0x80) {
gray = 1; gray = 1;
} }
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
if (color.w != 0xFF)
gray = color.w;
} }
this->buffer_[pos] = gray; this->buffer_[pos] = gray;
break; break;
} }
case ImageType::IMAGE_TYPE_RGB565: { case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color); uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
if (this->has_transparency()) if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 2] = color.w; this->buffer_[pos + 2] = color.w;
break;
}
case ImageType::IMAGE_TYPE_RGBA: {
this->buffer_[pos + 0] = color.r;
this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b;
this->buffer_[pos + 3] = color.w;
break;
}
case ImageType::IMAGE_TYPE_RGB24:
default: {
if (this->has_transparency()) {
if (color.b == 1 && color.r == 0 && color.g == 0) {
color.b = 0;
}
if (color.w < 0x80) {
color.r = 0;
color.g = 0;
color.b = 1;
}
} }
break;
}
case ImageType::IMAGE_TYPE_RGB: {
this->map_chroma_key(color);
this->buffer_[pos + 0] = color.r; this->buffer_[pos + 0] = color.r;
this->buffer_[pos + 1] = color.g; this->buffer_[pos + 1] = color.g;
this->buffer_[pos + 2] = color.b; this->buffer_[pos + 2] = color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}
break; break;
} }
} }

View File

@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent,
* @param buffer_size Size of the buffer used to download the image. * @param buffer_size Size of the buffer used to download the image.
*/ */
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
uint32_t buffer_size); image::Transparency transparency, uint32_t buffer_size);
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
void update() override; void update() override;
void loop() override; void loop() override;
void map_chroma_key(Color &color);
/** Set the URL to download the image from. */ /** Set the URL to download the image from. */
void set_url(const std::string &url) { void set_url(const std::string &url) {

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "image_decoder.h" #include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h> #include <pngle.h>

View File

@ -58,7 +58,19 @@ file_types = (
) )
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
py_include = ("*.py",) py_include = ("*.py",)
ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf") ignore_types = (
".ico",
".png",
".woff",
".woff2",
"",
".ttf",
".otf",
".pcf",
".apng",
".gif",
".webp",
)
LINT_FILE_CHECKS = [] LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = [] LINT_CONTENT_CHECKS = []
@ -669,8 +681,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
global EXECUTABLE_BIT EXECUTABLE_BIT.update(git_ls_files())
EXECUTABLE_BIT = git_ls_files()
files = list(EXECUTABLE_BIT.keys()) files = list(EXECUTABLE_BIT.keys())
# Match against re # Match against re
file_name_re = re.compile("|".join(args.files)) file_name_re = re.compile("|".join(args.files))

View File

@ -0,0 +1,4 @@
*.apng -text
*.webp -text
*.gif -text

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,23 @@
animation:
- id: rgb565_animation
file: $component_dir/anim.gif
type: RGB565
use_transparency: opaque
resize: 50x50
- id: rgb_animation
file: $component_dir/anim.apng
type: RGB
use_transparency: chroma_key
resize: 50x50
- id: grayscale_animation
file: $component_dir/anim.apng
type: grayscale
display:
lambda: |-
id(rgb565_animation).next_frame();
id(rgb_animation1).next_frame();
id(grayscale_animation2).next_frame();
it.image(0, 0, rgb565_animation);
it.image(120, 0, rgb_animation1);
it.image(240, 0, grayscale_animation2);

View File

@ -13,12 +13,6 @@ display:
reset_pin: 21 reset_pin: 21
invert_colors: false invert_colors: false
# Purposely test that `animation:` does auto-load `image:` packages:
# Keep the `image:` undefined. animation: !include common.yaml
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -13,12 +13,5 @@ display:
reset_pin: 10 reset_pin: 10
invert_colors: false invert_colors: false
# Purposely test that `animation:` does auto-load `image:` packages:
# Keep the `image:` undefined. animation: !include common.yaml
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -13,12 +13,5 @@ display:
reset_pin: 10 reset_pin: 10
invert_colors: false invert_colors: false
# Purposely test that `animation:` does auto-load `image:` packages:
# Keep the `image:` undefined. animation: !include common.yaml
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -13,12 +13,5 @@ display:
reset_pin: 21 reset_pin: 21
invert_colors: false invert_colors: false
# Purposely test that `animation:` does auto-load `image:` packages:
# Keep the `image:` undefined. animation: !include common.yaml
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -13,12 +13,5 @@ display:
reset_pin: 16 reset_pin: 16
invert_colors: false invert_colors: false
# Purposely test that `animation:` does auto-load `image:` packages:
# Keep the `image:` undefined. animation: !include common.yaml
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -13,12 +13,5 @@ display:
reset_pin: 22 reset_pin: 22
invert_colors: false invert_colors: false
# Purposely test that `animation:` does auto-load `image:` packages:
# Keep the `image:` undefined. animation: !include common.yaml
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false

View File

@ -5,32 +5,65 @@ image:
dither: FloydSteinberg dither: FloydSteinberg
- id: transparent_transparent_image - id: transparent_transparent_image
file: ../../pnglogo.png file: ../../pnglogo.png
type: TRANSPARENT_BINARY type: BINARY
use_transparency: chroma_key
- id: rgba_image - id: rgba_image
file: ../../pnglogo.png file: ../../pnglogo.png
type: RGBA type: RGB
use_transparency: alpha_channel
resize: 50x50 resize: 50x50
- id: rgb24_image - id: rgb24_image
file: ../../pnglogo.png file: ../../pnglogo.png
type: RGB24 type: RGB
use_transparency: yes use_transparency: chroma_key
- id: rgb_image
file: ../../pnglogo.png
type: RGB
use_transparency: opaque
- id: rgb565_image - id: rgb565_image
file: ../../pnglogo.png file: ../../pnglogo.png
type: RGB565 type: RGB565
use_transparency: no use_transparency: opaque
- id: rgb565_ck_image
file: ../../pnglogo.png
type: RGB565
use_transparency: chroma_key
- id: rgb565_alpha_image
file: ../../pnglogo.png
type: RGB565
use_transparency: alpha_channel
- id: grayscale_alpha_image
file: ../../pnglogo.png
type: grayscale
use_transparency: alpha_channel
resize: 50x50
- id: grayscale_ck_image
file: ../../pnglogo.png
type: grayscale
use_transparency: chroma_key
- id: grayscale_image
file: ../../pnglogo.png
type: grayscale
use_transparency: opaque
- id: web_svg_image - id: web_svg_image
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48 resize: 256x48
type: TRANSPARENT_BINARY type: BINARY
use_transparency: chroma_key
- id: web_tiff_image - id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24 type: RGB
resize: 48x48 resize: 48x48
- id: web_redirect_image - id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24 type: RGB
resize: 48x48 resize: 48x48
- id: mdi_alert - id: mdi_alert
type: BINARY
file: mdi:alert-circle-outline file: mdi:alert-circle-outline
resize: 50x50 resize: 50x50
- id: another_alert_icon - id: another_alert_icon

View File

@ -5,4 +5,44 @@ display:
width: 480 width: 480
height: 480 height: 480
<<: !include common.yaml image:
binary:
- id: binary_image
file: ../../pnglogo.png
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
use_transparency: chroma_key
rgb:
alpha_channel:
- id: rgba_image
file: ../../pnglogo.png
resize: 50x50
chroma_key:
- id: rgb24_image
file: ../../pnglogo.png
type: RGB
opaque:
- id: rgb_image
file: ../../pnglogo.png
rgb565:
- id: rgb565_image
file: ../../pnglogo.png
use_transparency: opaque
- id: rgb565_ck_image
file: ../../pnglogo.png
use_transparency: chroma_key
- id: rgb565_alpha_image
file: ../../pnglogo.png
use_transparency: alpha_channel
grayscale:
- id: grayscale_alpha_image
file: ../../pnglogo.png
use_transparency: alpha_channel
resize: 50x50
- id: grayscale_ck_image
file: ../../pnglogo.png
use_transparency: chroma_key
- id: grayscale_image
file: ../../pnglogo.png
use_transparency: opaque

View File

@ -13,33 +13,32 @@ online_image:
resize: 50x50 resize: 50x50
- id: online_binary_transparent_image - id: online_binary_transparent_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
type: TRANSPARENT_BINARY type: BINARY
use_transparency: chroma_key
format: png format: png
- id: online_rgba_image - id: online_rgba_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
format: PNG format: PNG
type: RGBA type: RGB
use_transparency: alpha_channel
- id: online_rgb24_image - id: online_rgb24_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
format: PNG format: PNG
type: RGB24 type: RGB
use_transparency: true use_transparency: chroma_key
# Check the set_url action # Check the set_url action
time: esphome:
- platform: sntp on_boot:
on_time: then:
- at: "13:37:42" - online_image.set_url:
then: id: online_rgba_image
- online_image.set_url: url: http://www.example.org/example.png
id: online_rgba_image - online_image.set_url:
url: http://www.example.org/example.png id: online_rgba_image
- online_image.set_url: url: !lambda |-
id: online_rgba_image return "http://www.example.org/example.png";
url: !lambda |- - online_image.set_url:
return "http://www.example.org/example.png"; id: online_rgba_image
- online_image.set_url: url: !lambda |-
id: online_rgba_image return str_sprintf("http://homeassistant.local:8123");
url: !lambda |-
return str_sprintf("http://homeassistant.local:8123");