v1.0 Initial commit of project

This commit is contained in:
2026-01-01 10:54:18 +01:00
commit 768cf78b57
990 changed files with 241213 additions and 0 deletions

View File

@@ -0,0 +1 @@
github: endroid

View File

@@ -0,0 +1,68 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.0', '8.1']
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: gd, mbstring, pcov, zip
ini-values: max_execution_time=600, memory_limit=-1
tools: composer:v2
coverage: pcov
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get composer cache directory
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install Composer dependencies
run: composer install --prefer-dist
- name: Check code quality
run: vendor/bin/code-quality
- name: Test against highest versions
run: |
vendor/bin/unit-test
vendor/bin/functional-test
- name: Test against lowest versions
run: |
composer update --prefer-lowest
vendor/bin/unit-test
vendor/bin/functional-test ^3.4
- name: Archive logs
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: logs
path: vendor/endroid/quality/application/var/log
- name: Archive code coverage results
uses: actions/upload-artifact@v2
with:
name: coverage
path: tests/coverage

4
vendor/endroid/qr-code/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/composer
/composer.lock
/tests/coverage/
/vendor/

19
vendor/endroid/qr-code/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright 2020 (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.

130
vendor/endroid/qr-code/README.md vendored Normal file
View File

@@ -0,0 +1,130 @@
# QR Code
*By [endroid](https://endroid.nl/)*
[![Latest Stable Version](http://img.shields.io/packagist/v/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code)
[![Build Status](https://github.com/endroid/qr-code/workflows/CI/badge.svg)](https://github.com/endroid/qr-code/actions)
[![Total Downloads](http://img.shields.io/packagist/dt/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code)
[![Monthly Downloads](http://img.shields.io/packagist/dm/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code)
[![License](http://img.shields.io/packagist/l/endroid/qr-code.svg)](https://packagist.org/packages/endroid/qr-code)
This library helps you generate QR codes in a jiffy. Makes use of [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode)
to generate the matrix and [khanamiryan/qrcode-detector-decoder](https://github.com/khanamiryan/php-qrcode-detector-decoder)
for validating generated QR codes. Further extended with Twig extensions, generation routes, a factory and a
Symfony bundle for easy installation and configuration.
Different writers are provided to generate the QR code as PNG, SVG, EPS, PDF or in binary format.
## Installation
Use [Composer](https://getcomposer.org/) to install the library.
``` bash
$ composer require endroid/qr-code
```
## Basic usage
```php
use Endroid\QrCode\QrCode;
$qrCode = new QrCode('Life is too short to be generating QR codes');
header('Content-Type: '.$qrCode->getContentType());
echo $qrCode->writeString();
```
## Advanced usage
```php
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\LabelAlignment;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Response\QrCodeResponse;
// Create a basic QR code
$qrCode = new QrCode('Life is too short to be generating QR codes');
$qrCode->setSize(300);
$qrCode->setMargin(10);
// Set advanced options
$qrCode->setWriterByName('png');
$qrCode->setEncoding('UTF-8');
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH());
$qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]);
$qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]);
$qrCode->setLabel('Scan the code', 16, __DIR__.'/../assets/fonts/noto_sans.otf', LabelAlignment::CENTER());
$qrCode->setLogoPath(__DIR__.'/../assets/images/symfony.png');
$qrCode->setLogoSize(150, 200);
$qrCode->setValidateResult(false);
// Round block sizes to improve readability and make the blocks sharper in pixel based outputs (like png).
// There are three approaches:
$qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_MARGIN); // The size of the qr code is shrinked, if necessary, but the size of the final image remains unchanged due to additional margin being added (default)
$qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE); // The size of the qr code and the final image is enlarged, if necessary
$qrCode->setRoundBlockSize(true, QrCode::ROUND_BLOCK_SIZE_MODE_SHRINK); // The size of the qr code and the final image is shrinked, if necessary
// Set additional writer options (SvgWriter example)
$qrCode->setWriterOptions(['exclude_xml_declaration' => true]);
// Directly output the QR code
header('Content-Type: '.$qrCode->getContentType());
echo $qrCode->writeString();
// Save it to a file
$qrCode->writeFile(__DIR__.'/qrcode.png');
// Generate a data URI to include image data inline (i.e. inside an <img> tag)
$dataUri = $qrCode->writeDataUri();
```
![QR Code](https://endroid.nl/qr-code/Life%20is%20too%20short%20to%20be%20generating%20QR%20codes.png)
### Encoding
You can pick one of these values for encoding:
`ISO-8859-1`, `ISO-8859-2`, `ISO-8859-3`, `ISO-8859-4`, `ISO-8859-5`, `ISO-8859-6`, `ISO-8859-7`, `ISO-8859-8`, `ISO-8859-9`, `ISO-8859-10`, `ISO-8859-11`, `ISO-8859-12`, `ISO-8859-13`, `ISO-8859-14`, `ISO-8859-15`, `ISO-8859-16`, `Shift_JIS`, `windows-1250`, `windows-1251`, `windows-1252`, `windows-1256`, `UTF-16BE`, `UTF-8`, `US-ASCII`, `GBK` `EUC-KR`
If you use a barcode scanner you can have some troubles while reading the generated QR codes. Depending on the encoding you chose you will have an extra amount of data corresponding to the ECI block. Some barcode scanner are not programmed to interpret this block of information. For exemple the ECI block for `UTF-8` is `000026` so the above exemple will produce : `\000026Life is too short to be generating QR codes`. To ensure a maximum compatibility you can use the `ISO-8859-1` encoding that is the default encoding used by barcode scanners.
## Readability
The readability of a QR code is primarily determined by the size, the input
length, the error correction level and any possible logo over the image so you
can tweak these parameters if you are looking for optimal results. You can also
check $qrCode->getRoundBlockSize() value to see if block dimensions are rounded
so that the image is more sharp and readable. Please note that rounding block
size can result in additional padding to compensate for the rounding difference.
## Built-in validation reader
You can enable the built-in validation reader (disabled by default) by calling
setValidateResult(true). This validation reader does not guarantee that the QR
code will be readable by all readers but it helps you provide a minimum level
of quality.
Take note that the validator can consume quite amount of additional resources.
## Symfony integration
The [endroid/qr-code-bundle](https://github.com/endroid/qr-code-bundle)
integrates the QR code library in Symfony for an even better experience.
* Configure your defaults (like image size, default writer etc.)
* Generate QR codes quickly from anywhere via the factory service
* Generate QR codes directly by typing an URL like /qr-code/\<text>.png?size=300
* Generate QR codes or URLs directly from Twig using dedicated functions
Read the [bundle documentation](https://github.com/endroid/qr-code-bundle)
for more information.
## Versioning
Version numbers follow the MAJOR.MINOR.PATCH scheme. Backwards compatibility
breaking changes will be kept to a minimum but be aware that these can occur.
Lock your dependencies for production and test your code when upgrading.
## License
This bundle is under the MIT license. For the full copyright and license
information please view the LICENSE file that was distributed with this source code.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,42 @@
<?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()
*
* @extends Enum<string>
* @psalm-immutable
*/
class ErrorCorrectionLevel extends Enum
{
const LOW = 'low';
const MEDIUM = 'medium';
const QUARTILE = 'quartile';
const HIGH = 'high';
/**
* @psalm-suppress ImpureMethodCall
*/
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 InvalidFontException 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 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,112 @@
<?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;
/** @var array<string, mixed> */
private $defaultOptions;
/** @var array<int, string> */
private $definedOptions = [
'writer',
'writer_options',
'size',
'margin',
'foreground_color',
'background_color',
'encoding',
'round_block_size',
'round_block_size_mode',
'error_correction_level',
'logo_path',
'logo_width',
'logo_height',
'label',
'label_font_size',
'label_font_path',
'label_alignment',
'label_margin',
'validate_result',
];
/** @param array<string, mixed> $defaultOptions */
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,20 @@
<?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
{
/** @param array<string, mixed> $options */
public function create(string $text = '', array $options = []): QrCodeInterface;
}

View File

@@ -0,0 +1,29 @@
<?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()
*
* @extends Enum<string>
* @psalm-immutable
*/
class LabelAlignment extends Enum
{
const LEFT = 'left';
const CENTER = 'center';
const RIGHT = 'right';
}

478
vendor/endroid/qr-code/src/QrCode.php vendored Normal file
View File

@@ -0,0 +1,478 @@
<?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\InvalidFontException;
use Endroid\QrCode\Exception\UnsupportedExtensionException;
use Endroid\QrCode\Exception\ValidationException;
use Endroid\QrCode\Writer\WriterInterface;
class QrCode implements QrCodeInterface
{
const LABEL_FONT_PATH_DEFAULT = __DIR__.'/../assets/fonts/noto_sans.otf';
const ROUND_BLOCK_SIZE_MODE_MARGIN = 'margin';
const ROUND_BLOCK_SIZE_MODE_SHRINK = 'shrink';
const ROUND_BLOCK_SIZE_MODE_ENLARGE = 'enlarge';
private $text;
/** @var int */
private $size = 300;
/** @var int */
private $margin = 10;
/** @var array<int> */
private $foregroundColor = [
'r' => 0,
'g' => 0,
'b' => 0,
'a' => 0,
];
/** @var array<int> */
private $backgroundColor = [
'r' => 255,
'g' => 255,
'b' => 255,
'a' => 0,
];
/** @var string */
private $encoding = 'UTF-8';
/** @var bool */
private $roundBlockSize = true;
/** @var string */
private $roundBlockSizeMode = self::ROUND_BLOCK_SIZE_MODE_MARGIN;
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<string, int> */
private $labelMargin = [
't' => 0,
'r' => 10,
'b' => 10,
'l' => 10,
];
/** @var WriterRegistryInterface */
private $writerRegistry;
/** @var WriterInterface|null */
private $writer;
/** @var array<mixed> */
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;
}
/** @param array<int> $foregroundColor */
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;
}
/** @param array<int> $backgroundColor */
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, string $roundBlockSizeMode = self::ROUND_BLOCK_SIZE_MODE_MARGIN): void
{
$this->roundBlockSize = $roundBlockSize;
$this->setRoundBlockSizeMode($roundBlockSizeMode);
}
public function getRoundBlockSize(): bool
{
return $this->roundBlockSize;
}
public function setRoundBlockSizeMode(string $roundBlockSizeMode): void
{
if (!in_array($roundBlockSizeMode, [
self::ROUND_BLOCK_SIZE_MODE_ENLARGE,
self::ROUND_BLOCK_SIZE_MODE_MARGIN,
self::ROUND_BLOCK_SIZE_MODE_SHRINK,
])) {
throw new ValidationException('Invalid round block size mode: '.$roundBlockSizeMode);
}
$this->roundBlockSizeMode = $roundBlockSizeMode;
}
public function setErrorCorrectionLevel(ErrorCorrectionLevel $errorCorrectionLevel): void
{
$this->errorCorrectionLevel = $errorCorrectionLevel;
}
public function getErrorCorrectionLevel(): ErrorCorrectionLevel
{
return $this->errorCorrectionLevel;
}
public function setLogoPath(string $logoPath): void
{
$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;
}
/** @param array<string, int> $labelMargin */
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 InvalidFontException('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();
}
/** @param array<string, int> $labelMargin */
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();
}
/** @param array<string, mixed> $writerOptions */
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) {
switch ($this->roundBlockSizeMode) {
case self::ROUND_BLOCK_SIZE_MODE_ENLARGE:
$data['block_size'] = intval(ceil($data['block_size']));
$this->size = $data['block_size'] * $data['block_count'];
break;
case self::ROUND_BLOCK_SIZE_MODE_SHRINK:
$data['block_size'] = intval(floor($data['block_size']));
$this->size = $data['block_size'] * $data['block_count'];
break;
case self::ROUND_BLOCK_SIZE_MODE_MARGIN:
default:
$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,68 @@
<?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;
/** @return array<int> */
public function getForegroundColor(): array;
/** @return array<int> */
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;
/** @return array<int> */
public function getLabelMargin(): array;
public function getValidateResult(): bool;
/** @return array<mixed> */
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;
/** @return array<mixed> */
public function getData(): array;
}

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,59 @@
<?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';
// Please note an EPS has a reversed Y axis compared to PNG and SVG
$data['matrix'] = array_reverse($data['matrix']);
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,131 @@
<?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\ValidationException;
use Endroid\QrCode\QrCodeInterface;
class FpdfWriter extends AbstractWriter
{
/**
* Defines as which unit the size is handled. Default is: "mm".
*
* Allowed values: 'mm', 'pt', 'cm', 'in'
*/
public const WRITER_OPTION_MEASURE_UNIT = 'fpdf_measure_unit';
public function writeString(QrCodeInterface $qrCode): string
{
if (!\class_exists(\FPDF::class)) {
throw new \BadMethodCallException('The Fpdf writer requires FPDF as dependency but the class "\\FPDF" couldn\'t be found.');
}
if ($qrCode->getValidateResult()) {
throw new ValidationException('Built-in validation reader can not check fpdf qr codes: please disable via setValidateResult(false)');
}
$foregroundColor = $qrCode->getForegroundColor();
if (0 !== $foregroundColor['a']) {
throw new \InvalidArgumentException('The foreground color has an alpha channel, but the fpdf qr writer doesn\'t support alpha channels.');
}
$backgroundColor = $qrCode->getBackgroundColor();
if (0 !== $backgroundColor['a']) {
throw new \InvalidArgumentException('The foreground color has an alpha channel, but the fpdf qr writer doesn\'t support alpha channels.');
}
$label = $qrCode->getLabel();
$labelHeight = null !== $label ? 30 : 0;
$data = $qrCode->getData();
$options = $qrCode->getWriterOptions();
$fpdf = new \FPDF(
'P',
$options[self::WRITER_OPTION_MEASURE_UNIT] ?? 'mm',
[$data['outer_width'], $data['outer_height'] + $labelHeight]
);
$fpdf->AddPage();
$fpdf->SetFillColor($backgroundColor['r'], $backgroundColor['g'], $backgroundColor['b']);
$fpdf->Rect(0, 0, $data['outer_width'], $data['outer_height'], 'F');
$fpdf->SetFillColor($foregroundColor['r'], $foregroundColor['g'], $foregroundColor['b']);
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
$fpdf->Rect(
$data['margin_left'] + ($column * $data['block_size']),
$data['margin_left'] + ($row * $data['block_size']),
$data['block_size'],
$data['block_size'],
'F'
);
}
}
}
$logoPath = $qrCode->getLogoPath();
if (null !== $logoPath) {
$this->addLogo(
$fpdf,
$logoPath,
$qrCode->getLogoWidth(),
$qrCode->getLogoHeight(),
$data['outer_width'],
$data['outer_height']
);
}
if (null !== $label) {
$fpdf->setY($data['outer_height'] + 5);
$fpdf->SetFont('Helvetica', null, $qrCode->getLabelFontSize());
$fpdf->Cell(0, 0, $label, 0, 0, strtoupper($qrCode->getLabelAlignment()[0]));
}
return $fpdf->Output('S');
}
protected function addLogo(\FPDF $fpdf, string $logoPath, ?int $logoWidth, ?int $logoHeight, int $imageWidth, int $imageHeight): void
{
if (null === $logoHeight || null === $logoWidth) {
[$logoSourceWidth, $logoSourceHeight] = \getimagesize($logoPath);
if (null === $logoWidth) {
$logoWidth = (int) $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = (int) ($logoSourceHeight * $aspectRatio);
}
}
$logoX = $imageWidth / 2 - (int) $logoWidth / 2;
$logoY = $imageHeight / 2 - (int) $logoHeight / 2;
$fpdf->Image($logoPath, $logoX, $logoY, $logoWidth, $logoHeight);
}
public static function getContentType(): string
{
return 'application/pdf';
}
public static function getSupportedExtensions(): array
{
return ['pdf'];
}
public function getName(): string
{
return 'fpdf';
}
}

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,32 @@
<?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;
/** @return array<string> */
public static function getSupportedExtensions(): array;
public function getName(): string;
}

View File

@@ -0,0 +1,93 @@
<?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\FpdfWriter;
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(),
new FpdfWriter(),
]);
$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,29 @@
<?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
{
/** @param WriterInterface[] $writers */
public function addWriters(iterable $writers): void;
public function addWriter(WriterInterface $writer): void;
public function getWriter(string $name): WriterInterface;
public function getDefaultWriter(): WriterInterface;
/** @return WriterInterface[] */
public function getWriters(): array;
}

View File

@@ -0,0 +1,296 @@
<?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\Tests;
use Endroid\QrCode\Exception\GenerateImageException;
use Endroid\QrCode\Factory\QrCodeFactory;
use Endroid\QrCode\QrCode;
use PHPUnit\Framework\TestCase;
use Zxing\QrReader;
class QrCodeTest extends TestCase
{
/**
* @dataProvider stringProvider
* @testdox QR code created with text $text is readable
*/
public function testReadable(string $text): void
{
$qrCode = new QrCode();
$qrCode->setSize(300);
$qrCode->setText($text);
$pngData = $qrCode->writeString();
$this->assertTrue(is_string($pngData));
$reader = new QrReader($pngData, QrReader::SOURCE_TYPE_BLOB);
$this->assertEquals($text, $reader->text());
}
public function stringProvider(): array
{
return [
['Tiny'],
['This one has spaces'],
['d2llMS9uU01BVmlvalM2YU9BUFBPTTdQMmJabHpqdndt'],
['http://this.is.an/url?with=query&string=attached'],
['11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'],
['{"i":"serialized.data","v":1,"t":1,"d":"4AEPc9XuIQ0OjsZoSRWp9DRWlN6UyDvuMlyOYy8XjOw="}'],
['Spëci&al ch@ract3rs'],
['有限公司'],
];
}
/**
* @dataProvider writerNameProvider
* @testdox Writer set by name $writerName results in the correct data type
*/
public function testWriteQrCodeByWriterName(string $writerName, ?string $fileContent): void
{
$qrCode = new QrCode('QR Code');
$qrCode->setLogoPath(__DIR__.'/../assets/images/symfony.png');
$qrCode->setLogoWidth(100);
$qrCode->setWriterByName($writerName);
$data = $qrCode->writeString();
$this->assertTrue(is_string($data));
if (null !== $fileContent) {
$uriData = $qrCode->writeDataUri();
$this->assertTrue(0 === strpos($uriData, $fileContent));
}
}
public function writerNameProvider(): array
{
return [
['binary', null],
['debug', null],
['eps', null],
['png', 'data:image/png;base64'],
['svg', 'data:image/svg+xml;base64'],
];
}
/**
* @dataProvider extensionsProvider
* @testdox Writer set by extension $extension results in the correct data type
*/
public function testWriteQrCodeByWriterExtension(string $extension, ?string $fileContent): void
{
$qrCode = new QrCode('QR Code');
$qrCode->setLogoPath(__DIR__.'/../assets/images/symfony.png');
$qrCode->setLogoWidth(100);
$qrCode->setWriterByExtension($extension);
$data = $qrCode->writeString();
$this->assertTrue(is_string($data));
if (null !== $fileContent) {
$uriData = $qrCode->writeDataUri();
$this->assertTrue(0 === strpos($uriData, $fileContent));
}
}
public function extensionsProvider(): array
{
return [
['bin', null],
['txt', null],
['eps', null],
['png', 'data:image/png;base64'],
['svg', 'data:image/svg+xml;base64'],
];
}
/**
* @testdox Factory creates a valid QR code
*/
public function testFactory(): void
{
$qrCodeFactory = new QrCodeFactory();
$qrCode = $qrCodeFactory->create('QR Code', [
'writer' => 'png',
'size' => 300,
'margin' => 10,
'round_block_size_mode' => 'shrink',
]);
$pngData = $qrCode->writeString();
$this->assertTrue(is_string($pngData));
$reader = new QrReader($pngData, QrReader::SOURCE_TYPE_BLOB);
$this->assertEquals('QR Code', $reader->text());
}
/**
* @testdox Size and margin are handled correctly
*/
public function testSetSize(): void
{
$size = 400;
$margin = 10;
$qrCode = new QrCode('QR Code');
$qrCode->setSize($size);
$qrCode->setMargin($margin);
$pngData = $qrCode->writeString();
$image = imagecreatefromstring($pngData);
$this->assertTrue(imagesx($image) === $size + 2 * $margin);
$this->assertTrue(imagesy($image) === $size + 2 * $margin);
}
/**
* @testdox Size and margin are handled correctly with rounded blocks
* @dataProvider roundedSizeProvider
*/
public function testSetSizeRounded($size, $margin, $round, $mode, $expectedSize): void
{
$qrCode = new QrCode('QR Code contents with some length to have some data');
$qrCode->setRoundBlockSize($round, $mode);
$qrCode->setSize($size);
$qrCode->setMargin($margin);
$pngData = $qrCode->writeString();
$image = imagecreatefromstring($pngData);
$this->assertTrue(imagesx($image) === $expectedSize);
$this->assertTrue(imagesy($image) === $expectedSize);
}
public function roundedSizeProvider()
{
return [
[
'size' => 400,
'margin' => 0,
'round' => true,
'mode' => QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE,
'expectedSize' => 406,
],
[
'size' => 400,
'margin' => 5,
'round' => true,
'mode' => QrCode::ROUND_BLOCK_SIZE_MODE_ENLARGE,
'expectedSize' => 416,
],
[
'size' => 400,
'margin' => 0,
'round' => true,
'mode' => QrCode::ROUND_BLOCK_SIZE_MODE_MARGIN,
'expectedSize' => 400,
],
[
'size' => 400,
'margin' => 5,
'round' => true,
'mode' => QrCode::ROUND_BLOCK_SIZE_MODE_MARGIN,
'expectedSize' => 410,
],
[
'size' => 400,
'margin' => 0,
'round' => true,
'mode' => QrCode::ROUND_BLOCK_SIZE_MODE_SHRINK,
'expectedSize' => 377,
],
[
'size' => 400,
'margin' => 5,
'round' => true,
'mode' => QrCode::ROUND_BLOCK_SIZE_MODE_SHRINK,
'expectedSize' => 387,
],
];
}
/**
* @testdox Label can be added and QR code is still readable
*/
public function testSetLabel(): void
{
$qrCode = new QrCode('QR Code');
$qrCode->setSize(300);
$qrCode->setLabel('Scan the code', 15);
$pngData = $qrCode->writeString();
$this->assertTrue(is_string($pngData));
$reader = new QrReader($pngData, QrReader::SOURCE_TYPE_BLOB);
$this->assertEquals('QR Code', $reader->text());
}
/**
* @testdox Logo can be added and QR code is still readable
*/
public function testSetLogo(): void
{
$qrCode = new QrCode('QR Code');
$qrCode->setSize(500);
$qrCode->setLogoPath(__DIR__.'/../assets/images/symfony.png');
$qrCode->setLogoWidth(100);
$qrCode->setValidateResult(true);
$pngData = $qrCode->writeString();
$this->assertTrue(is_string($pngData));
}
/**
* @testdox Resulting QR code can be written to file
*/
public function testWriteFile(): void
{
$filename = __DIR__.'/output/qr-code.png';
$qrCode = new QrCode('QR Code');
$qrCode->writeFile($filename);
$image = imagecreatefromstring(file_get_contents($filename));
$this->assertTrue(false !== $image);
imagedestroy($image);
}
/**
* @testdox QR code data can be retrieved
*/
public function testData(): void
{
$qrCode = new QrCode('QR Code');
$data = $qrCode->getData();
$this->assertArrayHasKey('block_count', $data);
$this->assertArrayHasKey('block_size', $data);
$this->assertArrayHasKey('inner_width', $data);
$this->assertArrayHasKey('inner_height', $data);
$this->assertArrayHasKey('outer_width', $data);
$this->assertArrayHasKey('outer_height', $data);
$this->assertArrayHasKey('margin_left', $data);
$this->assertArrayHasKey('margin_right', $data);
}
/**
* @testdox Invalid image data results in appropriate exception
*/
public function testNonImageData(): void
{
$qrCode = new QrCode('QR Code');
$qrCode->setLogoPath(__DIR__.'/QrCodeTest.php');
$qrCode->setLogoSize(200, 200);
$qrCode->setWriterByExtension('svg');
$this->expectException(GenerateImageException::class);
$qrCode->writeString();
}
}

View File

@@ -0,0 +1,2 @@
/*
!/.gitignore