commit message

This commit is contained in:
2025-10-09 12:14:29 +02:00
commit 684ab3a132
830 changed files with 161115 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
<?php
/**
* Class BitBuffer
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function count, floor, min;
/**
* Holds the raw binary data
*/
final class BitBuffer{
/**
* The buffer content
*
* @var int[]
*/
private array $buffer;
/**
* Length of the content (bits)
*/
private int $length;
/**
* Read count (bytes)
*/
private int $bytesRead = 0;
/**
* Read count (bits)
*/
private int $bitsRead = 0;
/**
* BitBuffer constructor.
*
* @param int[] $bytes
*/
public function __construct(array $bytes = []){
$this->buffer = $bytes;
$this->length = count($this->buffer);
}
/**
* appends a sequence of bits
*/
public function put(int $bits, int $length):self{
for($i = 0; $i < $length; $i++){
$this->putBit((($bits >> ($length - $i - 1)) & 1) === 1);
}
return $this;
}
/**
* appends a single bit
*/
public function putBit(bool $bit):self{
$bufIndex = (int)floor($this->length / 8);
if(count($this->buffer) <= $bufIndex){
$this->buffer[] = 0;
}
if($bit === true){
$this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8));
}
$this->length++;
return $this;
}
/**
* returns the current buffer length
*/
public function getLength():int{
return $this->length;
}
/**
* returns the buffer content
*
* to debug: array_map(fn($v) => sprintf('%08b', $v), $bitBuffer->getBuffer())
*/
public function getBuffer():array{
return $this->buffer;
}
/**
* @return int number of bits that can be read successfully
*/
public function available():int{
return ((8 * ($this->length - $this->bytesRead)) - $this->bitsRead);
}
/**
* @author Sean Owen, ZXing
*
* @param int $numBits number of bits to read
*
* @return int representing the bits read. The bits will appear as the least-significant bits of the int
* @throws \chillerlan\QRCode\QRCodeException if numBits isn't in [1,32] or more than is available
*/
public function read(int $numBits):int{
if($numBits < 1 || $numBits > $this->available()){
throw new QRCodeException('invalid $numBits: '.$numBits);
}
$result = 0;
// First, read remainder from current byte
if($this->bitsRead > 0){
$bitsLeft = (8 - $this->bitsRead);
$toRead = min($numBits, $bitsLeft);
$bitsToNotRead = ($bitsLeft - $toRead);
$mask = ((0xff >> (8 - $toRead)) << $bitsToNotRead);
$result = (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead);
$numBits -= $toRead;
$this->bitsRead += $toRead;
if($this->bitsRead === 8){
$this->bitsRead = 0;
$this->bytesRead++;
}
}
// Next read whole bytes
if($numBits > 0){
while($numBits >= 8){
$result = (($result << 8) | ($this->buffer[$this->bytesRead] & 0xff));
$this->bytesRead++;
$numBits -= 8;
}
// Finally read a partial byte
if($numBits > 0){
$bitsToNotRead = (8 - $numBits);
$mask = ((0xff >> $bitsToNotRead) << $bitsToNotRead);
$result = (($result << $numBits) | (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead));
$this->bitsRead += $numBits;
}
}
return $result;
}
/**
* Clears the buffer and resets the stats
*/
public function clear():self{
$this->buffer = [];
$this->length = 0;
return $this->rewind();
}
/**
* Resets the read-counters
*/
public function rewind():self{
$this->bytesRead = 0;
$this->bitsRead = 0;
return $this;
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Class ECICharset
*
* @created 21.01.2021
* @author ZXing Authors
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function sprintf;
/**
* ISO/IEC 18004:2000 - 8.4.1 Extended Channel Interpretation (ECI) Mode
*/
final class ECICharset{
public const CP437 = 0; // Code page 437, DOS Latin US
public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1
public const CP437_WO_GLI = 2; // An equivalent code table to CP437, without the return-to-GLI 0 logic
public const ISO_IEC_8859_1 = 3; // Latin-1 (Default)
public const ISO_IEC_8859_2 = 4; // Latin-2
public const ISO_IEC_8859_3 = 5; // Latin-3
public const ISO_IEC_8859_4 = 6; // Latin-4
public const ISO_IEC_8859_5 = 7; // Latin/Cyrillic
public const ISO_IEC_8859_6 = 8; // Latin/Arabic
public const ISO_IEC_8859_7 = 9; // Latin/Greek
public const ISO_IEC_8859_8 = 10; // Latin/Hebrew
public const ISO_IEC_8859_9 = 11; // Latin-5
public const ISO_IEC_8859_10 = 12; // Latin-6
public const ISO_IEC_8859_11 = 13; // Latin/Thai
// 14 reserved
public const ISO_IEC_8859_13 = 15; // Latin-7 (Baltic Rim)
public const ISO_IEC_8859_14 = 16; // Latin-8 (Celtic)
public const ISO_IEC_8859_15 = 17; // Latin-9
public const ISO_IEC_8859_16 = 18; // Latin-10
// 19 reserved
public const SHIFT_JIS = 20; // JIS X 0208 Annex 1 + JIS X 0201
public const WINDOWS_1250_LATIN_2 = 21; // Superset of Latin-2, Central Europe
public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic
public const WINDOWS_1252_LATIN_1 = 23; // Superset of Latin-1
public const WINDOWS_1256_ARABIC = 24;
public const ISO_IEC_10646_UCS_2 = 25; // High order byte first (UTF-16BE)
public const ISO_IEC_10646_UTF_8 = 26; // UTF-8
public const ISO_IEC_646_1991 = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII)
public const BIG5 = 28; // Big 5 (Taiwan) Chinese Character Set
public const GB18030 = 29; // GB (PRC) Chinese Character Set
public const EUC_KR = 30; // Korean Character Set
/**
* map of charset id -> name
*
* @see \mb_list_encodings()
*/
public const MB_ENCODINGS = [
self::CP437 => null,
self::ISO_IEC_8859_1_GLI => null,
self::CP437_WO_GLI => null,
self::ISO_IEC_8859_1 => 'ISO-8859-1',
self::ISO_IEC_8859_2 => 'ISO-8859-2',
self::ISO_IEC_8859_3 => 'ISO-8859-3',
self::ISO_IEC_8859_4 => 'ISO-8859-4',
self::ISO_IEC_8859_5 => 'ISO-8859-5',
self::ISO_IEC_8859_6 => 'ISO-8859-6',
self::ISO_IEC_8859_7 => 'ISO-8859-7',
self::ISO_IEC_8859_8 => 'ISO-8859-8',
self::ISO_IEC_8859_9 => 'ISO-8859-9',
self::ISO_IEC_8859_10 => 'ISO-8859-10',
self::ISO_IEC_8859_11 => null,
self::ISO_IEC_8859_13 => 'ISO-8859-13',
self::ISO_IEC_8859_14 => 'ISO-8859-14',
self::ISO_IEC_8859_15 => 'ISO-8859-15',
self::ISO_IEC_8859_16 => 'ISO-8859-16',
self::SHIFT_JIS => 'SJIS',
self::WINDOWS_1250_LATIN_2 => null, // @see https://www.php.net/manual/en/function.mb-convert-encoding.php#112547
self::WINDOWS_1251_CYRILLIC => 'Windows-1251',
self::WINDOWS_1252_LATIN_1 => 'Windows-1252',
self::WINDOWS_1256_ARABIC => null, // @see https://stackoverflow.com/a/8592995
self::ISO_IEC_10646_UCS_2 => 'UTF-16BE',
self::ISO_IEC_10646_UTF_8 => 'UTF-8',
self::ISO_IEC_646_1991 => 'ASCII',
self::BIG5 => 'BIG-5',
self::GB18030 => 'GB18030',
self::EUC_KR => 'EUC-KR',
];
/**
* The current ECI character set ID
*/
private int $charsetID;
/**
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $charsetID){
if($charsetID < 0 || $charsetID > 999999){
throw new QRCodeException(sprintf('invalid charset id: "%s"', $charsetID));
}
$this->charsetID = $charsetID;
}
/**
* Returns the current character set ID
*/
public function getID():int{
return $this->charsetID;
}
/**
* Returns the name of the current character set or null if no name is available
*
* @see \mb_convert_encoding()
* @see \iconv()
*/
public function getName():?string{
return (self::MB_ENCODINGS[$this->charsetID] ?? null);
}
}

View File

