SvgWriter.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. if ($qrCode->getValidateResult()) {
  20. throw new ValidationException('Built-in validation reader can not check SVG images: please disable via setValidateResult(false)');
  21. }
  22. $data = $qrCode->getData();
  23. $svg = new SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>');
  24. $svg->addAttribute('version', '1.1');
  25. $svg->addAttribute('width', $data['outer_width'].'px');
  26. $svg->addAttribute('height', $data['outer_height'].'px');
  27. $svg->addAttribute('viewBox', '0 0 '.$data['outer_width'].' '.$data['outer_height']);
  28. $svg->addChild('defs');
  29. // Block definition
  30. $blockDefinition = $svg->defs->addChild('rect');
  31. $blockDefinition->addAttribute('id', 'block');
  32. $blockDefinition->addAttribute('width', strval($data['block_size']));
  33. $blockDefinition->addAttribute('height', strval($data['block_size']));
  34. $blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b']));
  35. $blockDefinition->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getForegroundColor()['a'])));
  36. // Background
  37. $background = $svg->addChild('rect');
  38. $background->addAttribute('x', '0');
  39. $background->addAttribute('y', '0');
  40. $background->addAttribute('width', strval($data['outer_width']));
  41. $background->addAttribute('height', strval($data['outer_height']));
  42. $background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b']));
  43. $background->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getBackgroundColor()['a'])));
  44. foreach ($data['matrix'] as $row => $values) {
  45. foreach ($values as $column => $value) {
  46. if (1 === $value) {
  47. $block = $svg->addChild('use');
  48. $block->addAttribute('x', strval($data['margin_left'] + $data['block_size'] * $column));
  49. $block->addAttribute('y', strval($data['margin_left'] + $data['block_size'] * $row));
  50. $block->addAttribute('xlink:href', '#block', 'http://www.w3.org/1999/xlink');
  51. }
  52. }
  53. }
  54. $logoPath = $qrCode->getLogoPath();
  55. if (is_string($logoPath)) {
  56. $this->addLogo($svg, $data['outer_width'], $data['outer_height'], $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight());
  57. }
  58. $xml = $svg->asXML();
  59. if (!is_string($xml)) {
  60. throw new GenerateImageException('Unable to save SVG XML');
  61. }
  62. $options = $qrCode->getWriterOptions();
  63. if (isset($options['exclude_xml_declaration']) && $options['exclude_xml_declaration']) {
  64. $xml = str_replace("<?xml version=\"1.0\"?>\n", '', $xml);
  65. }
  66. return $xml;
  67. }
  68. private function addLogo(SimpleXMLElement $svg, int $imageWidth, int $imageHeight, string $logoPath, int $logoWidth = null, int $logoHeight = null): void
  69. {
  70. $mimeType = $this->getMimeType($logoPath);
  71. $imageData = file_get_contents($logoPath);
  72. if (!is_string($imageData)) {
  73. throw new GenerateImageException('Unable to read image data: check your logo path');
  74. }
  75. if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
  76. throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
  77. }
  78. if (null === $logoHeight || null === $logoWidth) {
  79. $logoImage = imagecreatefromstring(strval($imageData));
  80. if (!is_resource($logoImage)) {
  81. throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
  82. }
  83. $logoSourceWidth = imagesx($logoImage);
  84. $logoSourceHeight = imagesy($logoImage);
  85. imagedestroy($logoImage);
  86. if (null === $logoWidth) {
  87. $logoWidth = $logoSourceWidth;
  88. }
  89. if (null === $logoHeight) {
  90. $aspectRatio = $logoWidth / $logoSourceWidth;
  91. $logoHeight = intval($logoSourceHeight * $aspectRatio);
  92. }
  93. }
  94. $logoX = $imageWidth / 2 - $logoWidth / 2;
  95. $logoY = $imageHeight / 2 - $logoHeight / 2;
  96. $imageDefinition = $svg->addChild('image');
  97. $imageDefinition->addAttribute('x', strval($logoX));
  98. $imageDefinition->addAttribute('y', strval($logoY));
  99. $imageDefinition->addAttribute('width', strval($logoWidth));
  100. $imageDefinition->addAttribute('height', strval($logoHeight));
  101. $imageDefinition->addAttribute('preserveAspectRatio', 'none');
  102. $imageDefinition->addAttribute('xlink:href', 'data:'.$mimeType.';base64,'.base64_encode($imageData));
  103. }
  104. private function getOpacity(int $alpha): float
  105. {
  106. $opacity = 1 - $alpha / 127;
  107. return $opacity;
  108. }
  109. public static function getContentType(): string
  110. {
  111. return 'image/svg+xml';
  112. }
  113. public static function getSupportedExtensions(): array
  114. {
  115. return ['svg'];
  116. }
  117. public function getName(): string
  118. {
  119. return 'svg';
  120. }
  121. }