mirror of
https://github.com/esphome/esphome.git
synced 2026-05-21 02:01:57 +08:00
[image] Transparency changes; code refactor (#7908)
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user