@@ -0,0 +1,223 @@
<?php
/**
* Class EccLevel
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function array_column;
/**
* This class encapsulates the four error correction levels defined by the QR code standard.
*/
final class EccLevel{
// ISO/IEC 18004:2000 Tables 12, 25
/** @var int */
public const L = 0b01; // 7%.
/** @var int */
public const M = 0b00; // 15%.
/** @var int */
public const Q = 0b11; // 25%.
/** @var int */
public const H = 0b10; // 30%.
/**
* ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40
*
* @var int[][]
*/
private const MAX_BITS = [
// [ L, M, Q, H] // v => modules
[ 0, 0, 0, 0], // 0 => will be ignored, index starts at 1
[ 152, 128, 104, 72], // 1 => 21
[ 272, 224, 176, 128], // 2 => 25
[ 440, 352, 272, 208], // 3 => 29
[ 640, 512, 384, 288], // 4 => 33
[ 864, 688, 496, 368], // 5 => 37
[ 1088, 864, 608, 480], // 6 => 41
[ 1248, 992, 704, 528], // 7 => 45
[ 1552, 1232, 880, 688], // 8 => 49
[ 1856, 1456, 1056, 800], // 9 => 53
[ 2192, 1728, 1232, 976], // 10 => 57
[ 2592, 2032, 1440, 1120], // 11 => 61
[ 2960, 2320, 1648, 1264], // 12 => 65
[ 3424, 2672, 1952, 1440], // 13 => 69 NICE!
[ 3688, 2920, 2088, 1576], // 14 => 73
[ 4184, 3320, 2360, 1784], // 15 => 77
[ 4712, 3624, 2600, 2024], // 16 => 81
[ 5176, 4056, 2936, 2264], // 17 => 85
[ 5768, 4504, 3176, 2504], // 18 => 89
[ 6360, 5016, 3560, 2728], // 19 => 93
[ 6888, 5352, 3880, 3080], // 20 => 97
[ 7456, 5712, 4096, 3248], // 21 => 101
[ 8048, 6256, 4544, 3536], // 22 => 105
[ 8752, 6880, 4912, 3712], // 23 => 109
[ 9392, 7312, 5312, 4112], // 24 => 113
[10208, 8000, 5744, 4304], // 25 => 117
[10960, 8496, 6032, 4768], // 26 => 121
[11744, 9024, 6464, 5024], // 27 => 125
[12248, 9544, 6968, 5288], // 28 => 129
[13048, 10136, 7288, 5608], // 29 => 133
[13880, 10984, 7880, 5960], // 30 => 137
[14744, 11640, 8264, 6344], // 31 => 141
[15640, 12328, 8920, 6760], // 32 => 145
[16568, 13048, 9368, 7208], // 33 => 149
[17528, 13800, 9848, 7688], // 34 => 153
[18448, 14496, 10288, 7888], // 35 => 157
[19472, 15312, 10832, 8432], // 36 => 161
[20528, 15936, 11408, 8768], // 37 => 165
[21616, 16816, 12016, 9136], // 38 => 169
[22496, 17728, 12656, 9776], // 39 => 173
[23648, 18672, 13328, 10208], // 40 => 177
];
/**
* ISO/IEC 18004:2000 Section 8.9 - Format Information
*
* ECC level -> mask pattern
*
* @var int[][]
*/
private const FORMAT_PATTERN = [
[ // L
0b111011111000100,
0b111001011110011,
0b111110110101010,
0b111100010011101,
0b110011000101111,
0b110001100011000,
0b110110001000001,
0b110100101110110,
],
[ // M
0b101010000010010,
0b101000100100101,
0b101111001111100,
0b101101101001011,
0b100010111111001,
0b100000011001110,
0b100111110010111,
0b100101010100000,
],
[ // Q
0b011010101011111,
0b011000001101000,
0b011111100110001,
0b011101000000110,
0b010010010110100,
0b010000110000011,
0b010111011011010,
0b010101111101101,
],
[ // H
0b001011010001001,
0b001001110111110,
0b001110011100111,
0b001100111010000,
0b000011101100010,
0b000001001010101,
0b000110100001100,
0b000100000111011,
],
];
/**
* The current ECC level value
*
* L: 0b01
* M: 0b00
* Q: 0b11
* H: 0b10
*/
private int $eccLevel;
/**
* @param int $eccLevel containing the two bits encoding a QR Code's error correction level
*
* @todo: accept string values (PHP8+)
* @see https://github.com/chillerlan/php-qrcode/discussions/160
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $eccLevel){
if((0b11 & $eccLevel) !== $eccLevel){
throw new QRCodeException('invalid ECC level');
}
$this->eccLevel = $eccLevel;
}
/**
* returns the string representation of the current ECC level
*/
public function __toString():string{
return [
self::L => 'L',
self::M => 'M',
self::Q => 'Q',
self::H => 'H',
][$this->eccLevel];
}
/**
* returns the current ECC level
*/
public function getLevel():int{
return $this->eccLevel;
}
/**
* returns the ordinal value of the current ECC level
*
* references to the keys of the following tables:
*
* @see \chillerlan\QRCode\Common\EccLevel::MAX_BITS
* @see \chillerlan\QRCode\Common\EccLevel::FORMAT_PATTERN
* @see \chillerlan\QRCode\Common\Version::RSBLOCKS
*/
public function getOrdinal():int{
return [
self::L => 0,
self::M => 1,
self::Q => 2,
self::H => 3,
][$this->eccLevel];
}
/**
* returns the format pattern for the given $eccLevel and $maskPattern
*/
public function getformatPattern(MaskPattern $maskPattern):int{
return self::FORMAT_PATTERN[$this->getOrdinal()][$maskPattern->getPattern()];
}
/**
* returns an array with the max bit lengths for version 1-40 and the current ECC level
*
* @return int[]
*/
public function getMaxBits():array{
$col = array_column(self::MAX_BITS, $this->getOrdinal());
unset($col[0]); // remove the inavlid index 0
return $col;
}
/**
* Returns the maximum bit length for the given version and current ECC level
*/
public function getMaxBitsForVersion(Version $version):int{
return self::MAX_BITS[$version->getVersionNumber()][$this->getOrdinal()];
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Class GDLuminanceSource
*
* @created 17.01.2021
* @author Ashot Khanamiryan
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\Decoder\QRCodeDecoderException;
use chillerlan\Settings\SettingsContainerInterface;
use function file_get_contents, get_resource_type, imagecolorat, imagecolorsforindex,
imagecreatefromstring, imagefilter, imagesx, imagesy, is_resource;
use const IMG_FILTER_BRIGHTNESS, IMG_FILTER_CONTRAST, IMG_FILTER_GRAYSCALE, IMG_FILTER_NEGATE, PHP_MAJOR_VERSION;
/**
* This class is used to help decode images from files which arrive as GD Resource
* It does not support rotation.
*/
class GDLuminanceSource extends LuminanceSourceAbstract{
/**
* @var resource|\GdImage
*/
protected $gdImage;
/**
* GDLuminanceSource constructor.
*
* @param resource|\GdImage $gdImage
* @param \chillerlan\Settings\SettingsContainerInterface|null $options
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function __construct($gdImage, SettingsContainerInterface $options = null){
/** @noinspection PhpFullyQualifiedNameUsageInspection */
if(
(PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage) // @todo: remove version check in v6
|| (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd'))
){
throw new QRCodeDecoderException('Invalid GD image source.'); // @codeCoverageIgnore
}
parent::__construct(imagesx($gdImage), imagesy($gdImage), $options);
$this->gdImage = $gdImage;
if($this->options->readerGrayscale){
imagefilter($this->gdImage, IMG_FILTER_GRAYSCALE);
}
if($this->options->readerInvertColors){
imagefilter($this->gdImage, IMG_FILTER_NEGATE);
}
if($this->options->readerIncreaseContrast){
imagefilter($this->gdImage, IMG_FILTER_BRIGHTNESS, -100);
imagefilter($this->gdImage, IMG_FILTER_CONTRAST, -100);
}
$this->setLuminancePixels();
}
/**
*
*/
protected function setLuminancePixels():void{
for($j = 0; $j < $this->height; $j++){
for($i = 0; $i < $this->width; $i++){
$argb = imagecolorat($this->gdImage, $i, $j);
$pixel = imagecolorsforindex($this->gdImage, $argb);
$this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
}
}
}
/** @inheritDoc */
public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
return new self(imagecreatefromstring(file_get_contents(self::checkFile($path))), $options);
}
/** @inheritDoc */
public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
return new self(imagecreatefromstring($blob), $options);
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Class GF256
*
* @created 16.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function array_fill;
/**
* This class contains utility methods for performing mathematical operations over
* the Galois Fields. Operations use a given primitive polynomial in calculations.
*
* Throughout this package, elements of the GF are represented as an int
* for convenience and speed (but at the cost of memory).
*
*
* @author Sean Owen
* @author David Olivier
*/
final class GF256{
/**
* irreducible polynomial whose coefficients are represented by the bits of an int,
* where the least-significant bit represents the constant coefficient
*/
# private int $primitive = 0x011D;
private const logTable = [
0, // the first value is never returned, index starts at 1
0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75,
4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113,
5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69,
29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166,
6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136,
54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64,
30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61,
202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87,
7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24,
227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46,
55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97,
242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162,
31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246,
108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90,
203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215,
79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175,
];
private const expTable = [
1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38,
76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192,
157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35,
70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161,
95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240,
253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226,
217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206,
129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204,
133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84,
168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115,
230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255,
227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65,
130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166,
81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9,
18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22,
44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1,
];
/**
* Implements both addition and subtraction -- they are the same in GF(size).
*
* @return int sum/difference of a and b
*/
public static function addOrSubtract(int $a, int $b):int{
return ($a ^ $b);
}
/**
* @return GenericGFPoly the monomial representing coefficient * x^degree
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function buildMonomial(int $degree, int $coefficient):GenericGFPoly{
if($degree < 0){
throw new QRCodeException('degree < 0');
}
$coefficients = array_fill(0, ($degree + 1), 0);
$coefficients[0] = $coefficient;
return new GenericGFPoly($coefficients);
}
/**
* @return int 2 to the power of $a in GF(size)
*/
public static function exp(int $a):int{
if($a < 0){
$a += 255;
}
elseif($a >= 256){
$a -= 255;
}
return self::expTable[$a];
}
/**
* @return int base 2 log of $a in GF(size)
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function log(int $a):int{
if($a < 1){
throw new QRCodeException('$a < 1');
}
return self::logTable[$a];
}
/**
* @return int multiplicative inverse of a
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function inverse(int $a):int{
if($a === 0){
throw new QRCodeException('$a === 0');
}
return self::expTable[(256 - self::logTable[$a] - 1)];
}
/**
* @return int product of a and b in GF(size)
*/
public static function multiply(int $a, int $b):int{
if($a === 0 || $b === 0){
return 0;
}
return self::expTable[((self::logTable[$a] + self::logTable[$b]) % 255)];
}
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* Class GenericGFPoly
*
* @created 16.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use function array_fill, array_slice, array_splice, count;
/**
* Represents a polynomial whose coefficients are elements of a GF.
* Instances of this class are immutable.
*
* Much credit is due to William Rucklidge since portions of this code are an indirect
* port of his C++ Reed-Solomon implementation.
*
* @author Sean Owen
*/
final class GenericGFPoly{
private array $coefficients;
/**
* @param array $coefficients array coefficients as ints representing elements of GF(size), arranged
* from most significant (highest-power term) coefficient to the least significant
* @param int|null $degree
*
* @throws \chillerlan\QRCode\QRCodeException if argument is null or empty, or if leading coefficient is 0 and this
* is not a constant polynomial (that is, it is not the monomial "0")
*/
public function __construct(array $coefficients, int $degree = null){
$degree ??= 0;
if(empty($coefficients)){
throw new QRCodeException('arg $coefficients is empty');
}
if($degree < 0){
throw new QRCodeException('negative degree');
}
$coefficientsLength = count($coefficients);
// Leading term must be non-zero for anything except the constant polynomial "0"
$firstNonZero = 0;
while($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] === 0){
$firstNonZero++;
}
$this->coefficients = [0];
if($firstNonZero !== $coefficientsLength){
$this->coefficients = array_fill(0, ($coefficientsLength - $firstNonZero + $degree), 0);
for($i = 0; $i < ($coefficientsLength - $firstNonZero); $i++){
$this->coefficients[$i] = $coefficients[($i + $firstNonZero)];
}
}
}
/**
* @return int $coefficient of x^degree term in this polynomial
*/
public function getCoefficient(int $degree):int{
return $this->coefficients[(count($this->coefficients) - 1 - $degree)];
}
/**
* @return int[]
*/
public function getCoefficients():array{
return $this->coefficients;
}
/**
* @return int $degree of this polynomial
*/
public function getDegree():int{
return (count($this->coefficients) - 1);
}
/**
* @return bool true if this polynomial is the monomial "0"
*/
public function isZero():bool{
return $this->coefficients[0] === 0;
}
/**
* @return int evaluation of this polynomial at a given point
*/
public function evaluateAt(int $a):int{
if($a === 0){
// Just return the x^0 coefficient
return $this->getCoefficient(0);
}
$result = 0;
foreach($this->coefficients as $c){
// if $a === 1 just the sum of the coefficients
$result = GF256::addOrSubtract((($a === 1) ? $result : GF256::multiply($a, $result)), $c);
}
return $result;
}
/**
*
*/
public function multiply(GenericGFPoly $other):self{
if($this->isZero() || $other->isZero()){
return new self([0]);
}
$product = array_fill(0, (count($this->coefficients) + count($other->coefficients) - 1), 0);
foreach($this->coefficients as $i => $aCoeff){
foreach($other->coefficients as $j => $bCoeff){
$product[($i + $j)] ^= GF256::multiply($aCoeff, $bCoeff);
}
}
return new self($product);
}
/**
* @return \chillerlan\QRCode\Common\GenericGFPoly[] [quotient, remainder]
* @throws \chillerlan\QRCode\QRCodeException
*/
public function divide(GenericGFPoly $other):array{
if($other->isZero()){
throw new QRCodeException('Division by 0');
}
$quotient = new self([0]);
$remainder = clone $this;
$denominatorLeadingTerm = $other->getCoefficient($other->getDegree());
$inverseDenominatorLeadingTerm = GF256::inverse($denominatorLeadingTerm);
while($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()){
$scale = GF256::multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm);
$diff = ($remainder->getDegree() - $other->getDegree());
$quotient = $quotient->addOrSubtract(GF256::buildMonomial($diff, $scale));
$remainder = $remainder->addOrSubtract($other->multiplyByMonomial($diff, $scale));
}
return [$quotient, $remainder];
}
/**
*
*/
public function multiplyInt(int $scalar):self{
if($scalar === 0){
return new self([0]);
}
if($scalar === 1){
return $this;
}
$product = array_fill(0, count($this->coefficients), 0);
foreach($this->coefficients as $i => $c){
$product[$i] = GF256::multiply($c, $scalar);
}
return new self($product);
}
/**
* @throws \chillerlan\QRCode\QRCodeException
*/
public function multiplyByMonomial(int $degree, int $coefficient):self{
if($degree < 0){
throw new QRCodeException('degree < 0');
}
if($coefficient === 0){
return new self([0]);
}
$product = array_fill(0, (count($this->coefficients) + $degree), 0);
foreach($this->coefficients as $i => $c){
$product[$i] = GF256::multiply($c, $coefficient);
}
return new self($product);
}
/**
*
*/
public function mod(GenericGFPoly $other):self{
if((count($this->coefficients) - count($other->coefficients)) < 0){
return $this;
}
$ratio = (GF256::log($this->coefficients[0]) - GF256::log($other->coefficients[0]));
foreach($other->coefficients as $i => $c){
$this->coefficients[$i] ^= GF256::exp(GF256::log($c) + $ratio);
}
return (new self($this->coefficients))->mod($other);
}
/**
*
*/
public function addOrSubtract(GenericGFPoly $other):self{
if($this->isZero()){
return $other;
}
if($other->isZero()){
return $this;
}
$smallerCoefficients = $this->coefficients;
$largerCoefficients = $other->coefficients;
if(count($smallerCoefficients) > count($largerCoefficients)){
$temp = $smallerCoefficients;
$smallerCoefficients = $largerCoefficients;
$largerCoefficients = $temp;
}
$sumDiff = array_fill(0, count($largerCoefficients), 0);
$lengthDiff = (count($largerCoefficients) - count($smallerCoefficients));
// Copy high-order terms only found in higher-degree polynomial's coefficients
array_splice($sumDiff, 0, $lengthDiff, array_slice($largerCoefficients, 0, $lengthDiff));
$countLargerCoefficients = count($largerCoefficients);
for($i = $lengthDiff; $i < $countLargerCoefficients; $i++){
$sumDiff[$i] = GF256::addOrSubtract($smallerCoefficients[($i - $lengthDiff)], $largerCoefficients[$i]);
}
return new self($sumDiff);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* Class IMagickLuminanceSource
*
* @created 17.01.2021
* @author Ashot Khanamiryan
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Common;
use chillerlan\Settings\SettingsContainerInterface;
use Imagick;
use function count;
/**
* This class is used to help decode images from files which arrive as Imagick Resource
* It does not support rotation.
*/
class IMagickLuminanceSource extends LuminanceSourceAbstract{
protected Imagick $imagick;
/**
* IMagickLuminanceSource constructor.
*/
public function __construct(Imagick $imagick, SettingsContainerInterface $options = null){
parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight(), $options);
$this->imagick = $imagick;
if($this->options->readerGrayscale){
$this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
}
if($this->options->readerInvertColors){
$this->imagick->negateImage($this->options->readerGrayscale);
}
if($this->options->readerIncreaseContrast){
for($i = 0; $i < 10; $i++){
$this->imagick->contrastImage(false); // misleading docs
}
}
$this->setLuminancePixels();
}
/**
*
*/
protected function setLuminancePixels():void{
$pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
$count = count($pixels);
for($i = 0; $i < $count; $i += 3){
$this->setLuminancePixel(($pixels[$i] & 0xff), ($pixels[($i + 1)] & 0xff), ($pixels[($i + 2)] & 0xff));
}
}
/** @inheritDoc */
public static function fromFile(string $path, SettingsContainerInterface $options = null):self{
return new self(new Imagick(self::checkFile($path)), $options);
}
/** @inheritDoc */
public static function fromBlob(string $blob, SettingsContainerInterface $options = null):self{
$im = new Imagick;
$im->readImageBlob($blob);
return new self($im, $options);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* Class LuminanceSourceAbstract
*
* @created 24.01.2021
* @author ZXing Authors
* @author Ashot Khanamiryan
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\Decoder\QRCodeDecoderException;
use chillerlan\QRCode\QROptions;
use chillerlan\Settings\SettingsContainerInterface;
use function array_slice, array_splice, file_exists, is_file, is_readable, realpath;
/**
* The purpose of this class hierarchy is to abstract different bitmap implementations across
* platforms into a standard interface for requesting greyscale luminance values.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{
/** @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface */
protected SettingsContainerInterface $options;
protected array $luminances;
protected int $width;
protected int $height;
/**
*
*/
public function __construct(int $width, int $height, SettingsContainerInterface $options = null){
$this->width = $width;
$this->height = $height;
$this->options = ($options ?? new QROptions);
$this->luminances = [];
}
/** @inheritDoc */
public function getLuminances():array{
return $this->luminances;
}
/** @inheritDoc */
public function getWidth():int{
return $this->width;
}
/** @inheritDoc */
public function getHeight():int{
return $this->height;
}
/** @inheritDoc */
public function getRow(int $y):array{
if($y < 0 || $y >= $this->getHeight()){
throw new QRCodeDecoderException('Requested row is outside the image: '.$y);
}
$arr = [];
array_splice($arr, 0, $this->width, array_slice($this->luminances, ($y * $this->width), $this->width));
return $arr;
}
/**
*
*/
protected function setLuminancePixel(int $r, int $g, int $b):void{
$this->luminances[] = ($r === $g && $g === $b)
// Image is already greyscale, so pick any channel.
? $r // (($r + 128) % 256) - 128;
// Calculate luminance cheaply, favoring green.
: (($r + 2 * $g + $b) / 4); // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128;
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
protected static function checkFile(string $path):string{
$path = trim($path);
if(!file_exists($path) || !is_file($path) || !is_readable($path)){
throw new QRCodeDecoderException('invalid file: '.$path);
}
$realpath = realpath($path);
if($realpath === false){
throw new QRCodeDecoderException('unable to resolve path: '.$path);
}
return $realpath;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Interface LuminanceSourceInterface
*
* @created 18.11.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
/**
*/
interface LuminanceSourceInterface{
/**
* Fetches luminance data for the underlying bitmap. Values should be fetched using:
* `int luminance = array[y * width + x] & 0xff`
*
* @return array A row-major 2D array of luminance values. Do not use result $length as it may be
* larger than $width * $height bytes on some platforms. Do not modify the contents
* of the result.
*/
public function getLuminances():array;
/**
* @return int The width of the bitmap.
*/
public function getWidth():int;
/**
* @return int The height of the bitmap.
*/
public function getHeight():int;
/**
* Fetches one row of luminance data from the underlying platform's bitmap. Values range from
* 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have
* to bitwise and with 0xff for each value. It is preferable for implementations of this method
* to only fetch this row rather than the whole image, since no 2D Readers may be installed and
* getLuminances() may never be called.
*
* @param int $y The row to fetch, which must be in [0,getHeight())
*
* @return array An array containing the luminance data.
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function getRow(int $y):array;
/**
* Creates a LuminanceSource instance from the given file
*/
public static function fromFile(string $path):self;
/**
* Creates a LuminanceSource instance from the given data blob
*/
public static function fromBlob(string $blob):self;
}

View File

@@ -0,0 +1,329 @@
<?php
/**
* Class MaskPattern
*
* @created 19.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
use chillerlan\QRCode\Data\QRMatrix;
use Closure;
use function abs, array_column, array_search, intdiv, min;
/**
* ISO/IEC 18004:2000 Section 8.8.1
* ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
*
* @see http://www.thonky.com/qr-code-tutorial/data-masking
* @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java
*/
final class MaskPattern{
/**
* @see \chillerlan\QRCode\QROptionsTrait::$maskPattern
*
* @var int
*/
public const AUTO = -1;
public const PATTERN_000 = 0b000;
public const PATTERN_001 = 0b001;
public const PATTERN_010 = 0b010;
public const PATTERN_011 = 0b011;
public const PATTERN_100 = 0b100;
public const PATTERN_101 = 0b101;
public const PATTERN_110 = 0b110;
public const PATTERN_111 = 0b111;
/**
* @var int[]
*/
public const PATTERNS = [
self::PATTERN_000,
self::PATTERN_001,
self::PATTERN_010,
self::PATTERN_011,
self::PATTERN_100,
self::PATTERN_101,
self::PATTERN_110,
self::PATTERN_111,
];
/*
* Penalty scores
*
* ISO/IEC 18004:2000 Section 8.8.1 - Table 24
*/
private const PENALTY_N1 = 3;
private const PENALTY_N2 = 3;
private const PENALTY_N3 = 40;
private const PENALTY_N4 = 10;
/**
* The current mask pattern value (0-7)
*/
private int $maskPattern;
/**
* MaskPattern constructor.
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $maskPattern){
if((0b111 & $maskPattern) !== $maskPattern){
throw new QRCodeException('invalid mask pattern');
}
$this->maskPattern = $maskPattern;
}
/**
* Returns the current mask pattern
*/
public function getPattern():int{
return $this->maskPattern;
}
/**
* Returns a closure that applies the mask for the chosen mask pattern.
*
* Note that the diagram in section 6.8.1 is misleading since it indicates that $i is column position
* and $j is row position. In fact, as the text says, $i is row position and $j is column position.
*
* @see https://www.thonky.com/qr-code-tutorial/mask-patterns
* @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117
*/
public function getMask():Closure{
// $x = column (width), $y = row (height)
return [
self::PATTERN_000 => fn(int $x, int $y):bool => (($x + $y) % 2) === 0,
self::PATTERN_001 => fn(int $x, int $y):bool => ($y % 2) === 0,
self::PATTERN_010 => fn(int $x, int $y):bool => ($x % 3) === 0,
self::PATTERN_011 => fn(int $x, int $y):bool => (($x + $y) % 3) === 0,
self::PATTERN_100 => fn(int $x, int $y):bool => ((intdiv($y, 2) + intdiv($x, 3)) % 2) === 0,
self::PATTERN_101 => fn(int $x, int $y):bool => (($x * $y) % 6) === 0,
self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3,
self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0,
][$this->maskPattern];
}
/**
* Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result
*/
public static function getBestPattern(QRMatrix $QRMatrix):self{
$penalties = [];
$size = $QRMatrix->getSize();
foreach(self::PATTERNS as $pattern){
$mp = new self($pattern);
$matrix = (clone $QRMatrix)->setFormatInfo($mp)->mask($mp)->getMatrix(true);
$penalty = 0;
for($level = 1; $level <= 4; $level++){
$penalty += self::{'testRule'.$level}($matrix, $size, $size);
}
$penalties[$pattern] = (int)$penalty;
}
return new self(array_search(min($penalties), $penalties, true));
}
/**
* Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and
* give penalty to them. Example: 00000 or 11111.
*/
public static function testRule1(array $matrix, int $height, int $width):int{
$penalty = 0;
// horizontal
foreach($matrix as $row){
$penalty += self::applyRule1($row);
}
// vertical
for($x = 0; $x < $width; $x++){
$penalty += self::applyRule1(array_column($matrix, $x));
}
return $penalty;
}
/**
*
*/
private static function applyRule1(array $rc):int{
$penalty = 0;
$numSameBitCells = 0;
$prevBit = null;
foreach($rc as $val){
if($val === $prevBit){
$numSameBitCells++;
}
else{
if($numSameBitCells >= 5){
$penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
}
$numSameBitCells = 1; // Include the cell itself.
$prevBit = $val;
}
}
if($numSameBitCells >= 5){
$penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
}
return $penalty;
}
/**
* Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give
* 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 testRule2(array $matrix, int $height, int $width):int{
$penalty = 0;
foreach($matrix as $y => $row){
if($y > ($height - 2)){
break;
}
foreach($row as $x => $val){
if($x > ($width - 2)){
break;
}
if(
$val === $row[($x + 1)]
&& $val === $matrix[($y + 1)][$x]
&& $val === $matrix[($y + 1)][($x + 1)]
){
$penalty++;
}
}
}
return (self::PENALTY_N2 * $penalty);
}
/**
* Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4
* starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we
* find patterns like 000010111010000, we give penalty once.
*/
public static function testRule3(array $matrix, int $height, int $width):int{
$penalties = 0;
foreach($matrix as $y => $row){
foreach($row as $x => $val){
if(
($x + 6) < $width
&& $val
&& !$row[($x + 1)]
&& $row[($x + 2)]
&& $row[($x + 3)]
&& $row[($x + 4)]
&& !$row[($x + 5)]
&& $row[($x + 6)]
&& (
self::isWhiteHorizontal($row, $width, ($x - 4), $x)
|| self::isWhiteHorizontal($row, $width, ($x + 7), ($x + 11))
)
){
$penalties++;
}
if(
($y + 6) < $height
&& $val
&& !$matrix[($y + 1)][$x]
&& $matrix[($y + 2)][$x]
&& $matrix[($y + 3)][$x]
&& $matrix[($y + 4)][$x]
&& !$matrix[($y + 5)][$x]
&& $matrix[($y + 6)][$x]
&& (
self::isWhiteVertical($matrix, $height, $x, ($y - 4), $y)
|| self::isWhiteVertical($matrix, $height, $x, ($y + 7), ($y + 11))
)
){
$penalties++;
}
}
}
return ($penalties * self::PENALTY_N3);
}
/**
*
*/
private static function isWhiteHorizontal(array $row, int $width, int $from, int $to):bool{
if($from < 0 || $width < $to){
return false;
}
for($x = $from; $x < $to; $x++){
if($row[$x]){
return false;
}
}
return true;
}
/**
*
*/
private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to):bool{
if($from < 0 || $height < $to){
return false;
}
for($y = $from; $y < $to; $y++){
if($matrix[$y][$x] === true){
return false;
}
}
return true;
}
/**
* Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give
* penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.
*/
public static function testRule4(array $matrix, int $height, int $width):int{
$darkCells = 0;
$totalCells = ($height * $width);
foreach($matrix as $row){
foreach($row as $val){
if($val === true){
$darkCells++;
}
}
}
return (intdiv((abs($darkCells * 2 - $totalCells) * 10), $totalCells) * self::PENALTY_N4);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* Class Mode
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\Data\{AlphaNum, Byte, Hanzi, Kanji, Number};
use chillerlan\QRCode\QRCodeException;
/**
* Data mode information - ISO 18004:2006, 6.4.1, Tables 2 and 3
*/
final class Mode{
// ISO/IEC 18004:2000 Table 2
/** @var int */
public const TERMINATOR = 0b0000;
/** @var int */
public const NUMBER = 0b0001;
/** @var int */
public const ALPHANUM = 0b0010;
/** @var int */
public const BYTE = 0b0100;
/** @var int */
public const KANJI = 0b1000;
/** @var int */
public const HANZI = 0b1101;
/** @var int */
public const STRCTURED_APPEND = 0b0011;
/** @var int */
public const FNC1_FIRST = 0b0101;
/** @var int */
public const FNC1_SECOND = 0b1001;
/** @var int */
public const ECI = 0b0111;
/**
* mode length bits for the version breakpoints 1-9, 10-26 and 27-40
*
* ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator
*/
public const LENGTH_BITS = [
self::NUMBER => [10, 12, 14],
self::ALPHANUM => [ 9, 11, 13],
self::BYTE => [ 8, 16, 16],
self::KANJI => [ 8, 10, 12],
self::HANZI => [ 8, 10, 12],
self::ECI => [ 0, 0, 0],
];
/**
* Map of data mode => interface (detection order)
*
* @var string[]
*/
public const INTERFACES = [
self::NUMBER => Number::class,
self::ALPHANUM => AlphaNum::class,
self::KANJI => Kanji::class,
self::HANZI => Hanzi::class,
self::BYTE => Byte::class,
];
/**
* returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public static function getLengthBitsForVersion(int $mode, int $version):int{
if(!isset(self::LENGTH_BITS[$mode])){
throw new QRCodeException('invalid mode given');
}
$minVersion = 0;
foreach([9, 26, 40] as $key => $breakpoint){
if($version > $minVersion && $version <= $breakpoint){
return self::LENGTH_BITS[$mode][$key];
}
$minVersion = $breakpoint;
}
throw new QRCodeException(sprintf('invalid version number: %d', $version));
}
}

View File

@@ -0,0 +1,287 @@
<?php
/**
* Class Version
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Common;
use chillerlan\QRCode\QRCodeException;
/**
* Version related tables and methods
*/
final class Version{
/**
* Enable version auto detection
*
* @see \chillerlan\QRCode\QROptionsTrait::$version
*
* @var int
*/
public const AUTO = -1;
/**
* ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns
*
* version -> pattern
*
* @var int[][]
*/
private const ALIGNMENT_PATTERN = [
1 => [],
2 => [6, 18],
3 => [6, 22],
4 => [6, 26],
5 => [6, 30],
6 => [6, 34],
7 => [6, 22, 38],
8 => [6, 24, 42],
9 => [6, 26, 46],
10 => [6, 28, 50],
11 => [6, 30, 54],
12 => [6, 32, 58],
13 => [6, 34, 62],
14 => [6, 26, 46, 66],
15 => [6, 26, 48, 70],
16 => [6, 26, 50, 74],
17 => [6, 30, 54, 78],
18 => [6, 30, 56, 82],
19 => [6, 30, 58, 86],
20 => [6, 34, 62, 90],
21 => [6, 28, 50, 72, 94],
22 => [6, 26, 50, 74, 98],
23 => [6, 30, 54, 78, 102],
24 => [6, 28, 54, 80, 106],
25 => [6, 32, 58, 84, 110],
26 => [6, 30, 58, 86, 114],
27 => [6, 34, 62, 90, 118],
28 => [6, 26, 50, 74, 98, 122],
29 => [6, 30, 54, 78, 102, 126],
30 => [6, 26, 52, 78, 104, 130],
31 => [6, 30, 56, 82, 108, 134],
32 => [6, 34, 60, 86, 112, 138],
33 => [6, 30, 58, 86, 114, 142],
34 => [6, 34, 62, 90, 118, 146],
35 => [6, 30, 54, 78, 102, 126, 150],
36 => [6, 24, 50, 76, 102, 128, 154],
37 => [6, 28, 54, 80, 106, 132, 158],
38 => [6, 32, 58, 84, 110, 136, 162],
39 => [6, 26, 54, 82, 110, 138, 166],
40 => [6, 30, 58, 86, 114, 142, 170],
];
/**
* ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version
*
* no version pattern for QR Codes < 7
*
* @var int[]
*/
private const VERSION_PATTERN = [
7 => 0b000111110010010100,
8 => 0b001000010110111100,
9 => 0b001001101010011001,
10 => 0b001010010011010011,
11 => 0b001011101111110110,
12 => 0b001100011101100010,
13 => 0b001101100001000111,
14 => 0b001110011000001101,
15 => 0b001111100100101000,
16 => 0b010000101101111000,
17 => 0b010001010001011101,
18 => 0b010010101000010111,
19 => 0b010011010100110010,
20 => 0b010100100110100110,
21 => 0b010101011010000011,
22 => 0b010110100011001001,
23 => 0b010111011111101100,
24 => 0b011000111011000100,
25 => 0b011001000111100001,
26 => 0b011010111110101011,
27 => 0b011011000010001110,
28 => 0b011100110000011010,
29 => 0b011101001100111111,
30 => 0b011110110101110101,
31 => 0b011111001001010000,
32 => 0b100000100111010101,
33 => 0b100001011011110000,
34 => 0b100010100010111010,
35 => 0b100011011110011111,
36 => 0b100100101100001011,
37 => 0b100101010000101110,
38 => 0b100110101001100100,
39 => 0b100111010101000001,
40 => 0b101000110001101001,
];
/**
* ISO/IEC 18004:2000 Tables 13-22 - Error correction characteristics
*
* @see http://www.thonky.com/qr-code-tutorial/error-correction-table
*/
private const RSBLOCKS = [
1 => [[ 7, [[ 1, 19], [ 0, 0]]], [10, [[ 1, 16], [ 0, 0]]], [13, [[ 1, 13], [ 0, 0]]], [17, [[ 1, 9], [ 0, 0]]]],
2 => [[10, [[ 1, 34], [ 0, 0]]], [16, [[ 1, 28], [ 0, 0]]], [22, [[ 1, 22], [ 0, 0]]], [28, [[ 1, 16], [ 0, 0]]]],
3 => [[15, [[ 1, 55], [ 0, 0]]], [26, [[ 1, 44], [ 0, 0]]], [18, [[ 2, 17], [ 0, 0]]], [22, [[ 2, 13], [ 0, 0]]]],
4 => [[20, [[ 1, 80], [ 0, 0]]], [18, [[ 2, 32], [ 0, 0]]], [26, [[ 2, 24], [ 0, 0]]], [16, [[ 4, 9], [ 0, 0]]]],
5 => [[26, [[ 1, 108], [ 0, 0]]], [24, [[ 2, 43], [ 0, 0]]], [18, [[ 2, 15], [ 2, 16]]], [22, [[ 2, 11], [ 2, 12]]]],
6 => [[18, [[ 2, 68], [ 0, 0]]], [16, [[ 4, 27], [ 0, 0]]], [24, [[ 4, 19], [ 0, 0]]], [28, [[ 4, 15], [ 0, 0]]]],
7 => [[20, [[ 2, 78], [ 0, 0]]], [18, [[ 4, 31], [ 0, 0]]], [18, [[ 2, 14], [ 4, 15]]], [26, [[ 4, 13], [ 1, 14]]]],
8 => [[24, [[ 2, 97], [ 0, 0]]], [22, [[ 2, 38], [ 2, 39]]], [22, [[ 4, 18], [ 2, 19]]], [26, [[ 4, 14], [ 2, 15]]]],
9 => [[30, [[ 2, 116], [ 0, 0]]], [22, [[ 3, 36], [ 2, 37]]], [20, [[ 4, 16], [ 4, 17]]], [24, [[ 4, 12], [ 4, 13]]]],
10 => [[18, [[ 2, 68], [ 2, 69]]], [26, [[ 4, 43], [ 1, 44]]], [24, [[ 6, 19], [ 2, 20]]], [28, [[ 6, 15], [ 2, 16]]]],
11 => [[20, [[ 4, 81], [ 0, 0]]], [30, [[ 1, 50], [ 4, 51]]], [28, [[ 4, 22], [ 4, 23]]], [24, [[ 3, 12], [ 8, 13]]]],
12 => [[24, [[ 2, 92], [ 2, 93]]], [22, [[ 6, 36], [ 2, 37]]], [26, [[ 4, 20], [ 6, 21]]], [28, [[ 7, 14], [ 4, 15]]]],
13 => [[26, [[ 4, 107], [ 0, 0]]], [22, [[ 8, 37], [ 1, 38]]], [24, [[ 8, 20], [ 4, 21]]], [22, [[12, 11], [ 4, 12]]]],
14 => [[30, [[ 3, 115], [ 1, 116]]], [24, [[ 4, 40], [ 5, 41]]], [20, [[11, 16], [ 5, 17]]], [24, [[11, 12], [ 5, 13]]]],
15 => [[22, [[ 5, 87], [ 1, 88]]], [24, [[ 5, 41], [ 5, 42]]], [30, [[ 5, 24], [ 7, 25]]], [24, [[11, 12], [ 7, 13]]]],
16 => [[24, [[ 5, 98], [ 1, 99]]], [28, [[ 7, 45], [ 3, 46]]], [24, [[15, 19], [ 2, 20]]], [30, [[ 3, 15], [13, 16]]]],
17 => [[28, [[ 1, 107], [ 5, 108]]], [28, [[10, 46], [ 1, 47]]], [28, [[ 1, 22], [15, 23]]], [28, [[ 2, 14], [17, 15]]]],
18 => [[30, [[ 5, 120], [ 1, 121]]], [26, [[ 9, 43], [ 4, 44]]], [28, [[17, 22], [ 1, 23]]], [28, [[ 2, 14], [19, 15]]]],
19 => [[28, [[ 3, 113], [ 4, 114]]], [26, [[ 3, 44], [11, 45]]], [26, [[17, 21], [ 4, 22]]], [26, [[ 9, 13], [16, 14]]]],
20 => [[28, [[ 3, 107], [ 5, 108]]], [26, [[ 3, 41], [13, 42]]], [30, [[15, 24], [ 5, 25]]], [28, [[15, 15], [10, 16]]]],
21 => [[28, [[ 4, 116], [ 4, 117]]], [26, [[17, 42], [ 0, 0]]], [28, [[17, 22], [ 6, 23]]], [30, [[19, 16], [ 6, 17]]]],
22 => [[28, [[ 2, 111], [ 7, 112]]], [28, [[17, 46], [ 0, 0]]], [30, [[ 7, 24], [16, 25]]], [24, [[34, 13], [ 0, 0]]]],
23 => [[30, [[ 4, 121], [ 5, 122]]], [28, [[ 4, 47], [14, 48]]], [30, [[11, 24], [14, 25]]], [30, [[16, 15], [14, 16]]]],
24 => [[30, [[ 6, 117], [ 4, 118]]], [28, [[ 6, 45], [14, 46]]], [30, [[11, 24], [16, 25]]], [30, [[30, 16], [ 2, 17]]]],
25 => [[26, [[ 8, 106], [ 4, 107]]], [28, [[ 8, 47], [13, 48]]], [30, [[ 7, 24], [22, 25]]], [30, [[22, 15], [13, 16]]]],
26 => [[28, [[10, 114], [ 2, 115]]], [28, [[19, 46], [ 4, 47]]], [28, [[28, 22], [ 6, 23]]], [30, [[33, 16], [ 4, 17]]]],
27 => [[30, [[ 8, 122], [ 4, 123]]], [28, [[22, 45], [ 3, 46]]], [30, [[ 8, 23], [26, 24]]], [30, [[12, 15], [28, 16]]]],
28 => [[30, [[ 3, 117], [10, 118]]], [28, [[ 3, 45], [23, 46]]], [30, [[ 4, 24], [31, 25]]], [30, [[11, 15], [31, 16]]]],
29 => [[30, [[ 7, 116], [ 7, 117]]], [28, [[21, 45], [ 7, 46]]], [30, [[ 1, 23], [37, 24]]], [30, [[19, 15], [26, 16]]]],
30 => [[30, [[ 5, 115], [10, 116]]], [28, [[19, 47], [10, 48]]], [30, [[15, 24], [25, 25]]], [30, [[23, 15], [25, 16]]]],
31 => [[30, [[13, 115], [ 3, 116]]], [28, [[ 2, 46], [29, 47]]], [30, [[42, 24], [ 1, 25]]], [30, [[23, 15], [28, 16]]]],
32 => [[30, [[17, 115], [ 0, 0]]], [28, [[10, 46], [23, 47]]], [30, [[10, 24], [35, 25]]], [30, [[19, 15], [35, 16]]]],
33 => [[30, [[17, 115], [ 1, 116]]], [28, [[14, 46], [21, 47]]], [30, [[29, 24], [19, 25]]], [30, [[11, 15], [46, 16]]]],
34 => [[30, [[13, 115], [ 6, 116]]], [28, [[14, 46], [23, 47]]], [30, [[44, 24], [ 7, 25]]], [30, [[59, 16], [ 1, 17]]]],
35 => [[30, [[12, 121], [ 7, 122]]], [28, [[12, 47], [26, 48]]], [30, [[39, 24], [14, 25]]], [30, [[22, 15], [41, 16]]]],
36 => [[30, [[ 6, 121], [14, 122]]], [28, [[ 6, 47], [34, 48]]], [30, [[46, 24], [10, 25]]], [30, [[ 2, 15], [64, 16]]]],
37 => [[30, [[17, 122], [ 4, 123]]], [28, [[29, 46], [14, 47]]], [30, [[49, 24], [10, 25]]], [30, [[24, 15], [46, 16]]]],
38 => [[30, [[ 4, 122], [18, 123]]], [28, [[13, 46], [32, 47]]], [30, [[48, 24], [14, 25]]], [30, [[42, 15], [32, 16]]]],
39 => [[30, [[20, 117], [ 4, 118]]], [28, [[40, 47], [ 7, 48]]], [30, [[43, 24], [22, 25]]], [30, [[10, 15], [67, 16]]]],
40 => [[30, [[19, 118], [ 6, 119]]], [28, [[18, 47], [31, 48]]], [30, [[34, 24], [34, 25]]], [30, [[20, 15], [61, 16]]]],
];
/**
* ISO/IEC 18004:2000 Table 1 - Data capacity of all versions of QR Code
*/
private const TOTAL_CODEWORDS = [
1 => 26,
2 => 44,
3 => 70,
4 => 100,
5 => 134,
6 => 172,
7 => 196,
8 => 242,
9 => 292,
10 => 346,
11 => 404,
12 => 466,
13 => 532,
14 => 581,
15 => 655,
16 => 733,
17 => 815,
18 => 901,
19 => 991,
20 => 1085,
21 => 1156,
22 => 1258,
23 => 1364,
24 => 1474,
25 => 1588,
26 => 1706,
27 => 1828,
28 => 1921,
29 => 2051,
30 => 2185,
31 => 2323,
32 => 2465,
33 => 2611,
34 => 2761,
35 => 2876,
36 => 3034,
37 => 3196,
38 => 3362,
39 => 3532,
40 => 3706,
];
/**
* QR Code version number
*/
private int $version;
/**
* Version constructor.
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function __construct(int $version){
if($version < 1 || $version > 40){
throw new QRCodeException('invalid version given');
}
$this->version = $version;
}
/**
* returns the current version number as string
*/
public function __toString():string{
return (string)$this->version;
}
/**
* returns the current version number
*/
public function getVersionNumber():int{
return $this->version;
}
/**
* the matrix size for the given version
*/
public function getDimension():int{
return (($this->version * 4) + 17);
}
/**
* the version pattern for the given version
*/
public function getVersionPattern():?int{
return (self::VERSION_PATTERN[$this->version] ?? null);
}
/**
* the alignment patterns for the current version
*
* @return int[]
*/
public function getAlignmentPattern():array{
return self::ALIGNMENT_PATTERN[$this->version];
}
/**
* returns ECC block information for the given $version and $eccLevel
*/
public function getRSBlocks(EccLevel $eccLevel):array{
return self::RSBLOCKS[$this->version][$eccLevel->getOrdinal()];
}
/**
* returns the maximum codewords for the current version
*/
public function getTotalCodewords():int{
return self::TOTAL_CODEWORDS[$this->version];
}
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Class AlphaNum
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use function array_flip, ceil, intdiv, str_split;
/**
* Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / :
*
* ISO/IEC 18004:2000 Section 8.3.3
* ISO/IEC 18004:2000 Section 8.4.3
*/
final class AlphaNum extends QRDataModeAbstract{
/**
* ISO/IEC 18004:2000 Table 5
*
* @var int[]
*/
private const CHAR_TO_ORD = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7,
'8' => 8, '9' => 9, 'A' => 10, 'B' => 11, 'C' => 12, 'D' => 13, 'E' => 14, 'F' => 15,
'G' => 16, 'H' => 17, 'I' => 18, 'J' => 19, 'K' => 20, 'L' => 21, 'M' => 22, 'N' => 23,
'O' => 24, 'P' => 25, 'Q' => 26, 'R' => 27, 'S' => 28, 'T' => 29, 'U' => 30, 'V' => 31,
'W' => 32, 'X' => 33, 'Y' => 34, 'Z' => 35, ' ' => 36, '$' => 37, '%' => 38, '*' => 39,
'+' => 40, '-' => 41, '.' => 42, '/' => 43, ':' => 44,
];
/**
* @inheritDoc
*/
public const DATAMODE = Mode::ALPHANUM;
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return (int)ceil($this->getCharCount() * (11 / 2));
}
/**
* @inheritDoc
*/
public static function validateString(string $string):bool{
if($string === ''){
return false;
}
foreach(str_split($string) as $chr){
if(!isset(self::CHAR_TO_ORD[$chr])){
return false;
}
}
return true;
}
/**
* @inheritDoc
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$len = $this->getCharCount();
$bitBuffer
->put(self::DATAMODE, 4)
->put($len, $this::getLengthBits($versionNumber))
;
// encode 2 characters in 11 bits
for($i = 0; ($i + 1) < $len; $i += 2){
$bitBuffer->put((self::CHAR_TO_ORD[$this->data[$i]] * 45 + self::CHAR_TO_ORD[$this->data[($i + 1)]]), 11);
}
// encode a remaining character in 6 bits
if($i < $len){
$bitBuffer->put(self::CHAR_TO_ORD[$this->data[$i]], 6);
}
return $this;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
$charmap = array_flip(self::CHAR_TO_ORD);
// @todo
$toAlphaNumericChar = function(int $ord) use ($charmap):string{
if(isset($charmap[$ord])){
return $charmap[$ord];
}
throw new QRCodeDataException('invalid character value: '.$ord);
};
$result = '';
// Read two characters at a time
while($length > 1){
if($bitBuffer->available() < 11){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$nextTwoCharsBits = $bitBuffer->read(11);
$result .= $toAlphaNumericChar(intdiv($nextTwoCharsBits, 45));
$result .= $toAlphaNumericChar($nextTwoCharsBits % 45);
$length -= 2;
}
if($length === 1){
// special case: one character left
if($bitBuffer->available() < 6){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$result .= $toAlphaNumericChar($bitBuffer->read(6));
}
return $result;
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Class Byte
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use function chr, ord;
/**
* 8-bit Byte mode, ISO-8859-1 or UTF-8
*
* ISO/IEC 18004:2000 Section 8.3.4
* ISO/IEC 18004:2000 Section 8.4.4
*/
final class Byte extends QRDataModeAbstract{
/**
* @inheritDoc
*/
public const DATAMODE = Mode::BYTE;
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return ($this->getCharCount() * 8);
}
/**
* @inheritDoc
*/
public static function validateString(string $string):bool{
return $string !== '';
}
/**
* @inheritDoc
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$len = $this->getCharCount();
$bitBuffer
->put(self::DATAMODE, 4)
->put($len, $this::getLengthBits($versionNumber))
;
$i = 0;
while($i < $len){
$bitBuffer->put(ord($this->data[$i]), 8);
$i++;
}
return $this;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
if($bitBuffer->available() < (8 * $length)){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$readBytes = '';
for($i = 0; $i < $length; $i++){
$readBytes .= chr($bitBuffer->read(8));
}
return $readBytes;
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* Class ECI
*
* @created 20.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode};
use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf;
/**
* Adds an ECI Designator
*
* ISO/IEC 18004:2000 8.4.1.1
*
* Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment()
*/
final class ECI extends QRDataModeAbstract{
/**
* @inheritDoc
*/
public const DATAMODE = Mode::ECI;
/**
* The current ECI encoding id
*/
private int $encoding;
/**
* @inheritDoc
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(int $encoding){
if($encoding < 0 || $encoding > 999999){
throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding));
}
$this->encoding = $encoding;
}
/**
* @inheritDoc
*/
public function getLengthInBits():int{
if($this->encoding < 128){
return 8;
}
if($this->encoding < 16384){
return 16;
}
return 24;
}
/**
* Writes an ECI designator to the bitbuffer
*
* @inheritDoc
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$bitBuffer->put(self::DATAMODE, 4);
if($this->encoding < 128){
$bitBuffer->put($this->encoding, 8);
}
elseif($this->encoding < 16384){
$bitBuffer->put(($this->encoding | 0x8000), 16);
}
elseif($this->encoding < 1000000){
$bitBuffer->put(($this->encoding | 0xC00000), 24);
}
else{
throw new QRCodeDataException('invalid ECI ID');
}
return $this;
}
/**
* Reads and parses the value of an ECI designator
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function parseValue(BitBuffer $bitBuffer):ECICharset{
$firstByte = $bitBuffer->read(8);
// just one byte
if(($firstByte & 0b10000000) === 0){
$id = ($firstByte & 0b01111111);
}
// two bytes
elseif(($firstByte & 0b11000000) === 0b10000000){
$id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8));
}
// three bytes
elseif(($firstByte & 0b11100000) === 0b11000000){
$id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16));
}
else{
throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte)); // @codeCoverageIgnore
}
return new ECICharset($id);
}
/**
* @codeCoverageIgnore Unused, but required as per interface
*/
public static function validateString(string $string):bool{
return true;
}
/**
* Reads and decodes the ECI designator including the following byte sequence
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$eciCharset = self::parseValue($bitBuffer);
$nextMode = $bitBuffer->read(4);
if($nextMode !== Mode::BYTE){
throw new QRCodeDataException(sprintf('ECI designator followed by invalid mode: "%04b"', $nextMode));
}
$data = Byte::decodeSegment($bitBuffer, $versionNumber);
$encoding = $eciCharset->getName();
if($encoding === null){
// The spec isn't clear on this mode; see
// section 6.4.5: t does not say which encoding to assuming
// upon decoding. I have seen ISO-8859-1 used as well as
// Shift_JIS -- without anything like an ECI designator to
// give a hint.
$encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true);
if($encoding === false){
throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore
}
}
return mb_convert_encoding($data, mb_internal_encoding(), $encoding);
}
}

View File

@@ -0,0 +1,205 @@
<?php
/**
* Class Hanzi
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use Throwable;
use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding,
mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen;
/**
* Hanzi (simplified Chinese) mode, GBT18284-2000: 13-bit double-byte characters from the GB2312/GB18030 character set
*
* Please note that this is not part of the QR Code specification and may not be supported by all readers (ZXing-based ones do).
*
* @see https://en.wikipedia.org/wiki/GB_2312
* @see http://www.herongyang.com/GB2312/Introduction-of-GB2312.html
* @see https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
* @see https://gist.github.com/codemasher/91da33c44bfb48a81a6c1426bb8e4338
* @see https://github.com/zxing/zxing/blob/dfb06fa33b17a9e68321be151c22846c7b78048f/core/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java#L172-L209
* @see https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000
*/
final class Hanzi extends QRDataModeAbstract{
/**
* possible values: GB2312, GB18030
*
* @var string
*/
public const ENCODING = 'GB18030';
/**
* @todo: other subsets???
*
* @var int
*/
public const GB2312_SUBSET = 0b0001;
/**
* @inheritDoc
*/
public const DATAMODE = Mode::HANZI;
/**
* @inheritDoc
*/
protected function getCharCount():int{
return mb_strlen($this->data, self::ENCODING);
}
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return ($this->getCharCount() * 13);
}
/**
* @inheritDoc
*/
public static function convertEncoding(string $string):string{
mb_detect_order([mb_internal_encoding(), 'UTF-8', 'GB2312', 'GB18030', 'CP936', 'EUC-CN', 'HZ']);
$detected = mb_detect_encoding($string, null, true);
if($detected === false){
throw new QRCodeDataException('mb_detect_encoding error');
}
if($detected === self::ENCODING){
return $string;
}
$string = mb_convert_encoding($string, self::ENCODING, $detected);
if(!is_string($string)){
throw new QRCodeDataException('mb_convert_encoding error');
}
return $string;
}
/**
* checks if a string qualifies as Hanzi/GB2312
*/
public static function validateString(string $string):bool{
try{
$string = self::convertEncoding($string);
}
catch(Throwable $e){
return false;
}
$len = strlen($string);
if($len < 2 || ($len % 2) !== 0){
return false;
}
for($i = 0; $i < $len; $i += 2){
$byte1 = ord($string[$i]);
$byte2 = ord($string[($i + 1)]);
// byte 1 unused ranges
if($byte1 < 0xa1 || ($byte1 > 0xa9 && $byte1 < 0xb0) || $byte1 > 0xf7){
return false;
}
// byte 2 unused ranges
if($byte2 < 0xa1 || $byte2 > 0xfe){
return false;
}
}
return true;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$bitBuffer
->put(self::DATAMODE, 4)
->put($this::GB2312_SUBSET, 4)
->put($this->getCharCount(), $this::getLengthBits($versionNumber))
;
$len = strlen($this->data);
for($i = 0; ($i + 1) < $len; $i += 2){
$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
if($c >= 0xa1a1 && $c <= 0xaafe){
$c -= 0x0a1a1;
}
elseif($c >= 0xb0a1 && $c <= 0xfafe){
$c -= 0x0a6a1;
}
else{
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
}
$bitBuffer->put((((($c >> 8) & 0xff) * 0x060) + ($c & 0xff)), 13);
}
if($i < $len){
throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
}
return $this;
}
/**
* See specification GBT 18284-2000
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
// Hanzi mode contains a subset indicator right after mode indicator
if($bitBuffer->read(4) !== self::GB2312_SUBSET){
throw new QRCodeDataException('ecpected subset indicator for Hanzi mode');
}
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
if($bitBuffer->available() < ($length * 13)){
throw new QRCodeDataException('not enough bits available');
}
// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as GB2312 afterwards
$buffer = [];
$offset = 0;
while($length > 0){
// Each 13 bits encodes a 2-byte character
$twoBytes = $bitBuffer->read(13);
$assembledTwoBytes = ((intdiv($twoBytes, 0x060) << 8) | ($twoBytes % 0x060));
$assembledTwoBytes += ($assembledTwoBytes < 0x00a00) // 0x003BF
? 0x0a1a1 // In the 0xA1A1 to 0xAAFE range
: 0x0a6a1; // In the 0xB0A1 to 0xFAFE range
$buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8));
$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
$offset += 2;
$length--;
}
return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING);
}
}

View File

@@ -0,0 +1,191 @@
<?php
/**
* Class Kanji
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use Throwable;
use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding,
mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen;
/**
* Kanji mode: 13-bit double-byte characters from the Shift-JIS character set
*
* ISO/IEC 18004:2000 Section 8.3.5
* ISO/IEC 18004:2000 Section 8.4.5
*
* @see https://en.wikipedia.org/wiki/Shift_JIS#As_defined_in_JIS_X_0208:1997
* @see http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml
* @see https://gist.github.com/codemasher/d07d3e6e9346c08e7a41b8b978784952
*/
final class Kanji extends QRDataModeAbstract{
/**
* possible values: SJIS, SJIS-2004
*
* SJIS-2004 may produce errors in PHP < 8
*
* @var string
*/
public const ENCODING = 'SJIS';
/**
* @inheritDoc
*/
public const DATAMODE = Mode::KANJI;
/**
* @inheritDoc
*/
protected function getCharCount():int{
return mb_strlen($this->data, self::ENCODING);
}
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return ($this->getCharCount() * 13);
}
/**
* @inheritDoc
*/
public static function convertEncoding(string $string):string{
mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']);
$detected = mb_detect_encoding($string, null, true);
if($detected === false){
throw new QRCodeDataException('mb_detect_encoding error');
}
if($detected === self::ENCODING){
return $string;
}
$string = mb_convert_encoding($string, self::ENCODING, $detected);
if(!is_string($string)){
throw new QRCodeDataException(sprintf('invalid encoding: %s', $detected));
}
return $string;
}
/**
* checks if a string qualifies as SJIS Kanji
*/
public static function validateString(string $string):bool{
try{
$string = self::convertEncoding($string);
}
catch(Throwable $e){
return false;
}
$len = strlen($string);
if($len < 2 || ($len % 2) !== 0){
return false;
}
for($i = 0; $i < $len; $i += 2){
$byte1 = ord($string[$i]);
$byte2 = ord($string[($i + 1)]);
// byte 1 unused and vendor ranges
if($byte1 < 0x81 || ($byte1 > 0x84 && $byte1 < 0x88) || ($byte1 > 0x9f && $byte1 < 0xe0) || $byte1 > 0xea){
return false;
}
// byte 2 unused ranges
if($byte2 < 0x40 || $byte2 === 0x7f || $byte2 > 0xfc){
return false;
}
}
return true;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$bitBuffer
->put(self::DATAMODE, 4)
->put($this->getCharCount(), $this::getLengthBits($versionNumber))
;
$len = strlen($this->data);
for($i = 0; ($i + 1) < $len; $i += 2){
$c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)])));
if($c >= 0x8140 && $c <= 0x9ffc){
$c -= 0x8140;
}
elseif($c >= 0xe040 && $c <= 0xebbf){
$c -= 0xc140;
}
else{
throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c));
}
$bitBuffer->put((((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff)), 13);
}
if($i < $len){
throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1)));
}
return $this;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
if($bitBuffer->available() < ($length * 13)){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
// Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as SJIS afterwards
$buffer = [];
$offset = 0;
while($length > 0){
// Each 13 bits encodes a 2-byte character
$twoBytes = $bitBuffer->read(13);
$assembledTwoBytes = ((intdiv($twoBytes, 0x0c0) << 8) | ($twoBytes % 0x0c0));
$assembledTwoBytes += ($assembledTwoBytes < 0x01f00)
? 0x08140 // In the 0x8140 to 0x9FFC range
: 0x0c140; // In the 0xE040 to 0xEBBF range
$buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8));
$buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes);
$offset += 2;
$length--;
}
return mb_convert_encoding(implode($buffer), mb_internal_encoding(), self::ENCODING);
}
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* Class Number
*
* @created 26.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, Mode};
use function array_flip, ceil, intdiv, str_split, substr, unpack;
/**
* Numeric mode: decimal digits 0 to 9
*
* ISO/IEC 18004:2000 Section 8.3.2
* ISO/IEC 18004:2000 Section 8.4.2
*/
final class Number extends QRDataModeAbstract{
/**
* @var int[]
*/
private const NUMBER_TO_ORD = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
];
/**
* @inheritDoc
*/
public const DATAMODE = Mode::NUMBER;
/**
* @inheritDoc
*/
public function getLengthInBits():int{
return (int)ceil($this->getCharCount() * (10 / 3));
}
/**
* @inheritDoc
*/
public static function validateString(string $string):bool{
if($string === ''){
return false;
}
foreach(str_split($string) as $chr){
if(!isset(self::NUMBER_TO_ORD[$chr])){
return false;
}
}
return true;
}
/**
* @inheritDoc
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface{
$len = $this->getCharCount();
$bitBuffer
->put(self::DATAMODE, 4)
->put($len, $this::getLengthBits($versionNumber))
;
$i = 0;
// encode numeric triplets in 10 bits
while(($i + 2) < $len){
$bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10);
$i += 3;
}
if($i < $len){
// encode 2 remaining numbers in 7 bits
if(($len - $i) === 2){
$bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7);
}
// encode one remaining number in 4 bits
elseif(($len - $i) === 1){
$bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4);
}
}
return $this;
}
/**
* get the code for the given numeric string
*/
private function parseInt(string $string):int{
$num = 0;
foreach(unpack('C*', $string) as $chr){
$num = ($num * 10 + $chr - 48);
}
return $num;
}
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{
$length = $bitBuffer->read(self::getLengthBits($versionNumber));
$charmap = array_flip(self::NUMBER_TO_ORD);
// @todo
$toNumericChar = function(int $ord) use ($charmap):string{
if(isset($charmap[$ord])){
return $charmap[$ord];
}
throw new QRCodeDataException('invalid character value: '.$ord);
};
$result = '';
// Read three digits at a time
while($length >= 3){
// Each 10 bits encodes three digits
if($bitBuffer->available() < 10){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$threeDigitsBits = $bitBuffer->read(10);
if($threeDigitsBits >= 1000){
throw new QRCodeDataException('error decoding numeric value');
}
$result .= $toNumericChar(intdiv($threeDigitsBits, 100));
$result .= $toNumericChar(intdiv($threeDigitsBits, 10) % 10);
$result .= $toNumericChar($threeDigitsBits % 10);
$length -= 3;
}
if($length === 2){
// Two digits left over to read, encoded in 7 bits
if($bitBuffer->available() < 7){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$twoDigitsBits = $bitBuffer->read(7);
if($twoDigitsBits >= 100){
throw new QRCodeDataException('error decoding numeric value');
}
$result .= $toNumericChar(intdiv($twoDigitsBits, 10));
$result .= $toNumericChar($twoDigitsBits % 10);
}
elseif($length === 1){
// One digit left over to read
if($bitBuffer->available() < 4){
throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore
}
$digitBits = $bitBuffer->read(4);
if($digitBits >= 10){
throw new QRCodeDataException('error decoding numeric value');
}
$result .= $toNumericChar($digitBits);
}
return $result;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeDataException
*
* @created 09.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeDataException extends QRCodeException{
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* Class QRData
*
* @created 25.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, Version};
use chillerlan\Settings\SettingsContainerInterface;
use function count, sprintf;
/**
* Processes the binary data and maps it on a QRMatrix which is then being returned
*/
final class QRData{
/**
* the options instance
*
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
*/
private SettingsContainerInterface $options;
/**
* a BitBuffer instance
*/
private BitBuffer $bitBuffer;
/**
* an EccLevel instance
*/
private EccLevel $eccLevel;
/**
* current QR Code version
*/
private Version $version;
/**
* @var \chillerlan\QRCode\Data\QRDataModeInterface[]
*/
private array $dataSegments = [];
/**
* Max bits for the current ECC mode
*
* @var int[]
*/
private array $maxBitsForEcc;
/**
* QRData constructor.
*/
public function __construct(SettingsContainerInterface $options, array $dataSegments = []){
$this->options = $options;
$this->bitBuffer = new BitBuffer;
$this->eccLevel = new EccLevel($this->options->eccLevel);
$this->maxBitsForEcc = $this->eccLevel->getMaxBits();
$this->setData($dataSegments);
}
/**
* Sets the data string (internally called by the constructor)
*
* Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead
*
* @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments
*/
public function setData(array $dataSegments):self{
$this->dataSegments = $dataSegments;
$this->version = $this->getMinimumVersion();
$this->bitBuffer->clear();
$this->writeBitBuffer();
return $this;
}
/**
* Returns the current BitBuffer instance
*
* @codeCoverageIgnore
*/
public function getBitBuffer():BitBuffer{
return $this->bitBuffer;
}
/**
* Sets a BitBuffer object
*
* This can be used instead of setData(), however, the version auto-detection is not available in this case.
* The version needs to match the length bits range for the data mode the data has been encoded with,
* additionally the bit array needs to contain enough pad bits.
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setBitBuffer(BitBuffer $bitBuffer):self{
if($this->options->version === Version::AUTO){
throw new QRCodeDataException('version auto detection is not available');
}
if($bitBuffer->getLength() === 0){
throw new QRCodeDataException('the given BitBuffer is empty');
}
$this->dataSegments = [];
$this->bitBuffer = $bitBuffer;
$this->version = new Version($this->options->version);
return $this;
}
/**
* returns a fresh matrix object with the data written and masked with the given $maskPattern
*/
public function writeMatrix():QRMatrix{
return (new QRMatrix($this->version, $this->eccLevel))
->initFunctionalPatterns()
->writeCodewords($this->bitBuffer)
;
}
/**
* estimates the total length of the several mode segments in order to guess the minimum version
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function estimateTotalBitLength():int{
$length = 0;
foreach($this->dataSegments as $segment){
// data length of the current segment
$length += $segment->getLengthInBits();
// +4 bits for the mode descriptor
$length += 4;
// Hanzi mode sets an additional 4 bit long subset identifier
if($segment instanceof Hanzi){
$length += 4;
}
}
$provisionalVersion = null;
foreach($this->maxBitsForEcc as $version => $maxBits){
if($length <= $maxBits){
$provisionalVersion = $version;
}
}
if($provisionalVersion !== null){
// add character count indicator bits for the provisional version
foreach($this->dataSegments as $segment){
$length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion);
}
// it seems that in some cases the estimated total length is not 100% accurate,
// so we substract 4 bits from the total when not in mixed mode
if(count($this->dataSegments) <= 1){
$length -= 4;
}
// we've got a match!
// or let's see if there's a higher version number available
if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){
return $length;
}
}
throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length));
}
/**
* returns the minimum version number for the given string
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function getMinimumVersion():Version{
if($this->options->version !== Version::AUTO){
return new Version($this->options->version);
}
$total = $this->estimateTotalBitLength();
// guess the version number within the given range
for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){
if($total <= $this->maxBitsForEcc[$version]){
return new Version($version);
}
}
// it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first
throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore
}
/**
* creates a BitBuffer and writes the string data to it
*
* @throws \chillerlan\QRCode\QRCodeException on data overflow
*/
private function writeBitBuffer():void{
$MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version);
foreach($this->dataSegments as $segment){
$segment->write($this->bitBuffer, $this->version->getVersionNumber());
}
// overflow, likely caused due to invalid version setting
if($this->bitBuffer->getLength() > $MAX_BITS){
throw new QRCodeDataException(
sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS)
);
}
// add terminator (ISO/IEC 18004:2000 Table 2)
if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){
$this->bitBuffer->put(Mode::TERMINATOR, 4);
}
// Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion
// if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long
// by the addition of padding bits with binary value 0
while(($this->bitBuffer->getLength() % 8) !== 0){
if($this->bitBuffer->getLength() === $MAX_BITS){
break;
}
$this->bitBuffer->putBit(false);
}
// The message bit stream shall then be extended to fill the data capacity of the symbol
// corresponding to the Version and Error Correction Level, by the addition of the Pad
// Codewords 11101100 and 00010001 alternately.
$alternate = false;
while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){
$this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8);
$alternate = !$alternate;
}
// In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros)
// to the end of the message in order exactly to fill the symbol capacity
while($this->bitBuffer->getLength() <= $MAX_BITS){
$this->bitBuffer->putBit(false);
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Class QRDataModeAbstract
*
* @created 19.11.2020
* @author smiley <smiley@chillerlan.net>
* @copyright 2020 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\Mode;
/**
* abstract methods for the several data modes
*/
abstract class QRDataModeAbstract implements QRDataModeInterface{
/**
* The data to write
*/
protected string $data;
/**
* QRDataModeAbstract constructor.
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function __construct(string $data){
$data = $this::convertEncoding($data);
if(!$this::validateString($data)){
throw new QRCodeDataException('invalid data');
}
$this->data = $data;
}
/**
* returns the character count of the $data string
*/
protected function getCharCount():int{
return strlen($this->data);
}
/**
* @inheritDoc
*/
public static function convertEncoding(string $string):string{
return $string;
}
/**
* shortcut
*/
protected static function getLengthBits(int $versionNumber):int{
return Mode::getLengthBitsForVersion(static::DATAMODE, $versionNumber);
}
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Interface QRDataModeInterface
*
* @created 01.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\BitBuffer;
/**
* Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji)
*/
interface QRDataModeInterface{
/**
* the current data mode: Number, Alphanum, Kanji, Hanzi, Byte, ECI
*
* tbh I hate this constant here, but it's part of the interface, so I can't just declare it in the abstract class.
* (phan will complain about a PhanAccessOverridesFinalConstant)
*
* @see https://wiki.php.net/rfc/final_class_const
*
* @var int
* @see \chillerlan\QRCode\Common\Mode
* @internal do not call this constant from the interface, but rather from one of the child classes
*/
public const DATAMODE = -1;
/**
* retruns the length in bits of the data string
*/
public function getLengthInBits():int;
/**
* encoding conversion helper
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public static function convertEncoding(string $string):string;
/**
* checks if the given string qualifies for the encoder module
*/
public static function validateString(string $string):bool;
/**
* writes the actual data string to the BitBuffer, uses the given version to determine the length bits
*
* @see \chillerlan\QRCode\Data\QRData::writeBitBuffer()
*/
public function write(BitBuffer $bitBuffer, int $versionNumber):QRDataModeInterface;
/**
* reads a segment from the BitBuffer and decodes in the current data mode
*/
public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string;
}

View File

@@ -0,0 +1,812 @@
<?php
/**
* Class QRMatrix
*
* @created 15.11.2017
* @author Smiley <smiley@chillerlan.net>
* @copyright 2017 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version};
use function array_fill, array_map, array_reverse, count, intdiv;
/**
* Holds an array representation of the final QR Code that contains numerical values for later output modifications;
* maps the ECC coded binary data and applies the mask pattern
*
* @see http://www.thonky.com/qr-code-tutorial/format-version-information
*/
class QRMatrix{
/*
* special values
*/
/** @var int */
public const IS_DARK = 0b100000000000;
/** @var int */
public const M_NULL = 0b000000000000;
/** @var int */
public const M_LOGO = 0b001000000000;
/** @var int */
public const M_LOGO_DARK = 0b101000000000;
/*
* light values
*/
/** @var int */
public const M_DATA = 0b000000000010;
/** @var int */
public const M_FINDER = 0b000000000100;
/** @var int */
public const M_SEPARATOR = 0b000000001000;
/** @var int */
public const M_ALIGNMENT = 0b000000010000;
/** @var int */
public const M_TIMING = 0b000000100000;
/** @var int */
public const M_FORMAT = 0b000001000000;
/** @var int */
public const M_VERSION = 0b000010000000;
/** @var int */
public const M_QUIETZONE = 0b000100000000;
/*
* dark values
*/
/** @var int */
public const M_DARKMODULE = 0b100000000001;
/** @var int */
public const M_DATA_DARK = 0b100000000010;
/** @var int */
public const M_FINDER_DARK = 0b100000000100;
/** @var int */
public const M_ALIGNMENT_DARK = 0b100000010000;
/** @var int */
public const M_TIMING_DARK = 0b100000100000;
/** @var int */
public const M_FORMAT_DARK = 0b100001000000;
/** @var int */
public const M_VERSION_DARK = 0b100010000000;
/** @var int */
public const M_FINDER_DOT = 0b110000000000;
/*
* values used for reversed reflectance
*/
/** @var int */
public const M_DARKMODULE_LIGHT = 0b000000000001;
/** @var int */
public const M_FINDER_DOT_LIGHT = 0b010000000000;
/** @var int */
public const M_SEPARATOR_DARK = 0b100000001000;
/** @var int */
public const M_QUIETZONE_DARK = 0b100100000000;
/**
* Map of flag => coord
*
* @see \chillerlan\QRCode\Data\QRMatrix::checkNeighbours()
*
* @var array
*/
protected const neighbours = [
0b00000001 => [-1, -1],
0b00000010 => [ 0, -1],
0b00000100 => [ 1, -1],
0b00001000 => [ 1, 0],
0b00010000 => [ 1, 1],
0b00100000 => [ 0, 1],
0b01000000 => [-1, 1],
0b10000000 => [-1, 0],
];
/**
* the matrix version - always set in QRMatrix, may be null in BitMatrix
*/
protected ?Version $version = null;
/**
* the current ECC level - always set in QRMatrix, may be null in BitMatrix
*/
protected ?EccLevel $eccLevel = null;
/**
* the mask pattern that was used in the most recent operation, set via:
*
* - QRMatrix::setFormatInfo()
* - QRMatrix::mask()
* - BitMatrix::readFormatInformation()
*/
protected ?MaskPattern $maskPattern = null;
/**
* the size (side length) of the matrix, including quiet zone (if created)
*/
protected int $moduleCount;
/**
* the actual matrix data array
*
* @var int[][]
*/
protected array $matrix;
/**
* QRMatrix constructor.
*/
public function __construct(Version $version, EccLevel $eccLevel){
$this->version = $version;
$this->eccLevel = $eccLevel;
$this->moduleCount = $this->version->getDimension();
$this->matrix = $this->createMatrix($this->moduleCount, $this::M_NULL);
}
/**
* Creates a 2-dimensional array (square) of the given $size
*/
protected function createMatrix(int $size, int $value):array{
return array_fill(0, $size, array_fill(0, $size, $value));
}
/**
* shortcut to initialize the functional patterns
*/
public function initFunctionalPatterns():self{
return $this
->setFinderPattern()
->setSeparators()
->setAlignmentPattern()
->setTimingPattern()
->setDarkModule()
->setVersionNumber()
->setFormatInfo()
;
}
/**
* Returns the data matrix, returns a pure boolean representation if $boolean is set to true
*
* @return int[][]|bool[][]
*/
public function getMatrix(bool $boolean = null):array{
if($boolean !== true){
return $this->matrix;
}
$matrix = $this->matrix;
foreach($matrix as &$row){
$row = array_map([$this, 'isDark'], $row);
}
return $matrix;
}
/**
* @deprecated 5.0.0 use QRMatrix::getMatrix() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getMatrix()
* @codeCoverageIgnore
*/
public function matrix(bool $boolean = null):array{
return $this->getMatrix($boolean);
}
/**
* Returns the current version number
*/
public function getVersion():?Version{
return $this->version;
}
/**
* @deprecated 5.0.0 use QRMatrix::getVersion() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getVersion()
* @codeCoverageIgnore
*/
public function version():?Version{
return $this->getVersion();
}
/**
* Returns the current ECC level
*/
public function getEccLevel():?EccLevel{
return $this->eccLevel;
}
/**
* @deprecated 5.0.0 use QRMatrix::getEccLevel() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getEccLevel()
* @codeCoverageIgnore
*/
public function eccLevel():?EccLevel{
return $this->getEccLevel();
}
/**
* Returns the current mask pattern
*/
public function getMaskPattern():?MaskPattern{
return $this->maskPattern;
}
/**
* @deprecated 5.0.0 use QRMatrix::getMaskPattern() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getMaskPattern()
* @codeCoverageIgnore
*/
public function maskPattern():?MaskPattern{
return $this->getMaskPattern();
}
/**
* Returns the absoulute size of the matrix, including quiet zone (after setting it).
*
* size = version * 4 + 17 [ + 2 * quietzone size]
*/
public function getSize():int{
return $this->moduleCount;
}
/**
* @deprecated 5.0.0 use QRMatrix::getSize() instead
* @see \chillerlan\QRCode\Data\QRMatrix::getSize()
* @codeCoverageIgnore
*/
public function size():int{
return $this->getSize();
}
/**
* Returns the value of the module at position [$x, $y] or -1 if the coordinate is outside the matrix
*/
public function get(int $x, int $y):int{
if(!isset($this->matrix[$y][$x])){
return -1;
}
return $this->matrix[$y][$x];
}
/**
* Sets the $M_TYPE value for the module at position [$x, $y]
*
* true => $M_TYPE | 0x800
* false => $M_TYPE
*/
public function set(int $x, int $y, bool $value, int $M_TYPE):self{
if(isset($this->matrix[$y][$x])){
// we don't know whether the input is dark, so we remove the dark bit
$M_TYPE &= ~$this::IS_DARK;
if($value === true){
$M_TYPE |= $this::IS_DARK;
}
$this->matrix[$y][$x] = $M_TYPE;
}
return $this;
}
/**
* Fills an area of $width * $height, from the given starting point [$startX, $startY] (top left) with $value for $M_TYPE.
*/
public function setArea(int $startX, int $startY, int $width, int $height, bool $value, int $M_TYPE):self{
for($y = $startY; $y < ($startY + $height); $y++){
for($x = $startX; $x < ($startX + $width); $x++){
$this->set($x, $y, $value, $M_TYPE);
}
}
return $this;
}
/**
* Flips the value of the module at ($x, $y)
*/
public function flip(int $x, int $y):self{
if(isset($this->matrix[$y][$x])){
$this->matrix[$y][$x] ^= $this::IS_DARK;
}
return $this;
}
/**
* Checks whether the module at ($x, $y) is of the given $M_TYPE
*
* true => $value & $M_TYPE === $M_TYPE
*
* Also, returns false if the given coordinates are out of range.
*/
public function checkType(int $x, int $y, int $M_TYPE):bool{
if(isset($this->matrix[$y][$x])){
return ($this->matrix[$y][$x] & $M_TYPE) === $M_TYPE;
}
return false;
}
/**
* Checks whether the module at ($x, $y) is in the given array of $M_TYPES,
* returns true if a match is found, otherwise false.
*/
public function checkTypeIn(int $x, int $y, array $M_TYPES):bool{
foreach($M_TYPES as $type){
if($this->checkType($x, $y, $type)){
return true;
}
}
return false;
}
/**
* Checks whether the module at ($x, $y) is true (dark) or false (light)
*
* Also, returns false if the given coordinates are out of range.
*/
public function check(int $x, int $y):bool{
if(isset($this->matrix[$y][$x])){
return $this->isDark($this->matrix[$y][$x]);
}
return false;
}
/**
* Checks whether the given $M_TYPE is a dark value
*/
public function isDark(int $M_TYPE):bool{
return ($M_TYPE & $this::IS_DARK) === $this::IS_DARK;
}
/**
* Checks the status of the neighbouring modules for the module at ($x, $y) and returns a bitmask with the results.
*
* The 8 flags of the bitmask represent the status of each of the neighbouring fields,
* starting with the lowest bit for top left, going clockwise:
*
* 0 1 2
* 7 # 3
* 6 5 4
*/
public function checkNeighbours(int $x, int $y, int $M_TYPE = null):int{
$bits = 0;
foreach($this::neighbours as $bit => [$ix, $iy]){
$ix += $x;
$iy += $y;
// $M_TYPE is given, skip if the field is not the same type
if($M_TYPE !== null && !$this->checkType($ix, $iy, $M_TYPE)){
continue;
}
if($this->checkType($ix, $iy, $this::IS_DARK)){
$bits |= $bit;
}
}
return $bits;
}
/**
* Sets the "dark module", that is always on the same position 1x1px away from the bottom left finder
*
* 4 * version + 9 or moduleCount - 8
*/
public function setDarkModule():self{
$this->set(8, ($this->moduleCount - 8), true, $this::M_DARKMODULE);
return $this;
}
/**
* Draws the 7x7 finder patterns in the corners top left/right and bottom left
*
* ISO/IEC 18004:2000 Section 7.3.2
*/
public function setFinderPattern():self{
$pos = [
[0, 0], // top left
[($this->moduleCount - 7), 0], // top right
[0, ($this->moduleCount - 7)], // bottom left
];
foreach($pos as $c){
$this
->setArea( $c[0] , $c[1] , 7, 7, true, $this::M_FINDER)
->setArea(($c[0] + 1), ($c[1] + 1), 5, 5, false, $this::M_FINDER)
->setArea(($c[0] + 2), ($c[1] + 2), 3, 3, true, $this::M_FINDER_DOT)
;
}
return $this;
}
/**
* Draws the separator lines around the finder patterns
*
* ISO/IEC 18004:2000 Section 7.3.3
*/
public function setSeparators():self{
$h = [
[7, 0],
[($this->moduleCount - 8), 0],
[7, ($this->moduleCount - 8)],
];
$v = [
[7, 7],
[($this->moduleCount - 1), 7],
[7, ($this->moduleCount - 8)],
];
for($c = 0; $c < 3; $c++){
for($i = 0; $i < 8; $i++){
$this->set( $h[$c][0] , ($h[$c][1] + $i), false, $this::M_SEPARATOR);
$this->set(($v[$c][0] - $i), $v[$c][1] , false, $this::M_SEPARATOR);
}
}
return $this;
}
/**
* Draws the 5x5 alignment patterns
*
* ISO/IEC 18004:2000 Section 7.3.5
*/
public function setAlignmentPattern():self{
$alignmentPattern = $this->version->getAlignmentPattern();
foreach($alignmentPattern as $y){
foreach($alignmentPattern as $x){
// skip existing patterns
if($this->matrix[$y][$x] !== $this::M_NULL){
continue;
}
$this
->setArea(($x - 2), ($y - 2), 5, 5, true, $this::M_ALIGNMENT)
->setArea(($x - 1), ($y - 1), 3, 3, false, $this::M_ALIGNMENT)
->set($x, $y, true, $this::M_ALIGNMENT)
;
}
}
return $this;
}
/**
* Draws the timing pattern (h/v checkered line between the finder patterns)
*
* ISO/IEC 18004:2000 Section 7.3.4
*/
public function setTimingPattern():self{
for($i = 8; $i < ($this->moduleCount - 8); $i++){
if($this->matrix[6][$i] !== $this::M_NULL || $this->matrix[$i][6] !== $this::M_NULL){
continue;
}
$v = ($i % 2) === 0;
$this->set($i, 6, $v, $this::M_TIMING); // h
$this->set(6, $i, $v, $this::M_TIMING); // v
}
return $this;
}
/**
* Draws the version information, 2x 3x6 pixel
*
* ISO/IEC 18004:2000 Section 8.10
*/
public function setVersionNumber():self{
$bits = $this->version->getVersionPattern();
if($bits !== null){
for($i = 0; $i < 18; $i++){
$a = intdiv($i, 3);
$b = (($i % 3) + ($this->moduleCount - 8 - 3));
$v = (($bits >> $i) & 1) === 1;
$this->set($b, $a, $v, $this::M_VERSION); // ne
$this->set($a, $b, $v, $this::M_VERSION); // sw
}
}
return $this;
}
/**
* Draws the format info along the finder patterns. If no $maskPattern, all format info modules will be set to false.
*
* ISO/IEC 18004:2000 Section 8.9
*/
public function setFormatInfo(MaskPattern $maskPattern = null):self{
$this->maskPattern = $maskPattern;
$bits = 0; // sets all format fields to false (test mode)
if($this->maskPattern instanceof MaskPattern){
$bits = $this->eccLevel->getformatPattern($this->maskPattern);
}
for($i = 0; $i < 15; $i++){
$v = (($bits >> $i) & 1) === 1;
if($i < 6){
$this->set(8, $i, $v, $this::M_FORMAT);
}
elseif($i < 8){
$this->set(8, ($i + 1), $v, $this::M_FORMAT);
}
else{
$this->set(8, ($this->moduleCount - 15 + $i), $v, $this::M_FORMAT);
}
if($i < 8){
$this->set(($this->moduleCount - $i - 1), 8, $v, $this::M_FORMAT);
}
elseif($i < 9){
$this->set(((15 - $i)), 8, $v, $this::M_FORMAT);
}
else{
$this->set((15 - $i - 1), 8, $v, $this::M_FORMAT);
}
}
return $this;
}
/**
* Draws the "quiet zone" of $size around the matrix
*
* ISO/IEC 18004:2000 Section 7.3.7
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setQuietZone(int $quietZoneSize):self{
// early exit if there's nothing to add
if($quietZoneSize < 1){
return $this;
}
if($this->matrix[($this->moduleCount - 1)][($this->moduleCount - 1)] === $this::M_NULL){
throw new QRCodeDataException('use only after writing data');
}
// create a matrix with the new size
$newSize = ($this->moduleCount + ($quietZoneSize * 2));
$newMatrix = $this->createMatrix($newSize, $this::M_QUIETZONE);
// copy over the current matrix
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
$newMatrix[($y + $quietZoneSize)][($x + $quietZoneSize)] = $val;
}
}
// set the new values
$this->moduleCount = $newSize;
$this->matrix = $newMatrix;
return $this;
}
/**
* Rotates the matrix by 90 degrees clock wise
*/
public function rotate90():self{
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
$this->matrix = array_map((fn(int ...$a):array => array_reverse($a)), ...$this->matrix);
return $this;
}
/**
* Inverts the values of the whole matrix
*
* ISO/IEC 18004:2015 Section 6.2 - Reflectance reversal
*/
public function invert():self{
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
// skip null fields
if($val === $this::M_NULL){
continue;
}
$this->flip($x, $y);
}
}
return $this;
}
/**
* Clears a space of $width * $height in order to add a logo or text.
* If no $height is given, the space will be assumed a square of $width.
*
* Additionally, the logo space can be positioned within the QR Code using $startX and $startY.
* If either of these are null, the logo space will be centered in that direction.
* ECC level "H" (30%) is required.
*
* The coordinates of $startX and $startY do not include the quiet zone:
* [0, 0] is always the top left module of the top left finder pattern, negative values go into the quiet zone top and left.
*
* Please note that adding a logo space minimizes the error correction capacity of the QR Code and
* created images may become unreadable, especially when printed with a chance to receive damage.
* Please test thoroughly before using this feature in production.
*
* This method should be called from within an output module (after the matrix has been filled with data).
* Note that there is no restiction on how many times this method could be called on the same matrix instance.
*
* @link https://github.com/chillerlan/php-qrcode/issues/52
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setLogoSpace(int $width, int $height = null, int $startX = null, int $startY = null):self{
$height ??= $width;
// if width and height happen to be negative or 0 (default value), just return - nothing to do
if($width <= 0 || $height <= 0){
return $this; // @codeCoverageIgnore
}
// for logos, we operate in ECC H (30%) only
if($this->eccLevel->getLevel() !== EccLevel::H){
throw new QRCodeDataException('ECC level "H" required to add logo space');
}
// $this->moduleCount includes the quiet zone (if created), we need the QR size here
$dimension = $this->version->getDimension();
// throw if the size exceeds the qrcode size
if($width > $dimension || $height > $dimension){
throw new QRCodeDataException('logo dimensions exceed matrix size');
}
// we need uneven sizes to center the logo space, adjust if needed
if($startX === null && ($width % 2) === 0){
$width++;
}
if($startY === null && ($height % 2) === 0){
$height++;
}
// throw if the logo space exceeds the maximum error correction capacity
if(($width * $height) > (int)($dimension * $dimension * 0.25)){
throw new QRCodeDataException('logo space exceeds the maximum error correction capacity');
}
$quietzone = (($this->moduleCount - $dimension) / 2);
$end = ($this->moduleCount - $quietzone);
// determine start coordinates
$startX ??= (($dimension - $width) / 2);
$startY ??= (($dimension - $height) / 2);
$endX = ($quietzone + $startX + $width);
$endY = ($quietzone + $startY + $height);
// clear the space
for($y = ($quietzone + $startY); $y < $endY; $y++){
for($x = ($quietzone + $startX); $x < $endX; $x++){
// out of bounds, skip
if($x < $quietzone || $y < $quietzone ||$x >= $end || $y >= $end){
continue;
}
$this->set($x, $y, false, $this::M_LOGO);
}
}
return $this;
}
/**
* Maps the interleaved binary $data on the matrix
*/
public function writeCodewords(BitBuffer $bitBuffer):self{
$data = (new ReedSolomonEncoder($this->version, $this->eccLevel))->interleaveEcBytes($bitBuffer);
$byteCount = count($data);
$iByte = 0;
$iBit = 7;
$direction = true;
for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
// skip vertical alignment pattern
if($i === 6){
$i--;
}
for($count = 0; $count < $this->moduleCount; $count++){
$y = $count;
if($direction){
$y = ($this->moduleCount - 1 - $count);
}
for($col = 0; $col < 2; $col++){
$x = ($i - $col);
// skip functional patterns
if($this->matrix[$y][$x] !== $this::M_NULL){
continue;
}
$this->matrix[$y][$x] = $this::M_DATA;
if($iByte < $byteCount && (($data[$iByte] >> $iBit--) & 1) === 1){
$this->matrix[$y][$x] |= $this::IS_DARK;
}
if($iBit === -1){
$iByte++;
$iBit = 7;
}
}
}
$direction = !$direction; // switch directions
}
return $this;
}
/**
* Applies/reverses the mask pattern
*
* ISO/IEC 18004:2000 Section 8.8.1
*/
public function mask(MaskPattern $maskPattern):self{
$this->maskPattern = $maskPattern;
$mask = $this->maskPattern->getMask();
foreach($this->matrix as $y => $row){
foreach($row as $x => $val){
// skip non-data modules
if(($val & $this::M_DATA) === $this::M_DATA && $mask($x, $y)){
$this->flip($x, $y);
}
}
}
return $this;
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* Class ReedSolomonEncoder
*
* @created 07.01.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Data;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version};
use function array_fill, array_merge, count, max;
/**
* Reed-Solomon encoding - ISO/IEC 18004:2000 Section 8.5 ff
*
* @see http://www.thonky.com/qr-code-tutorial/error-correction-coding
*/
final class ReedSolomonEncoder{
private Version $version;
private EccLevel $eccLevel;
private array $interleavedData;
private int $interleavedDataIndex;
/**
* ReedSolomonDecoder constructor
*/
public function __construct(Version $version, EccLevel $eccLevel){
$this->version = $version;
$this->eccLevel = $eccLevel;
}
/**
* ECC encoding and interleaving
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function interleaveEcBytes(BitBuffer $bitBuffer):array{
[$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel);
$rsBlocks = array_fill(0, $l1, [($numEccCodewords + $b1), $b1]);
if($l2 > 0){
$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [($numEccCodewords + $b2), $b2]));
}
$bitBufferData = $bitBuffer->getBuffer();
$dataBytes = [];
$ecBytes = [];
$maxDataBytes = 0;
$maxEcBytes = 0;
$dataByteOffset = 0;
foreach($rsBlocks as $key => [$rsBlockTotal, $dataByteCount]){
$dataBytes[$key] = [];
for($i = 0; $i < $dataByteCount; $i++){
$dataBytes[$key][$i] = ($bitBufferData[($i + $dataByteOffset)] & 0xff);
}
$ecByteCount = ($rsBlockTotal - $dataByteCount);
$ecBytes[$key] = $this->encode($dataBytes[$key], $ecByteCount);
$maxDataBytes = max($maxDataBytes, $dataByteCount);
$maxEcBytes = max($maxEcBytes, $ecByteCount);
$dataByteOffset += $dataByteCount;
}
$this->interleavedData = array_fill(0, $this->version->getTotalCodewords(), 0);
$this->interleavedDataIndex = 0;
$numRsBlocks = ($l1 + $l2);
$this->interleave($dataBytes, $maxDataBytes, $numRsBlocks);
$this->interleave($ecBytes, $maxEcBytes, $numRsBlocks);
return $this->interleavedData;
}
/**
*
*/
private function encode(array $dataBytes, int $ecByteCount):array{
$rsPoly = new GenericGFPoly([1]);
for($i = 0; $i < $ecByteCount; $i++){
$rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)]));
}
$rsPolyDegree = $rsPoly->getDegree();
$modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree))
->mod($rsPoly)
->getCoefficients()
;
$ecBytes = array_fill(0, $rsPolyDegree, 0);
$count = (count($modCoefficients) - $rsPolyDegree);
foreach($ecBytes as $i => &$val){
$modIndex = ($i + $count);
$val = 0;
if($modIndex >= 0){
$val = $modCoefficients[$modIndex];
}
}
return $ecBytes;
}
/**
*
*/
private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks):void{
for($x = 0; $x < $maxBytes; $x++){
for($y = 0; $y < $numRsBlocks; $y++){
if($x < count($byteArray[$y])){
$this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x];
}
}
}
}
}

