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);
+ }
+
+}