fix: rotate portrait renders to EPD-native byte layout
CI / test (push) Has been cancelled

The .bin file for portrait orientation was packed as 480-pixel rows ×
800 rows, but firmware streams blindly at 800×480 (the EPD's native
scan order). Both layouts hit the same 192000-byte total, so the size
guard in epd_draw_image_with_border passed and the row-stride mismatch
showed up on the panel as the photo tiling/repeating.

Renderer now rotates the cropped photo 90° CW before dithering when
orientation is portrait, so the packed bytes always match the EPD's
800-pixel scan order. Firmware stays orientation-unaware (per the
"ESP32 never transforms images" decision in webApp/CLAUDE.md). Preview
decoder rotates -90° on the way out so the in-browser frame preview
stays upright.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:06:41 -04:00
parent c2b208f103
commit 70d48f9b11
3 changed files with 36 additions and 5 deletions
@@ -70,7 +70,7 @@ final class RenderImageMessageHandler
: $this->projectDir . '/' . $image->getStoragePath();
$width = $model->width($orientation);
$height = $model->height($orientation);
$bin = $this->renderToBin($originalPath, $width, $height);
$bin = $this->renderToBin($originalPath, $width, $height, $orientation);
$relPath = 'var/storage/images/' . $image->getId()
. '/' . $model->value . '_' . $orientation->value . '.bin';
@@ -87,7 +87,7 @@ final class RenderImageMessageHandler
$this->em->flush();
}
private function renderToBin(string $path, int $width, int $height): string
private function renderToBin(string $path, int $width, int $height, Orientation $orientation): string
{
$imagick = new \Imagick($path);
$imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
@@ -95,6 +95,16 @@ final class RenderImageMessageHandler
$imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$imagick->autoOrient();
$imagick->cropThumbnailImage($width, $height);
// Portrait: rotate the cropped photo 90° CW so the packed .bin's row
// layout matches the EPD's native 800-pixel scan order. The frame is
// physically rotated 90° CCW for portrait (ribbon on left), so the
// photo's bottom edge maps to the EPD's left column. Firmware streams
// bytes raw — no orientation awareness on-device.
if ($orientation === Orientation::Portrait) {
$imagick->rotateImage(new \ImagickPixel('white'), 90);
}
$imagick->setImageColorspace(\Imagick::COLORSPACE_SRGB);
// Auto-levels: stretch the tonal range, clipping 1% at each end.