svgRoundQuietzone.php 10 KB

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