View File

@@ -0,0 +1,361 @@
<?php
/**
* Class Binarizer
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\LuminanceSourceInterface;
use chillerlan\QRCode\Data\QRMatrix;
use function array_fill, count, intdiv, max;
/**
* This class implements a local thresholding algorithm, which while slower than the
* GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for
* high frequency images of barcodes with black data on white backgrounds. For this application,
* it does a much better job than a global blackpoint with severe shadows and gradients.
* However, it tends to produce artifacts on lower frequency images and is therefore not
* a good general purpose binarizer for uses outside ZXing.
*
* This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,
* and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already
* inherently local, and only fails for horizontal gradients. We can revisit that problem later,
* but for now it was not a win to use local blocks for 1D.
*
* This Binarizer is the default for the unit tests and the recommended class for library users.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
final class Binarizer{
// This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.
// So this is the smallest dimension in each axis we can accept.
private const BLOCK_SIZE_POWER = 3;
private const BLOCK_SIZE = 8; // ...0100...00
private const BLOCK_SIZE_MASK = 7; // ...0011...11
private const MINIMUM_DIMENSION = 40;
private const MIN_DYNAMIC_RANGE = 24;
# private const LUMINANCE_BITS = 5;
private const LUMINANCE_SHIFT = 3;
private const LUMINANCE_BUCKETS = 32;
private LuminanceSourceInterface $source;
private array $luminances;
/**
*
*/
public function __construct(LuminanceSourceInterface $source){
$this->source = $source;
$this->luminances = $this->source->getLuminances();
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function estimateBlackPoint(array $buckets):int{
// Find the tallest peak in the histogram.
$numBuckets = count($buckets);
$maxBucketCount = 0;
$firstPeak = 0;
$firstPeakSize = 0;
for($x = 0; $x < $numBuckets; $x++){
if($buckets[$x] > $firstPeakSize){
$firstPeak = $x;
$firstPeakSize = $buckets[$x];
}
if($buckets[$x] > $maxBucketCount){
$maxBucketCount = $buckets[$x];
}
}
// Find the second-tallest peak which is somewhat far from the tallest peak.
$secondPeak = 0;
$secondPeakScore = 0;
for($x = 0; $x < $numBuckets; $x++){
$distanceToBiggest = ($x - $firstPeak);
// Encourage more distant second peaks by multiplying by square of distance.
$score = ($buckets[$x] * $distanceToBiggest * $distanceToBiggest);
if($score > $secondPeakScore){
$secondPeak = $x;
$secondPeakScore = $score;
}
}
// Make sure firstPeak corresponds to the black peak.
if($firstPeak > $secondPeak){
$temp = $firstPeak;
$firstPeak = $secondPeak;
$secondPeak = $temp;
}
// If there is too little contrast in the image to pick a meaningful black point, throw rather
// than waste time trying to decode the image, and risk false positives.
if(($secondPeak - $firstPeak) <= ($numBuckets / 16)){
throw new QRCodeDecoderException('no meaningful dark point found'); // @codeCoverageIgnore
}
// Find a valley between them that is low and closer to the white peak.
$bestValley = ($secondPeak - 1);
$bestValleyScore = -1;
for($x = ($secondPeak - 1); $x > $firstPeak; $x--){
$fromFirst = ($x - $firstPeak);
$score = ($fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]));
if($score > $bestValleyScore){
$bestValley = $x;
$bestValleyScore = $score;
}
}
return ($bestValley << self::LUMINANCE_SHIFT);
}
/**
* Calculates the final BitMatrix once for all requests. This could be called once from the
* constructor instead, but there are some advantages to doing it lazily, such as making
* profiling easier, and not doing heavy lifting when callers don't expect it.
*
* Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive
* and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or
* may not apply sharpening. Therefore, a row from this matrix may not be identical to one
* fetched using getBlackRow(), so don't mix and match between them.
*
* @return \chillerlan\QRCode\Decoder\BitMatrix The 2D array of bits for the image (true means black).
*/
public function getBlackMatrix():BitMatrix{
$width = $this->source->getWidth();
$height = $this->source->getHeight();
if($width >= self::MINIMUM_DIMENSION && $height >= self::MINIMUM_DIMENSION){
$subWidth = ($width >> self::BLOCK_SIZE_POWER);
if(($width & self::BLOCK_SIZE_MASK) !== 0){
$subWidth++;
}
$subHeight = ($height >> self::BLOCK_SIZE_POWER);
if(($height & self::BLOCK_SIZE_MASK) !== 0){
$subHeight++;
}
return $this->calculateThresholdForBlock($subWidth, $subHeight, $width, $height);
}
// If the image is too small, fall back to the global histogram approach.
return $this->getHistogramBlackMatrix($width, $height);
}
/**
*
*/
private function getHistogramBlackMatrix(int $width, int $height):BitMatrix{
// Quickly calculates the histogram by sampling four rows from the image. This proved to be
// more robust on the blackbox tests than sampling a diagonal as we used to do.
$buckets = array_fill(0, self::LUMINANCE_BUCKETS, 0);
$right = intdiv(($width * 4), 5);
$x = intdiv($width, 5);
for($y = 1; $y < 5; $y++){
$row = intdiv(($height * $y), 5);
$localLuminances = $this->source->getRow($row);
for(; $x < $right; $x++){
$pixel = ($localLuminances[$x] & 0xff);
$buckets[($pixel >> self::LUMINANCE_SHIFT)]++;
}
}
$blackPoint = $this->estimateBlackPoint($buckets);
// We delay reading the entire image luminance until the black point estimation succeeds.
// Although we end up reading four rows twice, it is consistent with our motto of
// "fail quickly" which is necessary for continuous scanning.
$matrix = new BitMatrix(max($width, $height));
for($y = 0; $y < $height; $y++){
$offset = ($y * $width);
for($x = 0; $x < $width; $x++){
$matrix->set($x, $y, (($this->luminances[($offset + $x)] & 0xff) < $blackPoint), QRMatrix::M_DATA);
}
}
return $matrix;
}
/**
* Calculates a single black point for each block of pixels and saves it away.
* See the following thread for a discussion of this algorithm:
*
* @see http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0
*/
private function calculateBlackPoints(int $subWidth, int $subHeight, int $width, int $height):array{
$blackPoints = array_fill(0, $subHeight, array_fill(0, $subWidth, 0));
for($y = 0; $y < $subHeight; $y++){
$yoffset = ($y << self::BLOCK_SIZE_POWER);
$maxYOffset = ($height - self::BLOCK_SIZE);
if($yoffset > $maxYOffset){
$yoffset = $maxYOffset;
}
for($x = 0; $x < $subWidth; $x++){
$xoffset = ($x << self::BLOCK_SIZE_POWER);
$maxXOffset = ($width - self::BLOCK_SIZE);
if($xoffset > $maxXOffset){
$xoffset = $maxXOffset;
}
$sum = 0;
$min = 255;
$max = 0;
for($yy = 0, $offset = ($yoffset * $width + $xoffset); $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
$pixel = ((int)($this->luminances[(int)($offset + $xx)]) & 0xff);
$sum += $pixel;
// still looking for good contrast
if($pixel < $min){
$min = $pixel;
}
if($pixel > $max){
$max = $pixel;
}
}
// short-circuit min/max tests once dynamic range is met
if(($max - $min) > self::MIN_DYNAMIC_RANGE){
// finish the rest of the rows quickly
for($yy++, $offset += $width; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
$sum += ((int)($this->luminances[(int)($offset + $xx)]) & 0xff);
}
}
}
}
// The default estimate is the average of the values in the block.
$average = ($sum >> (self::BLOCK_SIZE_POWER * 2));
if(($max - $min) <= self::MIN_DYNAMIC_RANGE){
// If variation within the block is low, assume this is a block with only light or only
// dark pixels. In that case we do not want to use the average, as it would divide this
// low contrast area into black and white pixels, essentially creating data out of noise.
//
// The default assumption is that the block is light/background. Since no estimate for
// the level of dark pixels exists locally, use half the min for the block.
$average = ($min / 2);
if($y > 0 && $x > 0){
// Correct the "white background" assumption for blocks that have neighbors by comparing
// the pixels in this block to the previously calculated black points. This is based on
// the fact that dark barcode symbology is always surrounded by some amount of light
// background for which reasonable black point estimates were made. The bp estimated at
// the boundaries is used for the interior.
// The (min < bp) is arbitrary but works better than other heuristics that were tried.
$averageNeighborBlackPoint = (
($blackPoints[($y - 1)][$x] + (2 * $blackPoints[$y][($x - 1)]) + $blackPoints[($y - 1)][($x - 1)]) / 4
);
if($min < $averageNeighborBlackPoint){
$average = $averageNeighborBlackPoint;
}
}
}
$blackPoints[$y][$x] = $average;
}
}
return $blackPoints;
}
/**
* For each block in the image, calculate the average black point using a 5x5 grid
* of the surrounding blocks. Also handles the corner cases (fractional blocks are computed based
* on the last pixels in the row/column which are also used in the previous block).
*/
private function calculateThresholdForBlock(int $subWidth, int $subHeight, int $width, int $height):BitMatrix{
$matrix = new BitMatrix(max($width, $height));
$blackPoints = $this->calculateBlackPoints($subWidth, $subHeight, $width, $height);
for($y = 0; $y < $subHeight; $y++){
$yoffset = ($y << self::BLOCK_SIZE_POWER);
$maxYOffset = ($height - self::BLOCK_SIZE);
if($yoffset > $maxYOffset){
$yoffset = $maxYOffset;
}
for($x = 0; $x < $subWidth; $x++){
$xoffset = ($x << self::BLOCK_SIZE_POWER);
$maxXOffset = ($width - self::BLOCK_SIZE);
if($xoffset > $maxXOffset){
$xoffset = $maxXOffset;
}
$left = $this->cap($x, 2, ($subWidth - 3));
$top = $this->cap($y, 2, ($subHeight - 3));
$sum = 0;
for($z = -2; $z <= 2; $z++){
$br = $blackPoints[($top + $z)];
$sum += ($br[($left - 2)] + $br[($left - 1)] + $br[$left] + $br[($left + 1)] + $br[($left + 2)]);
}
$average = (int)($sum / 25);
// Applies a single threshold to a block of pixels.
for($j = 0, $o = ($yoffset * $width + $xoffset); $j < self::BLOCK_SIZE; $j++, $o += $width){
for($i = 0; $i < self::BLOCK_SIZE; $i++){
// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
$v = (((int)($this->luminances[($o + $i)]) & 0xff) <= $average);
$matrix->set(($xoffset + $i), ($yoffset + $j), $v, QRMatrix::M_DATA);
}
}
}
}
return $matrix;
}
/**
* @noinspection PhpSameParameterValueInspection
*/
private function cap(int $value, int $min, int $max):int{
if($value < $min){
return $min;
}
if($value > $max){
return $max;
}
return $value;
}
}

