Update Files GitIgnore

This commit is contained in:
ygbanzato 2023-08-02 19:29:17 +01:00
parent 56f0dd1b8a
commit dd26c4ec4e
9111 changed files with 1130369 additions and 19 deletions

78
.env Executable file
View File

@ -0,0 +1,78 @@
APP_NAME=ISPT4.0
APP_ENV=local
APP_KEY=base64:ahx5/AvVGu/iHQx1mjX/EQg4m1NHLvtjzb6pFa49TlE=
APP_DEBUG=true
APP_URL=http://localhost
APP_VERSION = 1.0.0
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_PORT=3306
# DB_HOST=127.0.0.1
# DB_DATABASE=ispt_4.0
# DB_USERNAME=root
# DB_PASSWORD=123456789
# Alteracoes para funcionar com o servidor, porem nao funciona na firma
DB_HOST=ispt-innovation.com
DB_DATABASE=ispt40
DB_USERNAME=ispt40
DB_PASSWORD=qu3ro3ntr@r
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# ***For MailGiun***
# MAIL_MAILER=mailgun
# MAILGUN_DOMAIN=sandboxdef4c3b1ceca4a53aa14d91d69bfe8a0.mailgun.org
# MAILGUN_SECRET=9b87b9ff449885a1b7ea856d8e4cad73-181449aa-479369f2
# MAIL_FROM_ADDRESS=ubuntu@ispt-innovation.com
# MAIL_FROM_NAME="Ispt-4.0"
MAIL_MAILER=smtp
# MAIL_HOST=smtp.sendgrid.net
MAIL_HOST= mail.ispt-innovation.com
# MAIL_PORT=587
MAIL_PORT= 465
# MAIL_USERNAME=apikey
MAIL_USERNAME= ubuntu
# MAIL_PASSWORD=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
MAIL_PASSWORD= 6bhWUN62KYSD
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=ubuntu@ispt-innovation.com
MAIL_FROM_NAME="Ispt4.0"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

38
.gitignore vendored
View File

@ -1,19 +1,19 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
#/.phpunit.cache
#/node_modules
#/public/build
#/public/hot
#/public/storage
#/storage/*.key
#/vendor
#.env
#.env.backup
#.env.production
#.phpunit.result.cache
#Homestead.json
#Homestead.yaml
#auth.json
#npm-debug.log
#yarn-error.log
#/.fleet
#/.idea
#/.vscode

12
vendor/autoload.php vendored Executable file
View File

@ -0,0 +1,12 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
echo '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;
exit(1);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit4de2290df2a8c5142f72130885c7079d::getLoader();

22
vendor/bacon/bacon-qr-code/LICENSE vendored Executable 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.

39
vendor/bacon/bacon-qr-code/README.md vendored Executable file
View File

@ -0,0 +1,39 @@
# 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

44
vendor/bacon/bacon-qr-code/composer.json vendored Executable file
View File

@ -0,0 +1,44 @@
{
"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": "^7.1 || ^8.0",
"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/"
}
},
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4",
"phly/keep-a-changelog": "^2.1"
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true
}
},
"archive": {
"exclude": [
"/test",
"/phpunit.xml.dist"
]
}
}

