Fix distorted animated icons on ESPHome 2026.4.0 (#331)#332
Closed
weirded wants to merge 1 commit into
Closed
Conversation
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.
3ea049a to
0200952
Compare
andrewjswan
added a commit
to andrewjswan/EspHoMaTriXv2
that referenced
this pull request
Apr 17, 2026
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: lubeda#332 Co-authored-by: Stefan Zier <stefan@zier.com>
andrewjswan
added a commit
to andrewjswan/EspHoMaTriXv2
that referenced
this pull request
Apr 17, 2026
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: lubeda#332 Co-authored-by: Stefan Zier <stefan@zier.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #331.
Root cause
ESPHome 2026.4.0 reworked
ImageRGB565in two ways that together break every RGB565-with-transparency icon produced by EHMTXv2:Image::get_rgb565_pixel_decoder still reads big-endian. Every pixel comes out byte-swapped.encoder.end_image()call. For multi-frame animations the new layout is actually broken end-to-end:write_image()callsend_image()per frame, each call appends the entire growing alpha buffer, andAnimation::update_data_start_()advancesdata_start_by onlyw*h*2bytes (the RGB-only frame size). The decoder then reads "alpha" bytes that fall inside the next frame's RGB data — producing the pixel-soup reported in the issue.Fix
Two small changes in
components/ehmtxv2/__init__.py:encoder.set_big_endian(True)so RGB565 bytes match what the C++ decoder reads. Guarded withhasattrfor backwards compatibility; no-op on older ESPHome where BE was already the default.ALPHA_CHANNELtoCHROMA_KEY. Chroma-key stores the transparent-pixel marker (0x0020) inline in the RGB565 stream, so every frame is exactlyw*h*2bytes andAnimationframe 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 pixel-art icons (LaMetric style) are binary-alpha in practice, so this matches real usage.Testing
Verified locally on ESPHome 2026.4.0 against a physical Ulanzi display with both static and animated icons — previously distorted animations now render correctly. Byte-level verification of the encoder output confirms each frame is
w*h*2bytes and the chroma-key sentinel0x0020is placed at transparent pixels in big-endian form.Compatibility
set_big_endian(True)matches the old default;CHROMA_KEYworked identically here.set_big_endiandidn't exist yet;hasattrguard makes this a no-op and byte order defaulted to BE anyway.Notes
dev-01. A cherry-pick onto2025.12.0is trivial (only this block is touched) if you'd like to ship a patch release for current stable users.