* @copyright 2015 Smiley * @license MIT * * @noinspection PhpComposerExtensionStubsInspection */ declare(strict_types=1); namespace chillerlan\QRCode\Output; use chillerlan\QRCode\QROptions; use chillerlan\QRCode\Data\QRMatrix; use chillerlan\Settings\SettingsContainerInterface; use GdImage; use function extension_loaded, imagecolorallocate, imagecolortransparent, imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, imagescale, imagetypes, intdiv, intval, max, min, ob_end_clean, ob_get_contents, ob_start, sprintf; use const IMG_AVIF, IMG_BMP, IMG_GIF, IMG_JPG, IMG_PNG, IMG_WEBP; /** * Converts the matrix into GD images, raw or base64 output (requires ext-gd) * * @see https://php.net/manual/book.image.php * @see https://github.com/chillerlan/php-qrcode/issues/223 */ abstract class QRGdImage extends QROutputAbstract{ use RGBArrayModuleValueTrait; /** * The GD image resource * * @see imagecreatetruecolor() */ protected GdImage $image; /** * The allocated background color * * @see \imagecolorallocate() */ protected int $background; /** * Whether we're running in upscale mode (scale < 20) * * @see \chillerlan\QRCode\QROptions::$drawCircularModules */ protected bool $upscaled = false; /** * @throws \chillerlan\QRCode\Output\QRCodeOutputException * @noinspection PhpMissingParentConstructorInspection */ public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix){ $this->options = $options; $this->matrix = $matrix; $this->checkGD(); if($this->options->invertMatrix){ $this->matrix->invert(); } $this->copyVars(); $this->setMatrixDimensions(); } /** * Checks whether GD is installed and if the given mode is supported * * @throws \chillerlan\QRCode\Output\QRCodeOutputException * @codeCoverageIgnore */ protected function checkGD():void{ if(!extension_loaded('gd')){ throw new QRCodeOutputException('ext-gd not loaded'); } $modes = [ QRGdImageAVIF::class => IMG_AVIF, QRGdImageBMP::class => IMG_BMP, QRGdImageGIF::class => IMG_GIF, QRGdImageJPEG::class => IMG_JPG, QRGdImagePNG::class => IMG_PNG, QRGdImageWEBP::class => IMG_WEBP, ]; // likely using custom output/manual invocation if(!isset($modes[$this->options->outputInterface])){ return; } $mode = $modes[$this->options->outputInterface]; if((imagetypes() & $mode) !== $mode){ throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputInterface)); } } /** * @inheritDoc * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ protected function prepareModuleValue(mixed $value):int{ $values = []; foreach(array_values($value) as $i => $val){ if($i > 2){ break; } $values[] = max(0, min(255, intval($val))); } /** @phan-suppress-next-line PhanParamTooFewInternalUnpack */ $color = imagecolorallocate($this->image, ...$values); if($color === false){ throw new QRCodeOutputException('could not set color: imagecolorallocate() error'); } return $color; } protected function getDefaultModuleValue(bool $isDark):int{ return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]); } /** * @inheritDoc * * @throws \ErrorException|\chillerlan\QRCode\Output\QRCodeOutputException */ public function dump(string|null $file = null):string|GdImage{ $this->image = $this->createImage(); // set module values after image creation because we need the GdImage instance $this->setModuleValues(); $this->setBgColor(); if(imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background) === false){ throw new QRCodeOutputException('imagefilledrectangle() error'); } $this->drawImage(); if($this->upscaled){ // scale down to the expected size $scaled = imagescale($this->image, ($this->length / 10), ($this->length / 10)); if($scaled === false){ throw new QRCodeOutputException('imagescale() error'); } $this->image = $scaled; $this->upscaled = false; // Reset scaled and length values after rescaling image to prevent issues with subclasses that use the output from dump() $this->setMatrixDimensions(); } // set transparency after scaling, otherwise it would be undone // @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099 $this->setTransparencyColor(); if($this->options->returnResource){ return $this->image; } $imageData = $this->dumpImage(); $this->saveToFile($imageData, $file); if($this->options->outputBase64){ $imageData = $this->toBase64DataURI($imageData); } return $imageData; } /** * Creates a new GdImage resource and scales it if necessary * * we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales * * @see https://github.com/chillerlan/php-qrcode/issues/23 * * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ protected function createImage():GdImage{ if($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20){ // increase the initial image size by 10 $this->length *= 10; $this->scale *= 10; $this->upscaled = true; } $im = imagecreatetruecolor($this->length, $this->length); if($im === false){ throw new QRCodeOutputException('imagecreatetruecolor() error'); } return $im; } /** * Sets the background color */ protected function setBgColor():void{ if(isset($this->background)){ return; } if($this::moduleValueIsValid($this->options->bgColor)){ $this->background = $this->prepareModuleValue($this->options->bgColor); return; } $this->background = $this->prepareModuleValue([255, 255, 255]); } /** * Sets the transparency color, returns the identifier of the new transparent color */ protected function setTransparencyColor():int{ if(!$this->options->imageTransparent){ return -1; } $transparencyColor = $this->background; if($this::moduleValueIsValid($this->options->transparencyColor)){ $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor); } return imagecolortransparent($this->image, $transparencyColor); } /** * Returns the image quality value for the current GdImage output child class (defaults to -1 ... 100) */ protected function getQuality():int{ return max(-1, min(100, $this->options->quality)); } /** * Draws the QR image */ protected function drawImage():void{ foreach($this->matrix->getMatrix() as $y => $row){ foreach($row as $x => $M_TYPE){ $this->module($x, $y, $M_TYPE); } } } /** * Creates a single QR pixel with the given settings */ protected function module(int $x, int $y, int $M_TYPE):void{ if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ return; } $color = $this->getModuleValue($M_TYPE); if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ imagefilledellipse( $this->image, (($x * $this->scale) + intdiv($this->scale, 2)), (($y * $this->scale) + intdiv($this->scale, 2)), (int)($this->circleDiameter * $this->scale), (int)($this->circleDiameter * $this->scale), $color, ); return; } imagefilledrectangle( $this->image, ($x * $this->scale), ($y * $this->scale), (($x + 1) * $this->scale), (($y + 1) * $this->scale), $color, ); } /** * Renders the image with the gdimage function for the desired output * * @see https://github.com/chillerlan/php-qrcode/issues/223 */ abstract protected function renderImage():void; /** * Creates the final image by calling the desired GD output function * * @throws \chillerlan\QRCode\Output\QRCodeOutputException */ protected function dumpImage():string{ ob_start(); $this->renderImage(); $imageData = ob_get_contents(); if($imageData === false){ throw new QRCodeOutputException('ob_get_contents() error'); } imagedestroy($this->image); ob_end_clean(); return $imageData; } }