13
vendor/bacon/bacon-qr-code/phpunit.xml.dist vendored Executable file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<testsuites>
<testsuite name="BaconQrCode Tests">
<directory>./test</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,372 @@
<?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 $bits;
/**
* Size of the bit array in bits.
*
* @var int
*/
private $size;
/**
* Creates a new bit array with a given size.
*/
public function __construct(int $size = 0)
{
$this->size = $size;
$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 $result > $this->size ? $this->size : $result;
}
/**
* 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 $result > $this->size ? $this->size : $result;
}
/**
* 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,313 @@
<?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.
*
* @var int
*/
private $width;
/**
* Height of the bit matrix.
*
* @var int
*/
private $height;
/**
* Size in bits of each individual row.
*
* @var int
*/
private $rowSize;
/**
* Bits representation.
*
* @var SplFixedArray<int>
*/
private $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,183 @@
<?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 int[]
*/
private $values;
/**
* @var string[]
*/
private $otherEncodingNames;
/**
* @var array<int, self>|null
*/
private static $valueToEci;
/**
* @var array<string, self>|null
*/
private static $nameToEci;
/**
* @param int[] $values
*/
public function __construct(array $values, string ...$otherEncodingNames)
{
$this->values = $values;
$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,49 @@
<?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
{
/**
* How many times the block is used.
*
* @var int
*/
private $count;
/**
* Number of data codewords.
*
* @var int
*/
private $dataCodewords;
public function __construct(int $count, int $dataCodewords)
{
$this->count = $count;
$this->dataCodewords = $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,74 @@
<?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
{
/**
* Number of EC codewords per block.
*
* @var int
*/
private $ecCodewordsPerBlock;
/**
* List of EC blocks.
*
* @var EcBlock[]
*/
private $ecBlocks;
public function __construct(int $ecCodewordsPerBlock, EcBlock ...$ecBlocks)
{
$this->ecCodewordsPerBlock = $ecCodewordsPerBlock;
$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,63 @@
<?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];
/**
* @var int
*/
private $bits;
protected function __construct(int $bits)
{
$this->bits = $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,203 @@
<?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.
*
* @var ErrorCorrectionLevel
*/
private $ecLevel;
/**
* Data mask.
*
* @var int
*/
private $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,79 @@
<?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];
/**
* @var int[]
*/
private $characterCountBitsForVersions;
/**
* @var int
*/
private $bits;
/**
* @param int[] $characterCountBitsForVersions
*/
protected function __construct(array $characterCountBitsForVersions, int $bits)
{
$this->characterCountBitsForVersions = $characterCountBitsForVersions;
$this->bits = $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,468 @@
<?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.
*
* @var int
*/
private $symbolSize;
/**
* Block size in symbols.
*
* @var int
*/
private $blockSize;
/**
* First root of RS code generator polynomial, index form.
*
* @var int
*/
private $firstRoot;
/**
* Primitive element to generate polynomial roots, index form.
*
* @var int
*/
private $primitive;
/**
* Prim-th root of 1, index form.
*
* @var int
*/
private $iPrimitive;
/**
* RS code generator polynomial degree (number of roots).
*
* @var int
*/
private $numRoots;
/**
* Padding bytes at front of shortened block.
*
* @var int
*/
private $padding;
/**
* Log lookup table.
*
* @var SplFixedArray
*/
private $alphaTo;
/**
* Anti-Log lookup table.
*
* @var SplFixedArray
*/
private $indexOf;
/**
* Generator polynomial.
*
* @var SplFixedArray
*/
private $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,596 @@
<?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.
*
* @var int
*/
private $versionNumber;
/**
* Alignment pattern centers.
*
* @var SplFixedArray
*/
private $alignmentPatternCenters;
/**
* Error correction blocks.
*
* @var EcBlocks[]
*/
private $ecBlocks;
/**
* Total number of codewords.
*
* @var int
*/
private $totalCodewords;
/**
* Cached version instances.
*
* @var array<int, self>|null
*/
private static $versions;
/**
* @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(3, 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,58 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Encoder;
use SplFixedArray;
/**
* Block pair.
*/
final class BlockPair
{
/**
* Data bytes in the block.
*
* @var SplFixedArray<int>
*/
private $dataBytes;
/**
* Error correction bytes in the block.
*
* @var SplFixedArray<int>
*/
private $errorCorrectionBytes;
/**
* Creates a new block pair.
*
* @param SplFixedArray<int> $data
* @param SplFixedArray<int> $errorCorrection
*/
public function __construct(SplFixedArray $data, SplFixedArray $errorCorrection)
{
$this->dataBytes = $data;
$this->errorCorrectionBytes = $errorCorrection;
}
/**
* 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,150 @@
<?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 $bytes;
/**
* Width of the matrix.
*
* @var int
*/
private $width;
/**
* Height of the matrix.
*
* @var int
*/
private $height;
public function __construct(int $width, int $height)
{
$this->height = $height;
$this->width = $width;
$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,668 @@
<?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_ECODING = 'ISO-8859-1';
/**
* 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 $codecs = [];
/**
* Encodes "content" with the error correction level "ecLevel".
*/
public static function encode(
string $content,
ErrorCorrectionLevel $ecLevel,
string $encoding = self::DEFAULT_BYTE_MODE_ECODING,
?Version $forcedVersion = null
) : 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 (Mode::BYTE() === $mode && self::DEFAULT_BYTE_MODE_ECODING !== $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 = $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
{
if (strlen($content) % 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($content);
for ($i = 0; $i < $length; $i += 2) {
$byte1 = ord($content[$i]) & 0xff;
$byte2 = ord($content[$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
*/
const N1 = 3;
const N2 = 3;
const N3 = 40;
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,141 @@
<?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;
/**
* Mode of the QR code.
*
* @var Mode
*/
private $mode;
/**
* EC level of the QR code.
*
* @var ErrorCorrectionLevel
*/
private $errorCorrectionLevel;
/**
* Version of the QR code.
*
* @var Version
*/
private $version;
/**
* Mask pattern of the QR code.
*
* @var int
*/
private $maskPattern = -1;
/**
* Matrix of the QR code.
*
* @var ByteMatrix
*/
private $matrix;
public function __construct(
Mode $mode,
ErrorCorrectionLevel $errorCorrectionLevel,
Version $version,
int $maskPattern,
ByteMatrix $matrix
) {
$this->mode = $mode;
$this->errorCorrectionLevel = $errorCorrectionLevel;
$this->version = $version;
$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;
}
/**
* Gets the matrix.
*
* @return ByteMatrix
*/
public function getMatrix()
{
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,57 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Alpha implements ColorInterface
{
/**
* @var int
*/
private $alpha;
/**
* @var ColorInterface
*/
private $baseColor;
/**
* @param int $alpha the alpha value, 0 to 100
*/
public function __construct(int $alpha, ColorInterface $baseColor)
{
if ($alpha < 0 || $alpha > 100) {
throw new Exception\InvalidArgumentException('Alpha must be between 0 and 100');
}
$this->alpha = $alpha;
$this->baseColor = $baseColor;
}
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,103 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Cmyk implements ColorInterface
{
/**
* @var int
*/
private $cyan;
/**
* @var int
*/
private $magenta;
/**
* @var int
*/
private $yellow;
/**
* @var int
*/
private $black;
/**
* @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(int $cyan, int $magenta, int $yellow, 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');
}
$this->cyan = $cyan;
$this->magenta = $magenta;
$this->yellow = $yellow;
$this->black = $black;
}
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,46 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Gray implements ColorInterface
{
/**
* @var int
*/
private $gray;
/**
* @param int $gray the gray value between 0 (black) and 100 (white)
*/
public function __construct(int $gray)
{
if ($gray < 0 || $gray > 100) {
throw new Exception\InvalidArgumentException('Gray must be between 0 and 100');
}
$this->gray = (int) $gray;
}
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,88 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Color;
use BaconQrCode\Exception;
final class Rgb implements ColorInterface
{
/**
* @var int
*/
private $red;
/**
* @var int
*/
private $green;
/**
* @var int
*/
private $blue;
/**
* @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(int $red, int $green, 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');
}
$this->red = $red;
$this->green = $green;
$this->blue = $blue;
}
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);
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,38 @@
<?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
{
/**
* @var EyeInterface
*/
private $externalEye;
/**
* @var EyeInterface
*/
private $internalEye;
public function __construct(EyeInterface $externalEye, EyeInterface $internalEye)
{
$this->externalEye = $externalEye;
$this->internalEye = $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,54 @@
<?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
{
/**
* @var ModuleInterface
*/
private $module;
public function __construct(ModuleInterface $module)
{
$this->module = $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,54 @@
<?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
{
/**
* @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, -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,53 @@
<?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
{
/**
* @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, -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,376 @@
<?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;
/**
* @var string|null
*/
private $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,336 @@
<?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
{
/**
* @var string
*/
private $imageFormat;
/**
* @var int
*/
private $compressionQuality;
/**
* @var Imagick|null
*/
private $image;
/**
* @var ImagickDraw|null
*/
private $draw;
/**
* @var int|null
*/
private $gradientCount;
/**
* @var TransformationMatrix[]|null
*/
private $matrices;
/**
* @var int|null
*/
private $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,369 @@
<?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;
/**
* @var XMLWriter|null
*/
private $xmlWriter;
/**
* @var int[]|null
*/
private $stack;
/**
* @var int|null
*/
private $currentStack;
/**
* @var int|null
*/
private $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('scale(%s)', 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('translate(%s,%s)', 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;
}
$id = sprintf('g%d', ++$this->gradientCount);
$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 $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,152 @@
<?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
{
/**
* @var RendererStyle
*/
private $rendererStyle;
/**
* @var ImageBackEndInterface
*/
private $imageBackEnd;
public function __construct(RendererStyle $rendererStyle, ImageBackEndInterface $imageBackEnd)
{
$this->rendererStyle = $rendererStyle;
$this->imageBackEnd = $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->translate($xTranslation, $yTranslation))
->append($internalPath->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->translate($xTranslation, $yTranslation));
} else {
$this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
}
if ($fill->inheritsInternalColor()) {
$modulePath = $modulePath->append($internalPath->translate($xTranslation, $yTranslation));
} else {
$this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
}
$this->imageBackEnd->pop();
return $modulePath;
}
}

View File

@ -0,0 +1,63 @@
<?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;
/**
* @var float
*/
private $size;
public function __construct(float $size)
{
if ($size <= 0 || $size > 1) {
throw new InvalidArgumentException('Size must between 0 (exclusive) and 1 (inclusive)');
}
$this->size = $size;
}
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,100 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Module\EdgeIterator;
final class Edge
{
/**
* @var bool
*/
private $positive;
/**
* @var array<int[]>
*/
private $points = [];
/**
* @var array<int[]>|null
*/
private $simplifiedPoints;
/**
* @var int
*/
private $minX = PHP_INT_MAX;
/**
* @var int
*/
private $minY = PHP_INT_MAX;
/**
* @var int
*/
private $maxX = -1;
/**
* @var int
*/
private $maxY = -1;
public function __construct(bool $positive)
{
$this->positive = $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,169 @@
<?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 $bytes = [];
/**
* @var int
*/
private $size;
/**
* @var int
*/
private $width;
/**
* @var int
*/
private $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,129 @@
<?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;
/**
* @var float
*/
private $intensity;
public function __construct(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,47 @@
<?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
{
/**
* @var self|null
*/
private static $instance;
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,29 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Close implements OperationInterface
{
/**
* @var self|null
*/
private static $instance;
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;
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Curve implements OperationInterface
{
/**
* @var float
*/
private $x1;
/**
* @var float
*/
private $y1;
/**
* @var float
*/
private $x2;
/**
* @var float
*/
private $y2;
/**
* @var float
*/
private $x3;
/**
* @var float
*/
private $y3;
public function __construct(float $x1, float $y1, float $x2, float $y2, float $x3, float $y3)
{
$this->x1 = $x1;
$this->y1 = $y1;
$this->x2 = $x2;
$this->y2 = $y2;
$this->x3 = $x3;
$this->y3 = $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
);
}
}

View File

@ -0,0 +1,278 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class EllipticArc implements OperationInterface
{
private const ZERO_TOLERANCE = 1e-05;
/**
* @var float
*/
private $xRadius;
/**
* @var float
*/
private $yRadius;
/**
* @var float
*/
private $xAxisAngle;
/**
* @var bool
*/
private $largeArc;
/**
* @var bool
*/
private $sweep;
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
public function __construct(
float $xRadius,
float $yRadius,
float $xAxisAngle,
bool $largeArc,
bool $sweep,
float $x,
float $y
) {
$this->xRadius = abs($xRadius);
$this->yRadius = abs($yRadius);
$this->xAxisAngle = $xAxisAngle % 360;
$this->largeArc = $largeArc;
$this->sweep = $sweep;
$this->x = $x;
$this->y = $y;
}
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
);
}
/**
* 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)
{
$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,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Line implements OperationInterface
{
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $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);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\Path;
final class Move implements OperationInterface
{
/**
* @var float
*/
private $x;
/**
* @var float
*/
private $y;
public function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $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);
}
}

View File

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

View File

@ -0,0 +1,106 @@
<?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 $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;
}
/**
* @return OperationInterface[]|Traversable
*/
public function getIterator() : Traversable
{
foreach ($this->operations as $operation) {
yield $operation;
}
}
}

View File

@ -0,0 +1,86 @@
<?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";
/**
* @var int
*/
private $margin;
public function __construct(int $margin = 2)
{
$this->margin = $margin;
}
/**
* @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,74 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use BaconQrCode\Exception\RuntimeException;
use BaconQrCode\Renderer\Color\ColorInterface;
final class EyeFill
{
/**
* @var ColorInterface|null
*/
private $externalColor;
/**
* @var ColorInterface|null
*/
private $internalColor;
/**
* @var self|null
*/
private static $inherit;
public function __construct(?ColorInterface $externalColor, ?ColorInterface $internalColor)
{
$this->externalColor = $externalColor;
$this->internalColor = $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,168 @@
<?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
{
/**
* @var ColorInterface
*/
private $backgroundColor;
/**
* @var ColorInterface|null
*/
private $foregroundColor;
/**
* @var Gradient|null
*/
private $foregroundGradient;
/**
* @var EyeFill
*/
private $topLeftEyeFill;
/**
* @var EyeFill
*/
private $topRightEyeFill;
/**
* @var EyeFill
*/
private $bottomLeftEyeFill;
/**
* @var self|null
*/
private static $default;
private function __construct(
ColorInterface $backgroundColor,
?ColorInterface $foregroundColor,
?Gradient $foregroundGradient,
EyeFill $topLeftEyeFill,
EyeFill $topRightEyeFill,
EyeFill $bottomLeftEyeFill
) {
$this->backgroundColor = $backgroundColor;
$this->foregroundColor = $foregroundColor;
$this->foregroundGradient = $foregroundGradient;
$this->topLeftEyeFill = $topLeftEyeFill;
$this->topRightEyeFill = $topRightEyeFill;
$this->bottomLeftEyeFill = $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,46 @@
<?php
declare(strict_types = 1);
namespace BaconQrCode\Renderer\RendererStyle;
use BaconQrCode\Renderer\Color\ColorInterface;
final class Gradient
{
/**
* @var ColorInterface
*/
private $startColor;
/**
* @var ColorInterface
*/
private $endColor;
/**
* @var GradientType
*/
private $type;
public function __construct(ColorInterface $startColor, ColorInterface $endColor, GradientType $type)
{
$this->startColor = $startColor;
$this->endColor = $endColor;
$this->type = $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,90 @@
<?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
{
/**
* @var int
*/
private $size;
/**
* @var int
*/
private $margin;
/**
* @var ModuleInterface
*/
private $module;
/**
* @var EyeInterface|null
*/
private $eye;
/**
* @var Fill
*/
private $fill;
public function __construct(
int $size,
int $margin = 4,
?ModuleInterface $module = null,
?EyeInterface $eye = null,
?Fill $fill = null
) {
$this->margin = $margin;
$this->size = $size;
$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;
}
}

71
vendor/bacon/bacon-qr-code/src/Writer.php vendored Executable file
View File

@ -0,0 +1,71 @@
<?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
{
/**
* Renderer instance.
*
* @var RendererInterface
*/
private $renderer;
/**
* Creates a new writer with a specific renderer.
*/
public function __construct(RendererInterface $renderer)
{
$this->renderer = $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_ECODING,
?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_ECODING,
?ErrorCorrectionLevel $ecLevel = null,
?Version $forcedVersion = null
) : void {
file_put_contents($filename, $this->writeString($content, $encoding, $ecLevel, $forcedVersion));
}
}

View File

@ -0,0 +1,222 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\BitArray;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\Version as PHPUnitVersion;
final class BitArrayTest extends TestCase
{
private function getPhpUnitMajorVersion(): int
{
return (int) explode('.', PHPUnitVersion::id())[0];
}
public function testGetSet() : void
{
$array = new BitArray(33);
for ($i = 0; $i < 33; ++$i) {
$this->assertFalse($array->get($i));
$array->set($i);
$this->assertTrue($array->get($i));
}
}
public function testGetNextSet1() : void
{
$array = new BitArray(32);
for ($i = 0; $i < $array->getSize(); ++$i) {
if ($this->getPhpUnitMajorVersion() === 7) {
$this->assertEquals($i, 32, '', $array->getNextSet($i));
} else {
$this->assertEqualsWithDelta($i, 32, $array->getNextSet($i));
}
}
$array = new BitArray(33);
for ($i = 0; $i < $array->getSize(); ++$i) {
if ($this->getPhpUnitMajorVersion() === 7) {
$this->assertEquals($i, 33, '', $array->getNextSet($i));
} else {
$this->assertEqualsWithDelta($i, 33, $array->getNextSet($i));
}
}
}
public function testGetNextSet2() : void
{
$array = new BitArray(33);
for ($i = 0; $i < $array->getSize(); ++$i) {
if ($this->getPhpUnitMajorVersion() === 7) {
$this->assertEquals($i, $i <= 31 ? 31 : 33, '', $array->getNextSet($i));
} else {
$this->assertEqualsWithDelta($i, $i <= 31 ? 31 : 33, $array->getNextSet($i));
}
}
$array = new BitArray(33);
for ($i = 0; $i < $array->getSize(); ++$i) {
if ($this->getPhpUnitMajorVersion() === 7) {
$this->assertEquals($i, 32, '', $array->getNextSet($i));
} else {
$this->assertEqualsWithDelta($i, 32, $array->getNextSet($i));
}
}
}
public function testGetNextSet3() : void
{
$array = new BitArray(63);
$array->set(31);
$array->set(32);
for ($i = 0; $i < $array->getSize(); ++$i) {
if ($i <= 31) {
$expected = 31;
} elseif ($i <= 32) {
$expected = 32;
} else {
$expected = 63;
}
if ($this->getPhpUnitMajorVersion() === 7) {
$this->assertEquals($i, $expected, '', $array->getNextSet($i));
} else {
$this->assertEqualsWithDelta($i, $expected, $array->getNextSet($i));
}
}
}
public function testGetNextSet4() : void
{
$array = new BitArray(63);
$array->set(33);
$array->set(40);
for ($i = 0; $i < $array->getSize(); ++$i) {
if ($i <= 33) {
$expected = 33;
} elseif ($i <= 40) {
$expected = 40;
} else {
$expected = 63;
}
if ($this->getPhpUnitMajorVersion() === 7) {
$this->assertEquals($i, $expected, '', $array->getNextSet($i));
} else {
$this->assertEqualsWithDelta($i, $expected, $array->getNextSet($i));
}
}
}
public function testGetNextSet5() : void
{
mt_srand(0xdeadbeef, MT_RAND_PHP);
for ($i = 0; $i < 10; ++$i) {
$array = new BitArray(mt_rand(1, 100));
$numSet = mt_rand(0, 19);
for ($j = 0; $j < $numSet; ++$j) {
$array->set(mt_rand(0, $array->getSize() - 1));
}
$numQueries = mt_rand(0, 19);
for ($j = 0; $j < $numQueries; ++$j) {
$query = mt_rand(0, $array->getSize() - 1);
$expected = $query;
while ($expected < $array->getSize() && ! $array->get($expected)) {
++$expected;
}
$actual = $array->getNextSet($query);
if ($actual !== $expected) {
$array->getNextSet($query);
}
$this->assertEquals($expected, $actual);
}
}
}
public function testSetBulk() : void
{
$array = new BitArray(64);
$array->setBulk(32, 0xFFFF0000);
for ($i = 0; $i < 48; ++$i) {
$this->assertFalse($array->get($i));
}
for ($i = 48; $i < 64; ++$i) {
$this->assertTrue($array->get($i));
}
}
public function testClear() : void
{
$array = new BitArray(32);
for ($i = 0; $i < 32; ++$i) {
$array->set($i);
}
$array->clear();
for ($i = 0; $i < 32; ++$i) {
$this->assertFalse($array->get($i));
}
}
public function testGetArray() : void
{
$array = new BitArray(64);
$array->set(0);
$array->set(63);
$ints = $array->getBitArray();
$this->assertSame(1, $ints[0]);
$this->assertSame(0x80000000, $ints[1]);
}
public function testIsRange() : void
{
$array = new BitArray(64);
$this->assertTrue($array->isRange(0, 64, false));
$this->assertFalse($array->isRange(0, 64, true));
$array->set(32);
$this->assertTrue($array->isRange(32, 33, true));
$array->set(31);
$this->assertTrue($array->isRange(31, 33, true));
$array->set(34);
$this->assertFalse($array->isRange(31, 35, true));
for ($i = 0; $i < 31; ++$i) {
$array->set($i);
}
$this->assertTrue($array->isRange(0, 33, true));
for ($i = 33; $i < 64; ++$i) {
$array->set($i);
}
$this->assertTrue($array->isRange(0, 64, true));
$this->assertFalse($array->isRange(0, 64, false));
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\BitMatrix;
use PHPUnit\Framework\TestCase;
class BitMatrixTest extends TestCase
{
public function testGetSet() : void
{
$matrix = new BitMatrix(33);
$this->assertEquals(33, $matrix->getHeight());
for ($y = 0; $y < 33; ++$y) {
for ($x = 0; $x < 33; ++$x) {
if ($y * $x % 3 === 0) {
$matrix->set($x, $y);
}
}
}
for ($y = 0; $y < 33; $y++) {
for ($x = 0; $x < 33; ++$x) {
$this->assertSame(0 === $x * $y % 3, $matrix->get($x, $y));
}
}
}
public function testSetRegion() : void
{
$matrix = new BitMatrix(5);
$matrix->setRegion(1, 1, 3, 3);
for ($y = 0; $y < 5; ++$y) {
for ($x = 0; $x < 5; ++$x) {
$this->assertSame($y >= 1 && $y <= 3 && $x >= 1 && $x <= 3, $matrix->get($x, $y));
}
}
}
public function testRectangularMatrix() : void
{
$matrix = new BitMatrix(75, 20);
$this->assertSame(75, $matrix->getWidth());
$this->assertSame(20, $matrix->getHeight());
$matrix->set(10, 0);
$matrix->set(11, 1);
$matrix->set(50, 2);
$matrix->set(51, 3);
$matrix->flip(74, 4);
$matrix->flip(0, 5);
$this->assertTrue($matrix->get(10, 0));
$this->assertTrue($matrix->get(11, 1));
$this->assertTrue($matrix->get(50, 2));
$this->assertTrue($matrix->get(51, 3));
$this->assertTrue($matrix->get(74, 4));
$this->assertTrue($matrix->get(0, 5));
$matrix->flip(50, 2);
$matrix->flip(51, 3);
$this->assertFalse($matrix->get(50, 2));
$this->assertFalse($matrix->get(51, 3));
}
public function testRectangularSetRegion() : void
{
$matrix = new BitMatrix(320, 240);
$this->assertSame(320, $matrix->getWidth());
$this->assertSame(240, $matrix->getHeight());
$matrix->setRegion(105, 22, 80, 12);
for ($y = 0; $y < 240; ++$y) {
for ($x = 0; $x < 320; ++$x) {
$this->assertEquals($y >= 22 && $y < 34 && $x >= 105 && $x < 185, $matrix->get($x, $y));
}
}
}
public function testGetRow() : void
{
$matrix = new BitMatrix(102, 5);
for ($x = 0; $x < 102; ++$x) {
if (0 === ($x & 3)) {
$matrix->set($x, 2);
}
}
$array1 = $matrix->getRow(2, null);
$this->assertSame(102, $array1->getSize());
$array2 = new BitArray(60);
$array2 = $matrix->getRow(2, $array2);
$this->assertSame(102, $array2->getSize());
$array3 = new BitArray(200);
$array3 = $matrix->getRow(2, $array3);
$this->assertSame(200, $array3->getSize());
for ($x = 0; $x < 102; ++$x) {
$on = (0 === ($x & 3));
$this->assertSame($on, $array1->get($x));
$this->assertSame($on, $array2->get($x));
$this->assertSame($on, $array3->get($x));
}
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\BitUtils;
use PHPUnit\Framework\TestCase;
class BitUtilsTest extends TestCase
{
public function testUnsignedRightShift() : void
{
$this->assertSame(1, BitUtils::unsignedRightShift(1, 0));
$this->assertSame(1, BitUtils::unsignedRightShift(10, 3));
$this->assertSame(536870910, BitUtils::unsignedRightShift(-10, 3));
}
public function testNumberOfTrailingZeros() : void
{
$this->assertSame(32, BitUtils::numberOfTrailingZeros(0));
$this->assertSame(1, BitUtils::numberOfTrailingZeros(10));
$this->assertSame(0, BitUtils::numberOfTrailingZeros(15));
$this->assertSame(2, BitUtils::numberOfTrailingZeros(20));
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Exception\OutOfBoundsException;
use PHPUnit\Framework\TestCase;
class ErrorCorrectionLevelTest extends TestCase
{
public function testBitsMatchConstants() : void
{
$this->assertSame(0x0, ErrorCorrectionLevel::M()->getBits());
$this->assertSame(0x1, ErrorCorrectionLevel::L()->getBits());
$this->assertSame(0x2, ErrorCorrectionLevel::H()->getBits());
$this->assertSame(0x3, ErrorCorrectionLevel::Q()->getBits());
}
public function testInvalidErrorCorrectionLevelThrowsException() : void
{
$this->expectException(OutOfBoundsException::class);
ErrorCorrectionLevel::forBits(4);
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\FormatInformation;
use PHPUnit\Framework\TestCase;
class FormatInformationTest extends TestCase
{
private const MASKED_TEST_FORMAT_INFO = 0x2bed;
private const UNMAKSED_TEST_FORMAT_INFO = self::MASKED_TEST_FORMAT_INFO ^ 0x5412;
public function testBitsDiffering() : void
{
$this->assertSame(0, FormatInformation::numBitsDiffering(1, 1));
$this->assertSame(1, FormatInformation::numBitsDiffering(0, 2));
$this->assertSame(2, FormatInformation::numBitsDiffering(1, 2));
$this->assertEquals(32, FormatInformation::numBitsDiffering(-1, 0));
}
public function testDecode() : void
{
$expected = FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO,
self::MASKED_TEST_FORMAT_INFO
);
$this->assertNotNull($expected);
$this->assertSame(7, $expected->getDataMask());
$this->assertSame(ErrorCorrectionLevel::Q(), $expected->getErrorCorrectionLevel());
$this->assertEquals(
$expected,
FormatInformation::decodeFormatInformation(
self::UNMAKSED_TEST_FORMAT_INFO,
self::MASKED_TEST_FORMAT_INFO
)
);
}
public function testDecodeWithBitDifference() : void
{
$expected = FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO,
self::MASKED_TEST_FORMAT_INFO
);
$this->assertEquals(
$expected,
FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO ^ 0x1,
self::MASKED_TEST_FORMAT_INFO ^ 0x1
)
);
$this->assertEquals(
$expected,
FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO ^ 0x3,
self::MASKED_TEST_FORMAT_INFO ^ 0x3
)
);
$this->assertEquals(
$expected,
FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO ^ 0x7,
self::MASKED_TEST_FORMAT_INFO ^ 0x7
)
);
$this->assertNull(
FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO ^ 0xf,
self::MASKED_TEST_FORMAT_INFO ^ 0xf
)
);
}
public function testDecodeWithMisRead() : void
{
$expected = FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO,
self::MASKED_TEST_FORMAT_INFO
);
$this->assertEquals(
$expected,
FormatInformation::decodeFormatInformation(
self::MASKED_TEST_FORMAT_INFO ^ 0x3,
self::MASKED_TEST_FORMAT_INFO ^ 0xf
)
);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\Mode;
use PHPUnit\Framework\TestCase;
class ModeTest extends TestCase
{
public function testBitsMatchConstants() : void
{
$this->assertSame(0x0, Mode::TERMINATOR()->getBits());
$this->assertSame(0x1, Mode::NUMERIC()->getBits());
$this->assertSame(0x2, Mode::ALPHANUMERIC()->getBits());
$this->assertSame(0x4, Mode::BYTE()->getBits());
$this->assertSame(0x8, Mode::KANJI()->getBits());
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\ReedSolomonCodec;
use PHPUnit\Framework\TestCase;
use SplFixedArray;
class ReedSolomonTest extends TestCase
{
public function tabs() : array
{
return [
[2, 0x7, 1, 1, 1],
[3, 0xb, 1, 1, 2],
[4, 0x13, 1, 1, 4],
[5, 0x25, 1, 1, 6],
[6, 0x43, 1, 1, 8],
[7, 0x89, 1, 1, 10],
[8, 0x11d, 1, 1, 32],
];
}
/**
* @dataProvider tabs
*/
public function testCodec(int $symbolSize, int $generatorPoly, int $firstRoot, int $primitive, int $numRoots) : void
{
mt_srand(0xdeadbeef, MT_RAND_PHP);
$blockSize = (1 << $symbolSize) - 1;
$dataSize = $blockSize - $numRoots;
$codec = new ReedSolomonCodec($symbolSize, $generatorPoly, $firstRoot, $primitive, $numRoots, 0);
for ($errors = 0; $errors <= $numRoots / 2; ++$errors) {
// Load block with random data and encode
$block = SplFixedArray::fromArray(array_fill(0, $blockSize, 0), false);
for ($i = 0; $i < $dataSize; ++$i) {
$block[$i] = mt_rand(0, $blockSize);
}
// Make temporary copy
$tBlock = clone $block;
$parity = SplFixedArray::fromArray(array_fill(0, $numRoots, 0), false);
$errorLocations = SplFixedArray::fromArray(array_fill(0, $blockSize, 0), false);
$erasures = [];
// Create parity
$codec->encode($block, $parity);
// Copy parity into test blocks
for ($i = 0; $i < $numRoots; ++$i) {
$block[$i + $dataSize] = $parity[$i];
$tBlock[$i + $dataSize] = $parity[$i];
}
// Seed with errors
for ($i = 0; $i < $errors; ++$i) {
$errorValue = mt_rand(1, $blockSize);
do {
$errorLocation = mt_rand(0, $blockSize);
} while (0 !== $errorLocations[$errorLocation]);
$errorLocations[$errorLocation] = 1;
if (mt_rand(0, 1)) {
$erasures[] = $errorLocation;
}
$tBlock[$errorLocation] ^= $errorValue;
}
$erasures = SplFixedArray::fromArray($erasures, false);
// Decode the errored block
$foundErrors = $codec->decode($tBlock, $erasures);
if ($errors > 0 && null === $foundErrors) {
$this->assertSame($block, $tBlock, 'Decoder failed to correct errors');
}
$this->assertSame($errors, $foundErrors, 'Found errors do not equal expected errors');
for ($i = 0; $i < $foundErrors; ++$i) {
if (0 === $errorLocations[$erasures[$i]]) {
$this->fail(sprintf('Decoder indicates error in location %d without error', $erasures[$i]));
}
}
$this->assertEquals($block, $tBlock, 'Decoder did not correct errors');
}
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Common;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use PHPUnit\Framework\TestCase;
class VersionTest extends TestCase
{
public function versions() : array
{
$array = [];
for ($i = 1; $i <= 40; ++$i) {
$array[] = [$i, 4 * $i + 17];
}
return $array;
}
public function decodeInformation() : array
{
return [
[7, 0x07c94],
[12, 0x0c762],
[17, 0x1145d],
[22, 0x168c9],
[27, 0x1b08e],
[32, 0x209d5],
];
}
/**
* @dataProvider versions
*/
public function testVersionForNumber(int $versionNumber, int $dimension) : void
{
$version = Version::getVersionForNumber($versionNumber);
$this->assertNotNull($version);
$this->assertEquals($versionNumber, $version->getVersionNumber());
$this->assertNotNull($version->getAlignmentPatternCenters());
if ($versionNumber > 1) {
$this->assertTrue(count($version->getAlignmentPatternCenters()) > 0);
}
$this->assertEquals($dimension, $version->getDimensionForVersion());
$this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::H()));
$this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::L()));
$this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::M()));
$this->assertNotNull($version->getEcBlocksForLevel(ErrorCorrectionLevel::Q()));
$this->assertNotNull($version->buildFunctionPattern());
}
/**
* @dataProvider versions
*/
public function testGetProvisionalVersionForDimension(int $versionNumber, int $dimension) : void
{
$this->assertSame(
$versionNumber,
Version::getProvisionalVersionForDimension($dimension)->getVersionNumber()
);
}
/**
* @dataProvider decodeInformation
*/
public function testDecodeVersionInformation(int $expectedVersion, int $mask) : void
{
$version = Version::decodeVersionInformation($mask);
$this->assertNotNull($version);
$this->assertSame($expectedVersion, $version->getVersionNumber());
}
}

View File

@ -0,0 +1,487 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Mode;
use BaconQrCode\Common\Version;
use BaconQrCode\Encoder\Encoder;
use BaconQrCode\Exception\WriterException;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionMethod;
use SplFixedArray;
final class EncoderTest extends TestCase
{
/**
* @var ReflectionMethod[]
*/
protected $methods = [];
public function setUp() : void
{
// Hack to be able to test protected methods
$reflection = new ReflectionClass(Encoder::class);
foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC) as $method) {
$method->setAccessible(true);
$this->methods[$method->getName()] = $method;
}
}
public function testGetAlphanumericCode() : void
{
// The first ten code points are numbers.
for ($i = 0; $i < 10; ++$i) {
$this->assertSame($i, $this->methods['getAlphanumericCode']->invoke(null, ord('0') + $i));
}
// The next 26 code points are capital alphabet letters.
for ($i = 10; $i < 36; ++$i) {
// The first ten code points are numbers
$this->assertSame($i, $this->methods['getAlphanumericCode']->invoke(null, ord('A') + $i - 10));
}
// Others are symbol letters.
$this->assertSame(36, $this->methods['getAlphanumericCode']->invoke(null, ord(' ')));
$this->assertSame(37, $this->methods['getAlphanumericCode']->invoke(null, ord('$')));
$this->assertSame(38, $this->methods['getAlphanumericCode']->invoke(null, ord('%')));
$this->assertSame(39, $this->methods['getAlphanumericCode']->invoke(null, ord('*')));
$this->assertSame(40, $this->methods['getAlphanumericCode']->invoke(null, ord('+')));
$this->assertSame(41, $this->methods['getAlphanumericCode']->invoke(null, ord('-')));
$this->assertSame(42, $this->methods['getAlphanumericCode']->invoke(null, ord('.')));
$this->assertSame(43, $this->methods['getAlphanumericCode']->invoke(null, ord('/')));
$this->assertSame(44, $this->methods['getAlphanumericCode']->invoke(null, ord(':')));
// Should return -1 for other letters.
$this->assertSame(-1, $this->methods['getAlphanumericCode']->invoke(null, ord('a')));
$this->assertSame(-1, $this->methods['getAlphanumericCode']->invoke(null, ord('#')));
$this->assertSame(-1, $this->methods['getAlphanumericCode']->invoke(null, ord("\0")));
}
public function testChooseMode() : void
{
// Numeric mode
$this->assertSame(Mode::NUMERIC(), $this->methods['chooseMode']->invoke(null, '0'));
$this->assertSame(Mode::NUMERIC(), $this->methods['chooseMode']->invoke(null, '0123456789'));
// Alphanumeric mode
$this->assertSame(Mode::ALPHANUMERIC(), $this->methods['chooseMode']->invoke(null, 'A'));
$this->assertSame(
Mode::ALPHANUMERIC(),
$this->methods['chooseMode']->invoke(null, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:')
);
// 8-bit byte mode
$this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, 'a'));
$this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, '#'));
$this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, ''));
// AIUE in Hiragana in SHIFT-JIS
$this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\x8\xa\x8\xa\x8\xa\x8\xa6"));
// Nihon in Kanji in SHIFT-JIS
$this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\x9\xf\x9\x7b"));
// Sou-Utso-Byou in Kanji in SHIFT-JIS
$this->assertSame(Mode::BYTE(), $this->methods['chooseMode']->invoke(null, "\xe\x4\x9\x5\x9\x61"));
}
public function testEncode() : void
{
$qrCode = Encoder::encode('ABCDEF', ErrorCorrectionLevel::H());
$expected = "<<\n"
. " mode: ALPHANUMERIC\n"
. " ecLevel: H\n"
. " version: 1\n"
. " maskPattern: 0\n"
. " matrix:\n"
. " 1 1 1 1 1 1 1 0 1 1 1 1 0 0 1 1 1 1 1 1 1\n"
. " 1 0 0 0 0 0 1 0 0 1 1 1 0 0 1 0 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 1 1 1 0 1 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 1 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0\n"
. " 0 0 1 0 1 1 1 0 1 1 0 0 1 1 0 0 0 1 0 0 1\n"
. " 1 0 1 1 1 0 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0\n"
. " 0 0 1 1 0 0 1 0 1 0 0 0 1 0 1 0 1 0 1 1 0\n"
. " 1 1 0 1 0 1 0 1 1 1 0 1 0 1 0 0 0 0 0 1 0\n"
. " 0 0 1 1 0 1 1 1 1 0 0 0 1 0 1 0 1 1 1 1 0\n"
. " 0 0 0 0 0 0 0 0 1 0 0 1 1 1 0 1 0 1 0 0 0\n"
. " 1 1 1 1 1 1 1 0 0 0 1 0 1 0 1 1 0 0 0 0 1\n"
. " 1 0 0 0 0 0 1 0 1 1 1 1 0 1 0 1 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 1 0 1 1 0 1 0 1 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 1 0 1 1 1 1 0 1 0 1 0\n"
. " 1 0 1 1 1 0 1 0 1 0 0 0 1 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 1 1 0 1 1 0 1 0 0 0 1 1\n"
. " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 0 1\n"
. ">>\n";
$this->assertSame($expected, (string) $qrCode);
}
public function testSimpleUtf8Eci() : void
{
$qrCode = Encoder::encode('hello', ErrorCorrectionLevel::H(), 'utf-8');
$expected = "<<\n"
. " mode: BYTE\n"
. " ecLevel: H\n"
. " version: 1\n"
. " maskPattern: 3\n"
. " matrix:\n"
. " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1\n"
. " 1 0 0 0 0 0 1 0 0 0 1 0 1 0 1 0 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 1 0 1 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 1 1 0 0 1 1 1 1 0 0 0 1 1 0 1 0 0 0 0\n"
. " 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 1 0 1 1 1 0\n"
. " 0 1 0 1 0 1 1 1 0 1 0 1 0 0 0 0 0 1 1 1 1\n"
. " 1 1 0 0 1 0 0 1 1 0 0 1 1 1 1 0 1 0 1 1 0\n"
. " 0 0 0 0 1 0 1 1 1 1 0 0 0 0 0 1 0 0 1 0 0\n"
. " 0 0 0 0 0 0 0 0 1 1 1 1 0 0 1 1 1 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 1 1 0 1 0 1 1 0 0 1 0 0\n"
. " 1 0 0 0 0 0 1 0 0 0 1 0 0 1 1 1 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 0\n"
. " 1 0 1 1 1 0 1 0 1 1 1 0 1 0 0 0 1 1 0 0 0\n"
. " 1 0 1 1 1 0 1 0 1 1 0 0 0 1 0 0 1 0 0 0 0\n"
. " 1 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 0 1 1 0\n"
. " 1 1 1 1 1 1 1 0 0 1 0 1 1 1 0 1 1 0 0 0 0\n"
. ">>\n";
$this->assertSame($expected, (string) $qrCode);
}
public function testAppendModeInfo() : void
{
$bits = new BitArray();
$this->methods['appendModeInfo']->invoke(null, Mode::NUMERIC(), $bits);
$this->assertSame(' ...X', (string) $bits);
}
public function testAppendLengthInfo() : void
{
// 1 letter (1/1), 10 bits.
$bits = new BitArray();
$this->methods['appendLengthInfo']->invoke(
null,
1,
Version::getVersionForNumber(1),
Mode::NUMERIC(),
$bits
);
$this->assertSame(' ........ .X', (string) $bits);
// 2 letters (2/1), 11 bits.
$bits = new BitArray();
$this->methods['appendLengthInfo']->invoke(
null,
2,
Version::getVersionForNumber(10),
Mode::ALPHANUMERIC(),
$bits
);
$this->assertSame(' ........ .X.', (string) $bits);
// 255 letters (255/1), 16 bits.
$bits = new BitArray();
$this->methods['appendLengthInfo']->invoke(
null,
255,
Version::getVersionForNumber(27),
Mode::BYTE(),
$bits
);
$this->assertSame(' ........ XXXXXXXX', (string) $bits);
// 512 letters (1024/2), 12 bits.
$bits = new BitArray();
$this->methods['appendLengthInfo']->invoke(
null,
512,
Version::getVersionForNumber(40),
Mode::KANJI(),
$bits
);
$this->assertSame(' ..X..... ....', (string) $bits);
}
public function testAppendBytes() : void
{
// Should use appendNumericBytes.
// 1 = 01 = 0001 in 4 bits.
$bits = new BitArray();
$this->methods['appendBytes']->invoke(
null,
'1',
Mode::NUMERIC(),
$bits,
Encoder::DEFAULT_BYTE_MODE_ECODING
);
$this->assertSame(' ...X', (string) $bits);
// Should use appendAlphaNumericBytes.
// A = 10 = 0xa = 001010 in 6 bits.
$bits = new BitArray();
$this->methods['appendBytes']->invoke(
null,
'A',
Mode::ALPHANUMERIC(),
$bits,
Encoder::DEFAULT_BYTE_MODE_ECODING
);
$this->assertSame(' ..X.X.', (string) $bits);
// Should use append8BitBytes.
// 0x61, 0x62, 0x63
$bits = new BitArray();
$this->methods['appendBytes']->invoke(
null,
'abc',
Mode::BYTE(),
$bits,
Encoder::DEFAULT_BYTE_MODE_ECODING
);
$this->assertSame(' .XX....X .XX...X. .XX...XX', (string) $bits);
// Should use appendKanjiBytes.
// 0x93, 0x5f
$bits = new BitArray();
$this->methods['appendBytes']->invoke(
null,
"\x93\x5f",
Mode::KANJI(),
$bits,
Encoder::DEFAULT_BYTE_MODE_ECODING
);
$this->assertSame(' .XX.XX.. XXXXX', (string) $bits);
// Lower letters such as 'a' cannot be encoded in alphanumeric mode.
$this->expectException(WriterException::class);
$this->methods['appendBytes']->invoke(
null,
'a',
Mode::ALPHANUMERIC(),
$bits,
Encoder::DEFAULT_BYTE_MODE_ECODING
);
}
public function testTerminateBits() : void
{
$bits = new BitArray();
$this->methods['terminateBits']->invoke(null, 0, $bits);
$this->assertSame('', (string) $bits);
$bits = new BitArray();
$this->methods['terminateBits']->invoke(null, 1, $bits);
$this->assertSame(' ........', (string) $bits);
$bits = new BitArray();
$bits->appendBits(0, 3);
$this->methods['terminateBits']->invoke(null, 1, $bits);
$this->assertSame(' ........', (string) $bits);
$bits = new BitArray();
$bits->appendBits(0, 5);
$this->methods['terminateBits']->invoke(null, 1, $bits);
$this->assertSame(' ........', (string) $bits);
$bits = new BitArray();
$bits->appendBits(0, 8);
$this->methods['terminateBits']->invoke(null, 1, $bits);
$this->assertSame(' ........', (string) $bits);
$bits = new BitArray();
$this->methods['terminateBits']->invoke(null, 2, $bits);
$this->assertSame(' ........ XXX.XX..', (string) $bits);
$bits = new BitArray();
$bits->appendBits(0, 1);
$this->methods['terminateBits']->invoke(null, 3, $bits);
$this->assertSame(' ........ XXX.XX.. ...X...X', (string) $bits);
}
public function testGetNumDataBytesAndNumEcBytesForBlockId() : void
{
// Version 1-H.
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 26, 9, 1, 0);
$this->assertSame(9, $numDataBytes);
$this->assertSame(17, $numEcBytes);
// Version 3-H. 2 blocks.
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 70, 26, 2, 0);
$this->assertSame(13, $numDataBytes);
$this->assertSame(22, $numEcBytes);
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 70, 26, 2, 1);
$this->assertSame(13, $numDataBytes);
$this->assertSame(22, $numEcBytes);
// Version 7-H. (4 + 1) blocks.
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 196, 66, 5, 0);
$this->assertSame(13, $numDataBytes);
$this->assertSame(26, $numEcBytes);
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 196, 66, 5, 4);
$this->assertSame(14, $numDataBytes);
$this->assertSame(26, $numEcBytes);
// Version 40-H. (20 + 61) blocks.
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 3706, 1276, 81, 0);
$this->assertSame(15, $numDataBytes);
$this->assertSame(30, $numEcBytes);
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 3706, 1276, 81, 20);
$this->assertSame(16, $numDataBytes);
$this->assertSame(30, $numEcBytes);
list($numDataBytes, $numEcBytes) = $this->methods['getNumDataBytesAndNumEcBytesForBlockId']
->invoke(null, 3706, 1276, 81, 80);
$this->assertSame(16, $numDataBytes);
$this->assertSame(30, $numEcBytes);
}
public function testInterleaveWithEcBytes() : void
{
$dataBytes = SplFixedArray::fromArray([32, 65, 205, 69, 41, 220, 46, 128, 236], false);
$in = new BitArray();
foreach ($dataBytes as $dataByte) {
$in->appendBits($dataByte, 8);
}
$outBits = $this->methods['interleaveWithEcBytes']->invoke(null, $in, 26, 9, 1);
$expected = SplFixedArray::fromArray([
// Data bytes.
32, 65, 205, 69, 41, 220, 46, 128, 236,
// Error correction bytes.
42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219, 61,
], false);
$out = $outBits->toBytes(0, count($expected));
$this->assertEquals($expected, $out);
}
public function testAppendNumericBytes() : void
{
// 1 = 01 = 0001 in 4 bits.
$bits = new BitArray();
$this->methods['appendNumericBytes']->invoke(null, '1', $bits);
$this->assertSame(' ...X', (string) $bits);
// 12 = 0xc = 0001100 in 7 bits.
$bits = new BitArray();
$this->methods['appendNumericBytes']->invoke(null, '12', $bits);
$this->assertSame(' ...XX..', (string) $bits);
// 123 = 0x7b = 0001111011 in 10 bits.
$bits = new BitArray();
$this->methods['appendNumericBytes']->invoke(null, '123', $bits);
$this->assertSame(' ...XXXX. XX', (string) $bits);
// 1234 = "123" + "4" = 0001111011 + 0100 in 14 bits.
$bits = new BitArray();
$this->methods['appendNumericBytes']->invoke(null, '1234', $bits);
$this->assertSame(' ...XXXX. XX.X..', (string) $bits);
// Empty
$bits = new BitArray();
$this->methods['appendNumericBytes']->invoke(null, '', $bits);
$this->assertSame('', (string) $bits);
}
public function testAppendAlphanumericBytes() : void
{
$bits = new BitArray();
$this->methods['appendAlphanumericBytes']->invoke(null, 'A', $bits);
$this->assertSame(' ..X.X.', (string) $bits);
$bits = new BitArray();
$this->methods['appendAlphanumericBytes']->invoke(null, 'AB', $bits);
$this->assertSame(' ..XXX..X X.X', (string) $bits);
$bits = new BitArray();
$this->methods['appendAlphanumericBytes']->invoke(null, 'ABC', $bits);
$this->assertSame(' ..XXX..X X.X..XX. .', (string) $bits);
// Empty
$bits = new BitArray();
$this->methods['appendAlphanumericBytes']->invoke(null, '', $bits);
$this->assertSame('', (string) $bits);
// Invalid data
$this->expectException(WriterException::class);
$bits = new BitArray();
$this->methods['appendAlphanumericBytes']->invoke(null, 'abc', $bits);
}
public function testAppend8BitBytes() : void
{
// 0x61, 0x62, 0x63
$bits = new BitArray();
$this->methods['append8BitBytes']->invoke(null, 'abc', $bits, Encoder::DEFAULT_BYTE_MODE_ECODING);
$this->assertSame(' .XX....X .XX...X. .XX...XX', (string) $bits);
// Empty
$bits = new BitArray();
$this->methods['append8BitBytes']->invoke(null, '', $bits, Encoder::DEFAULT_BYTE_MODE_ECODING);
$this->assertSame('', (string) $bits);
}
public function testAppendKanjiBytes() : void
{
// Numbers are from page 21 of JISX0510:2004
$bits = new BitArray();
$this->methods['appendKanjiBytes']->invoke(null, "\x93\x5f", $bits);
$this->assertSame(' .XX.XX.. XXXXX', (string) $bits);
$this->methods['appendKanjiBytes']->invoke(null, "\xe4\xaa", $bits);
$this->assertSame(' .XX.XX.. XXXXXXX. X.X.X.X. X.', (string) $bits);
}
public function testGenerateEcBytes() : void
{
// Numbers are from http://www.swetake.com/qr/qr3.html and
// http://www.swetake.com/qr/qr9.html
$dataBytes = SplFixedArray::fromArray([32, 65, 205, 69, 41, 220, 46, 128, 236], false);
$ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 17);
$expected = SplFixedArray::fromArray(
[42, 159, 74, 221, 244, 169, 239, 150, 138, 70, 237, 85, 224, 96, 74, 219, 61],
false
);
$this->assertEquals($expected, $ecBytes);
$dataBytes = SplFixedArray::fromArray(
[67, 70, 22, 38, 54, 70, 86, 102, 118, 134, 150, 166, 182, 198, 214],
false
);
$ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 18);
$expected = SplFixedArray::fromArray(
[175, 80, 155, 64, 178, 45, 214, 233, 65, 209, 12, 155, 117, 31, 140, 214, 27, 187],
false
);
$this->assertEquals($expected, $ecBytes);
// High-order zero coefficient case.
$dataBytes = SplFixedArray::fromArray([32, 49, 205, 69, 42, 20, 0, 236, 17], false);
$ecBytes = $this->methods['generateEcBytes']->invoke(null, $dataBytes, 17);
$expected = SplFixedArray::fromArray(
[0, 3, 130, 179, 194, 0, 55, 211, 110, 79, 98, 72, 170, 96, 211, 137, 213],
false
);
$this->assertEquals($expected, $ecBytes);
}
}

View File

@ -0,0 +1,251 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Encoder;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Encoder\MaskUtil;
use PHPUnit\Framework\TestCase;
class MaskUtilTest extends TestCase
{
public function dataMaskBits() : array
{
return [
[0, [
[1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 0, 1],
]],
[1, [
[1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0],
]],
[2, [
[1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
[1, 0, 0, 1, 0, 0],
]],
[3, [
[1, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 1],
[0, 1, 0, 0, 1, 0],
[1, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 1],
[0, 1, 0, 0, 1, 0],
]],
[4, [
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0],
[1, 1, 1, 0, 0, 0],
]],
[5, [
[1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0],
[1, 0, 1, 0, 1, 0],
[1, 0, 0, 1, 0, 0],
[1, 0, 0, 0, 0, 0],
]],
[6, [
[1, 1, 1, 1, 1, 1],
[1, 1, 1, 0, 0, 0],
[1, 1, 0, 1, 1, 0],
[1, 0, 1, 0, 1, 0],
[1, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 1, 1],
]],
[7, [
[1, 0, 1, 0, 1, 0],
[0, 0, 0, 1, 1, 1],
[1, 0, 0, 0, 1, 1],
[0, 1, 0, 1, 0, 1],
[1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 0, 0],
]],
];
}
/**
* @dataProvider dataMaskBits
*/
public function testGetDatMaskBit(int $maskPattern, array $expected) : void
{
for ($x = 0; $x < 6; ++$x) {
for ($y = 0; $y < 6; ++$y) {
$this->assertSame(
1 === $expected[$y][$x],
MaskUtil::getDataMaskBit($maskPattern, $x, $y)
);
}
}
}
public function testApplyMaskPenaltyRule1() : void
{
$matrix = new ByteMatrix(4, 1);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 0);
$matrix->set(2, 0, 0);
$matrix->set(3, 0, 0);
$this->assertSame(0, MaskUtil::applyMaskPenaltyRule1($matrix));
// Horizontal
$matrix = new ByteMatrix(6, 1);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 0);
$matrix->set(2, 0, 0);
$matrix->set(3, 0, 0);
$matrix->set(4, 0, 0);
$matrix->set(5, 0, 1);
$this->assertSame(3, MaskUtil::applyMaskPenaltyRule1($matrix));
$matrix->set(5, 0, 0);
$this->assertSame(4, MaskUtil::applyMaskPenaltyRule1($matrix));
// Vertical
$matrix = new ByteMatrix(1, 6);
$matrix->set(0, 0, 0);
$matrix->set(0, 1, 0);
$matrix->set(0, 2, 0);
$matrix->set(0, 3, 0);
$matrix->set(0, 4, 0);
$matrix->set(0, 5, 1);
$this->assertSame(3, MaskUtil::applyMaskPenaltyRule1($matrix));
$matrix->set(0, 5, 0);
$this->assertSame(4, MaskUtil::applyMaskPenaltyRule1($matrix));
}
public function testApplyMaskPenaltyRule2() : void
{
$matrix = new ByteMatrix(1, 1);
$matrix->set(0, 0, 0);
$this->assertSame(0, MaskUtil::applyMaskPenaltyRule2($matrix));
$matrix = new ByteMatrix(2, 2);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 0);
$matrix->set(0, 1, 0);
$matrix->set(1, 1, 1);
$this->assertSame(0, MaskUtil::applyMaskPenaltyRule2($matrix));
$matrix = new ByteMatrix(2, 2);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 0);
$matrix->set(0, 1, 0);
$matrix->set(1, 1, 0);
$this->assertSame(3, MaskUtil::applyMaskPenaltyRule2($matrix));
$matrix = new ByteMatrix(3, 3);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 0);
$matrix->set(2, 0, 0);
$matrix->set(0, 1, 0);
$matrix->set(1, 1, 0);
$matrix->set(2, 1, 0);
$matrix->set(0, 2, 0);
$matrix->set(1, 2, 0);
$matrix->set(2, 2, 0);
$this->assertSame(3 * 4, MaskUtil::applyMaskPenaltyRule2($matrix));
}
public function testApplyMaskPenalty3() : void
{
// Horizontal 00001011101
$matrix = new ByteMatrix(11, 1);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 0);
$matrix->set(2, 0, 0);
$matrix->set(3, 0, 0);
$matrix->set(4, 0, 1);
$matrix->set(5, 0, 0);
$matrix->set(6, 0, 1);
$matrix->set(7, 0, 1);
$matrix->set(8, 0, 1);
$matrix->set(9, 0, 0);
$matrix->set(10, 0, 1);
$this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix));
// Horizontal 10111010000
$matrix = new ByteMatrix(11, 1);
$matrix->set(0, 0, 1);
$matrix->set(1, 0, 0);
$matrix->set(2, 0, 1);
$matrix->set(3, 0, 1);
$matrix->set(4, 0, 1);
$matrix->set(5, 0, 0);
$matrix->set(6, 0, 1);
$matrix->set(7, 0, 0);
$matrix->set(8, 0, 0);
$matrix->set(9, 0, 0);
$matrix->set(10, 0, 0);
$this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix));
// Vertical 00001011101
$matrix = new ByteMatrix(1, 11);
$matrix->set(0, 0, 0);
$matrix->set(0, 1, 0);
$matrix->set(0, 2, 0);
$matrix->set(0, 3, 0);
$matrix->set(0, 4, 1);
$matrix->set(0, 5, 0);
$matrix->set(0, 6, 1);
$matrix->set(0, 7, 1);
$matrix->set(0, 8, 1);
$matrix->set(0, 9, 0);
$matrix->set(0, 10, 1);
$this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix));
// Vertical 10111010000
$matrix = new ByteMatrix(1, 11);
$matrix->set(0, 0, 1);
$matrix->set(0, 1, 0);
$matrix->set(0, 2, 1);
$matrix->set(0, 3, 1);
$matrix->set(0, 4, 1);
$matrix->set(0, 5, 0);
$matrix->set(0, 6, 1);
$matrix->set(0, 7, 0);
$matrix->set(0, 8, 0);
$matrix->set(0, 9, 0);
$matrix->set(0, 10, 0);
$this->assertSame(40, MaskUtil::applyMaskPenaltyRule3($matrix));
}
public function testApplyMaskPenaltyRule4() : void
{
// Dark cell ratio = 0%
$matrix = new ByteMatrix(1, 1);
$matrix->set(0, 0, 0);
$this->assertSame(100, MaskUtil::applyMaskPenaltyRule4($matrix));
// Dark cell ratio = 5%
$matrix = new ByteMatrix(2, 1);
$matrix->set(0, 0, 0);
$matrix->set(0, 0, 1);
$this->assertSame(0, MaskUtil::applyMaskPenaltyRule4($matrix));
// Dark cell ratio = 66.67%
$matrix = new ByteMatrix(6, 1);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 1);
$matrix->set(2, 0, 1);
$matrix->set(3, 0, 1);
$matrix->set(4, 0, 1);
$matrix->set(5, 0, 0);
$this->assertSame(30, MaskUtil::applyMaskPenaltyRule4($matrix));
}
}

