Custom-output-interface.md.txt 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. # Custom `QROutputInterface`
  2. 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.
  3. In this example we'll create a string output class that outputs the coordinates for each module, separated by module type.
  4. ## Class skeleton
  5. We'll start with a skeleton that extends `QROutputAbstract` and implements the methods that are required by `QROutputInterface`:
  6. ```php
  7. class MyCustomOutput extends QROutputAbstract{
  8. public static function moduleValueIsValid($value):bool{}
  9. protected function prepareModuleValue($value){}
  10. protected function getDefaultModuleValue(bool $isDark){}
  11. public function dump(string $file = null){}
  12. }
  13. ```
  14. ## Module values
  15. 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.
  16. 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.
  17. In this example we'll accept string values, the characters `a-z` (case-insensitive) and a hyphen `-`:
  18. ```php
  19. public static function moduleValueIsValid($value):bool{
  20. return is_string($value) && preg_match('/^[a-z-]+$/i', $value) === 1;
  21. }
  22. ```
  23. 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.
  24. 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.
  25. For our example, we'll lowercase the validated string:
  26. ```php
  27. protected function prepareModuleValue($value):string{
  28. return strtolower($value);
  29. }
  30. ```
  31. Finally, we need to provide a default value for dark and light, we can call `prepareModuleValue()` here if necessary.
  32. We'll return an empty string `''` as we're going to use the `QROutputInterface::LAYERNAMES` constant for non-existing values
  33. (returning `null` would run into an exception in `QROutputAbstract::getModuleValue()`).
  34. ```php
  35. protected function getDefaultModuleValue(bool $isDark):string{
  36. return '';
  37. }
  38. ```
  39. ## Transform the output
  40. In our example, we want to collect the modules by type and have the collections listed under a header for each type.
  41. In order to do so, we need to collect the modules per `$M_TYPE` before we can render the final output.
  42. ```php
  43. public function dump(string $file = null):string{
  44. $collections = [];
  45. // loop over the matrix and collect the modules per layer
  46. foreach($this->matrix->getMatrix() as $y => $row){
  47. foreach($row as $x => $M_TYPE){
  48. $collections[$M_TYPE][] = $this->module($x, $y, $M_TYPE);
  49. }
  50. }
  51. // build the final output
  52. $out = [];
  53. foreach($collections as $M_TYPE => $collection){
  54. $name = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
  55. // the section header
  56. $out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
  57. // the list of modules
  58. $out[] = sprintf("%s\n", implode("\n", $collection));
  59. }
  60. return implode("\n", $out);
  61. }
  62. ```
  63. We've introduced another method that handles the module rendering, which incooperates handling of the `QROptions::$drawLightModules` setting:
  64. ```php
  65. protected function module(int $x, int $y, int $M_TYPE):string{
  66. if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
  67. return '';
  68. }
  69. return sprintf('x: %s, y: %s', $x, $y);
  70. }
  71. ```
  72. 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!
  73. We'll modify the above `dump()` method to use `QROutputAbstract::collectModules()` instead.
  74. The module collector accepts a `Closure` as its only parameter, which is called with 4 parameters:
  75. - `$x` : current column
  76. - `$y` : current row
  77. - `$M_TYPE` : field value
  78. - `$M_TYPE_LAYER`: (possibly modified) field value that acts as layer id
  79. We only need the first 3 parameters, so our closure would look as follows:
  80. ```php
  81. $closure = fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE);
  82. ```
  83. 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):
  84. ```php
  85. $closure = $this->module(...);
  86. ```
  87. This is our final output method then:
  88. ```php
  89. public function dump(string $file = null):string{
  90. $collections = $this->collectModules($this->module(...));
  91. // build the final output
  92. $out = [];
  93. foreach($collections as $M_TYPE => $collection){
  94. $name = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
  95. // the section header
  96. $out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
  97. // the list of modules
  98. $out[] = sprintf("%s\n", implode("\n", $collection));
  99. }
  100. return implode("\n", $out);
  101. }
  102. ```
  103. ## Run the custom output
  104. To run the output we just need to set the `QROptions::$outputInterface` to our custom class:
  105. ```php
  106. $options = new QROptions;
  107. $options->outputType = QROutputInterface::CUSTOM;
  108. $options->outputInterface = MyCustomOutput::class;
  109. $options->connectPaths = true;
  110. $options->drawLightModules = true;
  111. // our custom module values
  112. $options->moduleValues = [
  113. QRMatrix::M_DATA => 'these-modules-are-light',
  114. QRMatrix::M_DATA_DARK => 'here-is-a-dark-module',
  115. ];
  116. $qrcode = new QRCode($options);
  117. $qrcode->addByteSegment('test');
  118. var_dump($qrcode->render());
  119. ```
  120. The output looks similar to the following:
  121. ```
  122. these-modules-are-light (000000000010)
  123. x: 0, y: 0
  124. x: 1, y: 0
  125. x: 2, y: 0
  126. ...
  127. here-is-a-dark-module (100000000010)
  128. x: 4, y: 4
  129. x: 5, y: 4
  130. x: 6, y: 4
  131. ...
  132. ```
  133. Profit!
  134. ## Summary
  135. 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.
  136. The full code of our custom class below:
  137. ```php
  138. class MyCustomOutput extends QROutputAbstract{
  139. protected function prepareModuleValue($value):string{
  140. return strtolower($value);
  141. }
  142. protected function getDefaultModuleValue(bool $isDark):string{
  143. return '';
  144. }
  145. public static function moduleValueIsValid($value):bool{
  146. return is_string($value) && preg_match('/^[a-z-]+$/i', $value) === 1;
  147. }
  148. public function dump(string $file = null):string{
  149. $collections = $this->collectModules($this->module(...));
  150. // build the final output
  151. $out = [];
  152. foreach($collections as $M_TYPE => $collection){
  153. $name = ($this->getModuleValue($M_TYPE) ?: $this::LAYERNAMES[$M_TYPE]);
  154. // the section header
  155. $out[] = sprintf("%s (%012b)\n", $name, $M_TYPE);
  156. // the list of modules
  157. $out[] = sprintf("%s\n", implode("\n", $collection));
  158. }
  159. return implode("\n", $out);
  160. }
  161. protected function module(int $x, int $y, int $M_TYPE):string{
  162. if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
  163. return '';
  164. }
  165. return sprintf('x: %s, y: %s', $x, $y);
  166. }
  167. }
  168. ```