init commit

This commit is contained in:
2026-03-17 09:56:00 +08:00
commit e2c8ae752d
6827 changed files with 1211784 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
Copyright (c) Jeroen van den Enden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode;
use BaconQrCode\Common\ErrorCorrectionLevel as BaconErrorCorrectionLevel;
use MyCLabs\Enum\Enum;
/**
* @method static ErrorCorrectionLevel LOW()
* @method static ErrorCorrectionLevel MEDIUM()
* @method static ErrorCorrectionLevel QUARTILE()
* @method static ErrorCorrectionLevel HIGH()
*/
class ErrorCorrectionLevel extends Enum
{
const LOW = 'low';
const MEDIUM = 'medium';
const QUARTILE = 'quartile';
const HIGH = 'high';
public function toBaconErrorCorrectionLevel(): BaconErrorCorrectionLevel
{
$name = strtoupper(substr($this->getValue(), 0, 1));
return BaconErrorCorrectionLevel::valueOf($name);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class GenerateImageException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class InvalidLogoException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class InvalidPathException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class InvalidWriterException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class MissingExtensionException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class MissingFunctionException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class MissingLogoHeightException extends QrCodeException
{
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
use Exception;
abstract class QrCodeException extends Exception
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class UnsupportedExtensionException extends QrCodeException
{
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Exception;
class ValidationException extends QrCodeException
{
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Factory;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\WriterRegistryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
class QrCodeFactory implements QrCodeFactoryInterface
{
private $writerRegistry;
/** @var OptionsResolver */
private $optionsResolver;
private $defaultOptions;
/** @var array */
private $definedOptions = [
'writer',
'writer_options',
'size',
'margin',
'foreground_color',
'background_color',
'encoding',
'round_block_size',
'error_correction_level',
'logo_path',
'logo_width',
'logo_height',
'label',
'label_font_size',
'label_font_path',
'label_alignment',
'label_margin',
'validate_result',
];
public function __construct(array $defaultOptions = [], WriterRegistryInterface $writerRegistry = null)
{
$this->defaultOptions = $defaultOptions;
$this->writerRegistry = $writerRegistry;
}
public function create(string $text = '', array $options = []): QrCodeInterface
{
$options = $this->getOptionsResolver()->resolve($options);
$accessor = PropertyAccess::createPropertyAccessor();
$qrCode = new QrCode($text);
if ($this->writerRegistry instanceof WriterRegistryInterface) {
$qrCode->setWriterRegistry($this->writerRegistry);
}
foreach ($this->definedOptions as $option) {
if (isset($options[$option])) {
if ('writer' === $option) {
$options['writer_by_name'] = $options[$option];
$option = 'writer_by_name';
}
if ('error_correction_level' === $option) {
$options[$option] = new ErrorCorrectionLevel($options[$option]);
}
$accessor->setValue($qrCode, $option, $options[$option]);
}
}
if (!$qrCode instanceof QrCodeInterface) {
throw new ValidationException('QR Code was messed up by property accessor');
}
return $qrCode;
}
private function getOptionsResolver(): OptionsResolver
{
if (!$this->optionsResolver instanceof OptionsResolver) {
$this->optionsResolver = $this->createOptionsResolver();
}
return $this->optionsResolver;
}
private function createOptionsResolver(): OptionsResolver
{
$optionsResolver = new OptionsResolver();
$optionsResolver
->setDefaults($this->defaultOptions)
->setDefined($this->definedOptions)
;
return $optionsResolver;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Factory;
use Endroid\QrCode\QrCodeInterface;
interface QrCodeFactoryInterface
{
public function create(string $text = '', array $options = []): QrCodeInterface;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode;
use MyCLabs\Enum\Enum;
/**
* @method static LabelAlignment LEFT()
* @method static LabelAlignment CENTER()
* @method static LabelAlignment RIGHT()
*/
class LabelAlignment extends Enum
{
const LEFT = 'left';
const CENTER = 'center';
const RIGHT = 'right';
}

View File

@@ -0,0 +1,445 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode;
use BaconQrCode\Encoder\Encoder;
use Endroid\QrCode\Exception\InvalidPathException;
use Endroid\QrCode\Exception\UnsupportedExtensionException;
use Endroid\QrCode\Writer\WriterInterface;
class QrCode implements QrCodeInterface
{
const LABEL_FONT_PATH_DEFAULT = __DIR__.'/../assets/fonts/noto_sans.otf';
private $text;
/** @var int */
private $size = 300;
/** @var int */
private $margin = 10;
/** @var array */
private $foregroundColor = [
'r' => 0,
'g' => 0,
'b' => 0,
'a' => 0,
];
/** @var array */
private $backgroundColor = [
'r' => 255,
'g' => 255,
'b' => 255,
'a' => 0,
];
/** @var string */
private $encoding = 'UTF-8';
/** @var bool */
private $roundBlockSize = true;
private $errorCorrectionLevel;
/** @var string */
private $logoPath;
/** @var int|null */
private $logoWidth;
/** @var int|null */
private $logoHeight;
/** @var string */
private $label;
/** @var int */
private $labelFontSize = 16;
/** @var string */
private $labelFontPath = self::LABEL_FONT_PATH_DEFAULT;
private $labelAlignment;
/** @var array */
private $labelMargin = [
't' => 0,
'r' => 10,
'b' => 10,
'l' => 10,
];
/** @var WriterRegistryInterface */
private $writerRegistry;
/** @var WriterInterface|null */
private $writer;
/** @var array */
private $writerOptions = [];
/** @var bool */
private $validateResult = false;
public function __construct(string $text = '')
{
$this->text = $text;
$this->errorCorrectionLevel = ErrorCorrectionLevel::LOW();
$this->labelAlignment = LabelAlignment::CENTER();
$this->createWriterRegistry();
}
public function setText(string $text): void
{
$this->text = $text;
}
public function getText(): string
{
return $this->text;
}
public function setSize(int $size): void
{
$this->size = $size;
}
public function getSize(): int
{
return $this->size;
}
public function setMargin(int $margin): void
{
$this->margin = $margin;
}
public function getMargin(): int
{
return $this->margin;
}
public function setForegroundColor(array $foregroundColor): void
{
if (!isset($foregroundColor['a'])) {
$foregroundColor['a'] = 0;
}
foreach ($foregroundColor as &$color) {
$color = intval($color);
}
$this->foregroundColor = $foregroundColor;
}
public function getForegroundColor(): array
{
return $this->foregroundColor;
}
public function setBackgroundColor(array $backgroundColor): void
{
if (!isset($backgroundColor['a'])) {
$backgroundColor['a'] = 0;
}
foreach ($backgroundColor as &$color) {
$color = intval($color);
}
$this->backgroundColor = $backgroundColor;
}
public function getBackgroundColor(): array
{
return $this->backgroundColor;
}
public function setEncoding(string $encoding): void
{
$this->encoding = $encoding;
}
public function getEncoding(): string
{
return $this->encoding;
}
public function setRoundBlockSize(bool $roundBlockSize): void
{
$this->roundBlockSize = $roundBlockSize;
}
public function getRoundBlockSize(): bool
{
return $this->roundBlockSize;
}
public function setErrorCorrectionLevel(ErrorCorrectionLevel $errorCorrectionLevel): void
{
$this->errorCorrectionLevel = $errorCorrectionLevel;
}
public function getErrorCorrectionLevel(): ErrorCorrectionLevel
{
return $this->errorCorrectionLevel;
}
public function setLogoPath(string $logoPath): void
{
$logoPath = realpath($logoPath);
if (false === $logoPath || !is_file($logoPath)) {
throw new InvalidPathException('Invalid logo path: '.$logoPath);
}
$this->logoPath = $logoPath;
}
public function getLogoPath(): ?string
{
return $this->logoPath;
}
public function setLogoSize(int $logoWidth, int $logoHeight = null): void
{
$this->logoWidth = $logoWidth;
$this->logoHeight = $logoHeight;
}
public function setLogoWidth(int $logoWidth): void
{
$this->logoWidth = $logoWidth;
}
public function getLogoWidth(): ?int
{
return $this->logoWidth;
}
public function setLogoHeight(int $logoHeight): void
{
$this->logoHeight = $logoHeight;
}
public function getLogoHeight(): ?int
{
return $this->logoHeight;
}
public function setLabel(string $label, int $labelFontSize = null, string $labelFontPath = null, string $labelAlignment = null, array $labelMargin = null): void
{
$this->label = $label;
if (null !== $labelFontSize) {
$this->setLabelFontSize($labelFontSize);
}
if (null !== $labelFontPath) {
$this->setLabelFontPath($labelFontPath);
}
if (null !== $labelAlignment) {
$this->setLabelAlignment($labelAlignment);
}
if (null !== $labelMargin) {
$this->setLabelMargin($labelMargin);
}
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabelFontSize(int $labelFontSize): void
{
$this->labelFontSize = $labelFontSize;
}
public function getLabelFontSize(): int
{
return $this->labelFontSize;
}
public function setLabelFontPath(string $labelFontPath): void
{
$resolvedLabelFontPath = (string) realpath($labelFontPath);
if (!is_file($resolvedLabelFontPath)) {
throw new InvalidPathException('Invalid label font path: '.$labelFontPath);
}
$this->labelFontPath = $resolvedLabelFontPath;
}
public function getLabelFontPath(): string
{
return $this->labelFontPath;
}
public function setLabelAlignment(string $labelAlignment): void
{
$this->labelAlignment = new LabelAlignment($labelAlignment);
}
public function getLabelAlignment(): string
{
return $this->labelAlignment->getValue();
}
public function setLabelMargin(array $labelMargin): void
{
$this->labelMargin = array_merge($this->labelMargin, $labelMargin);
}
public function getLabelMargin(): array
{
return $this->labelMargin;
}
public function setWriterRegistry(WriterRegistryInterface $writerRegistry): void
{
$this->writerRegistry = $writerRegistry;
}
public function setWriter(WriterInterface $writer): void
{
$this->writer = $writer;
}
public function getWriter(string $name = null): WriterInterface
{
if (!is_null($name)) {
return $this->writerRegistry->getWriter($name);
}
if ($this->writer instanceof WriterInterface) {
return $this->writer;
}
return $this->writerRegistry->getDefaultWriter();
}
public function setWriterOptions(array $writerOptions): void
{
$this->writerOptions = $writerOptions;
}
public function getWriterOptions(): array
{
return $this->writerOptions;
}
private function createWriterRegistry(): void
{
$this->writerRegistry = new WriterRegistry();
$this->writerRegistry->loadDefaultWriters();
}
public function setWriterByName(string $name): void
{
$this->writer = $this->getWriter($name);
}
public function setWriterByPath(string $path): void
{
$extension = pathinfo($path, PATHINFO_EXTENSION);
$this->setWriterByExtension($extension);
}
public function setWriterByExtension(string $extension): void
{
foreach ($this->writerRegistry->getWriters() as $writer) {
if ($writer->supportsExtension($extension)) {
$this->writer = $writer;
return;
}
}
throw new UnsupportedExtensionException('Missing writer for extension "'.$extension.'"');
}
public function writeString(): string
{
return $this->getWriter()->writeString($this);
}
public function writeDataUri(): string
{
return $this->getWriter()->writeDataUri($this);
}
public function writeFile(string $path): void
{
$this->getWriter()->writeFile($this, $path);
}
public function getContentType(): string
{
return $this->getWriter()->getContentType();
}
public function setValidateResult(bool $validateResult): void
{
$this->validateResult = $validateResult;
}
public function getValidateResult(): bool
{
return $this->validateResult;
}
public function getData(): array
{
$baconErrorCorrectionLevel = $this->errorCorrectionLevel->toBaconErrorCorrectionLevel();
$baconQrCode = Encoder::encode($this->text, $baconErrorCorrectionLevel, $this->encoding);
$baconMatrix = $baconQrCode->getMatrix();
$matrix = [];
$columnCount = $baconMatrix->getWidth();
$rowCount = $baconMatrix->getHeight();
for ($rowIndex = 0; $rowIndex < $rowCount; ++$rowIndex) {
$matrix[$rowIndex] = [];
for ($columnIndex = 0; $columnIndex < $columnCount; ++$columnIndex) {
$matrix[$rowIndex][$columnIndex] = $baconMatrix->get($columnIndex, $rowIndex);
}
}
$data = ['matrix' => $matrix];
$data['block_count'] = count($matrix[0]);
$data['block_size'] = $this->size / $data['block_count'];
if ($this->roundBlockSize) {
$data['block_size'] = intval(floor($data['block_size']));
}
$data['inner_width'] = $data['block_size'] * $data['block_count'];
$data['inner_height'] = $data['block_size'] * $data['block_count'];
$data['outer_width'] = $this->size + 2 * $this->margin;
$data['outer_height'] = $this->size + 2 * $this->margin;
$data['margin_left'] = ($data['outer_width'] - $data['inner_width']) / 2;
if ($this->roundBlockSize) {
$data['margin_left'] = intval(floor($data['margin_left']));
}
$data['margin_right'] = $data['outer_width'] - $data['inner_width'] - $data['margin_left'];
return $data;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode;
interface QrCodeInterface
{
public function getText(): string;
public function getSize(): int;
public function getMargin(): int;
public function getForegroundColor(): array;
public function getBackgroundColor(): array;
public function getEncoding(): string;
public function getRoundBlockSize(): bool;
public function getErrorCorrectionLevel(): ErrorCorrectionLevel;
public function getLogoPath(): ?string;
public function getLogoWidth(): ?int;
public function getLogoHeight(): ?int;
public function getLabel(): ?string;
public function getLabelFontPath(): string;
public function getLabelFontSize(): int;
public function getLabelAlignment(): string;
public function getLabelMargin(): array;
public function getValidateResult(): bool;
public function getWriterOptions(): array;
public function getContentType(): string;
public function setWriterRegistry(WriterRegistryInterface $writerRegistry): void;
public function writeString(): string;
public function writeDataUri(): string;
public function writeFile(string $path): void;
public function getData(): array;
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Response;
use Endroid\QrCode\QrCodeInterface;
use Symfony\Component\HttpFoundation\Response;
class QrCodeResponse extends Response
{
public function __construct(QrCodeInterface $qrCode)
{
parent::__construct($qrCode->writeString(), Response::HTTP_OK, ['Content-Type' => $qrCode->getContentType()]);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Exception\InvalidLogoException;
use Endroid\QrCode\Exception\MissingExtensionException;
use Endroid\QrCode\QrCodeInterface;
abstract class AbstractWriter implements WriterInterface
{
protected function getMimeType(string $path): string
{
if (false !== filter_var($path, FILTER_VALIDATE_URL)) {
return $this->getMimeTypeFromUrl($path);
}
return $this->getMimeTypeFromPath($path);
}
private function getMimeTypeFromUrl(string $url): string
{
/** @var mixed $format */
$format = PHP_VERSION > 80000 ? true : 1;
$headers = get_headers($url, $format);
if (!is_array($headers) || !isset($headers['Content-Type'])) {
throw new InvalidLogoException(sprintf('Content type could not be determined for logo URL "%s"', $url));
}
return $headers['Content-Type'];
}
private function getMimeTypeFromPath(string $path): string
{
if (!function_exists('mime_content_type')) {
throw new MissingExtensionException('You need the ext-fileinfo extension to determine logo mime type');
}
$mimeType = mime_content_type($path);
if (!is_string($mimeType)) {
throw new InvalidLogoException('Could not determine mime type');
}
if (!preg_match('#^image/#', $mimeType)) {
throw new GenerateImageException('Logo path is not an image');
}
// Passing mime type image/svg results in invisible images
if ('image/svg' === $mimeType) {
return 'image/svg+xml';
}
return $mimeType;
}
public function writeDataUri(QrCodeInterface $qrCode): string
{
$dataUri = 'data:'.$this->getContentType().';base64,'.base64_encode($this->writeString($qrCode));
return $dataUri;
}
public function writeFile(QrCodeInterface $qrCode, string $path): void
{
$string = $this->writeString($qrCode);
file_put_contents($path, $string);
}
public static function supportsExtension(string $extension): bool
{
return in_array($extension, static::getSupportedExtensions());
}
public static function getSupportedExtensions(): array
{
return [];
}
abstract public function getName(): string;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
class BinaryWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$rows = [];
$data = $qrCode->getData();
foreach ($data['matrix'] as $row) {
$values = '';
foreach ($row as $value) {
$values .= $value;
}
$rows[] = $values;
}
return implode("\n", $rows);
}
public static function getContentType(): string
{
return 'text/plain';
}
public static function getSupportedExtensions(): array
{
return ['bin', 'txt'];
}
public function getName(): string
{
return 'binary';
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
use Exception;
use ReflectionClass;
class DebugWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$data = [];
$skip = ['getData'];
$reflectionClass = new ReflectionClass($qrCode);
foreach ($reflectionClass->getMethods() as $method) {
$methodName = $method->getShortName();
if (0 === strpos($methodName, 'get') && 0 == $method->getNumberOfParameters() && !in_array($methodName, $skip)) {
$value = $qrCode->{$methodName}();
if (is_array($value) && !is_object(current($value))) {
$value = '['.implode(', ', $value).']';
} elseif (is_bool($value)) {
$value = $value ? 'true' : 'false';
} elseif (is_string($value)) {
$value = '"'.$value.'"';
} elseif (is_null($value)) {
$value = 'null';
}
try {
$data[] = $methodName.': '.$value;
} catch (Exception $exception) {
}
}
}
$string = implode(" \n", $data);
return $string;
}
public static function getContentType(): string
{
return 'text/plain';
}
public function getName(): string
{
return 'debug';
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
class EpsWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$data = $qrCode->getData();
$epsData = [];
$epsData[] = '%!PS-Adobe-3.0 EPSF-3.0';
$epsData[] = '%%BoundingBox: 0 0 '.$data['outer_width'].' '.$data['outer_height'];
$epsData[] = '/F { rectfill } def';
$epsData[] = number_format($qrCode->getBackgroundColor()['r'] / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()['g'] / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()['b'] / 100, 2, '.', ',').' setrgbcolor';
$epsData[] = '0 0 '.$data['outer_width'].' '.$data['outer_height'].' F';
$epsData[] = number_format($qrCode->getForegroundColor()['r'] / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()['g'] / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()['b'] / 100, 2, '.', ',').' setrgbcolor';
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
$x = $data['margin_left'] + $data['block_size'] * $column;
$y = $data['margin_left'] + $data['block_size'] * $row;
$epsData[] = $x.' '.$y.' '.$data['block_size'].' '.$data['block_size'].' F';
}
}
}
return implode("\n", $epsData);
}
public static function getContentType(): string
{
return 'image/eps';
}
public static function getSupportedExtensions(): array
{
return ['eps'];
}
public function getName(): string
{
return 'eps';
}
}

View File

@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Exception\MissingFunctionException;
use Endroid\QrCode\Exception\MissingLogoHeightException;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\LabelAlignment;
use Endroid\QrCode\QrCodeInterface;
use Zxing\QrReader;
class PngWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
if (!extension_loaded('gd')) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$image = $this->createImage($qrCode->getData(), $qrCode);
$logoPath = $qrCode->getLogoPath();
if (null !== $logoPath) {
$image = $this->addLogo($image, $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight());
}
$label = $qrCode->getLabel();
if (null !== $label) {
$image = $this->addLabel($image, $label, $qrCode->getLabelFontPath(), $qrCode->getLabelFontSize(), $qrCode->getLabelAlignment(), $qrCode->getLabelMargin(), $qrCode->getForegroundColor(), $qrCode->getBackgroundColor());
}
$string = $this->imageToString($image);
if (PHP_VERSION_ID < 80000) {
imagedestroy($image);
}
if ($qrCode->getValidateResult()) {
$reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB);
if ($reader->text() !== $qrCode->getText()) {
throw new ValidationException('Built-in validation reader read "'.$reader->text().'" instead of "'.$qrCode->getText().'".
Adjust your parameters to increase readability or disable built-in validation.');
}
}
return $string;
}
/**
* @param array<mixed> $data
*
* @return mixed
*/
private function createImage(array $data, QrCodeInterface $qrCode)
{
$baseSize = $qrCode->getRoundBlockSize() ? $data['block_size'] : 25;
$baseImage = $this->createBaseImage($baseSize, $data, $qrCode);
$interpolatedImage = $this->createInterpolatedImage($baseImage, $data, $qrCode);
if (PHP_VERSION_ID < 80000) {
imagedestroy($baseImage);
}
return $interpolatedImage;
}
/**
* @param array<mixed> $data
*
* @return mixed
*/
private function createBaseImage(int $baseSize, array $data, QrCodeInterface $qrCode)
{
$image = imagecreatetruecolor($data['block_count'] * $baseSize, $data['block_count'] * $baseSize);
if (!$image) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$foregroundColor = imagecolorallocatealpha($image, $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b'], $qrCode->getForegroundColor()['a']);
if (!is_int($foregroundColor)) {
throw new GenerateImageException('Foreground color could not be allocated');
}
$backgroundColor = imagecolorallocatealpha($image, $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'], $qrCode->getBackgroundColor()['a']);
if (!is_int($backgroundColor)) {
throw new GenerateImageException('Background color could not be allocated');
}
imagefill($image, 0, 0, $backgroundColor);
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
imagefilledrectangle($image, $column * $baseSize, $row * $baseSize, intval(($column + 1) * $baseSize), intval(($row + 1) * $baseSize), $foregroundColor);
}
}
}
return $image;
}
/**
* @param mixed $baseImage
* @param array<mixed> $data
*
* @return mixed
*/
private function createInterpolatedImage($baseImage, array $data, QrCodeInterface $qrCode)
{
$image = imagecreatetruecolor($data['outer_width'], $data['outer_height']);
if (!$image) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$backgroundColor = imagecolorallocatealpha($image, $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'], $qrCode->getBackgroundColor()['a']);
if (!is_int($backgroundColor)) {
throw new GenerateImageException('Background color could not be allocated');
}
imagefill($image, 0, 0, $backgroundColor);
imagecopyresampled($image, $baseImage, (int) $data['margin_left'], (int) $data['margin_left'], 0, 0, (int) $data['inner_width'], (int) $data['inner_height'], imagesx($baseImage), imagesy($baseImage));
if ($qrCode->getBackgroundColor()['a'] > 0) {
imagesavealpha($image, true);
}
return $image;
}
/**
* @param mixed $sourceImage
*
* @return mixed
*/
private function addLogo($sourceImage, string $logoPath, int $logoWidth = null, int $logoHeight = null)
{
$mimeType = $this->getMimeType($logoPath);
$logoImage = imagecreatefromstring(strval(file_get_contents($logoPath)));
if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
}
if (!$logoImage) {
throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
}
$logoSourceWidth = imagesx($logoImage);
$logoSourceHeight = imagesy($logoImage);
if (null === $logoWidth) {
$logoWidth = $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = intval($logoSourceHeight * $aspectRatio);
}
$logoX = imagesx($sourceImage) / 2 - $logoWidth / 2;
$logoY = imagesy($sourceImage) / 2 - $logoHeight / 2;
imagecopyresampled($sourceImage, $logoImage, intval($logoX), intval($logoY), 0, 0, $logoWidth, $logoHeight, $logoSourceWidth, $logoSourceHeight);
if (PHP_VERSION_ID < 80000) {
imagedestroy($logoImage);
}
return $sourceImage;
}
/**
* @param mixed $sourceImage
* @param array<int> $labelMargin
* @param array<int> $foregroundColor
* @param array<int> $backgroundColor
*
* @return mixed
*/
private function addLabel($sourceImage, string $label, string $labelFontPath, int $labelFontSize, string $labelAlignment, array $labelMargin, array $foregroundColor, array $backgroundColor)
{
if (!function_exists('imagettfbbox')) {
throw new MissingFunctionException('Missing function "imagettfbbox", please make sure you installed the FreeType library');
}
$labelBox = imagettfbbox($labelFontSize, 0, $labelFontPath, $label);
if (!$labelBox) {
throw new GenerateImageException('Unable to add label: check your GD installation');
}
$labelBoxWidth = intval($labelBox[2] - $labelBox[0]);
$labelBoxHeight = intval($labelBox[0] - $labelBox[7]);
$sourceWidth = imagesx($sourceImage);
$sourceHeight = imagesy($sourceImage);
$targetWidth = $sourceWidth;
$targetHeight = $sourceHeight + $labelBoxHeight + $labelMargin['t'] + $labelMargin['b'];
// Create empty target image
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
if (!$targetImage) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$foregroundColor = imagecolorallocate($targetImage, $foregroundColor['r'], $foregroundColor['g'], $foregroundColor['b']);
if (!is_int($foregroundColor)) {
throw new GenerateImageException('Foreground color could not be allocated');
}
$backgroundColor = imagecolorallocate($targetImage, $backgroundColor['r'], $backgroundColor['g'], $backgroundColor['b']);
if (!is_int($backgroundColor)) {
throw new GenerateImageException('Background color could not be allocated');
}
imagefill($targetImage, 0, 0, $backgroundColor);
// Copy source image to target image
imagecopyresampled($targetImage, $sourceImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, $sourceWidth, $sourceHeight);
if (PHP_VERSION_ID < 80000) {
imagedestroy($sourceImage);
}
switch ($labelAlignment) {
case LabelAlignment::LEFT:
$labelX = $labelMargin['l'];
break;
case LabelAlignment::RIGHT:
$labelX = $targetWidth - $labelBoxWidth - $labelMargin['r'];
break;
default:
$labelX = intval($targetWidth / 2 - $labelBoxWidth / 2);
break;
}
$labelY = $targetHeight - $labelMargin['b'];
imagettftext($targetImage, $labelFontSize, 0, $labelX, $labelY, $foregroundColor, $labelFontPath, $label);
return $targetImage;
}
/**
* @param mixed $image
*/
private function imageToString($image): string
{
ob_start();
imagepng($image);
return (string) ob_get_clean();
}
public static function getContentType(): string
{
return 'image/png';
}
public static function getSupportedExtensions(): array
{
return ['png'];
}
public function getName(): string
{
return 'png';
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Exception\MissingLogoHeightException;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\QrCodeInterface;
use SimpleXMLElement;
class SvgWriter extends AbstractWriter
{
public function writeString(QrCodeInterface $qrCode): string
{
$options = $qrCode->getWriterOptions();
if ($qrCode->getValidateResult()) {
throw new ValidationException('Built-in validation reader can not check SVG images: please disable via setValidateResult(false)');
}
$data = $qrCode->getData();
$svg = new SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>');
$svg->addAttribute('version', '1.1');
$svg->addAttribute('width', $data['outer_width'].'px');
$svg->addAttribute('height', $data['outer_height'].'px');
$svg->addAttribute('viewBox', '0 0 '.$data['outer_width'].' '.$data['outer_height']);
$svg->addChild('defs');
// Block definition
$block_id = isset($options['rect_id']) && $options['rect_id'] ? $options['rect_id'] : 'block';
$blockDefinition = $svg->defs->addChild('rect');
$blockDefinition->addAttribute('id', $block_id);
$blockDefinition->addAttribute('width', strval($data['block_size']));
$blockDefinition->addAttribute('height', strval($data['block_size']));
$blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b']));
$blockDefinition->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getForegroundColor()['a'])));
// Background
$background = $svg->addChild('rect');
$background->addAttribute('x', '0');
$background->addAttribute('y', '0');
$background->addAttribute('width', strval($data['outer_width']));
$background->addAttribute('height', strval($data['outer_height']));
$background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b']));
$background->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getBackgroundColor()['a'])));
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
$block = $svg->addChild('use');
$block->addAttribute('x', strval($data['margin_left'] + $data['block_size'] * $column));
$block->addAttribute('y', strval($data['margin_left'] + $data['block_size'] * $row));
$block->addAttribute('xlink:href', '#'.$block_id, 'http://www.w3.org/1999/xlink');
}
}
}
$logoPath = $qrCode->getLogoPath();
if (is_string($logoPath)) {
$forceXlinkHref = false;
if (isset($options['force_xlink_href']) && $options['force_xlink_href']) {
$forceXlinkHref = true;
}
$this->addLogo($svg, $data['outer_width'], $data['outer_height'], $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight(), $forceXlinkHref);
}
$xml = $svg->asXML();
if (!is_string($xml)) {
throw new GenerateImageException('Unable to save SVG XML');
}
if (isset($options['exclude_xml_declaration']) && $options['exclude_xml_declaration']) {
$xml = str_replace("<?xml version=\"1.0\"?>\n", '', $xml);
}
return $xml;
}
private function addLogo(SimpleXMLElement $svg, int $imageWidth, int $imageHeight, string $logoPath, int $logoWidth = null, int $logoHeight = null, bool $forceXlinkHref = false): void
{
$mimeType = $this->getMimeType($logoPath);
$imageData = file_get_contents($logoPath);
if (!is_string($imageData)) {
throw new GenerateImageException('Unable to read image data: check your logo path');
}
if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
}
if (null === $logoHeight || null === $logoWidth) {
$logoImage = imagecreatefromstring(strval($imageData));
if (!$logoImage) {
throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
}
/** @var mixed $logoImage */
$logoSourceWidth = imagesx($logoImage);
$logoSourceHeight = imagesy($logoImage);
if (PHP_VERSION_ID < 80000) {
imagedestroy($logoImage);
}
if (null === $logoWidth) {
$logoWidth = $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = intval($logoSourceHeight * $aspectRatio);
}
}
$logoX = $imageWidth / 2 - $logoWidth / 2;
$logoY = $imageHeight / 2 - $logoHeight / 2;
$imageDefinition = $svg->addChild('image');
$imageDefinition->addAttribute('x', strval($logoX));
$imageDefinition->addAttribute('y', strval($logoY));
$imageDefinition->addAttribute('width', strval($logoWidth));
$imageDefinition->addAttribute('height', strval($logoHeight));
$imageDefinition->addAttribute('preserveAspectRatio', 'none');
// xlink:href is actually deprecated, but still required when placing the qr code in a pdf.
// SimpleXML strips out the xlink part by using addAttribute(), so it must be set directly.
if ($forceXlinkHref) {
$imageDefinition['xlink:href'] = 'data:'.$mimeType.';base64,'.base64_encode($imageData);
} else {
$imageDefinition->addAttribute('href', 'data:'.$mimeType.';base64,'.base64_encode($imageData));
}
}
private function getOpacity(int $alpha): float
{
$opacity = 1 - $alpha / 127;
return $opacity;
}
public static function getContentType(): string
{
return 'image/svg+xml';
}
public static function getSupportedExtensions(): array
{
return ['svg'];
}
public function getName(): string
{
return 'svg';
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\QrCodeInterface;
interface WriterInterface
{
public function writeString(QrCodeInterface $qrCode): string;
public function writeDataUri(QrCodeInterface $qrCode): string;
public function writeFile(QrCodeInterface $qrCode, string $path): void;
public static function getContentType(): string;
public static function supportsExtension(string $extension): bool;
public static function getSupportedExtensions(): array;
public function getName(): string;
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode;
use Endroid\QrCode\Exception\InvalidWriterException;
use Endroid\QrCode\Writer\BinaryWriter;
use Endroid\QrCode\Writer\DebugWriter;
use Endroid\QrCode\Writer\EpsWriter;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\SvgWriter;
use Endroid\QrCode\Writer\WriterInterface;
class WriterRegistry implements WriterRegistryInterface
{
/** @var WriterInterface[] */
private $writers = [];
/** @var WriterInterface|null */
private $defaultWriter;
public function loadDefaultWriters(): void
{
if (count($this->writers) > 0) {
return;
}
$this->addWriters([
new BinaryWriter(),
new DebugWriter(),
new EpsWriter(),
new PngWriter(),
new SvgWriter(),
]);
$this->setDefaultWriter('png');
}
public function addWriters(iterable $writers): void
{
foreach ($writers as $writer) {
$this->addWriter($writer);
}
}
public function addWriter(WriterInterface $writer): void
{
$this->writers[$writer->getName()] = $writer;
}
public function getWriter(string $name): WriterInterface
{
$this->assertValidWriter($name);
return $this->writers[$name];
}
public function getDefaultWriter(): WriterInterface
{
if ($this->defaultWriter instanceof WriterInterface) {
return $this->defaultWriter;
}
throw new InvalidWriterException('Please set the default writer via the second argument of addWriter');
}
public function setDefaultWriter(string $name): void
{
$this->defaultWriter = $this->writers[$name];
}
public function getWriters(): array
{
return $this->writers;
}
private function assertValidWriter(string $name): void
{
if (!isset($this->writers[$name])) {
throw new InvalidWriterException('Invalid writer "'.$name.'"');
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode;
use Endroid\QrCode\Writer\WriterInterface;
interface WriterRegistryInterface
{
public function addWriters(iterable $writers): void;
public function addWriter(WriterInterface $writer): void;
public function getWriter(string $name): WriterInterface;
public function getDefaultWriter(): WriterInterface;
public function getWriters(): array;
}