View File

@ -0,0 +1,335 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Encoder;
use BaconQrCode\Common\BitArray;
use BaconQrCode\Common\ErrorCorrectionLevel;
use BaconQrCode\Common\Version;
use BaconQrCode\Encoder\ByteMatrix;
use BaconQrCode\Encoder\MatrixUtil;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionMethod;
class MatrixUtilTest extends TestCase
{
/**
* @var ReflectionMethod[]
*/
protected $methods = [];
public function setUp() : void
{
// Hack to be able to test protected methods
$reflection = new ReflectionClass(MatrixUtil::class);
foreach ($reflection->getMethods(ReflectionMethod::IS_STATIC) as $method) {
$method->setAccessible(true);
$this->methods[$method->getName()] = $method;
}
}
public function testToString() : void
{
$matrix = new ByteMatrix(3, 3);
$matrix->set(0, 0, 0);
$matrix->set(1, 0, 1);
$matrix->set(2, 0, 0);
$matrix->set(0, 1, 1);
$matrix->set(1, 1, 0);
$matrix->set(2, 1, 1);
$matrix->set(0, 2, -1);
$matrix->set(1, 2, -1);
$matrix->set(2, 2, -1);
$expected = " 0 1 0\n 1 0 1\n \n";
$this->assertSame($expected, (string) $matrix);
}
public function testClearMatrix() : void
{
$matrix = new ByteMatrix(2, 2);
MatrixUtil::clearMatrix($matrix);
$this->assertSame(-1, $matrix->get(0, 0));
$this->assertSame(-1, $matrix->get(1, 0));
$this->assertSame(-1, $matrix->get(0, 1));
$this->assertSame(-1, $matrix->get(1, 1));
}
public function testEmbedBasicPatterns1() : void
{
$matrix = new ByteMatrix(21, 21);
MatrixUtil::clearMatrix($matrix);
$this->methods['embedBasicPatterns']->invoke(
null,
Version::getVersionForNumber(1),
$matrix
);
$expected = " 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1\n"
. " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 \n"
. " 0 \n"
. " 1 \n"
. " 0 \n"
. " 1 \n"
. " 0 0 0 0 0 0 0 0 1 \n"
. " 1 1 1 1 1 1 1 0 \n"
. " 1 0 0 0 0 0 1 0 \n"
. " 1 0 1 1 1 0 1 0 \n"
. " 1 0 1 1 1 0 1 0 \n"
. " 1 0 1 1 1 0 1 0 \n"
. " 1 0 0 0 0 0 1 0 \n"
. " 1 1 1 1 1 1 1 0 \n";
$this->assertSame($expected, (string) $matrix);
}
public function testEmbedBasicPatterns2() : void
{
$matrix = new ByteMatrix(25, 25);
MatrixUtil::clearMatrix($matrix);
$this->methods['embedBasicPatterns']->invoke(
null,
Version::getVersionForNumber(2),
$matrix
);
$expected = " 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1\n"
. " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 \n"
. " 0 \n"
. " 1 \n"
. " 0 \n"
. " 1 \n"
. " 0 \n"
. " 1 \n"
. " 0 \n"
. " 1 1 1 1 1 1 \n"
. " 0 0 0 0 0 0 0 0 1 1 0 0 0 1 \n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 \n"
. " 1 0 0 0 0 0 1 0 1 0 0 0 1 \n"
. " 1 0 1 1 1 0 1 0 1 1 1 1 1 \n"
. " 1 0 1 1 1 0 1 0 \n"
. " 1 0 1 1 1 0 1 0 \n"
. " 1 0 0 0 0 0 1 0 \n"
. " 1 1 1 1 1 1 1 0 \n";
$this->assertSame($expected, (string) $matrix);
}
public function testEmbedTypeInfo() : void
{
$matrix = new ByteMatrix(21, 21);
MatrixUtil::clearMatrix($matrix);
$this->methods['embedTypeInfo']->invoke(
null,
ErrorCorrectionLevel::M(),
5,
$matrix
);
$expected = " 0 \n"
. " 1 \n"
. " 1 \n"
. " 1 \n"
. " 0 \n"
. " 0 \n"
. " \n"
. " 1 \n"
. " 1 0 0 0 0 0 0 1 1 1 0 0 1 1 1 0\n"
. " \n"
. " \n"
. " \n"
. " \n"
. " \n"
. " 0 \n"
. " 0 \n"
. " 0 \n"
. " 0 \n"
. " 0 \n"
. " 0 \n"
. " 1 \n";
$this->assertSame($expected, (string) $matrix);
}
public function testEmbedVersionInfo() : void
{
$matrix = new ByteMatrix(21, 21);
MatrixUtil::clearMatrix($matrix);
$this->methods['maybeEmbedVersionInfo']->invoke(
null,
Version::getVersionForNumber(7),
$matrix
);
$expected = " 0 0 1 \n"
. " 0 1 0 \n"
. " 0 1 0 \n"
. " 0 1 1 \n"
. " 1 1 1 \n"
. " 0 0 0 \n"
. " \n"
. " \n"
. " \n"
. " \n"
. " 0 0 0 0 1 0 \n"
. " 0 1 1 1 1 0 \n"
. " 1 0 0 1 1 0 \n"
. " \n"
. " \n"
. " \n"
. " \n"
. " \n"
. " \n"
. " \n"
. " \n";
$this->assertSame($expected, (string) $matrix);
}
public function testEmbedDataBits() : void
{
$matrix = new ByteMatrix(21, 21);
MatrixUtil::clearMatrix($matrix);
$this->methods['embedBasicPatterns']->invoke(
null,
Version::getVersionForNumber(1),
$matrix
);
$bits = new BitArray();
$this->methods['embedDataBits']->invoke(
null,
$bits,
-1,
$matrix
);
$expected = " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 1\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n"
. " 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n";
$this->assertSame($expected, (string) $matrix);
}
public function testBuildMatrix() : void
{
$bytes = [
32, 65, 205, 69, 41, 220, 46, 128, 236, 42, 159, 74, 221, 244, 169,
239, 150, 138, 70, 237, 85, 224, 96, 74, 219 , 61
];
$bits = new BitArray();
foreach ($bytes as $byte) {
$bits->appendBits($byte, 8);
}
$matrix = new ByteMatrix(21, 21);
MatrixUtil::buildMatrix(
$bits,
ErrorCorrectionLevel::H(),
Version::getVersionForNumber(1),
3,
$matrix
);
$expected = " 1 1 1 1 1 1 1 0 0 1 1 0 0 0 1 1 1 1 1 1 1\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 1\n"
. " 1 0 1 1 1 0 1 0 0 0 0 1 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 0 1 1 0 0 0 1 0 1 1 1 0 1\n"
. " 1 0 1 1 1 0 1 0 1 1 0 0 1 0 1 0 1 1 1 0 1\n"
. " 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 0 0 0 0 0 1\n"
. " 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0\n"
. " 0 0 1 1 0 0 1 1 1 0 0 1 1 1 1 0 1 0 0 0 0\n"
. " 1 0 1 0 1 0 0 0 0 0 1 1 1 0 0 1 0 1 1 1 0\n"
. " 1 1 1 1 0 1 1 0 1 0 1 1 1 0 0 1 1 1 0 1 0\n"
. " 1 0 1 0 1 1 0 1 1 1 0 0 1 1 1 0 0 1 0 1 0\n"
. " 0 0 1 0 0 1 1 1 0 0 0 0 0 0 1 0 1 1 1 1 1\n"
. " 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 0 0 1 0 1 1\n"
. " 1 1 1 1 1 1 1 0 1 1 1 1 0 0 0 0 1 0 1 1 0\n"
. " 1 0 0 0 0 0 1 0 0 0 0 1 0 1 1 1 0 0 0 0 0\n"
. " 1 0 1 1 1 0 1 0 0 1 0 0 1 1 0 0 1 0 0 1 1\n"
. " 1 0 1 1 1 0 1 0 1 1 0 1 0 0 0 0 0 1 1 1 0\n"
. " 1 0 1 1 1 0 1 0 1 1 1 1 0 0 0 0 1 1 1 0 0\n"
. " 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0\n"
. " 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 0 1 0 0 1 0\n";
$this->assertSame($expected, (string) $matrix);
}
public function testFindMsbSet() : void
{
$this->assertSame(0, $this->methods['findMsbSet']->invoke(null, 0));
$this->assertSame(1, $this->methods['findMsbSet']->invoke(null, 1));
$this->assertSame(8, $this->methods['findMsbSet']->invoke(null, 0x80));
$this->assertSame(32, $this->methods['findMsbSet']->invoke(null, 0x80000000));
}
public function testCalculateBchCode() : void
{
// Encoding of type information.
// From Appendix C in JISX0510:2004 (p 65)
$this->assertSame(0xdc, $this->methods['calculateBchCode']->invoke(null, 5, 0x537));
// From http://www.swetake.com/qr/qr6.html
$this->assertSame(0x1c2, $this->methods['calculateBchCode']->invoke(null, 0x13, 0x537));
// From http://www.swetake.com/qr/qr11.html
$this->assertSame(0x214, $this->methods['calculateBchCode']->invoke(null, 0x1b, 0x537));
// Encoding of version information.
// From Appendix D in JISX0510:2004 (p 68)
$this->assertSame(0xc94, $this->methods['calculateBchCode']->invoke(null, 7, 0x1f25));
$this->assertSame(0x5bc, $this->methods['calculateBchCode']->invoke(null, 8, 0x1f25));
$this->assertSame(0xa99, $this->methods['calculateBchCode']->invoke(null, 9, 0x1f25));
$this->assertSame(0x4d3, $this->methods['calculateBchCode']->invoke(null, 10, 0x1f25));
$this->assertSame(0x9a6, $this->methods['calculateBchCode']->invoke(null, 20, 0x1f25));
$this->assertSame(0xd75, $this->methods['calculateBchCode']->invoke(null, 30, 0x1f25));
$this->assertSame(0xc69, $this->methods['calculateBchCode']->invoke(null, 40, 0x1f25));
}
public function testMakeVersionInfoBits() : void
{
// From Appendix D in JISX0510:2004 (p 68)
$bits = new BitArray();
$this->methods['makeVersionInfoBits']->invoke(null, Version::getVersionForNumber(7), $bits);
$this->assertSame(' ...XXXXX ..X..X.X ..', (string) $bits);
}
public function testMakeTypeInfoBits() : void
{
// From Appendix D in JISX0510:2004 (p 68)
$bits = new BitArray();
$this->methods['makeTypeInfoBits']->invoke(null, ErrorCorrectionLevel::M(), 5, $bits);
$this->assertSame(' X......X X..XXX.', (string) $bits);
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);
namespace BaconQrCodeTest\Integration;
use BaconQrCode\Renderer\Color\Rgb;
use BaconQrCode\Renderer\Eye\SquareEye;
use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Module\SquareModule;
use BaconQrCode\Renderer\RendererStyle\EyeFill;
use BaconQrCode\Renderer\RendererStyle\Fill;
use BaconQrCode\Renderer\RendererStyle\Gradient;
use BaconQrCode\Renderer\RendererStyle\GradientType;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;
/**
* @group integration
*/
final class ImagickRenderingTest extends TestCase
{
use MatchesSnapshots;
/**
* @requires extension imagick
*/
public function testGenericQrCode() : void
{
$renderer = new ImageRenderer(
new RendererStyle(400),
new ImagickImageBackEnd()
);
$writer = new Writer($renderer);
$tempName = tempnam(sys_get_temp_dir(), 'test') . '.png';
$writer->writeFile('Hello World!', $tempName);
$this->assertMatchesFileSnapshot($tempName);
unlink($tempName);
}
/**
* @requires extension imagick
*/
public function testIssue79() : void
{
$eye = SquareEye::instance();
$squareModule = SquareModule::instance();
$eyeFill = new EyeFill(new Rgb(100, 100, 55), new Rgb(100, 100, 255));
$gradient = new Gradient(new Rgb(100, 100, 55), new Rgb(100, 100, 255), GradientType::HORIZONTAL());
$renderer = new ImageRenderer(
new RendererStyle(
400,
2,
$squareModule,
$eye,
Fill::withForegroundGradient(new Rgb(255, 255, 255), $gradient, $eyeFill, $eyeFill, $eyeFill)
),
new ImagickImageBackEnd()
);
$writer = new Writer($renderer);
$tempName = tempnam(sys_get_temp_dir(), 'test') . '.png';
$writer->writeFile('https://apiroad.net/very-long-url', $tempName);
$this->assertMatchesFileSnapshot($tempName);
unlink($tempName);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

120
vendor/bin/carbon 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 (../nesbot/carbon/bin/carbon)
* 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__ . '/..'.'/nesbot/carbon/bin/carbon');
exit(0);
}
}
include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';