View File

@@ -0,0 +1,430 @@
<?php
/**
* Class BitMatrix
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
use function array_fill, array_reverse, count;
use const PHP_INT_MAX, PHP_INT_SIZE;
/**
* Extended QRMatrix to map read data from the Binarizer
*/
final class BitMatrix extends QRMatrix{
/**
* See ISO 18004:2006, Annex C, Table C.1
*
* [data bits, sequence after masking]
*/
private const DECODE_LOOKUP = [
0x5412, // 0101010000010010
0x5125, // 0101000100100101
0x5E7C, // 0101111001111100
0x5B4B, // 0101101101001011
0x45F9, // 0100010111111001
0x40CE, // 0100000011001110
0x4F97, // 0100111110010111
0x4AA0, // 0100101010100000
0x77C4, // 0111011111000100
0x72F3, // 0111001011110011
0x7DAA, // 0111110110101010
0x789D, // 0111100010011101
0x662F, // 0110011000101111
0x6318, // 0110001100011000
0x6C41, // 0110110001000001
0x6976, // 0110100101110110
0x1689, // 0001011010001001
0x13BE, // 0001001110111110
0x1CE7, // 0001110011100111
0x19D0, // 0001100111010000
0x0762, // 0000011101100010
0x0255, // 0000001001010101
0x0D0C, // 0000110100001100
0x083B, // 0000100000111011
0x355F, // 0011010101011111
0x3068, // 0011000001101000
0x3F31, // 0011111100110001
0x3A06, // 0011101000000110
0x24B4, // 0010010010110100
0x2183, // 0010000110000011
0x2EDA, // 0010111011011010
0x2BED, // 0010101111101101
];
private const FORMAT_INFO_MASK_QR = 0x5412; // 0101010000010010
/**
* This flag has effect only on the copyVersionBit() method.
* Before proceeding with readCodewords() the resetInfo() method should be called.
*/
private bool $mirror = false;
/**
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(int $dimension){
$this->moduleCount = $dimension;
$this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
}
/**
* Resets the current version info in order to attempt another reading
*/
public function resetVersionInfo():self{
$this->version = null;
$this->eccLevel = null;
$this->maskPattern = null;
return $this;
}
/**
* Mirror the bit matrix diagonally in order to attempt a second reading.
*/
public function mirrorDiagonal():self{
$this->mirror = !$this->mirror;
// mirror vertically
$this->matrix = array_reverse($this->matrix);
// rotate by 90 degrees clockwise
/** @phan-suppress-next-line PhanTypeMismatchReturnSuperType */
return $this->rotate90();
}
/**
* Reads the bits in the BitMatrix representing the finder pattern in the
* correct order in order to reconstruct the codewords bytes contained within the
* QR Code. Throws if the exact number of bytes expected is not read.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function readCodewords():array{
$this
->readFormatInformation()
->readVersion()
->mask($this->maskPattern) // reverse the mask pattern
;
// invoke a fresh matrix with only the function & format patterns to compare against
$matrix = (new QRMatrix($this->version, $this->eccLevel))
->initFunctionalPatterns()
->setFormatInfo($this->maskPattern)
;
$result = [];
$byte = 0;
$bitsRead = 0;
$direction = true;
// Read columns in pairs, from right to left
for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
// Skip whole column with vertical alignment pattern;
// saves time and makes the other code proceed more cleanly
if($i === 6){
$i--;
}
// Read alternatingly from bottom to top then top to bottom
for($count = 0; $count < $this->moduleCount; $count++){
$y = ($direction) ? ($this->moduleCount - 1 - $count) : $count;
for($col = 0; $col < 2; $col++){
$x = ($i - $col);
// Ignore bits covered by the function pattern
if($matrix->get($x, $y) !== $this::M_NULL){
continue;
}
$bitsRead++;
$byte <<= 1;
if($this->check($x, $y)){
$byte |= 1;
}
// If we've made a whole byte, save it off
if($bitsRead === 8){
$result[] = $byte;
$bitsRead = 0;
$byte = 0;
}
}
}
$direction = !$direction; // switch directions
}
if(count($result) !== $this->version->getTotalCodewords()){
throw new QRCodeDecoderException('result count differs from total codewords for version');
}
// bytes encoded within the QR Code
return $result;
}
/**
* Reads format information from one of its two locations within the QR Code.
* Throws if both format information locations cannot be parsed as the valid encoding of format information.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function readFormatInformation():self{
if($this->eccLevel !== null && $this->maskPattern !== null){
return $this;
}
// Read top-left format info bits
$formatInfoBits1 = 0;
for($i = 0; $i < 6; $i++){
$formatInfoBits1 = $this->copyVersionBit($i, 8, $formatInfoBits1);
}
// ... and skip a bit in the timing pattern ...
$formatInfoBits1 = $this->copyVersionBit(7, 8, $formatInfoBits1);
$formatInfoBits1 = $this->copyVersionBit(8, 8, $formatInfoBits1);
$formatInfoBits1 = $this->copyVersionBit(8, 7, $formatInfoBits1);
// ... and skip a bit in the timing pattern ...
for($j = 5; $j >= 0; $j--){
$formatInfoBits1 = $this->copyVersionBit(8, $j, $formatInfoBits1);
}
// Read the top-right/bottom-left pattern too
$formatInfoBits2 = 0;
$jMin = ($this->moduleCount - 7);
for($j = ($this->moduleCount - 1); $j >= $jMin; $j--){
$formatInfoBits2 = $this->copyVersionBit(8, $j, $formatInfoBits2);
}
for($i = ($this->moduleCount - 8); $i < $this->moduleCount; $i++){
$formatInfoBits2 = $this->copyVersionBit($i, 8, $formatInfoBits2);
}
$formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
if($formatInfo === null){
// Should return null, but, some QR codes apparently do not mask this info.
// Try again by actually masking the pattern first.
$formatInfo = $this->doDecodeFormatInformation(
($formatInfoBits1 ^ $this::FORMAT_INFO_MASK_QR),
($formatInfoBits2 ^ $this::FORMAT_INFO_MASK_QR)
);
// still nothing???
if($formatInfo === null){
throw new QRCodeDecoderException('failed to read format info'); // @codeCoverageIgnore
}
}
$this->eccLevel = new EccLevel(($formatInfo >> 3) & 0x03); // Bits 3,4
$this->maskPattern = new MaskPattern($formatInfo & 0x07); // Bottom 3 bits
return $this;
}
/**
*
*/
private function copyVersionBit(int $i, int $j, int $versionBits):int{
$bit = $this->mirror
? $this->check($j, $i)
: $this->check($i, $j);
return ($bit) ? (($versionBits << 1) | 0x1) : ($versionBits << 1);
}
/**
* Returns information about the format it specifies, or null if it doesn't seem to match any known pattern
*/
private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?int{
$bestDifference = PHP_INT_MAX;
$bestFormatInfo = 0;
// Find the int in FORMAT_INFO_DECODE_LOOKUP with the fewest bits differing
foreach($this::DECODE_LOOKUP as $maskedBits => $dataBits){
if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
// Found an exact match
return $maskedBits;
}
$bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits);
if($bitsDifference < $bestDifference){
$bestFormatInfo = $maskedBits;
$bestDifference = $bitsDifference;
}
if($maskedFormatInfo1 !== $maskedFormatInfo2){
// also try the other option
$bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits);
if($bitsDifference < $bestDifference){
$bestFormatInfo = $maskedBits;
$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 $bestFormatInfo;
}
return null;
}
/**
* Reads version information from one of its two locations within the QR Code.
* Throws if both version information locations cannot be parsed as the valid encoding of version information.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
* @noinspection DuplicatedCode
*/
private function readVersion():self{
if($this->version !== null){
return $this;
}
$provisionalVersion = (($this->moduleCount - 17) / 4);
// no version info if v < 7
if($provisionalVersion < 7){
$this->version = new Version($provisionalVersion);
return $this;
}
// Read top-right version info: 3 wide by 6 tall
$versionBits = 0;
$ijMin = ($this->moduleCount - 11);
for($y = 5; $y >= 0; $y--){
for($x = ($this->moduleCount - 9); $x >= $ijMin; $x--){
$versionBits = $this->copyVersionBit($x, $y, $versionBits);
}
}
$this->version = $this->decodeVersionInformation($versionBits);
if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
return $this;
}
// Hmm, failed. Try bottom left: 6 wide by 3 tall
$versionBits = 0;
for($x = 5; $x >= 0; $x--){
for($y = ($this->moduleCount - 9); $y >= $ijMin; $y--){
$versionBits = $this->copyVersionBit($x, $y, $versionBits);
}
}
$this->version = $this->decodeVersionInformation($versionBits);
if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
return $this;
}
throw new QRCodeDecoderException('failed to read version');
}
/**
* Decodes the version information from the given bit sequence, returns null if no valid match is found.
*/
private function decodeVersionInformation(int $versionBits):?Version{
$bestDifference = PHP_INT_MAX;
$bestVersion = 0;
for($i = 7; $i <= 40; $i++){
$targetVersion = new Version($i);
$targetVersionPattern = $targetVersion->getVersionPattern();
// Do the version info bits match exactly? done.
if($targetVersionPattern === $versionBits){
return $targetVersion;
}
// Otherwise see if this is the closest to a real version info bit string
// we have seen so far
/** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
$bitsDifference = $this->numBitsDiffering($versionBits, $targetVersionPattern);
if($bitsDifference < $bestDifference){
$bestVersion = $i;
$bestDifference = $bitsDifference;
}
}
// We can tolerate up to 3 bits of error since no two version info codewords will
// differ in less than 8 bits.
if($bestDifference <= 3){
return new Version($bestVersion);
}
// If we didn't find a close enough match, fail
return null;
}
/**
*
*/
private function uRShift(int $a, int $b):int{
if($b === 0){
return $a;
}
return (($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)));
}
/**
*
*/
private function numBitsDiffering(int $a, int $b):int{
// a now has a 1 bit exactly where its bit differs with b's
$a ^= $b;
// Offset $i holds the number of 1-bits in the binary representation of $i
$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
// Count bits set quickly with a series of lookups:
$count = 0;
for($i = 0; $i < 32; $i += 4){
$count += $BITS_SET_IN_HALF_BYTE[($this->uRShift($a, $i) & 0x0F)];
}
return $count;
}
/**
* @codeCoverageIgnore
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setQuietZone(int $quietZoneSize = null):self{
throw new QRCodeDataException('not supported');
}
/**
* @codeCoverageIgnore
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function setLogoSpace(int $width, int $height = null, int $startX = null, int $startY = null):self{
throw new QRCodeDataException('not supported');
}
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* Class Decoder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, LuminanceSourceInterface, MaskPattern, Mode, Version};
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number};
use chillerlan\QRCode\Detector\Detector;
use Throwable;
use function chr, str_replace;
/**
* The main class which implements QR Code decoding -- as opposed to locating and extracting
* the QR Code from an image.
*
* @author Sean Owen
*/
final class Decoder{
private ?Version $version = null;
private ?EccLevel $eccLevel = null;
private ?MaskPattern $maskPattern = null;
private BitBuffer $bitBuffer;
/**
* Decodes a QR Code represented as a BitMatrix.
* A 1 or "true" is taken to mean a black module.
*
* @throws \Throwable|\chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
public function decode(LuminanceSourceInterface $source):DecoderResult{
$matrix = (new Detector($source))->detect();
try{
// clone the BitMatrix to avoid errors in case we run into mirroring
return $this->decodeMatrix(clone $matrix);
}
catch(Throwable $e){
try{
/*
* Prepare for a mirrored reading.
*
* Since we're here, this means we have successfully detected some kind
* of version and format information when mirrored. This is a good sign,
* that the QR code may be mirrored, and we should try once more with a
* mirrored content.
*/
return $this->decodeMatrix($matrix->resetVersionInfo()->mirrorDiagonal());
}
catch(Throwable $f){
// Throw the exception from the original reading
throw $e;
}
}
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function decodeMatrix(BitMatrix $matrix):DecoderResult{
// Read raw codewords
$rawCodewords = $matrix->readCodewords();
$this->version = $matrix->getVersion();
$this->eccLevel = $matrix->getEccLevel();
$this->maskPattern = $matrix->getMaskPattern();
if($this->version === null || $this->eccLevel === null || $this->maskPattern === null){
throw new QRCodeDecoderException('unable to read version or format info'); // @codeCoverageIgnore
}
$resultBytes = (new ReedSolomonDecoder($this->version, $this->eccLevel))->decode($rawCodewords);
return $this->decodeBitStream($resultBytes);
}
/**
* Decode the contents of that stream of bytes
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{
$this->bitBuffer = $bitBuffer;
$versionNumber = $this->version->getVersionNumber();
$symbolSequence = -1;
$parityData = -1;
$fc1InEffect = false;
$result = '';
// While still another segment to read...
while($this->bitBuffer->available() >= 4){
$datamode = $this->bitBuffer->read(4); // mode is encoded by 4 bits
// OK, assume we're done
if($datamode === Mode::TERMINATOR){
break;
}
elseif($datamode === Mode::NUMBER){
$result .= Number::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::ALPHANUM){
$result .= $this->decodeAlphanumSegment($versionNumber, $fc1InEffect);
}
elseif($datamode === Mode::BYTE){
$result .= Byte::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::KANJI){
$result .= Kanji::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::STRCTURED_APPEND){
if($this->bitBuffer->available() < 16){
throw new QRCodeDecoderException('structured append: not enough bits left');
}
// sequence number and parity is added later to the result metadata
// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
$symbolSequence = $this->bitBuffer->read(8);
$parityData = $this->bitBuffer->read(8);
}
elseif($datamode === Mode::FNC1_FIRST || $datamode === Mode::FNC1_SECOND){
// We do little with FNC1 except alter the parsed result a bit according to the spec
$fc1InEffect = true;
}
elseif($datamode === Mode::ECI){
$result .= ECI::decodeSegment($this->bitBuffer, $versionNumber);
}
elseif($datamode === Mode::HANZI){
$result .= Hanzi::decodeSegment($this->bitBuffer, $versionNumber);
}
else{
throw new QRCodeDecoderException('invalid data mode');
}
}
return new DecoderResult([
'rawBytes' => $this->bitBuffer,
'data' => $result,
'version' => $this->version,
'eccLevel' => $this->eccLevel,
'maskPattern' => $this->maskPattern,
'structuredAppendParity' => $parityData,
'structuredAppendSequence' => $symbolSequence,
]);
}
/**
*
*/
private function decodeAlphanumSegment(int $versionNumber, bool $fc1InEffect):string{
$str = AlphaNum::decodeSegment($this->bitBuffer, $versionNumber);
// See section 6.4.8.1, 6.4.8.2
if($fc1InEffect){ // ???
// We need to massage the result a bit if in an FNC1 mode:
$str = str_replace(chr(0x1d), '%', $str);
$str = str_replace('%%', '%', $str);
}
return $str;
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Class DecoderResult
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version};
use chillerlan\QRCode\Data\QRMatrix;
use function property_exists;
/**
* Encapsulates the result of decoding a matrix of bits. This typically
* applies to 2D barcode formats. For now, it contains the raw bytes obtained
* as well as a String interpretation of those bytes, if applicable.
*
* @property \chillerlan\QRCode\Common\BitBuffer $rawBytes
* @property string $data
* @property \chillerlan\QRCode\Common\Version $version
* @property \chillerlan\QRCode\Common\EccLevel $eccLevel
* @property \chillerlan\QRCode\Common\MaskPattern $maskPattern
* @property int $structuredAppendParity
* @property int $structuredAppendSequence
*/
final class DecoderResult{
private BitBuffer $rawBytes;
private Version $version;
private EccLevel $eccLevel;
private MaskPattern $maskPattern;
private string $data = '';
private int $structuredAppendParity = -1;
private int $structuredAppendSequence = -1;
/**
* DecoderResult constructor.
*/
public function __construct(iterable $properties = null){
if(!empty($properties)){
foreach($properties as $property => $value){
if(!property_exists($this, $property)){
continue;
}
$this->{$property} = $value;
}
}
}
/**
* @return mixed|null
*/
public function __get(string $property){
if(property_exists($this, $property)){
return $this->{$property};
}
return null;
}
/**
*
*/
public function __toString():string{
return $this->data;
}
/**
*
*/
public function hasStructuredAppend():bool{
return $this->structuredAppendParity >= 0 && $this->structuredAppendSequence >= 0;
}
/**
* Returns a QRMatrix instance with the settings and data of the reader result
*/
public function getQRMatrix():QRMatrix{
return (new QRMatrix($this->version, $this->eccLevel))
->initFunctionalPatterns()
->writeCodewords($this->rawBytes)
->setFormatInfo($this->maskPattern)
->mask($this->maskPattern)
;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeDecoderException
*
* @created 01.12.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeDecoderException extends QRCodeException{
}

View File

@@ -0,0 +1,313 @@
<?php
/**
* Class ReedSolomonDecoder
*
* @created 24.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Decoder;
use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version};
use function array_fill, array_reverse, count;
/**
* Implements Reed-Solomon decoding
*
* The algorithm will not be explained here, but the following references were helpful
* in creating this implementation:
*
* - Bruce Maggs "Decoding Reed-Solomon Codes" (see discussion of Forney's Formula)
* http://www.cs.cmu.edu/afs/cs.cmu.edu/project/pscico-guyb/realworld/www/rs_decode.ps
* - J.I. Hall. "Chapter 5. Generalized Reed-Solomon Codes" (see discussion of Euclidean algorithm)
* https://users.math.msu.edu/users/halljo/classes/codenotes/GRS.pdf
*
* Much credit is due to William Rucklidge since portions of this code are an indirect
* port of his C++ Reed-Solomon implementation.
*
* @author Sean Owen
* @author William Rucklidge
* @author sanfordsquires
*/
final class ReedSolomonDecoder{
private Version $version;
private EccLevel $eccLevel;
/**
* ReedSolomonDecoder constructor
*/
public function __construct(Version $version, EccLevel $eccLevel){
$this->version = $version;
$this->eccLevel = $eccLevel;
}
/**
* Error-correct and copy data blocks together into a stream of bytes
*/
public function decode(array $rawCodewords):BitBuffer{
$dataBlocks = $this->deinterleaveRawBytes($rawCodewords);
$dataBytes = [];
foreach($dataBlocks as [$numDataCodewords, $codewordBytes]){
$corrected = $this->correctErrors($codewordBytes, $numDataCodewords);
for($i = 0; $i < $numDataCodewords; $i++){
$dataBytes[] = $corrected[$i];
}
}
return new BitBuffer($dataBytes);
}
/**
* When QR Codes use multiple data blocks, they are actually interleaved.
* That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
* method will separate the data into original blocks.
*
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function deinterleaveRawBytes(array $rawCodewords):array{
// Figure out the number and size of data blocks used by this version and
// error correction level
[$numEccCodewords, $eccBlocks] = $this->version->getRSBlocks($this->eccLevel);
// Now establish DataBlocks of the appropriate size and number of data codewords
$result = [];//new DataBlock[$totalBlocks];
$numResultBlocks = 0;
foreach($eccBlocks as [$numEccBlocks, $eccPerBlock]){
for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
$result[$numResultBlocks] = [$eccPerBlock, array_fill(0, ($numEccCodewords + $eccPerBlock), 0)];
}
}
// All blocks have the same amount of data, except that the last n
// (where n may be 0) have 1 more byte. Figure out where these start.
/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
$shorterBlocksTotalCodewords = count($result[0][1]);
$longerBlocksStartAt = (count($result) - 1);
while($longerBlocksStartAt >= 0){
$numCodewords = count($result[$longerBlocksStartAt][1]);
if($numCodewords == $shorterBlocksTotalCodewords){
break;
}
$longerBlocksStartAt--;
}
$longerBlocksStartAt++;
$shorterBlocksNumDataCodewords = ($shorterBlocksTotalCodewords - $numEccCodewords);
// The last elements of result may be 1 element longer;
// first fill out as many elements as all of them have
$rawCodewordsOffset = 0;
for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
for($j = 0; $j < $numResultBlocks; $j++){
$result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
}
}
// Fill out the last data block in the longer ones
for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
$result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
}
// Now add in error correction blocks
/** @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset */
$max = count($result[0][1]);
for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
for($j = 0; $j < $numResultBlocks; $j++){
$iOffset = ($j < $longerBlocksStartAt) ? $i : ($i + 1);
$result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
}
}
// DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
return $result;
}
/**
* Given data and error-correction codewords received, possibly corrupted by errors, attempts to
* correct the errors in-place using Reed-Solomon error correction.
*/
private function correctErrors(array $codewordBytes, int $numDataCodewords):array{
// First read into an array of ints
$codewordsInts = [];
foreach($codewordBytes as $codewordByte){
$codewordsInts[] = ($codewordByte & 0xFF);
}
$decoded = $this->decodeWords($codewordsInts, (count($codewordBytes) - $numDataCodewords));
// Copy back into array of bytes -- only need to worry about the bytes that were data
// We don't care about errors in the error-correction codewords
for($i = 0; $i < $numDataCodewords; $i++){
$codewordBytes[$i] = $decoded[$i];
}
return $codewordBytes;
}
/**
* Decodes given set of received codewords, which include both data and error-correction
* codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place,
* in the input.
*
* @param array $received data and error-correction codewords
* @param int $numEccCodewords number of error-correction codewords available
*
* @return int[]
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException if decoding fails for any reason
*/
private function decodeWords(array $received, int $numEccCodewords):array{
$poly = new GenericGFPoly($received);
$syndromeCoefficients = [];
$error = false;
for($i = 0; $i < $numEccCodewords; $i++){
$syndromeCoefficients[$i] = $poly->evaluateAt(GF256::exp($i));
if($syndromeCoefficients[$i] !== 0){
$error = true;
}
}
if(!$error){
return $received;
}
[$sigma, $omega] = $this->runEuclideanAlgorithm(
GF256::buildMonomial($numEccCodewords, 1),
new GenericGFPoly(array_reverse($syndromeCoefficients)),
$numEccCodewords
);
$errorLocations = $this->findErrorLocations($sigma);
$errorMagnitudes = $this->findErrorMagnitudes($omega, $errorLocations);
$errorLocationsCount = count($errorLocations);
$receivedCount = count($received);
for($i = 0; $i < $errorLocationsCount; $i++){
$position = ($receivedCount - 1 - GF256::log($errorLocations[$i]));
if($position < 0){
throw new QRCodeDecoderException('Bad error location');
}
$received[$position] ^= $errorMagnitudes[$i];
}
return $received;
}
/**
* @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega]
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $z):array{
// Assume a's degree is >= b's
if($a->getDegree() < $b->getDegree()){
$temp = $a;
$a = $b;
$b = $temp;
}
$rLast = $a;
$r = $b;
$tLast = new GenericGFPoly([0]);
$t = new GenericGFPoly([1]);
// Run Euclidean algorithm until r's degree is less than z/2
while((2 * $r->getDegree()) >= $z){
$rLastLast = $rLast;
$tLastLast = $tLast;
$rLast = $r;
$tLast = $t;
// Divide rLastLast by rLast, with quotient in q and remainder in r
[$q, $r] = $rLastLast->divide($rLast);
$t = $q->multiply($tLast)->addOrSubtract($tLastLast);
if($r->getDegree() >= $rLast->getDegree()){
throw new QRCodeDecoderException('Division algorithm failed to reduce polynomial?');
}
}
$sigmaTildeAtZero = $t->getCoefficient(0);
if($sigmaTildeAtZero === 0){
throw new QRCodeDecoderException('sigmaTilde(0) was zero');
}
$inverse = GF256::inverse($sigmaTildeAtZero);
return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)];
}
/**
* @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
*/
private function findErrorLocations(GenericGFPoly $errorLocator):array{
// This is a direct application of Chien's search
$numErrors = $errorLocator->getDegree();
if($numErrors === 1){ // shortcut
return [$errorLocator->getCoefficient(1)];
}
$result = array_fill(0, $numErrors, 0);
$e = 0;
for($i = 1; $i < 256 && $e < $numErrors; $i++){
if($errorLocator->evaluateAt($i) === 0){
$result[$e] = GF256::inverse($i);
$e++;
}
}
if($e !== $numErrors){
throw new QRCodeDecoderException('Error locator degree does not match number of roots');
}
return $result;
}
/**
*
*/
private function findErrorMagnitudes(GenericGFPoly $errorEvaluator, array $errorLocations):array{
// This is directly applying Forney's Formula
$s = count($errorLocations);
$result = [];
for($i = 0; $i < $s; $i++){
$xiInverse = GF256::inverse($errorLocations[$i]);
$denominator = 1;
for($j = 0; $j < $s; $j++){
if($i !== $j){
# $denominator = GF256::multiply($denominator, GF256::addOrSubtract(1, GF256::multiply($errorLocations[$j], $xiInverse)));
// Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.
// Below is a funny-looking workaround from Steven Parkes
$term = GF256::multiply($errorLocations[$j], $xiInverse);
$denominator = GF256::multiply($denominator, ((($term & 0x1) === 0) ? ($term | 1) : ($term & ~1)));
}
}
$result[$i] = GF256::multiply($errorEvaluator->evaluateAt($xiInverse), GF256::inverse($denominator));
}
return $result;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Class AlignmentPattern
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
/**
* Encapsulates an alignment pattern, which are the smaller square patterns found in
* all but the simplest QR Codes.
*
* @author Sean Owen
*/
final class AlignmentPattern extends ResultPoint{
/**
* Combines this object's current estimate of a finder pattern position and module size
* with a new estimate. It returns a new FinderPattern containing an average of the two.
*/
public function combineEstimate(float $i, float $j, float $newModuleSize):self{
return new self(
(($this->x + $j) / 2.0),
(($this->y + $i) / 2.0),
(($this->estimatedModuleSize + $newModuleSize) / 2.0)
);
}
}

View File

@@ -0,0 +1,283 @@
<?php
/**
* Class AlignmentPatternFinder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Decoder\BitMatrix;
use function abs, count;
/**
* This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder
* patterns but are smaller and appear at regular intervals throughout the image.
*
* At the moment this only looks for the bottom-right alignment pattern.
*
* This is mostly a simplified copy of FinderPatternFinder. It is copied,
* pasted and stripped down here for maximum performance but does unfortunately duplicate
* some code.
*
* This class is thread-safe but not reentrant. Each thread must allocate its own object.
*
* @author Sean Owen
*/
final class AlignmentPatternFinder{
private BitMatrix $matrix;
private float $moduleSize;
/** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */
private array $possibleCenters;
/**
* Creates a finder that will look in a portion of the whole image.
*
* @param \chillerlan\QRCode\Decoder\BitMatrix $matrix image to search
* @param float $moduleSize estimated module size so far
*/
public function __construct(BitMatrix $matrix, float $moduleSize){
$this->matrix = $matrix;
$this->moduleSize = $moduleSize;
$this->possibleCenters = [];
}
/**
* This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since
* it's pretty performance-critical and so is written to be fast foremost.
*
* @param int $startX left column from which to start searching
* @param int $startY top row from which to start searching
* @param int $width width of region to search
* @param int $height height of region to search
*
* @return \chillerlan\QRCode\Detector\AlignmentPattern|null
*/
public function find(int $startX, int $startY, int $width, int $height):?AlignmentPattern{
$maxJ = ($startX + $width);
$middleI = ($startY + ($height / 2));
$stateCount = [];
// We are looking for black/white/black modules in 1:1:1 ratio;
// this tracks the number of black/white/black modules seen so far
for($iGen = 0; $iGen < $height; $iGen++){
// Search from middle outwards
$i = (int)($middleI + ((($iGen & 0x01) === 0) ? ($iGen + 1) / 2 : -(($iGen + 1) / 2)));
$stateCount[0] = 0;
$stateCount[1] = 0;
$stateCount[2] = 0;
$j = $startX;
// Burn off leading white pixels before anything else; if we start in the middle of
// a white run, it doesn't make sense to count its length, since we don't know if the
// white run continued to the left of the start point
while($j < $maxJ && !$this->matrix->check($j, $i)){
$j++;
}
$currentState = 0;
while($j < $maxJ){
if($this->matrix->check($j, $i)){
// Black pixel
if($currentState === 1){ // Counting black pixels
$stateCount[$currentState]++;
}
// Counting white pixels
else{
// A winner?
if($currentState === 2){
// Yes
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
if($confirmed !== null){
return $confirmed;
}
}
$stateCount[0] = $stateCount[2];
$stateCount[1] = 1;
$stateCount[2] = 0;
$currentState = 1;
}
else{
$stateCount[++$currentState]++;
}
}
}
// White pixel
else{
// Counting black pixels
if($currentState === 1){
$currentState++;
}
$stateCount[$currentState]++;
}
$j++;
}
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ);
if($confirmed !== null){
return $confirmed;
}
}
}
// Hmm, nothing we saw was observed and confirmed twice. If we had
// any guess at all, return it.
if(count($this->possibleCenters)){
return $this->possibleCenters[0];
}
return null;
}
/**
* @param int[] $stateCount count of black/white/black pixels just read
*
* @return bool true if the proportions of the counts is close enough to the 1/1/1 ratios
* used by alignment patterns to be considered a match
*/
private function foundPatternCross(array $stateCount):bool{
$maxVariance = ($this->moduleSize / 2.0);
for($i = 0; $i < 3; $i++){
if(abs($this->moduleSize - $stateCount[$i]) >= $maxVariance){
return false;
}
}
return true;
}
/**
* This is called when a horizontal scan finds a possible alignment pattern. It will
* cross-check with a vertical scan, and if successful, will see if this pattern had been
* found on a previous horizontal scan. If so, we consider it confirmed and conclude we have
* found the alignment pattern.
*
* @param int[] $stateCount reading state module counts from horizontal scan
* @param int $i row where alignment pattern may be found
* @param int $j end of possible alignment pattern in row
*
* @return \chillerlan\QRCode\Detector\AlignmentPattern|null if we have found the same pattern twice, or null if not
*/
private function handlePossibleCenter(array $stateCount, int $i, int $j):?AlignmentPattern{
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2]);
$centerJ = $this->centerFromEnd($stateCount, $j);
$centerI = $this->crossCheckVertical($i, (int)$centerJ, (2 * $stateCount[1]), $stateCountTotal);
if($centerI !== null){
$estimatedModuleSize = (($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0);
foreach($this->possibleCenters as $center){
// Look for about the same center and module size:
if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
}
}
// Hadn't found this before; save it
$point = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize);
$this->possibleCenters[] = $point;
}
return null;
}
/**
* Given a count of black/white/black pixels just seen and an end position,
* figures the location of the center of this black/white/black run.
*
* @param int[] $stateCount
* @param int $end
*
* @return float
*/
private function centerFromEnd(array $stateCount, int $end):float{
return (float)(($end - $stateCount[2]) - $stateCount[1] / 2);
}
/**
* After a horizontal scan finds a potential alignment pattern, this method
* "cross-checks" by scanning down vertically through the center of the possible
* alignment pattern to see if the same proportion is detected.
*
* @param int $startI row where an alignment pattern was detected
* @param int $centerJ center of the section that appears to cross an alignment pattern
* @param int $maxCount maximum reasonable number of modules that should be
* observed in any reading state, based on the results of the horizontal scan
* @param int $originalStateCountTotal
*
* @return float|null vertical center of alignment pattern, or null if not found
*/
private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
$maxI = $this->matrix->getSize();
$stateCount = [];
$stateCount[0] = 0;
$stateCount[1] = 0;
$stateCount[2] = 0;
// Start counting up from center
$i = $startI;
while($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$i--;
}
// If already too many modules in this state or ran off the edge:
if($i < 0 || $stateCount[1] > $maxCount){
return null;
}
while($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount){
$stateCount[0]++;
$i--;
}
if($stateCount[0] > $maxCount){
return null;
}
// Now also count down from center
$i = ($startI + 1);
while($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$i++;
}
if($i == $maxI || $stateCount[1] > $maxCount){
return null;
}
while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[2] <= $maxCount){
$stateCount[2]++;
$i++;
}
if($stateCount[2] > $maxCount){
return null;
}
if((5 * abs(($stateCount[0] + $stateCount[1] + $stateCount[2]) - $originalStateCountTotal)) >= (2 * $originalStateCountTotal)){
return null;
}
if(!$this->foundPatternCross($stateCount)){
return null;
}
return $this->centerFromEnd($stateCount, $i);
}
}

