OTP : set double authentication : create, send link, usable with Google Authenticator and FreeOTP

This commit is contained in:
sparkyx 2025-08-03 16:45:50 +02:00
parent 49f7a2f39c
commit bf503299c3
2720 changed files with 542245 additions and 0 deletions

10
composer.json Normal file
View file

@ -0,0 +1,10 @@
{
"require": {
"horstoeko/zugferd": "^1.0",
"zetacomponents/mail": "^1.10",
"num-num/ubl-invoice": "^1.21",
"php-curl-class/php-curl-class": "^12.0",
"chillerlan/php-authenticator": "^5.2",
"endroid/qr-code": "^6.0"
}
}

2290
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
<?php
/**
* Autogenerated file
* This file is part of NOALYSS.
*
* NOALYSS is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* NOALYSS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOALYSS; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
require_once NOALYSS_INCLUDE . '/lib/data_sql.class.php';
require_once NOALYSS_INCLUDE . '/class/database.class.php';
/**
* @file
* @brief abstract of the table public.otp_send_secret
*/
/**
* @class Otp_Send_Secret_SQL
* @brief abstract of the table public.otp_send_secret
*/
class Otp_Send_Secret_SQL extends \Table_Data_SQL {
function __construct(Database $p_cn, $p_id = -1) {
$this->table = "public.otp_send_secret";
$this->primary_key = "os_id";
/*
* List of columns
*/
$this->name = array(
"os_id" => "os_id"
, "use_email" => "use_email"
, "os_timestamp" => "os_timestamp"
, "os_request" => "os_request"
);
/*
* Type of columns
*/
$this->type = array(
"os_id" => "numeric"
, "use_email" => "text"
, "os_timestamp" => "date"
, "os_request" => "text"
);
$this->default = array("os_id" => "auto");
$this->date_format = "DD.MM.YYYY HH24:MI:SS";
parent::__construct($p_cn, $p_id);
}
}

103
include/lib/otp.class.php Normal file
View file

@ -0,0 +1,103 @@
<?php
namespace Noalyss;
/*
* This file is part of NOALYSS.
*
* NOALYSS is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* NOALYSS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOALYSS; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
// Copyright Author Dany De Bontridder danydb@aevalys.eu 22/10/23
/**
* @file
* @brief class using OTP
*
*/
/**
* @brief generate OTP
*/
use chillerlan\Authenticator\{
Authenticator,
AuthenticatorOptions
};
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
class OTP {
/**
* @brief build a secret key and returns it
* @return string random string of 32
*/
function build_secret() {
$options = new AuthenticatorOptions;
$options->secret_length = 32;
$options->algorithm = AuthenticatorInterface::ALGO_SHA512;
$options->digits=6;
$authenticator = new Authenticator($options);
// create a secret (stored somewhere in a *safe* place on the server. safe... hahaha jk)
$secret = $authenticator->createSecret();
return $secret;
}
/**
* @brief send an email with link to the user
* @param $user (\Noalyss_User )
*/
function send_mail(\Noalyss_User $user) {
$mail=new \Sendmail();
$mail->set_from(ADMIN_WEB);
$mail->mailto($user->getEmail());
$mail->set_subject(_("NOALYSS : Double authentification lien pour freeOTP"));
$noalyss_url=NOALYSS_URL;
$uuid=guidv4();
$id=$user->getId();
/**
* save in DB first
*/
$message="Bonjour,
Afin de pouvoir utiliser la double authentification avec freeOTP, pourriez-vous
suivre ce lien et scanner le QRCode avec votre application android freeOTP.
Ce lien ne sera actif que 24 heures.
{$noalyss_url}/index.php?otp={$uuid}
Merci d'utiliser NOALYSS
Cordialement,
Noalyss team
";
try {
$repository=new \Database();
$otp_send_secret_sql=new \Otp_Send_Secret_SQL($repository);
$otp_send_secret_sql->set('use_id',$id)
->set('os_request',$uuid);
$otp_send_secret_sql->save();
$mail->set_message($message);
$mail->compose();
$mail->send();
} catch (Exception $ex) {
}
}
}

116
include/otp-link.php Normal file
View file

@ -0,0 +1,116 @@
<?php
/*
* This file is part of NOALYSS.
*
* NOALYSS is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* NOALYSS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOALYSS; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
// Copyright Author Dany De Bontridder danydb@aevalys.eu 22/10/23
/**
* @file
* @brief generate a QRCode for setting up freeOTP
*
*
*/
if (!file_exists($dirname . '/config.inc.php')) {
die("system not installed");
}
require_once __DIR__ . '/constant.php';
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\RoundBlockSizeMode;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\ValidationException;
use chillerlan\Authenticator\{
Authenticator,
AuthenticatorOptions
};
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
$http = new HttpInput();
try {
$uuid = $http->get('otp');
$repository = new \Database(0);
// remove old request (> 24 hours)
$repository->exec_sql("delete from otp_send_secret where os_timestamp < now()-interval '24 hours'");
// check if UUID exist
$id = $repository->get_value("select os_id from otp_send_secret where os_request=$1",
[$uuid]);
// if UUID doesn't exist exit
if ($repository->count() == 0) {
return;
}
// get email from id
$otp_send = new Otp_Send_Secret_SQL($repository, $id);
$user = new Noalyss_User($repository, $otp_send->get('use_id'));
$secret = $user->get_otp_secret();
// OTP
$options = new AuthenticatorOptions;
$options->secret_length = 32;
$options->algorithm = AuthenticatorInterface::ALGO_SHA512;
$options->digits=6;
$authenticator = new Authenticator($options);
$authenticator->setSecret($secret);
$data= $authenticator->getUri(label:"noalyss:".$user->getEmail(),issuer:"noalyss.eu");
// load secret for this id
//echo "use with php -S localhost:5000 puis ouvrir index.html ";
$writer = new PngWriter();
// Create QR code
$qrCode = new QrCode(
data: $data,
encoding: new Encoding('UTF-8'),
errorCorrectionLevel: ErrorCorrectionLevel::Medium,
size: 600,
margin: 10,
roundBlockSizeMode: RoundBlockSizeMode::Margin,
foregroundColor: new Color(0, 0, 0),
backgroundColor: new Color(255, 255, 255)
);
$result = $writer->write($qrCode);
echo '<div style="margin:4rem">';
// generate the QRCode
echo '<h1>',_("Scanner ceci avec freeOTP"),'</h1>';
echo '<p>';
echo _("Scanner ce QRCode avec votre application OTP afin de l'ajouter");
if ( DEBUGNOALYSS > 1) { echo "code attendu",$authenticator->code();}
echo '</p>';
printf('<img src="data:image/png;base64,%s">', base64_encode($result->getString()));
echo '</div>';
} catch (Exception $exc) {
record_log($e);
return;
}
?>

25
vendor/autoload.php vendored Normal file
View file

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit1a0e4ea3fb1d108a445b9e95a7c251ad::getLoader();

22
vendor/bacon/bacon-qr-code/LICENSE vendored Normal file
View file

@ -0,0 +1,22 @@
Copyright (c) 2017, Ben Scholzen 'DASPRiD'
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

57
vendor/bacon/bacon-qr-code/README.md vendored Normal file
View file

@ -0,0 +1,57 @@
# QR Code generator
[![PHP CI](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml/badge.svg)](https://github.com/Bacon/BaconQrCode/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/Bacon/BaconQrCode/branch/master/graph/badge.svg?token=rD0HcAiEEx)](https://codecov.io/gh/Bacon/BaconQrCode)
[![Latest Stable Version](https://poser.pugx.org/bacon/bacon-qr-code/v/stable)](https://packagist.org/packages/bacon/bacon-qr-code)
[![Total Downloads](https://poser.pugx.org/bacon/bacon-qr-code/downloads)](https://packagist.org/packages/bacon/bacon-qr-code)
[![License](https://poser.pugx.org/bacon/bacon-qr-code/license)](https://packagist.org/packages/bacon/bacon-qr-code)
## Introduction
BaconQrCode is a port of QR code portion of the ZXing library. It currently
only features the encoder part, but could later receive the decoder part as
well.
As the Reed Solomon codec implementation of the ZXing library performs quite
slow in PHP, it was exchanged with the implementation by Phil Karn.
## Example usage
```php
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
$renderer = new ImageRenderer(
new RendererStyle(400),
new ImagickImageBackEnd()
);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```
## Available image renderer back ends
BaconQrCode comes with multiple back ends for rendering images. Currently included are the following:
- `ImagickImageBackEnd`: renders raster images using the Imagick library
- `SvgImageBackEnd`: renders SVG files using XMLWriter
- `EpsImageBackEnd`: renders EPS files
### GDLib Renderer
GD library has so many limitations, that GD support is not added as backend, but as separated renderer.
Use `GDLibRenderer` instead of `ImageRenderer`. These are the limitations:
- Does not support gradient.
- Does not support any curves, so you QR code is always squared.
Example usage:
```php
use BaconQrCode\Renderer\GDLibRenderer;
use BaconQrCode\Writer;
$renderer = new GDLibRenderer(400);
$writer = new Writer($renderer);
$writer->writeFile('Hello World!', 'qrcode.png');
```

View file

@ -0,0 +1,50 @@
{
"name": "bacon/bacon-qr-code",
"description": "BaconQrCode is a QR code generator for PHP.",
"license": "BSD-2-Clause",
"homepage": "https://github.com/Bacon/BaconQrCode",
"require": {
"php": "^8.1",
"ext-iconv": "*",
"dasprid/enum": "^1.0.3"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"BaconQrCodeTest\\": "test/"
}
},
"require-dev": {
"phpunit/phpunit": "^10.5.11 || 11.0.4",
"spatie/phpunit-snapshot-assertions": "^5.1.5",
"squizlabs/php_codesniffer": "^3.9",
"phly/keep-a-changelog": "^2.12"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"php-http/discovery": true
}
},
"archive": {
"exclude": [
"/test",
"/phpunit.xml.dist"
]
}
}

View file

@ -0,0 +1,364 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* A simple, fast array of bits.
*/
final class BitArray
{
/**
* Bits represented as an array of integers.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $bits;
/**
* Creates a new bit array with a given size.
*/
public function __construct(private int $size = 0)
{
$this->bits = SplFixedArray::fromArray(array_fill(0, ($this->size + 31) >> 3, 0));
}
/**
* Gets the size in bits.
*/
public function getSize() : int
{
return $this->size;
}
/**
* Gets the size in bytes.
*/
public function getSizeInBytes() : int
{
return ($this->size + 7) >> 3;
}
/**
* Ensures that the array has a minimum capacity.
*/
public function ensureCapacity(int $size) : void
{
if ($size > count($this->bits) << 5) {
$this->bits->setSize(($size + 31) >> 5);
}
}
/**
* Gets a specific bit.
*/
public function get(int $i) : bool
{
return 0 !== ($this->bits[$i >> 5] & (1 << ($i & 0x1f)));
}
/**
* Sets a specific bit.
*/
public function set(int $i) : void
{
$this->bits[$i >> 5] = $this->bits[$i >> 5] | 1 << ($i & 0x1f);
}
/**
* Flips a specific bit.
*/
public function flip(int $i) : void
{
$this->bits[$i >> 5] ^= 1 << ($i & 0x1f);
}
/**
* Gets the next set bit position from a given position.
*/
public function getNextSet(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = $this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = $this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return min($result, $this->size);
}
/**
* Gets the next unset bit position from a given position.
*/
public function getNextUnset(int $from) : int
{
if ($from >= $this->size) {
return $this->size;
}
$bitsOffset = $from >> 5;
$currentBits = ~$this->bits[$bitsOffset];
$bitsLength = count($this->bits);
$currentBits &= ~((1 << ($from & 0x1f)) - 1);
while (0 === $currentBits) {
if (++$bitsOffset === $bitsLength) {
return $this->size;
}
$currentBits = ~$this->bits[$bitsOffset];
}
$result = ($bitsOffset << 5) + BitUtils::numberOfTrailingZeros($currentBits);
return min($result, $this->size);
}
/**
* Sets a bulk of bits.
*/
public function setBulk(int $i, int $newBits) : void
{
$this->bits[$i >> 5] = $newBits;
}
/**
* Sets a range of bits.
*
* @throws InvalidArgumentException if end is smaller than start
*/
public function setRange(int $start, int $end) : void
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j < $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
$this->bits[$i] = $this->bits[$i] | $mask;
}
}
/**
* Clears the bit array, unsetting every bit.
*/
public function clear() : void
{
$bitsLength = count($this->bits);
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Checks if a range of bits is set or not set.
* @throws InvalidArgumentException if end is smaller than start
*/
public function isRange(int $start, int $end, bool $value) : bool
{
if ($end < $start) {
throw new InvalidArgumentException('End must be greater or equal to start');
}
if ($end === $start) {
return true;
}
--$end;
$firstInt = $start >> 5;
$lastInt = $end >> 5;
for ($i = $firstInt; $i <= $lastInt; ++$i) {
$firstBit = $i > $firstInt ? 0 : $start & 0x1f;
$lastBit = $i < $lastInt ? 31 : $end & 0x1f;
if (0 === $firstBit && 31 === $lastBit) {
$mask = 0x7fffffff;
} else {
$mask = 0;
for ($j = $firstBit; $j <= $lastBit; ++$j) {
$mask |= 1 << $j;
}
}
if (($this->bits[$i] & $mask) !== ($value ? $mask : 0)) {
return false;
}
}
return true;
}
/**
* Appends a bit to the array.
*/
public function appendBit(bool $bit) : void
{
$this->ensureCapacity($this->size + 1);
if ($bit) {
$this->bits[$this->size >> 5] = $this->bits[$this->size >> 5] | (1 << ($this->size & 0x1f));
}
++$this->size;
}
/**
* Appends a number of bits (up to 32) to the array.
* @throws InvalidArgumentException if num bits is not between 0 and 32
*/
public function appendBits(int $value, int $numBits) : void
{
if ($numBits < 0 || $numBits > 32) {
throw new InvalidArgumentException('Num bits must be between 0 and 32');
}
$this->ensureCapacity($this->size + $numBits);
for ($numBitsLeft = $numBits; $numBitsLeft > 0; $numBitsLeft--) {
$this->appendBit((($value >> ($numBitsLeft - 1)) & 0x01) === 1);
}
}
/**
* Appends another bit array to this array.
*/
public function appendBitArray(self $other) : void
{
$otherSize = $other->getSize();
$this->ensureCapacity($this->size + $other->getSize());
for ($i = 0; $i < $otherSize; ++$i) {
$this->appendBit($other->get($i));
}
}
/**
* Makes an exclusive-or comparision on the current bit array.
*
* @throws InvalidArgumentException if sizes don't match
*/
public function xorBits(self $other) : void
{
$bitsLength = count($this->bits);
$otherBits = $other->getBitArray();
if ($bitsLength !== count($otherBits)) {
throw new InvalidArgumentException('Sizes don\'t match');
}
for ($i = 0; $i < $bitsLength; ++$i) {
$this->bits[$i] = $this->bits[$i] ^ $otherBits[$i];
}
}
/**
* Converts the bit array to a byte array.
*
* @return SplFixedArray<int>
*/
public function toBytes(int $bitOffset, int $numBytes) : SplFixedArray
{
$bytes = new SplFixedArray($numBytes);
for ($i = 0; $i < $numBytes; ++$i) {
$byte = 0;
for ($j = 0; $j < 8; ++$j) {
if ($this->get($bitOffset)) {
$byte |= 1 << (7 - $j);
}
++$bitOffset;
}
$bytes[$i] = $byte;
}
return $bytes;
}
/**
* Gets the internal bit array.
*
* @return SplFixedArray<int>
*/
public function getBitArray() : SplFixedArray
{
return $this->bits;
}
/**
* Reverses the array.
*/
public function reverse() : void
{
$newBits = new SplFixedArray(count($this->bits));
for ($i = 0; $i < $this->size; ++$i) {
if ($this->get($this->size - $i - 1)) {
$newBits[$i >> 5] = $newBits[$i >> 5] | (1 << ($i & 0x1f));
}
}
$this->bits = $newBits;
}
/**
* Returns a string representation of the bit array.
*/
public function __toString() : string
{
$result = '';
for ($i = 0; $i < $this->size; ++$i) {
if (0 === ($i & 0x07)) {
$result .= ' ';
}
$result .= $this->get($i) ? 'X' : '.';
}
return $result;
}
}

View file

@ -0,0 +1,307 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Bit matrix.
*
* Represents a 2D matrix of bits. In function arguments below, and throughout
* the common module, x is the column position, and y is the row position. The
* ordering is always x, y. The origin is at the top-left.
*/
class BitMatrix
{
/**
* Width of the bit matrix.
*/
private int $width;
/**
* Height of the bit matrix.
*/
private ?int $height;
/**
* Size in bits of each individual row.
*/
private int $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray<int>
*/
private SplFixedArray $bits;
/**
* @throws InvalidArgumentException if a dimension is smaller than zero
*/
public function __construct(int $width, ?int $height = null)
{
if (null === $height) {
$height = $width;
}
if ($width < 1 || $height < 1) {
throw new InvalidArgumentException('Both dimensions must be greater than zero');
}
$this->width = $width;
$this->height = $height;
$this->rowSize = ($width + 31) >> 5;
$this->bits = SplFixedArray::fromArray(array_fill(0, $this->rowSize * $height, 0));
}
/**
* Gets the requested bit, where true means black.
*/
public function get(int $x, int $y) : bool
{
$offset = $y * $this->rowSize + ($x >> 5);
return 0 !== (BitUtils::unsignedRightShift($this->bits[$offset], ($x & 0x1f)) & 1);
}
/**
* Sets the given bit to true.
*/
public function set(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] | (1 << ($x & 0x1f));
}
/**
* Flips the given bit.
*/
public function flip(int $x, int $y) : void
{
$offset = $y * $this->rowSize + ($x >> 5);
$this->bits[$offset] = $this->bits[$offset] ^ (1 << ($x & 0x1f));
}
/**
* Clears all bits (set to false).
*/
public function clear() : void
{
$max = count($this->bits);
for ($i = 0; $i < $max; ++$i) {
$this->bits[$i] = 0;
}
}
/**
* Sets a square region of the bit matrix to true.
*
* @throws InvalidArgumentException if left or top are negative
* @throws InvalidArgumentException if width or height are smaller than 1
* @throws InvalidArgumentException if region does not fit into the matix
*/
public function setRegion(int $left, int $top, int $width, int $height) : void
{
if ($top < 0 || $left < 0) {
throw new InvalidArgumentException('Left and top must be non-negative');
}
if ($height < 1 || $width < 1) {
throw new InvalidArgumentException('Width and height must be at least 1');
}
$right = $left + $width;
$bottom = $top + $height;
if ($bottom > $this->height || $right > $this->width) {
throw new InvalidArgumentException('The region must fit inside the matrix');
}
for ($y = $top; $y < $bottom; ++$y) {
$offset = $y * $this->rowSize;
for ($x = $left; $x < $right; ++$x) {
$index = $offset + ($x >> 5);
$this->bits[$index] = $this->bits[$index] | (1 << ($x & 0x1f));
}
}
}
/**
* A fast method to retrieve one row of data from the matrix as a BitArray.
*/
public function getRow(int $y, ?BitArray $row = null) : BitArray
{
if (null === $row || $row->getSize() < $this->width) {
$row = new BitArray($this->width);
}
$offset = $y * $this->rowSize;
for ($x = 0; $x < $this->rowSize; ++$x) {
$row->setBulk($x << 5, $this->bits[$offset + $x]);
}
return $row;
}
/**
* Sets a row of data from a BitArray.
*/
public function setRow(int $y, BitArray $row) : void
{
$bits = $row->getBitArray();
for ($i = 0; $i < $this->rowSize; ++$i) {
$this->bits[$y * $this->rowSize + $i] = $bits[$i];
}
}
/**
* This is useful in detecting the enclosing rectangle of a 'pure' barcode.
*
* @return int[]|null
*/
public function getEnclosingRectangle() : ?array
{
$left = $this->width;
$top = $this->height;
$right = -1;
$bottom = -1;
for ($y = 0; $y < $this->height; ++$y) {
for ($x32 = 0; $x32 < $this->rowSize; ++$x32) {
$bits = $this->bits[$y * $this->rowSize + $x32];
if (0 !== $bits) {
if ($y < $top) {
$top = $y;
}
if ($y > $bottom) {
$bottom = $y;
}
if ($x32 * 32 < $left) {
$bit = 0;
while (($bits << (31 - $bit)) === 0) {
$bit++;
}
if (($x32 * 32 + $bit) < $left) {
$left = $x32 * 32 + $bit;
}
}
}
if ($x32 * 32 + 31 > $right) {
$bit = 31;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
if (($x32 * 32 + $bit) > $right) {
$right = $x32 * 32 + $bit;
}
}
}
}
$width = $right - $left;
$height = $bottom - $top;
if ($width < 0 || $height < 0) {
return null;
}
return [$left, $top, $width, $height];
}
/**
* Gets the most top left set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getTopLeftOnBit() : ?array
{
$bitsOffset = 0;
while ($bitsOffset < count($this->bits) && 0 === $this->bits[$bitsOffset]) {
++$bitsOffset;
}
if (count($this->bits) === $bitsOffset) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === ($bits << (31 - $bit))) {
++$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the most bottom right set bit.
*
* This is useful in detecting a corner of a 'pure' barcode.
*
* @return int[]|null
*/
public function getBottomRightOnBit() : ?array
{
$bitsOffset = count($this->bits) - 1;
while ($bitsOffset >= 0 && 0 === $this->bits[$bitsOffset]) {
--$bitsOffset;
}
if ($bitsOffset < 0) {
return null;
}
$x = intdiv($bitsOffset, $this->rowSize);
$y = ($bitsOffset % $this->rowSize) << 5;
$bits = $this->bits[$bitsOffset];
$bit = 0;
while (0 === BitUtils::unsignedRightShift($bits, $bit)) {
--$bit;
}
$x += $bit;
return [$x, $y];
}
/**
* Gets the width of the matrix,
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* General bit utilities.
*
* All utility methods are based on 32-bit integers and also work on 64-bit
* systems.
*/
final class BitUtils
{
private function __construct()
{
}
/**
* Performs an unsigned right shift.
*
* This is the same as the unsigned right shift operator ">>>" in other
* languages.
*/
public static function unsignedRightShift(int $a, int $b) : int
{
return (
$a >= 0
? $a >> $b
: (($a & 0x7fffffff) >> $b) | (0x40000000 >> ($b - 1))
);
}
/**
* Gets the number of trailing zeros.
*/
public static function numberOfTrailingZeros(int $i) : int
{
$lastPos = strrpos(str_pad(decbin($i), 32, '0', STR_PAD_LEFT), '1');
return $lastPos === false ? 32 : 31 - $lastPos;
}
}

View file

@ -0,0 +1,177 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use DASPRiD\Enum\AbstractEnum;
/**
* Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 of ISO 18004.
*
* @method static self CP437()
* @method static self ISO8859_1()
* @method static self ISO8859_2()
* @method static self ISO8859_3()
* @method static self ISO8859_4()
* @method static self ISO8859_5()
* @method static self ISO8859_6()
* @method static self ISO8859_7()
* @method static self ISO8859_8()
* @method static self ISO8859_9()
* @method static self ISO8859_10()
* @method static self ISO8859_11()
* @method static self ISO8859_12()
* @method static self ISO8859_13()
* @method static self ISO8859_14()
* @method static self ISO8859_15()
* @method static self ISO8859_16()
* @method static self SJIS()
* @method static self CP1250()
* @method static self CP1251()
* @method static self CP1252()
* @method static self CP1256()
* @method static self UNICODE_BIG_UNMARKED()
* @method static self UTF8()
* @method static self ASCII()
* @method static self BIG5()
* @method static self GB18030()
* @method static self EUC_KR()
*/
final class CharacterSetEci extends AbstractEnum
{
protected const CP437 = [[0, 2]];
protected const ISO8859_1 = [[1, 3], 'ISO-8859-1'];
protected const ISO8859_2 = [[4], 'ISO-8859-2'];
protected const ISO8859_3 = [[5], 'ISO-8859-3'];
protected const ISO8859_4 = [[6], 'ISO-8859-4'];
protected const ISO8859_5 = [[7], 'ISO-8859-5'];
protected const ISO8859_6 = [[8], 'ISO-8859-6'];
protected const ISO8859_7 = [[9], 'ISO-8859-7'];
protected const ISO8859_8 = [[10], 'ISO-8859-8'];
protected const ISO8859_9 = [[11], 'ISO-8859-9'];
protected const ISO8859_10 = [[12], 'ISO-8859-10'];
protected const ISO8859_11 = [[13], 'ISO-8859-11'];
protected const ISO8859_12 = [[14], 'ISO-8859-12'];
protected const ISO8859_13 = [[15], 'ISO-8859-13'];
protected const ISO8859_14 = [[16], 'ISO-8859-14'];
protected const ISO8859_15 = [[17], 'ISO-8859-15'];
protected const ISO8859_16 = [[18], 'ISO-8859-16'];
protected const SJIS = [[20], 'Shift_JIS'];
protected const CP1250 = [[21], 'windows-1250'];
protected const CP1251 = [[22], 'windows-1251'];
protected const CP1252 = [[23], 'windows-1252'];
protected const CP1256 = [[24], 'windows-1256'];
protected const UNICODE_BIG_UNMARKED = [[25], 'UTF-16BE', 'UnicodeBig'];
protected const UTF8 = [[26], 'UTF-8'];
protected const ASCII = [[27, 170], 'US-ASCII'];
protected const BIG5 = [[28]];
protected const GB18030 = [[29], 'GB2312', 'EUC_CN', 'GBK'];
protected const EUC_KR = [[30], 'EUC-KR'];
/**
* @var string[]
*/
private array $otherEncodingNames;
/**
* @var array<int, self>|null
*/
private static ?array $valueToEci;
/**
* @var array<string, self>|null
*/
private static ?array $nameToEci = null;
/**
* @param int[] $values
*/
public function __construct(private readonly array $values, string ...$otherEncodingNames)
{
$this->otherEncodingNames = $otherEncodingNames;
}
/**
* Returns the primary value.
*/
public function getValue() : int
{
return $this->values[0];
}
/**
* Gets character set ECI by value.
*
* Returns the representing ECI of a given value, or null if it is legal but unsupported.
*
* @throws InvalidArgumentException if value is not between 0 and 900
*/
public static function getCharacterSetEciByValue(int $value) : ?self
{
if ($value < 0 || $value >= 900) {
throw new InvalidArgumentException('Value must be between 0 and 900');
}
$valueToEci = self::valueToEci();
if (! array_key_exists($value, $valueToEci)) {
return null;
}
return $valueToEci[$value];
}
/**
* Returns character set ECI by name.
*
* Returns the representing ECI of a given name, or null if it is legal but unsupported
*/
public static function getCharacterSetEciByName(string $name) : ?self
{
$nameToEci = self::nameToEci();
$name = strtolower($name);
if (! array_key_exists($name, $nameToEci)) {
return null;
}
return $nameToEci[$name];
}
private static function valueToEci() : array
{
if (null !== self::$valueToEci) {
return self::$valueToEci;
}
self::$valueToEci = [];
foreach (self::values() as $eci) {
foreach ($eci->values as $value) {
self::$valueToEci[$value] = $eci;
}
}
return self::$valueToEci;
}
private static function nameToEci() : array
{
if (null !== self::$nameToEci) {
return self::$nameToEci;
}
self::$nameToEci = [];
foreach (self::values() as $eci) {
self::$nameToEci[strtolower($eci->name())] = $eci;
foreach ($eci->otherEncodingNames as $name) {
self::$nameToEci[strtolower($name)] = $eci;
}
}
return self::$nameToEci;
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* Encapsulates the parameters for one error-correction block in one symbol version.
*
* This includes the number of data codewords, and the number of times a block with these parameters is used
* consecutively in the QR code version's format.
*/
final class EcBlock
{
public function __construct(private readonly int $count, private readonly int $dataCodewords)
{
}
/**
* Returns how many times the block is used.
*/
public function getCount() : int
{
return $this->count;
}
/**
* Returns the number of data codewords.
*/
public function getDataCodewords() : int
{
return $this->dataCodewords;
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
/**
* Encapsulates a set of error-correction blocks in one symbol version.
*
* Most versions will use blocks of differing sizes within one version, so, this encapsulates the parameters for each
* set of blocks. It also holds the number of error-correction codewords per block since it will be the same across all
* blocks within one version.
*/
final class EcBlocks
{
/**
* List of EC blocks.
*
* @var EcBlock[]
*/
private array $ecBlocks;
public function __construct(private readonly int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
{
$this->ecBlocks = $ecBlocks;
}
/**
* Returns the number of EC codewords per block.
*/
public function getEcCodewordsPerBlock() : int
{
return $this->ecCodewordsPerBlock;
}
/**
* Returns the total number of EC block appearances.
*/
public function getNumBlocks() : int
{
$total = 0;
foreach ($this->ecBlocks as $ecBlock) {
$total += $ecBlock->getCount();
}
return $total;
}
/**
* Returns the total count of EC codewords.
*/
public function getTotalEcCodewords() : int
{
return $this->ecCodewordsPerBlock * $this->getNumBlocks();
}
/**
* Returns the EC blocks included in this collection.
*
* @return EcBlock[]
*/
public function getEcBlocks() : array
{
return $this->ecBlocks;
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\OutOfBoundsException;
use DASPRiD\Enum\AbstractEnum;
/**
* Enum representing the four error correction levels.
*
* @method static self L() ~7% correction
* @method static self M() ~15% correction
* @method static self Q() ~25% correction
* @method static self H() ~30% correction
*/
final class ErrorCorrectionLevel extends AbstractEnum
{
protected const L = [0x01];
protected const M = [0x00];
protected const Q = [0x03];
protected const H = [0x02];
protected function __construct(private readonly int $bits)
{
}
/**
* @throws OutOfBoundsException if number of bits is invalid
*/
public static function forBits(int $bits) : self
{
switch ($bits) {
case 0:
return self::M();
case 1:
return self::L();
case 2:
return self::H();
case 3:
return self::Q();
}
throw new OutOfBoundsException('Invalid number of bits');
}
/**
* Returns the two bits used to encode this error correction level.
*/
public function getBits() : int
{
return $this->bits;
}
}

View file

@ -0,0 +1,196 @@
<?php
/**
* BaconQrCode
*
* @link http://github.com/Bacon/BaconQrCode For the canonical source repository
* @copyright 2013 Ben 'DASPRiD' Scholzen
* @license http://opensource.org/licenses/BSD-2-Clause Simplified BSD License
*/
namespace BaconQrCode\Common;
/**
* Encapsulates a QR Code's format information, including the data mask used and error correction level.
*/
class FormatInformation
{
/**
* Mask for format information.
*/
private const FORMAT_INFO_MASK_QR = 0x5412;
/**
* Lookup table for decoding format information.
*
* See ISO 18004:2006, Annex C, Table C.1
*/
private const FORMAT_INFO_DECODE_LOOKUP = [
[0x5412, 0x00],
[0x5125, 0x01],
[0x5e7c, 0x02],
[0x5b4b, 0x03],
[0x45f9, 0x04],
[0x40ce, 0x05],
[0x4f97, 0x06],
[0x4aa0, 0x07],
[0x77c4, 0x08],
[0x72f3, 0x09],
[0x7daa, 0x0a],
[0x789d, 0x0b],
[0x662f, 0x0c],
[0x6318, 0x0d],
[0x6c41, 0x0e],
[0x6976, 0x0f],
[0x1689, 0x10],
[0x13be, 0x11],
[0x1ce7, 0x12],
[0x19d0, 0x13],
[0x0762, 0x14],
[0x0255, 0x15],
[0x0d0c, 0x16],
[0x083b, 0x17],
[0x355f, 0x18],
[0x3068, 0x19],
[0x3f31, 0x1a],
[0x3a06, 0x1b],
[0x24b4, 0x1c],
[0x2183, 0x1d],
[0x2eda, 0x1e],
[0x2bed, 0x1f],
];
/**
* Offset i holds the number of 1 bits in the binary representation of i.
*
* @var int[]
*/
private const BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
/**
* Error correction level.
*/
private ErrorCorrectionLevel $ecLevel;
private int $dataMask;
protected function __construct(int $formatInfo)
{
$this->ecLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x3);
$this->dataMask = $formatInfo & 0x7;
}
/**
* Checks how many bits are different between two integers.
*/
public static function numBitsDiffering(int $a, int $b) : int
{
$a ^= $b;
return (
self::BITS_SET_IN_HALF_BYTE[$a & 0xf]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 4) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 8) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 12) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 16) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 20) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 24) & 0xf)]
+ self::BITS_SET_IN_HALF_BYTE[(BitUtils::unsignedRightShift($a, 28) & 0xf)]
);
}
/**
* Decodes format information.
*/
public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
{
$formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2);
if (null !== $formatInfo) {
return $formatInfo;
}
// Should return null, but, some QR codes apparently do not mask this info. Try again by actually masking the
// pattern first.
return self::doDecodeFormatInformation(
$maskedFormatInfo1 ^ self::FORMAT_INFO_MASK_QR,
$maskedFormatInfo2 ^ self::FORMAT_INFO_MASK_QR
);
}
/**
* Internal method for decoding format information.
*/
private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) : ?self
{
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
foreach (self::FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) {
$targetInfo = $decodeInfo[0];
if ($targetInfo === $maskedFormatInfo1 || $targetInfo === $maskedFormatInfo2) {
// Found an exact match
return new self($decodeInfo[1]);
}
$bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
if ($maskedFormatInfo1 !== $maskedFormatInfo2) {
// Also try the other option
$bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo);
if ($bitsDifference < $bestDifference) {
$bestFormatInfo = $decodeInfo[1];
$bestDifference = $bitsDifference;
}
}
}
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match.
if ($bestDifference <= 3) {
return new self($bestFormatInfo);
}
return null;
}
/**
* Returns the error correction level.
*/
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
{
return $this->ecLevel;
}
/**
* Returns the data mask.
*/
public function getDataMask() : int
{
return $this->dataMask;
}
/**
* Hashes the code of the EC level.
*/
public function hashCode() : int
{
return ($this->ecLevel->getBits() << 3) | $this->dataMask;
}
/**
* Verifies if this instance equals another one.
*/
public function equals(self $other) : bool
{
return (
$this->ecLevel === $other->ecLevel
&& $this->dataMask === $other->dataMask
);
}
}

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use DASPRiD\Enum\AbstractEnum;
/**
* Enum representing various modes in which data can be encoded to bits.
*
* @method static self TERMINATOR()
* @method static self NUMERIC()
* @method static self ALPHANUMERIC()
* @method static self STRUCTURED_APPEND()
* @method static self BYTE()
* @method static self ECI()
* @method static self KANJI()
* @method static self FNC1_FIRST_POSITION()
* @method static self FNC1_SECOND_POSITION()
* @method static self HANZI()
*/
final class Mode extends AbstractEnum
{
protected const TERMINATOR = [[0, 0, 0], 0x00];
protected const NUMERIC = [[10, 12, 14], 0x01];
protected const ALPHANUMERIC = [[9, 11, 13], 0x02];
protected const STRUCTURED_APPEND = [[0, 0, 0], 0x03];
protected const BYTE = [[8, 16, 16], 0x04];
protected const ECI = [[0, 0, 0], 0x07];
protected const KANJI = [[8, 10, 12], 0x08];
protected const FNC1_FIRST_POSITION = [[0, 0, 0], 0x05];
protected const FNC1_SECOND_POSITION = [[0, 0, 0], 0x09];
protected const HANZI = [[8, 10, 12], 0x0d];
/**
* @param int[] $characterCountBitsForVersions
*/
protected function __construct(
private readonly array $characterCountBitsForVersions,
private readonly int $bits
) {
}
/**
* Returns the number of bits used in a specific QR code version.
*/
public function getCharacterCountBits(Version $version) : int
{
$number = $version->getVersionNumber();
if ($number <= 9) {
$offset = 0;
} elseif ($number <= 26) {
$offset = 1;
} else {
$offset = 2;
}
return $this->characterCountBitsForVersions[$offset];
}
/**
* Returns the four bits used to encode this mode.
*/
public function getBits() : int
{
return $this->bits;
}
}

