diff --git a/src/MessageHandler/RenderImageMessageHandler.php b/src/MessageHandler/RenderImageMessageHandler.php index 6f6a016..723123e 100644 --- a/src/MessageHandler/RenderImageMessageHandler.php +++ b/src/MessageHandler/RenderImageMessageHandler.php @@ -95,13 +95,34 @@ final class RenderImageMessageHandler $imagick->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN); $imagick->autoOrient(); - // 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. + // Fit the source into the target box without cropping aggressively. + // For correctly-cropped photos the source matches the target aspect + // and `thumbnailImage` produces target dims with no padding; for an + // aspect mismatch the photo comes out smaller than the target and + // gets composited onto a white canvas below so it shows up as a + // letterbox instead of a brutally center-cropped slice. $imagick->thumbnailImage($width, $height, true); + + $imagick->setImageColorspace(\Imagick::COLORSPACE_SRGB); + + // Per-pixel adjustments run on the PHOTO before the white canvas is + // composited under it — otherwise the bars dominate the histogram and + // the contrast stretch over-clips the photo content to white. + + // Auto-levels: stretch the tonal range, clipping 1% at each end. + // Fixes underexposed/dark photos so the full palette range is used. + $photoPixels = $imagick->getImageWidth() * $imagick->getImageHeight(); + $imagick->contrastStretchImage((int) ($photoPixels * 0.01), (int) ($photoPixels * 0.01)); + + // Boost saturation 130%. Dark desaturated photos otherwise map almost + // entirely to BLACK with scattered noise dots from error diffusion. + $imagick->modulateImage(100, 130, 100); + + // Light sharpen so edges survive the dithering scatter. + $imagick->sharpenImage(0, 0.8); + + // Now composite onto a white canvas of exact target dims so that any + // aspect mismatch shows up as letterbox bars, not as a sliced crop. if ($imagick->getImageWidth() !== $width || $imagick->getImageHeight() !== $height) { $canvas = new \Imagick(); $canvas->newImage($width, $height, new \ImagickPixel('white')); @@ -123,20 +144,6 @@ final class RenderImageMessageHandler $imagick->rotateImage(new \ImagickPixel('white'), -90); } - $imagick->setImageColorspace(\Imagick::COLORSPACE_SRGB); - - // Auto-levels: stretch the tonal range, clipping 1% at each end. - // Fixes underexposed/dark photos so the full palette range is used. - $pixels = $width * $height; - $imagick->contrastStretchImage((int) ($pixels * 0.01), (int) ($pixels * 0.01)); - - // Boost saturation 130%. Dark desaturated photos otherwise map almost - // entirely to BLACK with scattered noise dots from error diffusion. - $imagick->modulateImage(100, 130, 100); - - // Light sharpen so edges survive the dithering scatter. - $imagick->sharpenImage(0, 0.8); - // Build a strip of 6 palette pixels for remapImage $palImagick = new \Imagick(); foreach (self::PALETTE as $rgb) {