Skip to content

Fix distorted animated icons on ESPHome 2026.4.0 (#331)#332

Closed
weirded wants to merge 1 commit into
lubeda:dev-01from
weirded:fix-esphome-2026.4
Closed

Fix distorted animated icons on ESPHome 2026.4.0 (#331)#332
weirded wants to merge 1 commit into
lubeda:dev-01from
weirded:fix-esphome-2026.4

Conversation

@weirded
Copy link
Copy Markdown

@weirded weirded commented Apr 15, 2026

Fixes #331.

Root cause

ESPHome 2026.4.0 reworked ImageRGB565 in two ways that together break every RGB565-with-transparency icon produced by EHMTXv2:

  1. Default byte order flipped from big-endian to little-endian for the Python encoder, but the C++ Image::get_rgb565_pixel_ decoder still reads big-endian. Every pixel comes out byte-swapped.
  2. 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 actually broken end-to-end: write_image() calls end_image() per frame, each call appends the entire 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 — producing the pixel-soup reported in the issue.

Fix

Two small changes in components/ehmtxv2/__init__.py:

  • Call encoder.set_big_endian(True) so RGB565 bytes match what the C++ decoder reads. Guarded with hasattr for backwards compatibility; no-op on older ESPHome 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 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*2 bytes and the chroma-key sentinel 0x0020 is placed at transparent pixels in big-endian form.

Compatibility

  • ESPHome 2026.4.0+: fixes distorted icons.
  • ESPHome 2025.10.x – 2026.3.x: unchanged output. set_big_endian(True) matches the old default; CHROMA_KEY worked identically here.
  • ESPHome <2025.10: set_big_endian didn't exist yet; hasattr guard makes this a no-op and byte order defaulted to BE anyway.

Notes

  • This PR targets dev-01. A cherry-pick onto 2025.12.0 is trivial (only this block is touched) if you'd like to ship a patch release for current stable users.
  • No user-facing config change needed.

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.
@weirded weirded force-pushed the fix-esphome-2026.4 branch from 3ea049a to 0200952 Compare April 15, 2026 21:57
@weirded weirded changed the base branch from 2025.12.0 to dev-01 April 15, 2026 21:57
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>
@weirded weirded closed this Apr 17, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant