mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
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:
@@ -193,43 +193,51 @@ def oled_buffer_to_image(buffer, width, height):
|
|||||||
|
|
||||||
|
|
||||||
def eink_buffer_to_image(buffer, log_width, log_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
|
The firmware sends GxEPD2's own width()/height() in the header (using WIDTH_VISIBLE,
|
||||||
reflect WIDTH_VISIBLE (not the full physical WIDTH). So log_width = HEIGHT and
|
not the full physical WIDTH). Rotation is inferred from the aspect ratio:
|
||||||
log_height = WIDTH_VISIBLE for this rotation.
|
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):
|
Buffer layout (physical panel coordinates, rotation-independent):
|
||||||
row-major, stride = ceil(phys_width/8) bytes, MSB-first
|
row-major, stride bytes per row, MSB-first
|
||||||
byte index = phys_x // 8 + phys_y * stride
|
byte_idx = phys_x // 8 + phys_y * stride
|
||||||
bit = 7 - phys_x % 8
|
bit = 7 - phys_x % 8
|
||||||
value 1 = white (paper), 0 = black (ink)
|
1 = white (paper), 0 = black (ink)
|
||||||
|
|
||||||
With setRotation(1), GxEPD2 maps logical (lx, ly) → physical:
|
Physical HEIGHT = max(log_width, log_height) for any rotation on a non-square panel.
|
||||||
phys_x = WIDTH_VISIBLE - 1 - ly (= log_height - 1 - ly)
|
stride = buffer_size // HEIGHT (no panel-specific constants needed).
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
if log_width == 0 or log_height == 0:
|
if log_width == 0 or log_height == 0:
|
||||||
return Image.new("RGB", (1, 1), (255, 255, 255))
|
return Image.new("RGB", (1, 1), (255, 255, 255))
|
||||||
|
|
||||||
phys_stride = len(buffer) // log_width # bytes per physical row
|
# Physical panel HEIGHT is always the longer dimension.
|
||||||
vis_w = log_height # WIDTH_VISIBLE — from header after firmware fix
|
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()
|
pixels = image.load()
|
||||||
|
|
||||||
for ly in range(vis_w):
|
portrait = log_height > log_width # rotation=0 vs rotation=1
|
||||||
phys_x = vis_w - 1 - ly # WIDTH_VISIBLE - 1 - ly
|
|
||||||
|
for ly in range(log_height):
|
||||||
for lx in range(log_width):
|
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
|
byte_idx = phys_x // 8 + phys_y * phys_stride
|
||||||
bit = 7 - phys_x % 8
|
bit = 7 - phys_x % 8
|
||||||
if byte_idx < len(buffer):
|
if byte_idx < len(buffer):
|
||||||
raw = (buffer[byte_idx] >> bit) & 1
|
raw = (buffer[byte_idx] >> bit) & 1
|
||||||
# 1 = white (paper), 0 = black (ink)
|
|
||||||
color = (255, 255, 255) if raw else (0, 0, 0)
|
color = (255, 255, 255) if raw else (0, 0, 0)
|
||||||
pixels[lx, ly] = color
|
pixels[lx, ly] = color
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user