QRGdImage.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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;
  16. use Throwable;
  17. use function array_values, count, extension_loaded, gd_info, imagebmp, imagecolorallocate, imagecolortransparent,
  18. imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng,
  19. imagescale, imagewebp, intdiv, intval, is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start,
  20. restore_error_handler, set_error_handler, sprintf;
  21. /**
  22. * Converts the matrix into GD images, raw or base64 output (requires ext-gd)
  23. *
  24. * @see https://php.net/manual/book.image.php
  25. *
  26. * @deprecated 5.0.0 this class will be made abstract in future versions,
  27. * calling it directly is deprecated - use one of the child classes instead
  28. * @see https://github.com/chillerlan/php-qrcode/issues/223
  29. */
  30. class QRGdImage extends QROutputAbstract{
  31. /**
  32. * The GD image resource
  33. *
  34. * @see imagecreatetruecolor()
  35. * @var resource|\GdImage
  36. *
  37. * @todo: add \GdImage type in v6
  38. */
  39. protected $image;
  40. /**
  41. * The allocated background color
  42. *
  43. * @see \imagecolorallocate()
  44. */
  45. protected int $background;
  46. /**
  47. * Whether we're running in upscale mode (scale < 20)
  48. *
  49. * @see \chillerlan\QRCode\QROptions::$drawCircularModules
  50. */
  51. protected bool $upscaled = false;
  52. /**
  53. * @inheritDoc
  54. *
  55. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  56. * @noinspection PhpMissingParentConstructorInspection
  57. */
  58. public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
  59. $this->options = $options;
  60. $this->matrix = $matrix;
  61. $this->checkGD();
  62. if($this->options->invertMatrix){
  63. $this->matrix->invert();
  64. }
  65. $this->copyVars();
  66. $this->setMatrixDimensions();
  67. }
  68. /**
  69. * Checks whether GD is installed and if the given mode is supported
  70. *
  71. * @return void
  72. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  73. * @codeCoverageIgnore
  74. */
  75. protected function checkGD():void{
  76. if(!extension_loaded('gd')){
  77. throw new QRCodeOutputException('ext-gd not loaded');
  78. }
  79. $modes = [
  80. self::GDIMAGE_BMP => 'BMP Support',
  81. self::GDIMAGE_GIF => 'GIF Create Support',
  82. self::GDIMAGE_JPG => 'JPEG Support',
  83. self::GDIMAGE_PNG => 'PNG Support',
  84. self::GDIMAGE_WEBP => 'WebP Support',
  85. ];
  86. // likely using default or custom output
  87. if(!isset($modes[$this->options->outputType])){
  88. return;
  89. }
  90. $info = gd_info();
  91. $mode = $modes[$this->options->outputType];
  92. if(!isset($info[$mode]) || $info[$mode] !== true){
  93. throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputType));
  94. }
  95. }
  96. /**
  97. * @inheritDoc
  98. */
  99. public static function moduleValueIsValid($value):bool{
  100. if(!is_array($value) || count($value) < 3){
  101. return false;
  102. }
  103. // check the first 3 values of the array
  104. foreach(array_values($value) as $i => $val){
  105. if($i > 2){
  106. break;
  107. }
  108. if(!is_numeric($val)){
  109. return false;
  110. }
  111. }
  112. return true;
  113. }
  114. /**
  115. * @param array $value
  116. *
  117. * @inheritDoc
  118. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  119. */
  120. protected function prepareModuleValue($value):int{
  121. $values = [];
  122. foreach(array_values($value) as $i => $val){
  123. if($i > 2){
  124. break;
  125. }
  126. $values[] = max(0, min(255, intval($val)));
  127. }
  128. /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
  129. $color = imagecolorallocate($this->image, ...$values);
  130. if($color === false){
  131. throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
  132. }
  133. return $color;
  134. }
  135. /**
  136. * @inheritDoc
  137. */
  138. protected function getDefaultModuleValue(bool $isDark):int{
  139. return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
  140. }
  141. /**
  142. * @inheritDoc
  143. *
  144. * @return string|resource|\GdImage
  145. *
  146. * @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
  147. * @throws \ErrorException
  148. */
  149. public function dump(string $file = null){
  150. set_error_handler(function(int $errno, string $errstr):bool{
  151. throw new ErrorException($errstr, $errno);
  152. });
  153. $this->image = $this->createImage();
  154. // set module values after image creation because we need the GdImage instance
  155. $this->setModuleValues();
  156. $this->setBgColor();
  157. imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
  158. $this->drawImage();
  159. if($this->upscaled){
  160. // scale down to the expected size
  161. $this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10));
  162. $this->upscaled = false;
  163. }
  164. // set transparency after scaling, otherwise it would be undone
  165. // @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099
  166. $this->setTransparencyColor();
  167. if($this->options->returnResource){
  168. restore_error_handler();
  169. return $this->image;
  170. }
  171. $imageData = $this->dumpImage();
  172. $this->saveToFile($imageData, $file);
  173. if($this->options->outputBase64){
  174. // @todo: remove mime parameter in v6
  175. $imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
  176. }
  177. restore_error_handler();
  178. return $imageData;
  179. }
  180. /**
  181. * Creates a new GdImage resource and scales it if necessary
  182. *
  183. * we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
  184. *
  185. * @see https://github.com/chillerlan/php-qrcode/issues/23
  186. *
  187. * @return \GdImage|resource
  188. */
  189. protected function createImage(){
  190. if($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20){
  191. // increase the initial image size by 10
  192. $this->length *= 10;
  193. $this->scale *= 10;
  194. $this->upscaled = true;
  195. }
  196. return imagecreatetruecolor($this->length, $this->length);
  197. }
  198. /**
  199. * Sets the background color
  200. */
  201. protected function setBgColor():void{
  202. if(isset($this->background)){
  203. return;
  204. }
  205. if($this::moduleValueIsValid($this->options->bgColor)){
  206. $this->background = $this->prepareModuleValue($this->options->bgColor);
  207. return;
  208. }
  209. $this->background = $this->prepareModuleValue([255, 255, 255]);
  210. }
  211. /**
  212. * Sets the transparency color
  213. */
  214. protected function setTransparencyColor():void{
  215. // @todo: the jpg skip can be removed in v6
  216. if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
  217. return;
  218. }
  219. $transparencyColor = $this->background;
  220. if($this::moduleValueIsValid($this->options->transparencyColor)){
  221. $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
  222. }
  223. imagecolortransparent($this->image, $transparencyColor);
  224. }
  225. /**
  226. * Draws the QR image
  227. */
  228. protected function drawImage():void{
  229. foreach($this->matrix->getMatrix() as $y => $row){
  230. foreach($row as $x => $M_TYPE){
  231. $this->module($x, $y, $M_TYPE);
  232. }
  233. }
  234. }
  235. /**
  236. * Creates a single QR pixel with the given settings
  237. */
  238. protected function module(int $x, int $y, int $M_TYPE):void{
  239. if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
  240. return;
  241. }
  242. $color = $this->getModuleValue($M_TYPE);
  243. if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
  244. imagefilledellipse(
  245. $this->image,
  246. (($x * $this->scale) + intdiv($this->scale, 2)),
  247. (($y * $this->scale) + intdiv($this->scale, 2)),
  248. (int)($this->circleDiameter * $this->scale),
  249. (int)($this->circleDiameter * $this->scale),
  250. $color
  251. );
  252. return;
  253. }
  254. imagefilledrectangle(
  255. $this->image,
  256. ($x * $this->scale),
  257. ($y * $this->scale),
  258. (($x + 1) * $this->scale),
  259. (($y + 1) * $this->scale),
  260. $color
  261. );
  262. }
  263. /**
  264. * Renders the image with the gdimage function for the desired output
  265. *
  266. * @see \imagebmp()
  267. * @see \imagegif()
  268. * @see \imagejpeg()
  269. * @see \imagepng()
  270. * @see \imagewebp()
  271. *
  272. * @todo: v6.0: make abstract and call from child classes
  273. * @see https://github.com/chillerlan/php-qrcode/issues/223
  274. * @codeCoverageIgnore
  275. */
  276. protected function renderImage():void{
  277. switch($this->options->outputType){
  278. case QROutputInterface::GDIMAGE_BMP:
  279. imagebmp($this->image, null, ($this->options->quality > 0));
  280. break;
  281. case QROutputInterface::GDIMAGE_GIF:
  282. imagegif($this->image);
  283. break;
  284. case QROutputInterface::GDIMAGE_JPG:
  285. imagejpeg($this->image, null, max(-1, min(100, $this->options->quality)));
  286. break;
  287. case QROutputInterface::GDIMAGE_WEBP:
  288. imagewebp($this->image, null, max(-1, min(100, $this->options->quality)));
  289. break;
  290. // silently default to png output
  291. case QROutputInterface::GDIMAGE_PNG:
  292. default:
  293. imagepng($this->image, null, max(-1, min(9, $this->options->quality)));
  294. }
  295. }
  296. /**
  297. * Creates the final image by calling the desired GD output function
  298. *
  299. * @throws \chillerlan\QRCode\Output\QRCodeOutputException
  300. */
  301. protected function dumpImage():string{
  302. $exception = null;
  303. $imageData = null;
  304. ob_start();
  305. try{
  306. $this->renderImage();
  307. $imageData = ob_get_contents();
  308. imagedestroy($this->image);
  309. }
  310. // not going to cover edge cases
  311. // @codeCoverageIgnoreStart
  312. catch(Throwable $e){
  313. $exception = $e;
  314. }
  315. // @codeCoverageIgnoreEnd
  316. ob_end_clean();
  317. // throw here in case an exception happened within the output buffer
  318. if($exception instanceof Throwable){
  319. throw new QRCodeOutputException($exception->getMessage());
  320. }
  321. return $imageData;
  322. }
  323. }