View file

@ -0,0 +1,454 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Exception\RuntimeException;
use SplFixedArray;
/**
* Reed-Solomon codec for 8-bit characters.
*
* Based on libfec by Phil Karn, KA9Q.
*/
final class ReedSolomonCodec
{
/**
* Symbol size in bits.
*/
private int $symbolSize;
/**
* Block size in symbols.
*/
private int $blockSize;
/**
* First root of RS code generator polynomial, index form.
*/
private int $firstRoot;
/**
* Primitive element to generate polynomial roots, index form.
*/
private int $primitive;
/**
* Prim-th root of 1, index form.
*/
private int $iPrimitive;
/**
* RS code generator polynomial degree (number of roots).
*/
private int $numRoots;
/**
* Padding bytes at front of shortened block.
*/
private int $padding;
/**
* Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $alphaTo;
/**
* Anti-Log lookup table.
*
* @var SplFixedArray
*/
private SplFixedArray $indexOf;
/**
* Generator polynomial.
*
* @var SplFixedArray
*/
private SplFixedArray $generatorPoly;
/**
* @throws InvalidArgumentException if symbol size ist not between 0 and 8
* @throws InvalidArgumentException if first root is invalid
* @throws InvalidArgumentException if num roots is invalid
* @throws InvalidArgumentException if padding is invalid
* @throws RuntimeException if field generator polynomial is not primitive
*/
public function __construct(
int $symbolSize,
int $gfPoly,
int $firstRoot,
int $primitive,
int $numRoots,
int $padding
) {
if ($symbolSize < 0 || $symbolSize > 8) {
throw new InvalidArgumentException('Symbol size must be between 0 and 8');
}
if ($firstRoot < 0 || $firstRoot >= (1 << $symbolSize)) {
throw new InvalidArgumentException('First root must be between 0 and ' . (1 << $symbolSize));
}
if ($numRoots < 0 || $numRoots >= (1 << $symbolSize)) {
throw new InvalidArgumentException('Num roots must be between 0 and ' . (1 << $symbolSize));
}
if ($padding < 0 || $padding >= ((1 << $symbolSize) - 1 - $numRoots)) {
throw new InvalidArgumentException(
'Padding must be between 0 and ' . ((1 << $symbolSize) - 1 - $numRoots)
);
}
$this->symbolSize = $symbolSize;
$this->blockSize = (1 << $symbolSize) - 1;
$this->padding = $padding;
$this->alphaTo = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
$this->indexOf = SplFixedArray::fromArray(array_fill(0, $this->blockSize + 1, 0), false);
// Generate galous field lookup table
$this->indexOf[0] = $this->blockSize;
$this->alphaTo[$this->blockSize] = 0;
$sr = 1;
for ($i = 0; $i < $this->blockSize; ++$i) {
$this->indexOf[$sr] = $i;
$this->alphaTo[$i] = $sr;
$sr <<= 1;
if ($sr & (1 << $symbolSize)) {
$sr ^= $gfPoly;
}
$sr &= $this->blockSize;
}
if (1 !== $sr) {
throw new RuntimeException('Field generator polynomial is not primitive');
}
// Form RS code generator polynomial from its roots
$this->generatorPoly = SplFixedArray::fromArray(array_fill(0, $numRoots + 1, 0), false);
$this->firstRoot = $firstRoot;
$this->primitive = $primitive;
$this->numRoots = $numRoots;
// Find prim-th root of 1, used in decoding
for ($iPrimitive = 1; ($iPrimitive % $primitive) !== 0; $iPrimitive += $this->blockSize) {
}
$this->iPrimitive = intdiv($iPrimitive, $primitive);
$this->generatorPoly[0] = 1;
for ($i = 0, $root = $firstRoot * $primitive; $i < $numRoots; ++$i, $root += $primitive) {
$this->generatorPoly[$i + 1] = 1;
for ($j = $i; $j > 0; $j--) {
if ($this->generatorPoly[$j] !== 0) {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1] ^ $this->alphaTo[
$this->modNn($this->indexOf[$this->generatorPoly[$j]] + $root)
];
} else {
$this->generatorPoly[$j] = $this->generatorPoly[$j - 1];
}
}
$this->generatorPoly[$j] = $this->alphaTo[$this->modNn($this->indexOf[$this->generatorPoly[0]] + $root)];
}
// Convert generator poly to index form for quicker encoding
for ($i = 0; $i <= $numRoots; ++$i) {
$this->generatorPoly[$i] = $this->indexOf[$this->generatorPoly[$i]];
}
}
/**
* Encodes data and writes result back into parity array.
*/
public function encode(SplFixedArray $data, SplFixedArray $parity) : void
{
for ($i = 0; $i < $this->numRoots; ++$i) {
$parity[$i] = 0;
}
$iterations = $this->blockSize - $this->numRoots - $this->padding;
for ($i = 0; $i < $iterations; ++$i) {
$feedback = $this->indexOf[$data[$i] ^ $parity[0]];
if ($feedback !== $this->blockSize) {
// Feedback term is non-zero
$feedback = $this->modNn($this->blockSize - $this->generatorPoly[$this->numRoots] + $feedback);
for ($j = 1; $j < $this->numRoots; ++$j) {
$parity[$j] = $parity[$j] ^ $this->alphaTo[
$this->modNn($feedback + $this->generatorPoly[$this->numRoots - $j])
];
}
}
for ($j = 0; $j < $this->numRoots - 1; ++$j) {
$parity[$j] = $parity[$j + 1];
}
if ($feedback !== $this->blockSize) {
$parity[$this->numRoots - 1] = $this->alphaTo[$this->modNn($feedback + $this->generatorPoly[0])];
} else {
$parity[$this->numRoots - 1] = 0;
}
}
}
/**
* Decodes received data.
*/
public function decode(SplFixedArray $data, ?SplFixedArray $erasures = null) : ?int
{
// This speeds up the initialization a bit.
$numRootsPlusOne = SplFixedArray::fromArray(array_fill(0, $this->numRoots + 1, 0), false);
$numRoots = SplFixedArray::fromArray(array_fill(0, $this->numRoots, 0), false);
$lambda = clone $numRootsPlusOne;
$b = clone $numRootsPlusOne;
$t = clone $numRootsPlusOne;
$omega = clone $numRootsPlusOne;
$root = clone $numRoots;
$loc = clone $numRoots;
$numErasures = (null !== $erasures ? count($erasures) : 0);
// Form the Syndromes; i.e., evaluate data(x) at roots of g(x)
$syndromes = SplFixedArray::fromArray(array_fill(0, $this->numRoots, $data[0]), false);
for ($i = 1; $i < $this->blockSize - $this->padding; ++$i) {
for ($j = 0; $j < $this->numRoots; ++$j) {
if ($syndromes[$j] === 0) {
$syndromes[$j] = $data[$i];
} else {
$syndromes[$j] = $data[$i] ^ $this->alphaTo[
$this->modNn($this->indexOf[$syndromes[$j]] + ($this->firstRoot + $j) * $this->primitive)
];
}
}
}
// Convert syndromes to index form, checking for nonzero conditions
$syndromeError = 0;
for ($i = 0; $i < $this->numRoots; ++$i) {
$syndromeError |= $syndromes[$i];
$syndromes[$i] = $this->indexOf[$syndromes[$i]];
}
if (! $syndromeError) {
// If syndrome is zero, data[] is a codeword and there are no errors to correct, so return data[]
// unmodified.
return 0;
}
$lambda[0] = 1;
if ($numErasures > 0) {
// Init lambda to be the erasure locator polynomial
$lambda[1] = $this->alphaTo[$this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[0]))];
for ($i = 1; $i < $numErasures; ++$i) {
$u = $this->modNn($this->primitive * ($this->blockSize - 1 - $erasures[$i]));
for ($j = $i + 1; $j > 0; --$j) {
$tmp = $this->indexOf[$lambda[$j - 1]];
if ($tmp !== $this->blockSize) {
$lambda[$j] = $lambda[$j] ^ $this->alphaTo[$this->modNn($u + $tmp)];
}
}
}
}
for ($i = 0; $i <= $this->numRoots; ++$i) {
$b[$i] = $this->indexOf[$lambda[$i]];
}
// Begin Berlekamp-Massey algorithm to determine error+erasure locator polynomial
$r = $numErasures;
$el = $numErasures;
while (++$r <= $this->numRoots) {
// Compute discrepancy at the r-th step in poly form
$discrepancyR = 0;
for ($i = 0; $i < $r; ++$i) {
if ($lambda[$i] !== 0 && $syndromes[$r - $i - 1] !== $this->blockSize) {
$discrepancyR ^= $this->alphaTo[
$this->modNn($this->indexOf[$lambda[$i]] + $syndromes[$r - $i - 1])
];
}
}
$discrepancyR = $this->indexOf[$discrepancyR];
if ($discrepancyR === $this->blockSize) {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
continue;
}
$t[0] = $lambda[0];
for ($i = 0; $i < $this->numRoots; ++$i) {
if ($b[$i] !== $this->blockSize) {
$t[$i + 1] = $lambda[$i + 1] ^ $this->alphaTo[$this->modNn($discrepancyR + $b[$i])];
} else {
$t[$i + 1] = $lambda[$i + 1];
}
}
if (2 * $el <= $r + $numErasures - 1) {
$el = $r + $numErasures - $el;
for ($i = 0; $i <= $this->numRoots; ++$i) {
$b[$i] = (
$lambda[$i] === 0
? $this->blockSize
: $this->modNn($this->indexOf[$lambda[$i]] - $discrepancyR + $this->blockSize)
);
}
} else {
$tmp = $b->toArray();
array_unshift($tmp, $this->blockSize);
array_pop($tmp);
$b = SplFixedArray::fromArray($tmp, false);
}
$lambda = clone $t;
}
// Convert lambda to index form and compute deg(lambda(x))
$degLambda = 0;
for ($i = 0; $i <= $this->numRoots; ++$i) {
$lambda[$i] = $this->indexOf[$lambda[$i]];
if ($lambda[$i] !== $this->blockSize) {
$degLambda = $i;
}
}
// Find roots of the error+erasure locator polynomial by Chien search.
$reg = clone $lambda;
$reg[0] = 0;
$count = 0;
$i = 1;
for ($k = $this->iPrimitive - 1; $i <= $this->blockSize; ++$i, $k = $this->modNn($k + $this->iPrimitive)) {
$q = 1;
for ($j = $degLambda; $j > 0; $j--) {
if ($reg[$j] !== $this->blockSize) {
$reg[$j] = $this->modNn($reg[$j] + $j);
$q ^= $this->alphaTo[$reg[$j]];
}
}
if ($q !== 0) {
// Not a root
continue;
}
// Store root (index-form) and error location number
$root[$count] = $i;
$loc[$count] = $k;
if (++$count === $degLambda) {
break;
}
}
if ($degLambda !== $count) {
// deg(lambda) unequal to number of roots: uncorrectable error detected
return null;
}
// Compute err+eras evaluate poly omega(x) = s(x)*lambda(x) (modulo x**numRoots). In index form. Also find
// deg(omega).
$degOmega = $degLambda - 1;
for ($i = 0; $i <= $degOmega; ++$i) {
$tmp = 0;
for ($j = $i; $j >= 0; --$j) {
if ($syndromes[$i - $j] !== $this->blockSize && $lambda[$j] !== $this->blockSize) {
$tmp ^= $this->alphaTo[$this->modNn($syndromes[$i - $j] + $lambda[$j])];
}
}
$omega[$i] = $this->indexOf[$tmp];
}
// Compute error values in poly-form. num1 = omega(inv(X(l))), num2 = inv(X(l))**(firstRoot-1) and
// den = lambda_pr(inv(X(l))) all in poly form.
for ($j = $count - 1; $j >= 0; --$j) {
$num1 = 0;
for ($i = $degOmega; $i >= 0; $i--) {
if ($omega[$i] !== $this->blockSize) {
$num1 ^= $this->alphaTo[$this->modNn($omega[$i] + $i * $root[$j])];
}
}
$num2 = $this->alphaTo[$this->modNn($root[$j] * ($this->firstRoot - 1) + $this->blockSize)];
$den = 0;
// lambda[i+1] for i even is the formal derivativelambda_pr of lambda[i]
for ($i = min($degLambda, $this->numRoots - 1) & ~1; $i >= 0; $i -= 2) {
if ($lambda[$i + 1] !== $this->blockSize) {
$den ^= $this->alphaTo[$this->modNn($lambda[$i + 1] + $i * $root[$j])];
}
}
// Apply error to data
if ($num1 !== 0 && $loc[$j] >= $this->padding) {
$data[$loc[$j] - $this->padding] = $data[$loc[$j] - $this->padding] ^ (
$this->alphaTo[
$this->modNn(
$this->indexOf[$num1] + $this->indexOf[$num2] + $this->blockSize - $this->indexOf[$den]
)
]
);
}
}
if (null !== $erasures) {
if (count($erasures) < $count) {
$erasures->setSize($count);
}
for ($i = 0; $i < $count; $i++) {
$erasures[$i] = $loc[$i];
}
}
return $count;
}
/**
* Computes $x % GF_SIZE, where GF_SIZE is 2**GF_BITS - 1, without a slow divide.
*/
private function modNn(int $x) : int
{
while ($x >= $this->blockSize) {
$x -= $this->blockSize;
$x = ($x >> $this->symbolSize) + ($x & $this->blockSize);
}
return $x;
}
}

View file

