From 5a2350bac17d65eeb02f83a78d2633e7166d5d67 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 13 Jul 2021 13:22:50 +0200 Subject: [PATCH 1/5] Added suport for error correction level to QR codes --- docs/swagger/paths/{shortCode}_qr-code.json | 15 +++++++--- module/Core/src/Action/QrCodeAction.php | 32 +++++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/swagger/paths/{shortCode}_qr-code.json b/docs/swagger/paths/{shortCode}_qr-code.json index 00502ad5..43c1d38a 100644 --- a/docs/swagger/paths/{shortCode}_qr-code.json +++ b/docs/swagger/paths/{shortCode}_qr-code.json @@ -35,10 +35,7 @@ "required": false, "schema": { "type": "string", - "enum": [ - "png", - "svg" - ] + "enum": ["png", "svg"] } }, { @@ -51,6 +48,16 @@ "minimum": 0, "default": 0 } + }, + { + "name": "errorCorrection", + "in": "query", + "description": "The error correction level to apply to the the QR code: **[L]**ow, **[M]**edium, **[Q]**uartile or **[H]**igh. See [docs](https://www.qrcode.com/en/about/error_correction.html).", + "required": false, + "schema": { + "type": "string", + "enum": ["L", "M", "Q", "H"] + } } ], "responses": { diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index 177d90fc..a8dee3fd 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -5,6 +5,11 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; use Endroid\QrCode\Builder\Builder; +use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh; +use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface; +use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow; +use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium; +use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile; use Endroid\QrCode\Writer\SvgWriter; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -18,6 +23,9 @@ use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; +use function strtoupper; +use function trim; + class QrCodeAction implements MiddlewareInterface { private const DEFAULT_SIZE = 300; @@ -46,17 +54,18 @@ class QrCodeAction implements MiddlewareInterface } $query = $request->getQueryParams(); - $qrCode = Builder::create() + $qrCodeBuilder = Builder::create() ->data($this->stringifier->stringify($shortUrl)) ->size($this->resolveSize($request, $query)) - ->margin($this->resolveMargin($query)); + ->margin($this->resolveMargin($query)) + ->errorCorrectionLevel($this->resolveErrorCorrection($query)); $format = $query['format'] ?? 'png'; if ($format === 'svg') { - $qrCode->writer(new SvgWriter()); + $qrCodeBuilder->writer(new SvgWriter()); } - return new QrCodeResponse($qrCode->build()); + return new QrCodeResponse($qrCodeBuilder->build()); } private function resolveSize(Request $request, array $query): int @@ -72,11 +81,11 @@ class QrCodeAction implements MiddlewareInterface private function resolveMargin(array $query): int { - if (! isset($query['margin'])) { + $margin = $query['margin'] ?? null; + if ($margin === null) { return 0; } - $margin = $query['margin']; $intMargin = (int) $margin; if ($margin !== (string) $intMargin) { return 0; @@ -84,4 +93,15 @@ class QrCodeAction implements MiddlewareInterface return $intMargin < 0 ? 0 : $intMargin; } + + private function resolveErrorCorrection(array $query): ErrorCorrectionLevelInterface + { + $errorCorrectionLevel = strtoupper(trim($query['errorCorrection'] ?? '')); + return match ($errorCorrectionLevel) { + 'H' => new ErrorCorrectionLevelHigh(), + 'Q' => new ErrorCorrectionLevelQuartile(), + 'M' => new ErrorCorrectionLevelMedium(), + default => new ErrorCorrectionLevelLow(), // 'L' + }; + } } From d6e155d87435b4ccf6ddf94bfc7a6b0f9d95ead8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 13 Jul 2021 13:45:46 +0200 Subject: [PATCH 2/5] Extracted logic to determine QR code params to its own data object --- module/Core/src/Action/Model/QrCodeParams.php | 113 ++++++++++++++++++ module/Core/src/Action/QrCodeAction.php | 71 ++--------- module/Core/test/Action/QrCodeActionTest.php | 2 + 3 files changed, 122 insertions(+), 64 deletions(-) create mode 100644 module/Core/src/Action/Model/QrCodeParams.php diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php new file mode 100644 index 00000000..bcde20c6 --- /dev/null +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -0,0 +1,113 @@ +getQueryParams(); + + return new self( + self::resolveSize($request, $query), + self::resolveMargin($query), + self::resolveWriter($query), + self::resolveErrorCorrection($query), + ); + } + + private static function resolveSize(Request $request, array $query): int + { + // FIXME Size attribute is deprecated. After v3.0.0, always use the query param instead + $size = (int) $request->getAttribute('size', $query['size'] ?? self::DEFAULT_SIZE); + if ($size < self::MIN_SIZE) { + return self::MIN_SIZE; + } + + return $size > self::MAX_SIZE ? self::MAX_SIZE : $size; + } + + private static function resolveMargin(array $query): int + { + $margin = $query['margin'] ?? null; + if ($margin === null) { + return 0; + } + + $intMargin = (int) $margin; + if ($margin !== (string) $intMargin) { + return 0; + } + + return $intMargin < 0 ? 0 : $intMargin; + } + + private static function resolveWriter(array $query): WriterInterface + { + $format = strtolower(trim($query['format'] ?? 'png')); + return match ($format) { + 'svg' => new SvgWriter(), + default => new PngWriter(), + }; + } + + private static function resolveErrorCorrection(array $query): ErrorCorrectionLevelInterface + { + $errorCorrectionLevel = strtoupper(trim($query['errorCorrection'] ?? '')); + return match ($errorCorrectionLevel) { + 'H' => new ErrorCorrectionLevelHigh(), + 'Q' => new ErrorCorrectionLevelQuartile(), + 'M' => new ErrorCorrectionLevelMedium(), + default => new ErrorCorrectionLevelLow(), // 'L' + }; + } + + public function size(): int + { + return $this->size; + } + + public function margin(): int + { + return $this->margin; + } + + public function writer(): WriterInterface + { + return $this->writer; + } + + public function errorCorrectionLevel(): ErrorCorrectionLevelInterface + { + return $this->errorCorrectionLevel; + } +} diff --git a/module/Core/src/Action/QrCodeAction.php b/module/Core/src/Action/QrCodeAction.php index a8dee3fd..2f816c98 100644 --- a/module/Core/src/Action/QrCodeAction.php +++ b/module/Core/src/Action/QrCodeAction.php @@ -5,41 +5,25 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Action; use Endroid\QrCode\Builder\Builder; -use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh; -use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface; -use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow; -use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium; -use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile; -use Endroid\QrCode\Writer\SvgWriter; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Response\QrCodeResponse; +use Shlinkio\Shlink\Core\Action\Model\QrCodeParams; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; -use function strtoupper; -use function trim; - class QrCodeAction implements MiddlewareInterface { - private const DEFAULT_SIZE = 300; - private const MIN_SIZE = 50; - private const MAX_SIZE = 1000; - - private LoggerInterface $logger; - public function __construct( private ShortUrlResolverInterface $urlResolver, private ShortUrlStringifierInterface $stringifier, - ?LoggerInterface $logger = null + private LoggerInterface $logger ) { - $this->logger = $logger ?? new NullLogger(); } public function process(Request $request, RequestHandlerInterface $handler): Response @@ -53,55 +37,14 @@ class QrCodeAction implements MiddlewareInterface return $handler->handle($request); } - $query = $request->getQueryParams(); + $params = QrCodeParams::fromRequest($request); $qrCodeBuilder = Builder::create() ->data($this->stringifier->stringify($shortUrl)) - ->size($this->resolveSize($request, $query)) - ->margin($this->resolveMargin($query)) - ->errorCorrectionLevel($this->resolveErrorCorrection($query)); - - $format = $query['format'] ?? 'png'; - if ($format === 'svg') { - $qrCodeBuilder->writer(new SvgWriter()); - } + ->size($params->size()) + ->margin($params->margin()) + ->writer($params->writer()) + ->errorCorrectionLevel($params->errorCorrectionLevel()); return new QrCodeResponse($qrCodeBuilder->build()); } - - private function resolveSize(Request $request, array $query): int - { - // Size attribute is deprecated. After v3.0.0, always use the query param instead - $size = (int) $request->getAttribute('size', $query['size'] ?? self::DEFAULT_SIZE); - if ($size < self::MIN_SIZE) { - return self::MIN_SIZE; - } - - return $size > self::MAX_SIZE ? self::MAX_SIZE : $size; - } - - private function resolveMargin(array $query): int - { - $margin = $query['margin'] ?? null; - if ($margin === null) { - return 0; - } - - $intMargin = (int) $margin; - if ($margin !== (string) $intMargin) { - return 0; - } - - return $intMargin < 0 ? 0 : $intMargin; - } - - private function resolveErrorCorrection(array $query): ErrorCorrectionLevelInterface - { - $errorCorrectionLevel = strtoupper(trim($query['errorCorrection'] ?? '')); - return match ($errorCorrectionLevel) { - 'H' => new ErrorCorrectionLevelHigh(), - 'Q' => new ErrorCorrectionLevelQuartile(), - 'M' => new ErrorCorrectionLevelMedium(), - default => new ErrorCorrectionLevelLow(), // 'L' - }; - } } diff --git a/module/Core/test/Action/QrCodeActionTest.php b/module/Core/test/Action/QrCodeActionTest.php index 7326c41c..0595734e 100644 --- a/module/Core/test/Action/QrCodeActionTest.php +++ b/module/Core/test/Action/QrCodeActionTest.php @@ -14,6 +14,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\NullLogger; use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Core\Action\QrCodeAction; use Shlinkio\Shlink\Core\Entity\ShortUrl; @@ -41,6 +42,7 @@ class QrCodeActionTest extends TestCase $this->action = new QrCodeAction( $this->urlResolver->reveal(), new ShortUrlStringifier(['domain' => 'doma.in']), + new NullLogger(), ); } From 01e06f0503b26e11096535dbac2eb49bf8cb241b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 13 Jul 2021 13:53:10 +0200 Subject: [PATCH 3/5] Improved swagger docs for QR code endpoint --- docs/swagger/paths/{shortCode}_qr-code.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/swagger/paths/{shortCode}_qr-code.json b/docs/swagger/paths/{shortCode}_qr-code.json index 43c1d38a..04a88fd7 100644 --- a/docs/swagger/paths/{shortCode}_qr-code.json +++ b/docs/swagger/paths/{shortCode}_qr-code.json @@ -5,7 +5,7 @@ "URL Shortener" ], "summary": "Short URL QR code", - "description": "Generates a QR code image pointing to a short URL", + "description": "Generates a QR code image pointing to a short URL.
Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.", "parameters": [ { "name": "shortCode", @@ -35,7 +35,8 @@ "required": false, "schema": { "type": "string", - "enum": ["png", "svg"] + "enum": ["png", "svg"], + "default": "png" } }, { @@ -56,7 +57,8 @@ "required": false, "schema": { "type": "string", - "enum": ["L", "M", "Q", "H"] + "enum": ["L", "M", "Q", "H"], + "default": "L" } } ], From 67c7e503d97bb61765d3d7309e7a1ebf9acce9d3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 13 Jul 2021 13:55:00 +0200 Subject: [PATCH 4/5] Used lowercase values when trying to match the QR code error level --- module/Core/src/Action/Model/QrCodeParams.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/module/Core/src/Action/Model/QrCodeParams.php b/module/Core/src/Action/Model/QrCodeParams.php index bcde20c6..742d3f07 100644 --- a/module/Core/src/Action/Model/QrCodeParams.php +++ b/module/Core/src/Action/Model/QrCodeParams.php @@ -16,7 +16,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface as Request; use function strtolower; -use function strtoupper; use function trim; final class QrCodeParams @@ -82,12 +81,12 @@ final class QrCodeParams private static function resolveErrorCorrection(array $query): ErrorCorrectionLevelInterface { - $errorCorrectionLevel = strtoupper(trim($query['errorCorrection'] ?? '')); + $errorCorrectionLevel = strtolower(trim($query['errorCorrection'] ?? 'l')); return match ($errorCorrectionLevel) { - 'H' => new ErrorCorrectionLevelHigh(), - 'Q' => new ErrorCorrectionLevelQuartile(), - 'M' => new ErrorCorrectionLevelMedium(), - default => new ErrorCorrectionLevelLow(), // 'L' + 'h' => new ErrorCorrectionLevelHigh(), + 'q' => new ErrorCorrectionLevelQuartile(), + 'm' => new ErrorCorrectionLevelMedium(), + default => new ErrorCorrectionLevelLow(), // 'l' }; } From 6466045363a0107030acaa3df2983b0cd0f33381 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 13 Jul 2021 14:00:54 +0200 Subject: [PATCH 5/5] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7410ac..76e14a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this ## [Unreleased] ### Added * [#1089](https://github.com/shlinkio/shlink/issues/1089) Added new `ENABLE_PERIODIC_VISIT_LOCATE` env var to docker image which schedules the `visit:locate` command every hour when provided with value `true`. +* [#1082](https://github.com/shlinkio/shlink/issues/1082) Added support for error correction level on QR codes. + + Now, when calling the `GET /{shorCode}/qr-code` URL, you can pass the `errorCorrection` query param with values `L` for Low, `M` for Medium, `Q` for Quartile or `H` for High. ### Changed * *Nothing*