From 4b808611e9dee09a4ec19ea6f31c41525fed086b Mon Sep 17 00:00:00 2001 From: Marcel Feix Date: Mon, 14 Dec 2020 17:17:16 +0100 Subject: [PATCH] Add GIF Animation Support (#1378) * Adding GIF Animation Support * CLang tidy correction * Adding Codeowner --- CODEOWNERS | 1 + esphome/components/animation/__init__.py | 94 +++++++++++++++++++ esphome/components/display/display_buffer.cpp | 45 +++++++++ esphome/components/display/display_buffer.h | 22 ++++- 4 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 esphome/components/animation/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 50bb216bd5..09b10e84cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,6 +13,7 @@ esphome/core/* @esphome/core # Integrations esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core +esphome/components/animation/* @syndlex esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py new file mode 100644 index 0000000000..f19dbd6946 --- /dev/null +++ b/esphome/components/animation/__init__.py @@ -0,0 +1,94 @@ +import logging + +from esphome import core +from esphome.components import display, font +import esphome.components.image as espImage +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE +from esphome.core import CORE, HexInt + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['display'] +MULTI_CONF = True + +Animation_ = display.display_ns.class_('Animation') + +CONF_RAW_DATA_ID = 'raw_data_id' + +ANIMATION_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default='BINARY'): cv.enum(espImage.IMAGE_TYPE, upper=True), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), +}) + +CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) + +CODEOWNERS = ['@syndlex'] + + +def to_code(config): + from PIL import Image + + path = CORE.relative_config_path(config[CONF_FILE]) + 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: + image.thumbnail(config[CONF_RESIZE]) + width, height = image.size + else: + if width > 500 or height > 500: + _LOGGER.warning("The image you requested is very big. Please consider using" + " the resize parameter.") + + 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('L', dither=Image.NONE) + pixels = list(frame.getdata()) + for pix in pixels: + data[pos] = pix + 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('RGB') + pixels = list(frame.getdata()) + for pix in pixels: + data[pos] = pix[0] + pos += 1 + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] + pos += 1 + + elif config[CONF_TYPE] == 'BINARY': + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range((height * width8 // 8) * frames)] + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert('1', dither=Image.NONE) + for y in range(height): + for x in range(width): + if frame.getpixel((x, y)): + continue + pos = x + y * width8 + (height * width8 * frameIndex) + data[pos // 8] |= 0x80 >> (pos % 8) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, frames, + espImage.IMAGE_TYPE[config[CONF_TYPE]]) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index a329f6f58a..8b65ce72e1 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -474,6 +474,51 @@ 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) {} +bool Animation::get_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return false; + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; + if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) + return false; + const uint32_t pos = x + y * width_8 + frame_index; + return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); +} +Color Animation::get_color_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) + return 0; + const uint32_t pos = (x + y * this->width_ + frame_index) * 3; + const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | + (pgm_read_byte(this->data_start_ + pos + 1) << 8) | + (pgm_read_byte(this->data_start_ + pos + 0) << 16); + return Color(color32); +} +Color Animation::get_grayscale_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) + return 0; + const uint32_t pos = (x + y * this->width_ + frame_index); + const uint8_t gray = pgm_read_byte(this->data_start_ + pos); + return Color(gray | gray << 8 | gray << 16 | gray << 24); +} +Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) + : Image(data_start, width, height, type), animation_frame_count_(animation_frame_count) { + current_frame_ = 0; +} +int Animation::get_animation_frame_count() const { return this->animation_frame_count_; } +int Animation::get_current_frame() const { return this->current_frame_; } +void Animation::next_frame() { + this->current_frame_++; + if (this->current_frame_ >= animation_frame_count_) { + this->current_frame_ = 0; + } +} + DisplayPage::DisplayPage(const display_writer_t &writer) : writer_(writer) {} void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show_next() { this->next_->show(); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index e402b4b021..235224d42e 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -388,9 +388,9 @@ class Font { class Image { public: Image(const uint8_t *data_start, int width, int height, ImageType type); - bool get_pixel(int x, int y) const; - Color get_color_pixel(int x, int y) const; - Color get_grayscale_pixel(int x, int y) const; + virtual bool get_pixel(int x, int y) const; + virtual Color get_color_pixel(int x, int y) const; + virtual Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; ImageType get_type() const; @@ -402,6 +402,22 @@ class Image { const uint8_t *data_start_; }; +class Animation : public Image { + public: + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); + bool get_pixel(int x, int y) const override; + Color get_color_pixel(int x, int y) const override; + Color get_grayscale_pixel(int x, int y) const override; + + int get_animation_frame_count() const; + int get_current_frame() const; + void next_frame(); + + protected: + int current_frame_; + int animation_frame_count_; +}; + template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page)