fix:修复BUG/升级1.1.6版本

This commit is contained in:
Ying
2023-04-25 20:11:49 +08:00
parent 445e5f9662
commit 6a6866bbaf
2357 changed files with 456920 additions and 140567 deletions

View File

@@ -24,7 +24,7 @@ Use [Composer](https://getcomposer.org/) to install the library. Also make sure
[GD extension](https://www.php.net/manual/en/book.image.php) if you want to generate images.
``` bash
$ composer require endroid/qr-code
composer require endroid/qr-code
```
## Usage: using the builder
@@ -113,10 +113,32 @@ $dataUri = $result->getDataUri();
### Writer options
Some writers provide writer options. Each available writer option is can be
found as a constant prefixed with WRITER_OPTION_ in the writer class.
* `PdfWriter`
* `unit`: unit of measurement (default: mm)
* `fpdf`: PDF to place the image in (default: new PDF)
* `x`: image offset (default: 0)
* `y`: image offset (default: 0)
* `PngWriter`
* `compression_level`: compression level (0-9, default: -1 = zlib default)
* `SvgWriter`
* `block_id`: id of the block element for external reference (default: block)
* `exclude_xml_declaration`: exclude XML declaration (default: false)
* `exclude_svg_width_and_height`: exclude width and height (default: false)
* `force_xlink_href`: forces xlink namespace in case of compatibility issues (default: false)
* `WebPWriter`
* `quality`: image quality (0-100, default: 80)
You can provide any writer options like this.
```php
use Endroid\QrCode\Writer\SvgWriter;
$builder->setWriterOptions([SvgWriter::WRITER_OPTION_EXCLUDE_XML_DECLARATION => true]);
$builder->setWriterOptions([
SvgWriter::WRITER_OPTION_EXCLUDE_XML_DECLARATION => true
]);
```
### Encoding

View File

@@ -12,15 +12,18 @@
}
],
"require": {
"php": "^7.4||^8.0",
"php": "^8.0",
"bacon/bacon-qr-code": "^2.0.5"
},
"require-dev": {
"ext-gd": "*",
"endroid/quality": "dev-master",
"khanamiryan/qrcode-detector-decoder": "^1.0.4",
"khanamiryan/qrcode-detector-decoder": "^1.0.4||^2.0.2",
"setasign/fpdf": "^1.8.2"
},
"conflict": {
"khanamiryan/qrcode-detector-decoder": "^1.0.6"
},
"suggest": {
"ext-gd": "Enables you to write PNG images",
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
@@ -41,6 +44,9 @@
"sort-packages": true,
"preferred-install": {
"endroid/*": "source"
},
"allow-plugins": {
"endroid/installer": true
}
},
"extra": {

View File

@@ -25,7 +25,7 @@ use Endroid\QrCode\Writer\WriterInterface;
class Builder implements BuilderInterface
{
/**
* @var array<mixed>{
* @var array<string, mixed>{
* data: string,
* writer: WriterInterface,
* writerOptions: array,
@@ -78,7 +78,7 @@ class Builder implements BuilderInterface
return $this;
}
/** @param array<mixed> $writerOptions */
/** @param array<string, mixed> $writerOptions */
public function writerOptions(array $writerOptions): BuilderInterface
{
$this->options['writerOptions'] = $writerOptions;
@@ -243,7 +243,7 @@ class Builder implements BuilderInterface
*
* @return mixed
*/
private function buildObject(string $class, string $optionsPrefix = null)
private function buildObject(string $class, string|null $optionsPrefix = null)
{
/** @var \ReflectionClass<object> $reflectionClass */
$reflectionClass = new \ReflectionClass($class);

View File

@@ -20,7 +20,7 @@ interface BuilderInterface
public function writer(WriterInterface $writer): BuilderInterface;
/** @param array<mixed> $writerOptions */
/** @param array<string, mixed> $writerOptions */
public function writerOptions(array $writerOptions): BuilderInterface;
public function data(string $data): BuilderInterface;

View File

@@ -6,17 +6,12 @@ namespace Endroid\QrCode\Color;
final class Color implements ColorInterface
{
private int $red;
private int $green;
private int $blue;
private int $alpha;
public function __construct(int $red, int $green, int $blue, int $alpha = 0)
{
$this->red = $red;
$this->green = $green;
$this->blue = $blue;
$this->alpha = $alpha;
public function __construct(
private int $red,
private int $green,
private int $blue,
private int $alpha = 0
) {
}
public function getRed(): int

View File

@@ -6,15 +6,12 @@ namespace Endroid\QrCode\Encoding;
final class Encoding implements EncodingInterface
{
private string $value;
public function __construct(string $value)
{
public function __construct(
private string $value
) {
if (!in_array($value, mb_list_encodings())) {
throw new \Exception(sprintf('Invalid encoding "%s"', $value));
}
$this->value = $value;
}
public function __toString(): string

View File

@@ -16,11 +16,6 @@ final class ValidationException extends \Exception
return new self(sprintf('Please install "%s" or disable image validation', $packageName));
}
public static function createForIncompatiblePhpVersion(): self
{
return new self('The validator is not compatible with PHP 8 yet, see https://github.com/khanamiryan/php-qrcode-detector-decoder/pull/103');
}
public static function createForInvalidData(string $expectedData, string $actualData): self
{
return new self('The validation reader read "'.$actualData.'" instead of "'.$expectedData.'". Adjust your parameters to increase readability or disable validation.');

View File

@@ -8,13 +8,10 @@ use Endroid\QrCode\Label\LabelInterface;
class LabelImageData
{
private int $width;
private int $height;
private function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
private function __construct(
private int $width,
private int $height
) {
}
public static function createForLabel(LabelInterface $label): self

View File

@@ -8,31 +8,14 @@ use Endroid\QrCode\Logo\LogoInterface;
class LogoImageData
{
private string $data;
/** @var mixed */
private $image;
private string $mimeType;
private int $width;
private int $height;
private bool $punchoutBackground;
/** @param mixed $image */
private function __construct(
string $data,
$image,
string $mimeType,
int $width,
int $height,
bool $punchoutBackground
private string $data,
private \GdImage|null $image,
private string $mimeType,
private int $width,
private int $height,
private bool $punchoutBackground
) {
$this->data = $data;
$this->image = $image;
$this->mimeType = $mimeType;
$this->width = $width;
$this->height = $height;
$this->punchoutBackground = $punchoutBackground;
}
public static function createForLogo(LogoInterface $logo): self
@@ -89,10 +72,9 @@ class LogoImageData
return $this->data;
}
/** @return mixed */
public function getImage()
public function getImage(): \GdImage
{
if (null === $this->image) {
if (!$this->image instanceof \GdImage) {
throw new \Exception('SVG Images have no image resource');
}
@@ -126,10 +108,7 @@ class LogoImageData
private static function detectMimeTypeFromUrl(string $url): string
{
/** @var mixed $format */
$format = PHP_VERSION_ID >= 80000 ? true : 1;
$headers = get_headers($url, $format);
$headers = get_headers($url, true);
if (!is_array($headers) || !isset($headers['Content-Type'])) {
throw new \Exception(sprintf('Content type could not be determined for logo URL "%s"', $url));

View File

@@ -6,18 +6,14 @@ namespace Endroid\QrCode\Label\Font;
final class Font implements FontInterface
{
private string $path;
private int $size;
public function __construct(string $path, int $size = 16)
{
$this->validatePath($path);
$this->path = $path;
$this->size = $size;
public function __construct(
private string $path,
private int $size = 16
) {
$this->assertValidPath($path);
}
private function validatePath(string $path): void
private function assertValidPath(string $path): void
{
if (!file_exists($path)) {
throw new \Exception(sprintf('Invalid font path "%s"', $path));

View File

@@ -6,11 +6,9 @@ namespace Endroid\QrCode\Label\Font;
final class NotoSans implements FontInterface
{
private int $size;
public function __construct(int $size = 16)
{
$this->size = $size;
public function __construct(
private int $size = 16
) {
}
public function getPath(): string

View File

@@ -6,11 +6,9 @@ namespace Endroid\QrCode\Label\Font;
final class OpenSans implements FontInterface
{
private int $size;
public function __construct(int $size = 16)
{
$this->size = $size;
public function __construct(
private int $size = 16
) {
}
public function getPath(): string

View File

@@ -15,20 +15,18 @@ use Endroid\QrCode\Label\Margin\MarginInterface;
final class Label implements LabelInterface
{
private string $text;
private FontInterface $font;
private LabelAlignmentInterface $alignment;
private MarginInterface $margin;
private ColorInterface $textColor;
public function __construct(
string $text,
FontInterface $font = null,
LabelAlignmentInterface $alignment = null,
MarginInterface $margin = null,
ColorInterface $textColor = null
private string $text,
FontInterface|null $font = null,
LabelAlignmentInterface|null $alignment = null,
MarginInterface|null $margin = null,
ColorInterface|null $textColor = null
) {
$this->text = $text;
$this->font = $font ?? new Font(__DIR__.'/../../assets/noto_sans.otf', 16);
$this->alignment = $alignment ?? new LabelAlignmentCenter();
$this->margin = $margin ?? new Margin(0, 10, 10, 10);

View File

@@ -6,17 +6,12 @@ namespace Endroid\QrCode\Label\Margin;
final class Margin implements MarginInterface
{
private int $top;
private int $right;
private int $bottom;
private int $left;
public function __construct(int $top, int $right, int $bottom, int $left)
{
$this->top = $top;
$this->right = $right;
$this->bottom = $bottom;
$this->left = $left;
public function __construct(
private int $top,
private int $right,
private int $bottom,
private int $left
) {
}
public function getTop(): int

View File

@@ -6,17 +6,12 @@ namespace Endroid\QrCode\Logo;
final class Logo implements LogoInterface
{
private string $path;
private ?int $resizeToWidth;
private ?int $resizeToHeight;
private bool $punchoutBackground;
public function __construct(string $path, ?int $resizeToWidth = null, ?int $resizeToHeight = null, bool $punchoutBackground = false)
{
$this->path = $path;
$this->resizeToWidth = $resizeToWidth;
$this->resizeToHeight = $resizeToHeight;
$this->punchoutBackground = $punchoutBackground;
public function __construct(
private string $path,
private int|null $resizeToWidth = null,
private int|null $resizeToHeight = null,
private bool $punchoutBackground = false
) {
}
public static function create(string $path): self
@@ -36,24 +31,24 @@ final class Logo implements LogoInterface
return $this;
}
public function getResizeToWidth(): ?int
public function getResizeToWidth(): int|null
{
return $this->resizeToWidth;
}
public function setResizeToWidth(?int $resizeToWidth): self
public function setResizeToWidth(int|null $resizeToWidth): self
{
$this->resizeToWidth = $resizeToWidth;
return $this;
}
public function getResizeToHeight(): ?int
public function getResizeToHeight(): int|null
{
return $this->resizeToHeight;
}
public function setResizeToHeight(?int $resizeToHeight): self
public function setResizeToHeight(int|null $resizeToHeight): self
{
$this->resizeToHeight = $resizeToHeight;

View File

@@ -8,9 +8,9 @@ interface LogoInterface
{
public function getPath(): string;
public function getResizeToWidth(): ?int;
public function getResizeToWidth(): int|null;
public function getResizeToHeight(): ?int;
public function getResizeToHeight(): int|null;
public function getPunchoutBackground(): bool;
}

View File

@@ -11,9 +11,6 @@ use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeShrink;
final class Matrix implements MatrixInterface
{
/** @var array<int, array<int, int>> */
private array $blockValues = [];
private float $blockSize;
private int $innerSize;
private int $outerSize;
@@ -21,10 +18,12 @@ final class Matrix implements MatrixInterface
private int $marginRight;
/** @param array<array<int>> $blockValues */
public function __construct(array $blockValues, int $size, int $margin, RoundBlockSizeModeInterface $roundBlockSizeMode)
{
$this->blockValues = $blockValues;
public function __construct(
private array $blockValues,
int $size,
int $margin,
RoundBlockSizeModeInterface $roundBlockSizeMode
) {
$this->blockSize = $size / $this->getBlockCount();
$this->innerSize = $size;
$this->outerSize = $size + 2 * $margin;

View File

@@ -15,30 +15,24 @@ use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
final class QrCode implements QrCodeInterface
{
private string $data;
private EncodingInterface $encoding;
private ErrorCorrectionLevelInterface $errorCorrectionLevel;
private int $size;
private int $margin;
private RoundBlockSizeModeInterface $roundBlockSizeMode;
private ColorInterface $foregroundColor;
private ColorInterface $backgroundColor;
public function __construct(
string $data,
EncodingInterface $encoding = null,
ErrorCorrectionLevelInterface $errorCorrectionLevel = null,
int $size = 300,
int $margin = 10,
RoundBlockSizeModeInterface $roundBlockSizeMode = null,
ColorInterface $foregroundColor = null,
ColorInterface $backgroundColor = null
private string $data,
EncodingInterface|null $encoding = null,
ErrorCorrectionLevelInterface|null $errorCorrectionLevel = null,
private int $size = 300,
private int $margin = 10,
RoundBlockSizeModeInterface|null $roundBlockSizeMode = null,
ColorInterface|null $foregroundColor = null,
ColorInterface|null $backgroundColor = null
) {
$this->data = $data;
$this->encoding = $encoding ?? new Encoding('UTF-8');
$this->errorCorrectionLevel = $errorCorrectionLevel ?? new ErrorCorrectionLevelLow();
$this->size = $size;
$this->margin = $margin;
$this->roundBlockSizeMode = $roundBlockSizeMode ?? new RoundBlockSizeModeMargin();
$this->foregroundColor = $foregroundColor ?? new Color(0, 0, 0);
$this->backgroundColor = $backgroundColor ?? new Color(255, 255, 255);

View File

@@ -1,7 +0,0 @@
<?php
declare(strict_types=1);
interface WritableInterface
{
}

View File

@@ -0,0 +1,205 @@
<?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\GdResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Zxing\QrReader;
abstract class AbstractGdWriter implements WriterInterface, ValidatingWriterInterface
{
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $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');
}
$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 GdResult($matrix, $targetImage);
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, GdResult $result): GdResult
{
$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 GdResult($matrix, $targetImage);
}
private function addLabel(LabelInterface $label, GdResult $result): GdResult
{
$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 GdResult($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()));
}
}
}

View File

@@ -13,7 +13,7 @@ use Endroid\QrCode\Writer\Result\ResultInterface;
final class BinaryWriter implements WriterInterface
{
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);

View File

@@ -11,15 +11,9 @@ use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\ConsoleResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
/**
* Writer of QR Code for CLI.
*/
class ConsoleWriter implements WriterInterface
final class ConsoleWriter implements WriterInterface
{
/**
* {@inheritDoc}
*/
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, $options = []): ResultInterface
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, $options = []): ResultInterface
{
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);

View File

@@ -13,7 +13,7 @@ use Endroid\QrCode\Writer\Result\ResultInterface;
final class DebugWriter implements WriterInterface, ValidatingWriterInterface
{
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);

View File

@@ -15,7 +15,7 @@ final class EpsWriter implements WriterInterface
{
public const DECIMAL_PRECISION = 10;
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\GdResult;
use Endroid\QrCode\Writer\Result\GifResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
final class GifWriter extends AbstractGdWriter
{
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
/** @var GdResult $gdResult */
$gdResult = parent::write($qrCode, $logo, $label, $options);
return new GifResult($gdResult->getMatrix(), $gdResult->getImage());
}
}

View File

@@ -17,8 +17,9 @@ final class PdfWriter implements WriterInterface
public const WRITER_OPTION_PDF = 'fpdf';
public const WRITER_OPTION_X = 'x';
public const WRITER_OPTION_Y = 'y';
public const WRITER_OPTION_LINK = 'link';
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);
@@ -99,6 +100,11 @@ final class PdfWriter implements WriterInterface
$fpdf->Cell($matrix->getOuterSize(), 0, $label->getText(), 0, 0, 'C');
}
if (isset($options[self::WRITER_OPTION_LINK])) {
$link = $options[self::WRITER_OPTION_LINK];
$fpdf->Link($x, $y, $x + $matrix->getOuterSize(), $y + $matrix->getOuterSize(), $link);
}
return new PdfResult($matrix, $fpdf);
}

View File

@@ -4,214 +4,26 @@ 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\GdResult;
use Endroid\QrCode\Writer\Result\PngResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Zxing\QrReader;
final class PngWriter implements WriterInterface, ValidatingWriterInterface
final class PngWriter extends AbstractGdWriter
{
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
public const WRITER_OPTION_COMPRESSION_LEVEL = 'compression_level';
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $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');
if (!isset($options[self::WRITER_OPTION_COMPRESSION_LEVEL])) {
$options[self::WRITER_OPTION_COMPRESSION_LEVEL] = -1;
}
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);
/** @var GdResult $gdResult */
$gdResult = parent::write($qrCode, $logo, $label, $options);
$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 (PHP_VERSION_ID < 80000) {
imagedestroy($baseImage);
}
if ($qrCode->getBackgroundColor()->getAlpha() > 0) {
imagesavealpha($targetImage, true);
}
$result = new PngResult($matrix, $targetImage);
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, PngResult $result): PngResult
{
$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())
);
if (PHP_VERSION_ID < 80000) {
imagedestroy($logoImageData->getImage());
}
return new PngResult($matrix, $targetImage);
}
private function addLabel(LabelInterface $label, PngResult $result): PngResult
{
$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 PngResult($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');
}
if (PHP_VERSION_ID >= 80000) {
throw ValidationException::createForIncompatiblePhpVersion();
}
$reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB);
if ($reader->text() !== $expectedData) {
throw ValidationException::createForInvalidData($expectedData, strval($reader->text()));
}
return new PngResult($gdResult->getMatrix(), $gdResult->getImage(), $options[self::WRITER_OPTION_COMPRESSION_LEVEL]);
}
}

View File

@@ -8,11 +8,9 @@ use Endroid\QrCode\Matrix\MatrixInterface;
abstract class AbstractResult implements ResultInterface
{
private MatrixInterface $matrix;
public function __construct(MatrixInterface $matrix)
{
$this->matrix = $matrix;
public function __construct(
private MatrixInterface $matrix
) {
}
public function getMatrix(): MatrixInterface

View File

@@ -11,29 +11,17 @@ use Endroid\QrCode\QrCodeInterface;
final class DebugResult extends AbstractResult
{
private QrCodeInterface $qrCode;
private ?LogoInterface $logo;
private ?LabelInterface $label;
/** @var array<mixed> */
private array $options;
private bool $validateResult = false;
/** @param array<mixed> $options */
public function __construct(
MatrixInterface $matrix,
QrCodeInterface $qrCode,
LogoInterface $logo = null,
LabelInterface $label = null,
array $options = []
private QrCodeInterface $qrCode,
private LogoInterface|null $logo = null,
private LabelInterface|null $label = null,
/** @var array<string, mixed> $options */
private array $options = []
) {
parent::__construct($matrix);
$this->qrCode = $qrCode;
$this->logo = $logo;
$this->label = $label;
$this->options = $options;
}
public function setValidateResult(bool $validateResult): void

View File

@@ -8,15 +8,12 @@ use Endroid\QrCode\Matrix\MatrixInterface;
final class EpsResult extends AbstractResult
{
/** @var array<string> */
private array $lines;
/** @param array<string> $lines */
public function __construct(MatrixInterface $matrix, array $lines)
{
public function __construct(
MatrixInterface $matrix,
/** @var array<string> $lines */
private array $lines
) {
parent::__construct($matrix);
$this->lines = $lines;
}
public function getString(): string

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
use Endroid\QrCode\Matrix\MatrixInterface;
class GdResult extends AbstractResult
{
public function __construct(
MatrixInterface $matrix,
protected \GdImage $image
) {
parent::__construct($matrix);
}
public function getImage(): \GdImage
{
return $this->image;
}
public function getString(): string
{
throw new \Exception('You can only use this method in a concrete implementation');
}
public function getMimeType(): string
{
throw new \Exception('You can only use this method in a concrete implementation');
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
final class GifResult extends GdResult
{
public function getString(): string
{
ob_start();
imagegif($this->image);
return strval(ob_get_clean());
}
public function getMimeType(): string
{
return 'image/gif';
}
}

View File

@@ -8,13 +8,11 @@ use Endroid\QrCode\Matrix\MatrixInterface;
final class PdfResult extends AbstractResult
{
private \FPDF $fpdf;
public function __construct(MatrixInterface $matrix, \FPDF $fpdf)
{
public function __construct(
MatrixInterface $matrix,
private \FPDF $fpdf
) {
parent::__construct($matrix);
$this->fpdf = $fpdf;
}
public function getPdf(): \FPDF

View File

@@ -6,29 +6,20 @@ namespace Endroid\QrCode\Writer\Result;
use Endroid\QrCode\Matrix\MatrixInterface;
final class PngResult extends AbstractResult
final class PngResult extends GdResult
{
/** @var mixed */
private $image;
private int $quality;
/** @param mixed $image */
public function __construct(MatrixInterface $matrix, $image)
public function __construct(MatrixInterface $matrix, \GdImage $image, int $quality = -1)
{
parent::__construct($matrix);
$this->image = $image;
}
/** @return mixed */
public function getImage()
{
return $this->image;
parent::__construct($matrix, $image);
$this->quality = $quality;
}
public function getString(): string
{
ob_start();
imagepng($this->image);
imagepng($this->image, quality: $this->quality);
return strval(ob_get_clean());
}

View File

@@ -8,18 +8,12 @@ use Endroid\QrCode\Matrix\MatrixInterface;
final class SvgResult extends AbstractResult
{
private \SimpleXMLElement $xml;
private bool $excludeXmlDeclaration;
public function __construct(
MatrixInterface $matrix,
\SimpleXMLElement $xml,
bool $excludeXmlDeclaration = false
private \SimpleXMLElement $xml,
private bool $excludeXmlDeclaration = false
) {
parent::__construct($matrix);
$this->xml = $xml;
$this->excludeXmlDeclaration = $excludeXmlDeclaration;
}
public function getXml(): \SimpleXMLElement

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
use Endroid\QrCode\Matrix\MatrixInterface;
final class WebPResult extends GdResult
{
private int $quality;
public function __construct(MatrixInterface $matrix, \GdImage $image, int $quality = -1)
{
parent::__construct($matrix, $image);
$this->quality = $quality;
}
public function getString(): string
{
if (!function_exists('imagewebp')) {
throw new \Exception('WebP support is not available in your GD installation');
}
ob_start();
imagewebp($this->image, quality: $this->quality);
return strval(ob_get_clean());
}
public function getMimeType(): string
{
return 'image/webp';
}
}

View File

@@ -20,7 +20,7 @@ final class SvgWriter implements WriterInterface
public const WRITER_OPTION_EXCLUDE_SVG_WIDTH_AND_HEIGHT = 'exclude_svg_width_and_height';
public const WRITER_OPTION_FORCE_XLINK_HREF = 'force_xlink_href';
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
if (!isset($options[self::WRITER_OPTION_BLOCK_ID])) {
$options[self::WRITER_OPTION_BLOCK_ID] = 'block';
@@ -81,7 +81,7 @@ final class SvgWriter implements WriterInterface
return $result;
}
/** @param array<mixed> $options */
/** @param array<string, mixed> $options */
private function addLogo(LogoInterface $logo, SvgResult $result, array $options): void
{
$logoImageData = LogoImageData::createForLogo($logo);

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\GdResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Endroid\QrCode\Writer\Result\WebPResult;
final class WebPWriter extends AbstractGdWriter
{
public const WRITER_OPTION_QUALITY = 'quality';
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface
{
if (!isset($options[self::WRITER_OPTION_QUALITY])) {
$options[self::WRITER_OPTION_QUALITY] = -1;
}
/** @var GdResult $gdResult */
$gdResult = parent::write($qrCode, $logo, $label, $options);
return new WebPResult($gdResult->getMatrix(), $gdResult->getImage(), $options[self::WRITER_OPTION_QUALITY]);
}
}

View File

@@ -11,6 +11,6 @@ use Endroid\QrCode\Writer\Result\ResultInterface;
interface WriterInterface
{
/** @param array<mixed> $options */
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface;
/** @param array<string, mixed> $options */
public function write(QrCodeInterface $qrCode, LogoInterface|null $logo = null, LabelInterface|null $label = null, array $options = []): ResultInterface;
}