Skip to content

Commit

Permalink
Support for WebP and Gif output added (also quality option for PNG) (e…
Browse files Browse the repository at this point in the history
  • Loading branch information
erkens authored Feb 22, 2023
1 parent 30ea8c8 commit 1cea1e0
Show file tree
Hide file tree
Showing 10 changed files with 357 additions and 232 deletions.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\Label\Alignment\LabelAlignmentCenter;
use Endroid\QrCode\Label\Font\NotoSans;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\GdWriter;
use Endroid\QrCode\Writer\Result\PngResult;

$result = Builder::create()
->writer(new PngWriter())
->writer(new GdWriter())
->writerOptions([])
->data('Custom QR code contents')
->encoding(new Encoding('UTF-8'))
Expand All @@ -65,10 +66,10 @@ use Endroid\QrCode\QrCode;
use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\GdWriter;
use Endroid\QrCode\Writer\ValidationException;

$writer = new PngWriter();
$writer = new GdWriter();

// Create QR code
$qrCode = QrCode::create('Life is too short to be generating QR codes')
Expand Down Expand Up @@ -119,6 +120,27 @@ use Endroid\QrCode\Writer\SvgWriter;
$builder->setWriterOptions([SvgWriter::WRITER_OPTION_EXCLUDE_XML_DECLARATION => true]);
```

```php
use Endroid\QrCode\Writer\GdWriter;
use Endroid\QrCode\Writer\Result\PngResult;

$builder->setWriterOptions([GdWriter::WRITER_OPTION_RESULT_CLASS => PngResult::class, PngResult::RESULT_OPTION_QUALITY => -1]);
```

```php
use Endroid\QrCode\Writer\GdWriter;
use Endroid\QrCode\Writer\Result\WebpResult;

$builder->setWriterOptions([GdWriter::WRITER_OPTION_RESULT_CLASS => WebpResult::class, WebpResult::RESULT_OPTION_QUALITY => 80]);
```

```php
use Endroid\QrCode\Writer\GdWriter;
use Endroid\QrCode\Writer\Result\GifResult;

$builder->setWriterOptions([GdWriter::WRITER_OPTION_RESULT_CLASS => GifResult::class]);
```

### Encoding

If you use a barcode scanner you can have some troubles while reading the
Expand Down
4 changes: 2 additions & 2 deletions src/Builder/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\GdWriter;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Endroid\QrCode\Writer\ValidatingWriterInterface;
use Endroid\QrCode\Writer\WriterInterface;
Expand Down Expand Up @@ -57,7 +57,7 @@ public function __construct()
{
$this->options = [
'data' => '',
'writer' => new PngWriter(),
'writer' => new GdWriter(),
'writerOptions' => [],
'qrCodeClass' => QrCode::class,
'logoClass' => Logo::class,
Expand Down
217 changes: 217 additions & 0 deletions src/Writer/GdWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

declare(strict_types=1);

namespace Endroid\QrCode\Writer;

use Endroid\QrCode\Bacon\MatrixFactory;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\ImageData\LabelImageData;
use Endroid\QrCode\ImageData\LogoImageData;
use Endroid\QrCode\Label\Alignment\LabelAlignmentLeft;
use Endroid\QrCode\Label\Alignment\LabelAlignmentRight;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeNone;
use Endroid\QrCode\Writer\Result\AbstractGdResult;
use Endroid\QrCode\Writer\Result\PngResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Zxing\QrReader;

class GdWriter implements WriterInterface, ValidatingWriterInterface
{
public const WRITER_OPTION_RESULT_CLASS = 'result_class';
private string $resultClass = PngResult::class;

public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
if (isset($options[self::WRITER_OPTION_RESULT_CLASS])) {
$this->resultClass = $options[self::WRITER_OPTION_RESULT_CLASS];

if (!is_subclass_of($this->resultClass, AbstractGdResult::class)) {
throw new \Exception($this->resultClass . ' must extend ' . AbstractGdResult::class);
}
}

if (!extension_loaded('gd')) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
}

$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);

$baseBlockSize = $qrCode->getRoundBlockSizeMode() instanceof RoundBlockSizeModeNone ? 10 : intval($matrix->getBlockSize());
$baseImage = imagecreatetruecolor($matrix->getBlockCount() * $baseBlockSize, $matrix->getBlockCount() * $baseBlockSize);

if (!$baseImage) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
}

/** @var int $foregroundColor */
$foregroundColor = imagecolorallocatealpha(
$baseImage,
$qrCode->getForegroundColor()->getRed(),
$qrCode->getForegroundColor()->getGreen(),
$qrCode->getForegroundColor()->getBlue(),
$qrCode->getForegroundColor()->getAlpha()
);

/** @var int $transparentColor */
$transparentColor = imagecolorallocatealpha($baseImage, 255, 255, 255, 127);

imagefill($baseImage, 0, 0, $transparentColor);

for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) {
for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) {
if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) {
imagefilledrectangle(
$baseImage,
$columnIndex * $baseBlockSize,
$rowIndex * $baseBlockSize,
($columnIndex + 1) * $baseBlockSize - 1,
($rowIndex + 1) * $baseBlockSize - 1,
$foregroundColor
);
}
}
}

$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);

if (!$targetImage) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
}

/** @var int $backgroundColor */
$backgroundColor = imagecolorallocatealpha(
$targetImage,
$qrCode->getBackgroundColor()->getRed(),
$qrCode->getBackgroundColor()->getGreen(),
$qrCode->getBackgroundColor()->getBlue(),
$qrCode->getBackgroundColor()->getAlpha()
);

imagefill($targetImage, 0, 0, $backgroundColor);

imagecopyresampled(
$targetImage,
$baseImage,
$matrix->getMarginLeft(),
$matrix->getMarginLeft(),
0,
0,
$matrix->getInnerSize(),
$matrix->getInnerSize(),
imagesx($baseImage),
imagesy($baseImage)
);

if ($qrCode->getBackgroundColor()->getAlpha() > 0) {
imagesavealpha($targetImage, true);
}

$result = new $this->resultClass($matrix, $targetImage, $options);

if ($logo instanceof LogoInterface) {
$result = $this->addLogo($logo, $result);
}

if ($label instanceof LabelInterface) {
$result = $this->addLabel($label, $result);
}

return $result;
}

private function addLogo(LogoInterface $logo, AbstractGdResult $result): AbstractGdResult
{
$logoImageData = LogoImageData::createForLogo($logo);

if ('image/svg+xml' === $logoImageData->getMimeType()) {
throw new \Exception('PNG Writer does not support SVG logo');
}

$targetImage = $result->getImage();
$matrix = $result->getMatrix();

if ($logoImageData->getPunchoutBackground()) {
/** @var int $transparent */
$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);
}
}
}

imagecopyresampled(
$targetImage,
$logoImageData->getImage(),
intval($matrix->getOuterSize() / 2 - $logoImageData->getWidth() / 2),
intval($matrix->getOuterSize() / 2 - $logoImageData->getHeight() / 2),
0,
0,
$logoImageData->getWidth(),
$logoImageData->getHeight(),
imagesx($logoImageData->getImage()),
imagesy($logoImageData->getImage())
);

return new $this->resultClass($matrix, $targetImage);
}

private function addLabel(LabelInterface $label, AbstractGdResult $result): AbstractGdResult
{
$targetImage = $result->getImage();

$labelImageData = LabelImageData::createForLabel($label);

/** @var int $textColor */
$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 ($label->getAlignment() instanceof LabelAlignmentLeft) {
$x = $label->getMargin()->getLeft();
} elseif ($label->getAlignment() instanceof LabelAlignmentRight) {
$x = imagesx($targetImage) - $labelImageData->getWidth() - $label->getMargin()->getRight();
}

imagettftext($targetImage, $label->getFont()->getSize(), 0, $x, $y, $textColor, $label->getFont()->getPath(), $label->getText());

return new $this->resultClass($result->getMatrix(), $targetImage);
}

public function validateResult(ResultInterface $result, string $expectedData): void
{
$string = $result->getString();

if (!class_exists(QrReader::class)) {
throw ValidationException::createForMissingPackage('khanamiryan/qrcode-detector-decoder');
}

$reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB);
if ($reader->text() !== $expectedData) {
throw ValidationException::createForInvalidData($expectedData, strval($reader->text()));
}
}
}
Loading

0 comments on commit 1cea1e0

Please sign in to comment.