diff --git a/benchmark/OutputBenchmark.php b/benchmark/OutputBenchmark.php index a895a7097..84a9d56cc 100644 --- a/benchmark/OutputBenchmark.php +++ b/benchmark/OutputBenchmark.php @@ -14,7 +14,7 @@ use chillerlan\QRCode\Common\Mode; use chillerlan\QRCode\Data\Byte; use chillerlan\QRCode\Output\{ - QREps, QRFpdf, QRGdImageAVIF, QRGdImageJPEG, QRGdImagePNG, QRGdImageWEBP, QRImagick, QRMarkupSVG, QRStringJSON + QREps, QRFpdf, QRGdImageJPEG, QRGdImagePNG, QRGdImageWEBP, QRImagick, QRMarkupSVG, QRMarkupXML, QRStringJSON }; use PhpBench\Attributes\{BeforeMethods, Subject}; @@ -54,9 +54,9 @@ public function QRFpdf():void{ * for some reason imageavif() is extremely slow, ~50x slower than imagepng() */ #[Subject] - public function QRGdImageAVIF():void{ - (new QRGdImageAVIF($this->options, $this->matrix))->dump(); - } +# public function QRGdImageAVIF():void{ +# (new \chillerlan\QRCode\Output\QRGdImageAVIF($this->options, $this->matrix))->dump(); +# } #[Subject] public function QRGdImageJPEG():void{ @@ -83,6 +83,11 @@ public function QRMarkupSVG():void{ (new QRMarkupSVG($this->options, $this->matrix))->dump(); } + #[Subject] + public function QRMarkupXML():void{ + (new QRMarkupXML($this->options, $this->matrix))->dump(); + } + #[Subject] public function QRStringJSON():void{ (new QRStringJSON($this->options, $this->matrix))->dump(); diff --git a/examples/Readme.md b/examples/Readme.md index 2b56dafc2..e76264a6a 100644 --- a/examples/Readme.md +++ b/examples/Readme.md @@ -9,6 +9,7 @@ - [FPDF](./fpdf.php): PDF output via [FPDF](http://www.fpdf.org/) - [EPS](./eps.php): Encapsulated PostScript - [String](./text.php): String output +- [XML](./xml.php): XML output (rendered as SVG via an [XSLT style](./qrcode.style.xsl)) - [Multi mode](./multimode.php): a demostration of multi mode usage - [Reflectance](./reflectance.php): demonstrates reflectance reversal - [QRCode reader](./reader.php): a simple reader example diff --git a/examples/qrcode.style.xsl b/examples/qrcode.style.xsl new file mode 100644 index 000000000..16bc297cd --- /dev/null +++ b/examples/qrcode.style.xsl @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/xml.php b/examples/xml.php new file mode 100644 index 000000000..5ed68e2da --- /dev/null +++ b/examples/xml.php @@ -0,0 +1,70 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +use chillerlan\QRCode\{Data\QRMatrix, QRCode, QROptions}; +use chillerlan\QRCode\Output\QRMarkupXML; + +require_once __DIR__.'/../vendor/autoload.php'; + +$options = new QROptions; + +$options->version = 7; +$options->outputInterface = QRMarkupXML::class; +$options->outputBase64 = false; +$options->drawLightModules = false; + +// assign an XSLT stylesheet +$options->xmlStylesheet = './qrcode.style.xsl'; + +$options->moduleValues = [ + // finder + QRMatrix::M_FINDER_DARK => '#A71111', // dark (true) + QRMatrix::M_FINDER_DOT => '#A71111', // finder dot, dark (true) + QRMatrix::M_FINDER => '#FFBFBF', // light (false) + // alignment + QRMatrix::M_ALIGNMENT_DARK => '#A70364', + QRMatrix::M_ALIGNMENT => '#FFC9C9', + // timing + QRMatrix::M_TIMING_DARK => '#98005D', + QRMatrix::M_TIMING => '#FFB8E9', + // format + QRMatrix::M_FORMAT_DARK => '#003804', + QRMatrix::M_FORMAT => '#CCFB12', + // version + QRMatrix::M_VERSION_DARK => '#650098', + QRMatrix::M_VERSION => '#E0B8FF', + // data + QRMatrix::M_DATA_DARK => '#4A6000', + QRMatrix::M_DATA => '#ECF9BE', + // darkmodule + QRMatrix::M_DARKMODULE => '#080063', + // separator + QRMatrix::M_SEPARATOR => '#DDDDDD', + // quietzone + QRMatrix::M_QUIETZONE => '#DDDDDD', +]; + + +try{ + $out = (new QRCode($options))->render('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); +} +catch(Throwable $e){ + // handle the exception in whatever way you need + exit($e->getMessage()); +} + + +if(php_sapi_name() !== 'cli'){ + header('Content-type: '.QRMarkupXML::MIME_TYPE); +} + +echo $out; + +exit; diff --git a/src/Output/QRMarkupSVG.php b/src/Output/QRMarkupSVG.php index c49ce58f0..e6264029e 100644 --- a/src/Output/QRMarkupSVG.php +++ b/src/Output/QRMarkupSVG.php @@ -85,7 +85,7 @@ protected function createMarkup(bool $saveToFile):string{ // transform to data URI only when not saving to file if(!$saveToFile && $this->options->outputBase64){ - $svg = $this->toBase64DataURI($svg); + return $this->toBase64DataURI($svg); } return $svg; diff --git a/src/Output/QRMarkupXML.php b/src/Output/QRMarkupXML.php new file mode 100644 index 000000000..c24487590 --- /dev/null +++ b/src/Output/QRMarkupXML.php @@ -0,0 +1,142 @@ + + * @copyright 2024 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + * @phan-file-suppress PhanTypeMismatchArgumentInternal + */ + +namespace chillerlan\QRCode\Output; + +use DOMDocument; +use DOMElement; +use function sprintf; + +/** + * XML/XSLT output + */ +class QRMarkupXML extends QRMarkup{ + + final public const MIME_TYPE = 'application/xml'; + protected const XML_SCHEMA = 'https://raw.githubusercontent.com/chillerlan/php-qrcode/main/src/Output/qrcode.schema.xsd'; + + protected DOMDocument $dom; + + /** + * @inheritDoc + */ + protected function getOutputDimensions():array{ + return [$this->moduleCount, $this->moduleCount]; + } + + /** + * @inheritDoc + */ + protected function createMarkup(bool $saveToFile):string{ + /** @noinspection PhpComposerExtensionStubsInspection */ + $this->dom = new DOMDocument(encoding: 'UTF-8'); + $this->dom->formatOutput = true; + + if($this->options->xmlStylesheet !== null){ + $stylesheet = sprintf('type="text/xsl" href="%s"', $this->options->xmlStylesheet); + $xslt = $this->dom->createProcessingInstruction('xml-stylesheet', $stylesheet); + + $this->dom->appendChild($xslt); + } + + $root = $this->dom->createElement('qrcode'); + + $root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + $root->setAttribute('xsi:noNamespaceSchemaLocation', $this::XML_SCHEMA); + $root->setAttribute('version', $this->matrix->getVersion()); + $root->setAttribute('eccLevel', $this->matrix->getEccLevel()); + $root->appendChild($this->createMatrix()); + + $this->dom->appendChild($root); + + $xml = $this->dom->saveXML(); + + // transform to data URI only when not saving to file + if(!$saveToFile && $this->options->outputBase64){ + return $this->toBase64DataURI($xml); + } + + return $xml; + } + + /** + * Creates the matrix element + */ + protected function createMatrix():DOMElement{ + [$width, $height] = $this->getOutputDimensions(); + $matrix = $this->dom->createElement('matrix'); + $dimension = $this->matrix->getVersion()->getDimension(); + + $matrix->setAttribute('size', $dimension); + $matrix->setAttribute('quietzoneSize', (int)(($this->moduleCount - $dimension) / 2)); + $matrix->setAttribute('maskPattern', $this->matrix->getMaskPattern()->getPattern()); + $matrix->setAttribute('width', $width); + $matrix->setAttribute('height', $height); + + foreach($this->matrix->getMatrix() as $y => $row){ + $matrixRow = $this->row($y, $row); + + if($matrixRow !== null){ + $matrix->appendChild($matrixRow); + } + } + + return $matrix; + } + + /** + * Creates a DOM element for a matrix row + */ + protected function row(int $y, array $row):DOMElement|null{ + $matrixRow = $this->dom->createElement('row'); + + $matrixRow->setAttribute('y', $y); + + foreach($row as $x => $M_TYPE){ + $module = $this->module($x, $y, $M_TYPE); + + if($module !== null){ + $matrixRow->appendChild($module); + } + + } + + if($matrixRow->childElementCount > 0){ + return $matrixRow; + } + + // skip empty rows + return null; + } + + /** + * Creates a DOM element for single module + */ + protected function module(int $x, int $y, int $M_TYPE):DOMElement|null{ + $isDark = $this->matrix->isDark($M_TYPE); + + if(!$this->drawLightModules && !$isDark){ + return null; + } + + $module = $this->dom->createElement('module'); + + $module->setAttribute('x', $x); + $module->setAttribute('dark', ($isDark ? 'true' : 'false')); + $module->setAttribute('layer', ($this::LAYERNAMES[$M_TYPE] ?? '')); + $module->setAttribute('value', $this->getModuleValue($M_TYPE)); + + return $module; + } + +} diff --git a/src/Output/QROutputInterface.php b/src/Output/QROutputInterface.php index 94fdbba0e..78c768966 100644 --- a/src/Output/QROutputInterface.php +++ b/src/Output/QROutputInterface.php @@ -35,6 +35,7 @@ interface QROutputInterface{ QRImagick::class, QRMarkupHTML::class, QRMarkupSVG::class, + QRMarkupXML::class, QRStringJSON::class, QRStringText::class, ]; diff --git a/src/Output/qrcode.schema.xsd b/src/Output/qrcode.schema.xsd new file mode 100644 index 000000000..b56bdc60f --- /dev/null +++ b/src/Output/qrcode.schema.xsd @@ -0,0 +1,135 @@ + + + + + QR Code root element + + + + + + + + The ECC level: [L, M, Q, H] + + + + + + + + + + + + + The QR Code version: [1...40] + + + + + + + + + + + + + The matrix holds the encoded data in a 2-dimensional array of modules + + + + + + + + The total height of the matrix, including the quiet zone. + + + + + + + + + + The detected mask pattern that was used to mask this matrix. [0...7] + + + + + + + + + + The size of the quiet zone (margin around the QR symbol) + + + + + The side length of the QR symbol, excluding the quiet zone (version * 4 + 17). [21...177] + + + + + + + + + + + The total width of the matrix, including the quiet zone. + + + + + + + + + + + + A row holds an array of modules + + + + + + + + The "y" (vertical) coordinate of this row. + + + + + + + Represents a single module (pixel) of a QR symbol. + + + + + Indicates whether this module is dark. + + + + + The layer (functional pattern) this module belongs to. + + + + + The value for this module (CSS color). + + + + + The "x" (horizontal) coordinate of this module. + + + + + diff --git a/src/QROptionsTrait.php b/src/QROptionsTrait.php index 86a54b4ac..e224e7f85 100644 --- a/src/QROptionsTrait.php +++ b/src/QROptionsTrait.php @@ -426,6 +426,17 @@ trait QROptionsTrait{ */ protected string $fpdfMeasureUnit = 'pt'; + /* + * QRMarkupXML settings + */ + + /** + * Sets an optional XSLT stylesheet in the XML output + * + * @see https://developer.mozilla.org/en-US/docs/Web/XSLT + */ + protected ?string $xmlStylesheet = null; + /** * clamp min/max version number diff --git a/tests/Output/QRMarkupXMLTest.php b/tests/Output/QRMarkupXMLTest.php new file mode 100644 index 000000000..677c0e8af --- /dev/null +++ b/tests/Output/QRMarkupXMLTest.php @@ -0,0 +1,30 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\QRCodeTest\Output; + +use chillerlan\QRCode\QROptions; +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCode\Output\{QRMarkupXML, QROutputInterface}; +use chillerlan\Settings\SettingsContainerInterface; + +/** + * + */ +class QRMarkupXMLTest extends QRMarkupTestAbstract{ + + protected function getOutputInterface( + SettingsContainerInterface|QROptions $options, + QRMatrix $matrix, + ):QROutputInterface{ + return new QRMarkupXML($options, $matrix); + } + +}