fix(screenshot/eink): handle portrait and landscape rotations in decoder

The previous decoder always applied the rotation=1 (landscape) transform,
which caused three visible artefacts on a portrait (rotation=0) device:
  - Content rotated 90°     — wrong phys_x/phys_y mapping
  - Content doubled/skewed  — stride = buffer/log_width = 4000/122 = 32
                               instead of buffer/HEIGHT  = 4000/250 = 16

Fix: infer rotation from aspect ratio (portrait: log_height > log_width).
  portrait  → direct mapping: phys_x=lx, phys_y=ly
  landscape → rotation-1 map: phys_x=log_height-1-ly, phys_y=lx

Derive phys_stride from buffer_size // max(log_w, log_h) — the physical
HEIGHT is always the longer axis regardless of rotation, giving 16 bytes/row
for GxEPD2_213_B74 in both orientations.

No panel-specific constants remain; works for any GxEPD2 panel/rotation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-28 07:54:45 +02:00
parent 1332358ee7
commit 37432491dd

View File

@@ -193,43 +193,51 @@ def oled_buffer_to_image(buffer, width, height):
def eink_buffer_to_image(buffer, log_width, log_height):
"""Decode GxEPD2 framebuffer for any panel with DISPLAY_ROTATION=1 (landscape).
"""Decode GxEPD2 framebuffer for any panel/rotation.
The firmware now sends GxEPD2's own width()/height() in the header, which already
reflect WIDTH_VISIBLE (not the full physical WIDTH). So log_width = HEIGHT and
log_height = WIDTH_VISIBLE for this rotation.
The firmware sends GxEPD2's own width()/height() in the header (using WIDTH_VISIBLE,
not the full physical WIDTH). Rotation is inferred from the aspect ratio:
portrait (log_height > log_width) → rotation 0: direct mapping
landscape (log_width > log_height) → rotation 1: phys_x = WIDTH_VISIBLE-1-ly
square (log_width == log_height) → assumed rotation 1
Buffer layout (always in physical panel coordinates, regardless of rotation):
row-major, stride = ceil(phys_width/8) bytes, MSB-first
byte index = phys_x // 8 + phys_y * stride
bit = 7 - phys_x % 8
value 1 = white (paper), 0 = black (ink)
Buffer layout (physical panel coordinates, rotation-independent):
row-major, stride bytes per row, MSB-first
byte_idx = phys_x // 8 + phys_y * stride
bit = 7 - phys_x % 8
1 = white (paper), 0 = black (ink)
With setRotation(1), GxEPD2 maps logical (lx, ly) → physical:
phys_x = WIDTH_VISIBLE - 1 - ly (= log_height - 1 - ly)
phys_y = lx
phys_stride is derived from buffer size so no panel-specific constants are needed:
phys_stride = buffer_size / phys_height = buffer_size / log_width
Physical HEIGHT = max(log_width, log_height) for any rotation on a non-square panel.
stride = buffer_size // HEIGHT (no panel-specific constants needed).
"""
if log_width == 0 or log_height == 0:
return Image.new("RGB", (1, 1), (255, 255, 255))
phys_stride = len(buffer) // log_width # bytes per physical row
vis_w = log_height # WIDTH_VISIBLE — from header after firmware fix
# Physical panel HEIGHT is always the longer dimension.
phys_height = max(log_width, log_height)
phys_stride = len(buffer) // phys_height # bytes per physical row (e.g. 128/8 = 16)
image = Image.new("RGB", (log_width, vis_w), (255, 255, 255))
image = Image.new("RGB", (log_width, log_height), (255, 255, 255))
pixels = image.load()
for ly in range(vis_w):
phys_x = vis_w - 1 - ly # WIDTH_VISIBLE - 1 - ly
portrait = log_height > log_width # rotation=0 vs rotation=1
for ly in range(log_height):
for lx in range(log_width):
phys_y = lx
if portrait:
# rotation=0: logical (lx,ly) maps directly to physical (lx,ly)
phys_x = lx
phys_y = ly
else:
# rotation=1: logical (lx,ly) → physical (WIDTH_VISIBLE-1-ly, lx)
# WIDTH_VISIBLE = log_height (from screenshotHeight() in firmware)
phys_x = log_height - 1 - ly
phys_y = lx
byte_idx = phys_x // 8 + phys_y * phys_stride
bit = 7 - phys_x % 8
if byte_idx < len(buffer):
raw = (buffer[byte_idx] >> bit) & 1
# 1 = white (paper), 0 = black (ink)
raw = (buffer[byte_idx] >> bit) & 1
color = (255, 255, 255) if raw else (0, 0, 0)
pixels[lx, ly] = color