From 58a3791a5ce07cae4a474321719c01c03369c0de Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 18 Feb 2024 14:06:03 +0100 Subject: [PATCH] Allow customizing color, background color and logo in QR codes --- config/autoload/qr-codes.global.php | 5 ++ config/constants.php | 3 +- module/Core/src/Action/Model/QrCodeParams.php | 71 +++++++++++++++++-- module/Core/src/Action/QrCodeAction.php | 11 ++- module/Core/src/Config/EnvVars.php | 3 + module/Core/src/Options/QrCodeOptions.php | 5 ++ 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/config/autoload/qr-codes.global.php b/config/autoload/qr-codes.global.php index 808ff961..919beffa 100644 --- a/config/autoload/qr-codes.global.php +++ b/config/autoload/qr-codes.global.php @@ -4,6 +4,8 @@ declare(strict_types=1); use Shlinkio\Shlink\Core\Config\EnvVars; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS; use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION; use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT; @@ -26,6 +28,9 @@ return [ 'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv( DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS, ), + 'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(DEFAULT_QR_CODE_COLOR), + 'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(DEFAULT_QR_CODE_BG_COLOR), + 'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(), ], ]; diff --git a/config/constants.php b/config/constants.php index 7b263262..8d2e55cb 100644 --- a/config/constants.php +++ b/config/constants.php @@ -20,4 +20,5 @@ const DEFAULT_QR_CODE_FORMAT = 'png'; const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l'; const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true; const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true; -const MIN_TASK_WORKERS = 4; +const DEFAULT_QR_CODE_COLOR = '#000'; // Black +const DEFAULT_QR_CODE_BG_COLOR = '#fff'; // White diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index 05181f20..638d9929 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action\Model; +use Endroid\QrCode\Color\Color; +use Endroid\QrCode\Color\ColorInterface; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface; use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow; @@ -18,9 +20,19 @@ use Endroid\QrCode\Writer\WriterInterface; use Psr\Http\Message\ServerRequestInterface; use Shlinkio\Shlink\Core\Options\QrCodeOptions; +use Throwable; +use function hexdec; +use function ltrim; +use function max; +use function min; +use function self; use function Shlinkio\Shlink\Core\ArrayUtils\contains; +use function strlen; use function strtolower; +use function substr; use function trim; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; final class QrCodeParams { @@ -34,6 +46,8 @@ final class QrCodeParams public readonly WriterInterface $writer, public readonly ErrorCorrectionLevelInterface $errorCorrectionLevel, public readonly RoundBlockSizeModeInterface $roundBlockSizeMode, + public readonly ColorInterface $color, + public readonly ColorInterface $bgColor, ) { } @@ -42,11 +56,13 @@ final class QrCodeParams $query = $request->getQueryParams(); return new self( - self::resolveSize($query, $defaults), - self::resolveMargin($query, $defaults), - self::resolveWriter($query, $defaults), - self::resolveErrorCorrection($query, $defaults), - self::resolveRoundBlockSize($query, $defaults), + size: self::resolveSize($query, $defaults), + margin: self::resolveMargin($query, $defaults), + writer: self::resolveWriter($query, $defaults), + errorCorrectionLevel: self::resolveErrorCorrection($query, $defaults), + roundBlockSizeMode: self::resolveRoundBlockSize($query, $defaults), + color: self::resolveColor($query, $defaults), + bgColor: self::resolveBackgroundColor($query, $defaults), ); } @@ -57,7 +73,7 @@ final class QrCodeParams return self::MIN_SIZE; } - return $size > self::MAX_SIZE ? self::MAX_SIZE : $size; + return min($size, self::MAX_SIZE); } private static function resolveMargin(array $query, QrCodeOptions $defaults): int @@ -68,7 +84,7 @@ final class QrCodeParams return 0; } - return $intMargin < 0 ? 0 : $intMargin; + return max($intMargin, 0); } private static function resolveWriter(array $query, QrCodeOptions $defaults): WriterInterface @@ -101,6 +117,47 @@ final class QrCodeParams return $doNotRoundBlockSize ? new RoundBlockSizeModeNone() : new RoundBlockSizeModeMargin(); } + private static function resolveColor(array $query, QrCodeOptions $defaults): ColorInterface + { + $color = self::normalizeParam($query['color'] ?? $defaults->color); + return self::parseHexColor($color, DEFAULT_QR_CODE_COLOR); + } + + private static function resolveBackgroundColor(array $query, QrCodeOptions $defaults): ColorInterface + { + $bgColor = self::normalizeParam($query['bgColor'] ?? $defaults->bgColor); + return self::parseHexColor($bgColor, DEFAULT_QR_CODE_BG_COLOR); + } + + private static function parseHexColor(string $hexColor, ?string $fallback): Color + { + $hexColor = ltrim($hexColor, '#'); + + try { + if (strlen($hexColor) === 3) { + return new Color( + hexdec(substr($hexColor, 0, 1) . substr($hexColor, 0, 1)), + hexdec(substr($hexColor, 1, 1) . substr($hexColor, 1, 1)), + hexdec(substr($hexColor, 2, 1) . substr($hexColor, 2, 1)), + ); + } + + return new Color( + hexdec(substr($hexColor, 0, 2)), + hexdec(substr($hexColor, 2, 2)), + hexdec(substr($hexColor, 4, 2)), + ); + } catch (Throwable $e) { + // If a non-hex value was provided and an error occurs, fall back to the default color. + // Do not provide the fallback again this time, to avoid an infinite loop + if ($fallback !== null) { + return self::parseHexColor($fallback, null); + } + + throw $e; + } + } + private static function normalizeParam(string $param): string { return strtolower(trim($param)); diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index a952243a..748ed01b 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; use Endroid\QrCode\Builder\Builder; +use Endroid\QrCode\Color\Color; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; @@ -48,7 +49,15 @@ readonly class QrCodeAction implements MiddlewareInterface ->margin($params->margin) ->writer($params->writer) ->errorCorrectionLevel($params->errorCorrectionLevel) - ->roundBlockSizeMode($params->roundBlockSizeMode); + ->roundBlockSizeMode($params->roundBlockSizeMode) + ->foregroundColor($params->color) + ->backgroundColor($params->bgColor); + + $logoUrl = $this->options->logoUrl; + if ($logoUrl !== null) { + $qrCodeBuilder->logoPath($logoUrl) + ->logoResizeToHeight((int) ($params->size / 4)); + } return new QrCodeResponse($qrCodeBuilder->build()); } diff --git a/module/Core/src/Config/EnvVars.php b/module/Core/src/Config/EnvVars.php index 14a850c9..0ea74451 100644 --- a/module/Core/src/Config/EnvVars.php +++ b/module/Core/src/Config/EnvVars.php @@ -45,6 +45,9 @@ enum EnvVars: string case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION'; case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE'; case QR_CODE_FOR_DISABLED_SHORT_URLS = 'QR_CODE_FOR_DISABLED_SHORT_URLS'; + case DEFAULT_QR_CODE_COLOR = 'DEFAULT_QR_CODE_COLOR'; + case DEFAULT_QR_CODE_BG_COLOR = 'DEFAULT_QR_CODE_BG_COLOR'; + case DEFAULT_QR_CODE_LOGO_URL = 'DEFAULT_QR_CODE_LOGO_URL'; case DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT'; case DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT'; case DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT'; diff --git a/module/Core/src/Options/QrCodeOptions.php b/module/Core/src/Options/QrCodeOptions.php index fff27858..da130d17 100644 --- a/module/Core/src/Options/QrCodeOptions.php +++ b/module/Core/src/Options/QrCodeOptions.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Options; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR; +use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR; use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS; use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION; use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT; @@ -20,6 +22,9 @@ readonly final class QrCodeOptions public string $errorCorrection = DEFAULT_QR_CODE_ERROR_CORRECTION, public bool $roundBlockSize = DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, public bool $enabledForDisabledShortUrls = DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS, + public string $color = DEFAULT_QR_CODE_COLOR, + public string $bgColor = DEFAULT_QR_CODE_BG_COLOR, + public ?string $logoUrl = null, ) { } }