ImagickImageBackEnd.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  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\Cmyk;
  7. use BaconQrCode\Renderer\Color\ColorInterface;
  8. use BaconQrCode\Renderer\Color\Gray;
  9. use BaconQrCode\Renderer\Color\Rgb;
  10. use BaconQrCode\Renderer\Path\Close;
  11. use BaconQrCode\Renderer\Path\Curve;
  12. use BaconQrCode\Renderer\Path\EllipticArc;
  13. use BaconQrCode\Renderer\Path\Line;
  14. use BaconQrCode\Renderer\Path\Move;
  15. use BaconQrCode\Renderer\Path\Path;
  16. use BaconQrCode\Renderer\RendererStyle\Gradient;
  17. use BaconQrCode\Renderer\RendererStyle\GradientType;
  18. use Imagick;
  19. use ImagickDraw;
  20. use ImagickPixel;
  21. final class ImagickImageBackEnd implements ImageBackEndInterface
  22. {
  23. /**
  24. * @var string
  25. */
  26. private $imageFormat;
  27. /**
  28. * @var int
  29. */
  30. private $compressionQuality;
  31. /**
  32. * @var Imagick|null
  33. */
  34. private $image;
  35. /**
  36. * @var ImagickDraw|null
  37. */
  38. private $draw;
  39. /**
  40. * @var int|null
  41. */
  42. private $gradientCount;
  43. /**
  44. * @var TransformationMatrix[]|null
  45. */
  46. private $matrices;
  47. /**
  48. * @var int|null
  49. */
  50. private $matrixIndex;
  51. public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
  52. {
  53. if (! class_exists(Imagick::class)) {
  54. throw new RuntimeException('You need to install the imagick extension to use this back end');
  55. }
  56. $this->imageFormat = $imageFormat;
  57. $this->compressionQuality = $compressionQuality;
  58. }
  59. public function new(int $size, ColorInterface $backgroundColor) : void
  60. {
  61. $this->image = new Imagick();
  62. $this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
  63. $this->image->setImageFormat($this->imageFormat);
  64. $this->image->setCompressionQuality($this->compressionQuality);
  65. $this->draw = new ImagickDraw();
  66. $this->gradientCount = 0;
  67. $this->matrices = [new TransformationMatrix()];
  68. $this->matrixIndex = 0;
  69. }
  70. public function scale(float $size) : void
  71. {
  72. if (null === $this->draw) {
  73. throw new RuntimeException('No image has been started');
  74. }
  75. $this->draw->scale($size, $size);
  76. $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
  77. ->multiply(TransformationMatrix::scale($size));
  78. }
  79. public function translate(float $x, float $y) : void
  80. {
  81. if (null === $this->draw) {
  82. throw new RuntimeException('No image has been started');
  83. }
  84. $this->draw->translate($x, $y);
  85. $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
  86. ->multiply(TransformationMatrix::translate($x, $y));
  87. }
  88. public function rotate(int $degrees) : void
  89. {
  90. if (null === $this->draw) {
  91. throw new RuntimeException('No image has been started');
  92. }
  93. $this->draw->rotate($degrees);
  94. $this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
  95. ->multiply(TransformationMatrix::rotate($degrees));
  96. }
  97. public function push() : void
  98. {
  99. if (null === $this->draw) {
  100. throw new RuntimeException('No image has been started');
  101. }
  102. $this->draw->push();
  103. $this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
  104. }
  105. public function pop() : void
  106. {
  107. if (null === $this->draw) {
  108. throw new RuntimeException('No image has been started');
  109. }
  110. $this->draw->pop();
  111. unset($this->matrices[$this->matrixIndex--]);
  112. }
  113. public function drawPathWithColor(Path $path, ColorInterface $color) : void
  114. {
  115. if (null === $this->draw) {
  116. throw new RuntimeException('No image has been started');
  117. }
  118. $this->draw->setFillColor($this->getColorPixel($color));
  119. $this->drawPath($path);
  120. }
  121. public function drawPathWithGradient(
  122. Path $path,
  123. Gradient $gradient,
  124. float $x,
  125. float $y,
  126. float $width,
  127. float $height
  128. ) : void {
  129. if (null === $this->draw) {
  130. throw new RuntimeException('No image has been started');
  131. }
  132. $this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
  133. $this->drawPath($path);
  134. }
  135. public function done() : string
  136. {
  137. if (null === $this->draw) {
  138. throw new RuntimeException('No image has been started');
  139. }
  140. $this->image->drawImage($this->draw);
  141. $blob = $this->image->getImageBlob();
  142. $this->draw->clear();
  143. $this->image->clear();
  144. $this->draw = null;
  145. $this->image = null;
  146. $this->gradientCount = null;
  147. return $blob;
  148. }
  149. private function drawPath(Path $path) : void
  150. {
  151. $this->draw->pathStart();
  152. foreach ($path as $op) {
  153. switch (true) {
  154. case $op instanceof Move:
  155. $this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
  156. break;
  157. case $op instanceof Line:
  158. $this->draw->pathLineToAbsolute($op->getX(), $op->getY());
  159. break;
  160. case $op instanceof EllipticArc:
  161. $this->draw->pathEllipticArcAbsolute(
  162. $op->getXRadius(),
  163. $op->getYRadius(),
  164. $op->getXAxisAngle(),
  165. $op->isLargeArc(),
  166. $op->isSweep(),
  167. $op->getX(),
  168. $op->getY()
  169. );
  170. break;
  171. case $op instanceof Curve:
  172. $this->draw->pathCurveToAbsolute(
  173. $op->getX1(),
  174. $op->getY1(),
  175. $op->getX2(),
  176. $op->getY2(),
  177. $op->getX3(),
  178. $op->getY3()
  179. );
  180. break;
  181. case $op instanceof Close:
  182. $this->draw->pathClose();
  183. break;
  184. default:
  185. throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
  186. }
  187. }
  188. $this->draw->pathFinish();
  189. }
  190. private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
  191. {
  192. list($width, $height) = $this->matrices[$this->matrixIndex]->apply($x + $width, $y + $height);
  193. list($x, $y) = $this->matrices[$this->matrixIndex]->apply($x, $y);
  194. $width -= $x;
  195. $height -= $y;
  196. $startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
  197. $endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
  198. $gradientImage = new Imagick();
  199. switch ($gradient->getType()) {
  200. case GradientType::HORIZONTAL():
  201. $gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
  202. 'gradient:%s-%s',
  203. $startColor,
  204. $endColor
  205. ));
  206. $gradientImage->rotateImage('transparent', -90);
  207. break;
  208. case GradientType::VERTICAL():
  209. $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
  210. 'gradient:%s-%s',
  211. $startColor,
  212. $endColor
  213. ));
  214. break;
  215. case GradientType::DIAGONAL():
  216. case GradientType::INVERSE_DIAGONAL():
  217. $gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
  218. 'gradient:%s-%s',
  219. $startColor,
  220. $endColor
  221. ));
  222. if (GradientType::DIAGONAL() === $gradient->getType()) {
  223. $gradientImage->rotateImage('transparent', -45);
  224. } else {
  225. $gradientImage->rotateImage('transparent', -135);
  226. }
  227. $rotatedWidth = $gradientImage->getImageWidth();
  228. $rotatedHeight = $gradientImage->getImageHeight();
  229. $gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
  230. $gradientImage->cropImage(
  231. intdiv($rotatedWidth, 2) - 2,
  232. intdiv($rotatedHeight, 2) - 2,
  233. intdiv($rotatedWidth, 4) + 1,
  234. intdiv($rotatedWidth, 4) + 1
  235. );
  236. break;
  237. case GradientType::RADIAL():
  238. $gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
  239. 'radial-gradient:%s-%s',
  240. $startColor,
  241. $endColor
  242. ));
  243. break;
  244. }
  245. $id = sprintf('g%d', ++$this->gradientCount);
  246. $this->draw->pushPattern($id, 0, 0, $x + $width, $y + $height);
  247. $this->draw->composite(Imagick::COMPOSITE_COPY, $x, $y, $width, $height, $gradientImage);
  248. $this->draw->popPattern();
  249. return $id;
  250. }
  251. private function getColorPixel(ColorInterface $color) : ImagickPixel
  252. {
  253. $alpha = 100;
  254. if ($color instanceof Alpha) {
  255. $alpha = $color->getAlpha();
  256. $color = $color->getBaseColor();
  257. }
  258. if ($color instanceof Rgb) {
  259. return new ImagickPixel(sprintf(
  260. 'rgba(%d, %d, %d, %F)',
  261. $color->getRed(),
  262. $color->getGreen(),
  263. $color->getBlue(),
  264. $alpha / 100
  265. ));
  266. }
  267. if ($color instanceof Cmyk) {
  268. return new ImagickPixel(sprintf(
  269. 'cmyka(%d, %d, %d, %d, %F)',
  270. $color->getCyan(),
  271. $color->getMagenta(),
  272. $color->getYellow(),
  273. $color->getBlack(),
  274. $alpha / 100
  275. ));
  276. }
  277. if ($color instanceof Gray) {
  278. return new ImagickPixel(sprintf(
  279. 'graya(%d%%, %F)',
  280. $color->getGray(),
  281. $alpha / 100
  282. ));
  283. }
  284. return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
  285. }
  286. }