From 80cdac18dca5ed29a8d5c9024be7ba823068575a Mon Sep 17 00:00:00 2001 From: Matt Edholm Date: Wed, 6 May 2026 16:30:11 -0400 Subject: [PATCH] fix: letterbox renders instead of brutally center-cropping mismatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cropThumbnailImage was filling the target box by aspect-cover — for a portrait crop served to a landscape device (or vice versa), that meant slicing a thin band through the photo, which the user correctly called out as unacceptable on the screen. Switching to thumbnailImage(... bestfit=true) + composite onto a white canvas of exact target dims means the photo always shows up upright and recognizable: matching aspect renders byte-identically to before (no padding, no zoom change), mismatched aspect shows the photo fit-to-box with white bars instead of a cropped slice. Adds an app:rerender-assets console command to reset every Ready asset to Pending and re-dispatch its render message — needed once after this deploy so existing bins (rendered with the old cropThumbnail logic) get regenerated with the letterbox pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Command/RerenderAssetsCommand.php | 58 +++++++++++++++++++ .../RenderImageMessageHandler.php | 21 ++++++- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/Command/RerenderAssetsCommand.php diff --git a/src/Command/RerenderAssetsCommand.php b/src/Command/RerenderAssetsCommand.php new file mode 100644 index 0000000..c4cbf7b --- /dev/null +++ b/src/Command/RerenderAssetsCommand.php @@ -0,0 +1,58 @@ +em->getRepository(RenderedAsset::class)->findAll(); + $count = 0; + + foreach ($assets as $asset) { + $asset->setStatus(RenderStatus::Pending)->setFilePath(null); + $this->bus->dispatch(new RenderImageMessage( + $asset->getImage()->getId(), + $asset->getDeviceModel()->value, + $asset->getOrientation()->value, + )); + $count++; + } + + $this->em->flush(); + + $io->success("Reset and re-dispatched $count rendered assets."); + return Command::SUCCESS; + } +} diff --git a/src/MessageHandler/RenderImageMessageHandler.php b/src/MessageHandler/RenderImageMessageHandler.php index 7818cd2..6f6a016 100644 --- a/src/MessageHandler/RenderImageMessageHandler.php +++ b/src/MessageHandler/RenderImageMessageHandler.php @@ -94,9 +94,26 @@ final class RenderImageMessageHandler $imagick->setBackgroundColor('white'); $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN); $imagick->autoOrient(); - $imagick->cropThumbnailImage($width, $height); - // Portrait: rotate the cropped photo 90° CCW so the packed .bin's row + // Fit the source into the target box without cropping aggressively; + // composite onto a white canvas of exact target dims so an aspect + // mismatch (e.g. portrait crop served to a landscape device) shows + // up as letterbox bars instead of a brutally center-cropped slice. + // For correctly-cropped photos the source already matches the target + // aspect and `thumbnailImage` produces target dims with no padding. + $imagick->thumbnailImage($width, $height, true); + if ($imagick->getImageWidth() !== $width || $imagick->getImageHeight() !== $height) { + $canvas = new \Imagick(); + $canvas->newImage($width, $height, new \ImagickPixel('white')); + $canvas->setImageFormat('png'); + $offsetX = (int) (($width - $imagick->getImageWidth()) / 2); + $offsetY = (int) (($height - $imagick->getImageHeight()) / 2); + $canvas->compositeImage($imagick, \Imagick::COMPOSITE_OVER, $offsetX, $offsetY); + $imagick->destroy(); + $imagick = $canvas; + } + + // Portrait: rotate the fitted photo 90° CCW so the packed .bin's row // layout matches the EPD's native 800-pixel scan order. The frame is // physically rotated 90° CW for portrait (ribbon on right from EPD's // POV → on left from user's view), so the photo's top edge maps to the