[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
27 changed files with 845 additions and 787 deletions
+1 -1
View File
@@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
+23 -261
View File
@@ -1,28 +1,10 @@
import logging
from esphome import automation, core
from esphome import automation
import esphome.codegen as cg
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
from esphome.const import (
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
from esphome.const import CONF_ID, CONF_REPEAT
_LOGGER = logging.getLogger(__name__)
@@ -30,6 +12,7 @@ AUTO_LOAD = ["image"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_LOOP = "loop"
CONF_START_FRAME = "start_frame"
@@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
TYPED_FILE_SCHEMA = cv.typed_schema(
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
},
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(
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_LOOP): cv.All(
{
CONF_SOURCE: SOURCE_WEB,
CONF_URL: value,
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,
}
)
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(
{
@@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args):
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(
config[CONF_ID],
prog_arr,
width,
height,
frames,
espImage.IMAGE_TYPE[config[CONF_TYPE]],
frame_count,
image_type,
trans_value,
)
cg.add(var.set_transparency(transparent))
if loop_config := config.get(CONF_LOOP):
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)
cg.add(var.set_loop(start, end, count))
+2 -2
View File
@@ -6,8 +6,8 @@ namespace esphome {
namespace animation {
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
image::ImageType type)
: Image(data_start, width, height, type),
image::ImageType type, image::Transparency transparent)
: Image(data_start, width, height, type, transparent),
animation_data_start_(data_start),
current_frame_(0),
animation_frame_count_(animation_frame_count),
+2 -1
View File
@@ -8,7 +8,8 @@ namespace animation {
class Animation : public image::Image {
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;
int get_current_frame() const;
File diff suppressed because it is too large Load Diff
+86 -57
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++) {
if (this->get_binary_pixel_(img_x, img_y)) {
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);
}
}
@@ -39,20 +39,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
}
}
break;
case IMAGE_TYPE_RGB24:
case IMAGE_TYPE_RGB:
for (int img_x = 0; img_x < width_; img_x++) {
for (int img_y = 0; img_y < height_; img_y++) {
auto color = this->get_rgb24_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);
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);
}
@@ -61,20 +51,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color
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_)
return color_off;
switch (this->type_) {
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:
return this->get_grayscale_pixel_(x, y);
case IMAGE_TYPE_RGB565:
return this->get_rgb565_pixel_(x, y);
case IMAGE_TYPE_RGB24:
return this->get_rgb24_pixel_(x, y);
case IMAGE_TYPE_RGBA:
return this->get_rgba_pixel_(x, y);
case IMAGE_TYPE_RGB:
return this->get_rgb_pixel_(x, y);
default:
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;
break;
case IMAGE_TYPE_RGB24:
this->dsc_.header.cf = LV_IMG_CF_RGB888;
case IMAGE_TYPE_RGB:
#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;
case IMAGE_TYPE_RGB565:
#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
this->dsc_.header.cf = 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;
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
#endif
break;
}
@@ -128,51 +135,73 @@ bool Image::get_binary_pixel_(int x, int y) const {
const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
}
Color Image::get_rgba_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 4;
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
}
Color Image::get_rgb24_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * 3;
Color Image::get_rgb_pixel_(int x, int y) const {
const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8;
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
progmem_read_byte(this->data_start_ + pos + 2));
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
// (0, 0, 1) has been defined as transparent color for non-alpha images.
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
color.w = 0;
} else {
color.w = 0xFF;
progmem_read_byte(this->data_start_ + pos + 2), 0xFF);
switch (this->transparency_) {
case TRANSPARENCY_CHROMA_KEY:
if (color.g == 1 && color.r == 0 && color.b == 0) {
// (0, 1, 0) has been defined as transparent color for non-alpha images.
color.w = 0;
}
break;
case TRANSPARENCY_ALPHA_CHANNEL:
color.w = progmem_read_byte(this->data_start_ + (pos + 3));
break;
default:
break;
}
return color;
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_;
if (this->transparent_) {
pos += (x + y * this->width_) * 3;
} else {
pos += (x + y * this->width_) * 2;
}
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;
auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF;
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a);
return color;
auto a = 0xFF;
switch (this->transparency_) {
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 {
const uint32_t pos = (x + y * this->width_);
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);
}
int Image::get_width() const { return this->width_; }
int Image::get_height() const { return this->height_; }
ImageType Image::get_type() const { return this->type_; }
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
: width_(width), height_(height), type_(type), data_start_(data_start) {}
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), 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 esphome
+15 -24
View File
@@ -12,51 +12,40 @@ namespace image {
enum ImageType {
IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_RGB = 2,
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 {
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;
int get_width() const override;
int get_height() const override;
const uint8_t *get_data_start() const { return this->data_start_; }
ImageType get_type() const;
int get_bpp() const {
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;
}
int get_bpp() const { return this->bpp_; }
/// Return the stride of the image in bytes, that is, the distance in bytes
/// 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 set_transparency(bool transparent) { transparent_ = transparent; }
bool has_transparency() const { return transparent_; }
bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; }
#ifdef USE_LVGL
lv_img_dsc_t *get_lv_img_dsc();
#endif
protected:
bool get_binary_pixel_(int x, int y) const;
Color get_rgb24_pixel_(int x, int y) const;
Color get_rgba_pixel_(int x, int y) const;
Color get_rgb_pixel_(int x, int y) const;
Color get_rgb565_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_;
ImageType type_;
const uint8_t *data_start_;
bool transparent_;
Transparency transparency_;
size_t bpp_{};
size_t stride_{};
#ifdef USE_LVGL
lv_img_dsc_t dsc_{};
#endif
+73 -45
View File
@@ -4,14 +4,18 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_USE_TRANSPARENCY,
IMAGE_TYPE,
IMAGE_SCHEMA,
Image_,
validate_cross_dependencies,
get_image_type_enum,
get_transparency_enum,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_DITHER,
CONF_FILE,
CONF_FORMAT,
CONF_ID,
CONF_ON_ERROR,
@@ -23,7 +27,7 @@ from esphome.const import (
AUTO_LOAD = ["image"]
DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True
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")
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_)
@@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_(
"DownloadErrorTrigger", automation.Trigger.template()
)
ONLINE_IMAGE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
#
# Common image 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),
}
),
def remove_options(*options):
return {
cv.Optional(option): cv.invalid(
f"{option} is an invalid option for online_image"
)
for option in options
}
).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(
cv.All(
ONLINE_IMAGE_SCHEMA,
validate_cross_dependencies,
cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 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):
format = config[CONF_FORMAT]
if format in [FORMAT_PNG]:
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
image_format = IMAGE_FORMATS[config[CONF_FORMAT]]
image_format.actions()
url = config[CONF_URL]
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(
config[CONF_ID],
url,
width,
height,
format,
config[CONF_TYPE],
image_format.enum,
get_image_type_enum(config[CONF_TYPE]),
transparent,
config[CONF_BUFFER_SIZE],
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_transparency(transparent))
if placeholder_id := config.get(CONF_PLACEHOLDER):
placeholder = await cg.get_variable(placeholder_id)
cg.add(var.set_placeholder(placeholder))
@@ -1,5 +1,4 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/color.h"
namespace esphome {
@@ -23,7 +22,7 @@ class ImageDecoder {
/**
* @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; }
@@ -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
* 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.
@@ -50,7 +49,7 @@ class ImageDecoder {
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.
* 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.
@@ -59,7 +58,7 @@ class ImageDecoder {
* @param y The top-most coordinate of the rectangle.
* @param w The width 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);
@@ -67,7 +66,7 @@ class ImageDecoder {
protected:
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.
uint32_t download_size_ = 1;
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,
uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type),
image::Transparency transparency, uint32_t download_buffer_size)
: Image(nullptr, 0, 0, type, transparency),
buffer_(nullptr),
download_buffer_(download_buffer_size),
format_(format),
@@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on,
void OnlineImage::release() {
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->data_start_ = nullptr;
this->buffer_ = nullptr;
@@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) {
if (this->buffer_) {
return false;
}
auto new_size = this->get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
delay_microseconds_safe(2000);
size_t new_size = this->get_buffer_size_(width, height);
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
this->buffer_ = this->allocator_.allocate(new_size);
if (this->buffer_) {
this->buffer_width_ = width;
this->buffer_height_ = height;
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());
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
this->allocator_.get_max_free_block_size());
this->end_connection_();
return false;
}
this->buffer_width_ = width;
this->buffer_height_ = height;
this->width_ = width;
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
return true;
}
@@ -91,9 +90,8 @@ void OnlineImage::update() {
if (this->decoder_) {
ESP_LOGW(TAG, "Image already being updated.");
return;
} else {
ESP_LOGI(TAG, "Updating image");
}
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
this->downloader_ = this->parent_->get(this->url_);
@@ -142,10 +140,11 @@ void OnlineImage::loop() {
return;
}
if (!this->downloader_ || this->decoder_->is_finished()) {
ESP_LOGD(TAG, "Image fully downloaded");
this->data_start_ = buffer_;
this->width_ = buffer_width_;
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->download_finished_callback_.call();
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) {
if (!this->buffer_) {
ESP_LOGE(TAG, "Buffer not allocated!");
@@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) {
switch (this->type_) {
case ImageType::IMAGE_TYPE_BINARY: {
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
const uint32_t pos = x + y * width_8;
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) {
this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u));
pos = x + y * width_8;
auto bitno = 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 {
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u));
this->buffer_[pos] &= ~bitno;
}
break;
}
case ImageType::IMAGE_TYPE_GRAYSCALE: {
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) {
gray = 0;
}
if (color.w < 0x80) {
gray = 1;
}
} else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
if (color.w != 0xFF)
gray = color.w;
}
this->buffer_[pos] = gray;
break;
}
case ImageType::IMAGE_TYPE_RGB565: {
this->map_chroma_key(color);
uint16_t col565 = display::ColorUtil::color_to_565(color);
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 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;
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 + 1] = color.g;
this->buffer_[pos + 2] = color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}
break;
}
}
@@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent,
* @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,
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 update() override;
void loop() override;
void map_chroma_key(Color &color);
/** Set the URL to download the image from. */
void set_url(const std::string &url) {
@@ -1,6 +1,7 @@
#pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h>
+14 -3
View File
@@ -58,7 +58,19 @@ file_types = (
)
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
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_CONTENT_CHECKS = []
@@ -669,8 +681,7 @@ def main():
)
args = parser.parse_args()
global EXECUTABLE_BIT
EXECUTABLE_BIT = git_ls_files()
EXECUTABLE_BIT.update(git_ls_files())
files = list(EXECUTABLE_BIT.keys())
# Match against re
file_name_re = re.compile("|".join(args.files))
@@ -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

+23
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);
@@ -13,12 +13,6 @@ display:
reset_pin: 21
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
packages:
animation: !include common.yaml
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
@@ -13,12 +13,5 @@ display:
reset_pin: 10
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml
@@ -13,12 +13,5 @@ display:
reset_pin: 10
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml
@@ -13,12 +13,5 @@ display:
reset_pin: 21
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml
@@ -13,12 +13,5 @@ display:
reset_pin: 16
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml
@@ -13,12 +13,5 @@ display:
reset_pin: 22
invert_colors: false
# Purposely test that `animation:` does auto-load `image:`
# Keep the `image:` undefined.
# image:
animation:
- id: rgb565_animation
file: ../../pnglogo.png
type: RGB565
use_transparency: false
packages:
animation: !include common.yaml
+41 -8
View File
@@ -5,32 +5,65 @@ image:
dither: FloydSteinberg
- id: transparent_transparent_image
file: ../../pnglogo.png
type: TRANSPARENT_BINARY
type: BINARY
use_transparency: chroma_key
- id: rgba_image
file: ../../pnglogo.png
type: RGBA
type: RGB
use_transparency: alpha_channel
resize: 50x50
- id: rgb24_image
file: ../../pnglogo.png
type: RGB24
use_transparency: yes
type: RGB
use_transparency: chroma_key
- id: rgb_image
file: ../../pnglogo.png
type: RGB
use_transparency: opaque
- id: rgb565_image
file: ../../pnglogo.png
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
file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg
resize: 256x48
type: TRANSPARENT_BINARY
type: BINARY
use_transparency: chroma_key
- id: web_tiff_image
file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff
type: RGB24
type: RGB
resize: 48x48
- id: web_redirect_image
file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4
type: RGB24
type: RGB
resize: 48x48
- id: mdi_alert
type: BINARY
file: mdi:alert-circle-outline
resize: 50x50
- id: another_alert_icon
+41 -1
View File
@@ -5,4 +5,44 @@ display:
width: 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
+20 -21
View File
@@ -13,33 +13,32 @@ online_image:
resize: 50x50
- id: online_binary_transparent_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
type: TRANSPARENT_BINARY
type: BINARY
use_transparency: chroma_key
format: png
- id: online_rgba_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
format: PNG
type: RGBA
type: RGB
use_transparency: alpha_channel
- id: online_rgb24_image
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
format: PNG
type: RGB24
use_transparency: true
type: RGB
use_transparency: chroma_key
# Check the set_url action
time:
- platform: sntp
on_time:
- at: "13:37:42"
then:
- online_image.set_url:
id: online_rgba_image
url: http://www.example.org/example.png
- online_image.set_url:
id: online_rgba_image
url: !lambda |-
return "http://www.example.org/example.png";
- online_image.set_url:
id: online_rgba_image
url: !lambda |-
return str_sprintf("http://homeassistant.local:8123");
esphome:
on_boot:
then:
- online_image.set_url:
id: online_rgba_image
url: http://www.example.org/example.png
- online_image.set_url:
id: online_rgba_image
url: !lambda |-
return "http://www.example.org/example.png";
- online_image.set_url:
id: online_rgba_image
url: !lambda |-
return str_sprintf("http://homeassistant.local:8123");