svgRoundQuietzone.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. /**
  3. * round quiet zone example
  4. *
  5. * @see https://github.com/chillerlan/php-qrcode/discussions/137
  6. *
  7. * @created 09.07.2022
  8. * @author smiley <smiley@chillerlan.net>
  9. * @copyright 2022 smiley
  10. * @license MIT
  11. *
  12. * @noinspection PhpIllegalPsrClassPathInspection
  13. */
  14. declare(strict_types=1);
  15. use chillerlan\QRCode\{QRCode, QRCodeException, QROptions};
  16. use chillerlan\QRCode\Common\EccLevel;
  17. use chillerlan\QRCode\Data\QRMatrix;
  18. use chillerlan\QRCode\Output\QRMarkupSVG;
  19. require_once __DIR__.'/../vendor/autoload.php';
  20. /*
  21. * Class definition
  22. */
  23. /**
  24. * Create SVG QR Codes with embedded logos (that are also SVG),
  25. * randomly colored dots and a round quiet zone with added circle
  26. */
  27. class RoundQuietzoneSVGoutput extends QRMarkupSVG{
  28. protected float $center;
  29. protected function createMarkup(bool $saveToFile):string{
  30. // some Pythagorean magick
  31. $diameter = sqrt(2 * pow(($this->moduleCount + $this->options->additionalModules), 2));
  32. // calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it
  33. $quietzoneSize = ((int)ceil(($diameter - $this->moduleCount) / 2) + 1);
  34. // add the quiet zone to fill the circle
  35. $this->matrix->setQuietZone($quietzoneSize);
  36. // update the matrix dimensions to avoid errors in subsequent calculations
  37. // the moduleCount is now QR Code matrix + 2x quiet zone
  38. $this->setMatrixDimensions();
  39. $this->center = ($this->moduleCount / 2);
  40. // calculate the logo space
  41. $logoSpaceSize = (int)(ceil($this->moduleCount * $this->options->svgLogoScale) + 1);
  42. // we're calling QRMatrix::setLogoSpace() manually, so QROptions::$addLogoSpace has no effect here
  43. $this->matrix->setLogoSpace($logoSpaceSize);
  44. // color the quiet zone
  45. $this->colorQuietzone($quietzoneSize, ($diameter / 2));
  46. // start SVG output
  47. $svg = $this->header();
  48. if(!empty($this->options->svgDefs)){
  49. $svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->options->eol);
  50. }
  51. $svg .= $this->paths();
  52. $svg .= $this->getLogo();
  53. $svg .= $this->addCircle($diameter / 2);
  54. // close svg
  55. $svg .= sprintf('%1$s</svg>%1$s', $this->options->eol);
  56. return $svg;
  57. }
  58. protected function path(string $path, int $M_TYPE):string{
  59. // omit the "fill" and "opacity" attributes on the path element
  60. return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
  61. }
  62. /**
  63. * Sets random modules of the quiet zone to dark
  64. */
  65. protected function colorQuietzone(int $quietzoneSize, float $radius):void{
  66. $l1 = ($quietzoneSize - 1);
  67. $l2 = ($this->moduleCount - $quietzoneSize);
  68. // substract 1/2 stroke width and module radius from the circle radius to not cut off modules
  69. $r = ($radius - $this->options->circleRadius * 2);
  70. for($y = 0; $y < $this->moduleCount; $y++){
  71. for($x = 0; $x < $this->moduleCount; $x++){
  72. // skip anything that's not quiet zone
  73. if(!$this->matrix->checkType($x, $y, QRMatrix::M_QUIETZONE)){
  74. continue;
  75. }
  76. // leave one row of quiet zone around the matrix
  77. if(
  78. ($x === $l1 && $y >= $l1 && $y <= $l2)
  79. || ($x === $l2 && $y >= $l1 && $y <= $l2)
  80. || ($y === $l1 && $x >= $l1 && $x <= $l2)
  81. || ($y === $l2 && $x >= $l1 && $x <= $l2)
  82. ){
  83. continue;
  84. }
  85. // we need to add 0.5 units to the check values since we're calculating the element centers
  86. // ($x/$y is the element's assumed top left corner)
  87. if($this->checkIfInsideCircle(($x + 0.5), ($y + 0.5), $this->center, $this->center, $r)){
  88. $this->matrix->set($x, $y, (bool)rand(0, 1), QRMatrix::M_QUIETZONE);
  89. }
  90. }
  91. }
  92. }
  93. /**
  94. * @see https://stackoverflow.com/a/7227057
  95. */
  96. protected function checkIfInsideCircle(float $x, float $y, float $centerX, float $centerY, float $radius):bool{
  97. $dx = abs($x - $centerX);
  98. $dy = abs($y - $centerY);
  99. if(($dx + $dy) <= $radius){
  100. return true;
  101. }
  102. if($dx > $radius || $dy > $radius){
  103. return false;
  104. }
  105. return (pow($dx, 2) + pow($dy, 2)) <= pow($radius, 2);
  106. }
  107. /**
  108. * add a solid circle around the matrix
  109. */
  110. protected function addCircle(float $radius):string{
  111. return sprintf(
  112. '%4$s<circle id="circle" cx="%1$s" cy="%1$s" r="%2$s" stroke-width="%3$s"/>',
  113. $this->center,
  114. round($radius, 5),
  115. ($this->options->circleRadius * 2),
  116. $this->options->eol,
  117. );
  118. }
  119. /**
  120. * returns a <g> element that contains the SVG logo and positions it properly within the QR Code
  121. *
  122. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
  123. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
  124. */
  125. protected function getLogo():string{
  126. // @todo: customize the <g> element to your liking (css class, style...)
  127. return sprintf(
  128. '%5$s<g transform="translate(%1$s %1$s) scale(%2$s)" class="%3$s">%5$s %4$s%5$s</g>',
  129. (($this->moduleCount - ($this->moduleCount * $this->options->svgLogoScale)) / 2),
  130. $this->options->svgLogoScale,
  131. $this->options->svgLogoCssClass,
  132. file_get_contents($this->options->svgLogo),
  133. $this->options->eol,
  134. );
  135. }
  136. protected function collectModules():array{
  137. $paths = [];
  138. $dotColors = $this->options->dotColors; // avoid magic getter in long loops
  139. // collect the modules for each type
  140. foreach($this->matrix->getMatrix() as $y => $row){
  141. foreach($row as $x => $M_TYPE){
  142. $M_TYPE_LAYER = $M_TYPE;
  143. if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){
  144. // to connect paths we'll redeclare the $M_TYPE_LAYER to data only
  145. $M_TYPE_LAYER = QRMatrix::M_DATA;
  146. if($this->matrix->isDark($M_TYPE)){
  147. $M_TYPE_LAYER = QRMatrix::M_DATA_DARK;
  148. }
  149. }
  150. // randomly assign another $M_TYPE_LAYER for the given types
  151. // note that the layer id has to be an integer value,
  152. // ideally outside the several bitmask values
  153. if($M_TYPE_LAYER === QRMatrix::M_QUIETZONE_DARK){
  154. $M_TYPE_LAYER = array_rand($dotColors);
  155. }
  156. // collect the modules per $M_TYPE
  157. $module = $this->moduleTransform($x, $y, $M_TYPE, $M_TYPE_LAYER);
  158. if(!empty($module)){
  159. $paths[$M_TYPE_LAYER][] = $module;
  160. }
  161. }
  162. }
  163. // beautify output
  164. ksort($paths);
  165. return $paths;
  166. }
  167. }
  168. /**
  169. * the augmented options class
  170. *
  171. * @property int $additionalModules
  172. * @property array<int, string> $dotColors
  173. * @property string $svgLogo
  174. * @property float $svgLogoScale
  175. * @property string $svgLogoCssClass
  176. */
  177. class RoundQuietzoneOptions extends QROptions{
  178. /**
  179. * The amount of additional modules to be used in the circle diameter calculation
  180. *
  181. * Note that the middle of the circle stroke goes through the (assumed) outer corners
  182. * or centers of the QR Code (excluding quiet zone)
  183. *
  184. * Example:
  185. *
  186. * - a value of -1 would go through the center of the outer corner modules of the finder patterns
  187. * - a value of 0 would go through the corner of the outer modules of the finder patterns
  188. * - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45-degree angle
  189. */
  190. protected int $additionalModules = 0;
  191. /**
  192. * a map of $M_TYPE_LAYER => color
  193. *
  194. * @see \array_rand()
  195. * @var array<int, string>
  196. */
  197. protected array $dotColors = [];
  198. // path to svg logo
  199. protected string $svgLogo;
  200. // logo scale in % of QR Code size, clamped to 10%-30%
  201. protected float $svgLogoScale = 0.20;
  202. // css class for the logo (defined in $svgDefs)
  203. protected string $svgLogoCssClass = '';
  204. // check logo
  205. protected function set_svgLogo(string $svgLogo):void{
  206. if(!file_exists($svgLogo) || !is_readable($svgLogo)){
  207. throw new QRCodeException('invalid svg logo');
  208. }
  209. // @todo: validate svg
  210. $this->svgLogo = $svgLogo;
  211. }
  212. // clamp logo scale
  213. protected function set_svgLogoScale(float $svgLogoScale):void{
  214. $this->svgLogoScale = max(0.05, min(0.3, $svgLogoScale));
  215. }
  216. }
  217. /*
  218. * Runtime
  219. */
  220. $options = new RoundQuietzoneOptions;
  221. // custom dot options (see extended class)
  222. $options->additionalModules = 5;
  223. $options->dotColors = [
  224. 111 => '#e2453c',
  225. 222 => '#e07e39',
  226. 333 => '#e5d667',
  227. 444 => '#51b95b',
  228. 555 => '#1e72b7',
  229. 666 => '#6f5ba7',
  230. ];
  231. // generate the CSS for the several colored layers
  232. $layerColors = '';
  233. foreach($options->dotColors as $layer => $color){
  234. $layerColors .= sprintf("\n\t\t.qr-%s{ fill: %s; }", $layer, $color);
  235. }
  236. // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient
  237. // please forgive me for I have committed colorful crimes
  238. $options->svgDefs = '
  239. <linearGradient id="blurple" x1="100%" y2="100%">
  240. <stop stop-color="#D70071" offset="0"/>
  241. <stop stop-color="#9C4E97" offset="0.5"/>
  242. <stop stop-color="#0035A9" offset="1"/>
  243. </linearGradient>
  244. <linearGradient id="rainbow" x1="100%" y2="100%">
  245. <stop stop-color="#e2453c" offset="2.5%"/>
  246. <stop stop-color="#e07e39" offset="21.5%"/>
  247. <stop stop-color="#e5d667" offset="40.5%"/>
  248. <stop stop-color="#51b95b" offset="59.5%"/>
  249. <stop stop-color="#1e72b7" offset="78.5%"/>
  250. <stop stop-color="#6f5ba7" offset="97.5%"/>
  251. </linearGradient>
  252. <style><![CDATA[
  253. .light{ fill: #dedede; }
  254. .dark{ fill: url(#rainbow); }
  255. .logo{ fill: url(#blurple); }
  256. #circle{ fill: none; stroke: url(#blurple); }
  257. '.$layerColors.'
  258. ]]></style>';
  259. // custom SVG logo options
  260. $options->svgLogo = __DIR__.'/github.svg'; // logo from: https://github.com/simple-icons/simple-icons
  261. $options->svgLogoScale = 0.2;
  262. $options->svgLogoCssClass = 'logo';
  263. // common QRCode options
  264. $options->version = 7;
  265. $options->eccLevel = EccLevel::H;
  266. $options->addQuietzone = false; // we're not adding a quiet zone, this is done internally in our own module
  267. $options->outputBase64 = false; // avoid base64 URI output for the example
  268. $options->outputInterface = RoundQuietzoneSVGoutput::class; // load our own output class
  269. $options->drawLightModules = false; // set to true to add the light modules
  270. // common SVG options
  271. $options->connectPaths = true;
  272. $options->excludeFromConnect = [
  273. QRMatrix::M_QUIETZONE_DARK,
  274. ];
  275. $options->drawCircularModules = true;
  276. $options->circleRadius = 0.4;
  277. $options->keepAsSquare = [
  278. QRMatrix::M_FINDER_DARK,
  279. QRMatrix::M_FINDER_DOT,
  280. QRMatrix::M_ALIGNMENT_DARK,
  281. ];
  282. $out = (new QRCode($options))->render('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
  283. if(PHP_SAPI !== 'cli'){
  284. header('Content-type: image/svg+xml');
  285. if(extension_loaded('zlib')){
  286. header('Vary: Accept-Encoding');
  287. header('Content-Encoding: gzip');
  288. $out = gzencode($out, 9);
  289. }
  290. }
  291. echo $out;
  292. exit;