SvgWriter.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * (c) Jeroen van den Enden <info@endroid.nl>
  5. *
  6. * This source file is subject to the MIT license that is bundled
  7. * with this source code in the file LICENSE.
  8. */
  9. namespace Endroid\QrCode\Writer;
  10. use Endroid\QrCode\Exception\GenerateImageException;
  11. use Endroid\QrCode\Exception\MissingLogoHeightException;
  12. use Endroid\QrCode\Exception\ValidationException;
  13. use Endroid\QrCode\QrCodeInterface;
  14. use SimpleXMLElement;
  15. class SvgWriter extends AbstractWriter
  16. {
  17. public function writeString(QrCodeInterface $qrCode): string
  18. {
  19. $options = $qrCode->getWriterOptions();
  20. if ($qrCode->getValidateResult()) {
  21. throw new ValidationException('Built-in validation reader can not check SVG images: please disable via setValidateResult(false)');
  22. }
  23. $data = $qrCode->getData();
  24. $svg = new SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>');
  25. $svg->addAttribute('version', '1.1');
  26. $svg->addAttribute('width', $data['outer_width'].'px');
  27. $svg->addAttribute('height', $data['outer_height'].'px');
  28. $svg->addAttribute('viewBox', '0 0 '.$data['outer_width'].' '.$data['outer_height']);
  29. $svg->addChild('defs');
  30. // Block definition
  31. $block_id = isset($options['rect_id']) && $options['rect_id'] ? $options['rect_id'] : 'block';
  32. $blockDefinition = $svg->defs->addChild('rect');
  33. $blockDefinition->addAttribute('id', $block_id);
  34. $blockDefinition->addAttribute('width', strval($data['block_size']));
  35. $blockDefinition->addAttribute('height', strval($data['block_size']));
  36. $blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b']));
  37. $blockDefinition->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getForegroundColor()['a'])));
  38. // Background
  39. $background = $svg->addChild('rect');
  40. $background->addAttribute('x', '0');
  41. $background->addAttribute('y', '0');
  42. $background->addAttribute('width', strval($data['outer_width']));
  43. $background->addAttribute('height', strval($data['outer_height']));
  44. $background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b']));
  45. $background->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getBackgroundColor()['a'])));
  46. foreach ($data['matrix'] as $row => $values) {
  47. foreach ($values as $column => $value) {
  48. if (1 === $value) {
  49. $block = $svg->addChild('use');
  50. $block->addAttribute('x', strval($data['margin_left'] + $data['block_size'] * $column));
  51. $block->addAttribute('y', strval($data['margin_left'] + $data['block_size'] * $row));
  52. $block->addAttribute('xlink:href', '#'.$block_id, 'http://www.w3.org/1999/xlink');
  53. }
  54. }
  55. }
  56. $logoPath = $qrCode->getLogoPath();
  57. if (is_string($logoPath)) {
  58. $forceXlinkHref = false;
  59. if (isset($options['force_xlink_href']) && $options['force_xlink_href']) {
  60. $forceXlinkHref = true;
  61. }
  62. $this->addLogo($svg, $data['outer_width'], $data['outer_height'], $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight(), $forceXlinkHref);
  63. }
  64. $xml = $svg->asXML();
  65. if (!is_string($xml)) {
  66. throw new GenerateImageException('Unable to save SVG XML');
  67. }
  68. if (isset($options['exclude_xml_declaration']) && $options['exclude_xml_declaration']) {
  69. $xml = str_replace("<?xml version=\"1.0\"?>\n", '', $xml);
  70. }
  71. return $xml;
  72. }
  73. private function addLogo(SimpleXMLElement $svg, int $imageWidth, int $imageHeight, string $logoPath, int $logoWidth = null, int $logoHeight = null, bool $forceXlinkHref = false): void
  74. {
  75. $mimeType = $this->getMimeType($logoPath);
  76. $imageData = file_get_contents($logoPath);
  77. if (!is_string($imageData)) {
  78. throw new GenerateImageException('Unable to read image data: check your logo path');
  79. }
  80. if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
  81. throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
  82. }
  83. if (null === $logoHeight || null === $logoWidth) {
  84. $logoImage = imagecreatefromstring(strval($imageData));
  85. if (!$logoImage) {
  86. throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
  87. }
  88. /** @var mixed $logoImage */
  89. $logoSourceWidth = imagesx($logoImage);
  90. $logoSourceHeight = imagesy($logoImage);
  91. if (PHP_VERSION_ID < 80000) {
  92. imagedestroy($logoImage);
  93. }
  94. if (null === $logoWidth) {
  95. $logoWidth = $logoSourceWidth;
  96. }
  97. if (null === $logoHeight) {
  98. $aspectRatio = $logoWidth / $logoSourceWidth;
  99. $logoHeight = intval($logoSourceHeight * $aspectRatio);
  100. }
  101. }
  102. $logoX = $imageWidth / 2 - $logoWidth / 2;
  103. $logoY = $imageHeight / 2 - $logoHeight / 2;
  104. $imageDefinition = $svg->addChild('image');
  105. $imageDefinition->addAttribute('x', strval($logoX));
  106. $imageDefinition->addAttribute('y', strval($logoY));
  107. $imageDefinition->addAttribute('width', strval($logoWidth));
  108. $imageDefinition->addAttribute('height', strval($logoHeight));
  109. $imageDefinition->addAttribute('preserveAspectRatio', 'none');
  110. // xlink:href is actually deprecated, but still required when placing the qr code in a pdf.
  111. // SimpleXML strips out the xlink part by using addAttribute(), so it must be set directly.
  112. if ($forceXlinkHref) {
  113. $imageDefinition['xlink:href'] = 'data:'.$mimeType.';base64,'.base64_encode($imageData);
  114. } else {
  115. $imageDefinition->addAttribute('href', 'data:'.$mimeType.';base64,'.base64_encode($imageData));
  116. }
  117. }
  118. private function getOpacity(int $alpha): float
  119. {
  120. $opacity = 1 - $alpha / 127;
  121. return $opacity;
  122. }
  123. public static function getContentType(): string
  124. {
  125. return 'image/svg+xml';
  126. }
  127. public static function getSupportedExtensions(): array
  128. {
  129. return ['svg'];
  130. }
  131. public function getName(): string
  132. {
  133. return 'svg';
  134. }
  135. }