View File

@@ -0,0 +1,350 @@
<?php
/**
* Class Detector
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Common\{LuminanceSourceInterface, Version};
use chillerlan\QRCode\Decoder\{Binarizer, BitMatrix};
use function abs, intdiv, is_nan, max, min, round;
use const NAN;
/**
* Encapsulates logic that can detect a QR Code in an image, even if the QR Code
* is rotated or skewed, or partially obscured.
*
* @author Sean Owen
*/
final class Detector{
private BitMatrix $matrix;
/**
* Detector constructor.
*/
public function __construct(LuminanceSourceInterface $source){
$this->matrix = (new Binarizer($source))->getBlackMatrix();
}
/**
* Detects a QR Code in an image.
*/
public function detect():BitMatrix{
[$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->matrix))->find();
$moduleSize = $this->calculateModuleSize($topLeft, $topRight, $bottomLeft);
$dimension = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize);
$provisionalVersion = new Version(intdiv(($dimension - 17), 4));
$alignmentPattern = null;
// Anything above version 1 has an alignment pattern
if(!empty($provisionalVersion->getAlignmentPattern())){
// Guess where a "bottom right" finder pattern would have been
$bottomRightX = ($topRight->getX() - $topLeft->getX() + $bottomLeft->getX());
$bottomRightY = ($topRight->getY() - $topLeft->getY() + $bottomLeft->getY());
// Estimate that alignment pattern is closer by 3 modules
// from "bottom right" to known top left location
$correctionToTopLeft = (1.0 - 3.0 / (float)($provisionalVersion->getDimension() - 7));
$estAlignmentX = (int)($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX()));
$estAlignmentY = (int)($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY()));
// Kind of arbitrary -- expand search radius before giving up
for($i = 4; $i <= 16; $i <<= 1){//??????????
$alignmentPattern = $this->findAlignmentInRegion($moduleSize, $estAlignmentX, $estAlignmentY, (float)$i);
if($alignmentPattern !== null){
break;
}
}
// If we didn't find alignment pattern... well try anyway without it
}
$transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern);
return (new GridSampler)->sampleGrid($this->matrix, $dimension, $transform);
}
/**
* Computes an average estimated module size based on estimated derived from the positions
* of the three finder patterns.
*
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException
*/
private function calculateModuleSize(FinderPattern $topLeft, FinderPattern $topRight, FinderPattern $bottomLeft):float{
// Take the average
$moduleSize = ((
$this->calculateModuleSizeOneWay($topLeft, $topRight) +
$this->calculateModuleSizeOneWay($topLeft, $bottomLeft)
) / 2.0);
if($moduleSize < 1.0){
throw new QRCodeDetectorException('module size < 1.0');
}
return $moduleSize;
}
/**
* Estimates module size based on two finder patterns -- it uses
* #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int) to figure the
* width of each, measuring along the axis between their centers.
*/
private function calculateModuleSizeOneWay(FinderPattern $a, FinderPattern $b):float{
$moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays($a->getX(), $a->getY(), $b->getX(), $b->getY());
$moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays($b->getX(), $b->getY(), $a->getX(), $a->getY());
if(is_nan($moduleSizeEst1)){
return ($moduleSizeEst2 / 7.0);
}
if(is_nan($moduleSizeEst2)){
return ($moduleSizeEst1 / 7.0);
}
// Average them, and divide by 7 since we've counted the width of 3 black modules,
// and 1 white and 1 black module on either side. Ergo, divide sum by 14.
return (($moduleSizeEst1 + $moduleSizeEst2) / 14.0);
}
/**
* See #sizeOfBlackWhiteBlackRun(int, int, int, int); computes the total width of
* a finder pattern by looking for a black-white-black run from the center in the direction
* of another po$(another finder pattern center), and in the opposite direction too.
*
* @noinspection DuplicatedCode
*/
private function sizeOfBlackWhiteBlackRunBothWays(float $fromX, float $fromY, float $toX, float $toY):float{
$result = $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$toX, (int)$toY);
$dimension = $this->matrix->getSize();
// Now count other way -- don't run off image though of course
$scale = 1.0;
$otherToX = ($fromX - ($toX - $fromX));
if($otherToX < 0){
$scale = ($fromX / ($fromX - $otherToX));
$otherToX = 0;
}
elseif($otherToX >= $dimension){
$scale = (($dimension - 1 - $fromX) / ($otherToX - $fromX));
$otherToX = ($dimension - 1);
}
$otherToY = (int)($fromY - ($toY - $fromY) * $scale);
$scale = 1.0;
if($otherToY < 0){
$scale = ($fromY / ($fromY - $otherToY));
$otherToY = 0;
}
elseif($otherToY >= $dimension){
$scale = (($dimension - 1 - $fromY) / ($otherToY - $fromY));
$otherToY = ($dimension - 1);
}
$otherToX = (int)($fromX + ($otherToX - $fromX) * $scale);
$result += $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, $otherToX, $otherToY);
// Middle pixel is double-counted this way; subtract 1
return ($result - 1.0);
}
/**
* This method traces a line from a po$in the image, in the direction towards another point.
* It begins in a black region, and keeps going until it finds white, then black, then white again.
* It reports the distance from the start to this point.
*
* This is used when figuring out how wide a finder pattern is, when the finder pattern
* may be skewed or rotated.
*/
private function sizeOfBlackWhiteBlackRun(int $fromX, int $fromY, int $toX, int $toY):float{
// Mild variant of Bresenham's algorithm;
// @see https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
$steep = abs($toY - $fromY) > abs($toX - $fromX);
if($steep){
$temp = $fromX;
$fromX = $fromY;
$fromY = $temp;
$temp = $toX;
$toX = $toY;
$toY = $temp;
}
$dx = abs($toX - $fromX);
$dy = abs($toY - $fromY);
$error = (-$dx / 2);
$xstep = (($fromX < $toX) ? 1 : -1);
$ystep = (($fromY < $toY) ? 1 : -1);
// In black pixels, looking for white, first or second time.
$state = 0;
// Loop up until x == toX, but not beyond
$xLimit = ($toX + $xstep);
for($x = $fromX, $y = $fromY; $x !== $xLimit; $x += $xstep){
$realX = ($steep) ? $y : $x;
$realY = ($steep) ? $x : $y;
// Does current pixel mean we have moved white to black or vice versa?
// Scanning black in state 0,2 and white in state 1, so if we find the wrong
// color, advance to next state or end if we are in state 2 already
if(($state === 1) === $this->matrix->check($realX, $realY)){
if($state === 2){
return FinderPattern::distance($x, $y, $fromX, $fromY);
}
$state++;
}
$error += $dy;
if($error > 0){
if($y === $toY){
break;
}
$y += $ystep;
$error -= $dx;
}
}
// Found black-white-black; give the benefit of the doubt that the next pixel outside the image
// is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a
// small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.
if($state === 2){
return FinderPattern::distance(($toX + $xstep), $toY, $fromX, $fromY);
}
// else we didn't find even black-white-black; no estimate is really possible
return NAN;
}
/**
* Computes the dimension (number of modules on a size) of the QR Code based on the position
* of the finder patterns and estimated module size.
*
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException
*/
private function computeDimension(FinderPattern $nw, FinderPattern $ne, FinderPattern $sw, float $size):int{
$tltrCentersDimension = (int)round($nw->getDistance($ne) / $size);
$tlblCentersDimension = (int)round($nw->getDistance($sw) / $size);
$dimension = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7);
switch($dimension % 4){
case 0:
$dimension++;
break;
// 1? do nothing
case 2:
$dimension--;
break;
case 3:
throw new QRCodeDetectorException('estimated dimension: '.$dimension);
}
if(($dimension % 4) !== 1){
throw new QRCodeDetectorException('dimension mod 4 is not 1');
}
return $dimension;
}
/**
* Attempts to locate an alignment pattern in a limited region of the image, which is
* guessed to contain it.
*
* @param float $overallEstModuleSize estimated module size so far
* @param int $estAlignmentX x coordinate of center of area probably containing alignment pattern
* @param int $estAlignmentY y coordinate of above
* @param float $allowanceFactor number of pixels in all directions to search from the center
*
* @return \chillerlan\QRCode\Detector\AlignmentPattern|null if found, or null otherwise
*/
private function findAlignmentInRegion(
float $overallEstModuleSize,
int $estAlignmentX,
int $estAlignmentY,
float $allowanceFactor
):?AlignmentPattern{
// Look for an alignment pattern (3 modules in size) around where it should be
$dimension = $this->matrix->getSize();
$allowance = (int)($allowanceFactor * $overallEstModuleSize);
$alignmentAreaLeftX = max(0, ($estAlignmentX - $allowance));
$alignmentAreaRightX = min(($dimension - 1), ($estAlignmentX + $allowance));
if(($alignmentAreaRightX - $alignmentAreaLeftX) < ($overallEstModuleSize * 3)){
return null;
}
$alignmentAreaTopY = max(0, ($estAlignmentY - $allowance));
$alignmentAreaBottomY = min(($dimension - 1), ($estAlignmentY + $allowance));
if(($alignmentAreaBottomY - $alignmentAreaTopY) < ($overallEstModuleSize * 3)){
return null;
}
return (new AlignmentPatternFinder($this->matrix, $overallEstModuleSize))->find(
$alignmentAreaLeftX,
$alignmentAreaTopY,
($alignmentAreaRightX - $alignmentAreaLeftX),
($alignmentAreaBottomY - $alignmentAreaTopY),
);
}
/**
*
*/
private function createTransform(
FinderPattern $nw,
FinderPattern $ne,
FinderPattern $sw,
int $size,
AlignmentPattern $ap = null
):PerspectiveTransform{
$dimMinusThree = ($size - 3.5);
if($ap instanceof AlignmentPattern){
$bottomRightX = $ap->getX();
$bottomRightY = $ap->getY();
$sourceBottomRightX = ($dimMinusThree - 3.0);
$sourceBottomRightY = $sourceBottomRightX;
}
else{
// Don't have an alignment pattern, just make up the bottom-right point
$bottomRightX = ($ne->getX() - $nw->getX() + $sw->getX());
$bottomRightY = ($ne->getY() - $nw->getY() + $sw->getY());
$sourceBottomRightX = $dimMinusThree;
$sourceBottomRightY = $dimMinusThree;
}
return (new PerspectiveTransform)->quadrilateralToQuadrilateral(
3.5,
3.5,
$dimMinusThree,
3.5,
$sourceBottomRightX,
$sourceBottomRightY,
3.5,
$dimMinusThree,
$nw->getX(),
$nw->getY(),
$ne->getX(),
$ne->getY(),
$bottomRightX,
$bottomRightY,
$sw->getX(),
$sw->getY()
);
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* Class FinderPattern
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use function sqrt;
/**
* Encapsulates a finder pattern, which are the three square patterns found in
* the corners of QR Codes. It also encapsulates a count of similar finder patterns,
* as a convenience to the finder's bookkeeping.
*
* @author Sean Owen
*/
final class FinderPattern extends ResultPoint{
private int $count;
/**
*
*/
public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = null){
parent::__construct($posX, $posY, $estimatedModuleSize);
$this->count = ($count ?? 1);
}
/**
*
*/
public function getCount():int{
return $this->count;
}
/**
* @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern
*
* @return float distance between two points
*/
public function getDistance(FinderPattern $b):float{
return self::distance($this->x, $this->y, $b->x, $b->y);
}
/**
* Get square of distance between a and b.
*/
public function getSquaredDistance(FinderPattern $b):float{
return self::squaredDistance($this->x, $this->y, $b->x, $b->y);
}
/**
* Combines this object's current estimate of a finder pattern position and module size
* with a new estimate. It returns a new FinderPattern containing a weighted average
* based on count.
*/
public function combineEstimate(float $i, float $j, float $newModuleSize):self{
$combinedCount = ($this->count + 1);
return new self(
($this->count * $this->x + $j) / $combinedCount,
($this->count * $this->y + $i) / $combinedCount,
($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount,
$combinedCount
);
}
/**
*
*/
private static function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{
$xDiff = ($aX - $bX);
$yDiff = ($aY - $bY);
return ($xDiff * $xDiff + $yDiff * $yDiff);
}
/**
*
*/
public static function distance(float $aX, float $aY, float $bX, float $bY):float{
return sqrt(self::squaredDistance($aX, $aY, $bX, $bY));
}
}

View File

@@ -0,0 +1,770 @@
<?php
/**
* Class FinderPatternFinder
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*
* @phan-file-suppress PhanTypePossiblyInvalidDimOffset
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Decoder\BitMatrix;
use function abs, count, intdiv, usort;
use const PHP_FLOAT_MAX;
/**
* This class attempts to find finder patterns in a QR Code. Finder patterns are the square
* markers at three corners of a QR Code.
*
* This class is thread-safe but not reentrant. Each thread must allocate its own object.
*
* @author Sean Owen
*/
final class FinderPatternFinder{
private const MIN_SKIP = 2;
private const MAX_MODULES = 177; // 1 pixel/module times 3 modules/center
private const CENTER_QUORUM = 2; // support up to version 10 for mobile clients
private BitMatrix $matrix;
/** @var \chillerlan\QRCode\Detector\FinderPattern[] */
private array $possibleCenters;
private bool $hasSkipped = false;
/**
* Creates a finder that will search the image for three finder patterns.
*
* @param BitMatrix $matrix image to search
*/
public function __construct(BitMatrix $matrix){
$this->matrix = $matrix;
$this->possibleCenters = [];
}
/**
* @return \chillerlan\QRCode\Detector\FinderPattern[]
*/
public function find():array{
$dimension = $this->matrix->getSize();
// We are looking for black/white/black/white/black modules in
// 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
// Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
// image, and then account for the center being 3 modules in size. This gives the smallest
// number of pixels the center could be, so skip this often.
$iSkip = intdiv((3 * $dimension), (4 * self::MAX_MODULES));
if($iSkip < self::MIN_SKIP){
$iSkip = self::MIN_SKIP;
}
$done = false;
for($i = ($iSkip - 1); ($i < $dimension) && !$done; $i += $iSkip){
// Get a row of black/white values
$stateCount = $this->getCrossCheckStateCount();
$currentState = 0;
for($j = 0; $j < $dimension; $j++){
// Black pixel
if($this->matrix->check($j, $i)){
// Counting white pixels
if(($currentState & 1) === 1){
$currentState++;
}
$stateCount[$currentState]++;
}
// White pixel
else{
// Counting black pixels
if(($currentState & 1) === 0){
// A winner?
if($currentState === 4){
// Yes
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
if($confirmed){
// Start examining every other line. Checking each line turned out to be too
// expensive and didn't improve performance.
$iSkip = 3;
if($this->hasSkipped){
$done = $this->haveMultiplyConfirmedCenters();
}
else{
$rowSkip = $this->findRowSkip();
if($rowSkip > $stateCount[2]){
// Skip rows between row of lower confirmed center
// and top of presumed third confirmed center
// but back up a bit to get a full chance of detecting
// it, entire width of center of finder pattern
// Skip by rowSkip, but back off by $stateCount[2] (size of last center
// of pattern we saw) to be conservative, and also back off by iSkip which
// is about to be re-added
$i += ($rowSkip - $stateCount[2] - $iSkip);
$j = ($dimension - 1);
}
}
}
else{
$stateCount = $this->doShiftCounts2($stateCount);
$currentState = 3;
continue;
}
// Clear state to start looking again
$currentState = 0;
$stateCount = $this->getCrossCheckStateCount();
}
// No, shift counts back by two
else{
$stateCount = $this->doShiftCounts2($stateCount);
$currentState = 3;
}
}
else{
$stateCount[++$currentState]++;
}
}
// Counting white pixels
else{
$stateCount[$currentState]++;
}
}
}
if($this->foundPatternCross($stateCount)){
$confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
if($confirmed){
$iSkip = $stateCount[0];
if($this->hasSkipped){
// Found a third one
$done = $this->haveMultiplyConfirmedCenters();
}
}
}
}
return $this->orderBestPatterns($this->selectBestPatterns());
}
/**
* @return int[]
*/
private function getCrossCheckStateCount():array{
return [0, 0, 0, 0, 0];
}
/**
* @param int[] $stateCount
*
* @return int[]
*/
private function doShiftCounts2(array $stateCount):array{
$stateCount[0] = $stateCount[2];
$stateCount[1] = $stateCount[3];
$stateCount[2] = $stateCount[4];
$stateCount[3] = 1;
$stateCount[4] = 0;
return $stateCount;
}
/**
* Given a count of black/white/black/white/black pixels just seen and an end position,
* figures the location of the center of this run.
*
* @param int[] $stateCount
*/
private function centerFromEnd(array $stateCount, int $end):float{
return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2);
}
/**
* @param int[] $stateCount
*/
private function foundPatternCross(array $stateCount):bool{
// Allow less than 50% variance from 1-1-3-1-1 proportions
return $this->foundPatternVariance($stateCount, 2.0);
}
/**
* @param int[] $stateCount
*/
private function foundPatternDiagonal(array $stateCount):bool{
// Allow less than 75% variance from 1-1-3-1-1 proportions
return $this->foundPatternVariance($stateCount, 1.333);
}
/**
* @param int[] $stateCount count of black/white/black/white/black pixels just read
*
* @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
* used by finder patterns to be considered a match
*/
private function foundPatternVariance(array $stateCount, float $variance):bool{
$totalModuleSize = 0;
for($i = 0; $i < 5; $i++){
$count = $stateCount[$i];
if($count === 0){
return false;
}
$totalModuleSize += $count;
}
if($totalModuleSize < 7){
return false;
}
$moduleSize = ($totalModuleSize / 7.0);
$maxVariance = ($moduleSize / $variance);
return
abs($moduleSize - $stateCount[0]) < $maxVariance
&& abs($moduleSize - $stateCount[1]) < $maxVariance
&& abs(3.0 * $moduleSize - $stateCount[2]) < (3 * $maxVariance)
&& abs($moduleSize - $stateCount[3]) < $maxVariance
&& abs($moduleSize - $stateCount[4]) < $maxVariance;
}
/**
* After a vertical and horizontal scan finds a potential finder pattern, this method
* "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
* finder pattern to see if the same proportion is detected.
*
* @param int $centerI row where a finder pattern was detected
* @param int $centerJ center of the section that appears to cross a finder pattern
*
* @return bool true if proportions are withing expected limits
*/
private function crossCheckDiagonal(int $centerI, int $centerJ):bool{
$stateCount = $this->getCrossCheckStateCount();
// Start counting up, left from center finding black center mass
$i = 0;
while($centerI >= $i && $centerJ >= $i && $this->matrix->check(($centerJ - $i), ($centerI - $i))){
$stateCount[2]++;
$i++;
}
if($stateCount[2] === 0){
return false;
}
// Continue up, left finding white space
while($centerI >= $i && $centerJ >= $i && !$this->matrix->check(($centerJ - $i), ($centerI - $i))){
$stateCount[1]++;
$i++;
}
if($stateCount[1] === 0){
return false;
}
// Continue up, left finding black border
while($centerI >= $i && $centerJ >= $i && $this->matrix->check(($centerJ - $i), ($centerI - $i))){
$stateCount[0]++;
$i++;
}
if($stateCount[0] === 0){
return false;
}
$dimension = $this->matrix->getSize();
// Now also count down, right from center
$i = 1;
while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && $this->matrix->check(($centerJ + $i), ($centerI + $i))){
$stateCount[2]++;
$i++;
}
while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && !$this->matrix->check(($centerJ + $i), ($centerI + $i))){
$stateCount[3]++;
$i++;
}
if($stateCount[3] === 0){
return false;
}
while(($centerI + $i) < $dimension && ($centerJ + $i) < $dimension && $this->matrix->check(($centerJ + $i), ($centerI + $i))){
$stateCount[4]++;
$i++;
}
if($stateCount[4] === 0){
return false;
}
return $this->foundPatternDiagonal($stateCount);
}
/**
* After a horizontal scan finds a potential finder pattern, this method
* "cross-checks" by scanning down vertically through the center of the possible
* finder pattern to see if the same proportion is detected.
*
* @param int $startI row where a finder pattern was detected
* @param int $centerJ center of the section that appears to cross a finder pattern
* @param int $maxCount maximum reasonable number of modules that should be
* observed in any reading state, based on the results of the horizontal scan
* @param int $originalStateCountTotal
*
* @return float|null vertical center of finder pattern, or null if not found
* @noinspection DuplicatedCode
*/
private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
$maxI = $this->matrix->getSize();
$stateCount = $this->getCrossCheckStateCount();
// Start counting up from center
$i = $startI;
while($i >= 0 && $this->matrix->check($centerJ, $i)){
$stateCount[2]++;
$i--;
}
if($i < 0){
return null;
}
while($i >= 0 && !$this->matrix->check($centerJ, $i) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$i--;
}
// If already too many modules in this state or ran off the edge:
if($i < 0 || $stateCount[1] > $maxCount){
return null;
}
while($i >= 0 && $this->matrix->check($centerJ, $i) && $stateCount[0] <= $maxCount){
$stateCount[0]++;
$i--;
}
if($stateCount[0] > $maxCount){
return null;
}
// Now also count down from center
$i = ($startI + 1);
while($i < $maxI && $this->matrix->check($centerJ, $i)){
$stateCount[2]++;
$i++;
}
if($i === $maxI){
return null;
}
while($i < $maxI && !$this->matrix->check($centerJ, $i) && $stateCount[3] < $maxCount){
$stateCount[3]++;
$i++;
}
if($i === $maxI || $stateCount[3] >= $maxCount){
return null;
}
while($i < $maxI && $this->matrix->check($centerJ, $i) && $stateCount[4] < $maxCount){
$stateCount[4]++;
$i++;
}
if($stateCount[4] >= $maxCount){
return null;
}
// If we found a finder-pattern-like section, but its size is more than 40% different from
// the original, assume it's a false positive
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]);
if((5 * abs($stateCountTotal - $originalStateCountTotal)) >= (2 * $originalStateCountTotal)){
return null;
}
if(!$this->foundPatternCross($stateCount)){
return null;
}
return $this->centerFromEnd($stateCount, $i);
}
/**
* Like #crossCheckVertical(int, int, int, int), and in fact is basically identical,
* except it reads horizontally instead of vertically. This is used to cross-cross
* check a vertical cross-check and locate the real center of the alignment pattern.
* @noinspection DuplicatedCode
*/
private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{
$maxJ = $this->matrix->getSize();
$stateCount = $this->getCrossCheckStateCount();
$j = $startJ;
while($j >= 0 && $this->matrix->check($j, $centerI)){
$stateCount[2]++;
$j--;
}
if($j < 0){
return null;
}
while($j >= 0 && !$this->matrix->check($j, $centerI) && $stateCount[1] <= $maxCount){
$stateCount[1]++;
$j--;
}
if($j < 0 || $stateCount[1] > $maxCount){
return null;
}
while($j >= 0 && $this->matrix->check($j, $centerI) && $stateCount[0] <= $maxCount){
$stateCount[0]++;
$j--;
}
if($stateCount[0] > $maxCount){
return null;
}
$j = ($startJ + 1);
while($j < $maxJ && $this->matrix->check($j, $centerI)){
$stateCount[2]++;
$j++;
}
if($j === $maxJ){
return null;
}
while($j < $maxJ && !$this->matrix->check($j, $centerI) && $stateCount[3] < $maxCount){
$stateCount[3]++;
$j++;
}
if($j === $maxJ || $stateCount[3] >= $maxCount){
return null;
}
while($j < $maxJ && $this->matrix->check($j, $centerI) && $stateCount[4] < $maxCount){
$stateCount[4]++;
$j++;
}
if($stateCount[4] >= $maxCount){
return null;
}
// If we found a finder-pattern-like section, but its size is significantly different from
// the original, assume it's a false positive
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]);
if((5 * abs($stateCountTotal - $originalStateCountTotal)) >= $originalStateCountTotal){
return null;
}
if(!$this->foundPatternCross($stateCount)){
return null;
}
return $this->centerFromEnd($stateCount, $j);
}
/**
* This is called when a horizontal scan finds a possible alignment pattern. It will
* cross-check with a vertical scan, and if successful, will, ah, cross-cross-check
* with another horizontal scan. This is needed primarily to locate the real horizontal
* center of the pattern in cases of extreme skew.
* And then we cross-cross-cross check with another diagonal scan.
*
* If that succeeds the finder pattern location is added to a list that tracks
* the number of times each location has been nearly-matched as a finder pattern.
* Each additional find is more evidence that the location is in fact a finder
* pattern center
*
* @param int[] $stateCount reading state module counts from horizontal scan
* @param int $i row where finder pattern may be found
* @param int $j end of possible finder pattern in row
*
* @return bool if a finder pattern candidate was found this time
*/
private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{
$stateCountTotal = ($stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4]);
$centerJ = $this->centerFromEnd($stateCount, $j);
$centerI = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal);
if($centerI !== null){
// Re-cross check
$centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal);
if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){
$estimatedModuleSize = ($stateCountTotal / 7.0);
$found = false;
// cautious (was in for fool in which $this->possibleCenters is updated)
$count = count($this->possibleCenters);
for($index = 0; $index < $count; $index++){
$center = $this->possibleCenters[$index];
// Look for about the same center and module size:
if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
$this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
$found = true;
break;
}
}
if(!$found){
$point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
$this->possibleCenters[] = $point;
}
return true;
}
}
return false;
}
/**
* @return int number of rows we could safely skip during scanning, based on the first
* two finder patterns that have been located. In some cases their position will
* allow us to infer that the third pattern must lie below a certain point farther
* down in the image.
*/
private function findRowSkip():int{
$max = count($this->possibleCenters);
if($max <= 1){
return 0;
}
$firstConfirmedCenter = null;
foreach($this->possibleCenters as $center){
if($center->getCount() >= self::CENTER_QUORUM){
if($firstConfirmedCenter === null){
$firstConfirmedCenter = $center;
}
else{
// We have two confirmed centers
// How far down can we skip before resuming looking for the next
// pattern? In the worst case, only the difference between the
// difference in the x / y coordinates of the two centers.
// This is the case where you find top left last.
$this->hasSkipped = true;
return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) -
abs($firstConfirmedCenter->getY() - $center->getY())) / 2);
}
}
}
return 0;
}
/**
* @return bool true if we have found at least 3 finder patterns that have been detected
* at least #CENTER_QUORUM times each, and, the estimated module size of the
* candidates is "pretty similar"
*/
private function haveMultiplyConfirmedCenters():bool{
$confirmedCount = 0;
$totalModuleSize = 0.0;
$max = count($this->possibleCenters);
foreach($this->possibleCenters as $pattern){
if($pattern->getCount() >= self::CENTER_QUORUM){
$confirmedCount++;
$totalModuleSize += $pattern->getEstimatedModuleSize();
}
}
if($confirmedCount < 3){
return false;
}
// OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
// and that we need to keep looking. We detect this by asking if the estimated module sizes
// vary too much. We arbitrarily say that when the total deviation from average exceeds
// 5% of the total module size estimates, it's too much.
$average = ($totalModuleSize / (float)$max);
$totalDeviation = 0.0;
foreach($this->possibleCenters as $pattern){
$totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
}
return $totalDeviation <= (0.05 * $totalModuleSize);
}
/**
* @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best FinderPatterns from our list of candidates. The "best" are
* those that have been detected at least #CENTER_QUORUM times, and whose module
* size differs from the average among those patterns the least
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if 3 such finder patterns do not exist
*/
private function selectBestPatterns():array{
$startSize = count($this->possibleCenters);
if($startSize < 3){
throw new QRCodeDetectorException('could not find enough finder patterns');
}
usort(
$this->possibleCenters,
fn(FinderPattern $a, FinderPattern $b) => ($a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize())
);
$distortion = PHP_FLOAT_MAX;
$bestPatterns = [];
for($i = 0; $i < ($startSize - 2); $i++){
$fpi = $this->possibleCenters[$i];
$minModuleSize = $fpi->getEstimatedModuleSize();
for($j = ($i + 1); $j < ($startSize - 1); $j++){
$fpj = $this->possibleCenters[$j];
$squares0 = $fpi->getSquaredDistance($fpj);
for($k = ($j + 1); $k < $startSize; $k++){
$fpk = $this->possibleCenters[$k];
$maxModuleSize = $fpk->getEstimatedModuleSize();
// module size is not similar
if($maxModuleSize > ($minModuleSize * 1.4)){
continue;
}
$a = $squares0;
$b = $fpj->getSquaredDistance($fpk);
$c = $fpi->getSquaredDistance($fpk);
// sorts ascending - inlined
if($a < $b){
if($b > $c){
if($a < $c){
$temp = $b;
$b = $c;
$c = $temp;
}
else{
$temp = $a;
$a = $c;
$c = $b;
$b = $temp;
}
}
}
else{
if($b < $c){
if($a < $c){
$temp = $a;
$a = $b;
$b = $temp;
}
else{
$temp = $a;
$a = $b;
$b = $c;
$c = $temp;
}
}
else{
$temp = $a;
$a = $c;
$c = $temp;
}
}
// a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
// Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
// we need to check both two equal sides separately.
// The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
// from isosceles right triangle.
$d = (abs($c - 2 * $b) + abs($c - 2 * $a));
if($d < $distortion){
$distortion = $d;
$bestPatterns = [$fpi, $fpj, $fpk];
}
}
}
}
if($distortion === PHP_FLOAT_MAX){
throw new QRCodeDetectorException('finder patterns may be too distorted');
}
return $bestPatterns;
}
/**
* Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC
* and BC is less than AC, and the angle between BC and BA is less than 180 degrees.
*
* @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order
*
* @return \chillerlan\QRCode\Detector\FinderPattern[]
*/
private function orderBestPatterns(array $patterns):array{
// Find distances between pattern centers
$zeroOneDistance = $patterns[0]->getDistance($patterns[1]);
$oneTwoDistance = $patterns[1]->getDistance($patterns[2]);
$zeroTwoDistance = $patterns[0]->getDistance($patterns[2]);
// Assume one closest to other two is B; A and C will just be guesses at first
if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){
[$pointB, $pointA, $pointC] = $patterns;
}
elseif($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance){
[$pointA, $pointB, $pointC] = $patterns;
}
else{
[$pointA, $pointC, $pointB] = $patterns;
}
// Use cross product to figure out whether A and C are correct or flipped.
// This asks whether BC x BA has a positive z component, which is the arrangement
// we want for A, B, C. If it's negative, then we've got it flipped around and
// should swap A and C.
if($this->crossProductZ($pointA, $pointB, $pointC) < 0.0){
$temp = $pointA;
$pointA = $pointC;
$pointC = $temp;
}
return [$pointA, $pointB, $pointC];
}
/**
* Returns the z component of the cross product between vectors BC and BA.
*/
private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC):float{
$bX = $pointB->getX();
$bY = $pointB->getY();
return ((($pointC->getX() - $bX) * ($pointA->getY() - $bY)) - (($pointC->getY() - $bY) * ($pointA->getX() - $bX)));
}
}

