svgRoundQuietzone.php 10 KB

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