From 34820bd56f562f76cf4b054be81c8c2d5ee45cb9 Mon Sep 17 00:00:00 2001 From: smiley Date: Thu, 23 Nov 2023 18:08:11 +0100 Subject: [PATCH] :book: --- docs/Customizing/Custom-output-interface.md | 240 ++++++++++++++++++++ docs/Readme.md | 1 + docs/Usage/Overview.md | 1 + docs/index.rst | 1 + 4 files changed, 243 insertions(+) create mode 100644 docs/Customizing/Custom-output-interface.md diff --git a/docs/Customizing/Custom-output-interface.md b/docs/Customizing/Custom-output-interface.md new file mode 100644 index 000000000..e1cb52ba3 --- /dev/null +++ b/docs/Customizing/Custom-output-interface.md @@ -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); + } + +} +``` diff --git a/docs/Readme.md b/docs/Readme.md index 1739292b3..160a7a14e 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -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 diff --git a/docs/Usage/Overview.md b/docs/Usage/Overview.md index c763d4878..a6ab1c00d 100644 --- a/docs/Usage/Overview.md +++ b/docs/Usage/Overview.md @@ -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) diff --git a/docs/index.rst b/docs/index.rst index a2e042bd7..99784fbc8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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