QRMarkupSVG.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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 https://lea.verou.me/blog/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/
  19. * @see https://codepen.io/leaverou/full/RmwzKv
  20. * @see https://jakearchibald.github.io/svgomg/
  21. * @see https://web.archive.org/web/20200220211445/http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html
  22. */
  23. class QRMarkupSVG extends QRMarkup{
  24. final public const MIME_TYPE = 'image/svg+xml';
  25. /**
  26. * @todo: XSS proof
  27. *
  28. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
  29. * @inheritDoc
  30. */
  31. public static function moduleValueIsValid(mixed $value):bool{
  32. if(!is_string($value)){
  33. return false;
  34. }
  35. $value = trim($value);
  36. // url(...)
  37. if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){
  38. return true;
  39. }
  40. // otherwise check for standard css notation
  41. return parent::moduleValueIsValid($value);
  42. }
  43. /**
  44. * @inheritDoc
  45. */
  46. protected function getOutputDimensions():array{
  47. return [$this->moduleCount, $this->moduleCount];
  48. }
  49. /**
  50. * @inheritDoc
  51. */
  52. protected function getCssClass(int $M_TYPE = 0):string{
  53. return implode(' ', [
  54. 'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE),
  55. $this->matrix->isDark($M_TYPE) ? 'dark' : 'light',
  56. $this->options->cssClass,
  57. ]);
  58. }
  59. /**
  60. * @inheritDoc
  61. */
  62. protected function createMarkup(bool $saveToFile):string{
  63. $svg = $this->header();
  64. if(!empty($this->options->svgDefs)){
  65. $svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->eol);
  66. }
  67. $svg .= $this->paths();
  68. // close svg
  69. $svg .= sprintf('%1$s</svg>%1$s', $this->eol);
  70. // transform to data URI only when not saving to file
  71. if(!$saveToFile && $this->options->outputBase64){
  72. return $this->toBase64DataURI($svg);
  73. }
  74. return $svg;
  75. }
  76. /**
  77. * returns the value for the SVG viewBox attribute
  78. *
  79. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
  80. * @see https://css-tricks.com/scale-svg/#article-header-id-3
  81. */
  82. protected function getViewBox():string{
  83. [$width, $height] = $this->getOutputDimensions();
  84. return sprintf('0 0 %s %s', $width, $height);
  85. }
  86. /**
  87. * returns the <svg> header with the given options parsed
  88. *
  89. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
  90. */
  91. protected function header():string{
  92. $header = sprintf(
  93. '<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" viewBox="%2$s" preserveAspectRatio="%3$s">%4$s',
  94. $this->options->cssClass,
  95. $this->getViewBox(),
  96. $this->options->svgPreserveAspectRatio,
  97. $this->eol
  98. );
  99. if($this->options->svgAddXmlHeader){
  100. $header = sprintf('<?xml version="1.0" encoding="UTF-8"?>%s%s', $this->eol, $header);
  101. }
  102. return $header;
  103. }
  104. /**
  105. * returns one or more SVG <path> elements
  106. */
  107. protected function paths():string{
  108. $paths = $this->collectModules($this->module(...));
  109. $svg = [];
  110. // create the path elements
  111. foreach($paths as $M_TYPE => $modules){
  112. // limit the total line length
  113. $chunks = array_chunk($modules, 100);
  114. $chonks = [];
  115. foreach($chunks as $chunk){
  116. $chonks[] = implode(' ', $chunk);
  117. }
  118. $path = implode($this->eol, $chonks);
  119. if(empty($path)){
  120. continue;
  121. }
  122. $svg[] = $this->path($path, $M_TYPE);
  123. }
  124. return implode($this->eol, $svg);
  125. }
  126. /**
  127. * renders and returns a single <path> element
  128. *
  129. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
  130. */
  131. protected function path(string $path, int $M_TYPE):string{
  132. if($this->options->svgUseFillAttributes){
  133. return sprintf(
  134. '<path class="%s" fill="%s" d="%s"/>',
  135. $this->getCssClass($M_TYPE),
  136. $this->getModuleValue($M_TYPE),
  137. $path
  138. );
  139. }
  140. return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
  141. }
  142. /**
  143. * returns a path segment for a single module
  144. *
  145. * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
  146. */
  147. protected function module(int $x, int $y, int $M_TYPE):string{
  148. if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
  149. return '';
  150. }
  151. if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
  152. // string interpolation: ugly and fast
  153. $ix = ($x + 0.5 - $this->circleRadius);
  154. $iy = ($y + 0.5);
  155. // phpcs:ignore
  156. 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";
  157. }
  158. // phpcs:ignore
  159. return "M$x $y h1 v1 h-1Z";
  160. }
  161. }