View File

@@ -0,0 +1,181 @@
<?php
/**
* Class GridSampler
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\Decoder\BitMatrix;
use function array_fill, count, intdiv, sprintf;
/**
* Implementations of this class can, given locations of finder patterns for a QR code in an
* image, sample the right points in the image to reconstruct the QR code, accounting for
* perspective distortion. It is abstracted since it is relatively expensive and should be allowed
* to take advantage of platform-specific optimized implementations, like Sun's Java Advanced
* Imaging library, but which may not be available in other environments such as J2ME, and vice
* versa.
*
* The implementation used can be controlled by calling #setGridSampler(GridSampler)
* with an instance of a class which implements this interface.
*
* @author Sean Owen
*/
final class GridSampler{
private array $points;
/**
* Checks a set of points that have been transformed to sample points on an image against
* the image's dimensions to see if the point are even within the image.
*
* This method will actually "nudge" the endpoints back onto the image if they are found to be
* barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder
* patterns in an image where the QR Code runs all the way to the image border.
*
* For efficiency, the method will check points from either end of the line until one is found
* to be within the image. Because the set of points are assumed to be linear, this is valid.
*
* @param int $dimension matrix width/height
*
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if an endpoint is lies outside the image boundaries
*/
private function checkAndNudgePoints(int $dimension):void{
$nudged = true;
$max = count($this->points);
// Check and nudge points from start until we see some that are OK:
for($offset = 0; $offset < $max && $nudged; $offset += 2){
$x = (int)$this->points[$offset];
$y = (int)$this->points[($offset + 1)];
if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension));
}
$nudged = false;
if($x === -1){
$this->points[$offset] = 0.0;
$nudged = true;
}
elseif($x === $dimension){
$this->points[$offset] = ($dimension - 1);
$nudged = true;
}
if($y === -1){
$this->points[($offset + 1)] = 0.0;
$nudged = true;
}
elseif($y === $dimension){
$this->points[($offset + 1)] = ($dimension - 1);
$nudged = true;
}
}
// Check and nudge points from end:
$nudged = true;
for($offset = ($max - 2); $offset >= 0 && $nudged; $offset -= 2){
$x = (int)$this->points[$offset];
$y = (int)$this->points[($offset + 1)];
if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension));
}
$nudged = false;
if($x === -1){
$this->points[$offset] = 0.0;
$nudged = true;
}
elseif($x === $dimension){
$this->points[$offset] = ($dimension - 1);
$nudged = true;
}
if($y === -1){
$this->points[($offset + 1)] = 0.0;
$nudged = true;
}
elseif($y === $dimension){
$this->points[($offset + 1)] = ($dimension - 1);
$nudged = true;
}
}
}
/**
* Samples an image for a rectangular matrix of bits of the given dimension. The sampling
* transformation is determined by the coordinates of 4 points, in the original and transformed
* image space.
*
* @return \chillerlan\QRCode\Decoder\BitMatrix representing a grid of points sampled from the image within a region
* defined by the "from" parameters
* @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if image can't be sampled, for example, if the transformation defined
* by the given points is invalid or results in sampling outside the image boundaries
*/
public function sampleGrid(BitMatrix $matrix, int $dimension, PerspectiveTransform $transform):BitMatrix{
if($dimension <= 0){
throw new QRCodeDetectorException('invalid matrix size');
}
$bits = new BitMatrix($dimension);
$this->points = array_fill(0, (2 * $dimension), 0.0);
for($y = 0; $y < $dimension; $y++){
$max = count($this->points);
$iValue = ($y + 0.5);
for($x = 0; $x < $max; $x += 2){
$this->points[$x] = (($x / 2) + 0.5);
$this->points[($x + 1)] = $iValue;
}
// phpcs:ignore
[$this->points, ] = $transform->transformPoints($this->points);
// Quick check to see if points transformed to something inside the image;
// sufficient to check the endpoints
$this->checkAndNudgePoints($matrix->getSize());
// no need to try/catch as QRMatrix::set() will silently discard out of bounds values
# try{
for($x = 0; $x < $max; $x += 2){
// Black(-ish) pixel
$bits->set(
intdiv($x, 2),
$y,
$matrix->check((int)$this->points[$x], (int)$this->points[($x + 1)]),
QRMatrix::M_DATA
);
}
# }
# catch(\Throwable $aioobe){//ArrayIndexOutOfBoundsException
// This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting
// transform gets "twisted" such that it maps a straight line of points to a set of points
// whose endpoints are in bounds, but others are not. There is probably some mathematical
// way to detect this about the transformation that I don't know yet.
// This results in an ugly runtime exception despite our clever checks above -- can't have
// that. We could check each point's coordinates but that feels duplicative. We settle for
// catching and wrapping ArrayIndexOutOfBoundsException.
# throw new QRCodeDetectorException('ArrayIndexOutOfBoundsException');
# }
}
return $bits;
}
}

View File

@@ -0,0 +1,182 @@
<?php
/**
* Class PerspectiveTransform
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use function count;
/**
* This class implements a perspective transform in two dimensions. Given four source and four
* destination points, it will compute the transformation implied between them. The code is based
* directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.
*
* @author Sean Owen
*/
final class PerspectiveTransform{
private float $a11;
private float $a12;
private float $a13;
private float $a21;
private float $a22;
private float $a23;
private float $a31;
private float $a32;
private float $a33;
/**
*
*/
private function set(
float $a11, float $a21, float $a31,
float $a12, float $a22, float $a32,
float $a13, float $a23, float $a33
):self{
$this->a11 = $a11;
$this->a12 = $a12;
$this->a13 = $a13;
$this->a21 = $a21;
$this->a22 = $a22;
$this->a23 = $a23;
$this->a31 = $a31;
$this->a32 = $a32;
$this->a33 = $a33;
return $this;
}
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function quadrilateralToQuadrilateral(
float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3,
float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p
):self{
return (new self)
->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p)
->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3));
}
/**
*
*/
private function quadrilateralToSquare(
float $x0, float $y0, float $x1, float $y1,
float $x2, float $y2, float $x3, float $y3
):self{
// Here, the adjoint serves as the inverse:
return $this
->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
->buildAdjoint();
}
/**
*
*/
private function buildAdjoint():self{
// Adjoint is the transpose of the cofactor matrix:
return $this->set(
($this->a22 * $this->a33 - $this->a23 * $this->a32),
($this->a23 * $this->a31 - $this->a21 * $this->a33),
($this->a21 * $this->a32 - $this->a22 * $this->a31),
($this->a13 * $this->a32 - $this->a12 * $this->a33),
($this->a11 * $this->a33 - $this->a13 * $this->a31),
($this->a12 * $this->a31 - $this->a11 * $this->a32),
($this->a12 * $this->a23 - $this->a13 * $this->a22),
($this->a13 * $this->a21 - $this->a11 * $this->a23),
($this->a11 * $this->a22 - $this->a12 * $this->a21)
);
}
/**
*
*/
private function squareToQuadrilateral(
float $x0, float $y0, float $x1, float $y1,
float $x2, float $y2, float $x3, float $y3
):self{
$dx3 = ($x0 - $x1 + $x2 - $x3);
$dy3 = ($y0 - $y1 + $y2 - $y3);
if($dx3 === 0.0 && $dy3 === 0.0){
// Affine
return $this->set(($x1 - $x0), ($x2 - $x1), $x0, ($y1 - $y0), ($y2 - $y1), $y0, 0.0, 0.0, 1.0);
}
$dx1 = ($x1 - $x2);
$dx2 = ($x3 - $x2);
$dy1 = ($y1 - $y2);
$dy2 = ($y3 - $y2);
$denominator = ($dx1 * $dy2 - $dx2 * $dy1);
$a13 = (($dx3 * $dy2 - $dx2 * $dy3) / $denominator);
$a23 = (($dx1 * $dy3 - $dx3 * $dy1) / $denominator);
return $this->set(
($x1 - $x0 + $a13 * $x1),
($x3 - $x0 + $a23 * $x3),
$x0,
($y1 - $y0 + $a13 * $y1),
($y3 - $y0 + $a23 * $y3),
$y0,
$a13,
$a23,
1.0
);
}
/**
*
*/
private function times(PerspectiveTransform $other):self{
return $this->set(
($this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13),
($this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23),
($this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33),
($this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13),
($this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23),
($this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33),
($this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13),
($this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23),
($this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33)
);
}
/**
* @return array[] [$xValues, $yValues]
*/
public function transformPoints(array $xValues, array $yValues = null):array{
$max = count($xValues);
if($yValues !== null){ // unused
for($i = 0; $i < $max; $i++){
$x = $xValues[$i];
$y = $yValues[$i];
$denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33);
$xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator);
$yValues[$i] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator);
}
return [$xValues, $yValues];
}
for($i = 0; $i < $max; $i += 2){
$x = $xValues[$i];
$y = $xValues[($i + 1)];
$denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33);
$xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator);
$xValues[($i + 1)] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator);
}
return [$xValues, []];
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeDetectorException
*
* @created 01.12.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Detector;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeDetectorException extends QRCodeException{
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Class ResultPoint
*
* @created 17.01.2021
* @author ZXing Authors
* @author Smiley <smiley@chillerlan.net>
* @copyright 2021 Smiley
* @license Apache-2.0
*/
namespace chillerlan\QRCode\Detector;
use function abs;
/**
* Encapsulates a point of interest in an image containing a barcode. Typically, this
* would be the location of a finder pattern or the corner of the barcode, for example.
*
* @author Sean Owen
*/
abstract class ResultPoint{
protected float $x;
protected float $y;
protected float $estimatedModuleSize;
/**
*
*/
public function __construct(float $x, float $y, float $estimatedModuleSize){
$this->x = $x;
$this->y = $y;
$this->estimatedModuleSize = $estimatedModuleSize;
}
/**
*
*/
public function getX():float{
return $this->x;
}
/**
*
*/
public function getY():float{
return $this->y;
}
/**
*
*/
public function getEstimatedModuleSize():float{
return $this->estimatedModuleSize;
}
/**
* Determines if this finder pattern "about equals" a finder pattern at the stated
* position and size -- meaning, it is at nearly the same center with nearly the same size.
*/
public function aboutEquals(float $moduleSize, float $i, float $j):bool{
if(abs($i - $this->y) <= $moduleSize && abs($j - $this->x) <= $moduleSize){
$moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize);
return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize;
}
return false;
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeOutputException
*
* @created 09.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\QRCodeException;
/**
* An exception container
*/
final class QRCodeOutputException extends QRCodeException{
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* Class QREps
*
* @created 09.05.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf;
/**
* Encapsulated Postscript (EPS) output
*
* @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137
* @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf
* @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
* @see https://github.com/chillerlan/php-qrcode/discussions/148
*/
class QREps extends QROutputAbstract{
public const MIME_TYPE = 'application/postscript';
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first values of the array
foreach(array_values($value) as $i => $val){
if($i > 3){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}
/**
* @param array $value
*
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
$values = [];
foreach(array_values($value) as $i => $val){
if($i > 3){
break;
}
// clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range
$values[] = round((max(0, min(255, intval($val))) / 255), 6);
}
return $this->formatColor($values);
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]);
}
/**
* Set the color format string
*
* 4 values in the color array will be interpreted as CMYK, 3 as RGB
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function formatColor(array $values):string{
$count = count($values);
if($count < 3){
throw new QRCodeOutputException('invalid color value');
}
$format = ($count === 4)
// CMYK
? '%f %f %f %f C'
// RGB
:'%f %f %f R';
return sprintf($format, ...$values);
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
[$width, $height] = $this->getOutputDimensions();
$eps = [
// main header
'%!PS-Adobe-3.0 EPSF-3.0',
'%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)',
'%%Title: QR Code',
sprintf('%%%%CreationDate: %1$s', date('c')),
'%%DocumentData: Clean7Bit',
'%%LanguageLevel: 3',
sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height),
'%%EndComments',
// function definitions
'%%BeginProlog',
'/F { rectfill } def',
'/R { setrgbcolor } def',
'/C { setcmykcolor } def',
'%%EndProlog',
];
if($this::moduleValueIsValid($this->options->bgColor)){
$eps[] = $this->prepareModuleValue($this->options->bgColor);
$eps[] = sprintf('0 0 %s %s F', $width, $height);
}
// create the path elements
$paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE));
foreach($paths as $M_TYPE => $path){
if(empty($path)){
continue;
}
$eps[] = $this->getModuleValue($M_TYPE);
$eps[] = implode("\n", $path);
}
// end file
$eps[] = '%%EOF';
$data = implode("\n", $eps);
$this->saveToFile($data, $file);
return $data;
}
/**
* Returns a path segment for a single module
*/
protected function module(int $x, int $y, int $M_TYPE):string{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return '';
}
$outputX = ($x * $this->scale);
// Actual size - one block = Topmost y pos.
$top = ($this->length - $this->scale);
// Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here
$outputY = ($top - ($y * $this->scale));
return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale);
}
}

View File

@@ -0,0 +1,177 @@
<?php
/**
* Class QRFpdf
*
* @created 03.06.2020
* @author Maximilian Kresse
* @license MIT
*
* @see https://github.com/chillerlan/php-qrcode/pull/49
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use FPDF;
use function array_values, class_exists, count, intval, is_array, is_numeric, max, min;
/**
* QRFpdf output module (requires fpdf)
*
* @see https://github.com/Setasign/FPDF
* @see http://www.fpdf.org/
*/
class QRFpdf extends QROutputAbstract{
public const MIME_TYPE = 'application/pdf';
protected FPDF $fpdf;
protected ?array $prevColor = null;
/**
* QRFpdf constructor.
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
if(!class_exists(FPDF::class)){
// @codeCoverageIgnoreStart
throw new QRCodeOutputException(
'The QRFpdf output requires FPDF (https://github.com/Setasign/FPDF)'.
' as dependency but the class "\\FPDF" couldn\'t be found.'
);
// @codeCoverageIgnoreEnd
}
parent::__construct($options, $matrix);
}
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first 3 values of the array
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}
/**
* @param array $value
*
* @inheritDoc
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function prepareModuleValue($value):array{
$values = [];
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
$values[] = max(0, min(255, intval($val)));
}
if(count($values) !== 3){
throw new QRCodeOutputException('invalid color value');
}
return $values;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):array{
return ($isDark) ? [0, 0, 0] : [255, 255, 255];
}
/**
* Initializes an FPDF instance
*/
protected function initFPDF():FPDF{
return new FPDF('P', $this->options->fpdfMeasureUnit, $this->getOutputDimensions());
}
/**
* @inheritDoc
*
* @return string|\FPDF
*/
public function dump(string $file = null){
$this->fpdf = $this->initFPDF();
$this->fpdf->AddPage();
if($this::moduleValueIsValid($this->options->bgColor)){
$bgColor = $this->prepareModuleValue($this->options->bgColor);
[$width, $height] = $this->getOutputDimensions();
/** @phan-suppress-next-line PhanParamTooFewUnpack */
$this->fpdf->SetFillColor(...$bgColor);
$this->fpdf->Rect(0, 0, $width, $height, 'F');
}
$this->prevColor = null;
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->module($x, $y, $M_TYPE);
}
}
if($this->options->returnResource){
return $this->fpdf;
}
$pdfData = $this->fpdf->Output('S');
$this->saveToFile($pdfData, $file);
if($this->options->outputBase64){
$pdfData = $this->toBase64DataURI($pdfData);
}
return $pdfData;
}
/**
* Renders a single module
*/
protected function module(int $x, int $y, int $M_TYPE):void{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return;
}
$color = $this->getModuleValue($M_TYPE);
if($color !== null && $color !== $this->prevColor){
/** @phan-suppress-next-line PhanParamTooFewUnpack */
$this->fpdf->SetFillColor(...$color);
$this->prevColor = $color;
}
$this->fpdf->Rect(($x * $this->scale), ($y * $this->scale), $this->scale, $this->scale, 'F');
}
}

View File

