svgRoundQuietzone.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <?php
  2. /**
  3. * @see https://github.com/chillerlan/php-qrcode/discussions/137
  4. *
  5. * @created 09.07.2022
  6. * @author smiley <smiley@chillerlan.net>
  7. * @copyright 2022 smiley
  8. * @license MIT
  9. *
  10. * @noinspection PhpIllegalPsrClassPathInspection
  11. */
  12. use chillerlan\QRCode\Common\EccLevel;
  13. use chillerlan\QRCode\Data\QRMatrix;
  14. use chillerlan\QRCode\Output\{QROutputInterface, QRMarkupSVG};
  15. use chillerlan\QRCode\{QRCode, QROptions};
  16. require_once __DIR__.'/../vendor/autoload.php';
  17. /*
  18. * Class definition
  19. */
  20. /**
  21. * the extended SVG output module
  22. */
  23. class RoundQuietzoneSVGoutput extends QRMarkupSVG{
  24. /**
  25. * @inheritDoc
  26. */
  27. protected function createMarkup(bool $saveToFile):string{
  28. // some Pythagorean magick
  29. $diameter = sqrt(2 * pow($this->moduleCount + $this->options->additionalModules, 2));
  30. // calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it
  31. $quietzoneSize = (int)ceil(($diameter - $this->moduleCount) / 2) + 1;
  32. // add the quiet zone to fill the circle
  33. $this->matrix->setQuietZone($quietzoneSize);
  34. // update the matrix dimensions to avoid errors in subsequent calculations
  35. // the moduleCount is now QR Code matrix + 2x quiet zone
  36. $this->setMatrixDimensions();
  37. // color the quiet zone
  38. $this->colorQuietzone($quietzoneSize, $diameter / 2);
  39. // start SVG output
  40. $svg = $this->header();
  41. if(!empty($this->options->svgDefs)){
  42. $svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
  43. }
  44. $svg .= $this->paths();
  45. $svg .= $this->addCircle($diameter / 2);
  46. // close svg
  47. $svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
  48. // transform to data URI only when not saving to file
  49. if(!$saveToFile && $this->options->imageBase64){
  50. $svg = $this->toBase64DataURI($svg, 'image/svg+xml');
  51. }
  52. return $svg;
  53. }
  54. /**
  55. * @inheritDoc
  56. */
  57. protected function path(string $path, int $M_TYPE):string{
  58. // omit the "fill" and "opacity" attributes on the path element
  59. return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
  60. }
  61. /**
  62. * Sets random modules of the quiet zone to dark
  63. */
  64. protected function colorQuietzone(int $quietzoneSize, float $radius):void{
  65. $l1 = $quietzoneSize - 1;
  66. $l2 = $this->moduleCount - $quietzoneSize;
  67. // substract 1/2 stroke width and module radius from the circle radius to not cut off modules
  68. $r = $radius - $this->options->circleRadius * 2;
  69. foreach($this->matrix->matrix() as $y => $row){
  70. foreach($row as $x => $value){
  71. // skip anything that's not quiet zone
  72. if($value !== QRMatrix::M_QUIETZONE){
  73. continue;
  74. }
  75. // leave one row of quiet zone around the matrix
  76. if(
  77. ($x === $l1 && $y >= $l1 && $y <= $l2)
  78. || ($x === $l2 && $y >= $l1 && $y <= $l2)
  79. || ($y === $l1 && $x >= $l1 && $x <= $l2)
  80. || ($y === $l2 && $x >= $l1 && $x <= $l2)
  81. ){
  82. continue;
  83. }
  84. // we need to add 0.5 units to the check values since we're calculating the element centers
  85. // ($x/$y is the element's assumed top left corner)
  86. if($this->checkIfInsideCircle($x + 0.5, $y + 0.5, $r)){
  87. $this->matrix->set($x, $y, (bool)rand(0, 1), QRMatrix::M_QUIETZONE);
  88. }
  89. }
  90. }
  91. }
  92. /**
  93. * @see https://stackoverflow.com/a/7227057
  94. */
  95. protected function checkIfInsideCircle(float $x, float $y, float $radius):bool{
  96. $dx = abs($x - $this->moduleCount / 2);
  97. $dy = abs($y - $this->moduleCount / 2);
  98. if($dx + $dy <= $radius){
  99. return true;
  100. }
  101. if($dx > $radius || $dy > $radius){
  102. return false;
  103. }
  104. if(pow($dx, 2) + pow($dy, 2) <= pow($radius, 2)){
  105. return true;
  106. }
  107. return false;
  108. }
  109. /**
  110. * add a solid circle around the matrix
  111. */
  112. protected function addCircle(float $radius):string{
  113. return sprintf(
  114. '%4$s<circle id="circle" cx="%1$s" cy="%1$s" r="%2$s" stroke-width="%3$s"/>',
  115. $this->moduleCount / 2,
  116. round($radius, 5),
  117. $this->options->circleRadius * 2,
  118. $this->options->eol
  119. );
  120. }
  121. }
  122. /**
  123. * the augmented options class
  124. */
  125. class RoundQuietzoneOptions extends QROptions{
  126. /**
  127. * The amount of additional modules to be used in the circle diameter calculation
  128. *
  129. * Note that the middle of the circle stroke goes through the (assumed) outer corners
  130. * or centers of the QR Code (excluding quiet zone)
  131. *
  132. * Example:
  133. *
  134. * - a value of -1 would go through the center of the outer corner modules of the finder patterns
  135. * - a value of 0 would go through the corner of the outer modules of the finder patterns
  136. * - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45 degree angle
  137. */
  138. protected int $additionalModules = 0;
  139. }
  140. /*
  141. * Runtime
  142. */
  143. $options = new RoundQuietzoneOptions([
  144. 'additionalModules' => 5,
  145. 'version' => 7,
  146. 'eccLevel' => EccLevel::H, // maximum error correction capacity, esp. for print
  147. 'addQuietzone' => false, // we're not adding a quiet zone, this is done internally in our own module
  148. 'imageBase64' => false, // avoid base64 URI output
  149. 'outputType' => QROutputInterface::CUSTOM,
  150. 'outputInterface' => RoundQuietzoneSVGoutput::class, // load our own output class
  151. 'drawLightModules' => false, // set to true to add the light modules
  152. 'connectPaths' => true,
  153. 'excludeFromConnect' => [
  154. QRMatrix::M_FINDER|QRMatrix::IS_DARK,
  155. QRMatrix::M_FINDER_DOT|QRMatrix::IS_DARK,
  156. QRMatrix::M_ALIGNMENT|QRMatrix::IS_DARK,
  157. QRMatrix::M_QUIETZONE|QRMatrix::IS_DARK
  158. ],
  159. 'drawCircularModules' => true,
  160. 'circleRadius' => 0.4,
  161. 'keepAsSquare' => [
  162. QRMatrix::M_FINDER|QRMatrix::IS_DARK,
  163. QRMatrix::M_FINDER_DOT|QRMatrix::IS_DARK,
  164. QRMatrix::M_ALIGNMENT|QRMatrix::IS_DARK,
  165. ],
  166. // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient
  167. 'svgDefs' => '
  168. <linearGradient id="blurple" x1="100%" y2="100%">
  169. <stop stop-color="#D70071" offset="0"/>
  170. <stop stop-color="#9C4E97" offset="0.5"/>
  171. <stop stop-color="#0035A9" offset="1"/>
  172. </linearGradient>
  173. <linearGradient id="rainbow" x1="100%" y2="100%">
  174. <stop stop-color="#e2453c" offset="2.5%"/>
  175. <stop stop-color="#e07e39" offset="21.5%"/>
  176. <stop stop-color="#e5d667" offset="40.5%"/>
  177. <stop stop-color="#51b95b" offset="59.5%"/>
  178. <stop stop-color="#1e72b7" offset="78.5%"/>
  179. <stop stop-color="#6f5ba7" offset="97.5%"/>
  180. </linearGradient>
  181. <style><![CDATA[
  182. .dark{ fill: url(#rainbow); }
  183. .light{ fill: #dedede; }
  184. .qr-2304{ fill: url(#blurple); }
  185. #circle{ fill: none; stroke: url(#blurple); }
  186. ]]></style>',
  187. ]);
  188. $qrcode = (new QRCode($options))->render('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
  189. if(php_sapi_name() !== 'cli'){
  190. header('Content-type: image/svg+xml');
  191. if(extension_loaded('zlib')){
  192. header('Vary: Accept-Encoding');
  193. header('Content-Encoding: gzip');
  194. $qrcode = gzencode($qrcode, 9);
  195. }
  196. }
  197. echo $qrcode;
  198. exit;