QRGdImage.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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, intdiv, intval,
  18. is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start, restore_error_handler, set_error_handler;
  19. /**
  20. * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
  21. *
  22. * @see http://php.net/manual/book.image.php
  23. */
  24. class QRGdImage extends QROutputAbstract{
  25. /**
  26. * The GD image resource
  27. *
  28. * @see imagecreatetruecolor()
  29. * @var resource|\GdImage
  30. */
  31. protected $image;
  32. /**
  33. * The allocated background color
  34. *
  35. * @see \imagecolorallocate()
  36. */
  37. protected int $background;
  38. /**
  39. * Whether we're running in upscale mode (scale < 20)
  40. *
  41. * @see \chillerlan\QRCode\QROptions::$drawCircularModules
  42. */
  43. protected bool $upscaled = false;
  44. /**
  45. * @inheritDoc
  46. *
  47. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  48. * @noinspection PhpMissingParentConstructorInspection
  49. */
  50. public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
  51. if(!extension_loaded('gd')){
  52. throw new QRCodeOutputException('ext-gd not loaded'); // @codeCoverageIgnore
  53. }
  54. $this->options = $options;
  55. $this->matrix = $matrix;
  56. $this->setMatrixDimensions();
  57. // we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
  58. // @see https://github.com/chillerlan/php-qrcode/issues/23
  59. if($this->options->drawCircularModules && $this->options->scale < 20){
  60. // increase the initial image size by 10
  61. $this->length = (($this->length + 2) * 10);
  62. $this->scale *= 10;
  63. $this->upscaled = true;
  64. }
  65. $this->image = imagecreatetruecolor($this->length, $this->length);
  66. // set module values after image creation because we need the GdImage instance
  67. $this->setModuleValues();
  68. }
  69. /**
  70. * @inheritDoc
  71. */
  72. public static function moduleValueIsValid($value):bool{
  73. if(!is_array($value) || count($value) < 3){
  74. return false;
  75. }
  76. // check the first 3 values of the array
  77. foreach(array_values($value) as $i => $val){
  78. if($i > 2){
  79. break;
  80. }
  81. if(!is_numeric($val)){
  82. return false;
  83. }
  84. }
  85. return true;
  86. }
  87. /**
  88. * @param array $value
  89. *
  90. * @inheritDoc
  91. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  92. */
  93. protected function prepareModuleValue($value):int{
  94. $values = [];
  95. foreach(array_values($value) as $i => $val){
  96. if($i > 2){
  97. break;
  98. }
  99. $values[] = max(0, min(255, intval($val)));
  100. }
  101. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  102. $color = imagecolorallocate($this->image, ...$values);
  103. if($color === false){
  104. throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
  105. }
  106. return $color;
  107. }
  108. /**
  109. * @inheritDoc
  110. */
  111. protected function getDefaultModuleValue(bool $isDark):int{
  112. return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
  113. }
  114. /**
  115. * @inheritDoc
  116. *
  117. * @return string|resource|\GdImage
  118. *
  119. * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
  120. * @throws \ErrorException
  121. */
  122. public function dump(string $file = null){
  123. /** @phan-suppress-next-line PhanTypeMismatchArgumentInternal */
  124. set_error_handler(function(int $severity, string $msg, string $file, int $line):void{
  125. throw new ErrorException($msg, 0, $severity, $file, $line);
  126. });
  127. $this->setBgColor();
  128. imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
  129. $this->drawImage();
  130. if($this->upscaled){
  131. // scale down to the expected size
  132. $this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10));
  133. $this->upscaled = false;
  134. }
  135. // set transparency after scaling, otherwise it would be undone
  136. // @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099
  137. $this->setTransparencyColor();
  138. if($this->options->returnResource){
  139. restore_error_handler();
  140. return $this->image;
  141. }
  142. $imageData = $this->dumpImage();
  143. $this->saveToFile($imageData, $file);
  144. if($this->options->imageBase64){
  145. $imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
  146. }
  147. restore_error_handler();
  148. return $imageData;
  149. }
  150. /**
  151. * Sets the background color
  152. */
  153. protected function setBgColor():void{
  154. if(isset($this->background)){
  155. return;
  156. }
  157. if($this::moduleValueIsValid($this->options->bgColor)){
  158. $this->background = $this->prepareModuleValue($this->options->bgColor);
  159. return;
  160. }
  161. $this->background = $this->prepareModuleValue([255, 255, 255]);
  162. }
  163. /**
  164. * Sets the transparency color
  165. */
  166. protected function setTransparencyColor():void{
  167. if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
  168. return;
  169. }
  170. $transparencyColor = $this->background;
  171. if($this::moduleValueIsValid($this->options->transparencyColor)){
  172. $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
  173. }
  174. imagecolortransparent($this->image, $transparencyColor);
  175. }
  176. /**
  177. * Creates the QR image
  178. */
  179. protected function drawImage():void{
  180. for($y = 0; $y < $this->moduleCount; $y++){
  181. for($x = 0; $x < $this->moduleCount; $x++){
  182. $this->setPixel($x, $y);
  183. }
  184. }
  185. }
  186. /**
  187. * Creates a single QR pixel with the given settings
  188. */
  189. protected function setPixel(int $x, int $y):void{
  190. if(!$this->options->drawLightModules && !$this->matrix->check($x, $y)){
  191. return;
  192. }
  193. $color = $this->getModuleValueAt($x, $y);
  194. $this->options->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->options->keepAsSquare)
  195. ? imagefilledellipse(
  196. $this->image,
  197. (($x * $this->scale) + intdiv($this->scale, 2)),
  198. (($y * $this->scale) + intdiv($this->scale, 2)),
  199. (int)(2 * $this->options->circleRadius * $this->scale),
  200. (int)(2 * $this->options->circleRadius * $this->scale),
  201. $color
  202. )
  203. : imagefilledrectangle(
  204. $this->image,
  205. ($x * $this->scale),
  206. ($y * $this->scale),
  207. (($x + 1) * $this->scale),
  208. (($y + 1) * $this->scale),
  209. $color
  210. );
  211. }
  212. /**
  213. * Creates the final image by calling the desired GD output function
  214. *
  215. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  216. */
  217. protected function dumpImage():string{
  218. ob_start();
  219. try{
  220. switch($this->options->outputType){
  221. case QROutputInterface::GDIMAGE_GIF:
  222. imagegif($this->image);
  223. break;
  224. case QROutputInterface::GDIMAGE_JPG:
  225. imagejpeg($this->image, null, max(0, min(100, $this->options->jpegQuality)));
  226. break;
  227. // silently default to png output
  228. case QROutputInterface::GDIMAGE_PNG:
  229. default:
  230. imagepng($this->image, null, max(-1, min(9, $this->options->pngCompression)));
  231. }
  232. }
  233. // not going to cover edge cases
  234. // @codeCoverageIgnoreStart
  235. catch(Throwable $e){
  236. throw new QRCodeOutputException($e->getMessage());
  237. }
  238. // @codeCoverageIgnoreEnd
  239. $imageData = ob_get_contents();
  240. imagedestroy($this->image);
  241. ob_end_clean();
  242. return $imageData;
  243. }
  244. }