120
vendor/bin/patch-type-declarations 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/error-handler/Resources/bin/patch-type-declarations)
* 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/error-handler/Resources/bin/patch-type-declarations');
exit(0);
}
}
include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';

120
vendor/bin/php-parse 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 (../nikic/php-parser/bin/php-parse)
* 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__ . '/..'.'/nikic/php-parser/bin/php-parse');
exit(0);
}
}
include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';

123
vendor/bin/phpunit vendored Executable file
View File

@ -0,0 +1,123 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
* 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';
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
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 = 'phpvfscomposer://'.$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);
}
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
$data = str_replace('__FILE__', var_export($this->realpath, true), $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__ . '/..'.'/phpunit/phpunit/phpunit');
exit(0);
}
}
include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';

120
vendor/bin/pint 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 (../laravel/pint/builds/pint)
* 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__ . '/..'.'/laravel/pint/builds/pint');
exit(0);
}
}
include __DIR__ . '/..'.'/laravel/pint/builds/pint';

120
vendor/bin/psysh 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 (../psy/psysh/bin/psysh)
* 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__ . '/..'.'/psy/psysh/bin/psysh');
exit(0);
}
}
include __DIR__ . '/..'.'/psy/psysh/bin/psysh';

37
vendor/bin/sail vendored Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath $selfArg 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../laravel/sail/bin' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${dir}/sail" "$@"
return
fi
fi
"${dir}/sail" "$@"

