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:
@@ -215,8 +215,9 @@ class DeviceApiController extends AbstractController
|
|||||||
$this->renderBinToPng(
|
$this->renderBinToPng(
|
||||||
$binPath,
|
$binPath,
|
||||||
$pngPath,
|
$pngPath,
|
||||||
$device->getModel()->width($device->getOrientation()),
|
$device->getModel()->nativeWidth(),
|
||||||
$device->getModel()->height($device->getOrientation()),
|
$device->getModel()->nativeHeight(),
|
||||||
|
$device->getOrientation(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +230,7 @@ class DeviceApiController extends AbstractController
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function renderBinToPng(string $binPath, string $pngPath, int $width, int $height): void
|
private function renderBinToPng(string $binPath, string $pngPath, int $width, int $height, Orientation $orientation): void
|
||||||
{
|
{
|
||||||
$bin = (string) file_get_contents($binPath);
|
$bin = (string) file_get_contents($binPath);
|
||||||
$len = strlen($bin);
|
$len = strlen($bin);
|
||||||
@@ -250,6 +251,14 @@ class DeviceApiController extends AbstractController
|
|||||||
|
|
||||||
$im = new \Imagick();
|
$im = new \Imagick();
|
||||||
$im->readImageBlob($ppm);
|
$im->readImageBlob($ppm);
|
||||||
|
|
||||||
|
// The .bin is always laid out in EPD-native scan order. For portrait,
|
||||||
|
// the renderer pre-rotated the photo 90° CW; rotate -90° here so the
|
||||||
|
// browser-side preview shows the photo upright.
|
||||||
|
if ($orientation === Orientation::Portrait) {
|
||||||
|
$im->rotateImage(new \ImagickPixel('white'), -90);
|
||||||
|
}
|
||||||
|
|
||||||
$im->setImageFormat('png');
|
$im->setImageFormat('png');
|
||||||
$im->writeImage($pngPath);
|
$im->writeImage($pngPath);
|
||||||
$im->destroy();
|
$im->destroy();
|
||||||
|
|||||||
@@ -17,4 +17,16 @@ enum DeviceModel: string
|
|||||||
{
|
{
|
||||||
return $orientation === Orientation::Portrait ? 800 : 480;
|
return $orientation === Orientation::Portrait ? 800 : 480;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** EPD's hardware scan-row width — independent of user orientation. */
|
||||||
|
public function nativeWidth(): int
|
||||||
|
{
|
||||||
|
return 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EPD's hardware scan-row count — independent of user orientation. */
|
||||||
|
public function nativeHeight(): int
|
||||||
|
{
|
||||||
|
return 480;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ final class RenderImageMessageHandler
|
|||||||
: $this->projectDir . '/' . $image->getStoragePath();
|
: $this->projectDir . '/' . $image->getStoragePath();
|
||||||
$width = $model->width($orientation);
|
$width = $model->width($orientation);
|
||||||
$height = $model->height($orientation);
|
$height = $model->height($orientation);
|
||||||
$bin = $this->renderToBin($originalPath, $width, $height);
|
$bin = $this->renderToBin($originalPath, $width, $height, $orientation);
|
||||||
|
|
||||||
$relPath = 'var/storage/images/' . $image->getId()
|
$relPath = 'var/storage/images/' . $image->getId()
|
||||||
. '/' . $model->value . '_' . $orientation->value . '.bin';
|
. '/' . $model->value . '_' . $orientation->value . '.bin';
|
||||||
@@ -87,7 +87,7 @@ final class RenderImageMessageHandler
|
|||||||
$this->em->flush();
|
$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 = new \Imagick($path);
|
||||||
$imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
|
$imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE);
|
||||||
@@ -95,6 +95,16 @@ final class RenderImageMessageHandler
|
|||||||
$imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
|
$imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
|
||||||
$imagick->autoOrient();
|
$imagick->autoOrient();
|
||||||
$imagick->cropThumbnailImage($width, $height);
|
$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);
|
$imagick->setImageColorspace(\Imagick::COLORSPACE_SRGB);
|
||||||
|
|
||||||
// Auto-levels: stretch the tonal range, clipping 1% at each end.
|
// Auto-levels: stretch the tonal range, clipping 1% at each end.
|
||||||
|
|||||||
Reference in New Issue
Block a user