Skip to content

Commit

Permalink
📖
Browse files Browse the repository at this point in the history
  • Loading branch information
codemasher committed Nov 23, 2023
1 parent 4176e35 commit 34820bd
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 0 deletions.
240 changes: 240 additions & 0 deletions docs/Customizing/Custom-output-interface.md
Original file line number Diff line number Diff line change
@@ -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);
}

}
```
1 change: 1 addition & 0 deletions docs/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ The markdown sources for the [Read the Docs online manual](https://php-qrcode.re

- [Module values](./Customizing/Module-Values.md)
- [`QROutputAbstract`](./Customizing/QROutputAbstract.md)
- [Custom `QROutputInterface`](./Customizing/Custom-output-interface.md)


### Built-In Output Modules
Expand Down
1 change: 1 addition & 0 deletions docs/Usage/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ For the QR Code reader, either `ext-gd` or `ext-imagick` is required!
- [twill](https://github.com/area17/twill)
- [Elefant CMS](https://github.com/jbroadway/elefant)
- [OSIRIS](https://github.com/JKoblitz/osiris)
- [EspoCRM](https://github.com/espocrm/espocrm)
- Articles:
- [Twilio: How to Create a QR Code in PHP](https://www.twilio.com/blog/create-qr-code-in-php) (featuring v4.3.x)

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This work is licensed under the Creative Commons Attribution 4.0 International (

Customizing/Module-Values.md
Customizing/QROutputAbstract.md
Customizing/Custom-output-interface.md

.. toctree::
:maxdepth: 3
Expand Down

0 comments on commit 34820bd

Please sign in to comment.