From 81eb21a7f8ede1699cf6430be88838641a7e3138 Mon Sep 17 00:00:00 2001 From: smiley Date: Sat, 4 May 2024 19:33:12 +0200 Subject: [PATCH] :octocat: QRStringJSON overhaul --- src/Output/QRStringJSON.php | 92 ++++++++++++++----- src/Output/qrcode.schema.json | 142 ++++++++++++++++++++++++++++++ src/QROptionsTrait.php | 10 +-- tests/Output/QRStringJSONTest.php | 40 ++++----- 4 files changed, 230 insertions(+), 54 deletions(-) create mode 100644 src/Output/qrcode.schema.json diff --git a/src/Output/QRStringJSON.php b/src/Output/QRStringJSON.php index a4acc9c08..3182129d4 100644 --- a/src/Output/QRStringJSON.php +++ b/src/Output/QRStringJSON.php @@ -18,16 +18,52 @@ use function json_encode; * */ class QRStringJSON extends QROutputAbstract{ + use CssColorModuleValueTrait; final public const MIME_TYPE = 'application/json'; + final public const SCHEMA = 'https://raw.githubusercontent.com/chillerlan/php-qrcode/main/src/Output/qrcode.schema.json'; + + /** + * @inheritDoc + */ + protected function getOutputDimensions():array{ + return [$this->moduleCount, $this->moduleCount]; + } /** * @inheritDoc * @throws \JsonException */ public function dump(string $file = null):string{ - $matrix = $this->matrix->getMatrix($this->options->jsonAsBooleans); - $data = json_encode($matrix, $this->options->jsonFlags);; + [$width, $height] = $this->getOutputDimensions(); + $version = $this->matrix->getVersion(); + $dimension = $version->getDimension(); + + $json = [ + '$schema' => $this::SCHEMA, + 'qrcode' => [ + 'version' => $version->getVersionNumber(), + 'eccLevel' => (string)$this->matrix->getEccLevel(), + 'matrix' => [ + 'size' => $dimension, + 'quietzoneSize' => (int)(($this->moduleCount - $dimension) / 2), + 'maskPattern' => $this->matrix->getMaskPattern()->getPattern(), + 'width' => $width, + 'height' => $height, + 'rows' => [], + ], + ], + ]; + + foreach($this->matrix->getMatrix() as $y => $row){ + $matrixRow = $this->row($y, $row); + + if($matrixRow !== null){ + $json['qrcode']['matrix']['rows'][] = $matrixRow; + } + } + + $data = json_encode($json, $this->options->jsonFlags);; $this->saveToFile($data, $file); @@ -35,33 +71,43 @@ class QRStringJSON extends QROutputAbstract{ } /** - * unused - required by interface - * - * @inheritDoc - * @codeCoverageIgnore + * Creates an array element for a matrix row */ - protected function prepareModuleValue(mixed $value):string{ - return ''; + protected function row(int $y, array $row):array|null{ + $matrixRow = ['y' => $y, 'modules' => []]; + + foreach($row as $x => $M_TYPE){ + $module = $this->module($x, $y, $M_TYPE); + + if($module !== null){ + $matrixRow['modules'][] = $module; + } + } + + if(!empty($matrixRow['modules'])){ + return $matrixRow; + } + + // skip empty rows + return null; } /** - * unused - required by interface - * - * @inheritDoc - * @codeCoverageIgnore + * Creates an array element for a single module */ - protected function getDefaultModuleValue(bool $isDark):string{ - return ''; - } + protected function module(int $x, int $y, int $M_TYPE):array|null{ + $isDark = $this->matrix->isDark($M_TYPE); - /** - * unused - required by interface - * - * @inheritDoc - * @codeCoverageIgnore - */ - public static function moduleValueIsValid(mixed $value):bool{ - return true; + if(!$this->drawLightModules && !$isDark){ + return null; + } + + return [ + 'x' => $x, + 'dark' => $isDark, + 'layer' => ($this::LAYERNAMES[$M_TYPE] ?? ''), + 'value' => $this->getModuleValue($M_TYPE), + ]; } } diff --git a/src/Output/qrcode.schema.json b/src/Output/qrcode.schema.json new file mode 100644 index 000000000..734b4c43c --- /dev/null +++ b/src/Output/qrcode.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "chillerlan php-qrcode schema", + "type": "object", + "required": [ + "qrcode" + ], + "properties": { + "qrcode": { + "$ref": "#/$defs/qrcode" + } + }, + "$defs": { + "qrcode": { + "description": "QR Code root element", + "type": "object", + "required": [ + "eccLevel", + "matrix", + "version" + ], + "properties": { + "version": { + "description": "The QR Code version: [1...40]", + "type": "number", + "minimum": 1, + "maximum": 40 + }, + "eccLevel": { + "description": "The ECC level: [L, M, Q, H]", + "enum": [ + "L", + "M", + "Q", + "H" + ] + }, + "matrix": { + "$ref": "#/$defs/matrix" + } + } + }, + "matrix": { + "description": "The matrix holds the encoded data in a 2-dimensional array of modules.", + "type": "object", + "required": [ + "size", + "quietzoneSize", + "maskPattern", + "width", + "height" + ], + "properties": { + "size": { + "description": "The side length of the QR symbol, excluding the quiet zone (version * 4 + 17). [21...177]", + "type": "number", + "minimum": 21, + "maximum": 177 + }, + "quietzoneSize": { + "description": "The size of the quiet zone (margin around the QR symbol).", + "type": "number", + "minimum": 0 + }, + "maskPattern": { + "description": "The detected mask pattern that was used to mask this matrix. [0...7].", + "type": "number", + "minimum": 0, + "maximum": 7 + }, + "width": { + "description": "The total width of the matrix, including the quiet zone.", + "type": "number", + "minimum": 21 + }, + "height": { + "description": "The total height of the matrix, including the quiet zone.", + "type": "number", + "minimum": 21 + }, + "rows": { + "type": "array", + "items": { + "$ref": "#/$defs/row" + }, + "minItems": 0 + } + } + }, + "row": { + "description": "A row holds an array of modules", + "type": "object", + "required": [ + "y", + "modules" + ], + "properties": { + "y": { + "description": "The 'y' (vertical) coordinate of this row.", + "type": "number", + "minimum": 0 + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/$defs/module" + }, + "minItems": 0 + } + } + }, + "module": { + "description": "Represents a single module (pixel) of a QR symbol.", + "type": "object", + "required": [ + "dark", + "layer", + "value", + "x" + ], + "properties": { + "dark": { + "description": "Indicates whether this module is dark.", + "type": "boolean" + }, + "layer": { + "description": "The layer (functional pattern) this module belongs to.", + "type": "string" + }, + "value": { + "description": "The value for this module.", + "type": "string" + }, + "x": { + "description": "The 'x' (horizontal) coordinate of this module.", + "type": "integer", + "minimum": 0 + } + } + } + } +} diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index e224e7f85..6a50b7671 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -17,7 +17,8 @@ namespace chillerlan\QRCode; use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version}; use chillerlan\QRCode\Output\QRMarkupSVG; use function constant, in_array, is_string, max, min, sprintf, strtolower, strtoupper, trim; -use const JSON_THROW_ON_ERROR, PHP_EOL; +use const JSON_THROW_ON_ERROR, JSON_UNESCAPED_SLASHES, PHP_EOL; + /** * The QRCode plug-in settings & setter functionality @@ -408,12 +409,7 @@ trait QROptionsTrait{ * * @see https://www.php.net/manual/json.constants.php */ - protected int $jsonFlags = JSON_THROW_ON_ERROR; - - /** - * Whether to return matrix values in JSON as booleans or `$M_TYPE` integers - */ - protected bool $jsonAsBooleans = false; + protected int $jsonFlags = JSON_THROW_ON_ERROR|JSON_UNESCAPED_SLASHES; /* * QRFpdf settings diff --git a/tests/Output/QRStringJSONTest.php b/tests/Output/QRStringJSONTest.php index d339f61af..36ef4f995 100644 --- a/tests/Output/QRStringJSONTest.php +++ b/tests/Output/QRStringJSONTest.php @@ -14,25 +14,12 @@ use chillerlan\QRCode\QROptions; use chillerlan\QRCode\Data\QRMatrix; use chillerlan\QRCode\Output\{QROutputInterface, QRStringJSON}; use chillerlan\Settings\SettingsContainerInterface; -use PHPUnit\Framework\Attributes\DataProvider; -use function extension_loaded; /** * */ final class QRStringJSONTest extends QROutputTestAbstract{ - - /** - * @inheritDoc - */ - protected function setUp():void{ - // just in case someone's running this on some weird distro that's been compiled without ext-json - if(!extension_loaded('json')){ - $this::markTestSkipped('ext-json not loaded'); - } - - parent::setUp(); - } + use CssColorModuleValueProviderTrait; protected function getOutputInterface( SettingsContainerInterface|QROptions $options, @@ -41,17 +28,22 @@ final class QRStringJSONTest extends QROutputTestAbstract{ return new QRStringJSON($options, $matrix); } - public static function moduleValueProvider():array{ - return [[null, false]]; - } - - #[DataProvider('moduleValueProvider')] - public function testValidateModuleValues(mixed $value, bool $expected):void{ - $this::markTestSkipped('N/A (JSON test)'); - } - + /** + * @inheritDoc + */ public function testSetModuleValues():void{ - $this::markTestSkipped('N/A (JSON test)'); + + $this->options->moduleValues = [ + // data + QRMatrix::M_DATA_DARK => '#AAA', + QRMatrix::M_DATA => '#BBB', + ]; + + $this->outputInterface = $this->getOutputInterface($this->options, $this->matrix); + $data = $this->outputInterface->dump(); + + $this::assertStringContainsString('"layer":"data-dark","value":"#AAA"', $data); + $this::assertStringContainsString('"layer":"data","value":"#BBB"', $data); } }