QRGdImage.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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 $imageTransparencyBG has no effect"
  83. // https://stackoverflow.com/a/10455217
  84. $tbg = $this->options->imageTransparencyBG;
  85. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  86. $background = imagecolorallocate($this->image, ...$tbg);
  87. if($this->options->imageTransparent && $this->options->outputType !== QROutputInterface::GDIMAGE_JPG){
  88. imagecolortransparent($this->image, $background);
  89. }
  90. imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $background);
  91. foreach($this->matrix->matrix() as $y => $row){
  92. foreach($row as $x => $M_TYPE){
  93. $this->setPixel($x, $y, $M_TYPE);
  94. }
  95. }
  96. // scale down to the expected size
  97. if($this->options->drawCircularModules && $this->options->scale <= 20){
  98. $this->image = imagescale($this->image, $this->length/10, $this->length/10, IMG_BILINEAR_FIXED);
  99. }
  100. if($this->options->returnResource){
  101. restore_error_handler();
  102. return $this->image;
  103. }
  104. $imageData = $this->dumpImage();
  105. if($file !== null){
  106. $this->saveToFile($imageData, $file);
  107. }
  108. if($this->options->imageBase64){
  109. $imageData = $this->base64encode($imageData, 'image/'.$this->options->outputType);
  110. }
  111. restore_error_handler();
  112. return $imageData;
  113. }
  114. /**
  115. * Creates a single QR pixel with the given settings
  116. */
  117. protected function setPixel(int $x, int $y, int $M_TYPE):void{
  118. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  119. $color = imagecolorallocate($this->image, ...$this->moduleValues[$M_TYPE]);
  120. $this->options->drawCircularModules && $this->matrix->checkTypeNotIn($x, $y, $this->options->keepAsSquare)
  121. ? imagefilledellipse(
  122. $this->image,
  123. (int)(($x * $this->scale) + ($this->scale / 2)),
  124. (int)(($y * $this->scale) + ($this->scale / 2)),
  125. (int)(2 * $this->options->circleRadius * $this->scale),
  126. (int)(2 * $this->options->circleRadius * $this->scale),
  127. $color
  128. )
  129. : imagefilledrectangle(
  130. $this->image,
  131. $x * $this->scale,
  132. $y * $this->scale,
  133. ($x + 1) * $this->scale,
  134. ($y + 1) * $this->scale,
  135. $color
  136. );
  137. }
  138. /**
  139. * Creates the final image by calling the desired GD output function
  140. *
  141. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  142. */
  143. protected function dumpImage():string{
  144. ob_start();
  145. try{
  146. switch($this->options->outputType){
  147. case QROutputInterface::GDIMAGE_GIF:
  148. imagegif($this->image);
  149. break;
  150. case QROutputInterface::GDIMAGE_JPG:
  151. imagejpeg($this->image, null, max(0, min(100, $this->options->jpegQuality)));
  152. break;
  153. // silently default to png output
  154. case QROutputInterface::GDIMAGE_PNG:
  155. default:
  156. imagepng($this->image, null, max(-1, min(9, $this->options->pngCompression)));
  157. }
  158. }
  159. // not going to cover edge cases
  160. // @codeCoverageIgnoreStart
  161. catch(Throwable $e){
  162. throw new QRCodeOutputException($e->getMessage());
  163. }
  164. // @codeCoverageIgnoreEnd
  165. $imageData = ob_get_contents();
  166. imagedestroy($this->image);
  167. ob_end_clean();
  168. return $imageData;
  169. }
  170. }