120
vendor/bin/var-dump-server 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/var-dumper/Resources/bin/var-dump-server)
* 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/var-dumper/Resources/bin/var-dump-server');
exit(0);
}
}
include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';

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';

445
vendor/brick/math/CHANGELOG.md vendored Executable file
View File

@ -0,0 +1,445 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.11.0](https://github.com/brick/math/releases/tag/0.11.0) - 2023-01-16
💥 **Breaking changes**
- Minimum PHP version is now 8.0
- Methods accepting a union of types are now strongly typed<sup>*</sup>
- `MathException` now extends `Exception` instead of `RuntimeException`
<sup>* You may now run into type errors if you were passing `Stringable` objects to `of()` or any of the methods
internally calling `of()`, with `strict_types` enabled. You can fix this by casting `Stringable` objects to `string`
first.</sup>
## [0.10.2](https://github.com/brick/math/releases/tag/0.10.2) - 2022-08-11
👌 **Improvements**
- `BigRational::toFloat()` now simplifies the fraction before performing division (#73) thanks to @olsavmic
## [0.10.1](https://github.com/brick/math/releases/tag/0.10.1) - 2022-08-02
✨ **New features**
- `BigInteger::gcdMultiple()` returns the GCD of multiple `BigInteger` numbers
## [0.10.0](https://github.com/brick/math/releases/tag/0.10.0) - 2022-06-18
💥 **Breaking changes**
- Minimum PHP version is now 7.4
## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15
🚀 **Compatibility with PHP 8.1**
- Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (#60) thanks @TRowbotham
## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
🐛 **Bug fix**
- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
✨ **New features**
- `BigInteger::not()` returns the bitwise `NOT` value
🐛 **Bug fixes**
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
👌 **Improvements**
- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
💥 **Breaking changes**
- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
🐛 **Bug fix**
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
🚑 **Critical fix**
- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
✨ **New features**
- `BigInteger::modInverse()` calculates a modular multiplicative inverse
- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
💩 **Deprecations**
- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
🐛 **Fixes**
- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
⚡️ **Optimizations**
- additional optimization in `BigInteger::remainder()`
## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
✨ **New features**
- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
✨ **New features**
- `BigInteger::isEven()` tests whether the number is even
- `BigInteger::isOdd()` tests whether the number is odd
- `BigInteger::testBit()` tests if a bit is set
- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
🛠️ **Maintenance release**
Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
This is a maintenance release: no bug fixes, no new features, no breaking changes.
## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
✨ **New feature**
`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
✨ **New feature**
`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
⚡️ **Performance improvements**
A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
🐛 **Bug fixes**
- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
✨ **New features**
- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
💩 **Deprecations**
- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
- the `$base` parameter is required, it does not default to `10`
- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
**Improvements**
- Safer conversion from `float` when using custom locales
- **Much faster** `NativeCalculator` implementation 🚀
You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
**New method**
`BigNumber::sum()` returns the sum of one or more numbers.
## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
Thanks @manowark 👍
## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
**New method**
`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
**New method**
`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
**New exception**
`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
**Performance update**
- Further improvement of `toInt()` performance
- `NativeCalculator` can now perform some multiplications more efficiently
## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
Performance optimization of `toInt()` methods.
## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
**Breaking changes**
The following deprecated methods have been removed. Use the new method name instead:
| Method removed | Replacement method |
| --- | --- |
| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
---
**New features**
`BigInteger` has been augmented with 5 new methods for bitwise operations:
| New method | Description |
| --- | --- |
| `and()` | performs a bitwise `AND` operation on two numbers |
| `or()` | performs a bitwise `OR` operation on two numbers |
| `xor()` | performs a bitwise `XOR` operation on two numbers |
| `shiftedLeft()` | returns the number shifted left by a number of bits |
| `shiftedRight()` | returns the number shifted right by a number of bits |
Thanks to @DASPRiD 👍
## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
**New method:** `BigDecimal::hasNonZeroFractionalPart()`
**Renamed/deprecated methods:**
- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
**Performance update**
`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
This is a maintenance release, no code has been changed.
- When installed with `--no-dev`, the autoloader does not autoload tests anymore
- Tests and other files unnecessary for production are excluded from the dist package
This will help make installations more compact.
## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
Methods renamed:
- `BigNumber:sign()` has been renamed to `getSign()`
- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
- `BigDecimal::scale()` has been renamed to `getScale()`
- `BigDecimal::integral()` has been renamed to `getIntegral()`
- `BigDecimal::fraction()` has been renamed to `getFraction()`
- `BigRational::numerator()` has been renamed to `getNumerator()`
- `BigRational::denominator()` has been renamed to `getDenominator()`
Classes renamed:
- `ArithmeticException` has been renamed to `MathException`
## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
The base class for all exceptions is now `MathException`.
`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
A number of methods have been renamed:
- `BigNumber:sign()` is deprecated; use `getSign()` instead
- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
- `BigDecimal::scale()` is deprecated; use `getScale()` instead
- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
The old methods will be removed in version 0.7.0.
## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
- Method `BigNumber::toInteger()` has been renamed to `toInt()`
## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
The JSON output is always a string.
## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
**New method: `BigNumber::toScale()`**
This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
**New features**
- Common `BigNumber` interface for all classes, with the following methods:
- `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
- `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
- `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
- `toInteger()` and `toFloat()` conversion methods to native types
- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
- New methods: `BigRational::quotient()` and `remainder()`
- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
- Factory methods `zero()`, `one()` and `ten()` available in all classes
- Rounding mode reintroduced in `BigInteger::dividedBy()`
This release also comes with many performance improvements.
---
**Breaking changes**
- `BigInteger`:
- `getSign()` is renamed to `sign()`
- `toString()` is renamed to `toBase()`
- `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
- `BigDecimal`:
- `getSign()` is renamed to `sign()`
- `getUnscaledValue()` is renamed to `unscaledValue()`
- `getScale()` is renamed to `scale()`
- `getIntegral()` is renamed to `integral()`
- `getFraction()` is renamed to `fraction()`
- `divideAndRemainder()` is renamed to `quotientAndRemainder()`
- `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
- `toBigInteger()` does not accept a `$roundingMode` parameter anymore
- `toBigRational()` does not simplify the fraction anymore; explicitly add `->simplified()` to get the previous behaviour
- `BigRational`:
- `getSign()` is renamed to `sign()`
- `getNumerator()` is renamed to `numerator()`
- `getDenominator()` is renamed to `denominator()`
- `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
- Miscellaneous:
- `ArithmeticException` is moved to an `Exception\` sub-namespace
- `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
Backport of two bug fixes from the 0.5 branch:
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
New method: `BigDecimal::stripTrailingZeros()`
## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
Introducing a `BigRational` class, to perform calculations on fractions of any size.
## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
`BigInteger::dividedBy()` now always returns the quotient of the division.
## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
Backport of two bug fixes from the 0.5 branch:
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
New methods:
- `BigInteger::remainder()` returns the remainder of a division only
- `BigInteger::gcd()` returns the greatest common divisor of two numbers
## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
Fix `toString()` not handling negative numbers.
## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
- `-1` if the number is negative
- `0` if the number is zero
- `1` if the number is positive
## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
Minor performance improvements
## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
Added `BigDecimal::divideAndRemainder()`
## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
- `min()` and `max()` do not accept an `array` anymore, but a variable number of parameters
- **minimum PHP version is now 5.6**
- continuous integration with PHP 7
## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
- Added `BigInteger::power()`
- Added HHVM support
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
First beta release.

20
vendor/brick/math/LICENSE vendored Executable file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013-present Benjamin Morel
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.

34
vendor/brick/math/composer.json vendored Executable file
View File

@ -0,0 +1,34 @@
{
"name": "brick/math",
"description": "Arbitrary-precision arithmetic library",
"type": "library",
"keywords": [
"Brick",
"Math",
"Arbitrary-precision",
"Arithmetic",
"BigInteger",
"BigDecimal",
"BigRational",
"Bignum"
],
"license": "MIT",
"require": {
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"php-coveralls/php-coveralls": "^2.2",
"vimeo/psalm": "5.0.0"
},
"autoload": {
"psr-4": {
"Brick\\Math\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Brick\\Math\\Tests\\": "tests/"
}
}
}

786
vendor/brick/math/src/BigDecimal.php vendored Executable file
View File

@ -0,0 +1,786 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NegativeNumberException;
use Brick\Math\Internal\Calculator;
/**
* Immutable, arbitrary-precision signed decimal numbers.
*
* @psalm-immutable
*/
final class BigDecimal extends BigNumber
{
/**
* The unscaled value of this decimal number.
*
* This is a string of digits with an optional leading minus sign.
* No leading zero must be present.
* No leading minus sign must be present if the value is 0.
*/
private string $value;
/**
* The scale (number of digits after the decimal point) of this decimal number.
*
* This must be zero or more.
*/
private int $scale;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param string $value The unscaled value, validated.
* @param int $scale The scale, validated.
*/
protected function __construct(string $value, int $scale = 0)
{
$this->value = $value;
$this->scale = $scale;
}
/**
* Creates a BigDecimal of the given value.
*
* @throws MathException If the value cannot be converted to a BigDecimal.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigDecimal
{
return parent::of($value)->toBigDecimal();
}
/**
* Creates a BigDecimal from an unscaled value and a scale.
*
* Example: `(12345, 3)` will result in the BigDecimal `12.345`.
*
* @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
* @param int $scale The scale of the number, positive or zero.
*
* @throws \InvalidArgumentException If the scale is negative.
*
* @psalm-pure
*/
public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('The scale cannot be negative.');
}
return new BigDecimal((string) BigInteger::of($value), $scale);
}
/**
* Returns a BigDecimal representing zero, with a scale of zero.
*
* @psalm-pure
*/
public static function zero() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $zero
*/
static $zero;
if ($zero === null) {
$zero = new BigDecimal('0');
}
return $zero;
}
/**
* Returns a BigDecimal representing one, with a scale of zero.
*
* @psalm-pure
*/
public static function one() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $one
*/
static $one;
if ($one === null) {
$one = new BigDecimal('1');
}
return $one;
}
/**
* Returns a BigDecimal representing ten, with a scale of zero.
*
* @psalm-pure
*/
public static function ten() : BigDecimal
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigDecimal|null $ten
*/
static $ten;
if ($ten === null) {
$ten = new BigDecimal('10');
}
return $ten;
}
/**
* Returns the sum of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*/
public function plus(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
if ($this->value === '0' && $this->scale <= $that->scale) {
return $that;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->add($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the difference of this number and the given one.
*
* The result has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
*
* @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
*/
public function minus(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0' && $that->scale <= $this->scale) {
return $this;
}
[$a, $b] = $this->scaleValues($this, $that);
$value = Calculator::get()->sub($a, $b);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the product of this number and the given one.
*
* The result has a scale of `$this->scale + $that->scale`.
*
* @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
*
* @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
*/
public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '1' && $that->scale === 0) {
return $this;
}
if ($this->value === '1' && $this->scale === 0) {
return $that;
}
$value = Calculator::get()->mul($this->value, $that->value);
$scale = $this->scale + $that->scale;
return new BigDecimal($value, $scale);
}
/**
* Returns the result of the division of this number by the given one, at the given scale.
*
* @param BigNumber|int|float|string $that The divisor.
* @param int|null $scale The desired scale, or null to use the scale of this number.
* @param int $roundingMode An optional rounding mode.
*
* @throws \InvalidArgumentException If the scale or rounding mode is invalid.
* @throws MathException If the number is invalid, is zero, or rounding was necessary.
*/
public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
if ($scale === null) {
$scale = $this->scale;
} elseif ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
return $this;
}
$p = $this->valueWithMinScale($that->scale + $scale);
$q = $that->valueWithMinScale($this->scale - $scale);
$result = Calculator::get()->divRound($p, $q, $roundingMode);
return new BigDecimal($result, $scale);
}
/**
* Returns the exact result of the division of this number by the given one.
*
* The scale of the result is automatically calculated to fit all the fraction digits.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
* or the result yields an infinite number of digits.
*/
public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->value === '0') {
throw DivisionByZeroException::divisionByZero();
}
[, $b] = $this->scaleValues($this, $that);
$d = \rtrim($b, '0');
$scale = \strlen($b) - \strlen($d);
$calculator = Calculator::get();
foreach ([5, 2] as $prime) {
for (;;) {
$lastDigit = (int) $d[-1];
if ($lastDigit % $prime !== 0) {
break;
}
$d = $calculator->divQ($d, (string) $prime);
$scale++;
}
}
return $this->dividedBy($that, $scale)->stripTrailingZeros();
}
/**
* Returns this number exponentiated to the given value.
*
* The result has a scale of `$this->scale * $exponent`.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*/
public function power(int $exponent) : BigDecimal
{
if ($exponent === 0) {
return BigDecimal::one();
}
if ($exponent === 1) {
return $this;
}
if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
throw new \InvalidArgumentException(\sprintf(
'The exponent %d is not in the range 0 to %d.',
$exponent,
Calculator::MAX_POWER
));
}
return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
}
/**
* Returns the quotient of the division of this number by this given one.
*
* The quotient has a scale of `0`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function quotient(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$quotient = Calculator::get()->divQ($p, $q);
return new BigDecimal($quotient, 0);
}
/**
* Returns the remainder of the division of this number by this given one.
*
* The remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function remainder(BigNumber|int|float|string $that) : BigDecimal
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
$remainder = Calculator::get()->divR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
return new BigDecimal($remainder, $scale);
}
/**
* Returns the quotient and remainder of the division of this number by the given one.
*
* The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
*
* @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
*
* @return BigDecimal[] An array containing the quotient and the remainder.
*
* @throws MathException If the divisor is not a valid decimal number, or is zero.
*/
public function quotientAndRemainder(BigNumber|int|float|string $that) : array
{
$that = BigDecimal::of($that);
if ($that->isZero()) {
throw DivisionByZeroException::divisionByZero();
}
$p = $this->valueWithMinScale($that->scale);
$q = $that->valueWithMinScale($this->scale);
[$quotient, $remainder] = Calculator::get()->divQR($p, $q);
$scale = $this->scale > $that->scale ? $this->scale : $that->scale;
$quotient = new BigDecimal($quotient, 0);
$remainder = new BigDecimal($remainder, $scale);
return [$quotient, $remainder];
}
/**
* Returns the square root of this number, rounded down to the given number of decimals.
*
* @throws \InvalidArgumentException If the scale is negative.
* @throws NegativeNumberException If this number is negative.
*/
public function sqrt(int $scale) : BigDecimal
{
if ($scale < 0) {
throw new \InvalidArgumentException('Scale cannot be negative.');
}
if ($this->value === '0') {
return new BigDecimal('0', $scale);
}
if ($this->value[0] === '-') {
throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
}
$value = $this->value;
$addDigits = 2 * $scale - $this->scale;
if ($addDigits > 0) {
// add zeros
$value .= \str_repeat('0', $addDigits);
} elseif ($addDigits < 0) {
// trim digits
if (-$addDigits >= \strlen($this->value)) {
// requesting a scale too low, will always yield a zero result
return new BigDecimal('0', $scale);
}
$value = \substr($value, 0, $addDigits);
}
$value = Calculator::get()->sqrt($value);
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
*/
public function withPointMovedLeft(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedRight(-$n);
}
return new BigDecimal($this->value, $this->scale + $n);
}
/**
* Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
*/
public function withPointMovedRight(int $n) : BigDecimal
{
if ($n === 0) {
return $this;
}
if ($n < 0) {
return $this->withPointMovedLeft(-$n);
}
$value = $this->value;
$scale = $this->scale - $n;
if ($scale < 0) {
if ($value !== '0') {
$value .= \str_repeat('0', -$scale);
}
$scale = 0;
}
return new BigDecimal($value, $scale);
}
/**
* Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
*/
public function stripTrailingZeros() : BigDecimal
{
if ($this->scale === 0) {
return $this;
}
$trimmedValue = \rtrim($this->value, '0');
if ($trimmedValue === '') {
return BigDecimal::zero();
}
$trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
if ($trimmableZeros === 0) {
return $this;
}
if ($trimmableZeros > $this->scale) {
$trimmableZeros = $this->scale;
}
$value = \substr($this->value, 0, -$trimmableZeros);
$scale = $this->scale - $trimmableZeros;
return new BigDecimal($value, $scale);
}
/**
* Returns the absolute value of this number.
*/
public function abs() : BigDecimal
{
return $this->isNegative() ? $this->negated() : $this;
}
/**
* Returns the negated value of this number.
*/
public function negated() : BigDecimal
{
return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
}
public function compareTo(BigNumber|int|float|string $that) : int
{
$that = BigNumber::of($that);
if ($that instanceof BigInteger) {
$that = $that->toBigDecimal();
}
if ($that instanceof BigDecimal) {
[$a, $b] = $this->scaleValues($this, $that);
return Calculator::get()->cmp($a, $b);
}
return - $that->compareTo($this);
}
public function getSign() : int
{
return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
}
public function getUnscaledValue() : BigInteger
{
return self::newBigInteger($this->value);
}
public function getScale() : int
{
return $this->scale;
}
/**
* Returns a string representing the integral part of this decimal number.
*
* Example: `-123.456` => `-123`.
*/
public function getIntegralPart() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale);
}
/**
* Returns a string representing the fractional part of this decimal number.
*
* If the scale is zero, an empty string is returned.
*
* Examples: `-123.456` => '456', `123` => ''.
*/
public function getFractionalPart() : string
{
if ($this->scale === 0) {
return '';
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, -$this->scale);
}
/**
* Returns whether this decimal number has a non-zero fractional part.
*/
public function hasNonZeroFractionalPart() : bool
{
return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
}
public function toBigInteger() : BigInteger
{
$zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
return self::newBigInteger($zeroScaleDecimal->value);
}
public function toBigDecimal() : BigDecimal
{
return $this;
}
public function toBigRational() : BigRational
{
$numerator = self::newBigInteger($this->value);
$denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
return self::newBigRational($numerator, $denominator, false);
}
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
if ($scale === $this->scale) {
return $this;
}
return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
return (float) (string) $this;
}
public function __toString() : string
{
if ($this->scale === 0) {
return $this->value;
}
$value = $this->getUnscaledValueWithLeadingZeros();
return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{value: string, scale: int}
*/
public function __serialize(): array
{
return ['value' => $this->value, 'scale' => $this->scale];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{value: string, scale: int} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
if (isset($this->value)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
$this->value = $data['value'];
$this->scale = $data['scale'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->value . ':' . $this->scale;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->value)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$value, $scale] = \explode(':', $value);
$this->value = $value;
$this->scale = (int) $scale;
}
/**
* Puts the internal values of the given decimal numbers on the same scale.
*
* @return array{string, string} The scaled integer values of $x and $y.
*/
private function scaleValues(BigDecimal $x, BigDecimal $y) : array
{
$a = $x->value;
$b = $y->value;
if ($b !== '0' && $x->scale > $y->scale) {
$b .= \str_repeat('0', $x->scale - $y->scale);
} elseif ($a !== '0' && $x->scale < $y->scale) {
$a .= \str_repeat('0', $y->scale - $x->scale);
}
return [$a, $b];
}
private function valueWithMinScale(int $scale) : string
{
$value = $this->value;
if ($this->value !== '0' && $scale > $this->scale) {
$value .= \str_repeat('0', $scale - $this->scale);
}
return $value;
}
/**
* Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
*/
private function getUnscaledValueWithLeadingZeros() : string
{
$value = $this->value;
$targetLength = $this->scale + 1;
$negative = ($value[0] === '-');
$length = \strlen($value);
if ($negative) {
$length--;
}
if ($length >= $targetLength) {
return $this->value;
}
if ($negative) {
$value = \substr($value, 1);
}
$value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
if ($negative) {
$value = '-' . $value;
}
return $value;
}
}