@@ -0,0 +1,400 @@
<?php
/**
* Class QRGdImage
*
* @created 05.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use ErrorException;
use Throwable;
use function array_values, count, extension_loaded, imagebmp, imagecolorallocate, imagecolortransparent,
imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, imagegif, imagejpeg, imagepng,
imagescale, imagetypes, imagewebp, intdiv, intval, is_array, is_numeric, max, min, ob_end_clean, ob_get_contents, ob_start,
restore_error_handler, set_error_handler, sprintf;
use const IMG_BMP, IMG_GIF, IMG_JPG, IMG_PNG, IMG_WEBP;
/**
* Converts the matrix into GD images, raw or base64 output (requires ext-gd)
*
* @see https://php.net/manual/book.image.php
*
* @deprecated 5.0.0 this class will be made abstract in future versions,
* calling it directly is deprecated - use one of the child classes instead
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
class QRGdImage extends QROutputAbstract{
/**
* The GD image resource
*
* @see imagecreatetruecolor()
* @var resource|\GdImage
*
* @todo: add \GdImage type in v6
*/
protected $image;
/**
* The allocated background color
*
* @see \imagecolorallocate()
*/
protected int $background;
/**
* Whether we're running in upscale mode (scale < 20)
*
* @see \chillerlan\QRCode\QROptions::$drawCircularModules
*/
protected bool $upscaled = false;
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
* @noinspection PhpMissingParentConstructorInspection
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
$this->options = $options;
$this->matrix = $matrix;
$this->checkGD();
if($this->options->invertMatrix){
$this->matrix->invert();
}
$this->copyVars();
$this->setMatrixDimensions();
}
/**
* Checks whether GD is installed and if the given mode is supported
*
* @return void
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
* @codeCoverageIgnore
*/
protected function checkGD():void{
if(!extension_loaded('gd')){
throw new QRCodeOutputException('ext-gd not loaded');
}
$modes = [
self::GDIMAGE_BMP => IMG_BMP,
self::GDIMAGE_GIF => IMG_GIF,
self::GDIMAGE_JPG => IMG_JPG,
self::GDIMAGE_PNG => IMG_PNG,
self::GDIMAGE_WEBP => IMG_WEBP,
];
// likely using default or custom output
if(!isset($modes[$this->options->outputType])){
return;
}
$mode = $modes[$this->options->outputType];
if((imagetypes() & $mode) !== $mode){
throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputType));
}
}
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_array($value) || count($value) < 3){
return false;
}
// check the first 3 values of the array
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
if(!is_numeric($val)){
return false;
}
}
return true;
}
/**
* @param array $value
*
* @inheritDoc
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function prepareModuleValue($value):int{
$values = [];
foreach(array_values($value) as $i => $val){
if($i > 2){
break;
}
$values[] = max(0, min(255, intval($val)));
}
/** @phan-suppress-next-line PhanParamTooFewInternalUnpack */
$color = imagecolorallocate($this->image, ...$values);
if($color === false){
throw new QRCodeOutputException('could not set color: imagecolorallocate() error');
}
return $color;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):int{
return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]);
}
/**
* @inheritDoc
*
* @return string|resource|\GdImage
*
* @phan-suppress PhanUndeclaredTypeReturnType, PhanTypeMismatchReturn
* @throws \ErrorException
*/
public function dump(string $file = null){
set_error_handler(function(int $errno, string $errstr):bool{
throw new ErrorException($errstr, $errno);
});
$this->image = $this->createImage();
// set module values after image creation because we need the GdImage instance
$this->setModuleValues();
$this->setBgColor();
imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background);
$this->drawImage();
if($this->upscaled){
// scale down to the expected size
$this->image = imagescale($this->image, ($this->length / 10), ($this->length / 10));
$this->upscaled = false;
}
// set transparency after scaling, otherwise it would be undone
// @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099
$this->setTransparencyColor();
if($this->options->returnResource){
restore_error_handler();
return $this->image;
}
$imageData = $this->dumpImage();
$this->saveToFile($imageData, $file);
if($this->options->outputBase64){
// @todo: remove mime parameter in v6
$imageData = $this->toBase64DataURI($imageData, 'image/'.$this->options->outputType);
}
restore_error_handler();
return $imageData;
}
/**
* Creates a new GdImage resource and scales it if necessary
*
* we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales
*
* @see https://github.com/chillerlan/php-qrcode/issues/23
*
* @return \GdImage|resource
*/
protected function createImage(){
if($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20){
// increase the initial image size by 10
$this->length *= 10;
$this->scale *= 10;
$this->upscaled = true;
}
return imagecreatetruecolor($this->length, $this->length);
}
/**
* Sets the background color
*/
protected function setBgColor():void{
if(isset($this->background)){
return;
}
if($this::moduleValueIsValid($this->options->bgColor)){
$this->background = $this->prepareModuleValue($this->options->bgColor);
return;
}
$this->background = $this->prepareModuleValue([255, 255, 255]);
}
/**
* Sets the transparency color
*/
protected function setTransparencyColor():void{
// @todo: the jpg skip can be removed in v6
if($this->options->outputType === QROutputInterface::GDIMAGE_JPG || !$this->options->imageTransparent){
return;
}
$transparencyColor = $this->background;
if($this::moduleValueIsValid($this->options->transparencyColor)){
$transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
}
imagecolortransparent($this->image, $transparencyColor);
}
/**
* Draws the QR image
*/
protected function drawImage():void{
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->module($x, $y, $M_TYPE);
}
}
}
/**
* Creates a single QR pixel with the given settings
*/
protected function module(int $x, int $y, int $M_TYPE):void{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return;
}
$color = $this->getModuleValue($M_TYPE);
if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
imagefilledellipse(
$this->image,
(($x * $this->scale) + intdiv($this->scale, 2)),
(($y * $this->scale) + intdiv($this->scale, 2)),
(int)($this->circleDiameter * $this->scale),
(int)($this->circleDiameter * $this->scale),
$color
);
return;
}
imagefilledrectangle(
$this->image,
($x * $this->scale),
($y * $this->scale),
(($x + 1) * $this->scale),
(($y + 1) * $this->scale),
$color
);
}
/**
* Renders the image with the gdimage function for the desired output
*
* @see \imagebmp()
* @see \imagegif()
* @see \imagejpeg()
* @see \imagepng()
* @see \imagewebp()
*
* @todo: v6.0: make abstract and call from child classes
* @see https://github.com/chillerlan/php-qrcode/issues/223
* @codeCoverageIgnore
*/
protected function renderImage():void{
switch($this->options->outputType){
case QROutputInterface::GDIMAGE_BMP:
imagebmp($this->image, null, ($this->options->quality > 0));
break;
case QROutputInterface::GDIMAGE_GIF:
imagegif($this->image);
break;
case QROutputInterface::GDIMAGE_JPG:
imagejpeg($this->image, null, max(-1, min(100, $this->options->quality)));
break;
case QROutputInterface::GDIMAGE_WEBP:
imagewebp($this->image, null, max(-1, min(100, $this->options->quality)));
break;
// silently default to png output
case QROutputInterface::GDIMAGE_PNG:
default:
imagepng($this->image, null, max(-1, min(9, $this->options->quality)));
}
}
/**
* Creates the final image by calling the desired GD output function
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function dumpImage():string{
$exception = null;
$imageData = null;
ob_start();
try{
$this->renderImage();
$imageData = ob_get_contents();
imagedestroy($this->image);
}
// not going to cover edge cases
// @codeCoverageIgnoreStart
catch(Throwable $e){
$exception = $e;
}
// @codeCoverageIgnoreEnd
ob_end_clean();
// throw here in case an exception happened within the output buffer
if($exception instanceof Throwable){
throw new QRCodeOutputException($exception->getMessage());
}
return $imageData;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImageBMP
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagebmp;
/**
* GdImage bmp output
*
* @see \imagebmp()
*/
class QRGdImageBMP extends QRGdImage{
public const MIME_TYPE = 'image/bmp';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagebmp($this->image, null, ($this->options->quality > 0));
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImageGIF
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagegif;
/**
* GdImage gif output
*
* @see \imagegif()
*/
class QRGdImageGIF extends QRGdImage{
public const MIME_TYPE = 'image/gif';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagegif($this->image);
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Class QRGdImageJPEG
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagejpeg, max, min;
/**
* GdImage jpeg output
*
* @see \imagejpeg()
*/
class QRGdImageJPEG extends QRGdImage{
public const MIME_TYPE = 'image/jpg';
/**
* @inheritDoc
*/
protected function setTransparencyColor():void{
// noop - transparency is not supported
}
/**
* @inheritDoc
*/
protected function renderImage():void{
imagejpeg($this->image, null, max(-1, min(100, $this->options->quality)));
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImagePNG
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagepng, max, min;
/**
* GdImage png output
*
* @see \imagepng()
*/
class QRGdImagePNG extends QRGdImage{
public const MIME_TYPE = 'image/png';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagepng($this->image, null, max(-1, min(9, $this->options->quality)));
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Class QRGdImageWEBP
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function imagewebp, max, min;
/**
* GdImage webp output
*
* @see \imagewebp()
*/
class QRGdImageWEBP extends QRGdImage{
public const MIME_TYPE = 'image/webp';
/**
* @inheritDoc
*/
protected function renderImage():void{
imagewebp($this->image, null, max(-1, min(100, $this->options->quality)));
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* Class QRImage
*
* @created 14.12.2021
* @author smiley <smiley@chillerlan.net>
* @copyright 2021 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
/**
* @deprecated 5.0.0 backward compatibility, use QRGdImage instead
* @see \chillerlan\QRCode\Output\QRGdImage
*/
class QRImage extends QRGdImage{
}

View File

@@ -0,0 +1,235 @@
<?php
/**
* Class QRImagick
*
* @created 04.07.2018
* @author smiley <smiley@chillerlan.net>
* @copyright 2018 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use finfo, Imagick, ImagickDraw, ImagickPixel;
use function extension_loaded, in_array, is_string, max, min, preg_match, strlen;
use const FILEINFO_MIME_TYPE;
/**
* ImageMagick output module (requires ext-imagick)
*
* @see https://php.net/manual/book.imagick.php
* @see https://phpimagick.com
*/
class QRImagick extends QROutputAbstract{
/**
* The main image instance
*/
protected Imagick $imagick;
/**
* The main draw instance
*/
protected ImagickDraw $imagickDraw;
/**
* The allocated background color
*/
protected ImagickPixel $backgroundColor;
/**
* @inheritDoc
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
foreach(['fileinfo', 'imagick'] as $ext){
if(!extension_loaded($ext)){
throw new QRCodeOutputException(sprintf('ext-%s not loaded', $ext)); // @codeCoverageIgnore
}
}
parent::__construct($options, $matrix);
}
/**
* note: we're not necessarily validating the several values, just checking the general syntax
*
* @see https://www.php.net/manual/imagickpixel.construct.php
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_string($value)){
return false;
}
$value = trim($value);
// hex notation
// #rgb(a)
// #rrggbb(aa)
// #rrrrggggbbbb(aaaa)
// ...
if(preg_match('/^#[a-f\d]+$/i', $value) && in_array((strlen($value) - 1), [3, 4, 6, 8, 9, 12, 16, 24, 32], true)){
return true;
}
// css (-like) func(...values)
if(preg_match('#^(graya?|hs(b|la?)|rgba?)\([\d .,%]+\)$#i', $value)){
return true;
}
// predefined css color
if(preg_match('/^[a-z]+$/i', $value)){
return true;
}
return false;
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):ImagickPixel{
return new ImagickPixel($value);
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):ImagickPixel{
return $this->prepareModuleValue(($isDark) ? '#000' : '#fff');
}
/**
* @inheritDoc
*
* @return string|\Imagick
*/
public function dump(string $file = null){
$this->setBgColor();
$this->imagick = $this->createImage();
$this->drawImage();
// set transparency color after all operations
$this->setTransparencyColor();
if($this->options->returnResource){
return $this->imagick;
}
$imageData = $this->imagick->getImageBlob();
$this->imagick->destroy();
$this->saveToFile($imageData, $file);
if($this->options->outputBase64){
$imageData = $this->toBase64DataURI($imageData, (new finfo(FILEINFO_MIME_TYPE))->buffer($imageData));
}
return $imageData;
}
/**
* Sets the background color
*/
protected function setBgColor():void{
if($this::moduleValueIsValid($this->options->bgColor)){
$this->backgroundColor = $this->prepareModuleValue($this->options->bgColor);
return;
}
$this->backgroundColor = $this->prepareModuleValue('white');
}
/**
* Creates a new Imagick instance
*/
protected function createImage():Imagick{
$imagick = new Imagick;
[$width, $height] = $this->getOutputDimensions();
$imagick->newImage($width, $height, $this->backgroundColor, $this->options->imagickFormat);
if($this->options->quality > -1){
$imagick->setImageCompressionQuality(max(0, min(100, $this->options->quality)));
}
return $imagick;
}
/**
* Sets the transparency color
*/
protected function setTransparencyColor():void{
if(!$this->options->imageTransparent){
return;
}
$transparencyColor = $this->backgroundColor;
if($this::moduleValueIsValid($this->options->transparencyColor)){
$transparencyColor = $this->prepareModuleValue($this->options->transparencyColor);
}
$this->imagick->transparentPaintImage($transparencyColor, 0.0, 10, false);
}
/**
* Creates the QR image via ImagickDraw
*/
protected function drawImage():void{
$this->imagickDraw = new ImagickDraw;
$this->imagickDraw->setStrokeWidth(0);
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$this->module($x, $y, $M_TYPE);
}
}
$this->imagick->drawImage($this->imagickDraw);
}
/**
* draws a single pixel at the given position
*/
protected function module(int $x, int $y, int $M_TYPE):void{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return;
}
$this->imagickDraw->setFillColor($this->getModuleValue($M_TYPE));
if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
$this->imagickDraw->circle(
(($x + 0.5) * $this->scale),
(($y + 0.5) * $this->scale),
(($x + 0.5 + $this->circleRadius) * $this->scale),
(($y + 0.5) * $this->scale)
);
return;
}
$this->imagickDraw->rectangle(
($x * $this->scale),
($y * $this->scale),
((($x + 1) * $this->scale) - 1),
((($y + 1) * $this->scale) - 1)
);
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* Class QRMarkup
*
* @created 17.12.2016
* @author Smiley <smiley@chillerlan.net>
* @copyright 2016 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function is_string, preg_match, strip_tags, trim;
/**
* Abstract for markup types: HTML, SVG, ... XML anyone?
*/
abstract class QRMarkup extends QROutputAbstract{
/**
* note: we're not necessarily validating the several values, just checking the general syntax
* note: css4 colors are not included
*
* @todo: XSS proof
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_string($value)){
return false;
}
$value = trim(strip_tags($value), " '\"\r\n\t");
// hex notation
// #rgb(a)
// #rrggbb(aa)
if(preg_match('/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i', $value)){
return true;
}
// css: hsla/rgba(...values)
if(preg_match('#^(hsla?|rgba?)\([\d .,%/]+\)$#i', $value)){
return true;
}
// predefined css color
if(preg_match('/^[a-z]+$/i', $value)){
return true;
}
return false;
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
return trim(strip_tags($value), " '\"\r\n\t");
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return ($isDark) ? '#000' : '#fff';
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
$data = $this->createMarkup($file !== null);
$this->saveToFile($data, $file);
return $data;
}
/**
* returns a string with all css classes for the current element
*/
protected function getCssClass(int $M_TYPE = 0):string{
return $this->options->cssClass;
}
/**
* returns the fully parsed and rendered markup string for the given input
*/
abstract protected function createMarkup(bool $saveToFile):string;
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Class QRMarkupHTML
*
* @created 06.06.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function implode, sprintf;
/**
* HTML output (a cheap markup substitute when SVG is not available or not an option)
*/
class QRMarkupHTML extends QRMarkup{
public const MIME_TYPE = 'text/html';
/**
* @inheritDoc
*/
protected function createMarkup(bool $saveToFile):string{
$rows = [];
$cssClass = $this->getCssClass();
foreach($this->matrix->getMatrix() as $row){
$element = '<span style="background: %s;"></span>';
$modules = array_map(fn(int $M_TYPE):string => sprintf($element, $this->getModuleValue($M_TYPE)), $row);
$rows[] = sprintf('<div>%s</div>%s', implode('', $modules), $this->eol);
}
$html = sprintf('<div class="%1$s">%3$s%2$s</div>%3$s', $cssClass, implode('', $rows), $this->eol);
// wrap the snippet into a body when saving to file
if($saveToFile){
$html = sprintf(
'<!DOCTYPE html><html lang="none">%2$s<head>%2$s<meta charset="UTF-8">%2$s'.
'<title>QR Code</title></head>%2$s<body>%1$s</body>%2$s</html>',
$html,
$this->eol
);
}
return $html;
}
}

View File

@@ -0,0 +1,200 @@
<?php
/**
* Class QRMarkupSVG
*
* @created 06.06.2022
* @author smiley <smiley@chillerlan.net>
* @copyright 2022 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function array_chunk, implode, is_string, preg_match, sprintf, trim;
/**
* SVG output
*
* @see https://github.com/codemasher/php-qrcode/pull/5
* @see https://developer.mozilla.org/en-US/docs/Web/SVG
* @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/
* @see https://lea.verou.me/blog/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/
* @see https://codepen.io/leaverou/full/RmwzKv
* @see https://jakearchibald.github.io/svgomg/
* @see https://web.archive.org/web/20200220211445/http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html
*/
class QRMarkupSVG extends QRMarkup{
public const MIME_TYPE = 'image/svg+xml';
/**
* @todo: XSS proof
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
if(!is_string($value)){
return false;
}
$value = trim($value);
// url(...)
if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){
return true;
}
// otherwise check for standard css notation
return parent::moduleValueIsValid($value);
}
/**
* @inheritDoc
*/
protected function getOutputDimensions():array{
return [$this->moduleCount, $this->moduleCount];
}
/**
* @inheritDoc
*/
protected function getCssClass(int $M_TYPE = 0):string{
return implode(' ', [
'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE),
$this->matrix->isDark($M_TYPE) ? 'dark' : 'light',
$this->options->cssClass,
]);
}
/**
* @inheritDoc
*/
protected function createMarkup(bool $saveToFile):string{
$svg = $this->header();
if(!empty($this->options->svgDefs)){
$svg .= sprintf('<defs>%1$s%2$s</defs>%2$s', $this->options->svgDefs, $this->eol);
}
$svg .= $this->paths();
// close svg
$svg .= sprintf('%1$s</svg>%1$s', $this->eol);
// transform to data URI only when not saving to file
if(!$saveToFile && $this->options->outputBase64){
$svg = $this->toBase64DataURI($svg);
}
return $svg;
}
/**
* returns the value for the SVG viewBox attribute
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
* @see https://css-tricks.com/scale-svg/#article-header-id-3
*/
protected function getViewBox():string{
[$width, $height] = $this->getOutputDimensions();
return sprintf('0 0 %s %s', $width, $height);
}
/**
* returns the <svg> header with the given options parsed
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg
*/
protected function header():string{
$header = sprintf(
'<svg xmlns="http://www.w3.org/2000/svg" class="qr-svg %1$s" viewBox="%2$s" preserveAspectRatio="%3$s">%4$s',
$this->options->cssClass,
$this->getViewBox(),
$this->options->svgPreserveAspectRatio,
$this->eol
);
if($this->options->svgAddXmlHeader){
$header = sprintf('<?xml version="1.0" encoding="UTF-8"?>%s%s', $this->eol, $header);
}
return $header;
}
/**
* returns one or more SVG <path> elements
*/
protected function paths():string{
$paths = $this->collectModules(fn(int $x, int $y, int $M_TYPE):string => $this->module($x, $y, $M_TYPE));
$svg = [];
// create the path elements
foreach($paths as $M_TYPE => $modules){
// limit the total line length
$chunks = array_chunk($modules, 100);
$chonks = [];
foreach($chunks as $chunk){
$chonks[] = implode(' ', $chunk);
}
$path = implode($this->eol, $chonks);
if(empty($path)){
continue;
}
$svg[] = $this->path($path, $M_TYPE);
}
return implode($this->eol, $svg);
}
/**
* renders and returns a single <path> element
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
*/
protected function path(string $path, int $M_TYPE):string{
if($this->options->svgUseFillAttributes){
return sprintf(
'<path class="%s" fill="%s" d="%s"/>',
$this->getCssClass($M_TYPE),
$this->getModuleValue($M_TYPE),
$path
);
}
return sprintf('<path class="%s" d="%s"/>', $this->getCssClass($M_TYPE), $path);
}
/**
* returns a path segment for a single module
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
*/
protected function module(int $x, int $y, int $M_TYPE):string{
if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){
return '';
}
if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){
// string interpolation: ugly and fast
$ix = ($x + 0.5 - $this->circleRadius);
$iy = ($y + 0.5);
// phpcs:ignore
return "M$ix $iy a$this->circleRadius $this->circleRadius 0 1 0 $this->circleDiameter 0 a$this->circleRadius $this->circleRadius 0 1 0 -$this->circleDiameter 0Z";
}
// phpcs:ignore
return "M$x $y h1 v1 h-1Z";
}
}

View File

@@ -0,0 +1,261 @@
<?php
/**
* Class QROutputAbstract
*
* @created 09.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\Settings\SettingsContainerInterface;
use Closure;
use function base64_encode, dirname, file_put_contents, is_writable, ksort, sprintf;
/**
* common output abstract
*/
abstract class QROutputAbstract implements QROutputInterface{
/**
* the current size of the QR matrix
*
* @see \chillerlan\QRCode\Data\QRMatrix::getSize()
*/
protected int $moduleCount;
/**
* the side length of the QR image (modules * scale)
*/
protected int $length;
/**
* an (optional) array of color values for the several QR matrix parts
*/
protected array $moduleValues;
/**
* the (filled) data matrix object
*/
protected QRMatrix $matrix;
/**
* @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
*/
protected SettingsContainerInterface $options;
/** @see \chillerlan\QRCode\QROptions::$scale */
protected int $scale;
/** @see \chillerlan\QRCode\QROptions::$connectPaths */
protected bool $connectPaths;
/** @see \chillerlan\QRCode\QROptions::$excludeFromConnect */
protected array $excludeFromConnect;
/** @see \chillerlan\QRCode\QROptions::$eol */
protected string $eol;
/** @see \chillerlan\QRCode\QROptions::$drawLightModules */
protected bool $drawLightModules;
/** @see \chillerlan\QRCode\QROptions::$drawCircularModules */
protected bool $drawCircularModules;
/** @see \chillerlan\QRCode\QROptions::$keepAsSquare */
protected array $keepAsSquare;
/** @see \chillerlan\QRCode\QROptions::$circleRadius */
protected float $circleRadius;
protected float $circleDiameter;
/**
* QROutputAbstract constructor.
*/
public function __construct(SettingsContainerInterface $options, QRMatrix $matrix){
$this->options = $options;
$this->matrix = $matrix;
if($this->options->invertMatrix){
$this->matrix->invert();
}
$this->copyVars();
$this->setMatrixDimensions();
$this->setModuleValues();
}
/**
* Creates copies of several QROptions values to avoid calling the magic getters
* in long loops for a significant performance increase.
*
* These variables are usually used in the "module" methods and are called up to 31329 times (at version 40).
*/
protected function copyVars():void{
$vars = [
'connectPaths',
'excludeFromConnect',
'eol',
'drawLightModules',
'drawCircularModules',
'keepAsSquare',
'circleRadius',
];
foreach($vars as $property){
$this->{$property} = $this->options->{$property};
}
$this->circleDiameter = ($this->circleRadius * 2);
}
/**
* Sets/updates the matrix dimensions
*
* Call this method if you modify the matrix from within your custom module in case the dimensions have been changed
*/
protected function setMatrixDimensions():void{
$this->moduleCount = $this->matrix->getSize();
$this->scale = $this->options->scale;
$this->length = ($this->moduleCount * $this->scale);
}
/**
* Returns a 2 element array with the current output width and height
*
* The type and units of the values depend on the output class. The default value is the current module count * scale.
*/
protected function getOutputDimensions():array{
return [$this->length, $this->length];
}
/**
* Sets the initial module values
*/
protected function setModuleValues():void{
// first fill the map with the default values
foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){
$this->moduleValues[$M_TYPE] = $this->getDefaultModuleValue($defaultValue);
}
// now loop over the options values to replace defaults and add extra values
foreach($this->options->moduleValues as $M_TYPE => $value){
if($this::moduleValueIsValid($value)){
$this->moduleValues[$M_TYPE] = $this->prepareModuleValue($value);
}
}
}
/**
* Prepares the value for the given input ()
*
* @param mixed $value
*
* @return mixed|null return value depends on the output class
*/
abstract protected function prepareModuleValue($value);
/**
* Returns a default value for either dark or light modules
*
* @return mixed|null return value depends on the output class
*/
abstract protected function getDefaultModuleValue(bool $isDark);
/**
* Returns the prepared value for the given $M_TYPE
*
* @return mixed return value depends on the output class
* @throws \chillerlan\QRCode\Output\QRCodeOutputException if $moduleValues[$M_TYPE] doesn't exist
*/
protected function getModuleValue(int $M_TYPE){
if(!isset($this->moduleValues[$M_TYPE])){
throw new QRCodeOutputException(sprintf('$M_TYPE %012b not found in module values map', $M_TYPE));
}
return $this->moduleValues[$M_TYPE];
}
/**
* Returns the prepared module value at the given coordinate [$x, $y] (convenience)
*
* @return mixed|null
*/
protected function getModuleValueAt(int $x, int $y){
return $this->getModuleValue($this->matrix->get($x, $y));
}
/**
* Returns a base64 data URI for the given string and mime type
*/
protected function toBase64DataURI(string $data, string $mime = null):string{
return sprintf('data:%s;base64,%s', ($mime ?? $this::MIME_TYPE), base64_encode($data));
}
/**
* Saves the qr $data to a $file. If $file is null, nothing happens.
*
* @see file_put_contents()
* @see \chillerlan\QRCode\QROptions::$cachefile
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function saveToFile(string $data, string $file = null):void{
if($file === null){
return;
}
if(!is_writable(dirname($file))){
throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s', $file));
}
if(file_put_contents($file, $data) === false){
throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s (file_put_contents error)', $file));
}
}
/**
* collects the modules per QRMatrix::M_* type and runs a $transform function on each module and
* returns an array with the transformed modules
*
* The transform callback is called with the following parameters:
*
* $x - current column
* $y - current row
* $M_TYPE - field value
* $M_TYPE_LAYER - (possibly modified) field value that acts as layer id
*/
protected function collectModules(Closure $transform):array{
$paths = [];
// collect the modules for each type
foreach($this->matrix->getMatrix() as $y => $row){
foreach($row as $x => $M_TYPE){
$M_TYPE_LAYER = $M_TYPE;
if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){
// to connect paths we'll redeclare the $M_TYPE_LAYER to data only
$M_TYPE_LAYER = QRMatrix::M_DATA;
if($this->matrix->isDark($M_TYPE)){
$M_TYPE_LAYER = QRMatrix::M_DATA_DARK;
}
}
// collect the modules per $M_TYPE
$module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER);
if(!empty($module)){
$paths[$M_TYPE_LAYER][] = $module;
}
}
}
// beautify output
ksort($paths);
return $paths;
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* Interface QROutputInterface,
*
* @created 02.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use chillerlan\QRCode\Data\QRMatrix;
/**
* Converts the data matrix into readable output
*/
interface QROutputInterface{
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const MARKUP_HTML = 'html';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const MARKUP_SVG = 'svg';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_BMP = 'bmp';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_GIF = 'gif';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_JPG = 'jpg';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_PNG = 'png';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const GDIMAGE_WEBP = 'webp';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const STRING_JSON = 'json';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const STRING_TEXT = 'text';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const IMAGICK = 'imagick';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const FPDF = 'fpdf';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const EPS = 'eps';
/**
* @var string
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const CUSTOM = 'custom';
/**
* Map of built-in output modes => class FQN
*
* @var string[]
* @deprecated 5.0.0 <no replacement>
* @see https://github.com/chillerlan/php-qrcode/issues/223
*/
public const MODES = [
self::MARKUP_SVG => QRMarkupSVG::class,
self::MARKUP_HTML => QRMarkupHTML::class,
self::GDIMAGE_BMP => QRGdImageBMP::class,
self::GDIMAGE_GIF => QRGdImageGIF::class,
self::GDIMAGE_JPG => QRGdImageJPEG::class,
self::GDIMAGE_PNG => QRGdImagePNG::class,
self::GDIMAGE_WEBP => QRGdImageWEBP::class,
self::STRING_JSON => QRStringJSON::class,
self::STRING_TEXT => QRStringText::class,
self::IMAGICK => QRImagick::class,
self::FPDF => QRFpdf::class,
self::EPS => QREps::class,
];
/**
* Map of module type => default value
*
* @var bool[]
*/
public const DEFAULT_MODULE_VALUES = [
// light
QRMatrix::M_NULL => false,
QRMatrix::M_DARKMODULE_LIGHT => false,
QRMatrix::M_DATA => false,
QRMatrix::M_FINDER => false,
QRMatrix::M_SEPARATOR => false,
QRMatrix::M_ALIGNMENT => false,
QRMatrix::M_TIMING => false,
QRMatrix::M_FORMAT => false,
QRMatrix::M_VERSION => false,
QRMatrix::M_QUIETZONE => false,
QRMatrix::M_LOGO => false,
QRMatrix::M_FINDER_DOT_LIGHT => false,
// dark
QRMatrix::M_DARKMODULE => true,
QRMatrix::M_DATA_DARK => true,
QRMatrix::M_FINDER_DARK => true,
QRMatrix::M_SEPARATOR_DARK => true,
QRMatrix::M_ALIGNMENT_DARK => true,
QRMatrix::M_TIMING_DARK => true,
QRMatrix::M_FORMAT_DARK => true,
QRMatrix::M_VERSION_DARK => true,
QRMatrix::M_QUIETZONE_DARK => true,
QRMatrix::M_LOGO_DARK => true,
QRMatrix::M_FINDER_DOT => true,
];
/**
* Map of module type => readable name (for CSS etc.)
*
* @var string[]
*/
public const LAYERNAMES = [
// light
QRMatrix::M_NULL => 'null',
QRMatrix::M_DARKMODULE_LIGHT => 'darkmodule-light',
QRMatrix::M_DATA => 'data',
QRMatrix::M_FINDER => 'finder',
QRMatrix::M_SEPARATOR => 'separator',
QRMatrix::M_ALIGNMENT => 'alignment',
QRMatrix::M_TIMING => 'timing',
QRMatrix::M_FORMAT => 'format',
QRMatrix::M_VERSION => 'version',
QRMatrix::M_QUIETZONE => 'quietzone',
QRMatrix::M_LOGO => 'logo',
QRMatrix::M_FINDER_DOT_LIGHT => 'finder-dot-light',
// dark
QRMatrix::M_DARKMODULE => 'darkmodule',
QRMatrix::M_DATA_DARK => 'data-dark',
QRMatrix::M_FINDER_DARK => 'finder-dark',
QRMatrix::M_SEPARATOR_DARK => 'separator-dark',
QRMatrix::M_ALIGNMENT_DARK => 'alignment-dark',
QRMatrix::M_TIMING_DARK => 'timing-dark',
QRMatrix::M_FORMAT_DARK => 'format-dark',
QRMatrix::M_VERSION_DARK => 'version-dark',
QRMatrix::M_QUIETZONE_DARK => 'quietzone-dark',
QRMatrix::M_LOGO_DARK => 'logo-dark',
QRMatrix::M_FINDER_DOT => 'finder-dot',
];
/**
* @var string
* @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI()
* @internal do not call this constant from the interface, but rather from one of the child classes
*/
public const MIME_TYPE = '';
/**
* Determines whether the given value is valid
*
* @param mixed $value
*/
public static function moduleValueIsValid($value):bool;
/**
* Generates the output, optionally dumps it to a file, and returns it
*
* please note that the value of QROptions::$cachefile is already evaluated at this point.
* if the output module is invoked manually, it has no effect at all.
* you need to supply the $file parameter here in that case (or handle the option value in your custom output module).
*
* @see \chillerlan\QRCode\QRCode::renderMatrix()
*
* @return mixed
*/
public function dump(string $file = null);
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* Class QRString
*
* @created 05.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function implode, is_string, json_encode, max, min, sprintf;
use const JSON_THROW_ON_ERROR;
/**
* Converts the matrix data into string types
*
* @deprecated 5.0.0 this class will be removed in future versions, use one of QRStringText or QRStringJSON instead
*/
class QRString extends QROutputAbstract{
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
return is_string($value);
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
return $value;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return ($isDark) ? '██' : '░░';
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
switch($this->options->outputType){
case QROutputInterface::STRING_TEXT:
$data = $this->text();
break;
case QROutputInterface::STRING_JSON:
default:
$data = $this->json();
}
$this->saveToFile($data, $file);
return $data;
}
/**
* string output
*/
protected function text():string{
$lines = [];
$linestart = $this->options->textLineStart;
for($y = 0; $y < $this->moduleCount; $y++){
$r = [];
for($x = 0; $x < $this->moduleCount; $x++){
$r[] = $this->getModuleValueAt($x, $y);
}
$lines[] = $linestart.implode('', $r);
}
return implode($this->eol, $lines);
}
/**
* JSON output
*
* @throws \JsonException
*/
protected function json():string{
return json_encode($this->matrix->getMatrix($this->options->jsonAsBooleans), JSON_THROW_ON_ERROR);
}
//
/**
* a little helper to create a proper ANSI 8-bit color escape sequence
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
* @see https://en.wikipedia.org/wiki/Block_Elements
*
* @codeCoverageIgnore
*/
public static function ansi8(string $str, int $color, bool $background = null):string{
$color = max(0, min($color, 255));
$background = ($background === true) ? 48 : 38;
return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Class QRStringJSON
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*
* @noinspection PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode\Output;
use function json_encode;
/**
*
*/
class QRStringJSON extends QROutputAbstract{
public const MIME_TYPE = 'application/json';
/**
* @inheritDoc
* @throws \JsonException
*/
public function dump(string $file = null):string{
$matrix = $this->matrix->getMatrix($this->options->jsonAsBooleans);
$data = json_encode($matrix, $this->options->jsonFlags);;
$this->saveToFile($data, $file);
return $data;
}
/**
* unused - required by interface
*
* @inheritDoc
* @codeCoverageIgnore
*/
protected function prepareModuleValue($value):string{
return '';
}
/**
* unused - required by interface
*
* @inheritDoc
* @codeCoverageIgnore
*/
protected function getDefaultModuleValue(bool $isDark):string{
return '';
}
/**
* unused - required by interface
*
* @inheritDoc
* @codeCoverageIgnore
*/
public static function moduleValueIsValid($value):bool{
return true;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Class QRStringText
*
* @created 25.10.2023
* @author smiley <smiley@chillerlan.net>
* @copyright 2023 smiley
* @license MIT
*/
namespace chillerlan\QRCode\Output;
use function array_map, implode, is_string, max, min, sprintf;
/**
*
*/
class QRStringText extends QROutputAbstract{
public const MIME_TYPE = 'text/plain';
/**
* @inheritDoc
*/
public static function moduleValueIsValid($value):bool{
return is_string($value);
}
/**
* @inheritDoc
*/
protected function prepareModuleValue($value):string{
return $value;
}
/**
* @inheritDoc
*/
protected function getDefaultModuleValue(bool $isDark):string{
return ($isDark) ? '██' : '░░';
}
/**
* @inheritDoc
*/
public function dump(string $file = null):string{
$lines = [];
$linestart = $this->options->textLineStart;
foreach($this->matrix->getMatrix() as $row){
$lines[] = $linestart.implode('', array_map([$this, 'getModuleValue'], $row));
}
$data = implode($this->eol, $lines);
$this->saveToFile($data, $file);
return $data;
}
/**
* a little helper to create a proper ANSI 8-bit color escape sequence
*
* @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
* @see https://en.wikipedia.org/wiki/Block_Elements
*
* @codeCoverageIgnore
*/
public static function ansi8(string $str, int $color, bool $background = null):string{
$color = max(0, min($color, 255));
$background = ($background === true) ? 48 : 38;
return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str);
}
}

View File

@@ -0,0 +1,483 @@
<?php
/**
* Class QRCode
*
* @created 26.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
namespace chillerlan\QRCode;
use chillerlan\QRCode\Common\{
EccLevel, ECICharset, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode, Version
};
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number, QRData, QRDataModeInterface, QRMatrix};
use chillerlan\QRCode\Decoder\{Decoder, DecoderResult};
use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface};
use chillerlan\Settings\SettingsContainerInterface;
use function class_exists, class_implements, in_array, mb_convert_encoding, mb_internal_encoding;
/**
* Turns a text string into a Model 2 QR Code
*
* @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
* @see https://www.qrcode.com/en/codes/model12.html
* @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
* @see https://en.wikipedia.org/wiki/QR_code
* @see https://www.thonky.com/qr-code-tutorial/
*/
class QRCode{
/**
* @deprecated 5.0.0 use Version::AUTO instead
* @see \chillerlan\QRCode\Common\Version::AUTO
* @var int
*/
public const VERSION_AUTO = Version::AUTO;
/**
* @deprecated 5.0.0 use MaskPattern::AUTO instead
* @see \chillerlan\QRCode\Common\MaskPattern::AUTO
* @var int
*/
public const MASK_PATTERN_AUTO = MaskPattern::AUTO;
/**
* @deprecated 5.0.0 use EccLevel::L instead
* @see \chillerlan\QRCode\Common\EccLevel::L
* @var int
*/
public const ECC_L = EccLevel::L;
/**
* @deprecated 5.0.0 use EccLevel::M instead
* @see \chillerlan\QRCode\Common\EccLevel::M
* @var int
*/
public const ECC_M = EccLevel::M;
/**
* @deprecated 5.0.0 use EccLevel::Q instead
* @see \chillerlan\QRCode\Common\EccLevel::Q
* @var int
*/
public const ECC_Q = EccLevel::Q;
/**
* @deprecated 5.0.0 use EccLevel::H instead
* @see \chillerlan\QRCode\Common\EccLevel::H
* @var int
*/
public const ECC_H = EccLevel::H;
/**
* @deprecated 5.0.0 use QROutputInterface::MARKUP_HTML instead
* @see \chillerlan\QRCode\Output\QROutputInterface::MARKUP_HTML
* @var string
*/
public const OUTPUT_MARKUP_HTML = QROutputInterface::MARKUP_HTML;
/**
* @deprecated 5.0.0 use QROutputInterface::MARKUP_SVG instead
* @see \chillerlan\QRCode\Output\QROutputInterface::MARKUP_SVG
* @var string
*/
public const OUTPUT_MARKUP_SVG = QROutputInterface::MARKUP_SVG;
/**
* @deprecated 5.0.0 use QROutputInterface::GDIMAGE_PNG instead
* @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_PNG
* @var string
*/
public const OUTPUT_IMAGE_PNG = QROutputInterface::GDIMAGE_PNG;
/**
* @deprecated 5.0.0 use QROutputInterface::GDIMAGE_JPG instead
* @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_JPG
* @var string
*/
public const OUTPUT_IMAGE_JPG = QROutputInterface::GDIMAGE_JPG;
/**
* @deprecated 5.0.0 use QROutputInterface::GDIMAGE_GIF instead
* @see \chillerlan\QRCode\Output\QROutputInterface::GDIMAGE_GIF
* @var string
*/
public const OUTPUT_IMAGE_GIF = QROutputInterface::GDIMAGE_GIF;
/**
* @deprecated 5.0.0 use QROutputInterface::STRING_JSON instead
* @see \chillerlan\QRCode\Output\QROutputInterface::STRING_JSON
* @var string
*/
public const OUTPUT_STRING_JSON = QROutputInterface::STRING_JSON;
/**
* @deprecated 5.0.0 use QROutputInterface::STRING_TEXT instead
* @see \chillerlan\QRCode\Output\QROutputInterface::STRING_TEXT
* @var string
*/
public const OUTPUT_STRING_TEXT = QROutputInterface::STRING_TEXT;
/**
* @deprecated 5.0.0 use QROutputInterface::IMAGICK instead
* @see \chillerlan\QRCode\Output\QROutputInterface::IMAGICK
* @var string
*/
public const OUTPUT_IMAGICK = QROutputInterface::IMAGICK;
/**
* @deprecated 5.0.0 use QROutputInterface::FPDF instead
* @see \chillerlan\QRCode\Output\QROutputInterface::FPDF
* @var string
*/
public const OUTPUT_FPDF = QROutputInterface::FPDF;
/**
* @deprecated 5.0.0 use QROutputInterface::EPS instead
* @see \chillerlan\QRCode\Output\QROutputInterface::EPS
* @var string
*/
public const OUTPUT_EPS = QROutputInterface::EPS;
/**
* @deprecated 5.0.0 use QROutputInterface::CUSTOM instead
* @see \chillerlan\QRCode\Output\QROutputInterface::CUSTOM
* @var string
*/
public const OUTPUT_CUSTOM = QROutputInterface::CUSTOM;
/**
* @deprecated 5.0.0 use QROutputInterface::MODES instead
* @see \chillerlan\QRCode\Output\QROutputInterface::MODES
* @var string[]
*/
public const OUTPUT_MODES = QROutputInterface::MODES;
/**
* The settings container
*
* @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
*/
protected SettingsContainerInterface $options;
/**
* A collection of one or more data segments of QRDataModeInterface instances to write
*
* @var \chillerlan\QRCode\Data\QRDataModeInterface[]
*/
protected array $dataSegments = [];
/**
* The luminance source for the reader
*/
protected string $luminanceSourceFQN = GDLuminanceSource::class;
/**
* QRCode constructor.
*
* PHP8: accept iterable
*/
public function __construct(SettingsContainerInterface $options = null){
$this->setOptions(($options ?? new QROptions));
}
/**
* Sets an options instance
*/
public function setOptions(SettingsContainerInterface $options):self{
$this->options = $options;
if($this->options->readerUseImagickIfAvailable){
$this->luminanceSourceFQN = IMagickLuminanceSource::class;
}
return $this;
}
/**
* Renders a QR Code for the given $data and QROptions, saves $file optionally
*
* @return mixed
*/
public function render(string $data = null, string $file = null){
if($data !== null){
/** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
foreach(Mode::INTERFACES as $dataInterface){
if($dataInterface::validateString($data)){
$this->addSegment(new $dataInterface($data));
break;
}
}
}
return $this->renderMatrix($this->getQRMatrix(), $file);
}
/**
* Renders a QR Code for the given QRMatrix and QROptions, saves $file optionally
*
* @return mixed
*/
public function renderMatrix(QRMatrix $matrix, string $file = null){
return $this->initOutputInterface($matrix)->dump($file ?? $this->options->cachefile);
}
/**
* Returns a QRMatrix object for the given $data and current QROptions
*
* @throws \chillerlan\QRCode\Data\QRCodeDataException
*/
public function getQRMatrix():QRMatrix{
$matrix = (new QRData($this->options, $this->dataSegments))->writeMatrix();
$maskPattern = $this->options->maskPattern === MaskPattern::AUTO
? MaskPattern::getBestPattern($matrix)
: new MaskPattern($this->options->maskPattern);
$matrix->setFormatInfo($maskPattern)->mask($maskPattern);
return $this->addMatrixModifications($matrix);
}
/**
* add matrix modifications after mask pattern evaluation and before handing over to output
*/
protected function addMatrixModifications(QRMatrix $matrix):QRMatrix{
if($this->options->addLogoSpace){
// check whether one of the dimensions was omitted
$logoSpaceWidth = ($this->options->logoSpaceWidth ?? $this->options->logoSpaceHeight ?? 0);
$logoSpaceHeight = ($this->options->logoSpaceHeight ?? $logoSpaceWidth);
$matrix->setLogoSpace(
$logoSpaceWidth,
$logoSpaceHeight,
$this->options->logoSpaceStartX,
$this->options->logoSpaceStartY
);
}
if($this->options->addQuietzone){
$matrix->setQuietZone($this->options->quietzoneSize);
}
return $matrix;
}
/**
* @deprecated 5.0.0 use QRCode::getQRMatrix() instead
* @see \chillerlan\QRCode\QRCode::getQRMatrix()
* @codeCoverageIgnore
*/
public function getMatrix():QRMatrix{
return $this->getQRMatrix();
}
/**
* initializes a fresh built-in or custom QROutputInterface
*
* @throws \chillerlan\QRCode\Output\QRCodeOutputException
*/
protected function initOutputInterface(QRMatrix $matrix):QROutputInterface{
// @todo: remove custom invocation in v6
$outputInterface = (QROutputInterface::MODES[$this->options->outputType] ?? null);
if($this->options->outputType === QROutputInterface::CUSTOM){
$outputInterface = $this->options->outputInterface;
}
if(!$outputInterface || !class_exists($outputInterface)){
throw new QRCodeOutputException('invalid output module');
}
if(!in_array(QROutputInterface::class, class_implements($outputInterface))){
throw new QRCodeOutputException('output module does not implement QROutputInterface');
}
return new $outputInterface($this->options, $matrix);
}
/**
* checks if a string qualifies as numeric (convenience method)
*
* @deprecated 5.0.0 use Number::validateString() instead
* @see \chillerlan\QRCode\Data\Number::validateString()
* @codeCoverageIgnore
*/
public function isNumber(string $string):bool{
return Number::validateString($string);
}
/**
* checks if a string qualifies as alphanumeric (convenience method)
*
* @deprecated 5.0.0 use AlphaNum::validateString() instead
* @see \chillerlan\QRCode\Data\AlphaNum::validateString()
* @codeCoverageIgnore
*/
public function isAlphaNum(string $string):bool{
return AlphaNum::validateString($string);
}
/**
* checks if a string qualifies as Kanji (convenience method)
*
* @deprecated 5.0.0 use Kanji::validateString() instead
* @see \chillerlan\QRCode\Data\Kanji::validateString()
* @codeCoverageIgnore
*/
public function isKanji(string $string):bool{
return Kanji::validateString($string);
}
/**
* a dummy (convenience method)
*
* @deprecated 5.0.0 use Byte::validateString() instead
* @see \chillerlan\QRCode\Data\Byte::validateString()
* @codeCoverageIgnore
*/
public function isByte(string $string):bool{
return Byte::validateString($string);
}
/**
* Adds a data segment
*
* ISO/IEC 18004:2000 8.3.6 - Mixing modes
* ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
*/
public function addSegment(QRDataModeInterface $segment):self{
$this->dataSegments[] = $segment;
return $this;
}
/**
* Clears the data segments array
*
* @codeCoverageIgnore
*/
public function clearSegments():self{
$this->dataSegments = [];
return $this;
}
/**
* Adds a numeric data segment
*
* ISO/IEC 18004:2000 8.3.2 - Numeric Mode
*/
public function addNumericSegment(string $data):self{
return $this->addSegment(new Number($data));
}
/**
* Adds an alphanumeric data segment
*
* ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
*/
public function addAlphaNumSegment(string $data):self{
return $this->addSegment(new AlphaNum($data));
}
/**
* Adds a Kanji data segment (Japanese 13-bit double-byte characters, Shift-JIS)
*
* ISO/IEC 18004:2000 8.3.5 - Kanji Mode
*/
public function addKanjiSegment(string $data):self{
return $this->addSegment(new Kanji($data));
}
/**
* Adds a Hanzi data segment (simplified Chinese 13-bit double-byte characters, GB2312/GB18030)
*
* GBT18284-2000 Hanzi Mode
*/
public function addHanziSegment(string $data):self{
return $this->addSegment(new Hanzi($data));
}
/**
* Adds an 8-bit byte data segment
*
* ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
*/
public function addByteSegment(string $data):self{
return $this->addSegment(new Byte($data));
}
/**
* Adds a standalone ECI designator
*
* The ECI designator must be followed by a Byte segment that contains the string encoded according to the given ECI charset
*
* ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
*/
public function addEciDesignator(int $encoding):self{
return $this->addSegment(new ECI($encoding));
}
/**
* Adds an ECI data segment (including designator)
*
* The given string will be encoded from mb_internal_encoding() to the given ECI character set
*
* I hate this somehow, but I'll leave it for now
*
* @throws \chillerlan\QRCode\QRCodeException
*/
public function addEciSegment(int $encoding, string $data):self{
// validate the encoding id
$eciCharset = new ECICharset($encoding);
// get charset name
$eciCharsetName = $eciCharset->getName();
// convert the string to the given charset
if($eciCharsetName !== null){
$data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding());
return $this
->addEciDesignator($eciCharset->getID())
->addByteSegment($data)
;
}
throw new QRCodeException('unable to add ECI segment');
}
/**
* Reads a QR Code from a given file
*
* @noinspection PhpUndefinedMethodInspection
*/
public function readFromFile(string $path):DecoderResult{
return $this->readFromSource($this->luminanceSourceFQN::fromFile($path, $this->options));
}
/**
* Reads a QR Code from the given data blob
*
* @noinspection PhpUndefinedMethodInspection
*/
public function readFromBlob(string $blob):DecoderResult{
return $this->readFromSource($this->luminanceSourceFQN::fromBlob($blob, $this->options));
}
/**
* Reads a QR Code from the given luminance source
*/
public function readFromSource(LuminanceSourceInterface $source):DecoderResult{
return (new Decoder)->decode($source);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class QRCodeException
*
* @created 27.11.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use Exception;
/**
* An exception container
*/
class QRCodeException extends Exception{
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Class QROptions
*
* @created 08.12.2015
* @author Smiley <smiley@chillerlan.net>
* @copyright 2015 Smiley
* @license MIT
*/
namespace chillerlan\QRCode;
use chillerlan\Settings\SettingsContainerAbstract;
/**
* The QRCode settings container
*/
class QROptions extends SettingsContainerAbstract{
use QROptionsTrait;
}

View File

@@ -0,0 +1,729 @@
<?php
/**
* Trait QROptionsTrait
*
* Note: the docblocks in this file are optimized for readability in PhpStorm ond on readthedocs.io
*
* @created 10.03.2018
* @author smiley <smiley@chillerlan.net>
* @copyright 2018 smiley
* @license MIT
*
* @noinspection PhpUnused, PhpComposerExtensionStubsInspection
*/
namespace chillerlan\QRCode;
use chillerlan\QRCode\Output\QROutputInterface;
use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
use function extension_loaded, in_array, max, min, strtolower;
use const JSON_THROW_ON_ERROR, PHP_EOL;
/**
* The QRCode plug-in settings & setter functionality
*/
trait QROptionsTrait{
/*
* QR Code specific settings
*/
/**
* QR Code version number
*
* `1 ... 40` or `Version::AUTO` (default)
*
* @see \chillerlan\QRCode\Common\Version
*/
protected int $version = Version::AUTO;
/**
* Minimum QR version
*
* if `QROptions::$version` is set to `Version::AUTO` (default: 1)
*/
protected int $versionMin = 1;
/**
* Maximum QR version
*
* if `QROptions::$version` is set to `Version::AUTO` (default: 40)
*/
protected int $versionMax = 40;
/**
* Error correct level
*
* `EccLevel::X` where `X` is:
*
* - `L` => 7% (default)
* - `M` => 15%
* - `Q` => 25%
* - `H` => 30%
*
* @todo: accept string values (PHP8+)
* @see \chillerlan\QRCode\Common\EccLevel
* @see https://github.com/chillerlan/php-qrcode/discussions/160
*/
protected int $eccLevel = EccLevel::L;
/**
* Mask Pattern to use (no value in using, mostly for unit testing purposes)
*
* `0 ... 7` or `MaskPattern::PATTERN_AUTO` (default)
*
* @see \chillerlan\QRCode\Common\MaskPattern
*/
protected int $maskPattern = MaskPattern::AUTO;
/**
* Add a "quiet zone" (margin) according to the QR code spec
*
* @see https://www.qrcode.com/en/howto/code.html
*/
protected bool $addQuietzone = true;
/**
* Size of the quiet zone
*
* internally clamped to `0 ... $moduleCount / 2` (default: 4)
*/
protected int $quietzoneSize = 4;
/*
* General output settings
*/
/**
* The built-in output type
*
* - `QROutputInterface::MARKUP_SVG` (default)
* - `QROutputInterface::MARKUP_HTML`
* - `QROutputInterface::GDIMAGE_BMP`
* - `QROutputInterface::GDIMAGE_GIF`
* - `QROutputInterface::GDIMAGE_JPG`
* - `QROutputInterface::GDIMAGE_PNG`
* - `QROutputInterface::GDIMAGE_WEBP`
* - `QROutputInterface::STRING_TEXT`
* - `QROutputInterface::STRING_JSON`
* - `QROutputInterface::IMAGICK`
* - `QROutputInterface::EPS`
* - `QROutputInterface::FPDF`
* - `QROutputInterface::CUSTOM`
*
* @see \chillerlan\QRCode\Output\QREps
* @see \chillerlan\QRCode\Output\QRFpdf
* @see \chillerlan\QRCode\Output\QRGdImage
* @see \chillerlan\QRCode\Output\QRImagick
* @see \chillerlan\QRCode\Output\QRMarkupHTML
* @see \chillerlan\QRCode\Output\QRMarkupSVG
* @see \chillerlan\QRCode\Output\QRString
* @see https://github.com/chillerlan/php-qrcode/issues/223
*
* @deprecated 5.0.0 see issue #223
*/
protected string $outputType = QROutputInterface::MARKUP_SVG;
/**
* The FQCN of the custom `QROutputInterface`
*
* if `QROptions::$outputType` is set to `QROutputInterface::CUSTOM` (default: `null`)
*
* @deprecated 5.0.0 the nullable type will be removed in future versions
* and the default value will be set to `QRMarkupSVG::class`
*/
protected ?string $outputInterface = null;
/**
* Return the image resource instead of a render if applicable.
*
* - `QRGdImage`: `resource` (PHP < 8), `GdImage`
* - `QRImagick`: `Imagick`
* - `QRFpdf`: `FPDF`
*
* This option overrides/ignores other output settings, such as `QROptions::$cachefile`
* and `QROptions::$outputBase64`. (default: `false`)
*
* @see \chillerlan\QRCode\Output\QROutputInterface::dump()
*/
protected bool $returnResource = false;
/**
* Optional cache file path `/path/to/cache.file`
*
* Please note that the `$file` parameter in `QRCode::render()` and `QRCode::renderMatrix()`
* takes precedence over the `QROptions::$cachefile` value. (default: `null`)
*
* @see \chillerlan\QRCode\QRCode::render()
* @see \chillerlan\QRCode\QRCode::renderMatrix()
*/
protected ?string $cachefile = null;
/**
* Toggle base64 data URI or raw data output (if applicable)
*
* (default: `true`)
*
* @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI()
*/
protected bool $outputBase64 = true;
/**
* Newline string
*
* (default: `PHP_EOL`)
*/
protected string $eol = PHP_EOL;
/*
* Common visual modifications
*/
/**
* Sets the image background color (if applicable)
*
* - `QRImagick`: defaults to `"white"`
* - `QRGdImage`: defaults to `[255, 255, 255]`
* - `QRFpdf`: defaults to blank internally (white page)
*
* @var mixed|null
*/
protected $bgColor = null;
/**
* Whether to invert the matrix (reflectance reversal)
*
* (default: `false`)
*
* @see \chillerlan\QRCode\Data\QRMatrix::invert()
*/
protected bool $invertMatrix = false;
/**
* Whether to draw the light (false) modules
*
* (default: `true`)
*/
protected bool $drawLightModules = true;
/**
* Specify whether to draw the modules as filled circles
*
* a note for `GdImage` output:
*
* if `QROptions::$scale` is less than 20, the image will be upscaled internally, then the modules will be drawn
* using `imagefilledellipse()` and then scaled back to the expected size
*
* No effect in: `QREps`, `QRFpdf`, `QRMarkupHTML`
*
* @see \imagefilledellipse()
* @see https://github.com/chillerlan/php-qrcode/issues/23
* @see https://github.com/chillerlan/php-qrcode/discussions/122
*/
protected bool $drawCircularModules = false;
/**
* Specifies the radius of the modules when `QROptions::$drawCircularModules` is set to `true`
*
* (default: 0.45)
*/
protected float $circleRadius = 0.45;
/**
* Specifies which module types to exclude when `QROptions::$drawCircularModules` is set to `true`
*
* (default: `[]`)
*/
protected array $keepAsSquare = [];
/**
* Whether to connect the paths for the several module types to avoid weird glitches when using gradients etc.
*
* This option is exclusive to output classes that use the module collector `QROutputAbstract::collectModules()`,
* which converts the `$M_TYPE` of all modules to `QRMatrix::M_DATA` and `QRMatrix::M_DATA_DARK` respectively.
*
* Module types that should not be added to the connected path can be excluded via `QROptions::$excludeFromConnect`.
*
* Currentty used in `QREps` and `QRMarkupSVG`.
*
* @see \chillerlan\QRCode\Output\QROutputAbstract::collectModules()
* @see \chillerlan\QRCode\QROptionsTrait::$excludeFromConnect
* @see https://github.com/chillerlan/php-qrcode/issues/57
*/
protected bool $connectPaths = false;
/**
* Specify which paths/patterns to exclude from connecting if `QROptions::$connectPaths` is set to `true`
*
* @see \chillerlan\QRCode\QROptionsTrait::$connectPaths
*/
protected array $excludeFromConnect = [];
/**
* Module values map
*
* - `QRImagick`, `QRMarkupHTML`, `QRMarkupSVG`: #ABCDEF, cssname, rgb(), rgba()...
* - `QREps`, `QRFpdf`, `QRGdImage`: `[R, G, B]` // 0-255
* - `QREps`: `[C, M, Y, K]` // 0-255
*
* @see \chillerlan\QRCode\Output\QROutputAbstract::setModuleValues()
*/
protected array $moduleValues = [];
/**
* Toggles logo space creation
*
* @see \chillerlan\QRCode\QRCode::addMatrixModifications()
* @see \chillerlan\QRCode\Data\QRMatrix::setLogoSpace()
*/
protected bool $addLogoSpace = false;
/**
* Width of the logo space
*
* if only `QROptions::$logoSpaceWidth` is given, the logo space is assumed a square of that size
*/
protected ?int $logoSpaceWidth = null;
/**
* Height of the logo space
*
* if only `QROptions::$logoSpaceHeight` is given, the logo space is assumed a square of that size
*/
protected ?int $logoSpaceHeight = null;
/**
* Optional horizontal start position of the logo space (top left corner)
*/
protected ?int $logoSpaceStartX = null;
/**
* Optional vertical start position of the logo space (top left corner)
*/
protected ?int $logoSpaceStartY = null;
/*
* Common raster image settings (QRGdImage, QRImagick)
*/
/**
* Pixel size of a QR code module
*/
protected int $scale = 5;
/**
* Toggle transparency
*
* - `QRGdImage` and `QRImagick`: the given `QROptions::$transparencyColor` is set as transparent
*
* @see https://github.com/chillerlan/php-qrcode/discussions/121
*/
protected bool $imageTransparent = false;
/**
* Sets a transparency color for when `QROptions::$imageTransparent` is set to `true`.
*
* Defaults to `QROptions::$bgColor`.
*
* - `QRGdImage`: `[R, G, B]`, this color is set as transparent in `imagecolortransparent()`
* - `QRImagick`: `"color_str"`, this color is set in `Imagick::transparentPaintImage()`
*
* @see \imagecolortransparent()
* @see \Imagick::transparentPaintImage()
*
* @var mixed|null
*/
protected $transparencyColor = null;
/**
* Compression quality
*
* The given value depends on the used output type:
*
* - `QRGdImageBMP`: `[0...1]`
* - `QRGdImageJPEG`: `[0...100]`
* - `QRGdImageWEBP`: `[0...9]`
* - `QRGdImagePNG`: `[0...100]`
* - `QRImagick`: `[0...100]`
*
* @see \imagebmp()
* @see \imagejpeg()
* @see \imagepng()
* @see \imagewebp()
* @see \Imagick::setImageCompressionQuality()
*/
protected int $quality = -1;
/*
* QRGdImage settings
*/
/**
* Toggles the usage of internal upscaling when `QROptions::$drawCircularModules` is set to `true` and
* `QROptions::$scale` is less than 20
*
* @see \chillerlan\QRCode\Output\QRGdImage::createImage()
* @see https://github.com/chillerlan/php-qrcode/issues/23
*/
protected bool $gdImageUseUpscale = true;
/*
* QRImagick settings
*/
/**
* Imagick output format
*
* @see \Imagick::setImageFormat()
* @see https://www.imagemagick.org/script/formats.php
*/
protected string $imagickFormat = 'png32';
/*
* Common markup output settings (QRMarkupSVG, QRMarkupHTML)
*/
/**
* A common css class
*/
protected string $cssClass = 'qrcode';
/*
* QRMarkupSVG settings
*/
/**
* Whether to add an XML header line or not, e.g. to embed the SVG directly in HTML
*
* `<?xml version="1.0" encoding="UTF-8"?>`
*/
protected bool $svgAddXmlHeader = true;
/**
* Anything in the SVG `<defs>` tag
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
*/
protected string $svgDefs = '';
/**
* Sets the value for the "preserveAspectRatio" on the `<svg>` element
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
*/
protected string $svgPreserveAspectRatio = 'xMidYMid';
/**
* Whether to use the SVG `fill` attributes
*
* If set to `true` (default), the `fill` attribute will be set with the module value for the `<path>` element's `$M_TYPE`.
* When set to `false`, the module values map will be ignored and the QR Code may be styled via CSS.
*
* @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill
*/
protected bool $svgUseFillAttributes = true;
/*
* QRStringText settings
*/
/**
* An optional line prefix, e.g. empty space to align the QR Code in a console
*/
protected string $textLineStart = '';
/*
* QRStringJSON settings
*/
/**
* Sets the flags to use for the `json_encode()` call
*
* @see https://www.php.net/manual/json.constants.php
*/
protected int $jsonFlags = JSON_THROW_ON_ERROR;
/**
* Whether to return matrix values in JSON as booleans or `$M_TYPE` integers
*/
protected bool $jsonAsBooleans = false;
/*
* QRFpdf settings
*/
/**
* Measurement unit for `FPDF` output: `pt`, `mm`, `cm`, `in` (default: `pt`)
*
* @see FPDF::__construct()
*/
protected string $fpdfMeasureUnit = 'pt';
/*
* QR Code reader settings
*/
/**
* Use Imagick (if available) when reading QR Codes
*/
protected bool $readerUseImagickIfAvailable = false;
/**
* Grayscale the image before reading
*/
protected bool $readerGrayscale = false;
/**
* Invert the colors of the image
*/
protected bool $readerInvertColors = false;
/**
* Increase the contrast before reading
*
* note that applying contrast works different in GD and Imagick, so mileage may vary
*/
protected bool $readerIncreaseContrast = false;
/**
* clamp min/max version number
*/
protected function setMinMaxVersion(int $versionMin, int $versionMax):void{
$min = max(1, min(40, $versionMin));
$max = max(1, min(40, $versionMax));
$this->versionMin = min($min, $max);
$this->versionMax = max($min, $max);
}
/**
* sets the minimum version number
*/
protected function set_versionMin(int $version):void{
$this->setMinMaxVersion($version, $this->versionMax);
}
/**
* sets the maximum version number
*/
protected function set_versionMax(int $version):void{
$this->setMinMaxVersion($this->versionMin, $version);
}
/**
* sets/clamps the version number
*/
protected function set_version(int $version):void{
$this->version = ($version !== Version::AUTO) ? max(1, min(40, $version)) : Version::AUTO;
}
/**
* sets/clamps the quiet zone size
*/
protected function set_quietzoneSize(int $quietzoneSize):void{
$this->quietzoneSize = max(0, min($quietzoneSize, 75));
}
/**
* sets the FPDF measurement unit
*
* @codeCoverageIgnore
*/
protected function set_fpdfMeasureUnit(string $unit):void{
$unit = strtolower($unit);
if(in_array($unit, ['cm', 'in', 'mm', 'pt'], true)){
$this->fpdfMeasureUnit = $unit;
}
// @todo throw or ignore silently?
}
/**
* enables Imagick for the QR Code reader if the extension is available
*/
protected function set_readerUseImagickIfAvailable(bool $useImagickIfAvailable):void{
$this->readerUseImagickIfAvailable = ($useImagickIfAvailable && extension_loaded('imagick'));
}
/**
* clamp the logo space values between 0 and maximum length (177 modules at version 40)
*/
protected function clampLogoSpaceValue(?int $value):?int{
if($value === null){
return null;
}
return (int)max(0, min(177, $value));
}
/**
* clamp/set logo space width
*/
protected function set_logoSpaceWidth(?int $value):void{
$this->logoSpaceWidth = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set logo space height
*/
protected function set_logoSpaceHeight(?int $value):void{
$this->logoSpaceHeight = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set horizontal logo space start
*/
protected function set_logoSpaceStartX(?int $value):void{
$this->logoSpaceStartX = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set vertical logo space start
*/
protected function set_logoSpaceStartY(?int $value):void{
$this->logoSpaceStartY = $this->clampLogoSpaceValue($value);
}
/**
* clamp/set SVG circle radius
*/
protected function set_circleRadius(float $circleRadius):void{
$this->circleRadius = max(0.1, min(0.75, $circleRadius));
}
/*
* redirect calls of deprecated variables to new/renamed property
*/
/**
* @deprecated 5.0.0 use QROptions::$outputBase64 instead
* @see \chillerlan\QRCode\QROptions::$outputBase64
*/
protected bool $imageBase64;
/**
* redirect call to the new variable
*
* @deprecated 5.0.0 use QROptions::$outputBase64 instead
* @see \chillerlan\QRCode\QROptions::$outputBase64
* @codeCoverageIgnore
*/
protected function set_imageBase64(bool $imageBase64):void{
$this->outputBase64 = $imageBase64;
}
/**
* redirect call to the new variable
*
* @deprecated 5.0.0 use QROptions::$outputBase64 instead
* @see \chillerlan\QRCode\QROptions::$outputBase64
* @codeCoverageIgnore
*/
protected function get_imageBase64():bool{
return $this->outputBase64;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
*/
protected int $jpegQuality;
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function set_jpegQuality(int $jpegQuality):void{
$this->quality = $jpegQuality;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function get_jpegQuality():int{
return $this->quality;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
*/
protected int $pngCompression;
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function set_pngCompression(int $pngCompression):void{
$this->quality = $pngCompression;
}
/**
* @deprecated 5.0.0 use QROptions::$quality instead
* @see \chillerlan\QRCode\QROptions::$quality
* @codeCoverageIgnore
*/
protected function get_pngCompression():int{
return $this->quality;
}
/**
* @deprecated 5.0.0 use QROptions::$transparencyColor instead
* @see \chillerlan\QRCode\QROptions::$transparencyColor
*/
protected array $imageTransparencyBG;
/**
* @deprecated 5.0.0 use QROptions::$transparencyColor instead
* @see \chillerlan\QRCode\QROptions::$transparencyColor
* @codeCoverageIgnore
*/
protected function set_imageTransparencyBG(?array $imageTransparencyBG):void{
$this->transparencyColor = $imageTransparencyBG;
}
/**
* @deprecated 5.0.0 use QROptions::$transparencyColor instead
* @see \chillerlan\QRCode\QROptions::$transparencyColor
* @codeCoverageIgnore
*/
protected function get_imageTransparencyBG():?array{
return $this->transparencyColor;
}
/**
* @deprecated 5.0.0 use QROptions::$bgColor instead
* @see \chillerlan\QRCode\QROptions::$bgColor
*/
protected string $imagickBG;
/**
* @deprecated 5.0.0 use QROptions::$bgColor instead
* @see \chillerlan\QRCode\QROptions::$bgColor
* @codeCoverageIgnore
*/
protected function set_imagickBG(?string $imagickBG):void{
$this->bgColor = $imagickBG;
}
/**
* @deprecated 5.0.0 use QROptions::$bgColor instead
* @see \chillerlan\QRCode\QROptions::$bgColor
* @codeCoverageIgnore
*/
protected function get_imagickBG():?string{
return $this->bgColor;
}
}