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:
parent
aa87c60717
commit
f1c0570e3b
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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),
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
4
tests/components/animation/.gitattributes
vendored
Normal file
4
tests/components/animation/.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.apng -text
|
||||||
|
*.webp -text
|
||||||
|
*.gif -text
|
||||||
|
|
BIN
tests/components/animation/anim.apng
Normal file
BIN
tests/components/animation/anim.apng
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
tests/components/animation/anim.gif
Normal file
BIN
tests/components/animation/anim.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
tests/components/animation/anim.webp
Normal file
BIN
tests/components/animation/anim.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
23
tests/components/animation/common.yaml
Normal file
23
tests/components/animation/common.yaml
Normal 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);
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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");
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user