1079
vendor/brick/math/src/BigInteger.php vendored Executable file

File diff suppressed because it is too large Load Diff

512
vendor/brick/math/src/BigNumber.php vendored Executable file
View File

@ -0,0 +1,512 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
/**
* Common interface for arbitrary-precision rational numbers.
*
* @psalm-immutable
*/
abstract class BigNumber implements \Serializable, \JsonSerializable
{
/**
* The regular expression used to parse integer, decimal and rational numbers.
*/
private const PARSE_REGEXP =
'/^' .
'(?<sign>[\-\+])?' .
'(?:' .
'(?:' .
'(?<integral>[0-9]+)?' .
'(?<point>\.)?' .
'(?<fractional>[0-9]+)?' .
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
')|(?:' .
'(?<numerator>[0-9]+)' .
'\/?' .
'(?<denominator>[0-9]+)' .
')' .
')' .
'$/';
/**
* Creates a BigNumber of the given value.
*
* The concrete return type is dependent on the given value, with the following rules:
*
* - BigNumber instances are returned as is
* - integer numbers are returned as BigInteger
* - floating point numbers are converted to a string then parsed as such
* - strings containing a `/` character are returned as BigRational
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
*
* @throws NumberFormatException If the format of the number is not valid.
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigNumber
{
if ($value instanceof BigNumber) {
return $value;
}
if (\is_int($value)) {
return new BigInteger((string) $value);
}
$value = \is_float($value) ? self::floatToString($value) : $value;
$throw = static function() use ($value) : void {
throw new NumberFormatException(\sprintf(
'The given value "%s" does not represent a valid number.',
$value
));
};
if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
$throw();
}
$getMatch = static fn(string $value): ?string => (($matches[$value] ?? '') !== '') ? $matches[$value] : null;
$sign = $getMatch('sign');
$numerator = $getMatch('numerator');
$denominator = $getMatch('denominator');
if ($numerator !== null) {
assert($denominator !== null);
if ($sign !== null) {
$numerator = $sign . $numerator;
}
$numerator = self::cleanUp($numerator);
$denominator = self::cleanUp($denominator);
if ($denominator === '0') {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
return new BigRational(
new BigInteger($numerator),
new BigInteger($denominator),
false
);
}
$point = $getMatch('point');
$integral = $getMatch('integral');
$fractional = $getMatch('fractional');
$exponent = $getMatch('exponent');
if ($integral === null && $fractional === null) {
$throw();
}
if ($integral === null) {
$integral = '0';
}
if ($point !== null || $exponent !== null) {
$fractional = ($fractional ?? '');
$exponent = ($exponent !== null) ? (int) $exponent : 0;
if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
throw new NumberFormatException('Exponent too large.');
}
$unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
$scale = \strlen($fractional) - $exponent;
if ($scale < 0) {
if ($unscaledValue !== '0') {
$unscaledValue .= \str_repeat('0', - $scale);
}
$scale = 0;
}
return new BigDecimal($unscaledValue, $scale);
}
$integral = self::cleanUp(($sign ?? '') . $integral);
return new BigInteger($integral);
}
/**
* Safely converts float to string, avoiding locale-dependent issues.
*
* @see https://github.com/brick/math/pull/20
*
* @psalm-pure
* @psalm-suppress ImpureFunctionCall
*/
private static function floatToString(float $float) : string
{
$currentLocale = \setlocale(LC_NUMERIC, '0');
\setlocale(LC_NUMERIC, 'C');
$result = (string) $float;
\setlocale(LC_NUMERIC, $currentLocale);
return $result;
}
/**
* Proxy method to access BigInteger's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigInteger(string $value) : BigInteger
{
return new BigInteger($value);
}
/**
* Proxy method to access BigDecimal's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal
{
return new BigDecimal($value, $scale);
}
/**
* Proxy method to access BigRational's protected constructor from sibling classes.
*
* @internal
* @psalm-pure
*/
protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational
{
return new BigRational($numerator, $denominator, $checkDenominator);
}
/**
* Returns the minimum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function min(BigNumber|int|float|string ...$values) : static
{
$min = null;
foreach ($values as $value) {
$value = static::of($value);
if ($min === null || $value->isLessThan($min)) {
$min = $value;
}
}
if ($min === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $min;
}
/**
* Returns the maximum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-suppress LessSpecificReturnStatement
* @psalm-suppress MoreSpecificReturnType
* @psalm-pure
*/
public static function max(BigNumber|int|float|string ...$values) : static
{
$max = null;
foreach ($values as $value) {
$value = static::of($value);
if ($max === null || $value->isGreaterThan($max)) {
$max = $value;
}
}
if ($max === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $max;
}
/**
* Returns the sum of the given values.
*
* @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
* to an instance of the class this method is called on.
*
* @throws \InvalidArgumentException If no values are given.
* @throws MathException If an argument is not valid.
*
* @psalm-pure
*/
public static function sum(BigNumber|int|float|string ...$values) : static
{
/** @var static|null $sum */
$sum = null;
foreach ($values as $value) {
$value = static::of($value);
$sum = $sum === null ? $value : self::add($sum, $value);
}
if ($sum === null) {
throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
}
return $sum;
}
/**
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
*
* @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
* concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
* depending on their ability to perform the operation. This will also require a version bump because we're
* potentially breaking custom BigNumber implementations (if any...)
*
* @psalm-pure
*/
private static function add(BigNumber $a, BigNumber $b) : BigNumber
{
if ($a instanceof BigRational) {
return $a->plus($b);
}
if ($b instanceof BigRational) {
return $b->plus($a);
}
if ($a instanceof BigDecimal) {
return $a->plus($b);
}
if ($b instanceof BigDecimal) {
return $b->plus($a);
}
/** @var BigInteger $a */
return $a->plus($b);
}
/**
* Removes optional leading zeros and + sign from the given number.
*
* @param string $number The number, validated as a non-empty string of digits with optional leading sign.
*
* @psalm-pure
*/
private static function cleanUp(string $number) : string
{
$firstChar = $number[0];
if ($firstChar === '+' || $firstChar === '-') {
$number = \substr($number, 1);
}
$number = \ltrim($number, '0');
if ($number === '') {
return '0';
}
if ($firstChar === '-') {
return '-' . $number;
}
return $number;
}
/**
* Checks if this number is equal to the given one.
*/
public function isEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) === 0;
}
/**
* Checks if this number is strictly lower than the given one.
*/
public function isLessThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) < 0;
}
/**
* Checks if this number is lower than or equal to the given one.
*/
public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) <= 0;
}
/**
* Checks if this number is strictly greater than the given one.
*/
public function isGreaterThan(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) > 0;
}
/**
* Checks if this number is greater than or equal to the given one.
*/
public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool
{
return $this->compareTo($that) >= 0;
}
/**
* Checks if this number equals zero.
*/
public function isZero() : bool
{
return $this->getSign() === 0;
}
/**
* Checks if this number is strictly negative.
*/
public function isNegative() : bool
{
return $this->getSign() < 0;
}
/**
* Checks if this number is negative or zero.
*/
public function isNegativeOrZero() : bool
{
return $this->getSign() <= 0;
}
/**
* Checks if this number is strictly positive.
*/
public function isPositive() : bool
{
return $this->getSign() > 0;
}
/**
* Checks if this number is positive or zero.
*/
public function isPositiveOrZero() : bool
{
return $this->getSign() >= 0;
}
/**
* Returns the sign of this number.
*
* @return int -1 if the number is negative, 0 if zero, 1 if positive.
*/
abstract public function getSign() : int;
/**
* Compares this number to the given one.
*
* @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
*
* @throws MathException If the number is not valid.
*/
abstract public function compareTo(BigNumber|int|float|string $that) : int;
/**
* Converts this number to a BigInteger.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
*/
abstract public function toBigInteger() : BigInteger;
/**
* Converts this number to a BigDecimal.
*
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
*/
abstract public function toBigDecimal() : BigDecimal;
/**
* Converts this number to a BigRational.
*/
abstract public function toBigRational() : BigRational;
/**
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
*
* @param int $scale The scale of the resulting `BigDecimal`.
* @param int $roundingMode A `RoundingMode` constant.
*
* @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
* This only applies when RoundingMode::UNNECESSARY is used.
*/
abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
/**
* Returns the exact value of this number as a native integer.
*
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
*
* @throws MathException If this number cannot be exactly converted to a native integer.
*/
abstract public function toInt() : int;
/**
* Returns an approximation of this number as a floating-point value.
*
* Note that this method can discard information as the precision of a floating-point value
* is inherently limited.
*
* If the number is greater than the largest representable floating point number, positive infinity is returned.
* If the number is less than the smallest representable floating point number, negative infinity is returned.
*/
abstract public function toFloat() : float;
/**
* Returns a string representation of this number.
*
* The output of this method can be parsed by the `of()` factory method;
* this will yield an object equal to this one, without any information loss.
*/
abstract public function __toString() : string;
public function jsonSerialize() : string
{
return $this->__toString();
}
}