@ -0,0 +1,592 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Common;
use BaconQrCode\Exception\InvalidArgumentException;
use SplFixedArray;
/**
* Version representation.
*/
final class Version
{
private const VERSION_DECODE_INFO = [
0x07c94,
0x085bc,
0x09a99,
0x0a4d3,
0x0bbf6,
0x0c762,
0x0d847,
0x0e60d,
0x0f928,
0x10b78,
0x1145d,
0x12a17,
0x13532,
0x149a6,
0x15683,
0x168c9,
0x177ec,
0x18ec4,
0x191e1,
0x1afab,
0x1b08e,
0x1cc1a,
0x1d33f,
0x1ed75,
0x1f250,
0x209d5,
0x216f0,
0x228ba,
0x2379f,
0x24b0b,
0x2542e,
0x26a64,
0x27541,
0x28c69,
];
/**
* Version number of this version.
*/
private int $versionNumber;
/**
* Alignment pattern centers.
*
* @var SplFixedArray|array
*/
private SplFixedArray|array $alignmentPatternCenters;
/**
* Error correction blocks.
*
* @var EcBlocks[]
*/
private array $ecBlocks;
/**
* Total number of codewords.
*/
private null|int|float $totalCodewords;
/**
* Cached version instances.
*
* @var array<int, self>|null
*/
private static ?array $versions = null;
/**
* @param int[] $alignmentPatternCenters
*/
private function __construct(
int $versionNumber,
array $alignmentPatternCenters,
EcBlocks ...$ecBlocks
) {
$this->versionNumber = $versionNumber;
$this->alignmentPatternCenters = $alignmentPatternCenters;
$this->ecBlocks = $ecBlocks;
$totalCodewords = 0;
$ecCodewords = $ecBlocks[0]->getEcCodewordsPerBlock();
foreach ($ecBlocks[0]->getEcBlocks() as $ecBlock) {
$totalCodewords += $ecBlock->getCount() * ($ecBlock->getDataCodewords() + $ecCodewords);
}
$this->totalCodewords = $totalCodewords;
}
/**
* Returns the version number.
*/
public function getVersionNumber() : int
{
return $this->versionNumber;
}
/**
* Returns the alignment pattern centers.
*
* @return int[]
*/
public function getAlignmentPatternCenters() : array
{
return $this->alignmentPatternCenters;
}
/**
* Returns the total number of codewords.
*/
public function getTotalCodewords() : int
{
return $this->totalCodewords;
}
/**
* Calculates the dimension for the current version.
*/
public function getDimensionForVersion() : int
{
return 17 + 4 * $this->versionNumber;
}
/**
* Returns the number of EC blocks for a specific EC level.
*/
public function getEcBlocksForLevel(ErrorCorrectionLevel $ecLevel) : EcBlocks
{
return $this->ecBlocks[$ecLevel->ordinal()];
}
/**
* Gets a provisional version number for a specific dimension.
*
* @throws InvalidArgumentException if dimension is not 1 mod 4
*/
public static function getProvisionalVersionForDimension(int $dimension) : self
{
if (1 !== $dimension % 4) {
throw new InvalidArgumentException('Dimension is not 1 mod 4');
}
return self::getVersionForNumber(intdiv($dimension - 17, 4));
}
/**
* Gets a version instance for a specific version number.
*
* @throws InvalidArgumentException if version number is out of range
*/
public static function getVersionForNumber(int $versionNumber) : self
{
if ($versionNumber < 1 || $versionNumber > 40) {
throw new InvalidArgumentException('Version number must be between 1 and 40');
}
return self::versions()[$versionNumber - 1];
}
/**
* Decodes version information from an integer and returns the version.
*/
public static function decodeVersionInformation(int $versionBits) : ?self
{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
foreach (self::VERSION_DECODE_INFO as $i => $targetVersion) {
if ($targetVersion === $versionBits) {
return self::getVersionForNumber($i + 7);
}
$bitsDifference = FormatInformation::numBitsDiffering($versionBits, $targetVersion);
if ($bitsDifference < $bestDifference) {
$bestVersion = $i + 7;
$bestDifference = $bitsDifference;
}
}
if ($bestDifference <= 3) {
return self::getVersionForNumber($bestVersion);
}
return null;
}
/**
* Builds the function pattern for the current version.
*/
public function buildFunctionPattern() : BitMatrix
{
$dimension = $this->getDimensionForVersion();
$bitMatrix = new BitMatrix($dimension);
// Top left finder pattern + separator + format
$bitMatrix->setRegion(0, 0, 9, 9);
// Top right finder pattern + separator + format
$bitMatrix->setRegion($dimension - 8, 0, 8, 9);
// Bottom left finder pattern + separator + format
$bitMatrix->setRegion(0, $dimension - 8, 9, 8);
// Alignment patterns
$max = count($this->alignmentPatternCenters);
for ($x = 0; $x < $max; ++$x) {
$i = $this->alignmentPatternCenters[$x] - 2;
for ($y = 0; $y < $max; ++$y) {
if (($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)) {
// No alignment patterns near the three finder paterns
continue;
}
$bitMatrix->setRegion($this->alignmentPatternCenters[$y] - 2, $i, 5, 5);
}
}
// Vertical timing pattern
$bitMatrix->setRegion(6, 9, 1, $dimension - 17);
// Horizontal timing pattern
$bitMatrix->setRegion(9, 6, $dimension - 17, 1);
if ($this->versionNumber > 6) {
// Version info, top right
$bitMatrix->setRegion($dimension - 11, 0, 3, 6);
// Version info, bottom left
$bitMatrix->setRegion(0, $dimension - 11, 6, 3);
}
return $bitMatrix;
}
/**
* Returns a string representation for the version.
*/
public function __toString() : string
{
return (string) $this->versionNumber;
}
/**
* Build and cache a specific version.
*
* See ISO 18004:2006 6.5.1 Table 9.
*
* @return array<int, self>
*/
private static function versions() : array
{
if (null !== self::$versions) {
return self::$versions;
}
return self::$versions = [
new self(
1,
[],
new EcBlocks(7, new EcBlock(1, 19)),
new EcBlocks(10, new EcBlock(1, 16)),
new EcBlocks(13, new EcBlock(1, 13)),
new EcBlocks(17, new EcBlock(1, 9))
),
new self(
2,
[6, 18],
new EcBlocks(10, new EcBlock(1, 34)),
new EcBlocks(16, new EcBlock(1, 28)),
new EcBlocks(22, new EcBlock(1, 22)),
new EcBlocks(28, new EcBlock(1, 16))
),
new self(
3,
[6, 22],
new EcBlocks(15, new EcBlock(1, 55)),
new EcBlocks(26, new EcBlock(1, 44)),
new EcBlocks(18, new EcBlock(2, 17)),
new EcBlocks(22, new EcBlock(2, 13))
),
new self(
4,
[6, 26],
new EcBlocks(20, new EcBlock(1, 80)),
new EcBlocks(18, new EcBlock(2, 32)),
new EcBlocks(26, new EcBlock(2, 24)),
new EcBlocks(16, new EcBlock(4, 9))
),
new self(
5,
[6, 30],
new EcBlocks(26, new EcBlock(1, 108)),
new EcBlocks(24, new EcBlock(2, 43)),
new EcBlocks(18, new EcBlock(2, 15), new EcBlock(2, 16)),
new EcBlocks(22, new EcBlock(2, 11), new EcBlock(2, 12))
),
new self(
6,
[6, 34],
new EcBlocks(18, new EcBlock(2, 68)),
new EcBlocks(16, new EcBlock(4, 27)),
new EcBlocks(24, new EcBlock(4, 19)),
new EcBlocks(28, new EcBlock(4, 15))
),
new self(
7,
[6, 22, 38],
new EcBlocks(20, new EcBlock(2, 78)),
new EcBlocks(18, new EcBlock(4, 31)),
new EcBlocks(18, new EcBlock(2, 14), new EcBlock(4, 15)),
new EcBlocks(26, new EcBlock(4, 13), new EcBlock(1, 14))
),
new self(
8,
[6, 24, 42],
new EcBlocks(24, new EcBlock(2, 97)),
new EcBlocks(22, new EcBlock(2, 38), new EcBlock(2, 39)),
new EcBlocks(22, new EcBlock(4, 18), new EcBlock(2, 19)),
new EcBlocks(26, new EcBlock(4, 14), new EcBlock(2, 15))
),
new self(
9,
[6, 26, 46],
new EcBlocks(30, new EcBlock(2, 116)),
new EcBlocks(22, new EcBlock(3, 36), new EcBlock(2, 37)),
new EcBlocks(20, new EcBlock(4, 16), new EcBlock(4, 17)),
new EcBlocks(24, new EcBlock(4, 12), new EcBlock(4, 13))
),
new self(
10,
[6, 28, 50],
new EcBlocks(18, new EcBlock(2, 68), new EcBlock(2, 69)),
new EcBlocks(26, new EcBlock(4, 43), new EcBlock(1, 44)),
new EcBlocks(24, new EcBlock(6, 19), new EcBlock(2, 20)),
new EcBlocks(28, new EcBlock(6, 15), new EcBlock(2, 16))
),
new self(
11,
[6, 30, 54],
new EcBlocks(20, new EcBlock(4, 81)),
new EcBlocks(30, new EcBlock(1, 50), new EcBlock(4, 51)),
new EcBlocks(28, new EcBlock(4, 22), new EcBlock(4, 23)),
new EcBlocks(24, new EcBlock(3, 12), new EcBlock(8, 13))
),
new self(
12,
[6, 32, 58],
new EcBlocks(24, new EcBlock(2, 92), new EcBlock(2, 93)),
new EcBlocks(22, new EcBlock(6, 36), new EcBlock(2, 37)),
new EcBlocks(26, new EcBlock(4, 20), new EcBlock(6, 21)),
new EcBlocks(28, new EcBlock(7, 14), new EcBlock(4, 15))
),
new self(
13,
[6, 34, 62],
new EcBlocks(26, new EcBlock(4, 107)),
new EcBlocks(22, new EcBlock(8, 37), new EcBlock(1, 38)),
new EcBlocks(24, new EcBlock(8, 20), new EcBlock(4, 21)),
new EcBlocks(22, new EcBlock(12, 11), new EcBlock(4, 12))
),
new self(
14,
[6, 26, 46, 66],
new EcBlocks(30, new EcBlock(3, 115), new EcBlock(1, 116)),
new EcBlocks(24, new EcBlock(4, 40), new EcBlock(5, 41)),
new EcBlocks(20, new EcBlock(11, 16), new EcBlock(5, 17)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(5, 13))
),
new self(
15,
[6, 26, 48, 70],
new EcBlocks(22, new EcBlock(5, 87), new EcBlock(1, 88)),
new EcBlocks(24, new EcBlock(5, 41), new EcBlock(5, 42)),
new EcBlocks(30, new EcBlock(5, 24), new EcBlock(7, 25)),
new EcBlocks(24, new EcBlock(11, 12), new EcBlock(7, 13))
),
new self(
16,
[6, 26, 50, 74],
new EcBlocks(24, new EcBlock(5, 98), new EcBlock(1, 99)),
new EcBlocks(28, new EcBlock(7, 45), new EcBlock(3, 46)),
new EcBlocks(24, new EcBlock(15, 19), new EcBlock(2, 20)),
new EcBlocks(30, new EcBlock(3, 15), new EcBlock(13, 16))
),
new self(
17,
[6, 30, 54, 78],
new EcBlocks(28, new EcBlock(1, 107), new EcBlock(5, 108)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(1, 47)),
new EcBlocks(28, new EcBlock(1, 22), new EcBlock(15, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(17, 15))
),
new self(
18,
[6, 30, 56, 82],
new EcBlocks(30, new EcBlock(5, 120), new EcBlock(1, 121)),
new EcBlocks(26, new EcBlock(9, 43), new EcBlock(4, 44)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(1, 23)),
new EcBlocks(28, new EcBlock(2, 14), new EcBlock(19, 15))
),
new self(
19,
[6, 30, 58, 86],
new EcBlocks(28, new EcBlock(3, 113), new EcBlock(4, 114)),
new EcBlocks(26, new EcBlock(3, 44), new EcBlock(11, 45)),
new EcBlocks(26, new EcBlock(17, 21), new EcBlock(4, 22)),
new EcBlocks(26, new EcBlock(9, 13), new EcBlock(16, 14))
),
new self(
20,
[6, 34, 62, 90],
new EcBlocks(28, new EcBlock(3, 107), new EcBlock(5, 108)),
new EcBlocks(26, new EcBlock(3, 41), new EcBlock(13, 42)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(5, 25)),
new EcBlocks(28, new EcBlock(15, 15), new EcBlock(10, 16))
),
new self(
21,
[6, 28, 50, 72, 94],
new EcBlocks(28, new EcBlock(4, 116), new EcBlock(4, 117)),
new EcBlocks(26, new EcBlock(17, 42)),
new EcBlocks(28, new EcBlock(17, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(19, 16), new EcBlock(6, 17))
),
new self(
22,
[6, 26, 50, 74, 98],
new EcBlocks(28, new EcBlock(2, 111), new EcBlock(7, 112)),
new EcBlocks(28, new EcBlock(17, 46)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(16, 25)),
new EcBlocks(24, new EcBlock(34, 13))
),
new self(
23,
[6, 30, 54, 78, 102],
new EcBlocks(30, new EcBlock(4, 121), new EcBlock(5, 122)),
new EcBlocks(28, new EcBlock(4, 47), new EcBlock(14, 48)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(16, 15), new EcBlock(14, 16))
),
new self(
24,
[6, 28, 54, 80, 106],
new EcBlocks(30, new EcBlock(6, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(6, 45), new EcBlock(14, 46)),
new EcBlocks(30, new EcBlock(11, 24), new EcBlock(16, 25)),
new EcBlocks(30, new EcBlock(30, 16), new EcBlock(2, 17))
),
new self(
25,
[6, 32, 58, 84, 110],
new EcBlocks(26, new EcBlock(8, 106), new EcBlock(4, 107)),
new EcBlocks(28, new EcBlock(8, 47), new EcBlock(13, 48)),
new EcBlocks(30, new EcBlock(7, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(13, 16))
),
new self(
26,
[6, 30, 58, 86, 114],
new EcBlocks(28, new EcBlock(10, 114), new EcBlock(2, 115)),
new EcBlocks(28, new EcBlock(19, 46), new EcBlock(4, 47)),
new EcBlocks(28, new EcBlock(28, 22), new EcBlock(6, 23)),
new EcBlocks(30, new EcBlock(33, 16), new EcBlock(4, 17))
),
new self(
27,
[6, 34, 62, 90, 118],
new EcBlocks(30, new EcBlock(8, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(22, 45), new EcBlock(3, 46)),
new EcBlocks(30, new EcBlock(8, 23), new EcBlock(26, 24)),
new EcBlocks(30, new EcBlock(12, 15), new EcBlock(28, 16))
),
new self(
28,
[6, 26, 50, 74, 98, 122],
new EcBlocks(30, new EcBlock(3, 117), new EcBlock(10, 118)),
new EcBlocks(28, new EcBlock(3, 45), new EcBlock(23, 46)),
new EcBlocks(30, new EcBlock(4, 24), new EcBlock(31, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(31, 16))
),
new self(
29,
[6, 30, 54, 78, 102, 126],
new EcBlocks(30, new EcBlock(7, 116), new EcBlock(7, 117)),
new EcBlocks(28, new EcBlock(21, 45), new EcBlock(7, 46)),
new EcBlocks(30, new EcBlock(1, 23), new EcBlock(37, 24)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(26, 16))
),
new self(
30,
[6, 26, 52, 78, 104, 130],
new EcBlocks(30, new EcBlock(5, 115), new EcBlock(10, 116)),
new EcBlocks(28, new EcBlock(19, 47), new EcBlock(10, 48)),
new EcBlocks(30, new EcBlock(15, 24), new EcBlock(25, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(25, 16))
),
new self(
31,
[6, 30, 56, 82, 108, 134],
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(3, 116)),
new EcBlocks(28, new EcBlock(2, 46), new EcBlock(29, 47)),
new EcBlocks(30, new EcBlock(42, 24), new EcBlock(1, 25)),
new EcBlocks(30, new EcBlock(23, 15), new EcBlock(28, 16))
),
new self(
32,
[6, 34, 60, 86, 112, 138],
new EcBlocks(30, new EcBlock(17, 115)),
new EcBlocks(28, new EcBlock(10, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(10, 24), new EcBlock(35, 25)),
new EcBlocks(30, new EcBlock(19, 15), new EcBlock(35, 16))
),
new self(
33,
[6, 30, 58, 86, 114, 142],
new EcBlocks(30, new EcBlock(17, 115), new EcBlock(1, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(21, 47)),
new EcBlocks(30, new EcBlock(29, 24), new EcBlock(19, 25)),
new EcBlocks(30, new EcBlock(11, 15), new EcBlock(46, 16))
),
new self(
34,
[6, 34, 62, 90, 118, 146],
new EcBlocks(30, new EcBlock(13, 115), new EcBlock(6, 116)),
new EcBlocks(28, new EcBlock(14, 46), new EcBlock(23, 47)),
new EcBlocks(30, new EcBlock(44, 24), new EcBlock(7, 25)),
new EcBlocks(30, new EcBlock(59, 16), new EcBlock(1, 17))
),
new self(
35,
[6, 30, 54, 78, 102, 126, 150],
new EcBlocks(30, new EcBlock(12, 121), new EcBlock(7, 122)),
new EcBlocks(28, new EcBlock(12, 47), new EcBlock(26, 48)),
new EcBlocks(30, new EcBlock(39, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(22, 15), new EcBlock(41, 16))
),
new self(
36,
[6, 24, 50, 76, 102, 128, 154],
new EcBlocks(30, new EcBlock(6, 121), new EcBlock(14, 122)),
new EcBlocks(28, new EcBlock(6, 47), new EcBlock(34, 48)),
new EcBlocks(30, new EcBlock(46, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(2, 15), new EcBlock(64, 16))
),
new self(
37,
[6, 28, 54, 80, 106, 132, 158],
new EcBlocks(30, new EcBlock(17, 122), new EcBlock(4, 123)),
new EcBlocks(28, new EcBlock(29, 46), new EcBlock(14, 47)),
new EcBlocks(30, new EcBlock(49, 24), new EcBlock(10, 25)),
new EcBlocks(30, new EcBlock(24, 15), new EcBlock(46, 16))
),
new self(
38,
[6, 32, 58, 84, 110, 136, 162],
new EcBlocks(30, new EcBlock(4, 122), new EcBlock(18, 123)),
new EcBlocks(28, new EcBlock(13, 46), new EcBlock(32, 47)),
new EcBlocks(30, new EcBlock(48, 24), new EcBlock(14, 25)),
new EcBlocks(30, new EcBlock(42, 15), new EcBlock(32, 16))
),
new self(
39,
[6, 26, 54, 82, 110, 138, 166],
new EcBlocks(30, new EcBlock(20, 117), new EcBlock(4, 118)),
new EcBlocks(28, new EcBlock(40, 47), new EcBlock(7, 48)),
new EcBlocks(30, new EcBlock(43, 24), new EcBlock(22, 25)),
new EcBlocks(30, new EcBlock(10, 15), new EcBlock(67, 16))
),
new self(
40,
[6, 30, 58, 86, 114, 142, 170],
new EcBlocks(30, new EcBlock(19, 118), new EcBlock(6, 119)),
new EcBlocks(28, new EcBlock(18, 47), new EcBlock(31, 48)),
new EcBlocks(30, new EcBlock(34, 24), new EcBlock(34, 25)),
new EcBlocks(30, new EcBlock(20, 15), new EcBlock(61, 16))
),
];
}
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Block pair.
*/
final class BlockPair
{
/**
* Creates a new block pair.
*
* @param SplFixedArray<int> $dataBytes Data bytes in the block.
* @param SplFixedArray<int> $errorCorrectionBytes Error correction bytes in the block.
*/
public function __construct(
private readonly SplFixedArray $dataBytes,
private readonly SplFixedArray $errorCorrectionBytes
) {
}
/**
* Gets the data bytes.
*
* @return SplFixedArray<int>
*/
public function getDataBytes() : SplFixedArray
{
return $this->dataBytes;
}
/**
* Gets the error correction bytes.
*
* @return SplFixedArray<int>
*/
public function getErrorCorrectionBytes() : SplFixedArray
{
return $this->errorCorrectionBytes;
}
}

View file

@ -0,0 +1,134 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
use Traversable;
/**
* Byte matrix.
*/
final class ByteMatrix
{
/**
* Bytes in the matrix, represented as array.
*
* @var SplFixedArray<SplFixedArray<int>>
*/
private SplFixedArray $bytes;
public function __construct(private readonly int $width, private readonly int $height)
{
$this->bytes = new SplFixedArray($height);
for ($y = 0; $y < $height; ++$y) {
$this->bytes[$y] = SplFixedArray::fromArray(array_fill(0, $width, 0));
}
}
/**
* Gets the width of the matrix.
*/
public function getWidth() : int
{
return $this->width;
}
/**
* Gets the height of the matrix.
*/
public function getHeight() : int
{
return $this->height;
}
/**
* Gets the internal representation of the matrix.
*
* @return SplFixedArray<SplFixedArray<int>>
*/
public function getArray() : SplFixedArray
{
return $this->bytes;
}
/**
* @return Traversable<int>
*/
public function getBytes() : Traversable
{
foreach ($this->bytes as $row) {
foreach ($row as $byte) {
yield $byte;
}
}
}
/**
* Gets the byte for a specific position.
*/
public function get(int $x, int $y) : int
{
return $this->bytes[$y][$x];
}
/**
* Sets the byte for a specific position.
*/
public function set(int $x, int $y, int $value) : void
{
$this->bytes[$y][$x] = $value;
}
/**
* Clears the matrix with a specific value.
*/
public function clear(int $value) : void
{
for ($y = 0; $y < $this->height; ++$y) {
for ($x = 0; $x < $this->width; ++$x) {
$this->bytes[$y][$x] = $value;
}
}
}
public function __clone()
{
$this->bytes = clone $this->bytes;
foreach ($this->bytes as $index => $row) {
$this->bytes[$index] = clone $row;
}
}
/**
* Returns a string representation of the matrix.
*/
public function __toString() : string
{
$result = '';
for ($y = 0; $y < $this->height; $y++) {
for ($x = 0; $x < $this->width; $x++) {
switch ($this->bytes[$y][$x]) {
case 0:
$result .= ' 0';
break;
case 1:
$result .= ' 1';
break;
default:
$result .= ' ';
break;
}
}
$result .= "\n";
}
return $result;
}
}

View file

@ -0,0 +1,679 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\CharacterSetEci;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\ReedSolomonCodec;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception\WriterException;
use SplFixedArray;
/**
* Encoder.
*/
final class Encoder
{
/**
* Default byte encoding.
*/
public const DEFAULT_BYTE_MODE_ENCODING = 'ISO-8859-1';
/** @deprecated use DEFAULT_BYTE_MODE_ENCODING */
public const DEFAULT_BYTE_MODE_ECODING = self::DEFAULT_BYTE_MODE_ENCODING;
/**
* The original table is defined in the table 5 of JISX0510:2004 (p.19).
*/
private const ALPHANUMERIC_TABLE = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f
36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f
-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f
];
/**
* Codec cache.
*
* @var array<string,ReedSolomonCodec>
*/
private static array $codecs = [];
/**
* Encodes "content" with the error correction level "ecLevel".
*/
public static function encode(
string $content,
ErrorCorrectionLevel $ecLevel,
string $encoding = self::DEFAULT_BYTE_MODE_ENCODING,
?Version $forcedVersion = null,
// Barcode scanner might not be able to read the encoded message of the QR code with the prefix ECI of UTF-8
bool $prefixEci = true
) : QrCode {
// Pick an encoding mode appropriate for the content. Note that this
// will not attempt to use multiple modes / segments even if that were
// more efficient. Would be nice.
$mode = self::chooseMode($content, $encoding);
// This will store the header information, like mode and length, as well
// as "header" segments like an ECI segment.
$headerBits = new BitArray();
// Append ECI segment if applicable
if ($prefixEci && Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ENCODING !== $encoding) {
$eci = CharacterSetEci::getCharacterSetEciByName($encoding);
if (null !== $eci) {
self::appendEci($eci, $headerBits);
}
}
// (With ECI in place,) Write the mode marker
self::appendModeInfo($mode, $headerBits);
// Collect data within the main segment, separately, to count its size
// if needed. Don't add it to main payload yet.
$dataBits = new BitArray();
self::appendBytes($content, $mode, $dataBits, $encoding);
// Hard part: need to know version to know how many bits length takes.
// But need to know how many bits it takes to know version. First we
// take a guess at version by assuming version will be the minimum, 1:
$provisionalBitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits(Version::getVersionForNumber(1))
+ $dataBits->getSize();
$provisionalVersion = self::chooseVersion($provisionalBitsNeeded, $ecLevel);
// Use that guess to calculate the right version. I am still not sure
// this works in 100% of cases.
$bitsNeeded = $headerBits->getSize()
+ $mode->getCharacterCountBits($provisionalVersion)
+ $dataBits->getSize();
$version = self::chooseVersion($bitsNeeded, $ecLevel);
if (null !== $forcedVersion) {
// Forced version check
if ($version->getVersionNumber() <= $forcedVersion->getVersionNumber()) {
// Calculated minimum version is same or equal as forced version
$version = $forcedVersion;
} else {
throw new WriterException(
'Invalid version! Calculated version: '
. $version->getVersionNumber()
. ', requested version: '
. $forcedVersion->getVersionNumber()
);
}
}
$headerAndDataBits = new BitArray();
$headerAndDataBits->appendBitArray($headerBits);
// Find "length" of main segment and write it.
$numLetters = (Mode::BYTE() === $mode ? $dataBits->getSizeInBytes() : strlen($content));
self::appendLengthInfo($numLetters, $version, $mode, $headerAndDataBits);
// Put data together into the overall payload.
$headerAndDataBits->appendBitArray($dataBits);
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numDataBytes = $version->getTotalCodewords() - $ecBlocks->getTotalEcCodewords();
// Terminate the bits properly.
self::terminateBits($numDataBytes, $headerAndDataBits);
// Interleave data bits with error correction code.
$finalBits = self::interleaveWithEcBytes(
$headerAndDataBits,
$version->getTotalCodewords(),
$numDataBytes,
$ecBlocks->getNumBlocks()
);
// Choose the mask pattern.
$dimension = $version->getDimensionForVersion();
$matrix = new ByteMatrix($dimension, $dimension);
$maskPattern = self::chooseMaskPattern($finalBits, $ecLevel, $version, $matrix);
// Build the matrix.
MatrixUtil::buildMatrix($finalBits, $ecLevel, $version, $maskPattern, $matrix);
return new QrCode($mode, $ecLevel, $version, $maskPattern, $matrix);
}
/**
* Gets the alphanumeric code for a byte.
*/
private static function getAlphanumericCode(int $code) : int
{
if (isset(self::ALPHANUMERIC_TABLE[$code])) {
return self::ALPHANUMERIC_TABLE[$code];
}
return -1;
}
/**
* Chooses the best mode for a given content.
*/
private static function chooseMode(string $content, ?string $encoding = null) : Mode
{
if (null !== $encoding && 0 === strcasecmp($encoding, 'SHIFT-JIS')) {
return self::isOnlyDoubleByteKanji($content) ? Mode::KANJI() : Mode::BYTE();
}
$hasNumeric = false;
$hasAlphanumeric = false;
$contentLength = strlen($content);
for ($i = 0; $i < $contentLength; ++$i) {
$char = $content[$i];
if (ctype_digit($char)) {
$hasNumeric = true;
} elseif (-1 !== self::getAlphanumericCode(ord($char))) {
$hasAlphanumeric = true;
} else {
return Mode::BYTE();
}
}
if ($hasAlphanumeric) {
return Mode::ALPHANUMERIC();
} elseif ($hasNumeric) {
return Mode::NUMERIC();
}
return Mode::BYTE();
}
/**
* Calculates the mask penalty for a matrix.
*/
private static function calculateMaskPenalty(ByteMatrix $matrix) : int
{
return (
MaskUtil::applyMaskPenaltyRule1($matrix)
+ MaskUtil::applyMaskPenaltyRule2($matrix)
+ MaskUtil::applyMaskPenaltyRule3($matrix)
+ MaskUtil::applyMaskPenaltyRule4($matrix)
);
}
/**
* Checks if content only consists of double-byte kanji characters.
*/
private static function isOnlyDoubleByteKanji(string $content) : bool
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
return false;
}
$length = strlen($bytes);
if (0 !== $length % 2) {
return false;
}
for ($i = 0; $i < $length; $i += 2) {
$byte = ord($bytes[$i]) & 0xff;
if (($byte < 0x81 || $byte > 0x9f) && $byte < 0xe0 || $byte > 0xeb) {
return false;
}
}
return true;
}
/**
* Chooses the best mask pattern for a matrix.
*/
private static function chooseMaskPattern(
BitArray $bits,
ErrorCorrectionLevel $ecLevel,
Version $version,
ByteMatrix $matrix
) : int {
$minPenalty = PHP_INT_MAX;
$bestMaskPattern = -1;
for ($maskPattern = 0; $maskPattern < QrCode::NUM_MASK_PATTERNS; ++$maskPattern) {
MatrixUtil::buildMatrix($bits, $ecLevel, $version, $maskPattern, $matrix);
$penalty = self::calculateMaskPenalty($matrix);
if ($penalty < $minPenalty) {
$minPenalty = $penalty;
$bestMaskPattern = $maskPattern;
}
}
return $bestMaskPattern;
}
/**
* Chooses the best version for the input.
*
* @throws WriterException if data is too big
*/
private static function chooseVersion(int $numInputBits, ErrorCorrectionLevel $ecLevel) : Version
{
for ($versionNum = 1; $versionNum <= 40; ++$versionNum) {
$version = Version::getVersionForNumber($versionNum);
$numBytes = $version->getTotalCodewords();
$ecBlocks = $version->getEcBlocksForLevel($ecLevel);
$numEcBytes = $ecBlocks->getTotalEcCodewords();
$numDataBytes = $numBytes - $numEcBytes;
$totalInputBytes = intdiv($numInputBits + 8, 8);
if ($numDataBytes >= $totalInputBytes) {
return $version;
}
}
throw new WriterException('Data too big');
}
/**
* Terminates the bits in a bit array.
*
* @throws WriterException if data bits cannot fit in the QR code
* @throws WriterException if bits size does not equal the capacity
*/
private static function terminateBits(int $numDataBytes, BitArray $bits) : void
{
$capacity = $numDataBytes << 3;
if ($bits->getSize() > $capacity) {
throw new WriterException('Data bits cannot fit in the QR code');
}
for ($i = 0; $i < 4 && $bits->getSize() < $capacity; ++$i) {
$bits->appendBit(false);
}
$numBitsInLastByte = $bits->getSize() & 0x7;
if ($numBitsInLastByte > 0) {
for ($i = $numBitsInLastByte; $i < 8; ++$i) {
$bits->appendBit(false);
}
}
$numPaddingBytes = $numDataBytes - $bits->getSizeInBytes();
for ($i = 0; $i < $numPaddingBytes; ++$i) {
$bits->appendBits(0 === ($i & 0x1) ? 0xec : 0x11, 8);
}
if ($bits->getSize() !== $capacity) {
throw new WriterException('Bits size does not equal capacity');
}
}
/**
* Gets number of data- and EC bytes for a block ID.
*
* @return int[]
* @throws WriterException if block ID is too large
* @throws WriterException if EC bytes mismatch
* @throws WriterException if RS blocks mismatch
* @throws WriterException if total bytes mismatch
*/
private static function getNumDataBytesAndNumEcBytesForBlockId(
int $numTotalBytes,
int $numDataBytes,
int $numRsBlocks,
int $blockId
) : array {
if ($blockId >= $numRsBlocks) {
throw new WriterException('Block ID too large');
}
$numRsBlocksInGroup2 = $numTotalBytes % $numRsBlocks;
$numRsBlocksInGroup1 = $numRsBlocks - $numRsBlocksInGroup2;
$numTotalBytesInGroup1 = intdiv($numTotalBytes, $numRsBlocks);
$numTotalBytesInGroup2 = $numTotalBytesInGroup1 + 1;
$numDataBytesInGroup1 = intdiv($numDataBytes, $numRsBlocks);
$numDataBytesInGroup2 = $numDataBytesInGroup1 + 1;
$numEcBytesInGroup1 = $numTotalBytesInGroup1 - $numDataBytesInGroup1;
$numEcBytesInGroup2 = $numTotalBytesInGroup2 - $numDataBytesInGroup2;
if ($numEcBytesInGroup1 !== $numEcBytesInGroup2) {
throw new WriterException('EC bytes mismatch');
}
if ($numRsBlocks !== $numRsBlocksInGroup1 + $numRsBlocksInGroup2) {
throw new WriterException('RS blocks mismatch');
}
if ($numTotalBytes !==
(($numDataBytesInGroup1 + $numEcBytesInGroup1) * $numRsBlocksInGroup1)
+ (($numDataBytesInGroup2 + $numEcBytesInGroup2) * $numRsBlocksInGroup2)
) {
throw new WriterException('Total bytes mismatch');
}
if ($blockId < $numRsBlocksInGroup1) {
return [$numDataBytesInGroup1, $numEcBytesInGroup1];
} else {
return [$numDataBytesInGroup2, $numEcBytesInGroup2];
}
}
/**
* Interleaves data with EC bytes.
*
* @throws WriterException if number of bits and data bytes does not match
* @throws WriterException if data bytes does not match offset
* @throws WriterException if an interleaving error occurs
*/
private static function interleaveWithEcBytes(
BitArray $bits,
int $numTotalBytes,
int $numDataBytes,
int $numRsBlocks
) : BitArray {
if ($bits->getSizeInBytes() !== $numDataBytes) {
throw new WriterException('Number of bits and data bytes does not match');
}
$dataBytesOffset = 0;
$maxNumDataBytes = 0;
$maxNumEcBytes = 0;
$blocks = new SplFixedArray($numRsBlocks);
for ($i = 0; $i < $numRsBlocks; ++$i) {
list($numDataBytesInBlock, $numEcBytesInBlock) = self::getNumDataBytesAndNumEcBytesForBlockId(
$numTotalBytes,
$numDataBytes,
$numRsBlocks,
$i
);
$size = $numDataBytesInBlock;
$dataBytes = $bits->toBytes(8 * $dataBytesOffset, $size);
$ecBytes = self::generateEcBytes($dataBytes, $numEcBytesInBlock);
$blocks[$i] = new BlockPair($dataBytes, $ecBytes);
$maxNumDataBytes = max($maxNumDataBytes, $size);
$maxNumEcBytes = max($maxNumEcBytes, count($ecBytes));
$dataBytesOffset += $numDataBytesInBlock;
}
if ($numDataBytes !== $dataBytesOffset) {
throw new WriterException('Data bytes does not match offset');
}
$result = new BitArray();
for ($i = 0; $i < $maxNumDataBytes; ++$i) {
foreach ($blocks as $block) {
$dataBytes = $block->getDataBytes();
if ($i < count($dataBytes)) {
$result->appendBits($dataBytes[$i], 8);
}
}
}
for ($i = 0; $i < $maxNumEcBytes; ++$i) {
foreach ($blocks as $block) {
$ecBytes = $block->getErrorCorrectionBytes();
if ($i < count($ecBytes)) {
$result->appendBits($ecBytes[$i], 8);
}
}
}
if ($numTotalBytes !== $result->getSizeInBytes()) {
throw new WriterException(
'Interleaving error: ' . $numTotalBytes . ' and ' . $result->getSizeInBytes() . ' differ'
);
}
return $result;
}
/**
* Generates EC bytes for given data.
*
* @param SplFixedArray<int> $dataBytes
* @return SplFixedArray<int>
*/
private static function generateEcBytes(SplFixedArray $dataBytes, int $numEcBytesInBlock) : SplFixedArray
{
$numDataBytes = count($dataBytes);
$toEncode = new SplFixedArray($numDataBytes + $numEcBytesInBlock);
for ($i = 0; $i < $numDataBytes; $i++) {
$toEncode[$i] = $dataBytes[$i] & 0xff;
}
$ecBytes = new SplFixedArray($numEcBytesInBlock);
$codec = self::getCodec($numDataBytes, $numEcBytesInBlock);
$codec->encode($toEncode, $ecBytes);
return $ecBytes;
}
/**
* Gets an RS codec and caches it.
*/
private static function getCodec(int $numDataBytes, int $numEcBytesInBlock) : ReedSolomonCodec
{
$cacheId = $numDataBytes . '-' . $numEcBytesInBlock;
if (isset(self::$codecs[$cacheId])) {
return self::$codecs[$cacheId];
}
return self::$codecs[$cacheId] = new ReedSolomonCodec(
8,
0x11d,
0,
1,
$numEcBytesInBlock,
255 - $numDataBytes - $numEcBytesInBlock
);
}
/**
* Appends mode information to a bit array.
*/
private static function appendModeInfo(Mode $mode, BitArray $bits) : void
{
$bits->appendBits($mode->getBits(), 4);
}
/**
* Appends length information to a bit array.
*
* @throws WriterException if num letters is bigger than expected
*/
private static function appendLengthInfo(int $numLetters, Version $version, Mode $mode, BitArray $bits) : void
{
$numBits = $mode->getCharacterCountBits($version);
if ($numLetters >= (1 << $numBits)) {
throw new WriterException($numLetters . ' is bigger than ' . ((1 << $numBits) - 1));
}
$bits->appendBits($numLetters, $numBits);
}
/**
* Appends bytes to a bit array in a specific mode.
*
* @throws WriterException if an invalid mode was supplied
*/
private static function appendBytes(string $content, Mode $mode, BitArray $bits, string $encoding) : void
{
switch ($mode) {
case Mode::NUMERIC():
self::appendNumericBytes($content, $bits);
break;
case Mode::ALPHANUMERIC():
self::appendAlphanumericBytes($content, $bits);
break;
case Mode::BYTE():
self::append8BitBytes($content, $bits, $encoding);
break;
case Mode::KANJI():
self::appendKanjiBytes($content, $bits);
break;
default:
throw new WriterException('Invalid mode: ' . $mode);
}
}
/**
* Appends numeric bytes to a bit array.
*/
private static function appendNumericBytes(string $content, BitArray $bits) : void
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$num1 = (int) $content[$i];
if ($i + 2 < $length) {
// Encode three numeric letters in ten bits.
$num2 = (int) $content[$i + 1];
$num3 = (int) $content[$i + 2];
$bits->appendBits($num1 * 100 + $num2 * 10 + $num3, 10);
$i += 3;
} elseif ($i + 1 < $length) {
// Encode two numeric letters in seven bits.
$num2 = (int) $content[$i + 1];
$bits->appendBits($num1 * 10 + $num2, 7);
$i += 2;
} else {
// Encode one numeric letter in four bits.
$bits->appendBits($num1, 4);
++$i;
}
}
}
/**
* Appends alpha-numeric bytes to a bit array.
*
* @throws WriterException if an invalid alphanumeric code was found
*/
private static function appendAlphanumericBytes(string $content, BitArray $bits) : void
{
$length = strlen($content);
$i = 0;
while ($i < $length) {
$code1 = self::getAlphanumericCode(ord($content[$i]));
if (-1 === $code1) {
throw new WriterException('Invalid alphanumeric code');
}
if ($i + 1 < $length) {
$code2 = self::getAlphanumericCode(ord($content[$i + 1]));
if (-1 === $code2) {
throw new WriterException('Invalid alphanumeric code');
}
// Encode two alphanumeric letters in 11 bits.
$bits->appendBits($code1 * 45 + $code2, 11);
$i += 2;
} else {
// Encode one alphanumeric letter in six bits.
$bits->appendBits($code1, 6);
++$i;
}
}
}
/**
* Appends regular 8-bit bytes to a bit array.
*
* @throws WriterException if content cannot be encoded to target encoding
*/
private static function append8BitBytes(string $content, BitArray $bits, string $encoding) : void
{
$bytes = @iconv('utf-8', $encoding, $content);
if (false === $bytes) {
throw new WriterException('Could not encode content to ' . $encoding);
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i++) {
$bits->appendBits(ord($bytes[$i]), 8);
}
}
/**
* Appends KANJI bytes to a bit array.
*
* @throws WriterException if content does not seem to be encoded in SHIFT-JIS
* @throws WriterException if an invalid byte sequence occurs
*/
private static function appendKanjiBytes(string $content, BitArray $bits) : void
{
$bytes = @iconv('utf-8', 'SHIFT-JIS', $content);
if (false === $bytes) {
throw new WriterException('Content could not be converted to SHIFT-JIS');
}
if (strlen($bytes) % 2 > 0) {
// We just do a simple length check here. The for loop will check
// individual characters.
throw new WriterException('Content does not seem to be encoded in SHIFT-JIS');
}
$length = strlen($bytes);
for ($i = 0; $i < $length; $i += 2) {
$byte1 = ord($bytes[$i]) & 0xff;
$byte2 = ord($bytes[$i + 1]) & 0xff;
$code = ($byte1 << 8) | $byte2;
if ($code >= 0x8140 && $code <= 0x9ffc) {
$subtracted = $code - 0x8140;
} elseif ($code >= 0xe040 && $code <= 0xebbf) {
$subtracted = $code - 0xc140;
} else {
throw new WriterException('Invalid byte sequence');
}
$encoded = (($subtracted >> 8) * 0xc0) + ($subtracted & 0xff);
$bits->appendBits($encoded, 13);
}
}
/**
* Appends ECI information to a bit array.
*/
private static function appendEci(CharacterSetEci $eci, BitArray $bits) : void
{
$mode = Mode::ECI();
$bits->appendBits($mode->getBits(), 4);
$bits->appendBits($eci->getValue(), 8);
}
}

View file

@ -0,0 +1,271 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitUtils;
use BaconQrCode\Exception\InvalidArgumentException;
/**
* Mask utility.
*/
final class MaskUtil
{
/**#@+
* Penalty weights from section 6.8.2.1
*/
public const N1 = 3;
public const N2 = 3;
public const N3 = 40;
public const N4 = 10;
/**#@-*/
private function __construct()
{
}
/**
* Applies mask penalty rule 1 and returns the penalty.
*
* Finds repetitive cells with the same color and gives penalty to them.
* Example: 00000 or 11111.
*/
public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
{
return (
self::applyMaskPenaltyRule1Internal($matrix, true)
+ self::applyMaskPenaltyRule1Internal($matrix, false)
);
}
/**
* Applies mask penalty rule 2 and returns the penalty.
*
* Finds 2x2 blocks with the same color and gives penalty to them. This is
* actually equivalent to the spec's rule, which is to find MxN blocks and
* give a penalty proportional to (M-1)x(N-1), because this is the number of
* 2x2 blocks inside such a block.
*/
public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height - 1; ++$y) {
for ($x = 0; $x < $width - 1; ++$x) {
$value = $array[$y][$x];
if ($value === $array[$y][$x + 1]
&& $value === $array[$y + 1][$x]
&& $value === $array[$y + 1][$x + 1]
) {
++$penalty;
}
}
}
return self::N2 * $penalty;
}
/**
* Applies mask penalty rule 3 and returns the penalty.
*
* Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
* to them. If we find patterns like 000010111010000, we give penalties
* twice (i.e. 40 * 2).
*/
public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
{
$penalty = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
if ($x + 6 < $width
&& 1 === $array[$y][$x]
&& 0 === $array[$y][$x + 1]
&& 1 === $array[$y][$x + 2]
&& 1 === $array[$y][$x + 3]
&& 1 === $array[$y][$x + 4]
&& 0 === $array[$y][$x + 5]
&& 1 === $array[$y][$x + 6]
&& (
(
$x + 10 < $width
&& 0 === $array[$y][$x + 7]
&& 0 === $array[$y][$x + 8]
&& 0 === $array[$y][$x + 9]
&& 0 === $array[$y][$x + 10]
)
|| (
$x - 4 >= 0
&& 0 === $array[$y][$x - 1]
&& 0 === $array[$y][$x - 2]
&& 0 === $array[$y][$x - 3]
&& 0 === $array[$y][$x - 4]
)
)
) {
$penalty += self::N3;
}
if ($y + 6 < $height
&& 1 === $array[$y][$x]
&& 0 === $array[$y + 1][$x]
&& 1 === $array[$y + 2][$x]
&& 1 === $array[$y + 3][$x]
&& 1 === $array[$y + 4][$x]
&& 0 === $array[$y + 5][$x]
&& 1 === $array[$y + 6][$x]
&& (
(
$y + 10 < $height
&& 0 === $array[$y + 7][$x]
&& 0 === $array[$y + 8][$x]
&& 0 === $array[$y + 9][$x]
&& 0 === $array[$y + 10][$x]
)
|| (
$y - 4 >= 0
&& 0 === $array[$y - 1][$x]
&& 0 === $array[$y - 2][$x]
&& 0 === $array[$y - 3][$x]
&& 0 === $array[$y - 4][$x]
)
)
) {
$penalty += self::N3;
}
}
}
return $penalty;
}
/**
* Applies mask penalty rule 4 and returns the penalty.
*
* Calculates the ratio of dark cells and gives penalty if the ratio is far
* from 50%. It gives 10 penalty for 5% distance.
*/
public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
{
$numDarkCells = 0;
$array = $matrix->getArray();
$width = $matrix->getWidth();
$height = $matrix->getHeight();
for ($y = 0; $y < $height; ++$y) {
$arrayY = $array[$y];
for ($x = 0; $x < $width; ++$x) {
if (1 === $arrayY[$x]) {
++$numDarkCells;
}
}
}
$numTotalCells = $height * $width;
$darkRatio = $numDarkCells / $numTotalCells;
$fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20);
return $fixedPercentVariances * self::N4;
}
/**
* Returns the mask bit for "getMaskPattern" at "x" and "y".
*
* See 8.8 of JISX0510:2004 for mask pattern conditions.
*
* @throws InvalidArgumentException if an invalid mask pattern was supplied
*/
public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
{
switch ($maskPattern) {
case 0:
$intermediate = ($y + $x) & 0x1;
break;
case 1:
$intermediate = $y & 0x1;
break;
case 2:
$intermediate = $x % 3;
break;
case 3:
$intermediate = ($y + $x) % 3;
break;
case 4:
$intermediate = (BitUtils::unsignedRightShift($y, 1) + (int) ($x / 3)) & 0x1;
break;
case 5:
$temp = $y * $x;
$intermediate = ($temp & 0x1) + ($temp % 3);
break;
case 6:
$temp = $y * $x;
$intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
break;
case 7:
$temp = $y * $x;
$intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
break;
default:
throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
}
return 0 == $intermediate;
}
/**
* Helper function for applyMaskPenaltyRule1.
*
* We need this for doing this calculation in both vertical and horizontal
* orders respectively.
*/
private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
{
$penalty = 0;
$iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
$jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
$array = $matrix->getArray();
for ($i = 0; $i < $iLimit; ++$i) {
$numSameBitCells = 0;
$prevBit = -1;
for ($j = 0; $j < $jLimit; $j++) {
$bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
if ($bit === $prevBit) {
++$numSameBitCells;
} else {
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
$numSameBitCells = 1;
$prevBit = $bit;
}
}
if ($numSameBitCells >= 5) {
$penalty += self::N1 + ($numSameBitCells - 5);
}
}
return $penalty;
}
}

View file

@ -0,0 +1,513 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Exception\WriterException;
/**
* Matrix utility.
*/
final class MatrixUtil
{
/**
* Position detection pattern.
*/
private const POSITION_DETECTION_PATTERN = [
[1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1],
];
/**
* Position adjustment pattern.
*/
private const POSITION_ADJUSTMENT_PATTERN = [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 1, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1],
];
/**
* Coordinates for position adjustment patterns for each version.
*/
private const POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = [
[null, null, null, null, null, null, null], // Version 1
[ 6, 18, null, null, null, null, null], // Version 2
[ 6, 22, null, null, null, null, null], // Version 3
[ 6, 26, null, null, null, null, null], // Version 4
[ 6, 30, null, null, null, null, null], // Version 5
[ 6, 34, null, null, null, null, null], // Version 6
[ 6, 22, 38, null, null, null, null], // Version 7
[ 6, 24, 42, null, null, null, null], // Version 8
[ 6, 26, 46, null, null, null, null], // Version 9
[ 6, 28, 50, null, null, null, null], // Version 10
[ 6, 30, 54, null, null, null, null], // Version 11
[ 6, 32, 58, null, null, null, null], // Version 12
[ 6, 34, 62, null, null, null, null], // Version 13
[ 6, 26, 46, 66, null, null, null], // Version 14
[ 6, 26, 48, 70, null, null, null], // Version 15
[ 6, 26, 50, 74, null, null, null], // Version 16
[ 6, 30, 54, 78, null, null, null], // Version 17
[ 6, 30, 56, 82, null, null, null], // Version 18
[ 6, 30, 58, 86, null, null, null], // Version 19
[ 6, 34, 62, 90, null, null, null], // Version 20
[ 6, 28, 50, 72, 94, null, null], // Version 21
[ 6, 26, 50, 74, 98, null, null], // Version 22
[ 6, 30, 54, 78, 102, null, null], // Version 23
[ 6, 28, 54, 80, 106, null, null], // Version 24
[ 6, 32, 58, 84, 110, null, null], // Version 25
[ 6, 30, 58, 86, 114, null, null], // Version 26
[ 6, 34, 62, 90, 118, null, null], // Version 27
[ 6, 26, 50, 74, 98, 122, null], // Version 28
[ 6, 30, 54, 78, 102, 126, null], // Version 29
[ 6, 26, 52, 78, 104, 130, null], // Version 30
[ 6, 30, 56, 82, 108, 134, null], // Version 31
[ 6, 34, 60, 86, 112, 138, null], // Version 32
[ 6, 30, 58, 86, 114, 142, null], // Version 33
[ 6, 34, 62, 90, 118, 146, null], // Version 34
[ 6, 30, 54, 78, 102, 126, 150], // Version 35
[ 6, 24, 50, 76, 102, 128, 154], // Version 36
[ 6, 28, 54, 80, 106, 132, 158], // Version 37
[ 6, 32, 58, 84, 110, 136, 162], // Version 38
[ 6, 26, 54, 82, 110, 138, 166], // Version 39
[ 6, 30, 58, 86, 114, 142, 170], // Version 40
];
/**
* Type information coordinates.
*/
private const TYPE_INFO_COORDINATES = [
[8, 0],
[8, 1],
[8, 2],
[8, 3],
[8, 4],
[8, 5],
[8, 7],
[8, 8],
[7, 8],
[5, 8],
[4, 8],
[3, 8],
[2, 8],
[1, 8],
[0, 8],
];
/**
* Version information polynomial.
*/
private const VERSION_INFO_POLY = 0x1f25;
/**
* Type information polynomial.
*/
private const TYPE_INFO_POLY = 0x537;
/**
* Type information mask pattern.
*/
private const TYPE_INFO_MASK_PATTERN = 0x5412;
/**
* Clears a given matrix.
*/
public static function clearMatrix(ByteMatrix $matrix) : void
{
$matrix->clear(-1);
}
/**
* Builds a complete matrix.
*/
public static function buildMatrix(
BitArray $dataBits,
ErrorCorrectionLevel $level,
Version $version,
int $maskPattern,
ByteMatrix $matrix
) : void {
self::clearMatrix($matrix);
self::embedBasicPatterns($version, $matrix);
self::embedTypeInfo($level, $maskPattern, $matrix);
self::maybeEmbedVersionInfo($version, $matrix);
self::embedDataBits($dataBits, $maskPattern, $matrix);
}
/**
* Removes the position detection patterns from a matrix.
*
* This can be useful if you need to render those patterns separately.
*/
public static function removePositionDetectionPatterns(ByteMatrix $matrix) : void
{
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
self::removePositionDetectionPattern(0, 0, $matrix);
self::removePositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::removePositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
}
/**
* Embeds type information into a matrix.
*/
private static function embedTypeInfo(ErrorCorrectionLevel $level, int $maskPattern, ByteMatrix $matrix) : void
{
$typeInfoBits = new BitArray();
self::makeTypeInfoBits($level, $maskPattern, $typeInfoBits);
$typeInfoBitsSize = $typeInfoBits->getSize();
for ($i = 0; $i < $typeInfoBitsSize; ++$i) {
$bit = $typeInfoBits->get($typeInfoBitsSize - 1 - $i);
$x1 = self::TYPE_INFO_COORDINATES[$i][0];
$y1 = self::TYPE_INFO_COORDINATES[$i][1];
$matrix->set($x1, $y1, (int) $bit);
if ($i < 8) {
$x2 = $matrix->getWidth() - $i - 1;
$y2 = 8;
} else {
$x2 = 8;
$y2 = $matrix->getHeight() - 7 + ($i - 8);
}
$matrix->set($x2, $y2, (int) $bit);
}
}
/**
* Generates type information bits and appends them to a bit array.
*
* @throws RuntimeException if bit array resulted in invalid size
*/
private static function makeTypeInfoBits(ErrorCorrectionLevel $level, int $maskPattern, BitArray $bits) : void
{
$typeInfo = ($level->getBits() << 3) | $maskPattern;
$bits->appendBits($typeInfo, 5);
$bchCode = self::calculateBchCode($typeInfo, self::TYPE_INFO_POLY);
$bits->appendBits($bchCode, 10);
$maskBits = new BitArray();
$maskBits->appendBits(self::TYPE_INFO_MASK_PATTERN, 15);
$bits->xorBits($maskBits);
if (15 !== $bits->getSize()) {
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Embeds version information if required.
*/
private static function maybeEmbedVersionInfo(Version $version, ByteMatrix $matrix) : void
{
if ($version->getVersionNumber() < 7) {
return;
}
$versionInfoBits = new BitArray();
self::makeVersionInfoBits($version, $versionInfoBits);
$bitIndex = 6 * 3 - 1;
for ($i = 0; $i < 6; ++$i) {
for ($j = 0; $j < 3; ++$j) {
$bit = $versionInfoBits->get($bitIndex);
--$bitIndex;
$matrix->set($i, $matrix->getHeight() - 11 + $j, (int) $bit);
$matrix->set($matrix->getHeight() - 11 + $j, $i, (int) $bit);
}
}
}
/**
* Generates version information bits and appends them to a bit array.
*
* @throws RuntimeException if bit array resulted in invalid size
*/
private static function makeVersionInfoBits(Version $version, BitArray $bits) : void
{
$bits->appendBits($version->getVersionNumber(), 6);
$bchCode = self::calculateBchCode($version->getVersionNumber(), self::VERSION_INFO_POLY);
$bits->appendBits($bchCode, 12);
if (18 !== $bits->getSize()) {
throw new RuntimeException('Bit array resulted in invalid size: ' . $bits->getSize());
}
}
/**
* Calculates the BCH code for a value and a polynomial.
*/
private static function calculateBchCode(int $value, int $poly) : int
{
$msbSetInPoly = self::findMsbSet($poly);
$value <<= $msbSetInPoly - 1;
while (self::findMsbSet($value) >= $msbSetInPoly) {
$value ^= $poly << (self::findMsbSet($value) - $msbSetInPoly);
}
return $value;
}
/**
* Finds and MSB set.
*/
private static function findMsbSet(int $value) : int
{
$numDigits = 0;
while (0 !== $value) {
$value >>= 1;
++$numDigits;
}
return $numDigits;
}
/**
* Embeds basic patterns into a matrix.
*/
private static function embedBasicPatterns(Version $version, ByteMatrix $matrix) : void
{
self::embedPositionDetectionPatternsAndSeparators($matrix);
self::embedDarkDotAtLeftBottomCorner($matrix);
self::maybeEmbedPositionAdjustmentPatterns($version, $matrix);
self::embedTimingPatterns($matrix);
}
/**
* Embeds position detection patterns and separators into a byte matrix.
*/
private static function embedPositionDetectionPatternsAndSeparators(ByteMatrix $matrix) : void
{
$pdpWidth = count(self::POSITION_DETECTION_PATTERN[0]);
self::embedPositionDetectionPattern(0, 0, $matrix);
self::embedPositionDetectionPattern($matrix->getWidth() - $pdpWidth, 0, $matrix);
self::embedPositionDetectionPattern(0, $matrix->getWidth() - $pdpWidth, $matrix);
$hspWidth = 8;
self::embedHorizontalSeparationPattern(0, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern($matrix->getWidth() - $hspWidth, $hspWidth - 1, $matrix);
self::embedHorizontalSeparationPattern(0, $matrix->getWidth() - $hspWidth, $matrix);
$vspSize = 7;
self::embedVerticalSeparationPattern($vspSize, 0, $matrix);
self::embedVerticalSeparationPattern($matrix->getHeight() - $vspSize - 1, 0, $matrix);
self::embedVerticalSeparationPattern($vspSize, $matrix->getHeight() - $vspSize, $matrix);
}
/**
* Embeds a single position detection pattern into a byte matrix.
*/
private static function embedPositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; ++$y) {
for ($x = 0; $x < 7; ++$x) {
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_DETECTION_PATTERN[$y][$x]);
}
}
}
private static function removePositionDetectionPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; ++$y) {
for ($x = 0; $x < 7; ++$x) {
$matrix->set($xStart + $x, $yStart + $y, 0);
}
}
}
/**
* Embeds a single horizontal separation pattern.
*
* @throws RuntimeException if a byte was already set
*/
private static function embedHorizontalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($x = 0; $x < 8; $x++) {
if (-1 !== $matrix->get($xStart + $x, $yStart)) {
throw new RuntimeException('Byte already set');
}
$matrix->set($xStart + $x, $yStart, 0);
}
}
/**
* Embeds a single vertical separation pattern.
*
* @throws RuntimeException if a byte was already set
*/
private static function embedVerticalSeparationPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 7; $y++) {
if (-1 !== $matrix->get($xStart, $yStart + $y)) {
throw new RuntimeException('Byte already set');
}
$matrix->set($xStart, $yStart + $y, 0);
}
}
/**
* Embeds a dot at the left bottom corner.
*
* @throws RuntimeException if a byte was already set to 0
*/
private static function embedDarkDotAtLeftBottomCorner(ByteMatrix $matrix) : void
{
if (0 === $matrix->get(8, $matrix->getHeight() - 8)) {
throw new RuntimeException('Byte already set to 0');
}
$matrix->set(8, $matrix->getHeight() - 8, 1);
}
/**
* Embeds position adjustment patterns if required.
*/
private static function maybeEmbedPositionAdjustmentPatterns(Version $version, ByteMatrix $matrix) : void
{
if ($version->getVersionNumber() < 2) {
return;
}
$index = $version->getVersionNumber() - 1;
$coordinates = self::POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[$index];
$numCoordinates = count($coordinates);
for ($i = 0; $i < $numCoordinates; ++$i) {
for ($j = 0; $j < $numCoordinates; ++$j) {
$y = $coordinates[$i];
$x = $coordinates[$j];
if (null === $x || null === $y) {
continue;
}
if (-1 === $matrix->get($x, $y)) {
self::embedPositionAdjustmentPattern($x - 2, $y - 2, $matrix);
}
}
}
}
/**
* Embeds a single position adjustment pattern.
*/
private static function embedPositionAdjustmentPattern(int $xStart, int $yStart, ByteMatrix $matrix) : void
{
for ($y = 0; $y < 5; $y++) {
for ($x = 0; $x < 5; $x++) {
$matrix->set($xStart + $x, $yStart + $y, self::POSITION_ADJUSTMENT_PATTERN[$y][$x]);
}
}
}
/**
* Embeds timing patterns into a matrix.
*/
private static function embedTimingPatterns(ByteMatrix $matrix) : void
{
$matrixWidth = $matrix->getWidth();
for ($i = 8; $i < $matrixWidth - 8; ++$i) {
$bit = ($i + 1) % 2;
if (-1 === $matrix->get($i, 6)) {
$matrix->set($i, 6, $bit);
}
if (-1 === $matrix->get(6, $i)) {
$matrix->set(6, $i, $bit);
}
}
}
/**
* Embeds "dataBits" using "getMaskPattern".
*
* For debugging purposes, it skips masking process if "getMaskPattern" is -1. See 8.7 of JISX0510:2004 (p.38) for
* how to embed data bits.
*
* @throws WriterException if not all bits could be consumed
*/
private static function embedDataBits(BitArray $dataBits, int $maskPattern, ByteMatrix $matrix) : void
{
$bitIndex = 0;
$direction = -1;
// Start from the right bottom cell.
$x = $matrix->getWidth() - 1;
$y = $matrix->getHeight() - 1;
while ($x > 0) {
// Skip vertical timing pattern.
if (6 === $x) {
--$x;
}
while ($y >= 0 && $y < $matrix->getHeight()) {
for ($i = 0; $i < 2; $i++) {
$xx = $x - $i;
// Skip the cell if it's not empty.
if (-1 !== $matrix->get($xx, $y)) {
continue;
}
if ($bitIndex < $dataBits->getSize()) {
$bit = $dataBits->get($bitIndex);
++$bitIndex;
} else {
// Padding bit. If there is no bit left, we'll fill the
// left cells with 0, as described in 8.4.9 of
// JISX0510:2004 (p. 24).
$bit = false;
}
// Skip masking if maskPattern is -1.
if (-1 !== $maskPattern && MaskUtil::getDataMaskBit($maskPattern, $xx, $y)) {
$bit = ! $bit;
}
$matrix->set($xx, $y, (int) $bit);
}
$y += $direction;
}
$direction = -$direction;
$y += $direction;
$x -= 2;
}
// All bits should be consumed
if ($dataBits->getSize() !== $bitIndex) {
throw new WriterException('Not all bits consumed (' . $bitIndex . ' out of ' . $dataBits->getSize() .')');
}
}
}

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\Version;
/**
* QR code.
*/
final class QrCode
{
/**
* Number of possible mask patterns.
*/
public const NUM_MASK_PATTERNS = 8;
/**
* Mask pattern of the QR code.
*/
private int $maskPattern = -1;
/**
* Matrix of the QR code.
*/
private ByteMatrix $matrix;
public function __construct(
private readonly Mode $mode,
private readonly ErrorCorrectionLevel $errorCorrectionLevel,
private readonly Version $version,
int $maskPattern,
ByteMatrix $matrix
) {
$this->maskPattern = $maskPattern;
$this->matrix = $matrix;
}
/**
* Gets the mode.
*/
public function getMode() : Mode
{
return $this->mode;
}
/**
* Gets the EC level.
*/
public function getErrorCorrectionLevel() : ErrorCorrectionLevel
{
return $this->errorCorrectionLevel;
}
/**
* Gets the version.
*/
public function getVersion() : Version
{
return $this->version;
}
/**
* Gets the mask pattern.
*/
public function getMaskPattern() : int
{
return $this->maskPattern;
}
public function getMatrix(): ByteMatrix
{
return $this->matrix;
}
/**
* Validates whether a mask pattern is valid.
*/
public static function isValidMaskPattern(int $maskPattern) : bool
{
return $maskPattern > 0 && $maskPattern < self::NUM_MASK_PATTERNS;
}
/**
* Returns a string representation of the QR code.
*/
public function __toString() : string
{
$result = "<<\n"
. ' mode: ' . $this->mode . "\n"
. ' ecLevel: ' . $this->errorCorrectionLevel . "\n"
. ' version: ' . $this->version . "\n"
. ' maskPattern: ' . $this->maskPattern . "\n";
if ($this->matrix === null) {
$result .= " matrix: null\n";
} else {
$result .= " matrix:\n";
$result .= $this->matrix;
}
$result .= ">>\n";
return $result;
}
}

