QRMarkupSVG.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. /**
  3. * Class QRMarkupSVG
  4. *
  5. * @created 06.06.2022
  6. * @author smiley <smiley@chillerlan.net>
  7. * @copyright 2022 smiley
  8. * @license MIT
  9. */
  10. namespace chillerlan\QRCode\Output;
  11. use function array_chunk, implode, is_string, preg_match, sprintf, trim;
  12. /**
  13. * SVG output
  14. *
  15. * @see https://github.com/codemasher/php-qrcode/pull/5
  16. * @see https://developer.mozilla.org/en-US/docs/Web/SVG
  17. * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/
  18. * @see http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html
  19. */
  20. class QRMarkupSVG extends QRMarkup{
  21. /**
  22. * @todo: XSS proof
  23. *
  24. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
  25. * @inheritDoc
  26. */
  27. public static function moduleValueIsValid($value):bool{
  28. if(!is_string($value)){
  29. return false;
  30. }
  31. $value = trim($value);
  32. // url(...)
  33. if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){
  34. return true;
  35. }
  36. // otherwise check for standard css notation
  37. return parent::moduleValueIsValid($value);
  38. }
  39. /**
  40. * @inheritDoc
  41. */
  42. protected function getOutputDimensions():array{
  43. return [$this->moduleCount, $this->moduleCount];
  44. }
  45. /**
  46. * @inheritDoc
  47. */
  48. protected function createMarkup(bool $saveToFile):string{
  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->eol);
  52. }
  53. $svg .= $this->paths();
  54. // close svg
  55. $svg .= sprintf('%1$s</svg>%1$s', $this->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. * returns the value for the SVG viewBox attribute
  64. *
  65. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
  66. * @see https://css-tricks.com/scale-svg/#article-header-id-3
  67. */
  68. protected function getViewBox():string{
  69. [$width, $height] = $this->getOutputDimensions();
  70. return sprintf('0 0 %s %s', $width, $height);
  71. }
  72. /**
  73. * returns the <svg> header with the given options parsed
  74. *
  75. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
  76. */
  77. protected function header():string{
  78. $header = sprintf(
  79. '<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" viewBox="%2$s" preserveAspectRatio="%3$s">%4$s',
  80. $this->options->cssClass,
  81. $this->getViewBox(),
  82. $this->options->svgPreserveAspectRatio,
  83. $this->eol
  84. );
  85. if($this->options->svgAddXmlHeader){
  86. $header = sprintf('<?xml version="1.0" encoding="UTF-8"?>%s%s', $this->eol, $header);
  87. }
  88. return $header;
  89. }
  90. /**
  91. * returns one or more SVG <path> elements
  92. */
  93. protected function paths():string{
  94. $paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE));
  95. $svg = [];
  96. // create the path elements
  97. foreach($paths as $M_TYPE => $modules){
  98. // limit the total line length
  99. $chunks = array_chunk($modules, 100);
  100. $chonks = [];
  101. foreach($chunks as $chunk){
  102. $chonks[] = implode(' ', $chunk);
  103. }
  104. $path = implode($this->eol, $chonks);
  105. if(empty($path)){
  106. continue;
  107. }
  108. $svg[] = $this->path($path, $M_TYPE);
  109. }
  110. return implode($this->eol, $svg);
  111. }
  112. /**
  113. * renders and returns a single <path> element
  114. *
  115. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
  116. */
  117. protected function path(string $path, int $M_TYPE):string{
  118. if($this->options->svgUseFillAttributes){
  119. return sprintf(
  120. '<path class="%s" fill="%s" d="%s"/>',
  121. $this->getCssClass($M_TYPE),
  122. $this->getModuleValue($M_TYPE),
  123. $path
  124. );
  125. }
  126. return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
  127. }
  128. /**
  129. * @inheritDoc
  130. */
  131. protected function getCssClass(int $M_TYPE = 0):string{
  132. return implode(' ', [
  133. 'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE),
  134. $this->matrix->isDark($M_TYPE) ? 'dark' : 'light',
  135. $this->options->cssClass,
  136. ]);
  137. }
  138. /**
  139. * returns a path segment for a single module
  140. *
  141. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
  142. */
  143. protected function module(int $x, int $y, int $M_TYPE):string{
  144. if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
  145. return '';
  146. }
  147. if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
  148. // string interpolation: ugly and fast
  149. $ix = ($x + 0.5 - $this->circleRadius);
  150. $iy = ($y + 0.5);
  151. // phpcs:ignore
  152. return "M$ix $iy a$this->circleRadius $this->circleRadius 0 1 0 $this->circleDiameter 0 a$this->circleRadius $this->circleRadius 0 1 0 -$this->circleDiameter 0Z";
  153. }
  154. // phpcs:ignore
  155. return "M$x $y h1 v1 h-1Z";
  156. }
  157. }