SvgImageBackEnd.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. declare(strict_types = 1);
  3. namespace BaconQrCode\Renderer\Image;
  4. use BaconQrCode\Exception\RuntimeException;
  5. use BaconQrCode\Renderer\Color\Alpha;
  6. use BaconQrCode\Renderer\Color\ColorInterface;
  7. use BaconQrCode\Renderer\Path\Close;
  8. use BaconQrCode\Renderer\Path\Curve;
  9. use BaconQrCode\Renderer\Path\EllipticArc;
  10. use BaconQrCode\Renderer\Path\Line;
  11. use BaconQrCode\Renderer\Path\Move;
  12. use BaconQrCode\Renderer\Path\Path;
  13. use BaconQrCode\Renderer\RendererStyle\Gradient;
  14. use BaconQrCode\Renderer\RendererStyle\GradientType;
  15. use XMLWriter;
  16. final class SvgImageBackEnd implements ImageBackEndInterface
  17. {
  18. private const PRECISION = 3;
  19. /**
  20. * @var XMLWriter|null
  21. */
  22. private $xmlWriter;
  23. /**
  24. * @var int[]|null
  25. */
  26. private $stack;
  27. /**
  28. * @var int|null
  29. */
  30. private $currentStack;
  31. /**
  32. * @var int|null
  33. */
  34. private $gradientCount;
  35. public function __construct()
  36. {
  37. if (! class_exists(XMLWriter::class)) {
  38. throw new RuntimeException('You need to install the libxml extension to use this back end');
  39. }
  40. }
  41. public function new(int $size, ColorInterface $backgroundColor) : void
  42. {
  43. $this->xmlWriter = new XMLWriter();
  44. $this->xmlWriter->openMemory();
  45. $this->xmlWriter->startDocument('1.0', 'UTF-8');
  46. $this->xmlWriter->startElement('svg');
  47. $this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
  48. $this->xmlWriter->writeAttribute('version', '1.1');
  49. $this->xmlWriter->writeAttribute('width', (string) $size);
  50. $this->xmlWriter->writeAttribute('height', (string) $size);
  51. $this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
  52. $this->gradientCount = 0;
  53. $this->currentStack = 0;
  54. $this->stack[0] = 0;
  55. $alpha = 1;
  56. if ($backgroundColor instanceof Alpha) {
  57. $alpha = $backgroundColor->getAlpha() / 100;
  58. }
  59. if (0 === $alpha) {
  60. return;
  61. }
  62. $this->xmlWriter->startElement('rect');
  63. $this->xmlWriter->writeAttribute('x', '0');
  64. $this->xmlWriter->writeAttribute('y', '0');
  65. $this->xmlWriter->writeAttribute('width', (string) $size);
  66. $this->xmlWriter->writeAttribute('height', (string) $size);
  67. $this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
  68. if ($alpha < 1) {
  69. $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
  70. }
  71. $this->xmlWriter->endElement();
  72. }
  73. public function scale(float $size) : void
  74. {
  75. if (null === $this->xmlWriter) {
  76. throw new RuntimeException('No image has been started');
  77. }
  78. $this->xmlWriter->startElement('g');
  79. $this->xmlWriter->writeAttribute(
  80. 'transform',
  81. sprintf('scale(%s)', round($size, self::PRECISION))
  82. );
  83. ++$this->stack[$this->currentStack];
  84. }
  85. public function translate(float $x, float $y) : void
  86. {
  87. if (null === $this->xmlWriter) {
  88. throw new RuntimeException('No image has been started');
  89. }
  90. $this->xmlWriter->startElement('g');
  91. $this->xmlWriter->writeAttribute(
  92. 'transform',
  93. sprintf('translate(%s,%s)', round($x, self::PRECISION), round($y, self::PRECISION))
  94. );
  95. ++$this->stack[$this->currentStack];
  96. }
  97. public function rotate(int $degrees) : void
  98. {
  99. if (null === $this->xmlWriter) {
  100. throw new RuntimeException('No image has been started');
  101. }
  102. $this->xmlWriter->startElement('g');
  103. $this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
  104. ++$this->stack[$this->currentStack];
  105. }
  106. public function push() : void
  107. {
  108. if (null === $this->xmlWriter) {
  109. throw new RuntimeException('No image has been started');
  110. }
  111. $this->xmlWriter->startElement('g');
  112. $this->stack[] = 1;
  113. ++$this->currentStack;
  114. }
  115. public function pop() : void
  116. {
  117. if (null === $this->xmlWriter) {
  118. throw new RuntimeException('No image has been started');
  119. }
  120. for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
  121. $this->xmlWriter->endElement();
  122. }
  123. array_pop($this->stack);
  124. --$this->currentStack;
  125. }
  126. public function drawPathWithColor(Path $path, ColorInterface $color) : void
  127. {
  128. if (null === $this->xmlWriter) {
  129. throw new RuntimeException('No image has been started');
  130. }
  131. $alpha = 1;
  132. if ($color instanceof Alpha) {
  133. $alpha = $color->getAlpha() / 100;
  134. }
  135. $this->startPathElement($path);
  136. $this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
  137. if ($alpha < 1) {
  138. $this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
  139. }
  140. $this->xmlWriter->endElement();
  141. }
  142. public function drawPathWithGradient(
  143. Path $path,
  144. Gradient $gradient,
  145. float $x,
  146. float $y,
  147. float $width,
  148. float $height
  149. ) : void {
  150. if (null === $this->xmlWriter) {
  151. throw new RuntimeException('No image has been started');
  152. }
  153. $gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
  154. $this->startPathElement($path);
  155. $this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
  156. $this->xmlWriter->endElement();
  157. }
  158. public function done() : string
  159. {
  160. if (null === $this->xmlWriter) {
  161. throw new RuntimeException('No image has been started');
  162. }
  163. foreach ($this->stack as $openElements) {
  164. for ($i = $openElements; $i > 0; --$i) {
  165. $this->xmlWriter->endElement();
  166. }
  167. }
  168. $this->xmlWriter->endDocument();
  169. $blob = $this->xmlWriter->outputMemory(true);
  170. $this->xmlWriter = null;
  171. $this->stack = null;
  172. $this->currentStack = null;
  173. $this->gradientCount = null;
  174. return $blob;
  175. }
  176. private function startPathElement(Path $path) : void
  177. {
  178. $pathData = [];
  179. foreach ($path as $op) {
  180. switch (true) {
  181. case $op instanceof Move:
  182. $pathData[] = sprintf(
  183. 'M%s %s',
  184. round($op->getX(), self::PRECISION),
  185. round($op->getY(), self::PRECISION)
  186. );
  187. break;
  188. case $op instanceof Line:
  189. $pathData[] = sprintf(
  190. 'L%s %s',
  191. round($op->getX(), self::PRECISION),
  192. round($op->getY(), self::PRECISION)
  193. );
  194. break;
  195. case $op instanceof EllipticArc:
  196. $pathData[] = sprintf(
  197. 'A%s %s %s %u %u %s %s',
  198. round($op->getXRadius(), self::PRECISION),
  199. round($op->getYRadius(), self::PRECISION),
  200. round($op->getXAxisAngle(), self::PRECISION),
  201. $op->isLargeArc(),
  202. $op->isSweep(),
  203. round($op->getX(), self::PRECISION),
  204. round($op->getY(), self::PRECISION)
  205. );
  206. break;
  207. case $op instanceof Curve:
  208. $pathData[] = sprintf(
  209. 'C%s %s %s %s %s %s',
  210. round($op->getX1(), self::PRECISION),
  211. round($op->getY1(), self::PRECISION),
  212. round($op->getX2(), self::PRECISION),
  213. round($op->getY2(), self::PRECISION),
  214. round($op->getX3(), self::PRECISION),
  215. round($op->getY3(), self::PRECISION)
  216. );
  217. break;
  218. case $op instanceof Close:
  219. $pathData[] = 'Z';
  220. break;
  221. default:
  222. throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
  223. }
  224. }
  225. $this->xmlWriter->startElement('path');
  226. $this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
  227. $this->xmlWriter->writeAttribute('d', implode('', $pathData));
  228. }
  229. private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
  230. {
  231. $this->xmlWriter->startElement('defs');
  232. $startColor = $gradient->getStartColor();
  233. $endColor = $gradient->getEndColor();
  234. if ($gradient->getType() === GradientType::RADIAL()) {
  235. $this->xmlWriter->startElement('radialGradient');
  236. } else {
  237. $this->xmlWriter->startElement('linearGradient');
  238. }
  239. $this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
  240. switch ($gradient->getType()) {
  241. case GradientType::HORIZONTAL():
  242. $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
  243. $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
  244. $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
  245. $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
  246. break;
  247. case GradientType::VERTICAL():
  248. $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
  249. $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
  250. $this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
  251. $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
  252. break;
  253. case GradientType::DIAGONAL():
  254. $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
  255. $this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
  256. $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
  257. $this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
  258. break;
  259. case GradientType::INVERSE_DIAGONAL():
  260. $this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
  261. $this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
  262. $this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
  263. $this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
  264. break;
  265. case GradientType::RADIAL():
  266. $this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
  267. $this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
  268. $this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
  269. break;
  270. }
  271. $id = sprintf('g%d', ++$this->gradientCount);
  272. $this->xmlWriter->writeAttribute('id', $id);
  273. $this->xmlWriter->startElement('stop');
  274. $this->xmlWriter->writeAttribute('offset', '0%');
  275. $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
  276. if ($startColor instanceof Alpha) {
  277. $this->xmlWriter->writeAttribute('stop-opacity', $startColor->getAlpha());
  278. }
  279. $this->xmlWriter->endElement();
  280. $this->xmlWriter->startElement('stop');
  281. $this->xmlWriter->writeAttribute('offset', '100%');
  282. $this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
  283. if ($endColor instanceof Alpha) {
  284. $this->xmlWriter->writeAttribute('stop-opacity', $endColor->getAlpha());
  285. }
  286. $this->xmlWriter->endElement();
  287. $this->xmlWriter->endElement();
  288. $this->xmlWriter->endElement();
  289. return $id;
  290. }
  291. private function getColorString(ColorInterface $color) : string
  292. {
  293. $color = $color->toRgb();
  294. return sprintf(
  295. '#%02x%02x%02x',
  296. $color->getRed(),
  297. $color->getGreen(),
  298. $color->getBlue()
  299. );
  300. }
  301. }