View file

@ -0,0 +1,10 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface
{
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}

View file

@ -0,0 +1,8 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Exception;
final class WriterException extends \RuntimeException implements ExceptionInterface
{
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Alpha implements ColorInterface
{
/**
* @param int $alpha the alpha value, 0 to 100
*/
public function __construct(private readonly int $alpha, private readonly ColorInterface $baseColor)
{
if ($alpha < 0 || $alpha > 100) {
throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
}
}
public function getAlpha() : int
{
return $this->alpha;
}
public function getBaseColor() : ColorInterface
{
return $this->baseColor;
}
public function toRgb() : Rgb
{
return $this->baseColor->toRgb();
}
public function toCmyk() : Cmyk
{
return $this->baseColor->toCmyk();
}
public function toGray() : Gray
{
return $this->baseColor->toGray();
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Cmyk implements ColorInterface
{
/**
* @param int $cyan the cyan amount, 0 to 100
* @param int $magenta the magenta amount, 0 to 100
* @param int $yellow the yellow amount, 0 to 100
* @param int $black the black amount, 0 to 100
*/
public function __construct(
private readonly int $cyan,
private readonly int $magenta,
private readonly int $yellow,
private readonly int $black
) {
if ($cyan < 0 || $cyan > 100) {
throw new Exception\InvalidArgumentException('Cyan must be between 0 and 100');
}
if ($magenta < 0 || $magenta > 100) {
throw new Exception\InvalidArgumentException('Magenta must be between 0 and 100');
}
if ($yellow < 0 || $yellow > 100) {
throw new Exception\InvalidArgumentException('Yellow must be between 0 and 100');
}
if ($black < 0 || $black > 100) {
throw new Exception\InvalidArgumentException('Black must be between 0 and 100');
}
}
public function getCyan() : int
{
return $this->cyan;
}
public function getMagenta() : int
{
return $this->magenta;
}
public function getYellow() : int
{
return $this->yellow;
}
public function getBlack() : int
{
return $this->black;
}
public function toRgb() : Rgb
{
$k = $this->black / 100;
$c = (-$k * $this->cyan + $k * 100 + $this->cyan) / 100;
$m = (-$k * $this->magenta + $k * 100 + $this->magenta) / 100;
$y = (-$k * $this->yellow + $k * 100 + $this->yellow) / 100;
return new Rgb(
(int) (-$c * 255 + 255),
(int) (-$m * 255 + 255),
(int) (-$y * 255 + 255)
);
}
public function toCmyk() : Cmyk
{
return $this;
}
public function toGray() : Gray
{
return $this->toRgb()->toGray();
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
interface ColorInterface
{
/**
* Converts the color to RGB.
*/
public function toRgb() : Rgb;
/**
* Converts the color to CMYK.
*/
public function toCmyk() : Cmyk;
/**
* Converts the color to gray.
*/
public function toGray() : Gray;
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Gray implements ColorInterface
{
/**
* @param int $gray the gray value between 0 (black) and 100 (white)
*/
public function __construct(private readonly int $gray)
{
if ($gray < 0 || $gray > 100) {
throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
}
}
public function getGray() : int
{
return $this->gray;
}
public function toRgb() : Rgb
{
return new Rgb((int) ($this->gray * 2.55), (int) ($this->gray * 2.55), (int) ($this->gray * 2.55));
}
public function toCmyk() : Cmyk
{
return new Cmyk(0, 0, 0, 100 - $this->gray);
}
public function toGray() : Gray
{
return $this;
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Rgb implements ColorInterface
{
/**
* @param int $red the red amount of the color, 0 to 255
* @param int $green the green amount of the color, 0 to 255
* @param int $blue the blue amount of the color, 0 to 255
*/
public function __construct(private readonly int $red, private readonly int $green, private readonly int $blue)
{
if ($red < 0 || $red > 255) {
throw new Exception\InvalidArgumentException('Red must be between 0 and 255');
}
if ($green < 0 || $green > 255) {
throw new Exception\InvalidArgumentException('Green must be between 0 and 255');
}
if ($blue < 0 || $blue > 255) {
throw new Exception\InvalidArgumentException('Blue must be between 0 and 255');
}
}
public function getRed() : int
{
return $this->red;
}
public function getGreen() : int
{
return $this->green;
}
public function getBlue() : int
{
return $this->blue;
}
public function toRgb() : Rgb
{
return $this;
}
public function toCmyk() : Cmyk
{
$c = 1 - ($this->red / 255);
$m = 1 - ($this->green / 255);
$y = 1 - ($this->blue / 255);
$k = min($c, $m, $y);
if ($k === 0) {
return new Cmyk(0, 0, 0, 0);
}
return new Cmyk(
(int) (100 * ($c - $k) / (1 - $k)),
(int) (100 * ($m - $k) / (1 - $k)),
(int) (100 * ($y - $k) / (1 - $k)),
(int) (100 * $k)
);
}
public function toGray() : Gray
{
return new Gray((int) (($this->red * 0.21 + $this->green * 0.71 + $this->blue * 0.07) / 2.55));
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Combines the style of two different eyes.
*/
final class CompositeEye implements EyeInterface
{
public function __construct(private readonly EyeInterface $externalEye, private readonly EyeInterface $internalEye)
{
}
public function getExternalPath() : Path
{
return $this->externalEye->getExternalPath();
}
public function getInternalPath() : Path
{
return $this->internalEye->getInternalPath();
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Interface for describing the look of an eye.
*/
interface EyeInterface
{
/**
* Returns the path of the external eye element.
*
* The path origin point (0, 0) must be anchored at the middle of the path.
*/
public function getExternalPath() : Path;
/**
* Returns the path of the internal eye element.
*
* The path origin point (0, 0) must be anchored at the middle of the path.
*/
public function getInternalPath() : Path;
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Module\ModuleInterface;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders an eye based on a module renderer.
*/
final class ModuleEye implements EyeInterface
{
public function __construct(private readonly ModuleInterface $module)
{
}
public function getExternalPath() : Path
{
$matrix = new ByteMatrix(7, 7);
for ($x = 0; $x < 7; ++$x) {
$matrix->set($x, 0, 1);
$matrix->set($x, 6, 1);
}
for ($y = 1; $y < 6; ++$y) {
$matrix->set(0, $y, 1);
$matrix->set(6, $y, 1);
}
return $this->module->createPath($matrix)->translate(-3.5, -3.5);
}
public function getInternalPath() : Path
{
$matrix = new ByteMatrix(3, 3);
for ($x = 0; $x < 3; ++$x) {
for ($y = 0; $y < 3; ++$y) {
$matrix->set($x, $y, 1);
}
}
return $this->module->createPath($matrix)->translate(-1.5, -1.5);
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the outer eye as solid with a curved corner and inner eye as a circle.
*/
final class PointyEye implements EyeInterface
{
/**
* @var self|null
*/
private static $instance;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, 3.5)
->line(-3.5, 0)
->ellipticArc(3.5, 3.5, 0, false, true, 0, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->close()
->move(2.5, 0)
->ellipticArc(2.5, 2.5, 0, false, true, 0, 2.5)
->ellipticArc(2.5, 2.5, 0, false, true, -2.5, 0)
->ellipticArc(2.5, 2.5, 0, false, true, 0, -2.5)
->ellipticArc(2.5, 2.5, 0, false, true, 2.5, 0)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(1.5, 0)
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
->close()
;
}
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the inner eye as a circle.
*/
final class SimpleCircleEye implements EyeInterface
{
private static ?SimpleCircleEye $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->line(-3.5, 3.5)
->close()
->move(-2.5, -2.5)
->line(-2.5, 2.5)
->line(2.5, 2.5)
->line(2.5, -2.5)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(1.5, 0)
->ellipticArc(1.5, 1.5, 0., false, true, 0., 1.5)
->ellipticArc(1.5, 1.5, 0., false, true, -1.5, 0.)
->ellipticArc(1.5, 1.5, 0., false, true, 0., -1.5)
->ellipticArc(1.5, 1.5, 0., false, true, 1.5, 0.)
->close()
;
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Eye;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders the eyes in their default square shape.
*/
final class SquareEye implements EyeInterface
{
private static ?SquareEye $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function getExternalPath() : Path
{
return (new Path())
->move(-3.5, -3.5)
->line(3.5, -3.5)
->line(3.5, 3.5)
->line(-3.5, 3.5)
->close()
->move(-2.5, -2.5)
->line(-2.5, 2.5)
->line(2.5, 2.5)
->line(2.5, -2.5)
->close()
;
}
public function getInternalPath() : Path
{
return (new Path())
->move(-1.5, -1.5)
->line(1.5, -1.5)
->line(1.5, 1.5)
->line(-1.5, 1.5)
->close()
;
}
}

View file

@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Encoder\MatrixUtil;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\Fill;
use GdImage;
final class GDLibRenderer implements RendererInterface
{
private ?GdImage $image;
/**
* @var array<string, int>
*/
private array $colors;
public function __construct(
private int $size,
private int $margin = 4,
private string $imageFormat = 'png',
private int $compressionQuality = 9,
private ?Fill $fill = null
) {
if (! extension_loaded('gd') || ! function_exists('gd_info')) {
throw new RuntimeException('You need to install the GD extension to use this back end');
}
if ($this->fill === null) {
$this->fill = Fill::default();
}
if ($this->fill->hasGradientFill()) {
throw new InvalidArgumentException('GDLibRenderer does not support gradients');
}
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode): string
{
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
MatrixUtil::removePositionDetectionPatterns($matrix);
$this->newImage();
$this->draw($matrix);
return $this->renderImage();
}
private function newImage(): void
{
$img = imagecreatetruecolor($this->size, $this->size);
if ($img === false) {
throw new RuntimeException('Failed to create image of that size');
}
$this->image = $img;
imagealphablending($this->image, false);
imagesavealpha($this->image, true);
$bg = $this->getColor($this->fill->getBackgroundColor());
imagefilledrectangle($this->image, 0, 0, $this->size, $this->size, $bg);
imagealphablending($this->image, true);
}
private function draw(ByteMatrix $matrix): void
{
$matrixSize = $matrix->getWidth();
$pointsOnSide = $matrix->getWidth() + $this->margin * 2;
$pointInPx = $this->size / $pointsOnSide;
$this->drawEye(0, 0, $pointInPx, $this->fill->getTopLeftEyeFill());
$this->drawEye($matrixSize - 7, 0, $pointInPx, $this->fill->getTopRightEyeFill());
$this->drawEye(0, $matrixSize - 7, $pointInPx, $this->fill->getBottomLeftEyeFill());
$rows = $matrix->getArray()->toArray();
$color = $this->getColor($this->fill->getForegroundColor());
for ($y = 0; $y < $matrixSize; $y += 1) {
for ($x = 0; $x < $matrixSize; $x += 1) {
if (! $rows[$y][$x]) {
continue;
}
$points = $this->normalizePoints([
($this->margin + $x) * $pointInPx, ($this->margin + $y) * $pointInPx,
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y) * $pointInPx,
($this->margin + $x + 1) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
($this->margin + $x) * $pointInPx, ($this->margin + $y + 1) * $pointInPx,
]);
imagefilledpolygon($this->image, $points, $color);
}
}
}
private function drawEye(int $xOffset, int $yOffset, float $pointInPx, EyeFill $eyeFill): void
{
$internalColor = $this->getColor($eyeFill->inheritsInternalColor()
? $this->fill->getForegroundColor()
: $eyeFill->getInternalColor());
$externalColor = $this->getColor($eyeFill->inheritsExternalColor()
? $this->fill->getForegroundColor()
: $eyeFill->getExternalColor());
for ($y = 0; $y < 7; $y += 1) {
for ($x = 0; $x < 7; $x += 1) {
if ((($y === 1 || $y === 5) && $x > 0 && $x < 6) || (($x === 1 || $x === 5) && $y > 0 && $y < 6)) {
continue;
}
$points = $this->normalizePoints([
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset) * $pointInPx,
($this->margin + $x + $xOffset + 1) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
($this->margin + $x + $xOffset) * $pointInPx, ($this->margin + $y + $yOffset + 1) * $pointInPx,
]);
if ($y > 1 && $y < 5 && $x > 1 && $x < 5) {
imagefilledpolygon($this->image, $points, $internalColor);
} else {
imagefilledpolygon($this->image, $points, $externalColor);
}
}
}
}
/**
* Normalize points will trim right and bottom line by 1 pixel.
* Otherwise pixels of neighbors are overlapping which leads to issue with transparency and small QR codes.
*/
private function normalizePoints(array $points): array
{
$maxX = $maxY = 0;
for ($i = 0; $i < count($points); $i += 2) {
// Do manual round as GD just removes decimal part
$points[$i] = $newX = round($points[$i]);
$points[$i + 1] = $newY = round($points[$i + 1]);
$maxX = max($maxX, $newX);
$maxY = max($maxY, $newY);
}
// Do trimming only if there are 4 points (8 coordinates), assumes this is square.
for ($i = 0; $i < count($points); $i += 2) {
$points[$i] = min($points[$i], $maxX - 1);
$points[$i + 1] = min($points[$i + 1], $maxY - 1);
}
return $points;
}
private function renderImage(): string
{
ob_start();
$quality = $this->compressionQuality;
switch ($this->imageFormat) {
case 'png':
if ($quality > 9 || $quality < 0) {
$quality = 9;
}
imagepng($this->image, null, $quality);
break;
case 'gif':
imagegif($this->image, null);
break;
case 'jpeg':
case 'jpg':
if ($quality > 100 || $quality < 0) {
$quality = 85;
}
imagejpeg($this->image, null, $quality);
break;
default:
ob_end_clean();
throw new InvalidArgumentException(
'Supported image formats are jpeg, png and gif, got: ' . $this->imageFormat
);
}
imagedestroy($this->image);
$this->colors = [];
$this->image = null;
return ob_get_clean();
}
private function getColor(ColorInterface $color): int
{
$alpha = 100;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha();
$color = $color->getBaseColor();
}
$rgb = $color->toRgb();
$colorKey = sprintf('%02X%02X%02X%02X', $rgb->getRed(), $rgb->getGreen(), $rgb->getBlue(), $alpha);
if (! isset($this->colors[$colorKey])) {
$colorId = imagecolorallocatealpha(
$this->image,
$rgb->getRed(),
$rgb->getGreen(),
$rgb->getBlue(),
(int)((100 - $alpha) / 100 * 127) // Alpha for GD is in range 0 (opaque) - 127 (transparent)
);
if ($colorId === false) {
throw new RuntimeException('Failed to create color: #' . $colorKey);
}
$this->colors[$colorKey] = $colorId;
}
return $this->colors[$colorKey];
}
}

View file

@ -0,0 +1,373 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
final class EpsImageBackEnd implements ImageBackEndInterface
{
private const PRECISION = 3;
private ?string $eps;
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
. "%%Creator: BaconQrCode\n"
. sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
. "%%BeginProlog\n"
. "save\n"
. "50 dict begin\n"
. "/q { gsave } bind def\n"
. "/Q { grestore } bind def\n"
. "/s { scale } bind def\n"
. "/t { translate } bind def\n"
. "/r { rotate } bind def\n"
. "/n { newpath } bind def\n"
. "/m { moveto } bind def\n"
. "/l { lineto } bind def\n"
. "/c { curveto } bind def\n"
. "/z { closepath } bind def\n"
. "/f { eofill } bind def\n"
. "/rgb { setrgbcolor } bind def\n"
. "/cmyk { setcmykcolor } bind def\n"
. "/gray { setgray } bind def\n"
. "%%EndProlog\n"
. "1 -1 s\n"
. sprintf("0 -%d t\n", $size);
if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
return;
}
$this->eps .= wordwrap(
'0 0 m'
. sprintf(' %s 0 l', (string) $size)
. sprintf(' %s %s l', (string) $size, (string) $size)
. sprintf(' 0 %s l', (string) $size)
. ' z'
. ' ' .$this->getColorSetString($backgroundColor) . " f\n",
75,
"\n "
);
}
public function scale(float $size) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
}
public function translate(float $x, float $y) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
}
public function rotate(int $degrees) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= sprintf("%d r\n", $degrees);
}
public function push() : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "q\n";
}
public function pop() : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "Q\n";
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$fromX = 0;
$fromY = 0;
$this->eps .= wordwrap(
'n '
. $this->drawPathOperations($path, $fromX, $fromY)
. ' ' . $this->getColorSetString($color) . " f\n",
75,
"\n "
);
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$fromX = 0;
$fromY = 0;
$this->eps .= wordwrap(
'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
75,
"\n "
);
$this->createGradientFill($gradient, $x, $y, $width, $height);
}
public function done() : string
{
if (null === $this->eps) {
throw new RuntimeException('No image has been started');
}
$this->eps .= "%%TRAILER\nend restore\n%%EOF";
$blob = $this->eps;
$this->eps = null;
return $blob;
}
private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
{
$pathData = [];
foreach ($ops as $op) {
switch (true) {
case $op instanceof Move:
$fromX = $toX = round($op->getX(), self::PRECISION);
$fromY = $toY = round($op->getY(), self::PRECISION);
$pathData[] = sprintf('%s %s m', $toX, $toY);
break;
case $op instanceof Line:
$fromX = $toX = round($op->getX(), self::PRECISION);
$fromY = $toY = round($op->getY(), self::PRECISION);
$pathData[] = sprintf('%s %s l', $toX, $toY);
break;
case $op instanceof EllipticArc:
$pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
break;
case $op instanceof Curve:
$x1 = round($op->getX1(), self::PRECISION);
$y1 = round($op->getY1(), self::PRECISION);
$x2 = round($op->getX2(), self::PRECISION);
$y2 = round($op->getY2(), self::PRECISION);
$fromX = $x3 = round($op->getX3(), self::PRECISION);
$fromY = $y3 = round($op->getY3(), self::PRECISION);
$pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
break;
case $op instanceof Close:
$pathData[] = 'z';
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
return implode(' ', $pathData);
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
{
$startColor = $gradient->getStartColor();
$endColor = $gradient->getEndColor();
if ($startColor instanceof Alpha) {
$startColor = $startColor->getBaseColor();
}
$startColorType = get_class($startColor);
if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
$startColorType = Cmyk::class;
$startColor = $startColor->toCmyk();
}
if (get_class($endColor) !== $startColorType) {
switch ($startColorType) {
case Cmyk::class:
$endColor = $endColor->toCmyk();
break;
case Rgb::class:
$endColor = $endColor->toRgb();
break;
case Gray::class:
$endColor = $endColor->toGray();
break;
}
}
$this->eps .= "eoclip\n<<\n";
if ($gradient->getType() === GradientType::RADIAL()) {
$this->eps .= " /ShadingType 3\n";
} else {
$this->eps .= " /ShadingType 2\n";
}
$this->eps .= " /Extend [ true true ]\n"
. " /AntiAlias true\n";
switch ($startColorType) {
case Cmyk::class:
$this->eps .= " /ColorSpace /DeviceCMYK\n";
break;
case Rgb::class:
$this->eps .= " /ColorSpace /DeviceRGB\n";
break;
case Gray::class:
$this->eps .= " /ColorSpace /DeviceGray\n";
break;
}
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x + $width, self::PRECISION),
round($y, self::PRECISION)
);
break;
case GradientType::VERTICAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x, self::PRECISION),
round($y + $height, self::PRECISION)
);
break;
case GradientType::DIAGONAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y, self::PRECISION),
round($x + $width, self::PRECISION),
round($y + $height, self::PRECISION)
);
break;
case GradientType::INVERSE_DIAGONAL():
$this->eps .= sprintf(
" /Coords [ %s %s %s %s ]\n",
round($x, self::PRECISION),
round($y + $height, self::PRECISION),
round($x + $width, self::PRECISION),
round($y, self::PRECISION)
);
break;
case GradientType::RADIAL():
$centerX = ($x + $width) / 2;
$centerY = ($y + $height) / 2;
$this->eps .= sprintf(
" /Coords [ %s %s 0 %s %s %s ]\n",
round($centerX, self::PRECISION),
round($centerY, self::PRECISION),
round($centerX, self::PRECISION),
round($centerY, self::PRECISION),
round(max($width, $height) / 2, self::PRECISION)
);
break;
}
$this->eps .= " /Function\n"
. " <<\n"
. " /FunctionType 2\n"
. " /Domain [ 0 1 ]\n"
. sprintf(" /C0 [ %s ]\n", $this->getColorString($startColor))
. sprintf(" /C1 [ %s ]\n", $this->getColorString($endColor))
. " /N 1\n"
. " >>\n>>\nshfill\nQ\n";
}
private function getColorSetString(ColorInterface $color) : string
{
if ($color instanceof Rgb) {
return $this->getColorString($color) . ' rgb';
}
if ($color instanceof Cmyk) {
return $this->getColorString($color) . ' cmyk';
}
if ($color instanceof Gray) {
return $this->getColorString($color) . ' gray';
}
return $this->getColorSetString($color->toCmyk());
}
private function getColorString(ColorInterface $color) : string
{
if ($color instanceof Rgb) {
return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
}
if ($color instanceof Cmyk) {
return sprintf(
'%s %s %s %s',
$color->getCyan() / 100,
$color->getMagenta() / 100,
$color->getYellow() / 100,
$color->getBlack() / 100
);
}
if ($color instanceof Gray) {
return sprintf('%s', $color->getGray() / 100);
}
return $this->getColorString($color->toCmyk());
}
}

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
/**
* Interface for back ends able to to produce path based images.
*/
interface ImageBackEndInterface
{
/**
* Starts a new image.
*
* If a previous image was already started, previous data get erased.
*/
public function new(int $size, ColorInterface $backgroundColor) : void;
/**
* Transforms all following drawing operation coordinates by scaling them by a given factor.
*
* @throws RuntimeException if no image was started yet.
*/
public function scale(float $size) : void;
/**
* Transforms all following drawing operation coordinates by translating them by a given amount.
*
* @throws RuntimeException if no image was started yet.
*/
public function translate(float $x, float $y) : void;
/**
* Transforms all following drawing operation coordinates by rotating them by a given amount.
*
* @throws RuntimeException if no image was started yet.
*/
public function rotate(int $degrees) : void;
/**
* Pushes the current coordinate transformation onto a stack.
*
* @throws RuntimeException if no image was started yet.
*/
public function push() : void;
/**
* Pops the last coordinate transformation from a stack.
*
* @throws RuntimeException if no image was started yet.
*/
public function pop() : void;
/**
* Draws a path with a given color.
*
* @throws RuntimeException if no image was started yet.
*/
public function drawPathWithColor(Path $path, ColorInterface $color) : void;
/**
* Draws a path with a given gradient which spans the box described by the position and size.
*
* @throws RuntimeException if no image was started yet.
*/
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void;
/**
* Ends the image drawing operation and returns the resulting blob.
*
* This should reset the state of the back end and thus this method should only be callable once per image.
*
* @throws RuntimeException if no image was started yet.
*/
public function done() : string;
}

View file

@ -0,0 +1,318 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\Cmyk;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use Imagick;
use ImagickDraw;
use ImagickPixel;
final class ImagickImageBackEnd implements ImageBackEndInterface
{
private string $imageFormat;
private int $compressionQuality;
private ?Imagick $image;
private ?ImagickDraw $draw;
private ?int $gradientCount;
/**
* @var TransformationMatrix[]|null
*/
private ?array $matrices;
private ?int $matrixIndex;
public function __construct(string $imageFormat = 'png', int $compressionQuality = 100)
{
if (! class_exists(Imagick::class)) {
throw new RuntimeException('You need to install the imagick extension to use this back end');
}
$this->imageFormat = $imageFormat;
$this->compressionQuality = $compressionQuality;
}
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->image = new Imagick();
$this->image->newImage($size, $size, $this->getColorPixel($backgroundColor));
$this->image->setImageFormat($this->imageFormat);
$this->image->setCompressionQuality($this->compressionQuality);
$this->draw = new ImagickDraw();
$this->gradientCount = 0;
$this->matrices = [new TransformationMatrix()];
$this->matrixIndex = 0;
}
public function scale(float $size) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->scale($size, $size);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::scale($size));
}
public function translate(float $x, float $y) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->translate($x, $y);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::translate($x, $y));
}
public function rotate(int $degrees) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->rotate($degrees);
$this->matrices[$this->matrixIndex] = $this->matrices[$this->matrixIndex]
->multiply(TransformationMatrix::rotate($degrees));
}
public function push() : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->push();
$this->matrices[++$this->matrixIndex] = $this->matrices[$this->matrixIndex - 1];
}
public function pop() : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->pop();
unset($this->matrices[$this->matrixIndex--]);
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->setFillColor($this->getColorPixel($color));
$this->drawPath($path);
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->draw->setFillPatternURL('#' . $this->createGradientFill($gradient, $x, $y, $width, $height));
$this->drawPath($path);
}
public function done() : string
{
if (null === $this->draw) {
throw new RuntimeException('No image has been started');
}
$this->image->drawImage($this->draw);
$blob = $this->image->getImageBlob();
$this->draw->clear();
$this->image->clear();
$this->draw = null;
$this->image = null;
$this->gradientCount = null;
return $blob;
}
private function drawPath(Path $path) : void
{
$this->draw->pathStart();
foreach ($path as $op) {
switch (true) {
case $op instanceof Move:
$this->draw->pathMoveToAbsolute($op->getX(), $op->getY());
break;
case $op instanceof Line:
$this->draw->pathLineToAbsolute($op->getX(), $op->getY());
break;
case $op instanceof EllipticArc:
$this->draw->pathEllipticArcAbsolute(
$op->getXRadius(),
$op->getYRadius(),
$op->getXAxisAngle(),
$op->isLargeArc(),
$op->isSweep(),
$op->getX(),
$op->getY()
);
break;
case $op instanceof Curve:
$this->draw->pathCurveToAbsolute(
$op->getX1(),
$op->getY1(),
$op->getX2(),
$op->getY2(),
$op->getX3(),
$op->getY3()
);
break;
case $op instanceof Close:
$this->draw->pathClose();
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
$this->draw->pathFinish();
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
{
list($width, $height) = $this->matrices[$this->matrixIndex]->apply($width, $height);
$startColor = $this->getColorPixel($gradient->getStartColor())->getColorAsString();
$endColor = $this->getColorPixel($gradient->getEndColor())->getColorAsString();
$gradientImage = new Imagick();
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$gradientImage->newPseudoImage((int) $height, (int) $width, sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
$gradientImage->rotateImage('transparent', -90);
break;
case GradientType::VERTICAL():
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
break;
case GradientType::DIAGONAL():
case GradientType::INVERSE_DIAGONAL():
$gradientImage->newPseudoImage((int) ($width * sqrt(2)), (int) ($height * sqrt(2)), sprintf(
'gradient:%s-%s',
$startColor,
$endColor
));
if (GradientType::DIAGONAL() === $gradient->getType()) {
$gradientImage->rotateImage('transparent', -45);
} else {
$gradientImage->rotateImage('transparent', -135);
}
$rotatedWidth = $gradientImage->getImageWidth();
$rotatedHeight = $gradientImage->getImageHeight();
$gradientImage->setImagePage($rotatedWidth, $rotatedHeight, 0, 0);
$gradientImage->cropImage(
intdiv($rotatedWidth, 2) - 2,
intdiv($rotatedHeight, 2) - 2,
intdiv($rotatedWidth, 4) + 1,
intdiv($rotatedWidth, 4) + 1
);
break;
case GradientType::RADIAL():
$gradientImage->newPseudoImage((int) $width, (int) $height, sprintf(
'radial-gradient:%s-%s',
$startColor,
$endColor
));
break;
}
$id = sprintf('g%d', ++$this->gradientCount);
$this->draw->pushPattern($id, 0, 0, $width, $height);
$this->draw->composite(Imagick::COMPOSITE_COPY, 0, 0, $width, $height, $gradientImage);
$this->draw->popPattern();
return $id;
}
private function getColorPixel(ColorInterface $color) : ImagickPixel
{
$alpha = 100;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha();
$color = $color->getBaseColor();
}
if ($color instanceof Rgb) {
return new ImagickPixel(sprintf(
'rgba(%d, %d, %d, %F)',
$color->getRed(),
$color->getGreen(),
$color->getBlue(),
$alpha / 100
));
}
if ($color instanceof Cmyk) {
return new ImagickPixel(sprintf(
'cmyka(%d, %d, %d, %d, %F)',
$color->getCyan(),
$color->getMagenta(),
$color->getYellow(),
$color->getBlack(),
$alpha / 100
));
}
if ($color instanceof Gray) {
return new ImagickPixel(sprintf(
'graya(%d%%, %F)',
$color->getGray(),
$alpha / 100
));
}
return $this->getColorPixel(new Alpha($alpha, $color->toRgb()));
}
}

View file

@ -0,0 +1,363 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\Alpha;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Path\Close;
use BaconQrCode\Renderer\Path\Curve;
use BaconQrCode\Renderer\Path\EllipticArc;
use BaconQrCode\Renderer\Path\Line;
use BaconQrCode\Renderer\Path\Move;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use XMLWriter;
final class SvgImageBackEnd implements ImageBackEndInterface
{
private const PRECISION = 3;
private const SCALE_FORMAT = 'scale(%.' . self::PRECISION . 'F)';
private const TRANSLATE_FORMAT = 'translate(%.' . self::PRECISION . 'F,%.' . self::PRECISION . 'F)';
private ?XMLWriter $xmlWriter;
private ?array $stack;
private ?int $currentStack;
private ?int $gradientCount;
public function __construct()
{
if (! class_exists(XMLWriter::class)) {
throw new RuntimeException('You need to install the libxml extension to use this back end');
}
}
public function new(int $size, ColorInterface $backgroundColor) : void
{
$this->xmlWriter = new XMLWriter();
$this->xmlWriter->openMemory();
$this->xmlWriter->startDocument('1.0', 'UTF-8');
$this->xmlWriter->startElement('svg');
$this->xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/2000/svg');
$this->xmlWriter->writeAttribute('version', '1.1');
$this->xmlWriter->writeAttribute('width', (string) $size);
$this->xmlWriter->writeAttribute('height', (string) $size);
$this->xmlWriter->writeAttribute('viewBox', '0 0 '. $size . ' ' . $size);
$this->gradientCount = 0;
$this->currentStack = 0;
$this->stack[0] = 0;
$alpha = 1;
if ($backgroundColor instanceof Alpha) {
$alpha = $backgroundColor->getAlpha() / 100;
}
if (0 === $alpha) {
return;
}
$this->xmlWriter->startElement('rect');
$this->xmlWriter->writeAttribute('x', '0');
$this->xmlWriter->writeAttribute('y', '0');
$this->xmlWriter->writeAttribute('width', (string) $size);
$this->xmlWriter->writeAttribute('height', (string) $size);
$this->xmlWriter->writeAttribute('fill', $this->getColorString($backgroundColor));
if ($alpha < 1) {
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
}
$this->xmlWriter->endElement();
}
public function scale(float $size) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute(
'transform',
sprintf(self::SCALE_FORMAT, round($size, self::PRECISION))
);
++$this->stack[$this->currentStack];
}
public function translate(float $x, float $y) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute(
'transform',
sprintf(self::TRANSLATE_FORMAT, round($x, self::PRECISION), round($y, self::PRECISION))
);
++$this->stack[$this->currentStack];
}
public function rotate(int $degrees) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->xmlWriter->writeAttribute('transform', sprintf('rotate(%d)', $degrees));
++$this->stack[$this->currentStack];
}
public function push() : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$this->xmlWriter->startElement('g');
$this->stack[] = 1;
++$this->currentStack;
}
public function pop() : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
for ($i = 0; $i < $this->stack[$this->currentStack]; ++$i) {
$this->xmlWriter->endElement();
}
array_pop($this->stack);
--$this->currentStack;
}
public function drawPathWithColor(Path $path, ColorInterface $color) : void
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$alpha = 1;
if ($color instanceof Alpha) {
$alpha = $color->getAlpha() / 100;
}
$this->startPathElement($path);
$this->xmlWriter->writeAttribute('fill', $this->getColorString($color));
if ($alpha < 1) {
$this->xmlWriter->writeAttribute('fill-opacity', (string) $alpha);
}
$this->xmlWriter->endElement();
}
public function drawPathWithGradient(
Path $path,
Gradient $gradient,
float $x,
float $y,
float $width,
float $height
) : void {
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
$gradientId = $this->createGradientFill($gradient, $x, $y, $width, $height);
$this->startPathElement($path);
$this->xmlWriter->writeAttribute('fill', 'url(#' . $gradientId . ')');
$this->xmlWriter->endElement();
}
public function done() : string
{
if (null === $this->xmlWriter) {
throw new RuntimeException('No image has been started');
}
foreach ($this->stack as $openElements) {
for ($i = $openElements; $i > 0; --$i) {
$this->xmlWriter->endElement();
}
}
$this->xmlWriter->endDocument();
$blob = $this->xmlWriter->outputMemory(true);
$this->xmlWriter = null;
$this->stack = null;
$this->currentStack = null;
$this->gradientCount = null;
return $blob;
}
private function startPathElement(Path $path) : void
{
$pathData = [];
foreach ($path as $op) {
switch (true) {
case $op instanceof Move:
$pathData[] = sprintf(
'M%s %s',
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof Line:
$pathData[] = sprintf(
'L%s %s',
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof EllipticArc:
$pathData[] = sprintf(
'A%s %s %s %u %u %s %s',
round($op->getXRadius(), self::PRECISION),
round($op->getYRadius(), self::PRECISION),
round($op->getXAxisAngle(), self::PRECISION),
$op->isLargeArc(),
$op->isSweep(),
round($op->getX(), self::PRECISION),
round($op->getY(), self::PRECISION)
);
break;
case $op instanceof Curve:
$pathData[] = sprintf(
'C%s %s %s %s %s %s',
round($op->getX1(), self::PRECISION),
round($op->getY1(), self::PRECISION),
round($op->getX2(), self::PRECISION),
round($op->getY2(), self::PRECISION),
round($op->getX3(), self::PRECISION),
round($op->getY3(), self::PRECISION)
);
break;
case $op instanceof Close:
$pathData[] = 'Z';
break;
default:
throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
}
}
$this->xmlWriter->startElement('path');
$this->xmlWriter->writeAttribute('fill-rule', 'evenodd');
$this->xmlWriter->writeAttribute('d', implode('', $pathData));
}
private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : string
{
$this->xmlWriter->startElement('defs');
$startColor = $gradient->getStartColor();
$endColor = $gradient->getEndColor();
if ($gradient->getType() === GradientType::RADIAL()) {
$this->xmlWriter->startElement('radialGradient');
} else {
$this->xmlWriter->startElement('linearGradient');
}
$this->xmlWriter->writeAttribute('gradientUnits', 'userSpaceOnUse');
switch ($gradient->getType()) {
case GradientType::HORIZONTAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
break;
case GradientType::VERTICAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
break;
case GradientType::DIAGONAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y + $height, self::PRECISION));
break;
case GradientType::INVERSE_DIAGONAL():
$this->xmlWriter->writeAttribute('x1', (string) round($x, self::PRECISION));
$this->xmlWriter->writeAttribute('y1', (string) round($y + $height, self::PRECISION));
$this->xmlWriter->writeAttribute('x2', (string) round($x + $width, self::PRECISION));
$this->xmlWriter->writeAttribute('y2', (string) round($y, self::PRECISION));
break;
case GradientType::RADIAL():
$this->xmlWriter->writeAttribute('cx', (string) round(($x + $width) / 2, self::PRECISION));
$this->xmlWriter->writeAttribute('cy', (string) round(($y + $height) / 2, self::PRECISION));
$this->xmlWriter->writeAttribute('r', (string) round(max($width, $height) / 2, self::PRECISION));
break;
}
$toBeHashed = $this->getColorString($startColor) . $this->getColorString($endColor) . $gradient->getType();
if ($startColor instanceof Alpha) {
$toBeHashed .= (string) $startColor->getAlpha();
}
$id = sprintf('g%d-%s', ++$this->gradientCount, hash('xxh64', $toBeHashed));
$this->xmlWriter->writeAttribute('id', $id);
$this->xmlWriter->startElement('stop');
$this->xmlWriter->writeAttribute('offset', '0%');
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($startColor));
if ($startColor instanceof Alpha) {
$this->xmlWriter->writeAttribute('stop-opacity', (string) $startColor->getAlpha());
}
$this->xmlWriter->endElement();
$this->xmlWriter->startElement('stop');
$this->xmlWriter->writeAttribute('offset', '100%');
$this->xmlWriter->writeAttribute('stop-color', $this->getColorString($endColor));
if ($endColor instanceof Alpha) {
$this->xmlWriter->writeAttribute('stop-opacity', (string) $endColor->getAlpha());
}
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
$this->xmlWriter->endElement();
return $id;
}
private function getColorString(ColorInterface $color) : string
{
$color = $color->toRgb();
return sprintf(
'#%02x%02x%02x',
$color->getRed(),
$color->getGreen(),
$color->getBlue()
);
}
}

