QRGdImage.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. /**
  3. * Class QRGdImage
  4. *
  5. * @created 05.12.2015
  6. * @author Smiley <smiley@chillerlan.net>
  7. * @copyright 2015 Smiley
  8. * @license MIT
  9. *
  10. * @noinspection PhpComposerExtensionStubsInspection
  11. */
  12. namespace chillerlan\QRCode\Output;
  13. use chillerlan\QRCode\Data\QRMatrix;
  14. use chillerlan\Settings\SettingsContainerInterface;
  15. use ErrorException, Throwable;
  16. use function array_values, count, extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor,
  17. imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng, imagescale, is_array,
  18. max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler;
  19. use const IMG_BILINEAR_FIXED;
  20. /**
  21. * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
  22. *
  23. * @see http://php.net/manual/book.image.php
  24. */
  25. class QRGdImage extends QROutputAbstract{
  26. /**
  27. * The GD image resource
  28. *
  29. * @see imagecreatetruecolor()
  30. * @var resource|\GdImage
  31. */
  32. protected $image;
  33. /**
  34. * @inheritDoc
  35. *
  36. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  37. */
  38. public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
  39. if(!extension_loaded('gd')){
  40. throw new QRCodeOutputException('ext-gd not loaded'); // @codeCoverageIgnore
  41. }
  42. parent::__construct($options, $matrix);
  43. }
  44. /**
  45. * @inheritDoc
  46. */
  47. protected function moduleValueIsValid($value):bool{
  48. return is_array($value) && count($value) >= 3;
  49. }
  50. /**
  51. * @inheritDoc
  52. */
  53. protected function getModuleValue($value):array{
  54. return array_values($value);
  55. }
  56. /**
  57. * @inheritDoc
  58. */
  59. protected function getDefaultModuleValue(bool $isDark):array{
  60. return $isDark ? [0, 0, 0] : [255, 255, 255];
  61. }
  62. /**
  63. * @inheritDoc
  64. *
  65. * @return string|resource|\GdImage
  66. *
  67. * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
  68. * @throws \ErrorException
  69. */
  70. public function dump(string $file = null){
  71. /** @phan-suppress-next-line PhanTypeMismatchArgumentInternal */
  72. set_error_handler(function(int $severity, string $msg, string $file, int $line):void{
  73. throw new ErrorException($msg, 0, $severity, $file, $line);
  74. });
  75. $file ??= $this->options->cachefile;
  76. // we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
  77. if($this->options->drawCircularModules && $this->options->scale <= 20){
  78. $this->length = ($this->length + 2) * 10;
  79. $this->scale *= 10;
  80. }
  81. $this->image = imagecreatetruecolor($this->length, $this->length);
  82. // avoid: "Indirect modification of overloaded property $x has no effect"
  83. // https://stackoverflow.com/a/10455217
  84. $bgColor = $this->options->imageTransparencyBG;
  85. if($this->moduleValueIsValid($this->options->bgColor)){
  86. $bgColor = $this->getModuleValue($this->options->bgColor);
  87. }
  88. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  89. $background = imagecolorallocate($this->image, ...$bgColor);
  90. if($this->options->imageTransparent && $this->options->outputType !== QROutputInterface::GDIMAGE_JPG){
  91. $tbg = $this->options->imageTransparencyBG;
  92. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  93. imagecolortransparent($this->image, imagecolorallocate($this->image, ...$tbg));
  94. }
  95. imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $background);
  96. foreach($this->matrix->matrix() as $y => $row){
  97. foreach($row as $x => $M_TYPE){
  98. $this->setPixel($x, $y, $M_TYPE);
  99. }
  100. }
  101. // scale down to the expected size
  102. if($this->options->drawCircularModules && $this->options->scale <= 20){
  103. $this->image = imagescale($this->image, $this->length/10, $this->length/10, IMG_BILINEAR_FIXED);
  104. }
  105. if($this->options->returnResource){
  106. restore_error_handler();
  107. return $this->image;
  108. }
  109. $imageData = $this->dumpImage();
  110. if($file !== null){
  111. $this->saveToFile($imageData, $file);
  112. }
  113. if($this->options->imageBase64){
  114. $imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
  115. }
  116. restore_error_handler();
  117. return $imageData;
  118. }
  119. /**
  120. * Creates a single QR pixel with the given settings
  121. */
  122. protected function setPixel(int $x, int $y, int $M_TYPE):void{
  123. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  124. $color = imagecolorallocate($this->image, ...$this->moduleValues[$M_TYPE]);
  125. $this->options->drawCircularModules && $this->matrix->checkTypeNotIn($x, $y, $this->options->keepAsSquare)
  126. ? imagefilledellipse(
  127. $this->image,
  128. (int)(($x * $this->scale) + ($this->scale / 2)),
  129. (int)(($y * $this->scale) + ($this->scale / 2)),
  130. (int)(2 * $this->options->circleRadius * $this->scale),
  131. (int)(2 * $this->options->circleRadius * $this->scale),
  132. $color
  133. )
  134. : imagefilledrectangle(
  135. $this->image,
  136. $x * $this->scale,
  137. $y * $this->scale,
  138. ($x + 1) * $this->scale,
  139. ($y + 1) * $this->scale,
  140. $color
  141. );
  142. }
  143. /**
  144. * Creates the final image by calling the desired GD output function
  145. *
  146. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  147. */
  148. protected function dumpImage():string{
  149. ob_start();
  150. try{
  151. switch($this->options->outputType){
  152. case QROutputInterface::GDIMAGE_GIF:
  153. imagegif($this->image);
  154. break;
  155. case QROutputInterface::GDIMAGE_JPG:
  156. imagejpeg($this->image, null, max(0, min(100, $this->options->jpegQuality)));
  157. break;
  158. // silently default to png output
  159. case QROutputInterface::GDIMAGE_PNG:
  160. default:
  161. imagepng($this->image, null, max(-1, min(9, $this->options->pngCompression)));
  162. }
  163. }
  164. // not going to cover edge cases
  165. // @codeCoverageIgnoreStart
  166. catch(Throwable $e){
  167. throw new QRCodeOutputException($e->getMessage());
  168. }
  169. // @codeCoverageIgnoreEnd
  170. $imageData = ob_get_contents();
  171. imagedestroy($this->image);
  172. ob_end_clean();
  173. return $imageData;
  174. }
  175. }