ソースを参照

:sparkles: XML output!?

smiley 1 年間 前
コミット
1f73485224

+ 9 - 4
benchmark/OutputBenchmark.php

@@ -14,7 +14,7 @@ namespace chillerlan\QRCodeBenchmark;
 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 @@ final class OutputBenchmark extends BenchmarkAbstract{
 	 * 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 @@ final class OutputBenchmark extends BenchmarkAbstract{
 		(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();

+ 1 - 0
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

+ 36 - 0
examples/qrcode.style.xsl

@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- XSLT style for the XML output example -->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+	<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
+	<xsl:template match="/">
+		<!-- SVG header -->
+		<svg xmlns="http://www.w3.org/2000/svg"
+		     version="1.0"
+		     viewBox="0 0 {qrcode/matrix/@width} {qrcode/matrix/@height}"
+		     preserveAspectRatio="xMidYMid"
+		>
+			<!--
+				path for a single module
+				we could define a path for each layer and use the @layer attribute for selection,
+				but that would exaggerate this example
+			-->
+			<symbol id="module" width="1" height="1">
+				<circle cx="0.5" cy="0.5" r="0.4" />
+			</symbol>
+			<!-- loop over the rows -->
+			<xsl:for-each select="qrcode/matrix/row">
+				<!-- set a variable for $y (vertical) -->
+				<xsl:variable name="y" select="@y"/>
+				<xsl:for-each select="module">
+					<!-- set a variable for $x (horizontal) -->
+					<xsl:variable name="x" select="@x"/>
+					<!-- draw only dark modules -->
+					<xsl:if test="@dark='true'">
+						<!-- position the module and set its fill color -->
+						<use href="#module" class="{@layer}" x="{$x}" y="{$y}" fill="{@value}"/>
+					</xsl:if>
+				</xsl:for-each>
+			</xsl:for-each>
+		</svg>
+	</xsl:template>
+</xsl:stylesheet>

+ 70 - 0
examples/xml.php

@@ -0,0 +1,70 @@
+<?php
+/**
+ * XML output example (not a meme)
+ *
+ * @created      02.05.2024
+ * @author       smiley <smiley@chillerlan.net>
+ * @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;

+ 1 - 1
src/Output/QRMarkupSVG.php

@@ -85,7 +85,7 @@ class QRMarkupSVG extends QRMarkup{
 
 		// 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;

+ 142 - 0
src/Output/QRMarkupXML.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * Class QRMarkupXML
+ *
+ * @created      01.05.2024
+ * @author       smiley <smiley@chillerlan.net>
+ * @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;
+	}
+
+}

+ 1 - 0
src/Output/QROutputInterface.php

@@ -35,6 +35,7 @@ interface QROutputInterface{
 		QRImagick::class,
 		QRMarkupHTML::class,
 		QRMarkupSVG::class,
+		QRMarkupXML::class,
 		QRStringJSON::class,
 		QRStringText::class,
 	];

+ 135 - 0
src/Output/qrcode.schema.xsd

@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
+	<xs:element name="qrcode">
+		<xs:annotation>
+			<xs:documentation>QR Code root element</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element ref="matrix"/>
+			</xs:sequence>
+			<xs:attribute name="eccLevel" use="required">
+				<xs:annotation>
+					<xs:documentation>The ECC level: [L, M, Q, H]</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:string">
+						<xs:enumeration value="H"/>
+						<xs:enumeration value="L"/>
+						<xs:enumeration value="M"/>
+						<xs:enumeration value="Q"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="version" use="required">
+				<xs:annotation>
+					<xs:documentation>The QR Code version: [1...40]</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:nonNegativeInteger">
+						<xs:minInclusive value="1"/>
+						<xs:maxInclusive value="40"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="matrix">
+		<xs:annotation>
+			<xs:documentation>The matrix holds the encoded data in a 2-dimensional array of modules</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element minOccurs="0" maxOccurs="unbounded" ref="row"/>
+			</xs:sequence>
+			<xs:attribute name="height" use="required">
+				<xs:annotation>
+					<xs:documentation>The total height of the matrix, including the quiet zone.</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:nonNegativeInteger">
+						<xs:minInclusive value="21"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="maskPattern" use="required">
+				<xs:annotation>
+					<xs:documentation>The detected mask pattern that was used to mask this matrix. [0...7]</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:nonNegativeInteger">
+						<xs:maxInclusive value="7"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="quietzoneSize" use="required" type="xs:nonNegativeInteger">
+				<xs:annotation>
+					<xs:documentation>The size of the quiet zone (margin around the QR symbol)</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="size" use="required">
+				<xs:annotation>
+					<xs:documentation>The side length of the QR symbol, excluding the quiet zone (version * 4 + 17). [21...177]</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:nonNegativeInteger">
+						<xs:minInclusive value="21"/>
+						<xs:maxInclusive value="177"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="width" use="required">
+				<xs:annotation>
+					<xs:documentation>The total width of the matrix, including the quiet zone.</xs:documentation>
+				</xs:annotation>
+				<xs:simpleType>
+					<xs:restriction base="xs:nonNegativeInteger">
+						<xs:minInclusive value="21"/>
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="row">
+		<xs:annotation>
+			<xs:documentation>A row holds an array of modules</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:sequence>
+				<xs:element minOccurs="0" maxOccurs="unbounded" ref="module"/>
+			</xs:sequence>
+			<xs:attribute name="y" use="required" type="xs:nonNegativeInteger">
+				<xs:annotation>
+					<xs:documentation>The "y" (vertical) coordinate of this row.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="module">
+		<xs:annotation>
+			<xs:documentation>Represents a single module (pixel) of a QR symbol.</xs:documentation>
+		</xs:annotation>
+		<xs:complexType>
+			<xs:attribute name="dark" use="required" type="xs:boolean">
+				<xs:annotation>
+					<xs:documentation>Indicates whether this module is dark.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="layer" use="required" type="xs:normalizedString">
+				<xs:annotation>
+					<xs:documentation>The layer (functional pattern) this module belongs to.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="value" use="required" type="xs:normalizedString">
+				<xs:annotation>
+					<xs:documentation>The value for this module (CSS color).</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+			<xs:attribute name="x" use="required" type="xs:nonNegativeInteger">
+				<xs:annotation>
+					<xs:documentation>The "x" (horizontal) coordinate of this module.</xs:documentation>
+				</xs:annotation>
+			</xs:attribute>
+		</xs:complexType>
+	</xs:element>
+</xs:schema>

+ 11 - 0
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

+ 30 - 0
tests/Output/QRMarkupXMLTest.php

@@ -0,0 +1,30 @@
+<?php
+/**
+ * Class QRMarkupXMLTest
+ *
+ * @created      01.05.2024
+ * @author       smiley <smiley@chillerlan.net>
+ * @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);
+	}
+
+}