445
vendor/brick/math/src/BigRational.php vendored Executable file
View File

@ -0,0 +1,445 @@
<?php
declare(strict_types=1);
namespace Brick\Math;
use Brick\Math\Exception\DivisionByZeroException;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
/**
* An arbitrarily large rational number.
*
* This class is immutable.
*
* @psalm-immutable
*/
final class BigRational extends BigNumber
{
/**
* The numerator.
*/
private BigInteger $numerator;
/**
* The denominator. Always strictly positive.
*/
private BigInteger $denominator;
/**
* Protected constructor. Use a factory method to obtain an instance.
*
* @param BigInteger $numerator The numerator.
* @param BigInteger $denominator The denominator.
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
*
* @throws DivisionByZeroException If the denominator is zero.
*/
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
{
if ($checkDenominator) {
if ($denominator->isZero()) {
throw DivisionByZeroException::denominatorMustNotBeZero();
}
if ($denominator->isNegative()) {
$numerator = $numerator->negated();
$denominator = $denominator->negated();
}
}
$this->numerator = $numerator;
$this->denominator = $denominator;
}
/**
* Creates a BigRational of the given value.
*
* @throws MathException If the value cannot be converted to a BigRational.
*
* @psalm-pure
*/
public static function of(BigNumber|int|float|string $value) : BigRational
{
return parent::of($value)->toBigRational();
}
/**
* Creates a BigRational out of a numerator and a denominator.
*
* If the denominator is negative, the signs of both the numerator and the denominator
* will be inverted to ensure that the denominator is always positive.
*
* @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger.
* @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
*
* @throws NumberFormatException If an argument does not represent a valid number.
* @throws RoundingNecessaryException If an argument represents a non-integer number.
* @throws DivisionByZeroException If the denominator is zero.
*
* @psalm-pure
*/
public static function nd(
BigNumber|int|float|string $numerator,
BigNumber|int|float|string $denominator,
) : BigRational {
$numerator = BigInteger::of($numerator);
$denominator = BigInteger::of($denominator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns a BigRational representing zero.
*
* @psalm-pure
*/
public static function zero() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $zero
*/
static $zero;
if ($zero === null) {
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
}
return $zero;
}
/**
* Returns a BigRational representing one.
*
* @psalm-pure
*/
public static function one() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $one
*/
static $one;
if ($one === null) {
$one = new BigRational(BigInteger::one(), BigInteger::one(), false);
}
return $one;
}
/**
* Returns a BigRational representing ten.
*
* @psalm-pure
*/
public static function ten() : BigRational
{
/**
* @psalm-suppress ImpureStaticVariable
* @var BigRational|null $ten
*/
static $ten;
if ($ten === null) {
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
}
return $ten;
}
public function getNumerator() : BigInteger
{
return $this->numerator;
}
public function getDenominator() : BigInteger
{
return $this->denominator;
}
/**
* Returns the quotient of the division of the numerator by the denominator.
*/
public function quotient() : BigInteger
{
return $this->numerator->quotient($this->denominator);
}
/**
* Returns the remainder of the division of the numerator by the denominator.
*/
public function remainder() : BigInteger
{
return $this->numerator->remainder($this->denominator);
}
/**
* Returns the quotient and remainder of the division of the numerator by the denominator.
*
* @return BigInteger[]
*/
public function quotientAndRemainder() : array
{
return $this->numerator->quotientAndRemainder($this->denominator);
}
/**
* Returns the sum of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to add.
*
* @throws MathException If the number is not valid.
*/
public function plus(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the difference of this number and the given one.
*
* @param BigNumber|int|float|string $that The number to subtract.
*
* @throws MathException If the number is not valid.
*/
public function minus(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the product of this number and the given one.
*
* @param BigNumber|int|float|string $that The multiplier.
*
* @throws MathException If the multiplier is not a valid number.
*/
public function multipliedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->numerator);
$denominator = $this->denominator->multipliedBy($that->denominator);
return new BigRational($numerator, $denominator, false);
}
/**
* Returns the result of the division of this number by the given one.
*
* @param BigNumber|int|float|string $that The divisor.
*
* @throws MathException If the divisor is not a valid number, or is zero.
*/
public function dividedBy(BigNumber|int|float|string $that) : BigRational
{
$that = BigRational::of($that);
$numerator = $this->numerator->multipliedBy($that->denominator);
$denominator = $this->denominator->multipliedBy($that->numerator);
return new BigRational($numerator, $denominator, true);
}
/**
* Returns this number exponentiated to the given value.
*
* @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
*/
public function power(int $exponent) : BigRational
{
if ($exponent === 0) {
$one = BigInteger::one();
return new BigRational($one, $one, false);
}
if ($exponent === 1) {
return $this;
}
return new BigRational(
$this->numerator->power($exponent),
$this->denominator->power($exponent),
false
);
}
/**
* Returns the reciprocal of this BigRational.
*
* The reciprocal has the numerator and denominator swapped.
*
* @throws DivisionByZeroException If the numerator is zero.
*/
public function reciprocal() : BigRational
{
return new BigRational($this->denominator, $this->numerator, true);
}
/**
* Returns the absolute value of this BigRational.
*/
public function abs() : BigRational
{
return new BigRational($this->numerator->abs(), $this->denominator, false);
}
/**
* Returns the negated value of this BigRational.
*/
public function negated() : BigRational
{
return new BigRational($this->numerator->negated(), $this->denominator, false);
}
/**
* Returns the simplified value of this BigRational.
*/
public function simplified() : BigRational
{
$gcd = $this->numerator->gcd($this->denominator);
$numerator = $this->numerator->quotient($gcd);
$denominator = $this->denominator->quotient($gcd);
return new BigRational($numerator, $denominator, false);
}
public function compareTo(BigNumber|int|float|string $that) : int
{
return $this->minus($that)->getSign();
}
public function getSign() : int
{
return $this->numerator->getSign();
}
public function toBigInteger() : BigInteger
{
$simplified = $this->simplified();
if (! $simplified->denominator->isEqualTo(1)) {
throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
}
return $simplified->numerator;
}
public function toBigDecimal() : BigDecimal
{
return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
}
public function toBigRational() : BigRational
{
return $this;
}
public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
{
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
}
public function toInt() : int
{
return $this->toBigInteger()->toInt();
}
public function toFloat() : float
{
$simplified = $this->simplified();
return $simplified->numerator->toFloat() / $simplified->denominator->toFloat();
}
public function __toString() : string
{
$numerator = (string) $this->numerator;
$denominator = (string) $this->denominator;
if ($denominator === '1') {
return $numerator;
}
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is required for serializing the object and SHOULD NOT be accessed directly.
*
* @internal
*
* @return array{numerator: BigInteger, denominator: BigInteger}
*/
public function __serialize(): array
{
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
}
/**
* This method is only here to allow unserializing the object and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @param array{numerator: BigInteger, denominator: BigInteger} $data
*
* @throws \LogicException
*/
public function __unserialize(array $data): void
{
if (isset($this->numerator)) {
throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
}
$this->numerator = $data['numerator'];
$this->denominator = $data['denominator'];
}
/**
* This method is required by interface Serializable and SHOULD NOT be accessed directly.
*
* @internal
*/
public function serialize() : string
{
return $this->numerator . '/' . $this->denominator;
}
/**
* This method is only here to implement interface Serializable and cannot be accessed directly.
*
* @internal
* @psalm-suppress RedundantPropertyInitializationCheck
*
* @throws \LogicException
*/
public function unserialize($value) : void
{
if (isset($this->numerator)) {
throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
}
[$numerator, $denominator] = \explode('/', $value);
$this->numerator = BigInteger::of($numerator);
$this->denominator = BigInteger::of($denominator);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Exception thrown when a division by zero occurs.
*/
class DivisionByZeroException extends MathException
{
/**
* @psalm-pure
*/
public static function divisionByZero() : DivisionByZeroException
{
return new self('Division by zero.');
}
/**
* @psalm-pure
*/
public static function modulusMustNotBeZero() : DivisionByZeroException
{
return new self('The modulus must not be zero.');
}
/**
* @psalm-pure
*/
public static function denominatorMustNotBeZero() : DivisionByZeroException
{
return new self('The denominator of a rational number cannot be zero.');
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
use Brick\Math\BigInteger;
/**
* Exception thrown when an integer overflow occurs.
*/
class IntegerOverflowException extends MathException
{
/**
* @psalm-pure
*/
public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
{
$message = '%s is out of range %d to %d and cannot be represented as an integer.';
return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Brick\Math\Exception;
/**
* Base class for all math exceptions.
*/
class MathException extends \Exception
{
}

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