View file

@ -0,0 +1,68 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Image;
final class TransformationMatrix
{
/**
* @var float[]
*/
private array $values;
public function __construct()
{
$this->values = [1, 0, 0, 1, 0, 0];
}
public function multiply(self $other) : self
{
$matrix = new self();
$matrix->values[0] = $this->values[0] * $other->values[0] + $this->values[2] * $other->values[1];
$matrix->values[1] = $this->values[1] * $other->values[0] + $this->values[3] * $other->values[1];
$matrix->values[2] = $this->values[0] * $other->values[2] + $this->values[2] * $other->values[3];
$matrix->values[3] = $this->values[1] * $other->values[2] + $this->values[3] * $other->values[3];
$matrix->values[4] = $this->values[0] * $other->values[4] + $this->values[2] * $other->values[5]
+ $this->values[4];
$matrix->values[5] = $this->values[1] * $other->values[4] + $this->values[3] * $other->values[5]
+ $this->values[5];
return $matrix;
}
public static function scale(float $size) : self
{
$matrix = new self();
$matrix->values = [$size, 0, 0, $size, 0, 0];
return $matrix;
}
public static function translate(float $x, float $y) : self
{
$matrix = new self();
$matrix->values = [1, 0, 0, 1, $x, $y];
return $matrix;
}
public static function rotate(int $degrees) : self
{
$matrix = new self();
$rad = deg2rad($degrees);
$matrix->values = [cos($rad), sin($rad), -sin($rad), cos($rad), 0, 0];
return $matrix;
}
/**
* Applies this matrix onto a point and returns the resulting viewport point.
*
* @return float[]
*/
public function apply(float $x, float $y) : array
{
return [
$x * $this->values[0] + $y * $this->values[2] + $this->values[4],
$x * $this->values[1] + $y * $this->values[3] + $this->values[5],
];
}
}

View file

