Custom QROutputInterface

Let’s suppose that we want to create our own output interface because there’s no built-in output class that supports the format we need for our 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, we 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, we can call prepareModuleValue() here if necessary. We’ll return an empty string '' 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, which 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 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);
	}

}