From 02009528f1dd1876c08a0879355383098a0c4353 Mon Sep 17 00:00:00 2001 From: Stefan Zier Date: Wed, 15 Apr 2026 14:57:14 -0700 Subject: [PATCH] Fix distorted animated icons on ESPHome 2026.4.0 (#331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESPHome 2026.4.0 reworked ImageRGB565 in two ways that together broke every RGB565-with-transparency icon produced by EHMTXv2: 1. The Python encoder's default byte order flipped from big-endian to little-endian, but the C++ Image::get_rgb565_pixel_ decoder still reads big-endian — every pixel comes out byte-swapped. 2. The alpha channel moved from per-pixel interleaved to an appended block via a new encoder.end_image() call. For multi-frame animations the new layout is broken end-to-end: write_image() calls end_image() per frame, each call appends the full growing alpha buffer, and Animation::update_data_start_() advances data_start_ by only w*h*2 bytes (the RGB-only frame size). The decoder then reads "alpha" bytes that fall inside the next frame's RGB data — the pixel-soup in #331. Two minimal changes: - Call encoder.set_big_endian(True) so bytes match the C++ decoder. Guarded with hasattr for pre-2025.10 ESPHome; no-op on older releases where BE was already the default. - Switch icon transparency from ALPHA_CHANNEL to CHROMA_KEY. Chroma-key stores the transparent-pixel marker (0x0020) inline in the RGB565 stream, so every frame is exactly w*h*2 bytes and Animation frame indexing works correctly on every ESPHome version. The only semantic change is that alpha values between 1 and 127 now render fully transparent instead of partially opaque — 8x8 LaMetric-style pixel-art icons are binary-alpha in practice, so this matches real usage. --- components/ehmtxv2/__init__.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/components/ehmtxv2/__init__.py b/components/ehmtxv2/__init__.py index 755fe29..c86d70c 100644 --- a/components/ehmtxv2/__init__.py +++ b/components/ehmtxv2/__init__.py @@ -10,7 +10,7 @@ from esphome import config_validation as cv from esphome.components import display, font, graph, time import esphome.components.image as espImage -from esphome.components.image import CONF_ALPHA_CHANNEL, IMAGE_TYPE +from esphome.components.image import CONF_CHROMA_KEY, IMAGE_TYPE from esphome.const import ( CONF_BRIGHTNESS, CONF_FILE, @@ -483,7 +483,15 @@ def load_icon(conf, cache_enabled): yaml_string += f"\"{conf[CONF_ID]}\"," dither = Image.Dither.NONE - transparency = CONF_ALPHA_CHANNEL + # Use CHROMA_KEY instead of ALPHA_CHANNEL: on ESPHome 2026.4.0 the + # appended-alpha layout is incompatible with Animation frame indexing + # (Animation::update_data_start_ advances by w*h*2 per frame, ignoring + # the alpha block). Chroma-key stores the transparent-pixel marker + # inline (0x0020), so each frame is exactly w*h*2 bytes and the C++ + # Animation class can index into it correctly on every ESPHome + # version. 8x8 pixel-art icons use binary transparency anyway — + # fixes issue #331. + transparency = CONF_CHROMA_KEY invert_alpha = False total_rows = height * frames @@ -494,6 +502,12 @@ def load_icon(conf, cache_enabled): dither, invert_alpha, ) + # ESPHome 2026.4.0 flipped the default RGB565 byte order to little- + # endian, but the C++ Image::get_rgb565_pixel_ decoder still reads + # big-endian. set_big_endian exists on every ESPHome release that + # supports EHMTXv2; no-op on <2026.4.0 where BE was already default. + if hasattr(encoder, "set_big_endian"): + encoder.set_big_endian(True) for frame_index in range(frames): image.seek(frame_index) pixels = encoder.convert(image.resize((width, height)), path).getdata()