@ -0,0 +1,150 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\MatrixUtil;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Image\ImageBackEndInterface;
use BaconQrCode\Renderer\Path\Path;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
final class ImageRenderer implements RendererInterface
{
public function __construct(
private readonly RendererStyle $rendererStyle,
private readonly ImageBackEndInterface $imageBackEnd
) {
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode) : string
{
$size = $this->rendererStyle->getSize();
$margin = $this->rendererStyle->getMargin();
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
$totalSize = $matrixSize + ($margin * 2);
$moduleSize = $size / $totalSize;
$fill = $this->rendererStyle->getFill();
$this->imageBackEnd->new($size, $fill->getBackgroundColor());
$this->imageBackEnd->scale((float) $moduleSize);
$this->imageBackEnd->translate((float) $margin, (float) $margin);
$module = $this->rendererStyle->getModule();
$moduleMatrix = clone $matrix;
MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
$modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
if ($fill->hasGradientFill()) {
$this->imageBackEnd->drawPathWithGradient(
$modulePath,
$fill->getForegroundGradient(),
0,
0,
$matrixSize,
$matrixSize
);
} else {
$this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
}
return $this->imageBackEnd->done();
}
private function drawEyes(int $matrixSize, Path $modulePath) : Path
{
$fill = $this->rendererStyle->getFill();
$eye = $this->rendererStyle->getEye();
$externalPath = $eye->getExternalPath();
$internalPath = $eye->getInternalPath();
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getTopLeftEyeFill(),
3.5,
3.5,
0,
$modulePath
);
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getTopRightEyeFill(),
$matrixSize - 3.5,
3.5,
90,
$modulePath
);
$modulePath = $this->drawEye(
$externalPath,
$internalPath,
$fill->getBottomLeftEyeFill(),
3.5,
$matrixSize - 3.5,
-90,
$modulePath
);
return $modulePath;
}
private function drawEye(
Path $externalPath,
Path $internalPath,
EyeFill $fill,
float $xTranslation,
float $yTranslation,
int $rotation,
Path $modulePath
) : Path {
if ($fill->inheritsBothColors()) {
return $modulePath
->append(
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
)
->append(
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
}
$this->imageBackEnd->push();
$this->imageBackEnd->translate($xTranslation, $yTranslation);
if (0 !== $rotation) {
$this->imageBackEnd->rotate($rotation);
}
if ($fill->inheritsExternalColor()) {
$modulePath = $modulePath->append(
$externalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
} else {
$this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
}
if ($fill->inheritsInternalColor()) {
$modulePath = $modulePath->append(
$internalPath->rotate($rotation)->translate($xTranslation, $yTranslation)
);
} else {
$this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
}
$this->imageBackEnd->pop();
return $modulePath;
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Path\Path;
/**
* Renders individual modules as dots.
*/
final class DotsModule implements ModuleInterface
{
public const LARGE = 1;
public const MEDIUM = .8;
public const SMALL = .6;
public function __construct(private readonly float $size)
{
if ($size <= 0 || $size > 1) {
throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
}
}
public function createPath(ByteMatrix $matrix) : Path
{
$width = $matrix->getWidth();
$height = $matrix->getHeight();
$path = new Path();
$halfSize = $this->size / 2;
$margin = (1 - $this->size) / 2;
for ($y = 0; $y < $height; ++$y) {
for ($x = 0; $x < $width; ++$x) {
if (! $matrix->get($x, $y)) {
continue;
}
$pathX = $x + $margin;
$pathY = $y + $margin;
$path = $path
->move($pathX + $this->size, $pathY + $halfSize)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY + $this->size)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX, $pathY + $halfSize)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $halfSize, $pathY)
->ellipticArc($halfSize, $halfSize, 0, false, true, $pathX + $this->size, $pathY + $halfSize)
->close()
;
}
}
return $path;
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
final class Edge
{
/**
* @var array<int[]>
*/
private array $points = [];
/**
* @var array<int[]>|null
*/
private ?array $simplifiedPoints = null;
private int $minX = PHP_INT_MAX;
private int $minY = PHP_INT_MAX;
private int $maxX = -1;
private int $maxY = -1;
public function __construct(private readonly bool $positive)
{
}
public function addPoint(int $x, int $y) : void
{
$this->points[] = [$x, $y];
$this->minX = min($this->minX, $x);
$this->minY = min($this->minY, $y);
$this->maxX = max($this->maxX, $x);
$this->maxY = max($this->maxY, $y);
}
public function isPositive() : bool
{
return $this->positive;
}
/**
* @return array<int[]>
*/
public function getPoints() : array
{
return $this->points;
}
public function getMaxX() : int
{
return $this->maxX;
}
public function getSimplifiedPoints() : array
{
if (null !== $this->simplifiedPoints) {
return $this->simplifiedPoints;
}
$points = [];
$length = count($this->points);
for ($i = 0; $i < $length; ++$i) {
$previousPoint = $this->points[(0 === $i ? $length : $i) - 1];
$nextPoint = $this->points[($length - 1 === $i ? -1 : $i) + 1];
$currentPoint = $this->points[$i];
if (($previousPoint[0] === $currentPoint[0] && $currentPoint[0] === $nextPoint[0])
|| ($previousPoint[1] === $currentPoint[1] && $currentPoint[1] === $nextPoint[1])
) {
continue;
}
$points[] = $currentPoint;
}
return $this->simplifiedPoints = $points;
}
}

View file

@ -0,0 +1,160 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
use BaconQrCode\Encoder\ByteMatrix;
use IteratorAggregate;
use Traversable;
/**
* Edge iterator based on potrace.
*/
final class EdgeIterator implements IteratorAggregate
{
/**
* @var int[]
*/
private array $bytes = [];
private ?int $size;
private int $width;
private int $height;
public function __construct(ByteMatrix $matrix)
{
$this->bytes = iterator_to_array($matrix->getBytes());
$this->size = count($this->bytes);
$this->width = $matrix->getWidth();
$this->height = $matrix->getHeight();
}
/**
* @return Traversable<Edge>
*/
public function getIterator() : Traversable
{
$originalBytes = $this->bytes;
$point = $this->findNext(0, 0);
while (null !== $point) {
$edge = $this->findEdge($point[0], $point[1]);
$this->xorEdge($edge);
yield $edge;
$point = $this->findNext($point[0], $point[1]);
}
$this->bytes = $originalBytes;
}
/**
* @return int[]|null
*/
private function findNext(int $x, int $y) : ?array
{
$i = $this->width * $y + $x;
while ($i < $this->size && 1 !== $this->bytes[$i]) {
++$i;
}
if ($i < $this->size) {
return $this->pointOf($i);
}
return null;
}
private function findEdge(int $x, int $y) : Edge
{
$edge = new Edge($this->isSet($x, $y));
$startX = $x;
$startY = $y;
$dirX = 0;
$dirY = 1;
while (true) {
$edge->addPoint($x, $y);
$x += $dirX;
$y += $dirY;
if ($x === $startX && $y === $startY) {
break;
}
$left = $this->isSet($x + ($dirX + $dirY - 1 ) / 2, $y + ($dirY - $dirX - 1) / 2);
$right = $this->isSet($x + ($dirX - $dirY - 1) / 2, $y + ($dirY + $dirX - 1) / 2);
if ($right && ! $left) {
$tmp = $dirX;
$dirX = -$dirY;
$dirY = $tmp;
} elseif ($right) {
$tmp = $dirX;
$dirX = -$dirY;
$dirY = $tmp;
} elseif (! $left) {
$tmp = $dirX;
$dirX = $dirY;
$dirY = -$tmp;
}
}
return $edge;
}
private function xorEdge(Edge $path) : void
{
$points = $path->getPoints();
$y1 = $points[0][1];
$length = count($points);
$maxX = $path->getMaxX();
for ($i = 1; $i < $length; ++$i) {
$y = $points[$i][1];
if ($y === $y1) {
continue;
}
$x = $points[$i][0];
$minY = min($y1, $y);
for ($j = $x; $j < $maxX; ++$j) {
$this->flip($j, $minY);
}
$y1 = $y;
}
}
private function isSet(int $x, int $y) : bool
{
return (
$x >= 0
&& $x < $this->width
&& $y >= 0
&& $y < $this->height
) && 1 === $this->bytes[$this->width * $y + $x];
}
/**
* @return int[]
*/
private function pointOf(int $i) : array
{
$y = intdiv($i, $this->width);
return [$i - $y * $this->width, $y];
}
private function flip(int $x, int $y) : void
{
$this->bytes[$this->width * $y + $x] = (
$this->isSet($x, $y) ? 0 : 1
);
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Path\Path;
/**
* Interface describing how modules should be rendered.
*
* A module always receives a byte matrix (with values either being 1 or 0). It returns a path, where the origin
* coordinate (0, 0) equals the top left corner of the first matrix value.
*/
interface ModuleInterface
{
public function createPath(ByteMatrix $matrix) : Path;
}

View file

@ -0,0 +1,124 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
use BaconQrCode\Renderer\Path\Path;
/**
* Rounds the corners of module groups.
*/
final class RoundnessModule implements ModuleInterface
{
public const STRONG = 1;
public const MEDIUM = .5;
public const SOFT = .25;
public function __construct(private float $intensity)
{
if ($intensity <= 0 || $intensity > 1) {
throw new InvalidArgumentException('Intensity must between 0 (exclusive) and 1 (inclusive)');
}
$this->intensity = $intensity / 2;
}
public function createPath(ByteMatrix $matrix) : Path
{
$path = new Path();
foreach (new EdgeIterator($matrix) as $edge) {
$points = $edge->getSimplifiedPoints();
$length = count($points);
$currentPoint = $points[0];
$nextPoint = $points[1];
$horizontal = ($currentPoint[1] === $nextPoint[1]);
if ($horizontal) {
$right = $nextPoint[0] > $currentPoint[0];
$path = $path->move(
$currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
$currentPoint[1]
);
} else {
$up = $nextPoint[0] < $currentPoint[0];
$path = $path->move(
$currentPoint[0],
$currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
);
}
for ($i = 1; $i <= $length; ++$i) {
if ($i === $length) {
$previousPoint = $points[$length - 1];
$currentPoint = $points[0];
$nextPoint = $points[1];
} else {
$previousPoint = $points[(0 === $i ? $length : $i) - 1];
$currentPoint = $points[$i];
$nextPoint = $points[($length - 1 === $i ? -1 : $i) + 1];
}
$horizontal = ($previousPoint[1] === $currentPoint[1]);
if ($horizontal) {
$right = $previousPoint[0] < $currentPoint[0];
$up = $nextPoint[1] < $currentPoint[1];
$sweep = ($up xor $right);
if ($this->intensity < 0.5
|| ($right && $previousPoint[0] !== $currentPoint[0] - 1)
|| (! $right && $previousPoint[0] - 1 !== $currentPoint[0])
) {
$path = $path->line(
$currentPoint[0] + ($right ? -$this->intensity : $this->intensity),
$currentPoint[1]
);
}
$path = $path->ellipticArc(
$this->intensity,
$this->intensity,
0,
false,
$sweep,
$currentPoint[0],
$currentPoint[1] + ($up ? -$this->intensity : $this->intensity)
);
} else {
$up = $previousPoint[1] > $currentPoint[1];
$right = $nextPoint[0] > $currentPoint[0];
$sweep = ! ($up xor $right);
if ($this->intensity < 0.5
|| ($up && $previousPoint[1] !== $currentPoint[1] + 1)
|| (! $up && $previousPoint[0] + 1 !== $currentPoint[0])
) {
$path = $path->line(
$currentPoint[0],
$currentPoint[1] + ($up ? $this->intensity : -$this->intensity)
);
}
$path = $path->ellipticArc(
$this->intensity,
$this->intensity,
0,
false,
$sweep,
$currentPoint[0] + ($right ? $this->intensity : -$this->intensity),
$currentPoint[1]
);
}
}
$path = $path->close();
}
return $path;
}
}

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Renderer\Module\EdgeIterator\EdgeIterator;
use BaconQrCode\Renderer\Path\Path;
/**
* Groups modules together to a single path.
*/
final class SquareModule implements ModuleInterface
{
private static ?SquareModule $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
public function createPath(ByteMatrix $matrix) : Path
{
$path = new Path();
foreach (new EdgeIterator($matrix) as $edge) {
$points = $edge->getSimplifiedPoints();
$length = count($points);
$path = $path->move($points[0][0], $points[0][1]);
for ($i = 1; $i < $length; ++$i) {
$path = $path->line($points[$i][0], $points[$i][1]);
}
$path = $path->close();
}
return $path;
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Close implements OperationInterface
{
private static ?Close $instance = null;
private function __construct()
{
}
public static function instance() : self
{
return self::$instance ?: self::$instance = new self();
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return $this;
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
return $this;
}
}

View file

@ -0,0 +1,86 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Curve implements OperationInterface
{
public function __construct(
private readonly float $x1,
private readonly float $y1,
private readonly float $x2,
private readonly float $y2,
private readonly float $x3,
private readonly float $y3
) {
}
public function getX1() : float
{
return $this->x1;
}
public function getY1() : float
{
return $this->y1;
}
public function getX2() : float
{
return $this->x2;
}
public function getY2() : float
{
return $this->y2;
}
public function getX3() : float
{
return $this->x3;
}
public function getY3() : float
{
return $this->y3;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->x1 + $x,
$this->y1 + $y,
$this->x2 + $x,
$this->y2 + $y,
$this->x3 + $x,
$this->y3 + $y
);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$x1r = $this->x1 * $cos - $this->y1 * $sin;
$y1r = $this->x1 * $sin + $this->y1 * $cos;
$x2r = $this->x2 * $cos - $this->y2 * $sin;
$y2r = $this->x2 * $sin + $this->y2 * $cos;
$x3r = $this->x3 * $cos - $this->y3 * $sin;
$y3r = $this->x3 * $sin + $this->y3 * $cos;
return new self(
$x1r,
$y1r,
$x2r,
$y2r,
$x3r,
$y3r
);
}
}

View file

@ -0,0 +1,264 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class EllipticArc implements OperationInterface
{
private const ZERO_TOLERANCE = 1e-05;
private float $xRadius;
private float $yRadius;
private float $xAxisAngle;
public function __construct(
float $xRadius,
float $yRadius,
float $xAxisAngle,
private readonly bool $largeArc,
private readonly bool $sweep,
private readonly float $x,
private readonly float $y
) {
$this->xRadius = abs($xRadius);
$this->yRadius = abs($yRadius);
$this->xAxisAngle = $xAxisAngle % 360;
}
public function getXRadius() : float
{
return $this->xRadius;
}
public function getYRadius() : float
{
return $this->yRadius;
}
public function getXAxisAngle() : float
{
return $this->xAxisAngle;
}
public function isLargeArc() : bool
{
return $this->largeArc;
}
public function isSweep() : bool
{
return $this->sweep;
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self(
$this->xRadius,
$this->yRadius,
$this->xAxisAngle,
$this->largeArc,
$this->sweep,
$this->x + $x,
$this->y + $y
);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$xr = $this->x * $cos - $this->y * $sin;
$yr = $this->x * $sin + $this->y * $cos;
return new self(
$this->xRadius,
$this->yRadius,
$this->xAxisAngle,
$this->largeArc,
$this->sweep,
$xr,
$yr
);
}
/**
* Converts the elliptic arc to multiple curves.
*
* Since not all image back ends support elliptic arcs, this method allows to convert the arc into multiple curves
* resembling the same result.
*
* @see https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
* @return array<Curve|Line>
*/
public function toCurves(float $fromX, float $fromY) : array
{
if (sqrt(($fromX - $this->x) ** 2 + ($fromY - $this->y) ** 2) < self::ZERO_TOLERANCE) {
return [];
}
if ($this->xRadius < self::ZERO_TOLERANCE || $this->yRadius < self::ZERO_TOLERANCE) {
return [new Line($this->x, $this->y)];
}
return $this->createCurves($fromX, $fromY);
}
/**
* @return Curve[]
*/
private function createCurves(float $fromX, float $fromY) : array
{
$xAngle = deg2rad($this->xAxisAngle);
list($centerX, $centerY, $radiusX, $radiusY, $startAngle, $deltaAngle) =
$this->calculateCenterPointParameters($fromX, $fromY, $xAngle);
$s = $startAngle;
$e = $s + $deltaAngle;
$sign = ($e < $s) ? -1 : 1;
$remain = abs($e - $s);
$p1 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s);
$curves = [];
while ($remain > self::ZERO_TOLERANCE) {
$step = min($remain, pi() / 2);
$signStep = $step * $sign;
$p2 = self::point($centerX, $centerY, $radiusX, $radiusY, $xAngle, $s + $signStep);
$alphaT = tan($signStep / 2);
$alpha = sin($signStep) * (sqrt(4 + 3 * $alphaT ** 2) - 1) / 3;
$d1 = self::derivative($radiusX, $radiusY, $xAngle, $s);
$d2 = self::derivative($radiusX, $radiusY, $xAngle, $s + $signStep);
$curves[] = new Curve(
$p1[0] + $alpha * $d1[0],
$p1[1] + $alpha * $d1[1],
$p2[0] - $alpha * $d2[0],
$p2[1] - $alpha * $d2[1],
$p2[0],
$p2[1]
);
$s += $signStep;
$remain -= $step;
$p1 = $p2;
}
return $curves;
}
/**
* @return float[]
*/
private function calculateCenterPointParameters(float $fromX, float $fromY, float $xAngle): array
{
$rX = $this->xRadius;
$rY = $this->yRadius;
// F.6.5.1
$dx2 = ($fromX - $this->x) / 2;
$dy2 = ($fromY - $this->y) / 2;
$x1p = cos($xAngle) * $dx2 + sin($xAngle) * $dy2;
$y1p = -sin($xAngle) * $dx2 + cos($xAngle) * $dy2;
// F.6.5.2
$rxs = $rX ** 2;
$rys = $rY ** 2;
$x1ps = $x1p ** 2;
$y1ps = $y1p ** 2;
$cr = $x1ps / $rxs + $y1ps / $rys;
if ($cr > 1) {
$s = sqrt($cr);
$rX *= $s;
$rY *= $s;
$rxs = $rX ** 2;
$rys = $rY ** 2;
}
$dq = ($rxs * $y1ps + $rys * $x1ps);
$pq = ($rxs * $rys - $dq) / $dq;
$q = sqrt(max(0, $pq));
if ($this->largeArc === $this->sweep) {
$q = -$q;
}
$cxp = $q * $rX * $y1p / $rY;
$cyp = -$q * $rY * $x1p / $rX;
// F.6.5.3
$cx = cos($xAngle) * $cxp - sin($xAngle) * $cyp + ($fromX + $this->x) / 2;
$cy = sin($xAngle) * $cxp + cos($xAngle) * $cyp + ($fromY + $this->y) / 2;
// F.6.5.5
$theta = self::angle(1, 0, ($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY);
// F.6.5.6
$delta = self::angle(($x1p - $cxp) / $rX, ($y1p - $cyp) / $rY, (-$x1p - $cxp) / $rX, (-$y1p - $cyp) / $rY);
$delta = fmod($delta, pi() * 2);
if (! $this->sweep) {
$delta -= 2 * pi();
}
return [$cx, $cy, $rX, $rY, $theta, $delta];
}
private static function angle(float $ux, float $uy, float $vx, float $vy) : float
{
// F.6.5.4
$dot = $ux * $vx + $uy * $vy;
$length = sqrt($ux ** 2 + $uy ** 2) * sqrt($vx ** 2 + $vy ** 2);
$angle = acos(min(1, max(-1, $dot / $length)));
if (($ux * $vy - $uy * $vx) < 0) {
return -$angle;
}
return $angle;
}
/**
* @return float[]
*/
private static function point(
float $centerX,
float $centerY,
float $radiusX,
float $radiusY,
float $xAngle,
float $angle
) : array {
return [
$centerX + $radiusX * cos($xAngle) * cos($angle) - $radiusY * sin($xAngle) * sin($angle),
$centerY + $radiusX * sin($xAngle) * cos($angle) + $radiusY * cos($xAngle) * sin($angle),
];
}
/**
* @return float[]
*/
private static function derivative(float $radiusX, float $radiusY, float $xAngle, float $angle) : array
{
return [
-$radiusX * cos($xAngle) * sin($angle) - $radiusY * sin($xAngle) * cos($angle),
-$radiusX * sin($xAngle) * sin($angle) + $radiusY * cos($xAngle) * cos($angle),
];
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Line implements OperationInterface
{
public function __construct(private readonly float $x, private readonly float $y)
{
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self($this->x + $x, $this->y + $y);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$xr = $this->x * $cos - $this->y * $sin;
$yr = $this->x * $sin + $this->y * $cos;
return new self($xr, $yr);
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Move implements OperationInterface
{
public function __construct(private readonly float $x, private readonly float $y)
{
}
public function getX() : float
{
return $this->x;
}
public function getY() : float
{
return $this->y;
}
/**
* @return self
*/
public function translate(float $x, float $y) : OperationInterface
{
return new self($this->x + $x, $this->y + $y);
}
/**
* @return self
*/
public function rotate(int $degrees) : OperationInterface
{
$radians = deg2rad($degrees);
$sin = sin($radians);
$cos = cos($radians);
$xr = $this->x * $cos - $this->y * $sin;
$yr = $this->x * $sin + $this->y * $cos;
return new self($xr, $yr);
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
interface OperationInterface
{
/**
* Translates the operation's coordinates.
*/
public function translate(float $x, float $y) : self;
/**
* Rotates the operation's coordinates.
*/
public function rotate(int $degrees) : self;
}

View file

@ -0,0 +1,117 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
use IteratorAggregate;
use Traversable;
/**
* Internal Representation of a vector path.
*/
final class Path implements IteratorAggregate
{
/**
* @var OperationInterface[]
*/
private array $operations = [];
/**
* Moves the drawing operation to a certain position.
*/
public function move(float $x, float $y) : self
{
$path = clone $this;
$path->operations[] = new Move($x, $y);
return $path;
}
/**
* Draws a line from the current position to another position.
*/
public function line(float $x, float $y) : self
{
$path = clone $this;
$path->operations[] = new Line($x, $y);
return $path;
}
/**
* Draws an elliptic arc from the current position to another position.
*/
public function ellipticArc(
float $xRadius,
float $yRadius,
float $xAxisRotation,
bool $largeArc,
bool $sweep,
float $x,
float $y
) : self {
$path = clone $this;
$path->operations[] = new EllipticArc($xRadius, $yRadius, $xAxisRotation, $largeArc, $sweep, $x, $y);
return $path;
}
/**
* Draws a curve from the current position to another position.
*/
public function curve(float $x1, float $y1, float $x2, float $y2, float $x3, float $y3) : self
{
$path = clone $this;
$path->operations[] = new Curve($x1, $y1, $x2, $y2, $x3, $y3);
return $path;
}
/**
* Closes a sub-path.
*/
public function close() : self
{
$path = clone $this;
$path->operations[] = Close::instance();
return $path;
}
/**
* Appends another path to this one.
*/
public function append(self $other) : self
{
$path = clone $this;
$path->operations = array_merge($this->operations, $other->operations);
return $path;
}
public function translate(float $x, float $y) : self
{
$path = new self();
foreach ($this->operations as $operation) {
$path->operations[] = $operation->translate($x, $y);
}
return $path;
}
public function rotate(int $degrees) : self
{
$path = new self();
foreach ($this->operations as $operation) {
$path->operations[] = $operation->rotate($degrees);
}
return $path;
}
/**
* @return Traversable<int, OperationInterface>
*/
public function getIterator() : Traversable
{
foreach ($this->operations as $operation) {
yield $operation;
}
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\QrCode;
use BaconQrCode\Exception\InvalidArgumentException;
final class PlainTextRenderer implements RendererInterface
{
/**
* UTF-8 full block (U+2588)
*/
private const FULL_BLOCK = "\xe2\x96\x88";
/**
* UTF-8 upper half block (U+2580)
*/
private const UPPER_HALF_BLOCK = "\xe2\x96\x80";
/**
* UTF-8 lower half block (U+2584)
*/
private const LOWER_HALF_BLOCK = "\xe2\x96\x84";
/**
* UTF-8 no-break space (U+00A0)
*/
private const EMPTY_BLOCK = "\xc2\xa0";
public function __construct(private readonly int $margin = 2)
{
}
/**
* @throws InvalidArgumentException if matrix width doesn't match height
*/
public function render(QrCode $qrCode) : string
{
$matrix = $qrCode->getMatrix();
$matrixSize = $matrix->getWidth();
if ($matrixSize !== $matrix->getHeight()) {
throw new InvalidArgumentException('Matrix must have the same width and height');
}
$rows = $matrix->getArray()->toArray();
if (0 !== $matrixSize % 2) {
$rows[] = array_fill(0, $matrixSize, 0);
}
$horizontalMargin = str_repeat(self::EMPTY_BLOCK, $this->margin);
$result = str_repeat("\n", (int) ceil($this->margin / 2));
for ($i = 0; $i < $matrixSize; $i += 2) {
$result .= $horizontalMargin;
$upperRow = $rows[$i];
$lowerRow = $rows[$i + 1];
for ($j = 0; $j < $matrixSize; ++$j) {
$upperBit = $upperRow[$j];
$lowerBit = $lowerRow[$j];
if ($upperBit) {
$result .= $lowerBit ? self::FULL_BLOCK : self::UPPER_HALF_BLOCK;
} else {
$result .= $lowerBit ? self::LOWER_HALF_BLOCK : self::EMPTY_BLOCK;
}
}
$result .= $horizontalMargin . "\n";
}
$result .= str_repeat("\n", (int) ceil($this->margin / 2));
return $result;
}
}

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer;
use BaconQrCode\Encoder\QrCode;
interface RendererInterface
{
public function render(QrCode $qrCode) : string;
}

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\ColorInterface;
final class EyeFill
{
private static ?EyeFill $inherit = null;
public function __construct(
private readonly ?ColorInterface $externalColor,
private readonly ?ColorInterface $internalColor
) {
}
public static function uniform(ColorInterface $color) : self
{
return new self($color, $color);
}
public static function inherit() : self
{
return self::$inherit ?: self::$inherit = new self(null, null);
}
public function inheritsBothColors() : bool
{
return null === $this->externalColor && null === $this->internalColor;
}
public function inheritsExternalColor() : bool
{
return null === $this->externalColor;
}
public function inheritsInternalColor() : bool
{
return null === $this->internalColor;
}
public function getExternalColor() : ColorInterface
{
if (null === $this->externalColor) {
throw new RuntimeException('External eye color inherits foreground color');
}
return $this->externalColor;
}
public function getInternalColor() : ColorInterface
{
if (null === $this->internalColor) {
throw new RuntimeException('Internal eye color inherits foreground color');
}
return $this->internalColor;
}
}

View file

@ -0,0 +1,129 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\ColorInterface;
use BaconQrCode\Renderer\Color\Gray;
final class Fill
{
private static ?Fill $default = null;
private function __construct(
private readonly ColorInterface $backgroundColor,
private readonly ?ColorInterface $foregroundColor,
private readonly ?Gradient $foregroundGradient,
private readonly EyeFill $topLeftEyeFill,
private readonly EyeFill $topRightEyeFill,
private readonly EyeFill $bottomLeftEyeFill
) {
}
public static function default() : self
{
return self::$default ?: self::$default = self::uniformColor(new Gray(100), new Gray(0));
}
public static function withForegroundColor(
ColorInterface $backgroundColor,
ColorInterface $foregroundColor,
EyeFill $topLeftEyeFill,
EyeFill $topRightEyeFill,
EyeFill $bottomLeftEyeFill
) : self {
return new self(
$backgroundColor,
$foregroundColor,
null,
$topLeftEyeFill,
$topRightEyeFill,
$bottomLeftEyeFill
);
}
public static function withForegroundGradient(
ColorInterface $backgroundColor,
Gradient $foregroundGradient,
EyeFill $topLeftEyeFill,
EyeFill $topRightEyeFill,
EyeFill $bottomLeftEyeFill
) : self {
return new self(
$backgroundColor,
null,
$foregroundGradient,
$topLeftEyeFill,
$topRightEyeFill,
$bottomLeftEyeFill
);
}
public static function uniformColor(ColorInterface $backgroundColor, ColorInterface $foregroundColor) : self
{
return new self(
$backgroundColor,
$foregroundColor,
null,
EyeFill::inherit(),
EyeFill::inherit(),
EyeFill::inherit()
);
}
public static function uniformGradient(ColorInterface $backgroundColor, Gradient $foregroundGradient) : self
{
return new self(
$backgroundColor,
null,
$foregroundGradient,
EyeFill::inherit(),
EyeFill::inherit(),
EyeFill::inherit()
);
}
public function hasGradientFill() : bool
{
return null !== $this->foregroundGradient;
}
public function getBackgroundColor() : ColorInterface
{
return $this->backgroundColor;
}
public function getForegroundColor() : ColorInterface
{
if (null === $this->foregroundColor) {
throw new RuntimeException('Fill uses a gradient, thus no foreground color is available');
}
return $this->foregroundColor;
}
public function getForegroundGradient() : Gradient
{
if (null === $this->foregroundGradient) {
throw new RuntimeException('Fill uses a single color, thus no foreground gradient is available');
}
return $this->foregroundGradient;
}
public function getTopLeftEyeFill() : EyeFill
{
return $this->topLeftEyeFill;
}
public function getTopRightEyeFill() : EyeFill
{
return $this->topRightEyeFill;
}
public function getBottomLeftEyeFill() : EyeFill
{
return $this->bottomLeftEyeFill;
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use BaconQrCode\Renderer\Color\ColorInterface;
final class Gradient
{
public function __construct(
private readonly ColorInterface $startColor,
private readonly ColorInterface $endColor,
private readonly GradientType $type
) {
}
public function getStartColor() : ColorInterface
{
return $this->startColor;
}
public function getEndColor() : ColorInterface
{
return $this->endColor;
}
public function getType() : GradientType
{
return $this->type;
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use DASPRiD\Enum\AbstractEnum;
/**
* @method static self VERTICAL()
* @method static self HORIZONTAL()
* @method static self DIAGONAL()
* @method static self INVERSE_DIAGONAL()
* @method static self RADIAL()
*/
final class GradientType extends AbstractEnum
{
protected const VERTICAL = null;
protected const HORIZONTAL = null;
protected const DIAGONAL = null;
protected const INVERSE_DIAGONAL = null;
protected const RADIAL = null;
}

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use BaconQrCode\Renderer\Eye\EyeInterface;
use BaconQrCode\Renderer\Eye\ModuleEye;
use BaconQrCode\Renderer\Module\ModuleInterface;
use BaconQrCode\Renderer\Module\SquareModule;
final class RendererStyle
{
private ModuleInterface $module;
private EyeInterface|null $eye;
private Fill $fill;
public function __construct(
private int $size,
private int $margin = 4,
?ModuleInterface $module = null,
?EyeInterface $eye = null,
?Fill $fill = null
) {
$this->module = $module ?: SquareModule::instance();
$this->eye = $eye ?: new ModuleEye($this->module);
$this->fill = $fill ?: Fill::default();
}
public function withSize(int $size) : self
{
$style = clone $this;
$style->size = $size;
return $style;
}
public function withMargin(int $margin) : self
{
$style = clone $this;
$style->margin = $margin;
return $style;
}
public function getSize() : int
{
return $this->size;
}
public function getMargin() : int
{
return $this->margin;
}
public function getModule() : ModuleInterface
{
return $this->module;
}
public function getEye() : EyeInterface
{
return $this->eye;
}
public function getFill() : Fill
{
return $this->fill;
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use BaconQrCode\Encoder\Encoder;
use BaconQrCode\Exception\InvalidArgumentException;
use BaconQrCode\Renderer\RendererInterface;
/**
* QR code writer.
*/
final class Writer
{
/**
* Creates a new writer with a specific renderer.
*/
public function __construct(private readonly RendererInterface $renderer)
{
}
/**
* Writes QR code and returns it as string.
*
* Content is a string which *should* be encoded in UTF-8, in case there are
* non ASCII-characters present.
*
* @throws InvalidArgumentException if the content is empty
*/
public function writeString(
string $content,
string $encoding = Encoder::DEFAULT_BYTE_MODE_ENCODING,
?ErrorCorrectionLevel $ecLevel = null,
?Version $forcedVersion = null
) : string {
if (strlen($content) === 0) {
throw new InvalidArgumentException('Found empty contents');
}
if (null === $ecLevel) {
$ecLevel = ErrorCorrectionLevel::L();
}
return $this->renderer->render(Encoder::encode($content, $ecLevel, $encoding, $forcedVersion));
}
/**
* Writes QR code to a file.
*
* @see Writer::writeString()
*/
public function writeFile(
string $content,
string $filename,
string $encoding = Encoder::DEFAULT_BYTE_MODE_ENCODING,
?ErrorCorrectionLevel $ecLevel = null,
?Version $forcedVersion = null
) : void {
file_put_contents($filename, $this->writeString($content, $encoding, $ecLevel, $forcedVersion));
}
}

120
vendor/bin/yaml-lint vendored Executable file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
exit(0);
}
}
include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Smiley <smiley@chillerlan.net>
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,181 @@
# chillerlan/php-authenticator
A generator for counter based ([RFC 4226](https://tools.ietf.org/html/rfc4226)) and time based ([RFC 6238](https://tools.ietf.org/html/rfc6238)) one time passwords (OTP). (a.k.a. Yet Another Google Authenticator Implementation!)
[![License][license-badge]][license]
[![GitHub actions workflow][gh-action-badge]][gh-action]
[![Coverage][coverage-badge]][coverage]
[![Codacy][codacy-badge]][codacy]
[license-badge]: https://img.shields.io/github/license/chillerlan/php-authenticator.svg
[license]: https://github.com/chillerlan/php-authenticator/blob/v5.x/LICENSE
[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-authenticator/ci.yml?branch=v5.x&logo=github&logoColor=fff
[gh-action]: https://github.com/chillerlan/php-authenticator/actions?query=branch%3Av5.x
[coverage-badge]: https://img.shields.io/codecov/c/gh/chillerlan/php-authenticator/v5.x?logo=codecov&logoColor=fff
[coverage]: https://app.codecov.io/github/chillerlan/php-authenticator/tree/v5.x
[codacy-badge]: https://img.shields.io/codacy/grade/a2793225b448495c9659f27f7f52380a/v5.x?logo=codacy&logoColor=fff
[codacy]: https://www.codacy.com/gh/chillerlan/php-authenticator/dashboard?branch=v5.x
# Documentation
## Requirements
- PHP 8.2+
- [`ext-curl`](https://www.php.net/manual/book.curl) for Steam Guard server time synchronization
- [`ext-sodium`](https://www.php.net/manual/book.sodium) for constant time implementations of base64 encode/decode and hex2bin/bin2hex
([`paragonie/constant_time_encoding`](https://github.com/paragonie/constant_time_encoding) is used as fallback)
## Installation
**requires [composer](https://getcomposer.org)**
via terminal: `composer require chillerlan/php-authenticator`
**composer.json**
```json
{
"require": {
"php": "^8.2",
"chillerlan/php-authenticator": "dev-v5.x"
}
}
```
Note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^5.0` - see [releases](https://github.com/chillerlan/php-authenticator/releases) for valid versions
Profit!
## Usage
### Create a secret
The secret is usually being created once during the activation process in a user control panel.
So all you need to do there is to display it to the user in a convenient way -
as a text string and QR code for example - and save it somewhere with the user data.
```php
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
$options = new AuthenticatorOptions;
$options->secret_length = 32;
$authenticator = new Authenticator($options);
// create a secret (stored somewhere in a *safe* place on the server. safe... hahaha jk)
$secret = $authenticator->createSecret();
// you can also specify the length of the secret key, which overrides the options setting
$secret = $authenticator->createSecret(20);
// set an existing secret
$authenticator->setSecret($secret);
```
A secret created with `Authenticator::createSecret()` will also be stored internally,
so that you don't need to provide the secret you just created on follow-up operations with the current instance.
### Verify a one time code
Now during the login process - after the user has successfully entered their credentials - you would
ask them for a one time code to check it against the secret from your user database.
```php
// verify the code
if($authenticator->verify($otp)){
// that's it - 2FA has never been easier! :D
}
```
#### time based (TOTP)
Verify adjacent codes
```php
// try the first adjacent
$authenticator->verify($otp, time() - $options->period); // -> true
// try the second adjacent, default is 1
$authenticator->verify($otp, time() + 2 * $options->period); // -> false
// allow 2 adjacent codes
$options->adjacent = 2;
$authenticator->verify($otp, time() + 2 * $options->period); // -> true
```
#### counter based (HOTP)
```php
// switch mode to HOTP
$options->mode = AuthenticatorInterface::HOTP;
// user sends the OTP for code #42, which is equivalent to
$otp = $authenticator->code(42); // -> 123456
// verify [123456, 42]
$authenticator->verify($otp, $counterValueFromUserDatabase) // -> true
```
### URI creation
In order to display a QR code for a mobile authenticator you'll need an `otpauth://` URI, which can be created using the following method.
- `$label` should be something that identifies the account to which the secret belongs
- `$issuer` is the name of your website or company for example, so that the user is able to identify multiple accounts.
```php
$uri = $authenticator->getUri($label, $issuer);
// -> otpauth://totp/my%20label?secret=NKSOQG7UKKID4IXW&issuer=chillerlan.net&digits=6&period=30&algorithm=SHA1
```
#### Notes
Keep in mind that several URI settings are not (yet) recognized by all authenticators. Check [the Google Authenticator wiki](https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters) for more info.
```php
// code length, currently 6 or 8
$options->digits = 8;
// valid period between 15 and 60 seconds
$options->period = 45;
// set the HMAC hash algorithm
$options->algorithm = AuthenticatorInterface::ALGO_SHA512;
```
## API
### `Authenticator`
| method | return | description |
|---------------------------------------------------------------------------------------------|-----------------|------------------------------------------------------------------|
| `__construct(SettingsContainerInterface $options = null, string $secret = null)` | - | |
| `setOptions(SettingsContainerInterface $options)` | `Authenticator` | called internally by `__construct()` |
| `setSecret(string $secret)` | `Authenticator` | called internally by `__construct()` |
| `getSecret()` | `string` | |
| `createSecret(int $length = null)` | `string` | `$length` overrides `AuthenticatorOptions` setting |
| `code(int $data = null)` | `string` | `$data` may be a UNIX timestamp (TOTP) or a counter value (HOTP) |
| `verify(string $otp, int $data = null)` | `bool` | for `$data` see `Authenticator::code()` |
| `getUri(string $label, string $issuer, int $hotpCounter = null, bool $omitSettings = null)` | `string` | |
### `AuthenticatorOptions`
#### Properties
| property | type | default | allowed | description |
|---------------------|----------|---------|----------------------------------------|---------------------------------------------------------------------------------|
| `$digits` | `int` | 6 | 6 or 8 | auth code length |
| `$period` | `int` | 30 | 15 - 60 | validation period (seconds) |
| `$secret_length` | `int` | 20 | &gt;= 16 | length of the secret phrase (bytes, unencoded binary) |
| `$algorithm` | `string` | `SHA1` | `SHA1`, `SHA256` or `SHA512` | HMAC hash algorithm, see `AuthenticatorInterface::HASH_ALGOS` |
| `$mode` | `string` | `totp` | `totp`, `hotp`, `battlenet` or `steam` | authenticator mode: time- or counter based, see `AuthenticatorInterface::MODES` |
| `$adjacent` | `int` | 1 | &gt;= 0 | number of allowed adjacent codes |
| `$time_offset` | `int` | 0 | * | fixed time offset that will be added to the current time value |
| `$useLocalTime` | `bool` | true | * | whether to use local time or request server time |
| `$forceTimeRefresh` | `bool` | false | * | whether to force refreshing server time on each call |
### `AuthenticatorInterface`
#### Methods
| method | return | description |
|---------------------------------------------------|--------------------------|-------------|
| `setOptions(SettingsContainerInterface $options)` | `AuthenticatorInterface` | |
| `setSecret(string $encodedSecret)` | `AuthenticatorInterface` | |
| `getSecret()` | `string` | |
| `createSecret(int $length = null)` | `string` | |
| `getServertime()` | `int` | |
| `getCounter(int $data = null)` | `int` | internal |
| `getHMAC(int $counter)` | `string` | internal |
| `getCode(string $hmac)` | `int` | internal |
| `getOTP(int $code)` | `string` | internal |
| `code(int $data = null)` | `string` | |
| `verify(string $otp, int $data = null)` | `bool` | |
#### Constants
| constant | type | description |
|---------------|----------|-----------------------------------|
| `TOTP` | `string` | |
| `HOTP` | `string` | |
| `STEAM_GUARD` | `string` | |
| `ALGO_SHA1` | `string` | |
| `ALGO_SHA256` | `string` | |
| `ALGO_SHA512` | `string` | |
| `MODES` | `array` | map of mode -> classname |
| `HASH_ALGOS` | `array` | list of available hash algorithms |
<p align="center">
<a href="https://twofactorauth.org">
<img alt="2FA ALL THE THINGS!" src="https://raw.githubusercontent.com/chillerlan/php-authenticator/v5.x/.github/images/2fa-all-the-things.jpg">
</a>
</p>

View file

@ -0,0 +1,71 @@
{
"name": "chillerlan/php-authenticator",
"description": "A generator for counter- and time based 2-factor authentication codes (Google Authenticator). PHP 8.2+",
"homepage": "https://github.com/chillerlan/php-authenticator",
"license": "MIT",
"type": "library",
"keywords": [
"2fa", "authenticator", "google2fa", "hotp", "mfa", "otp", "rfc4226", "rfc6238", "tfa", "totp", "two factor"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
},
{
"name": "Contributors",
"homepage":"https://github.com/chillerlan/php-authenticator/graphs/contributors"
}
],
"funding": [
{
"type": "Ko-Fi",
"url": "https://ko-fi.com/codemasher"
}
],
"support": {
"issues": "https://github.com/chillerlan/php-authenticator/issues",
"source": "https://github.com/chillerlan/php-authenticator"
},
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": "^8.2",
"chillerlan/php-settings-container": "^3.2.1",
"paragonie/constant_time_encoding": "^3.0"
},
"require-dev": {
"ext-curl": "*",
"ext-json": "*",
"ext-sodium": "*",
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^11.2",
"squizlabs/php_codesniffer": "^3.10"
},
"suggest": {
"chillerlan/php-qrcode": "Create QR Codes for use with an authenticator app."
},
"autoload": {
"psr-4": {
"chillerlan\\Authenticator\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\AuthenticatorTest\\": "tests"
}
},
"scripts": {
"phpcs": "@php vendor/bin/phpcs",
"phpunit": "@php vendor/bin/phpunit",
"phpstan": "@php vendor/bin/phpstan"
},
"config": {
"lock": false,
"sort-packages": true,
"platform-check": true
}
}

View file

@ -0,0 +1,168 @@
<?php
/**
* Class Authenticator
*
* @created 24.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator;
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
use chillerlan\Settings\SettingsContainerInterface;
use InvalidArgumentException;
use SensitiveParameter;
use function http_build_query;
use function rawurlencode;
use function sprintf;
use function trim;
use const PHP_QUERY_RFC3986;
/**
* Yet another Google authenticator implementation!
*
* @link https://tools.ietf.org/html/rfc4226
* @link https://tools.ietf.org/html/rfc6238
* @link https://github.com/google/google-authenticator
* @link https://openauthentication.org/specifications-technical-resources/
* @link https://blog.ircmaxell.com/2014/11/its-all-about-time.html
*/
class Authenticator{
protected SettingsContainerInterface|AuthenticatorOptions $options;
protected AuthenticatorInterface $authenticator;
protected string $mode = AuthenticatorInterface::TOTP;
/**
* Authenticator constructor
*/
public function __construct(
SettingsContainerInterface|AuthenticatorOptions $options = new AuthenticatorOptions,
string|null $secret = null
){
// phpcs:ignore
$this->setOptions($options);
if($secret !== null){
$this->setSecret($secret);
}
}
/**
* Sets an options instance and invokes an authenticator according to the given mode
*
* Please note that this will reset the secret phrase stored with the authenticator instance
* if a different mode than the current is given.
*/
public function setOptions(SettingsContainerInterface|AuthenticatorOptions $options):self{
$this->options = $options;
// invoke a new authenticator interface if necessary
if(!isset($this->authenticator) || $this->options->mode !== $this->mode){
$this->mode = $this->options->mode;
$this->authenticator = new (AuthenticatorInterface::MODES[$this->options->mode])($this->options);
}
$this->authenticator->setOptions($this->options);
return $this;
}
/**
* Sets a secret phrase from a Base32 representation
*
* @codeCoverageIgnore
*/
public function setSecret(#[SensitiveParameter] string $encodedSecret):self{
$this->authenticator->setSecret($encodedSecret);
return $this;
}
/**
* Returns a Base32 representation of the current secret phrase
*
* @codeCoverageIgnore
*/
public function getSecret():string{
return $this->authenticator->getSecret();
}
/**
* Generates a new (secure random) secret phrase
*
* @codeCoverageIgnore
*/
public function createSecret(int|null $length = null):string{
return $this->authenticator->createSecret($length);
}
/**
* Creates a new OTP code with the given secret
*
* $data may be
* - a UNIX timestamp (TOTP)
* - a counter value (HOTP)
*
* @codeCoverageIgnore
*/
public function code(int|null $data = null):string{
return $this->authenticator->code($data);
}
/**
* Checks the given $code against the secret
*
* $data may be
* - a UNIX timestamp (TOTP)
* - a counter value (HOTP)
*
* @codeCoverageIgnore
*/
public function verify(#[SensitiveParameter] string $otp, int|null $data = null):bool{
return $this->authenticator->verify($otp, $data);
}
/**
* Creates a URI for use in QR codes for example
*
* @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters
*
* @throws \InvalidArgumentException
*/
public function getUri(string $label, string $issuer, int|null $hotpCounter = null, bool|null $omitSettings = null):string{
$label = trim($label);
$issuer = trim($issuer);
if(empty($label) || empty($issuer)){
throw new InvalidArgumentException('$label and $issuer cannot be empty');
}
$values = [
'secret' => $this->authenticator->getSecret(),
'issuer' => $issuer,
];
if($omitSettings !== true){
$values['digits'] = $this->options->digits;
$values['algorithm'] = $this->options->algorithm;
if($this->mode === AuthenticatorInterface::TOTP){
$values['period'] = $this->options->period;
}
if($this->mode === AuthenticatorInterface::HOTP && $hotpCounter !== null){
$values['counter'] = $hotpCounter;
}
}
$values = http_build_query($values, '', '&', PHP_QUERY_RFC3986);
return sprintf('otpauth://%s/%s?%s', $this->mode, rawurlencode($label), $values);
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* Class AuthenticatorOptions
*
* @created 07.03.2019
* @author smiley <smiley@chillerlan.net>
* @copyright 2019 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator;
use chillerlan\Settings\SettingsContainerAbstract;
/**
*
*/
class AuthenticatorOptions extends SettingsContainerAbstract{
use AuthenticatorOptionsTrait;
}

View file

@ -0,0 +1,178 @@
<?php
/**
* Trait AuthenticatorOptionsTrait
*
* @created 07.03.2019
* @author smiley <smiley@chillerlan.net>
* @copyright 2019 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator;
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
use InvalidArgumentException;
use function in_array;
use function strtolower;
use function strtoupper;
/**
* @property int $digits
* @property int $period
* @property int $secret_length
* @property string $algorithm
* @property string $mode
* @property int $adjacent
* @property int $time_offset
* @property bool $useLocalTime
* @property bool $forceTimeRefresh
*/
trait AuthenticatorOptionsTrait{
/**
* Code length: either 6 or 8
*/
protected int $digits = 6;
/**
* Validation period (seconds): 15 - 60
*/
protected int $period = 30;
/**
* Length of the secret phrase (bytes, unencoded binary)
*
* @see \random_bytes()
*/
protected int $secret_length = 20;
/**
* Hash algorithm:
*
* - `AuthenticatorInterface::ALGO_SHA1`
* - `AuthenticatorInterface::ALGO_SHA256`
* - `AuthenticatorInterface::ALGO_SHA512`
*/
protected string $algorithm = AuthenticatorInterface::ALGO_SHA1;
/**
* Authenticator mode:
*
* - `AuthenticatorInterface::HOTP` = counter based
* - `AuthenticatorInterface::TOTP` = time based
* - `AuthenticatorInterface::STEAM` = time based (Steam Guard)
*/
protected string $mode = AuthenticatorInterface::TOTP;
/**
* Number of allowed adjacent codes
*/
protected int $adjacent = 1;
/**
* A fixed time offset that will be added to the current time value
*
* @see \chillerlan\Authenticator\Authenticators\AuthenticatorInterface::getCounter()
*/
protected int $time_offset = 0;
/**
* Whether to use local time or request server time from the API
*
* This can be useful when the device time sync is unreliable.
*
* note: API requests needs ext-curl installed
*/
protected bool $useLocalTime = true;
/**
* Whether to force refreshing server time on each call or use the time returned from the last request
*/
protected bool $forceTimeRefresh = false;
/**
* Sets the code length to either 6 or 8
*
* @throws \InvalidArgumentException
*/
protected function set_digits(int $digits):void{
if(!in_array($digits, [6, 8], true)){
throw new InvalidArgumentException('Invalid code length: '.$digits);
}
$this->digits = $digits;
}
/**
* Sets the period to a value between 15 and 60 seconds
*
* @throws \InvalidArgumentException
*/
protected function set_period(int $period):void{
if($period < 15 || $period > 60){
throw new InvalidArgumentException('Invalid period: '.$period);
}
$this->period = $period;
}
/**
* Sets the hash algorithm
*
* @throws \InvalidArgumentException
*/
protected function set_algorithm(string $algorithm):void{
$algorithm = strtoupper($algorithm);
if(!in_array($algorithm, AuthenticatorInterface::HASH_ALGOS, true)){
throw new InvalidArgumentException('Invalid algorithm: '.$algorithm);
}
$this->algorithm = $algorithm;
}
/**
* Sets the authenticator mode
*
* @throws \InvalidArgumentException
*/
protected function set_mode(string $mode):void{
$mode = strtolower($mode);
if(!isset(AuthenticatorInterface::MODES[$mode])){
throw new InvalidArgumentException('Invalid mode: '.$mode);
}
$this->mode = $mode;
}
/**
* Sets the adjacent amount
*
* @throws \InvalidArgumentException
*/
protected function set_adjacent(int $adjacent):void{
if($adjacent < 0){
throw new InvalidArgumentException('Invalid adjacent: '.$adjacent);
}
$this->adjacent = $adjacent;
}
/**
* @throws \InvalidArgumentException
*/
protected function set_secret_length(int $secret_length):void{
// ~ 80 to 640 bits
if($secret_length < 16 || $secret_length > 1024){
throw new InvalidArgumentException('Invalid secret length: '.$secret_length);
}
$this->secret_length = $secret_length;
}
}

View file

@ -0,0 +1,119 @@
<?php
/**
* Class AuthenticatorAbstract
*
* @created 25.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use chillerlan\Authenticator\AuthenticatorOptions;
use chillerlan\Authenticator\Common\Base32;
use chillerlan\Settings\SettingsContainerInterface;
use InvalidArgumentException;
use RuntimeException;
use SensitiveParameter;
use function random_bytes;
use function time;
use function trim;
/**
*
*/
abstract class AuthenticatorAbstract implements AuthenticatorInterface{
protected const userAgent = 'chillerlanAuthenticator/5.0 +https://github.com/chillerlan/php-authenticator';
protected SettingsContainerInterface|AuthenticatorOptions $options;
protected string|null $secret = null;
protected int $serverTime = 0;
protected int $lastRequestTime = 0;
/**
* AuthenticatorInterface constructor
*/
public function __construct(SettingsContainerInterface|AuthenticatorOptions $options = new AuthenticatorOptions){
$this->setOptions($options);
}
/**
* @inheritDoc
*/
public function setOptions(SettingsContainerInterface $options):AuthenticatorInterface{
$this->options = $options;
return $this;
}
/**
* @inheritDoc
*/
public function setSecret(#[SensitiveParameter] string $encodedSecret):AuthenticatorInterface{
$this->secret = Base32::decode($this->checkEncodedSecret($encodedSecret));
return $this;
}
/**
* @inheritDoc
*/
public function getSecret():string{
if($this->secret === null){
throw new RuntimeException('No secret set');
}
return Base32::encode($this->secret);
}
/**
* @inheritDoc
*/
public function createSecret(int|null $length = null):string{
$length ??= $this->options->secret_length;
if($length < 16){
throw new InvalidArgumentException('Invalid secret length: '.$length);
}
$this->secret = random_bytes($length);
return $this->getSecret();
}
/**
* @inheritDoc
*/
public function getServertime():int{
return time();
}
/**
* Get an adjusted time stamp for the given server time
*/
protected function getAdjustedTime(int $serverTime, int $lastRequestTime):int{
$diff = (time() - $lastRequestTime);
return ($serverTime + $diff);
}
/**
* Checks if the encoded secret is non-empty, returns the trimmed string on success
*
* @throws \InvalidArgumentException
*/
protected function checkEncodedSecret(string $encodedSecret):string{
$encodedSecret = trim($encodedSecret);
if($encodedSecret === ''){
throw new InvalidArgumentException('The given secret string is empty');
}
return $encodedSecret;
}
}

View file

@ -0,0 +1,119 @@
<?php
/**
* Interface AuthenticatorInterface
*
* @created 15.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use chillerlan\Settings\SettingsContainerInterface;
use SensitiveParameter;
/**
*
*/
interface AuthenticatorInterface{
public const TOTP = 'totp';
public const HOTP = 'hotp';
public const STEAM_GUARD = 'steam';
public const ALGO_SHA1 = 'SHA1';
public const ALGO_SHA256 = 'SHA256';
public const ALGO_SHA512 = 'SHA512';
public const MODES = [
self::HOTP => HOTP::class,
self::TOTP => TOTP::class,
self::STEAM_GUARD => SteamGuard::class,
];
public const HASH_ALGOS = [
self::ALGO_SHA1,
self::ALGO_SHA256,
self::ALGO_SHA512,
];
/**
* Sets the options
*/
public function setOptions(SettingsContainerInterface $options):AuthenticatorInterface;
/**
* Sets a secret phrase from an encoded representation
*
* @throws \RuntimeException
*/
public function setSecret(#[SensitiveParameter] string $encodedSecret):AuthenticatorInterface;
/**
* Returns an encoded representation of the current secret phrase
*
* @throws \RuntimeException
*/
public function getSecret():string;
/**
* Generates a new (secure random) secret phrase
*
* @throws \InvalidArgumentException
*/
public function createSecret(int|null $length = null):string;
/**
* Returns the current server time as UNIX timestamp for the given application (or `time()` if not applicable)
*/
public function getServertime():int;
/**
* Prepares the given $data value and returns an integer that will be passed as counter value to the hash function
*
* @internal
*/
public function getCounter(int|null $data = null):int;
/**
* HMAC hashes the given $data integer with the given secret
*
* @internal
*/
public function getHMAC(int $counter):string;
/**
* Extracts the intermediate code from the given $hmac hash
*
* @internal
*/
public function getCode(#[SensitiveParameter] string $hmac):int;
/**
* Formats the final output OTP from the given intermediate $code
*
* @internal
*/
public function getOTP(#[SensitiveParameter] int $code):string;
/**
* Creates a new OTP code with the given secret
*
* $data may be
* - a UNIX timestamp (TOTP)
* - a counter value (HOTP)
*/
public function code(int|null $data = null):string;
/**
* Checks the given $code against the secret
*
* $data may be
* - a UNIX timestamp (TOTP)
* - a counter value (HOTP)
*/
public function verify(#[SensitiveParameter] string $otp, int|null $data = null):bool;
}

View file

@ -0,0 +1,93 @@
<?php
/**
* Class HOTP
*
* @created 15.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use RuntimeException;
use SensitiveParameter;
use function hash_equals;
use function hash_hmac;
use function pack;
use function str_pad;
use function strlen;
use function unpack;
use const PHP_INT_SIZE;
use const STR_PAD_LEFT;
/**
* @link https://tools.ietf.org/html/rfc4226
*/
class HOTP extends AuthenticatorAbstract{
/**
* @inheritDoc
*/
public function getCounter(int|null $data = null):int{
return ($data ?? 0);
}
/**
* @inheritDoc
*/
public function getHMAC(int $counter):string{
if($this->secret === null){
throw new RuntimeException('No secret given');
}
// @codeCoverageIgnoreStart
$data = (PHP_INT_SIZE < 8)
? "\x00\x00\x00\x00".pack('N', $counter)
: pack('J', $counter);
// @codeCoverageIgnoreEnd
return hash_hmac($this->options->algorithm, $data, $this->secret, true);
}
/**
* @inheritDoc
*/
public function getCode(#[SensitiveParameter] string $hmac):int{
$data = unpack('C*', $hmac);
if($data === false){
throw new RuntimeException('error while unpacking HMAC'); // @codeCoverageIgnore
}
$b = ($data[strlen($hmac)] & 0xF);
// phpcs:ignore
return (($data[$b + 1] & 0x7F) << 24) | ($data[$b + 2] << 16) | ($data[$b + 3] << 8) | $data[$b + 4];
}
/**
* @inheritDoc
*/
public function getOTP(#[SensitiveParameter] int $code):string{
$code %= (10 ** $this->options->digits);
return str_pad((string)$code, $this->options->digits, '0', STR_PAD_LEFT);
}
/**
* @inheritDoc
*/
public function code(int|null $data = null):string{
$hmac = $this->getHMAC($this->getCounter($data));
return $this->getOTP($this->getCode($hmac));
}
/**
* @inheritDoc
*/
public function verify(#[SensitiveParameter] string $otp, int|null $data = null):bool{
return hash_equals($this->code($data), $otp);
}
}

View file

@ -0,0 +1,162 @@
<?php
/**
* Class SteamGuard
*
* @created 20.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use chillerlan\Authenticator\Common\Base64;
use RuntimeException;
use SensitiveParameter;
use function curl_close;
use function curl_exec;
use function curl_getinfo;
use function curl_init;
use function curl_setopt_array;
use function floor;
use function intdiv;
use function is_array;
use function json_decode;
use function sprintf;
use function time;
use const CURLOPT_HTTPHEADER;
use const CURLOPT_POST;
use const CURLOPT_POSTFIELDS;
use const CURLOPT_RETURNTRANSFER;
/**
* /data/data/com.valvesoftware.android.steam.community/f/Steamguard-STEAMID64
*
* @see https://help.steampowered.com/en/faqs/view/7EFD-3CAE-64D3-1C31
* @see https://github.com/SoftCreatR/php-steam-guard
*/
final class SteamGuard extends TOTP{
private const steamCodeChars = '23456789BCDFGHJKMNPQRTVWXY';
private const steamTimeURL = 'https://api.steampowered.com/ITwoFactorService/QueryTime/v0001';
/**
* @inheritDoc
*/
public function setSecret(#[SensitiveParameter] string $encodedSecret):AuthenticatorInterface{
$this->secret = Base64::decode($this->checkEncodedSecret($encodedSecret));
return $this;
}
/**
* @inheritDoc
*/
public function getSecret():string{
if($this->secret === null){
throw new RuntimeException('No secret set');
}
return Base64::encode($this->secret);
}
/**
* @inheritDoc
* @codeCoverageIgnore
*/
public function createSecret(int|null $length = null):string{
throw new RuntimeException('Not implemented');
}
/**
* @inheritDoc
*/
public function getCounter(int|null $data = null):int{
// the period is fixed to 30 seconds for Steam Guard
$this->options->period = 30;
return parent::getCounter($data);
}
/**
* @inheritDoc
*/
public function getHMAC(int $counter):string{
// algorithm is fixed to sha1 for Steam Guard
$this->options->algorithm = self::ALGO_SHA1;
return parent::getHMAC($counter);
}
/**
* @inheritDoc
*/
public function getOTP(#[SensitiveParameter] int $code):string{
$str = '';
$len = 26; // strlen($this::steamCodeChars)
// length is fixed to 5 for Steam
for($i = 0; $i < 5; $i++){
$str .= $this::steamCodeChars[($code % $len)];
$code = intdiv($code, $len);
}
return $str;
}
/**
* @inheritDoc
* @throws \RuntimeException
*/
public function getServerTime():int{
if($this->options->forceTimeRefresh === false && $this->serverTime !== 0){
return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
}
$this->serverTime = 0;
$this->lastRequestTime = 0;
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => 'steamid=0',
CURLOPT_HTTPHEADER => [sprintf('User-Agent: %s', $this::userAgent)],
];
$ch = curl_init($this::steamTimeURL);
// it's almost impossible to run into this, but hey, phpstan happy
if($ch === false){
throw new RuntimeException('curl_init error'); // @codeCoverageIgnore
}
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if($info['http_code'] !== 200 || $response === false){
// I'm not going to investigate the error further as this shouldn't happen usually
throw new RuntimeException(sprintf('Steam API request error: HTTP/%s', $info['http_code'])); // @codeCoverageIgnore
}
$json = json_decode($response, true);
if(!is_array($json) || !isset($json['response']['server_time'])){
throw new RuntimeException('Unable to decode Steam API response'); // @codeCoverageIgnore
}
$this->serverTime = (int)$json['response']['server_time'];
$this->lastRequestTime = (time() - (int)floor($info['total_time']));
return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* Class TOTP
*
* @created 15.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Authenticators;
use SensitiveParameter;
use function floor;
use function hash_equals;
use function time;
/**
* @link https://tools.ietf.org/html/rfc6238
*/
class TOTP extends HOTP{
/**
* @inheritDoc
*/
public function getCounter(int|null $data = null):int{
$data ??= time();
if($this->options->useLocalTime === false){
$data = $this->getServerTime();
}
return (int)floor(($data + $this->options->time_offset) / $this->options->period);
}
/**
* @inheritDoc
*/
public function verify(#[SensitiveParameter] string $otp, int|null $data = null):bool{
$limit = $this->options->adjacent;
if($limit === 0){
return parent::verify($otp, $data); // @codeCoverageIgnore
}
$timeslice = $this->getCounter($data);
// phpcs:ignore
for($i = -$limit; $i <= $limit; $i++){
$hash = $this->getHMAC($timeslice + $i);
$code = $this->getOTP($this->getCode($hash));
if(hash_equals($code, $otp)){
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* Class Base32
*
* @created 23.06.2023
* @author Smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Common;
use InvalidArgumentException;
use ParagonIE\ConstantTime\Base32 as ConstantTimeBase32;
use SensitiveParameter;
use function preg_match;
/**
* Class to provide base32 encoding/decoding of strings using constant time functions
*/
final class Base32{
/**
* The Base32 character set as defined by RFC3548
*
* @see https://datatracker.ietf.org/doc/html/rfc3548#section-5
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-6
*
* @var string
*/
public const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Encode a string to Base32
*/
public static function encode(#[SensitiveParameter] string $str):string{
return ConstantTimeBase32::encodeUpperUnpadded($str);
}
/**
* Decode a string from Base32
*/
public static function decode(#[SensitiveParameter] string $base32):string{
self::checkCharacterSet($base32);
return ConstantTimeBase32::decodeNoPadding($base32, true);
}
/**
* @throws \InvalidArgumentException
*/
public static function checkCharacterSet(#[SensitiveParameter] string $base32):void{
if(!preg_match('/^['.self::CHARSET.']+$/', $base32)){
throw new InvalidArgumentException('Base32 must match RFC3548 character set');
}
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* Class Base64
*
* @created 23.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Common;
use InvalidArgumentException;
use ParagonIE\ConstantTime\Base64 as ConstantTimeBase64;
use SensitiveParameter;
use function function_exists;
use function preg_match;
/**
* Class to provide base64 encoding/decoding of strings using constant time functions
*/
class Base64{
/**
* The Base64 character set as defined by RFC3548
*
* @see https://datatracker.ietf.org/doc/html/rfc3548#section-3
* @see https://datatracker.ietf.org/doc/html/rfc4648#section-4
*
* @var string
*/
public const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
/**
* Encode a string to Base64
*/
public static function encode(#[SensitiveParameter] string $str):string{
if(function_exists('sodium_bin2base64')){
return \sodium_bin2base64($str, \SODIUM_BASE64_VARIANT_ORIGINAL);
}
return ConstantTimeBase64::encode($str);
}
/**
* Decode a string from Base64
*/
public static function decode(#[SensitiveParameter] string $base64):string{
self::checkCharacterSet($base64);
if(function_exists('sodium_base642bin')){
return \sodium_base642bin($base64, \SODIUM_BASE64_VARIANT_ORIGINAL);
}
return ConstantTimeBase64::decode($base64);
}
/**
* @throws \InvalidArgumentException
*/
public static function checkCharacterSet(#[SensitiveParameter] string $base64):void{
if(!preg_match('#^[a-z\d/=+]+$#i', $base64)){
throw new InvalidArgumentException('Base64 must match RFC4648 character set');
}
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* Class Hex
*
* @created 28.06.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Authenticator\Common;
use InvalidArgumentException;
use ParagonIE\ConstantTime\Hex as ConstantTimeHex;
use SensitiveParameter;
use function preg_match;
/**
*
*/
class Hex{
/**
* The allowed hex character set (either upper or lower case)
*
* @var string
*/
public const CHARSET = '1234567890ABCDEFabcdef';
/**
* Encode a string to hexadecimal
*
* @codeCoverageIgnore
*/
public static function encode(#[SensitiveParameter] string $str):string{
if(function_exists('sodium_bin2hex')){
return \sodium_bin2hex($str);
}
return ConstantTimeHex::encode($str);
}
/**
* Decode a string from hexadecimal
*
* @codeCoverageIgnore
*/
public static function decode(#[SensitiveParameter] string $hex):string{
self::checkCharacterSet($hex);
if(function_exists('sodium_hex2bin')){
return \sodium_hex2bin($hex);
}
return ConstantTimeHex::decode($hex);
}
/**
* @throws \InvalidArgumentException
*/
public static function checkCharacterSet(#[SensitiveParameter] string $hex):void{
if(!preg_match('#^[a-f\d]+$#i', $hex)){
throw new InvalidArgumentException('hex string must match hexadecimal character set: 0-9, A-F, a-f');
}
}
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Smiley <smiley@chillerlan.net>
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,167 @@
# chillerlan/php-settings-container
A container class for settings objects - decouple configuration logic from your application! Not a DI container.
- [`SettingsContainerInterface`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerInterface.php) provides immutable properties with magic getter & setter and some fancy.
[![PHP Version Support][php-badge]][php]
[![version][packagist-badge]][packagist]
[![license][license-badge]][license]
[![Continuous Integration][gh-action-badge]][gh-action]
[![Coverage][coverage-badge]][coverage]
[![Codacy][codacy-badge]][codacy]
[![Packagist downloads][downloads-badge]][downloads]
[php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-settings-container?logo=php&color=8892BF
[php]: https://www.php.net/supported-versions.php
[packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-settings-container.svg?logo=packagist
[packagist]: https://packagist.org/packages/chillerlan/php-settings-container
[license-badge]: https://img.shields.io/github/license/chillerlan/php-settings-container.svg
[license]: https://github.com/chillerlan/php-settings-container/blob/main/LICENSE
[coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-settings-container.svg?logo=codecov
[coverage]: https://codecov.io/github/chillerlan/php-settings-container
[codacy-badge]: https://img.shields.io/codacy/grade/bd2467799e2943d2853ce3ebad5af490/main?logo=codacy
[codacy]: https://www.codacy.com/gh/chillerlan/php-settings-container/dashboard?branch=main
[downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-settings-container.svg?logo=packagist
[downloads]: https://packagist.org/packages/chillerlan/php-settings-container/stats
[gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-settings-container/ci.yml?branch=main&logo=github
[gh-action]: https://github.com/chillerlan/php-settings-container/actions/workflows/ci.yml?query=branch%3Amain
## Documentation
### Installation
**requires [composer](https://getcomposer.org)**
*composer.json* (note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^3.0` - see [releases](https://github.com/chillerlan/php-settings-container/releases) for valid versions)
```json
{
"require": {
"php": "^8.1",
"chillerlan/php-settings-container": "dev-main"
}
}
```
Profit!
## Usage
The `SettingsContainerInterface` (wrapped in`SettingsContainerAbstract`) provides plug-in functionality for immutable object properties and adds some fancy, like loading/saving JSON, arrays etc.
It takes an `iterable` as the only constructor argument and calls a method with the trait's name on invocation (`MyTrait::MyTrait()`) for each used trait.
A PHPStan ruleset to exclude errors generated by accessing magic properties on `SettingsContainerInterface` can be found in `rules-magic-access.neon`.
### Simple usage
```php
class MyContainer extends SettingsContainerAbstract{
protected string $foo;
protected string $bar;
}
```
```php
// use it just like a \stdClass (except the properties are fixed)
$container = new MyContainer;
$container->foo = 'what';
$container->bar = 'foo';
// which is equivalent to
$container = new MyContainer(['bar' => 'foo', 'foo' => 'what']);
// ...or try
$container->fromJSON('{"foo": "what", "bar": "foo"}');
// fetch all properties as array
$container->toArray(); // -> ['foo' => 'what', 'bar' => 'foo']
// or JSON
$container->toJSON(); // -> {"foo": "what", "bar": "foo"}
// JSON via JsonSerializable
$json = json_encode($container); // -> {"foo": "what", "bar": "foo"}
//non-existing properties will be ignored:
$container->nope = 'what';
var_dump($container->nope); // -> null
```
### Advanced usage
```php
// from library 1
trait SomeOptions{
protected string $foo;
protected string $what;
// this method will be called in SettingsContainerAbstract::construct()
// after the properties have been set
protected function SomeOptions():void{
// just some constructor stuff...
$this->foo = strtoupper($this->foo);
}
/*
* special prefixed magic setters & getters
*/
// this method will be called from __set() when property $what is set
protected function set_what(string $value):void{
$this->what = md5($value);
}
// this method is called on __get() for the property $what
protected function get_what():string{
return 'hash: '.$this->what;
}
}
// from library 2
trait MoreOptions{
protected string $bar = 'whatever'; // provide default values
}
```
```php
$commonOptions = [
// SomeOptions
'foo' => 'whatever',
// MoreOptions
'bar' => 'nothing',
];
// now plug the several library options together to a single object
$container = new class ($commonOptions) extends SettingsContainerAbstract{
use SomeOptions, MoreOptions;
};
var_dump($container->foo); // -> WHATEVER (constructor ran strtoupper on the value)
var_dump($container->bar); // -> nothing
$container->what = 'some value';
var_dump($container->what); // -> hash: 5946210c9e93ae37891dfe96c3e39614 (custom getter added "hash: ")
```
### API
#### [`SettingsContainerAbstract`](https://github.com/chillerlan/php-settings-container/blob/main/src/SettingsContainerAbstract.php)
| method | return | info |
|--------------------------------------------|------------------------------|---------------------------------------------------------------------------------------------------------------------|
| `__construct(iterable $properties = null)` | - | calls `construct()` internally after the properties have been set |
| (protected) `construct()` | void | calls a method with trait name as replacement constructor for each used trait |
| `__get(string $property)` | mixed | calls `$this->{'get_'.$property}()` if such a method exists |
| `__set(string $property, $value)` | void | calls `$this->{'set_'.$property}($value)` if such a method exists |
| `__isset(string $property)` | bool | |
| `__unset(string $property)` | void | |
| `__toString()` | string | a JSON string |
| `toArray()` | array | |
| `fromIterable(iterable $properties)` | `SettingsContainerInterface` | |
| `toJSON(int $jsonOptions = null)` | string | accepts [JSON options constants](http://php.net/manual/json.constants.php) |
| `fromJSON(string $json)` | `SettingsContainerInterface` | |
| `jsonSerialize()` | mixed | implements the [`JsonSerializable`](https://www.php.net/manual/en/jsonserializable.jsonserialize.php) interface |
| `serialize()` | string | implements the [`Serializable`](https://www.php.net/manual/en/serializable.serialize.php) interface |
| `unserialize(string $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/serializable.unserialize.php) interface |
| `__serialize()` | array | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.serialize) interface |
| `__unserialize(array $data)` | void | implements the [`Serializable`](https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize) interface |
## Disclaimer
This might be either an utterly genius or completely stupid idea - you decide. However, i like it and it works.
Also, this is not a dependency injection container. Stop using DI containers FFS.

View file

@ -0,0 +1,52 @@
{
"name": "chillerlan/php-settings-container",
"description": "A container class for immutable settings objects. Not a DI container.",
"homepage": "https://github.com/chillerlan/php-settings-container",
"license": "MIT",
"type": "library",
"minimum-stability": "stable",
"keywords": [
"helper", "container", "settings", "configuration"
],
"authors": [
{
"name": "Smiley",
"email": "smiley@chillerlan.net",
"homepage": "https://github.com/codemasher"
}
],
"support": {
"issues": "https://github.com/chillerlan/php-settings-container/issues",
"source": "https://github.com/chillerlan/php-settings-container"
},
"require": {
"php": "^8.1",
"ext-json": "*"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-deprecation-rules": "^1.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.10"
},
"autoload": {
"psr-4": {
"chillerlan\\Settings\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"chillerlan\\SettingsTest\\": "tests"
}
},
"scripts": {
"phpunit": "@php vendor/bin/phpunit",
"phpstan": "@php vendor/bin/phpstan"
},
"config": {
"lock": false,
"sort-packages": true,
"platform-check": true
}
}

View file

@ -0,0 +1,4 @@
parameters:
ignoreErrors:
# yes, these are magic
- message: "#^Access to an undefined property chillerlan\\\\Settings\\\\SettingsContainerInterface\\:\\:\\$[\\w]+\\.$#"

View file

@ -0,0 +1,252 @@
<?php
/**
* Class SettingsContainerAbstract
*
* @created 28.08.2018
* @author Smiley <smiley@chillerlan.net>
* @copyright 2018 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Settings;
use InvalidArgumentException, JsonException, ReflectionClass, ReflectionProperty;
use function array_keys, get_object_vars, is_object, json_decode, json_encode,
json_last_error_msg, method_exists, property_exists, serialize, unserialize;
use const JSON_THROW_ON_ERROR;
abstract class SettingsContainerAbstract implements SettingsContainerInterface{
/**
* SettingsContainerAbstract constructor.
*
* @phpstan-param array<string, mixed> $properties
*/
public function __construct(iterable|null $properties = null){
if(!empty($properties)){
$this->fromIterable($properties);
}
$this->construct();
}
/**
* calls a method with trait name as replacement constructor for each used trait
* (remember pre-php5 classname constructors? yeah, basically this.)
*/
protected function construct():void{
$traits = (new ReflectionClass($this))->getTraits();
foreach($traits as $trait){
$method = $trait->getShortName();
if(method_exists($this, $method)){
$this->{$method}();
}
}
}
/**
* @inheritdoc
*/
public function __get(string $property):mixed{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return null;
}
$method = 'get_'.$property;
if(method_exists($this, $method)){
return $this->{$method}();
}
return $this->{$property};
}
/**
* @inheritdoc
*/
public function __set(string $property, mixed $value):void{
if(!property_exists($this, $property) || $this->isPrivate($property)){
return;
}
$method = 'set_'.$property;
if(method_exists($this, $method)){
$this->{$method}($value);
return;
}
$this->{$property} = $value;
}
/**
* @inheritdoc
*/
public function __isset(string $property):bool{
return isset($this->{$property}) && !$this->isPrivate($property);
}
/**
* @internal Checks if a property is private
*/
protected function isPrivate(string $property):bool{
return (new ReflectionProperty($this, $property))->isPrivate();
}
/**
* @inheritdoc
*/
public function __unset(string $property):void{
if($this->__isset($property)){
unset($this->{$property});
}
}
/**
* @inheritdoc
*/
public function __toString():string{
return $this->toJSON();
}
/**
* @inheritdoc
*/
public function toArray():array{
$properties = [];
foreach(array_keys(get_object_vars($this)) as $key){
$properties[$key] = $this->__get($key);
}
return $properties;
}
/**
* @inheritdoc
*/
public function fromIterable(iterable $properties):static{
foreach($properties as $key => $value){
$this->__set($key, $value);
}
return $this;
}
/**
* @inheritdoc
*/
public function toJSON(int|null $jsonOptions = null):string{
$json = json_encode($this, ($jsonOptions ?? 0));
if($json === false){
throw new JsonException(json_last_error_msg());
}
return $json;
}
/**
* @inheritdoc
*/
public function fromJSON(string $json):static{
/** @phpstan-var array<string, mixed> $data */
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return $this->fromIterable($data);
}
/**
* @inheritdoc
* @return array<string, mixed>
*/
public function jsonSerialize():array{
return $this->toArray();
}
/**
* Returns a serialized string representation of the object in its current state (except static/readonly properties)
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function serialize():string{
return serialize($this);
}
/**
* Restores the data (except static/readonly properties) from the given serialized object to the current instance
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*/
public function unserialize(string $data):void{
$obj = unserialize($data);
if($obj === false || !is_object($obj)){
throw new InvalidArgumentException('The given serialized string is invalid');
}
$reflection = new ReflectionClass($obj);
if(!$reflection->isInstance($this)){
throw new InvalidArgumentException('The unserialized object does not match the class of this container');
}
$properties = $reflection->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY));
foreach($properties as $reflectionProperty){
$this->{$reflectionProperty->name} = $reflectionProperty->getValue($obj);
}
}
/**
* Returns a serialized string representation of the object in its current state (except static/readonly properties)
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*/
public function __serialize():array{
$properties = (new ReflectionClass($this))
->getProperties(~(ReflectionProperty::IS_STATIC | ReflectionProperty::IS_READONLY))
;
$data = [];
foreach($properties as $reflectionProperty){
$data[$reflectionProperty->name] = $reflectionProperty->getValue($this);
}
return $data;
}
/**
* Restores the data from the given array to the current instance
*
* @inheritdoc
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*
* @param array<string, mixed> $data
*/
public function __unserialize(array $data):void{
foreach($data as $key => $value){
$this->{$key} = $value;
}
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* Interface SettingsContainerInterface
*
* @created 28.08.2018
* @author Smiley <smiley@chillerlan.net>
* @copyright 2018 Smiley
* @license MIT
*/
declare(strict_types=1);
namespace chillerlan\Settings;
use JsonSerializable, Serializable;
/**
* a generic container with magic getter and setter
*/
interface SettingsContainerInterface extends JsonSerializable, Serializable{
/**
* Retrieve the value of $property
*
* @return mixed|null
*/
public function __get(string $property):mixed;
/**
* Set $property to $value while avoiding private and non-existing properties
*/
public function __set(string $property, mixed $value):void;
/**
* Checks if $property is set (aka. not null), excluding private properties
*/
public function __isset(string $property):bool;
/**
* Unsets $property while avoiding private and non-existing properties
*/
public function __unset(string $property):void;
/**
* @see \chillerlan\Settings\SettingsContainerInterface::toJSON()
*/
public function __toString():string;
/**
* Returns an array representation of the settings object
*
* The values will be run through the magic __get(), which may also call custom getters.
*
* @return array<string, mixed>
*/
public function toArray():array;
/**
* Sets properties from a given iterable
*
* The values will be run through the magic __set(), which may also call custom setters.
*
* @phpstan-param array<string, mixed> $properties
*/
public function fromIterable(iterable $properties):static;
/**
* Returns a JSON representation of the settings object
*
* @see \json_encode()
* @see \chillerlan\Settings\SettingsContainerInterface::toArray()
*
* @throws \JsonException
*/
public function toJSON(int|null $jsonOptions = null):string;
/**
* Sets properties from a given JSON string
*
* @see \chillerlan\Settings\SettingsContainerInterface::fromIterable()
*
* @throws \Exception
* @throws \JsonException
*/
public function fromJSON(string $json):static;
}

585
vendor/composer/ClassLoader.php vendored Normal file
View file

@ -0,0 +1,585 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

359
vendor/composer/InstalledVersions.php vendored Normal file
View file

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

19
vendor/composer/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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.

123
vendor/composer/autoload_classmap.php vendored Normal file
View file

@ -0,0 +1,123 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
'DateInvalidTimeZoneException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
'DateMalformedIntervalStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
'DateMalformedPeriodStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
'DateMalformedStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
'DateObjectError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
'FPDF' => $vendorDir . '/setasign/fpdf/fpdf.php',
'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php',
'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
'ezcBase' => $vendorDir . '/zetacomponents/base/src/base.php',
'ezcBaseAutoloadException' => $vendorDir . '/zetacomponents/base/src/exceptions/autoload.php',
'ezcBaseAutoloadOptions' => $vendorDir . '/zetacomponents/base/src/options/autoload.php',
'ezcBaseConfigurationInitializer' => $vendorDir . '/zetacomponents/base/src/interfaces/configuration_initializer.php',
'ezcBaseDoubleClassRepositoryPrefixException' => $vendorDir . '/zetacomponents/base/src/exceptions/double_class_repository_prefix.php',
'ezcBaseException' => $vendorDir . '/zetacomponents/base/src/exceptions/exception.php',
'ezcBaseExportable' => $vendorDir . '/zetacomponents/base/src/interfaces/exportable.php',
'ezcBaseExtensionNotFoundException' => $vendorDir . '/zetacomponents/base/src/exceptions/extension_not_found.php',
'ezcBaseFeatures' => $vendorDir . '/zetacomponents/base/src/features.php',
'ezcBaseFile' => $vendorDir . '/zetacomponents/base/src/file.php',
'ezcBaseFileException' => $vendorDir . '/zetacomponents/base/src/exceptions/file_exception.php',
'ezcBaseFileFindContext' => $vendorDir . '/zetacomponents/base/src/structs/file_find_context.php',
'ezcBaseFileIoException' => $vendorDir . '/zetacomponents/base/src/exceptions/file_io.php',
'ezcBaseFileNotFoundException' => $vendorDir . '/zetacomponents/base/src/exceptions/file_not_found.php',
'ezcBaseFilePermissionException' => $vendorDir . '/zetacomponents/base/src/exceptions/file_permission.php',
'ezcBaseFunctionalityNotSupportedException' => $vendorDir . '/zetacomponents/base/src/exceptions/functionality_not_supported.php',
'ezcBaseInit' => $vendorDir . '/zetacomponents/base/src/init.php',
'ezcBaseInitCallbackConfiguredException' => $vendorDir . '/zetacomponents/base/src/exceptions/init_callback_configured.php',
'ezcBaseInitInvalidCallbackClassException' => $vendorDir . '/zetacomponents/base/src/exceptions/invalid_callback_class.php',
'ezcBaseInvalidParentClassException' => $vendorDir . '/zetacomponents/base/src/exceptions/invalid_parent_class.php',
'ezcBaseMetaData' => $vendorDir . '/zetacomponents/base/src/metadata.php',
'ezcBaseMetaDataPearReader' => $vendorDir . '/zetacomponents/base/src/metadata/pear.php',
'ezcBaseMetaDataTarballReader' => $vendorDir . '/zetacomponents/base/src/metadata/tarball.php',
'ezcBaseOptions' => $vendorDir . '/zetacomponents/base/src/options.php',
'ezcBasePersistable' => $vendorDir . '/zetacomponents/base/src/interfaces/persistable.php',
'ezcBasePropertyNotFoundException' => $vendorDir . '/zetacomponents/base/src/exceptions/property_not_found.php',
'ezcBasePropertyPermissionException' => $vendorDir . '/zetacomponents/base/src/exceptions/property_permission.php',
'ezcBaseRepositoryDirectory' => $vendorDir . '/zetacomponents/base/src/structs/repository_directory.php',
'ezcBaseSettingNotFoundException' => $vendorDir . '/zetacomponents/base/src/exceptions/setting_not_found.php',
'ezcBaseSettingValueException' => $vendorDir . '/zetacomponents/base/src/exceptions/setting_value.php',
'ezcBaseStruct' => $vendorDir . '/zetacomponents/base/src/struct.php',
'ezcBaseValueException' => $vendorDir . '/zetacomponents/base/src/exceptions/value.php',
'ezcBaseWhateverException' => $vendorDir . '/zetacomponents/base/src/exceptions/whatever.php',
'ezcMail' => $vendorDir . '/zetacomponents/mail/src/mail.php',
'ezcMailAddress' => $vendorDir . '/zetacomponents/mail/src/structs/mail_address.php',
'ezcMailCharsetConverter' => $vendorDir . '/zetacomponents/mail/src/internal/charset_convert.php',
'ezcMailComposer' => $vendorDir . '/zetacomponents/mail/src/composer.php',
'ezcMailComposerOptions' => $vendorDir . '/zetacomponents/mail/src/options/composer_options.php',
'ezcMailContentDispositionHeader' => $vendorDir . '/zetacomponents/mail/src/structs/content_disposition_header.php',
'ezcMailDeliveryStatus' => $vendorDir . '/zetacomponents/mail/src/parts/delivery_status.php',
'ezcMailDeliveryStatusParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/delivery_status_parser.php',
'ezcMailException' => $vendorDir . '/zetacomponents/mail/src/exceptions/mail_exception.php',
'ezcMailFile' => $vendorDir . '/zetacomponents/mail/src/parts/fileparts/disk_file.php',
'ezcMailFileParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/file_parser.php',
'ezcMailFilePart' => $vendorDir . '/zetacomponents/mail/src/parts/file.php',
'ezcMailFileSet' => $vendorDir . '/zetacomponents/mail/src/transports/file/file_set.php',
'ezcMailHeaderFolder' => $vendorDir . '/zetacomponents/mail/src/internal/header_folder.php',
'ezcMailHeadersHolder' => $vendorDir . '/zetacomponents/mail/src/parser/headers_holder.php',
'ezcMailImapSet' => $vendorDir . '/zetacomponents/mail/src/transports/imap/imap_set.php',
'ezcMailImapSetOptions' => $vendorDir . '/zetacomponents/mail/src/options/imap_set_options.php',
'ezcMailImapTransport' => $vendorDir . '/zetacomponents/mail/src/transports/imap/imap_transport.php',
'ezcMailImapTransportOptions' => $vendorDir . '/zetacomponents/mail/src/options/imap_options.php',
'ezcMailInvalidLimitException' => $vendorDir . '/zetacomponents/mail/src/exceptions/invalid_limit.php',
'ezcMailMboxSet' => $vendorDir . '/zetacomponents/mail/src/transports/mbox/mbox_set.php',
'ezcMailMboxTransport' => $vendorDir . '/zetacomponents/mail/src/transports/mbox/mbox_transport.php',
'ezcMailMtaTransport' => $vendorDir . '/zetacomponents/mail/src/transports/mta/mta_transport.php',
'ezcMailMultipart' => $vendorDir . '/zetacomponents/mail/src/parts/multipart.php',
'ezcMailMultipartAlternative' => $vendorDir . '/zetacomponents/mail/src/parts/multiparts/multipart_alternative.php',
'ezcMailMultipartAlternativeParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/multipart_alternative_parser.php',
'ezcMailMultipartDigest' => $vendorDir . '/zetacomponents/mail/src/parts/multiparts/multipart_digest.php',
'ezcMailMultipartDigestParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/multipart_digest_parser.php',
'ezcMailMultipartMixed' => $vendorDir . '/zetacomponents/mail/src/parts/multiparts/multipart_mixed.php',
'ezcMailMultipartMixedParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/multipart_mixed_parser.php',
'ezcMailMultipartParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/multipart_parser.php',
'ezcMailMultipartRelated' => $vendorDir . '/zetacomponents/mail/src/parts/multiparts/multipart_related.php',
'ezcMailMultipartRelatedParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/multipart_related_parser.php',
'ezcMailMultipartReport' => $vendorDir . '/zetacomponents/mail/src/parts/multiparts/multipart_report.php',
'ezcMailMultipartReportParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/multipart_report_parser.php',
'ezcMailNoSuchMessageException' => $vendorDir . '/zetacomponents/mail/src/exceptions/no_such_message.php',
'ezcMailOffsetOutOfRangeException' => $vendorDir . '/zetacomponents/mail/src/exceptions/offset_out_of_range.php',
'ezcMailOptions' => $vendorDir . '/zetacomponents/mail/src/options/mail_options.php',
'ezcMailParser' => $vendorDir . '/zetacomponents/mail/src/parser/parser.php',
'ezcMailParserOptions' => $vendorDir . '/zetacomponents/mail/src/options/parser_options.php',
'ezcMailParserSet' => $vendorDir . '/zetacomponents/mail/src/parser/interfaces/parser_set.php',
'ezcMailParserShutdownHandler' => $vendorDir . '/zetacomponents/mail/src/parser/shutdown_handler.php',
'ezcMailPart' => $vendorDir . '/zetacomponents/mail/src/interfaces/part.php',
'ezcMailPartParser' => $vendorDir . '/zetacomponents/mail/src/parser/interfaces/part_parser.php',
'ezcMailPartWalkContext' => $vendorDir . '/zetacomponents/mail/src/structs/walk_context.php',
'ezcMailPop3Set' => $vendorDir . '/zetacomponents/mail/src/transports/pop3/pop3_set.php',
'ezcMailPop3Transport' => $vendorDir . '/zetacomponents/mail/src/transports/pop3/pop3_transport.php',
'ezcMailPop3TransportOptions' => $vendorDir . '/zetacomponents/mail/src/options/pop3_options.php',
'ezcMailRfc2231Implementation' => $vendorDir . '/zetacomponents/mail/src/parser/rfc2231_implementation.php',
'ezcMailRfc822Digest' => $vendorDir . '/zetacomponents/mail/src/parts/rfc822_digest.php',
'ezcMailRfc822DigestParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/rfc822_digest_parser.php',
'ezcMailRfc822Parser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/rfc822_parser.php',
'ezcMailSmtpTransport' => $vendorDir . '/zetacomponents/mail/src/transports/smtp/smtp_transport.php',
'ezcMailSmtpTransportOptions' => $vendorDir . '/zetacomponents/mail/src/options/smtp_options.php',
'ezcMailStorageSet' => $vendorDir . '/zetacomponents/mail/src/transports/storage/storage_set.php',
'ezcMailStreamFile' => $vendorDir . '/zetacomponents/mail/src/parts/fileparts/stream_file.php',
'ezcMailText' => $vendorDir . '/zetacomponents/mail/src/parts/text.php',
'ezcMailTextParser' => $vendorDir . '/zetacomponents/mail/src/parser/parts/text_parser.php',
'ezcMailTools' => $vendorDir . '/zetacomponents/mail/src/tools.php',
'ezcMailTransport' => $vendorDir . '/zetacomponents/mail/src/interfaces/transport.php',
'ezcMailTransportConnection' => $vendorDir . '/zetacomponents/mail/src/transports/transport_connection.php',
'ezcMailTransportException' => $vendorDir . '/zetacomponents/mail/src/exceptions/transport_exception.php',
'ezcMailTransportMta' => $vendorDir . '/zetacomponents/mail/src/transports/mta/transport_mta.php',
'ezcMailTransportOptions' => $vendorDir . '/zetacomponents/mail/src/options/transport_options.php',
'ezcMailTransportSmtp' => $vendorDir . '/zetacomponents/mail/src/transports/smtp/transport_smtp.php',
'ezcMailTransportSmtpException' => $vendorDir . '/zetacomponents/mail/src/exceptions/transport_smtp_exception.php',
'ezcMailVariableSet' => $vendorDir . '/zetacomponents/mail/src/transports/variable/var_set.php',
'ezcMailVirtualFile' => $vendorDir . '/zetacomponents/mail/src/parts/fileparts/virtual_file.php',
);

16
vendor/composer/autoload_files.php vendored Normal file
View file

@ -0,0 +1,16 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
);

10
vendor/composer/autoload_namespaces.php vendored Normal file
View file

@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Smalot\\PdfParser\\' => array($vendorDir . '/smalot/pdfparser/src'),
);

38
vendor/composer/autoload_psr4.php vendored Normal file
View file

@ -0,0 +1,38 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'),
'horstoeko\\zugferd\\' => array($vendorDir . '/horstoeko/zugferd/src'),
'horstoeko\\stringmanagement\\' => array($vendorDir . '/horstoeko/stringmanagement/src'),
'horstoeko\\mimedb\\' => array($vendorDir . '/horstoeko/mimedb/src'),
'chillerlan\\Settings\\' => array($vendorDir . '/chillerlan/php-settings-container/src'),
'chillerlan\\Authenticator\\' => array($vendorDir . '/chillerlan/php-authenticator/src'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
'Symfony\\Component\\Validator\\' => array($vendorDir . '/symfony/validator'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'),
'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'PHPStan\\PhpDocParser\\' => array($vendorDir . '/phpstan/phpdoc-parser/src'),
'NumNum\\UBL\\Tests\\' => array($vendorDir . '/num-num/ubl-invoice/tests'),
'NumNum\\UBL\\' => array($vendorDir . '/num-num/ubl-invoice/src'),
'Metadata\\' => array($vendorDir . '/jms/metadata/src'),
'JMS\\Serializer\\' => array($vendorDir . '/jms/serializer/src'),
'GoetasWebservices\\Xsd\\XsdToPhpRuntime\\' => array($vendorDir . '/goetas-webservices/xsd2php-runtime/src'),
'Endroid\\QrCode\\' => array($vendorDir . '/endroid/qr-code/src'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
'DASPRiD\\Enum\\' => array($vendorDir . '/dasprid/enum/src'),
'Curl\\' => array($vendorDir . '/php-curl-class/php-curl-class/src/Curl'),
'BaconQrCode\\' => array($vendorDir . '/bacon/bacon-qr-code/src'),
);

50
vendor/composer/autoload_real.php vendored Normal file
View file

@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit1a0e4ea3fb1d108a445b9e95a7c251ad
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit1a0e4ea3fb1d108a445b9e95a7c251ad', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit1a0e4ea3fb1d108a445b9e95a7c251ad', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit1a0e4ea3fb1d108a445b9e95a7c251ad::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit1a0e4ea3fb1d108a445b9e95a7c251ad::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

Some files were not shown because too many files have changed in this diff Show more