diff --git a/src/MessageHandler/RenderImageMessageHandler.php b/src/MessageHandler/RenderImageMessageHandler.php index f95a96c..5c41a69 100644 --- a/src/MessageHandler/RenderImageMessageHandler.php +++ b/src/MessageHandler/RenderImageMessageHandler.php @@ -28,6 +28,27 @@ final class RenderImageMessageHandler 0x6 => [16, 160, 64 ], // GREEN ]; + // ── Render tunables ────────────────────────────────────────────────────── + // Tweak these to change how photos look on the panel; each value has the + // baseline it shipped with as a comment so we can rollback per-knob. + // git tag render-baseline-2026-05-14 captures the full known-working set. + + /** modulateImage(brightness=100, SATURATION, hue=100). 100 = no change. + * Baseline: 130 (+30% saturation, prevents desaturated photos collapsing + * to dither noise). Experiment #1: 115 (+15%, less risk of garish faces). */ + private const SATURATION_PCT = 115; + + /** gammaImage(GAMMA). 1.0 = no change; >1 lifts midtones, <1 crushes them. + * Baseline: 1.0 (no gamma pass). Experiment #1: 1.2 (gentle midtone lift — + * faces and shadows climb out of the BLACK cluster). */ + private const GAMMA = 1.2; + + /** sharpenImage(radius=0, SHARPEN_SIGMA). Baseline: 0.8 (light). */ + private const SHARPEN_SIGMA = 0.8; + + /** Imagick dither method. Baseline: DITHERMETHOD_FLOYDSTEINBERG. */ + private const DITHER_METHOD = \Imagick::DITHERMETHOD_FLOYDSTEINBERG; + public function __construct( private readonly ImageRepository $imageRepo, private readonly RenderedAssetRepository $assetRepo, @@ -116,12 +137,21 @@ final class RenderImageMessageHandler // histogram percentiles and produces gentle, correct stretching. $imagick->normalizeImage(); - // Boost saturation 130%. Dark desaturated photos otherwise map almost - // entirely to BLACK with scattered noise dots from error diffusion. - $imagick->modulateImage(100, 130, 100); + // Midtone lift before the saturation + dither passes. Spectra-6's + // tonal range is squeezed (effectively ~3 bits per channel after + // dither); without a gamma push, faces and shadow detail get + // crushed into BLACK and the photo reads as too dark on the panel. + if (self::GAMMA !== 1.0) { + $imagick->gammaImage(self::GAMMA); + } + + // Saturation boost. Dark desaturated photos otherwise map almost + // entirely to BLACK with scattered noise dots from error diffusion; + // too much saturation and skin tones go ruddy / sky goes synthetic. + $imagick->modulateImage(100, self::SATURATION_PCT, 100); // Light sharpen so edges survive the dithering scatter. - $imagick->sharpenImage(0, 0.8); + $imagick->sharpenImage(0, self::SHARPEN_SIGMA); // 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. @@ -161,7 +191,7 @@ final class RenderImageMessageHandler $palImagick->resetIterator(); $palStrip = $palImagick->appendImages(false); - $imagick->remapImage($palStrip, \Imagick::DITHERMETHOD_FLOYDSTEINBERG); + $imagick->remapImage($palStrip, self::DITHER_METHOD); $palStrip->destroy(); $palImagick->destroy();