+
+ +
+

Custom QROutputInterface

+

Let’s suppose that you want to create your own output interface because there’s no built-in output class that supports the format you need for your application. +In this example we’ll create a string output class that outputs the coordinates for each module, separated by module type.

+
+

Class skeleton

+

We’ll start with a skeleton that extends QROutputAbstract and implements the methods that are required by QROutputInterface:

+
class MyCustomOutput extends QROutputAbstract{
+
+	public static function moduleValueIsValid($value):bool{}
+
+	protected function prepareModuleValue($value){}
+
+	protected function getDefaultModuleValue(bool $isDark){}
+
+	public function dump(string $file = null){}
+
+}
+
+
+
+
+

Module values

+

The validator should check whether the given input value and range is valid for the output class and if it can be given to the QROutputAbstract::prepareModuleValue() method. +For example in the built-in GD output it would check if the value is an array that has a minimum of 3 elements (for RGB), each of which is numeric.

+

In this example we’ll accept string values, the characters a-z (case-insensitive) and a hyphen -:

+
	public static function moduleValueIsValid($value):bool{
+		return is_string($value) && preg_match('/^[a-z-]+$/i', $value) === 1;
+	}
+
+
+

To prepare the final module substitute, you should transform the given (validated) input value in a way so that it can be accessed without any further calls or transformation. +In the built-in output for example this means it would return an ImagickPixel instance or the integer value returned by imagecolorallocate() on the current GdImage instance.

+

For our example, we’ll lowercase the validated string:

+
	protected function prepareModuleValue($value):string{
+		return strtolower($value);
+	}
+
+
+

Finally, we need to provide a default value for dark and light, you can call prepareModuleValue() here if necessary. +We’ll return an empty string '' here as we’re going to use the QROutputInterface::LAYERNAMES constant for non-existing values +(returning null would run into an exception in QROutputAbstract::getModuleValue()).

+
	protected function getDefaultModuleValue(bool $isDark):string{
+		return '';
+	}
+
+
+
+
+

Transform the output

+

In our example, we want to collect the modules by type and have the collections listed under a header for each type. +In order to do so, we need to collect the modules per $M_TYPE before we can render the final output.

+
	public function dump(string $file = null):string{
+		$collections = [];
+
+		// loop over the matrix and collect the modules per layer
+		foreach($this->matrix->getMatrix() as $y => $row){
+			foreach($row as $x => $M_TYPE){
+				$collections[$M_TYPE][] = $this->module($x, $y, $M_TYPE);
+			}
+		}
+
+		// build the final output
+		$out = [];
+
+		foreach($collections as $M_TYPE => $collection){
+			$name  = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
+			// the section header
+			$out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
+			// the list of modules
+			$out[] = sprintf("%s\n", implode("\n", $collection));
+		}
+
+		return implode("\n", $out);
+	}
+
+
+

We’ve introduced another method that handles the module rendering, which incooperates handling of the QROptions::$drawLightModules setting:

+
	protected function module(int $x, int $y, int $M_TYPE):string{
+
+		if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
+			return '';
+		}
+
+		return sprintf('x: %s, y: %s', $x, $y);
+	}
+
+
+

Speaking of option settings, there’s also QROptions::$connectPaths which we haven’t taken care of yet - the good news is that we don’t need to as it is already implemented! +We’ll modify the above dump() method to use QROutputAbstract::collectModules() instead.

+

The module collector accepts a closure as its only parameter, the closure is called with 4 parameters:

+
    +
  • $x : current column

  • +
  • $y : current row

  • +
  • $M_TYPE : field value

  • +
  • $M_TYPE_LAYER: (possibly modified) field value that acts as layer id

  • +
+

We’ll only need the first 3 parameters, so our closure would look as follows:

+
$closure = fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE);
+
+
+

As of PHP 8.1+ we can narrow this down with the first class callable syntax:

+
$closure = $this->module(...);
+
+
+

This is our final output method then:

+
	public function dump(string $file = null):string{
+		$collections = $this->collectModules($this->module(...));
+
+		// build the final output
+		$out = [];
+
+		foreach($collections as $M_TYPE => $collection){
+			$name  = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
+			// the section header
+			$out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
+			// the list of modules
+			$out[] = sprintf("%s\n", implode("\n", $collection));
+		}
+
+		return implode("\n", $out);
+	}
+
+
+
+
+

Run the custom output

+

To run the output we just need to set the QROptions::$outputInterface to our custom class:

+
$options = new QROptions;
+$options->outputType       = QROutputInterface::CUSTOM;
+$options->outputInterface  = MyCustomOutput::class;
+$options->connectPaths     = true;
+$options->drawLightModules = true;
+
+// our custom module values
+$options->moduleValues    = [
+	QRMatrix::M_DATA      => 'these-modules-are-light',
+	QRMatrix::M_DATA_DARK => 'here-is-a-dark-module',
+];
+
+$qrcode = new QRCode($options);
+$qrcode->addByteSegment('test');
+
+var_dump($qrcode->render());
+
+
+

The output looks similar to the following:

+
these-modules-are-light (000000000010)
+
+x: 0, y: 0
+x: 1, y: 0
+x: 2, y: 0
+...
+
+here-is-a-dark-module (100000000010)
+
+x: 4, y: 4
+x: 5, y: 4
+x: 6, y: 4
+...
+
+
+

Profit!

+
+
+

Summary

+

We’ve learned how to create a custom output class for a string based format similar to several of the built-in formats such as SVG or EPS.

+

The full code of our custom class below:

+
class MyCustomOutput extends QROutputAbstract{
+
+	protected function prepareModuleValue($value):string{
+		return strtolower($value);
+	}
+
+	protected function getDefaultModuleValue(bool $isDark):string{
+		return '';
+	}
+
+	public static function moduleValueIsValid($value):bool{
+		return is_string($value) && preg_match('/^[a-z-]+$/i', $value) === 1;
+	}
+
+	public function dump(string $file = null):string{
+		$collections = $this->collectModules($this->module(...));
+
+		// build the final output
+		$out = [];
+
+		foreach($collections as $M_TYPE => $collection){
+			$name  = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
+			// the section header
+			$out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
+			// the list of modules
+			$out[] = sprintf("%s\n", implode("\n", $collection));
+		}
+
+		return implode("\n", $out);
+	}
+
+	protected function module(int $x, int $y, int $M_TYPE):string{
+
+		if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
+			return '';
+		}
+
+		return sprintf('x: %s, y: %s', $x, $y);
+	}
+
+}
+
+
+
+
+ + +
+