From 977444cf27b31227ad9efb4f98cc9675c2842dd1 Mon Sep 17 00:00:00 2001 From: "Andrew J.Swan" Date: Fri, 17 Apr 2026 15:44:19 +0300 Subject: [PATCH] Fix distorted animated icons on ESPHome 2026.4.0 (lubeda#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 lubeda#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. Original PR: https://github.com/lubeda/EspHoMaTriXv2/pull/332 Co-authored-by: Stefan Zier --- 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 4955d57..c303bfc 100644 --- a/components/ehmtxv2/__init__.py +++ b/components/ehmtxv2/__init__.py @@ -13,7 +13,7 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_RESIZE, CONF_FILE, CONF_ID, CONF_BRIGHTNESS, CONF_RAW_DATA_ID, CONF_TIME, CONF_TRIGGER_ID from esphome.core import CORE, HexInt from esphome.cpp_generator import RawExpression -from esphome.components.image import CONF_ALPHA_CHANNEL, IMAGE_TYPE +from esphome.components.image import CONF_CHROMA_KEY, IMAGE_TYPE from urllib.parse import urlparse @@ -484,7 +484,15 @@ def openImageFile(path): 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 @@ -495,6 +503,12 @@ def openImageFile(path): 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()