<?php
declare(strict_types=1);
namespace HeroQR\Contracts\Customs;
use Endroid\QrCode\{QrCodeInterface, RoundBlockSizeMode};
use Endroid\QrCode\{Logo\LogoInterface, Matrix\MatrixInterface};
use Endroid\QrCode\ImageData\{LabelImageData, LogoImageData};
use Endroid\QrCode\Label\{LabelAlignment, LabelInterface};
use Endroid\QrCode\Writer\{AbstractGdWriter, WriterInterface};
use Endroid\QrCode\Writer\Result\{GdResult, ResultInterface};
use HeroQR\Customs\{ImageOverlay, ShapePaths};
/**
* The AbstractWriter class is responsible for generating QR code images using the GD library
* This class provides base functionality for creating QR code images with options like logo and label embedding
* Subclasses should implement specific logic for rendering QR codes using the GD image processing library
*
* @package HeroQR\Contracts\Customs
*/
readonly abstract class AbstractWriter extends AbstractGdWriter implements WriterInterface
{
private const QUALITY_MULTIPLIER = 10;
private ImageOverlay $imageOverlay;
protected string $blockShape;
public function __construct($background, $overlay, $blockShape)
{
$this->imageOverlay = new ImageOverlay($background, $overlay);
$this->blockShape = ShapePaths::getValueByKey($blockShape) ?? 'drawSquare';
}
/**
* Writes a QR code image with optional logo and label
*
* @param QrCodeInterface $qrCode The QR code to be generated
* @param LogoInterface|null $logo The logo to be embedded in the QR code (optional)
* @param LabelInterface|null $label The label to be added to the QR code (optional)
* @param array $options Additional options for the QR code generation
*
* @return ResultInterface The result containing the generated QR code image
*
* @throws \Exception If the GD extension is not loaded
*/
public function write(
QrCodeInterface $qrCode,
?LogoInterface $logo = null,
?LabelInterface $label = null,
array $options = []
): ResultInterface
{
if (!extension_loaded('gd')) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
}
$matrix = $this->getMatrix($qrCode);
$baseBlockSize = (RoundBlockSizeMode::Margin === $qrCode->getRoundBlockSizeMode() ? 10 : intval($matrix->getBlockSize())) * self::QUALITY_MULTIPLIER;
$baseImage = $this->createBaseImage($matrix, $baseBlockSize);
$foregroundColor = $this->allocateForegroundColor($baseImage, $qrCode);
Drawer::drawQrCode($baseImage, $matrix, $baseBlockSize, $foregroundColor, $qrCode, $this->blockShape, $logo, $this->imageOverlay);
$targetImage = $this->createTargetImage($matrix, $qrCode, $label);
Drawer::copyResampledImage(
$targetImage,
$baseImage,
['X' => $matrix->getMarginLeft(), 'Y' => $matrix->getMarginRight(), 'Width' => $matrix->getInnerSize(), 'Height' => $matrix->getInnerSize()],
['X' => 0, 'Y' => 0, 'Width' => imagesy($baseImage), 'Height' => imagesy($baseImage)]
);
$this->destroyImages([$baseImage]);
$result = new GdResult($matrix, $targetImage);
if ($logo instanceof LogoInterface) {
$result = $this->addLogoToResult($logo, $result);
}
if ($label instanceof LabelInterface) {
$result = $this->addLabelToResult($label, $result);
}
return $result;
}
/**
* Creates the base image for the QR code
*
* @param MatrixInterface $matrix The matrix that defines the block layout for the QR code
* @param int $baseBlockSize The size of each block in the QR code
*
* @return \GdImage The created base image resource
*/
private function createBaseImage(
MatrixInterface $matrix,
int $baseBlockSize
): \GdImage
{
$baseImage = imagecreatetruecolor($matrix->getBlockCount() * $baseBlockSize, $matrix->getBlockCount() * $baseBlockSize);
imageantialias($baseImage, true);
imagesavealpha($baseImage, true);
imagealphablending($baseImage, false);
$transparentColor = imagecolorallocatealpha($baseImage, 0, 0, 0, 127);
imagefill($baseImage, 0, 0, $transparentColor);
return $baseImage;
}
/**
* Allocates the foreground color for the base image
*
* @param \GdImage $baseImage The base image to apply the foreground color to
* @param QrCodeInterface $qrCode The QR code object to get the foreground color from
*
* @return int The allocated color identifier
*/
private function allocateForegroundColor(
\GdImage $baseImage,
QrCodeInterface $qrCode
): int
{
return imagecolorallocatealpha(
$baseImage,
$qrCode->getForegroundColor()->getRed(),
$qrCode->getForegroundColor()->getGreen(),
$qrCode->getForegroundColor()->getBlue(),
$qrCode->getForegroundColor()->getAlpha()
);
}
/**
* Creates the target image for the QR code, including label if provided
*
* @param MatrixInterface $matrix The matrix representation of the QR code
* @param QrCodeInterface $qrCode The QR code instance
* @param LabelInterface|null $label The optional label to add below the QR code
*
* @return resource|\GdImage The created target image.
* @throws \Exception
*/
private function createTargetImage(
MatrixInterface $matrix,
QrCodeInterface $qrCode,
?LabelInterface $label
): \GdImage
{
$targetWidth = $matrix->getOuterSize();
$targetHeight = $matrix->getOuterSize();
if ($label instanceof LabelInterface) {
$labelImageData = LabelImageData::createForLabel($label);
$targetHeight += $labelImageData->getHeight() + $label->getMargin()->getTop() + $label->getMargin()->getBottom();
}
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
imageantialias($targetImage, true);
imagesavealpha($targetImage, true);
imagealphablending($targetImage, false);
$backgroundColor = imagecolorallocatealpha(
$targetImage,
$qrCode->getBackgroundColor()->getRed(),
$qrCode->getBackgroundColor()->getGreen(),
$qrCode->getBackgroundColor()->getBlue(),
$qrCode->getBackgroundColor()->getAlpha()
);
imagefill($targetImage, 0, 0, $backgroundColor);
imagealphablending($targetImage, true);
return $targetImage;
}
/**
* Adds a logo to the image, centered and with optional punch out background
* Throws an exception if the logo is not in PNG format
*
* @param LogoInterface $logo The logo to add
* @param GdResult $result The image to add the logo to
*
* @return GdResult The updated image with the logo
* @throws \Exception
*/
private function addLogoToResult(
LogoInterface $logo,
GdResult $result
): GdResult
{
$logoImageData = LogoImageData::createForLogo($logo);
if ('image/png' !== $logoImageData->getMimeType()) {
throw new \Exception('PNG Writer does not support SVG logo');
}
$targetImage = $result->getImage();
$matrix = $result->getMatrix();
if ($logoImageData->getPunchoutBackground()) {
$transparent = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
imagealphablending($targetImage, false);
$xOffsetStart = intval($matrix->getOuterSize() / 2 - $logoImageData->getWidth() / 2);
$yOffsetStart = intval($matrix->getOuterSize() / 2 - $logoImageData->getHeight() / 2);
for ($xOffset = $xOffsetStart; $xOffset < $xOffsetStart + $logoImageData->getWidth(); ++$xOffset) {
for ($yOffset = $yOffsetStart; $yOffset < $yOffsetStart + $logoImageData->getHeight(); ++$yOffset) {
imagesetpixel($targetImage, $xOffset, $yOffset, $transparent);
}
}
}
Drawer::copyResampledImage(
$targetImage,
$logoImageData->getImage(),
['X' => intval($matrix->getOuterSize() / 2 - $logoImageData->getWidth() / 2), 'Y' => intval($matrix->getOuterSize() / 2 - $logoImageData->getWidth() / 2), 'Width' => $logoImageData->getWidth(), 'Height' => $logoImageData->getHeight()],
['X' => 0, 'Y' => 0, 'Width' => imagesy($logoImageData->getImage()), 'Height' => imagesy($logoImageData->getImage())]
);
return new GdResult($matrix, $targetImage);
}
/**
* Adds a label with text to the image
*
* The label's position is based on its alignment (left, center, right) and margin
* The text is drawn using the specified font and color
*
* @param LabelInterface $label The label to add
* @param GdResult $result The image to add the label to
*
* @return GdResult The updated image
* @throws \Exception
*/
private function addLabelToResult(
LabelInterface $label,
GdResult $result
): GdResult
{
$targetImage = $result->getImage();
$labelImageData = LabelImageData::createForLabel($label);
$textColor = imagecolorallocatealpha(
$targetImage,
$label->getTextColor()->getRed(),
$label->getTextColor()->getGreen(),
$label->getTextColor()->getBlue(),
$label->getTextColor()->getAlpha()
);
$x = intval(imagesx($targetImage) / 2 - $labelImageData->getWidth() / 2);
$y = imagesy($targetImage) - $label->getMargin()->getBottom();
if (LabelAlignment::Left === $label->getAlignment()) {
$x = $label->getMargin()->getLeft();
} elseif (LabelAlignment::Right === $label->getAlignment()) {
$x = imagesx($targetImage) - $labelImageData->getWidth() - $label->getMargin()->getRight();
}
imagettftext($targetImage, $label->getFont()->getSize(), 0, $x, $y, $textColor, $label->getFont()->getPath(), $label->getText());
return new GdResult($result->getMatrix(), $targetImage);
}
/**
* Frees up memory by destroying image resources.
*
* @param array $images An array of image resources.
* @return void
*/
private function destroyImages(array $images): void
{
foreach ($images as $image) {
imagedestroy($image);
}
}
}
|