|
|
@@ -0,0 +1,240 @@
|
|
|
+# 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`:
|
|
|
+
|
|
|
+```php
|
|
|
+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 `-`:
|
|
|
+
|
|
|
+```php
|
|
|
+ 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:
|
|
|
+
|
|
|
+```php
|
|
|
+ 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()`).
|
|
|
+
|
|
|
+```php
|
|
|
+ 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.
|
|
|
+
|
|
|
+```php
|
|
|
+ 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:
|
|
|
+
|
|
|
+```php
|
|
|
+ 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:
|
|
|
+
|
|
|
+```php
|
|
|
+$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](https://www.php.net/manual/en/functions.first_class_callable_syntax.php):
|
|
|
+
|
|
|
+```php
|
|
|
+$closure = $this->module(...);
|
|
|
+```
|
|
|
+
|
|
|
+This is our final output method then:
|
|
|
+
|
|
|
+```php
|
|
|
+ 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:
|
|
|
+
|
|
|
+```php
|
|
|
+$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:
|
|
|
+
|
|
|
+```php
|
|
|
+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);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+```
|