svgRoundQuietzone.php 10 KB

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