├── .github └── workflows │ ├── static_tests.yml │ └── tests.yml ├── .gitignore ├── LICENSE-ASL-2.0 ├── LICENSE-MIT ├── README.md ├── composer.json ├── ecs.php ├── lib ├── Binarizer.php ├── BinaryBitmap.php ├── ChecksumException.php ├── Common │ ├── AbstractEnum.php │ ├── BitArray.php │ ├── BitMatrix.php │ ├── BitSource.php │ ├── CharacterSetECI.php │ ├── DecoderResult.php │ ├── DefaultGridSampler.php │ ├── Detector │ │ ├── MathUtils.php │ │ └── MonochromeRectangleDetector.php │ ├── DetectorResult.php │ ├── GlobalHistogramBinarizer.php │ ├── GridSampler.php │ ├── HybridBinarizer.php │ ├── PerspectiveTransform.php │ ├── Reedsolomon │ │ ├── GenericGF.php │ │ ├── GenericGFPoly.php │ │ ├── ReedSolomonDecoder.php │ │ └── ReedSolomonException.php │ └── customFunctions.php ├── FormatException.php ├── GDLuminanceSource.php ├── IMagickLuminanceSource.php ├── LuminanceSource.php ├── NotFoundException.php ├── PlanarYUVLuminanceSource.php ├── QrReader.php ├── Qrcode │ ├── Decoder │ │ ├── BitMatrixParser.php │ │ ├── DataBlock.php │ │ ├── DataMask.php │ │ ├── DecodedBitStreamParser.php │ │ ├── Decoder.php │ │ ├── ErrorCorrectionLevel.php │ │ ├── FormatInformation.php │ │ ├── Mode.php │ │ ├── QRCodeDecoderMetaData.php │ │ └── Version.php │ ├── Detector │ │ ├── AlignmentPattern.php │ │ ├── AlignmentPatternFinder.php │ │ ├── Detector.php │ │ ├── FinderPattern.php │ │ ├── FinderPatternFinder.php │ │ └── FinderPatternInfo.php │ └── QRCodeReader.php ├── RGBLuminanceSource.php ├── Reader.php ├── ReaderException.php ├── Result.php └── ResultPoint.php ├── phpunit.xml.dist ├── psalm.xml ├── rector.php └── tests ├── QrReaderTest.php └── qrcodes ├── 139225861-398ccbbd-2bfd-4736-889b-878c10573888.png ├── 174419877-f6b5dae1-2251-4b67-95f1-5e1143e40fae.jpg ├── empty.png ├── hello_world.png └── test.png /.github/workflows/static_tests.yml: -------------------------------------------------------------------------------- 1 | name: Static Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | static_tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: 8.1 17 | coverage: none # disable xdebug, pcov 18 | - run: composer install --no-progress --no-interaction --no-suggest 19 | - run: composer static-tests 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php: 15 | - "8.1" 16 | - "8.2" 17 | - "8.3" 18 | dependency-version: 19 | # - prefer-lowest 20 | - prefer-stable 21 | 22 | name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} - tests 23 | steps: 24 | # basically git clone 25 | - uses: actions/checkout@v4 26 | 27 | - name: Setup PHP 28 | # use PHP of specific version 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php }} 32 | coverage: none # disable xdebug, pcov 33 | tools: composer 34 | 35 | - name: Install Composer Dependencies 36 | run: | 37 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 38 | 39 | - name: Run PHPUnit Tests 40 | run: composer tests 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | .idea/ 4 | .phpunit.result.cache 5 | 6 | tests/qrcodes/private_test.png 7 | tests/qrcodes/private_test2.png -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Ashot Khanamiryan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QR code decoder / reader for PHP 2 | 3 | [![Tests](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/tests.yml/badge.svg)](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/tests.yml) 4 | [![Static Tests](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/static_tests.yml/badge.svg)](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/static_tests.yml) 5 | 6 | This is a PHP library to detect and decode QR-codes.
This is first and only QR code reader that works without extensions.
7 | Ported from [ZXing library](https://github.com/zxing/zxing) 8 | 9 | 10 | ## Installation 11 | The recommended method of installing this library is via [Composer](https://getcomposer.org/). 12 | 13 | Run the following command from your project root: 14 | 15 | ```bash 16 | $ composer require khanamiryan/qrcode-detector-decoder 17 | ``` 18 | 19 | 20 | ## Usage 21 | ```php 22 | require __DIR__ . "/vendor/autoload.php"; 23 | use Zxing\QrReader; 24 | $qrcode = new QrReader('path/to_image'); 25 | $text = $qrcode->text(); //return decoded text from QR Code 26 | ``` 27 | 28 | ## Requirements 29 | * PHP >= 8.1 30 | * GD Library 31 | 32 | 33 | ## Contributing 34 | 35 | You can help the project by adding features, cleaning the code, adding composer and other. 36 | 37 | 38 | 1. Fork it 39 | 2. Create your feature branch: `git checkout -b my-new-feature` 40 | 3. Commit your changes: `git commit -am 'Add some feature'` 41 | 4. Push to the branch: `git push origin my-new-feature` 42 | 5. Submit a pull request 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "khanamiryan/qrcode-detector-decoder", 3 | "type": "library", 4 | "description": "QR code decoder / reader", 5 | "keywords": [ 6 | "qr", 7 | "zxing", 8 | "barcode" 9 | ], 10 | "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder/", 11 | "license": [ 12 | "MIT", 13 | "Apache-2.0" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Ashot Khanamiryan", 18 | "email": "a.khanamiryan@gmail.com", 19 | "homepage": "https://github.com/khanamiryan", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": ">=8.1" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^7.5 | ^8.0 | ^9.0", 28 | "rector/rector": "^1.0.4", 29 | "symplify/easy-coding-standard": "^11.0", 30 | "vimeo/psalm": "^4.24" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Zxing\\": "lib/" 35 | }, 36 | "files": [ 37 | "lib/Common/customFunctions.php" 38 | ] 39 | }, 40 | "scripts": { 41 | "check-cs": "./vendor/bin/ecs check", 42 | "fix-cs": "./vendor/bin/ecs check --fix", 43 | "tests": "./vendor/bin/phpunit", 44 | "static-tests": "./vendor/bin/psalm --php-version=8.1" 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Khanamiryan\\QrCodeTests\\": "tests/" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([__DIR__ . '/lib', __DIR__ . '/tests']); 16 | 17 | // choose 18 | $configurator->sets([ 19 | SetList::CLEAN_CODE, SetList::PSR_12//, LevelSetList::UP_TO_PHP_81 //, SymfonySetList::SYMFONY_60 20 | ]); 21 | 22 | $configurator->ruleWithConfiguration(ConcatSpaceFixer::class, [ 23 | 'spacing' => 'one' 24 | ]); 25 | 26 | // indent and tabs/spaces 27 | // [default: spaces]. BUT: tabs are superiour due to accessibility reasons 28 | $configurator->indentation('tab'); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/Binarizer.php: -------------------------------------------------------------------------------- 1 | source; 43 | } 44 | 45 | /** 46 | * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return 47 | * cached data. Callers should assume this method is expensive and call it as seldom as possible. 48 | * This method is intended for decoding 1D barcodes and may choose to apply sharpening. 49 | * For callers which only examine one row of pixels at a time, the same BitArray should be reused 50 | * and passed in with each call for performance. However it is legal to keep more than one row 51 | * at a time if needed. 52 | * 53 | * @param int $y The row to fetch, which must be in [0, bitmap height) 54 | * @param BitArray|null $row An optional preallocated array. If null or too small, it will be ignored. 55 | * If used, the Binarizer will call BitArray.clear(). Always use the returned object. 56 | * 57 | * @return BitArray The array of bits for this row (true means black). 58 | * @throws NotFoundException if row can't be binarized 59 | */ 60 | abstract public function getBlackRow(int $y, ?BitArray $row = null): BitArray; 61 | 62 | /** 63 | * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive 64 | * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or 65 | * may not apply sharpening. Therefore, a row from this matrix may not be identical to one 66 | * fetched using getBlackRow(), so don't mix and match between them. 67 | * 68 | * @return BitMatrix The 2D array of bits for the image (true means black). 69 | * @throws NotFoundException if image can't be binarized to make a matrix 70 | */ 71 | abstract public function getBlackMatrix(); 72 | 73 | /** 74 | * Creates a new object with the same type as this Binarizer implementation, but with pristine 75 | * state. This is needed because Binarizer implementations may be stateful, e.g. keeping a cache 76 | * of 1 bit data. See Effective Java for why we can't use Java's clone() method. 77 | * 78 | * @param $source The LuminanceSource this Binarizer will operate on. 79 | * 80 | * @return Binarizer A new concrete Binarizer implementation object. 81 | */ 82 | abstract public function createBinarizer($source); 83 | 84 | final public function getWidth() 85 | { 86 | return $this->source->getWidth(); 87 | } 88 | 89 | final public function getHeight() 90 | { 91 | return $this->source->getHeight(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/BinaryBitmap.php: -------------------------------------------------------------------------------- 1 | binarizer = $binarizer; 39 | } 40 | 41 | /** 42 | * @return int The width of the bitmap. 43 | */ 44 | public function getWidth() 45 | { 46 | return $this->binarizer->getWidth(); 47 | } 48 | 49 | /** 50 | * @return int The height of the bitmap. 51 | */ 52 | public function getHeight() 53 | { 54 | return $this->binarizer->getHeight(); 55 | } 56 | 57 | /** 58 | * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return 59 | * cached data. Callers should assume this method is expensive and call it as seldom as possible. 60 | * This method is intended for decoding 1D barcodes and may choose to apply sharpening. 61 | * 62 | * @param $y The row to fetch, which must be in [0, bitmap height) 63 | * @param array|null $row An optional preallocated array. If null or too small, it will be ignored. 64 | * If used, the Binarizer will call BitArray.clear(). Always use the returned object. 65 | * 66 | * @return Common\BitArray The array of bits for this row (true means black). 67 | * 68 | * @throws NotFoundException if row can't be binarized 69 | */ 70 | public function getBlackRow($y, $row): Common\BitArray 71 | { 72 | return $this->binarizer->getBlackRow($y, $row); 73 | } 74 | 75 | /** 76 | * @return bool Whether this bitmap can be cropped. 77 | */ 78 | public function isCropSupported() 79 | { 80 | return $this->binarizer->getLuminanceSource()->isCropSupported(); 81 | } 82 | 83 | /** 84 | * Returns a new object with cropped image data. Implementations may keep a reference to the 85 | * original data rather than a copy. Only callable if isCropSupported() is true. 86 | * 87 | * @param $left The left coordinate, which must be in [0,getWidth()) 88 | * @param $top The top coordinate, which must be in [0,getHeight()) 89 | * @param $width The width of the rectangle to crop. 90 | * @param $height The height of the rectangle to crop. 91 | * 92 | * @return BinaryBitmap A cropped version of this object. 93 | */ 94 | public function crop($left, $top, $width, $height): \Zxing\BinaryBitmap 95 | { 96 | $newSource = $this->binarizer->getLuminanceSource()->crop($left, $top, $width, $height); 97 | 98 | return new BinaryBitmap($this->binarizer->createBinarizer($newSource)); 99 | } 100 | 101 | /** 102 | * @return bool this Whether bitmap supports counter-clockwise rotation. 103 | */ 104 | public function isRotateSupported() 105 | { 106 | return $this->binarizer->getLuminanceSource()->isRotateSupported(); 107 | } 108 | 109 | /** 110 | * Returns a new object with rotated image data by 90 degrees counterclockwise. 111 | * Only callable if {@link #isRotateSupported()} is true. 112 | * 113 | * @return BinaryBitmap A rotated version of this object. 114 | */ 115 | public function rotateCounterClockwise(): \Zxing\BinaryBitmap 116 | { 117 | $newSource = $this->binarizer->getLuminanceSource()->rotateCounterClockwise(); 118 | 119 | return new BinaryBitmap($this->binarizer->createBinarizer($newSource)); 120 | } 121 | 122 | /** 123 | * Returns a new object with rotated image data by 45 degrees counterclockwise. 124 | * Only callable if {@link #isRotateSupported()} is true. 125 | * 126 | * @return BinaryBitmap A rotated version of this object. 127 | */ 128 | public function rotateCounterClockwise45(): \Zxing\BinaryBitmap 129 | { 130 | $newSource = $this->binarizer->getLuminanceSource()->rotateCounterClockwise45(); 131 | 132 | return new BinaryBitmap($this->binarizer->createBinarizer($newSource)); 133 | } 134 | 135 | public function toString(): string 136 | { 137 | try { 138 | return $this->getBlackMatrix()->toString(); 139 | } catch (NotFoundException) { 140 | } 141 | 142 | return ''; 143 | } 144 | 145 | /** 146 | * Converts a 2D array of luminance data to 1 bit. As above, assume this method is expensive 147 | * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or 148 | * may not apply sharpening. Therefore, a row from this matrix may not be identical to one 149 | * fetched using getBlackRow(), so don't mix and match between them. 150 | * 151 | * @return BitMatrix The 2D array of bits for the image (true means black). 152 | * @throws NotFoundException if image can't be binarized to make a matrix 153 | */ 154 | public function getBlackMatrix() 155 | { 156 | // The matrix is created on demand the first time it is requested, then cached. There are two 157 | // reasons for this: 158 | // 1. This work will never be done if the caller only installs 1D Reader objects, or if a 159 | // 1D Reader finds a barcode before the 2D Readers run. 160 | // 2. This work will only be done once even if the caller installs multiple 2D Readers. 161 | if ($this->matrix === null) { 162 | $this->matrix = $this->binarizer->getBlackMatrix(); 163 | } 164 | 165 | return $this->matrix; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/ChecksumException.php: -------------------------------------------------------------------------------- 1 | |null 27 | */ 28 | private ?array $constants = null; 29 | 30 | /** 31 | * Creates a new enum. 32 | * 33 | * @param mixed $initialValue 34 | * @param boolean $strict 35 | */ 36 | public function __construct($initialValue = null, private $strict = false) 37 | { 38 | $this->change($initialValue); 39 | } 40 | 41 | /** 42 | * Changes the value of the enum. 43 | * 44 | * @param mixed $value 45 | * 46 | * @return void 47 | */ 48 | public function change($value) 49 | { 50 | if (!in_array($value, $this->getConstList(), $this->strict)) { 51 | throw new \UnexpectedValueException('Value not a const in enum ' . $this::class); 52 | } 53 | $this->value = $value; 54 | } 55 | 56 | /** 57 | * Gets all constants (possible values) as an array. 58 | * 59 | * 60 | * @return array 61 | */ 62 | public function getConstList(bool $includeDefault = true) 63 | { 64 | $constants = []; 65 | if ($this->constants === null) { 66 | $reflection = new ReflectionClass($this); 67 | $this->constants = $reflection->getConstants(); 68 | } 69 | if ($includeDefault) { 70 | return $this->constants; 71 | } 72 | $constants = $this->constants; 73 | unset($constants['__default']); 74 | 75 | return $constants; 76 | } 77 | 78 | /** 79 | * Gets current value. 80 | * 81 | * @return mixed 82 | */ 83 | public function get() 84 | { 85 | return $this->value; 86 | } 87 | 88 | /** 89 | * Gets the name of the enum. 90 | * 91 | * @return string 92 | */ 93 | public function __toString(): string 94 | { 95 | return (string)array_search($this->value, $this->getConstList()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/Common/BitSource.php: -------------------------------------------------------------------------------- 1 | This provides an easy abstraction to read bits at a time from a sequence of bytes, where the 22 | * number of bits read is not often a multiple of 8.

23 | * 24 | *

This class is thread-safe but not reentrant -- unless the caller modifies the bytes array 25 | * it passed in, in which case all bets are off.

26 | * 27 | * @author Sean Owen 28 | */ 29 | final class BitSource 30 | { 31 | private int $byteOffset = 0; 32 | private int $bitOffset = 0; 33 | 34 | /** 35 | * @param array $bytes bytes from which this will read bits. Bits will be read from the first byte first. 36 | * Bits are read within a byte from most-significant to least-significant bit. 37 | */ 38 | public function __construct(private array $bytes) 39 | { 40 | } 41 | 42 | /** 43 | * @return int of next bit in current byte which would be read by the next call to {@link #readBits(int)}. 44 | */ 45 | public function getBitOffset(): int 46 | { 47 | return $this->bitOffset; 48 | } 49 | 50 | /** 51 | * @return int of next byte in input byte array which would be read by the next call to {@link #readBits(int)}. 52 | */ 53 | public function getByteOffset(): int 54 | { 55 | return $this->byteOffset; 56 | } 57 | 58 | /** 59 | * @param int $numBits number of bits to read 60 | * 61 | * @return int representing the bits read. The bits will appear as the least-significant 62 | * bits of the int 63 | * @throws \InvalidArgumentException if numBits isn't in [1,32] or more than is available 64 | */ 65 | public function readBits($numBits) 66 | { 67 | if ($numBits < 1 || $numBits > 32 || $numBits > $this->available()) { 68 | throw new \InvalidArgumentException(strval($numBits)); 69 | } 70 | 71 | $result = 0; 72 | 73 | // First, read remainder from current byte 74 | if ($this->bitOffset > 0) { 75 | $bitsLeft = 8 - $this->bitOffset; 76 | $toRead = $numBits < $bitsLeft ? $numBits : $bitsLeft; 77 | $bitsToNotRead = $bitsLeft - $toRead; 78 | $mask = (0xFF >> (8 - $toRead)) << $bitsToNotRead; 79 | $result = ($this->bytes[$this->byteOffset] & $mask) >> $bitsToNotRead; 80 | $numBits -= $toRead; 81 | $this->bitOffset += $toRead; 82 | if ($this->bitOffset == 8) { 83 | $this->bitOffset = 0; 84 | $this->byteOffset++; 85 | } 86 | } 87 | 88 | // Next read whole bytes 89 | if ($numBits > 0) { 90 | while ($numBits >= 8) { 91 | $result = ($result << 8) | ($this->bytes[$this->byteOffset] & 0xFF); 92 | $this->byteOffset++; 93 | $numBits -= 8; 94 | } 95 | 96 | // Finally read a partial byte 97 | if ($numBits > 0) { 98 | $bitsToNotRead = 8 - $numBits; 99 | $mask = (0xFF >> $bitsToNotRead) << $bitsToNotRead; 100 | $result = ($result << $numBits) | (($this->bytes[$this->byteOffset] & $mask) >> $bitsToNotRead); 101 | $this->bitOffset += $numBits; 102 | } 103 | } 104 | 105 | return $result; 106 | } 107 | 108 | /** 109 | * @return int of bits that can be read successfully 110 | */ 111 | public function available(): int 112 | { 113 | return 8 * ((is_countable($this->bytes) ? count($this->bytes) : 0) - $this->byteOffset) - $this->bitOffset; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/Common/CharacterSetECI.php: -------------------------------------------------------------------------------- 1 | self::ISO8859_1, 131 | 'ISO-8859-2' => self::ISO8859_2, 132 | 'ISO-8859-3' => self::ISO8859_3, 133 | 'ISO-8859-4' => self::ISO8859_4, 134 | 'ISO-8859-5' => self::ISO8859_5, 135 | 'ISO-8859-6' => self::ISO8859_6, 136 | 'ISO-8859-7' => self::ISO8859_7, 137 | 'ISO-8859-8' => self::ISO8859_8, 138 | 'ISO-8859-9' => self::ISO8859_9, 139 | 'ISO-8859-10' => self::ISO8859_10, 140 | 'ISO-8859-11' => self::ISO8859_11, 141 | 'ISO-8859-12' => self::ISO8859_12, 142 | 'ISO-8859-13' => self::ISO8859_13, 143 | 'ISO-8859-14' => self::ISO8859_14, 144 | 'ISO-8859-15' => self::ISO8859_15, 145 | 'ISO-8859-16' => self::ISO8859_16, 146 | 'SHIFT-JIS' => self::SJIS, 147 | 'WINDOWS-1250' => self::CP1250, 148 | 'WINDOWS-1251' => self::CP1251, 149 | 'WINDOWS-1252' => self::CP1252, 150 | 'WINDOWS-1256' => self::CP1256, 151 | 'UTF-16BE' => self::UNICODE_BIG_UNMARKED, 152 | 'UTF-8' => self::UTF8, 153 | 'ASCII' => self::ASCII, 154 | 'GBK' => self::GB18030, 155 | 'EUC-KR' => self::EUC_KR, 156 | ]; 157 | /**#@-*/ 158 | /** 159 | * Additional possible values for character sets. 160 | */ 161 | private static array $additionalValues = [ 162 | self::CP437 => 2, 163 | self::ASCII => 170, 164 | ]; 165 | private static int|string|null $name = null; 166 | 167 | /** 168 | * Gets character set ECI by value. 169 | * 170 | * 171 | * @return CharacterSetEci|null 172 | */ 173 | public static function getCharacterSetECIByValue(string $value) 174 | { 175 | if ($value < 0 || $value >= 900) { 176 | throw new \InvalidArgumentException('Value must be between 0 and 900'); 177 | } 178 | if (false !== ($key = array_search($value, self::$additionalValues))) { 179 | $value = $key; 180 | } 181 | array_search($value, self::$nameToEci); 182 | try { 183 | self::setName($value); 184 | 185 | return new self($value); 186 | } catch (\UnexpectedValueException) { 187 | return null; 188 | } 189 | } 190 | 191 | /** 192 | * @param (int|string) $value 193 | * 194 | * @psalm-param array-key $value 195 | * 196 | * @return null|true 197 | */ 198 | private static function setName($value) 199 | { 200 | foreach (self::$nameToEci as $name => $key) { 201 | if ($key == $value) { 202 | self::$name = $name; 203 | 204 | return true; 205 | } 206 | } 207 | if (self::$name == null) { 208 | foreach (self::$additionalValues as $name => $key) { 209 | if ($key == $value) { 210 | self::$name = $name; 211 | 212 | return true; 213 | } 214 | } 215 | } 216 | } 217 | 218 | /** 219 | * Gets character set ECI name. 220 | * 221 | * @return int|null|string set ECI name|null 222 | */ 223 | public static function name(): string|int|null 224 | { 225 | return self::$name; 226 | } 227 | 228 | /** 229 | * Gets character set ECI by name. 230 | * 231 | * 232 | * @return CharacterSetEci|null 233 | */ 234 | public static function getCharacterSetECIByName(string $name) 235 | { 236 | $name = strtoupper($name); 237 | if (isset(self::$nameToEci[$name])) { 238 | return new self(self::$nameToEci[$name]); 239 | } 240 | 241 | return null; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /lib/Common/DecoderResult.php: -------------------------------------------------------------------------------- 1 | Encapsulates the result of decoding a matrix of bits. This typically 22 | * applies to 2D barcode formats. For now it contains the raw bytes obtained, 23 | * as well as a String interpretation of those bytes, if applicable.

24 | * 25 | * @author Sean Owen 26 | */ 27 | final class DecoderResult 28 | { 29 | /** 30 | * @var mixed|null 31 | */ 32 | private $errorsCorrected; 33 | /** 34 | * @var mixed|null 35 | */ 36 | private $erasures; 37 | /** 38 | * @var mixed|null 39 | */ 40 | private $other; 41 | 42 | 43 | public function __construct(private $rawBytes, private $text, private $byteSegments, private $ecLevel, private $structuredAppendSequenceNumber = -1, private $structuredAppendParity = -1) 44 | { 45 | } 46 | 47 | public function getRawBytes() 48 | { 49 | return $this->rawBytes; 50 | } 51 | 52 | public function getText() 53 | { 54 | return $this->text; 55 | } 56 | 57 | public function getByteSegments() 58 | { 59 | return $this->byteSegments; 60 | } 61 | 62 | public function getECLevel() 63 | { 64 | return $this->ecLevel; 65 | } 66 | 67 | public function getErrorsCorrected() 68 | { 69 | return $this->errorsCorrected; 70 | } 71 | 72 | public function setErrorsCorrected($errorsCorrected): void 73 | { 74 | $this->errorsCorrected = $errorsCorrected; 75 | } 76 | 77 | public function getErasures() 78 | { 79 | return $this->erasures; 80 | } 81 | 82 | public function setErasures($erasures): void 83 | { 84 | $this->erasures = $erasures; 85 | } 86 | 87 | public function getOther() 88 | { 89 | return $this->other; 90 | } 91 | 92 | public function setOther(\Zxing\Qrcode\Decoder\QRCodeDecoderMetaData $other): void 93 | { 94 | $this->other = $other; 95 | } 96 | 97 | public function hasStructuredAppend(): bool 98 | { 99 | return $this->structuredAppendParity >= 0 && $this->structuredAppendSequenceNumber >= 0; 100 | } 101 | 102 | public function getStructuredAppendParity() 103 | { 104 | return $this->structuredAppendParity; 105 | } 106 | 107 | public function getStructuredAppendSequenceNumber() 108 | { 109 | return $this->structuredAppendSequenceNumber; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/Common/DefaultGridSampler.php: -------------------------------------------------------------------------------- 1 | sampleGrid_($image, $dimensionX, $dimensionY, $transform); 71 | } 72 | 73 | 74 | /** 75 | * @return BitMatrix 76 | */ 77 | public function sampleGrid_( 78 | BitMatrix $image, 79 | int $dimensionX, 80 | int $dimensionY, 81 | PerspectiveTransform $transform 82 | ): BitMatrix { 83 | if ($dimensionX <= 0 || $dimensionY <= 0) { 84 | throw new NotFoundException("X or Y dimensions smaller than zero"); 85 | } 86 | $bits = new BitMatrix($dimensionX, $dimensionY); 87 | $points = fill_array(0, 2 * $dimensionX, 0.0); 88 | for ($y = 0; $y < $dimensionY; $y++) { 89 | $max = is_countable($points) ? count($points) : 0; 90 | $iValue = (float)$y + 0.5; 91 | for ($x = 0; $x < $max; $x += 2) { 92 | $points[$x] = (float)($x / 2) + 0.5; 93 | $points[$x + 1] = $iValue; 94 | } 95 | $transform->transformPoints($points); 96 | // Quick check to see if points transformed to something inside the image; 97 | // sufficient to check the endpoints 98 | self::checkAndNudgePoints($image, $points); 99 | try { 100 | for ($x = 0; $x < $max; $x += 2) { 101 | if ($image->get((int)$points[$x], (int)$points[$x + 1])) { 102 | // Black(-ish) pixel 103 | $bits->set($x / 2, $y); 104 | } 105 | } 106 | } catch (\Exception) { //ArrayIndexOutOfBoundsException 107 | // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting 108 | // transform gets "twisted" such that it maps a straight line of points to a set of points 109 | // whose endpoints are in bounds, but others are not. There is probably some mathematical 110 | // way to detect this about the transformation that I don't know yet. 111 | // This results in an ugly runtime exception despite our clever checks above -- can't have 112 | // that. We could check each point's coordinates but that feels duplicative. We settle for 113 | // catching and wrapping ArrayIndexOutOfBoundsException. 114 | throw new NotFoundException("ArrayIndexOutOfBoundsException"); 115 | } 116 | } 117 | 118 | return $bits; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/Common/Detector/MathUtils.php: -------------------------------------------------------------------------------- 1 | A somewhat generic detector that looks for a barcode-like rectangular region within an image. 30 | * It looks within a mostly white region of an image for a region of black and white, but mostly 31 | * black. It returns the four corners of the region, as best it can determine.

32 | * 33 | * @author Sean Owen 34 | * @port Ashot Khanamiryan 35 | */ 36 | class MonochromeRectangleDetector 37 | { 38 | private static int $MAX_MODULES = 32; 39 | 40 | public function __construct(private readonly BinaryBitmap $image) 41 | { 42 | } 43 | 44 | /** 45 | *

Detects a rectangular region of black and white -- mostly black -- with a region of mostly 46 | * white, in an image.

47 | * 48 | * @return {@link ResultPoint}[] describing the corners of the rectangular region. The first and 49 | * last points are opposed on the diagonal, as are the second and third. The first point will be 50 | * the topmost point and the last, the bottommost. The second point will be leftmost and the 51 | * third, the rightmost 52 | * @throws NotFoundException if no Data Matrix Code can be found 53 | */ 54 | public function detect(): \Zxing\ResultPoint 55 | { 56 | $height = $this->image->getHeight(); 57 | $width = $this->image->getWidth(); 58 | $halfHeight = $height / 2; 59 | $halfWidth = $width / 2; 60 | 61 | $deltaY = max(1, $height / (self::$MAX_MODULES * 8)); 62 | $deltaX = max(1, $width / (self::$MAX_MODULES * 8)); 63 | 64 | 65 | $top = 0; 66 | $bottom = $height; 67 | $left = 0; 68 | $right = $width; 69 | $pointA = $this->findCornerFromCenter( 70 | $halfWidth, 71 | 0, 72 | $left, 73 | $right, 74 | $halfHeight, 75 | -$deltaY, 76 | $top, 77 | $bottom, 78 | $halfWidth / 2 79 | ); 80 | $top = (int)$pointA->getY() - 1; 81 | $pointB = $this->findCornerFromCenter( 82 | $halfWidth, 83 | -$deltaX, 84 | $left, 85 | $right, 86 | $halfHeight, 87 | 0, 88 | $top, 89 | $bottom, 90 | $halfHeight / 2 91 | ); 92 | $left = (int)$pointB->getX() - 1; 93 | $pointC = $this->findCornerFromCenter( 94 | $halfWidth, 95 | $deltaX, 96 | $left, 97 | $right, 98 | $halfHeight, 99 | 0, 100 | $top, 101 | $bottom, 102 | $halfHeight / 2 103 | ); 104 | $right = (int)$pointC->getX() + 1; 105 | $pointD = $this->findCornerFromCenter( 106 | $halfWidth, 107 | 0, 108 | $left, 109 | $right, 110 | $halfHeight, 111 | $deltaY, 112 | $top, 113 | $bottom, 114 | $halfWidth / 2 115 | ); 116 | $bottom = (int)$pointD->getY() + 1; 117 | 118 | // Go try to find po$A again with better information -- might have been off at first. 119 | $pointA = $this->findCornerFromCenter( 120 | $halfWidth, 121 | 0, 122 | $left, 123 | $right, 124 | $halfHeight, 125 | -$deltaY, 126 | $top, 127 | $bottom, 128 | $halfWidth / 4 129 | ); 130 | 131 | return new ResultPoint($pointA, $pointB, $pointC, $pointD); 132 | } 133 | 134 | 135 | /** 136 | * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center 137 | * point which should be within the barcode. 138 | * 139 | * @param float $centerX center's x component (horizontal) 140 | * @param float $deltaX same as deltaY but change in x per step instead 141 | * @param int $left 142 | * @param int $right 143 | * @param float $centerY center's y component (vertical) 144 | * @param float $deltaY change in y per step. If scanning up this is negative; down, positive; 145 | * left or right, 0 146 | * @param int $top 147 | * @param int $bottom 148 | * @param float $maxWhiteRun maximum run of white pixels that can still be considered to be within 149 | * the barcode 150 | * 151 | * @return ResultPoint {@link com.google.zxing.ResultPoint} encapsulating the corner that was found 152 | * 153 | * @throws NotFoundException if such a point cannot be found 154 | */ 155 | private function findCornerFromCenter( 156 | int|float $centerX, 157 | int|float $deltaX, 158 | int $left, 159 | int $right, 160 | int|float $centerY, 161 | float|int $deltaY, 162 | int $top, 163 | int $bottom, 164 | int|float $maxWhiteRun 165 | ): \Zxing\ResultPoint { 166 | $lastRange = null; 167 | for ($y = $centerY, $x = $centerX; 168 | $y < $bottom && $y >= $top && $x < $right && $x >= $left; 169 | $y += $deltaY, $x += $deltaX) { 170 | $range = 0; 171 | if ($deltaX == 0) { 172 | // horizontal slices, up and down 173 | $range = $this->blackWhiteRange($y, $maxWhiteRun, $left, $right, true); 174 | } else { 175 | // vertical slices, left and right 176 | $range = $this->blackWhiteRange($x, $maxWhiteRun, $top, $bottom, false); 177 | } 178 | if ($range == null) { 179 | if ($lastRange == null) { 180 | throw new NotFoundException("No corner from center found"); 181 | } 182 | // lastRange was found 183 | if ($deltaX == 0) { 184 | $lastY = $y - $deltaY; 185 | if ($lastRange[0] < $centerX) { 186 | if ($lastRange[1] > $centerX) { 187 | // straddle, choose one or the other based on direction 188 | return new ResultPoint($deltaY > 0 ? $lastRange[0] : $lastRange[1], $lastY); 189 | } 190 | 191 | return new ResultPoint($lastRange[0], $lastY); 192 | } else { 193 | return new ResultPoint($lastRange[1], $lastY); 194 | } 195 | } else { 196 | $lastX = $x - $deltaX; 197 | if ($lastRange[0] < $centerY) { 198 | if ($lastRange[1] > $centerY) { 199 | return new ResultPoint($lastX, $deltaX < 0 ? $lastRange[0] : $lastRange[1]); 200 | } 201 | 202 | return new ResultPoint($lastX, $lastRange[0]); 203 | } else { 204 | return new ResultPoint($lastX, $lastRange[1]); 205 | } 206 | } 207 | } 208 | $lastRange = $range; 209 | } 210 | throw new NotFoundException("No corner from center found"); 211 | } 212 | 213 | 214 | /** 215 | * Computes the start and end of a region of pixels, either horizontally or vertically, that could 216 | * be part of a Data Matrix barcode. 217 | * 218 | * @param float|int $fixedDimension 219 | * @param float|int $maxWhiteRun 220 | * @param int $minDim 221 | * @param int $maxDim 222 | * @param bool $horizontal 223 | * 224 | * @return (float|int)[]|null with start and end of found range, or null if no such range is found (e.g. only white was found) 225 | * 226 | * @psalm-return array{0: float|int, 1: float|int}|null 227 | */ 228 | private function blackWhiteRange(float|int $fixedDimension, int|float $maxWhiteRun, int $minDim, int $maxDim, bool $horizontal): array|null 229 | { 230 | $center = ($minDim + $maxDim) / 2; 231 | 232 | // Scan left/up first 233 | $start = $center; 234 | while ($start >= $minDim) { 235 | if ($horizontal ? $this->image->get($start, $fixedDimension) : $this->image->get($fixedDimension, $start)) { 236 | $start--; 237 | } else { 238 | $whiteRunStart = $start; 239 | do { 240 | $start--; 241 | } while ($start >= $minDim && !($horizontal ? $this->image->get($start, $fixedDimension) : 242 | $this->image->get($fixedDimension, $start))); 243 | $whiteRunSize = $whiteRunStart - $start; 244 | if ($start < $minDim || $whiteRunSize > $maxWhiteRun) { 245 | $start = $whiteRunStart; 246 | break; 247 | } 248 | } 249 | } 250 | $start++; 251 | 252 | // Then try right/down 253 | $end = $center; 254 | while ($end < $maxDim) { 255 | if ($horizontal ? $this->image->get($end, $fixedDimension) : $this->image->get($fixedDimension, $end)) { 256 | $end++; 257 | } else { 258 | $whiteRunStart = $end; 259 | do { 260 | $end++; 261 | } while ($end < $maxDim && !($horizontal ? $this->image->get($end, $fixedDimension) : 262 | $this->image->get($fixedDimension, $end))); 263 | $whiteRunSize = $end - $whiteRunStart; 264 | if ($end >= $maxDim || $whiteRunSize > $maxWhiteRun) { 265 | $end = $whiteRunStart; 266 | break; 267 | } 268 | } 269 | } 270 | $end--; 271 | 272 | return $end > $start ? [$start, $end] : null; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /lib/Common/DetectorResult.php: -------------------------------------------------------------------------------- 1 | Encapsulates the result of detecting a barcode in an image. This includes the raw 22 | * matrix of black/white pixels corresponding to the barcode, and possibly points of interest 23 | * in the image, like the location of finder patterns or corners of the barcode in the image.

24 | * 25 | * @author Sean Owen 26 | */ 27 | class DetectorResult 28 | { 29 | public function __construct(private $bits, private $points) 30 | { 31 | } 32 | 33 | final public function getBits() 34 | { 35 | return $this->bits; 36 | } 37 | 38 | final public function getPoints() 39 | { 40 | return $this->points; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/Common/GlobalHistogramBinarizer.php: -------------------------------------------------------------------------------- 1 | luminances = self::$EMPTY; 57 | $this->buckets = fill_array(0, self::$LUMINANCE_BUCKETS, 0); 58 | $this->source = $source; 59 | } 60 | 61 | // Applies simple sharpening to the row data to improve performance of the 1D Readers. 62 | public function getBlackRow(int $y, ?BitArray $row = null): BitArray 63 | { 64 | $this->source = $this->getLuminanceSource(); 65 | $width = $this->source->getWidth(); 66 | if ($row == null || $row->getSize() < $width) { 67 | $row = new BitArray($width); 68 | } else { 69 | $row->clear(); 70 | } 71 | 72 | $this->initArrays($width); 73 | $localLuminances = $this->source->getRow($y, $this->luminances); 74 | $localBuckets = $this->buckets; 75 | for ($x = 0; $x < $width; $x++) { 76 | $pixel = $localLuminances[$x] & 0xff; 77 | $localBuckets[$pixel >> self::$LUMINANCE_SHIFT]++; 78 | } 79 | $blackPoint = self::estimateBlackPoint($localBuckets); 80 | 81 | $left = $localLuminances[0] & 0xff; 82 | $center = $localLuminances[1] & 0xff; 83 | for ($x = 1; $x < $width - 1; $x++) { 84 | $right = $localLuminances[$x + 1] & 0xff; 85 | // A simple -1 4 -1 box filter with a weight of 2. 86 | $luminance = (($center * 4) - $left - $right) / 2; 87 | if ($luminance < $blackPoint) { 88 | $row->set($x); 89 | } 90 | $left = $center; 91 | $center = $right; 92 | } 93 | 94 | return $row; 95 | } 96 | 97 | // Does not sharpen the data, as this call is intended to only be used by 2D Readers. 98 | private function initArrays(float $luminanceSize): void 99 | { 100 | if (count($this->luminances) < $luminanceSize) { 101 | $this->luminances = []; 102 | } 103 | for ($x = 0; $x < self::$LUMINANCE_BUCKETS; $x++) { 104 | $this->buckets[$x] = 0; 105 | } 106 | } 107 | 108 | private static function estimateBlackPoint(array $buckets): int 109 | { 110 | // Find the tallest peak in the histogram. 111 | $numBuckets = is_countable($buckets) ? count($buckets) : 0; 112 | $maxBucketCount = 0; 113 | $firstPeak = 0; 114 | $firstPeakSize = 0; 115 | for ($x = 0; $x < $numBuckets; $x++) { 116 | if ($buckets[$x] > $firstPeakSize) { 117 | $firstPeak = $x; 118 | $firstPeakSize = $buckets[$x]; 119 | } 120 | if ($buckets[$x] > $maxBucketCount) { 121 | $maxBucketCount = $buckets[$x]; 122 | } 123 | } 124 | 125 | // Find the second-tallest peak which is somewhat far from the tallest peak. 126 | $secondPeak = 0; 127 | $secondPeakScore = 0; 128 | for ($x = 0; $x < $numBuckets; $x++) { 129 | $distanceToBiggest = $x - $firstPeak; 130 | // Encourage more distant second peaks by multiplying by square of distance. 131 | $score = $buckets[$x] * $distanceToBiggest * $distanceToBiggest; 132 | if ($score > $secondPeakScore) { 133 | $secondPeak = $x; 134 | $secondPeakScore = $score; 135 | } 136 | } 137 | 138 | // Make sure firstPeak corresponds to the black peak. 139 | if ($firstPeak > $secondPeak) { 140 | $temp = $firstPeak; 141 | $firstPeak = $secondPeak; 142 | $secondPeak = $temp; 143 | } 144 | 145 | // If there is too little contrast in the image to pick a meaningful black point, throw rather 146 | // than waste time trying to decode the image, and risk false positives. 147 | if ($secondPeak - $firstPeak <= $numBuckets / 16) { 148 | throw new NotFoundException("too little contrast in the image to pick a meaningful black point"); 149 | } 150 | 151 | // Find a valley between them that is low and closer to the white peak. 152 | $bestValley = $secondPeak - 1; 153 | $bestValleyScore = -1; 154 | for ($x = $secondPeak - 1; $x > $firstPeak; $x--) { 155 | $fromFirst = $x - $firstPeak; 156 | $score = $fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]); 157 | if ($score > $bestValleyScore) { 158 | $bestValley = $x; 159 | $bestValleyScore = $score; 160 | } 161 | } 162 | 163 | return $bestValley << self::$LUMINANCE_SHIFT; 164 | } 165 | 166 | public function getBlackMatrix() 167 | { 168 | $source = $this->getLuminanceSource(); 169 | $width = $source->getWidth(); 170 | $height = $source->getHeight(); 171 | $matrix = new BitMatrix($width, $height); 172 | 173 | // Quickly calculates the histogram by sampling four rows from the image. This proved to be 174 | // more robust on the blackbox tests than sampling a diagonal as we used to do. 175 | $this->initArrays($width); 176 | $localBuckets = $this->buckets; 177 | for ($y = 1; $y < 5; $y++) { 178 | $row = (int)($height * $y / 5); 179 | $localLuminances = $source->getRow($row, $this->luminances); 180 | $right = (int)(($width * 4) / 5); 181 | for ($x = (int)($width / 5); $x < $right; $x++) { 182 | $pixel = ($localLuminances[(int)($x)] & 0xff); 183 | $localBuckets[($pixel >> self::$LUMINANCE_SHIFT)]++; 184 | } 185 | } 186 | $blackPoint = self::estimateBlackPoint($localBuckets); 187 | 188 | // We delay reading the entire image luminance until the black point estimation succeeds. 189 | // Although we end up reading four rows twice, it is consistent with our motto of 190 | // "fail quickly" which is necessary for continuous scanning. 191 | $localLuminances = $source->getMatrix(); 192 | for ($y = 0; $y < $height; $y++) { 193 | $offset = $y * $width; 194 | for ($x = 0; $x < $width; $x++) { 195 | $pixel = (int)($localLuminances[$offset + $x] & 0xff); 196 | if ($pixel < $blackPoint) { 197 | $matrix->set($x, $y); 198 | } 199 | } 200 | } 201 | 202 | return $matrix; 203 | } 204 | 205 | public function createBinarizer($source): \Zxing\Common\GlobalHistogramBinarizer 206 | { 207 | return new GlobalHistogramBinarizer($source); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/Common/GridSampler.php: -------------------------------------------------------------------------------- 1 | Checks a set of points that have been transformed to sample points on an image against 70 | * the image's dimensions to see if the point are even within the image.

71 | * 72 | *

This method will actually "nudge" the endpoints back onto the image if they are found to be 73 | * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder 74 | * patterns in an image where the QR Code runs all the way to the image border.

75 | * 76 | *

For efficiency, the method will check points from either end of the line until one is found 77 | * to be within the image. Because the set of points are assumed to be linear, this is valid.

78 | * 79 | * @param BitMatrix $image image into which the points should map 80 | * @param array $points actual points in x1,y1,...,xn,yn form 81 | * @param float[] $points 82 | * 83 | * @throws NotFoundException if an endpoint is lies outside the image boundaries 84 | * 85 | * @psalm-param array $points 86 | */ 87 | protected static function checkAndNudgePoints( 88 | BitMatrix $image, 89 | array $points 90 | ): void { 91 | $width = $image->getWidth(); 92 | $height = $image->getHeight(); 93 | // Check and nudge points from start until we see some that are OK: 94 | $nudged = true; 95 | for ($offset = 0; $offset < (is_countable($points) ? count($points) : 0) && $nudged; $offset += 2) { 96 | $x = (int)$points[$offset]; 97 | $y = (int)$points[$offset + 1]; 98 | if ($x < -1 || $x > $width || $y < -1 || $y > $height) { 99 | throw new NotFoundException("Endpoint ($x, $y) lies outside the image boundaries ($width, $height)"); 100 | } 101 | $nudged = false; 102 | if ($x == -1) { 103 | $points[$offset] = 0.0; 104 | $nudged = true; 105 | } elseif ($x == $width) { 106 | $points[$offset] = $width - 1; 107 | $nudged = true; 108 | } 109 | if ($y == -1) { 110 | $points[$offset + 1] = 0.0; 111 | $nudged = true; 112 | } elseif ($y == $height) { 113 | $points[$offset + 1] = $height - 1; 114 | $nudged = true; 115 | } 116 | } 117 | // Check and nudge points from end: 118 | $nudged = true; 119 | for ($offset = (is_countable($points) ? count($points) : 0) - 2; $offset >= 0 && $nudged; $offset -= 2) { 120 | $x = (int)$points[$offset]; 121 | $y = (int)$points[$offset + 1]; 122 | if ($x < -1 || $x > $width || $y < -1 || $y > $height) { 123 | throw new NotFoundException("Endpoint ($x, $y) lies outside the image boundaries ($width, $height)"); 124 | } 125 | $nudged = false; 126 | if ($x == -1) { 127 | $points[$offset] = 0.0; 128 | $nudged = true; 129 | } elseif ($x == $width) { 130 | $points[$offset] = $width - 1; 131 | $nudged = true; 132 | } 133 | if ($y == -1) { 134 | $points[$offset + 1] = 0.0; 135 | $nudged = true; 136 | } elseif ($y == $height) { 137 | $points[$offset + 1] = $height - 1; 138 | $nudged = true; 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * Samples an image for a rectangular matrix of bits of the given dimension. The sampling 145 | * transformation is determined by the coordinates of 4 points, in the original and transformed 146 | * image space. 147 | * 148 | * @param BitMatrix $image image to sample 149 | * @param int $dimensionX width of {@link BitMatrix} to sample from image 150 | * @param int $dimensionY height of {@link BitMatrix} to sample from image 151 | * @param float $p1ToX point 1 preimage X 152 | * @param float $p1ToY point 1 preimage Y 153 | * @param float $p2ToX point 2 preimage X 154 | * @param float $p2ToY point 2 preimage Y 155 | * @param float $p3ToX point 3 preimage X 156 | * @param float $p3ToY point 3 preimage Y 157 | * @param float $p4ToX point 4 preimage X 158 | * @param float $p4ToY point 4 preimage Y 159 | * @param float $p1FromX point 1 image X 160 | * @param float $p1FromY point 1 image Y 161 | * @param float $p2FromX point 2 image X 162 | * @param float $p2FromY point 2 image Y 163 | * @param float $p3FromX point 3 image X 164 | * @param float $p3FromY point 3 image Y 165 | * @param float $p4FromX point 4 image X 166 | * @param float $p4FromY point 4 image Y 167 | * 168 | * @return {@link BitMatrix} representing a grid of points sampled from the image within a region 169 | * defined by the "from" parameters 170 | * @throws NotFoundException if image can't be sampled, for example, if the transformation defined 171 | * by the given points is invalid or results in sampling outside the image boundaries 172 | */ 173 | abstract public function sampleGrid( 174 | $image, 175 | $dimensionX, 176 | $dimensionY, 177 | $p1ToX, 178 | $p1ToY, 179 | $p2ToX, 180 | $p2ToY, 181 | $p3ToX, 182 | $p3ToY, 183 | $p4ToX, 184 | $p4ToY, 185 | $p1FromX, 186 | $p1FromY, 187 | $p2FromX, 188 | $p2FromY, 189 | $p3FromX, 190 | $p3FromY, 191 | $p4FromX, 192 | $p4FromY 193 | ); 194 | 195 | abstract public function sampleGrid_( 196 | BitMatrix $image, 197 | int $dimensionX, 198 | int $dimensionY, 199 | PerspectiveTransform $transform 200 | ): BitMatrix; 201 | } 202 | -------------------------------------------------------------------------------- /lib/Common/PerspectiveTransform.php: -------------------------------------------------------------------------------- 1 | This class implements a perspective transform in two dimensions. Given four source and four 22 | * destination points, it will compute the transformation implied between them. The code is based 23 | * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.

24 | * 25 | * @author Sean Owen 26 | */ 27 | final class PerspectiveTransform 28 | { 29 | private function __construct(private $a11, private $a21, private $a31, private $a12, private $a22, private $a32, private $a13, private $a23, private $a33) 30 | { 31 | } 32 | 33 | public static function quadrilateralToQuadrilateral( 34 | float $x0, 35 | float $y0, 36 | float $x1, 37 | float $y1, 38 | float $x2, 39 | float $y2, 40 | float $x3, 41 | float $y3, 42 | float $x0p, 43 | float $y0p, 44 | float $x1p, 45 | float $y1p, 46 | float $x2p, 47 | float $y2p, 48 | float $x3p, 49 | float $y3p 50 | ): self { 51 | $qToS = self::quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3); 52 | $sToQ = self::squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p); 53 | 54 | return $sToQ->times($qToS); 55 | } 56 | 57 | public static function quadrilateralToSquare( 58 | float $x0, 59 | float $y0, 60 | float $x1, 61 | float $y1, 62 | float $x2, 63 | float $y2, 64 | float $x3, 65 | float $y3 66 | ): self { 67 | // Here, the adjoint serves as the inverse: 68 | return self::squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)->buildAdjoint(); 69 | } 70 | 71 | public function buildAdjoint(): \Zxing\Common\PerspectiveTransform 72 | { 73 | // Adjoint is the transpose of the cofactor matrix: 74 | return new PerspectiveTransform( 75 | $this->a22 * $this->a33 - $this->a23 * $this->a32, 76 | $this->a23 * $this->a31 - $this->a21 * $this->a33, 77 | $this->a21 * $this->a32 - $this->a22 * $this->a31, 78 | $this->a13 * $this->a32 - $this->a12 * $this->a33, 79 | $this->a11 * $this->a33 - $this->a13 * $this->a31, 80 | $this->a12 * $this->a31 - $this->a11 * $this->a32, 81 | $this->a12 * $this->a23 - $this->a13 * $this->a22, 82 | $this->a13 * $this->a21 - $this->a11 * $this->a23, 83 | $this->a11 * $this->a22 - $this->a12 * $this->a21 84 | ); 85 | } 86 | 87 | public static function squareToQuadrilateral( 88 | float $x0, 89 | float $y0, 90 | float $x1, 91 | float $y1, 92 | float $x2, 93 | float $y2, 94 | float $x3, 95 | float $y3 96 | ): \Zxing\Common\PerspectiveTransform { 97 | $dx3 = $x0 - $x1 + $x2 - $x3; 98 | $dy3 = $y0 - $y1 + $y2 - $y3; 99 | if ($dx3 == 0.0 && $dy3 == 0.0) { 100 | // Affine 101 | return new PerspectiveTransform( 102 | $x1 - $x0, 103 | $x2 - $x1, 104 | $x0, 105 | $y1 - $y0, 106 | $y2 - $y1, 107 | $y0, 108 | 0.0, 109 | 0.0, 110 | 1.0 111 | ); 112 | } else { 113 | $dx1 = $x1 - $x2; 114 | $dx2 = $x3 - $x2; 115 | $dy1 = $y1 - $y2; 116 | $dy2 = $y3 - $y2; 117 | $denominator = $dx1 * $dy2 - $dx2 * $dy1; 118 | $a13 = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator; 119 | $a23 = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator; 120 | 121 | return new PerspectiveTransform( 122 | $x1 - $x0 + $a13 * $x1, 123 | $x3 - $x0 + $a23 * $x3, 124 | $x0, 125 | $y1 - $y0 + $a13 * $y1, 126 | $y3 - $y0 + $a23 * $y3, 127 | $y0, 128 | $a13, 129 | $a23, 130 | 1.0 131 | ); 132 | } 133 | } 134 | 135 | public function times(self $other): \Zxing\Common\PerspectiveTransform 136 | { 137 | return new PerspectiveTransform( 138 | $this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13, 139 | $this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23, 140 | $this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33, 141 | $this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13, 142 | $this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23, 143 | $this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33, 144 | $this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13, 145 | $this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23, 146 | $this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33 147 | ); 148 | } 149 | 150 | /** 151 | * @param (float|mixed)[] $points 152 | * 153 | * @psalm-param array $points 154 | */ 155 | public function transformPoints(array &$points, &$yValues = 0): void 156 | { 157 | if ($yValues) { 158 | $this->transformPoints_($points, $yValues); 159 | 160 | return; 161 | } 162 | $max = is_countable($points) ? count($points) : 0; 163 | $a11 = $this->a11; 164 | $a12 = $this->a12; 165 | $a13 = $this->a13; 166 | $a21 = $this->a21; 167 | $a22 = $this->a22; 168 | $a23 = $this->a23; 169 | $a31 = $this->a31; 170 | $a32 = $this->a32; 171 | $a33 = $this->a33; 172 | for ($i = 0; $i < $max; $i += 2) { 173 | $x = $points[$i]; 174 | $y = $points[$i + 1]; 175 | $denominator = $a13 * $x + $a23 * $y + $a33; 176 | // TODO: think what we do if $denominator == 0 (division by zero) 177 | if ($denominator != 0.0) { 178 | $points[$i] = ($a11 * $x + $a21 * $y + $a31) / $denominator; 179 | $points[$i + 1] = ($a12 * $x + $a22 * $y + $a32) / $denominator; 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * @param (float|mixed)[] $xValues 186 | * 187 | * @psalm-param array $xValues 188 | */ 189 | public function transformPoints_(array &$xValues, &$yValues): void 190 | { 191 | $n = is_countable($xValues) ? count($xValues) : 0; 192 | for ($i = 0; $i < $n; $i++) { 193 | $x = $xValues[$i]; 194 | $y = $yValues[$i]; 195 | $denominator = $this->a13 * $x + $this->a23 * $y + $this->a33; 196 | $xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator; 197 | $yValues[$i] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator; 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /lib/Common/Reedsolomon/GenericGF.php: -------------------------------------------------------------------------------- 1 | This class contains utility methods for performing mathematical operations over 22 | * the Galois Fields. Operations use a given primitive polynomial in calculations.

23 | * 24 | *

Throughout this package, elements of the GF are represented as an {@code int} 25 | * for convenience and speed (but at the cost of memory). 26 | *

27 | * 28 | * @author Sean Owen 29 | * @author David Olivier 30 | */ 31 | final class GenericGF 32 | { 33 | public static $AZTEC_DATA_12; 34 | public static $AZTEC_DATA_10; 35 | public static $AZTEC_DATA_6; 36 | public static $AZTEC_PARAM; 37 | public static $QR_CODE_FIELD_256; 38 | public static $DATA_MATRIX_FIELD_256; 39 | public static $AZTEC_DATA_8; 40 | public static $MAXICODE_FIELD_64; 41 | 42 | private array $expTable = []; 43 | private array $logTable = []; 44 | private readonly \Zxing\Common\Reedsolomon\GenericGFPoly $zero; 45 | private readonly \Zxing\Common\Reedsolomon\GenericGFPoly $one; 46 | 47 | /** 48 | * Create a representation of GF(size) using the given primitive polynomial. 49 | * 50 | * @param int $primitive irreducible polynomial whose coefficients are represented by 51 | * the bits of an int, where the least-significant bit represents the constant 52 | * coefficient 53 | * @param int $size the size of the field 54 | * @param int $generatorBase the factor b in the generator polynomial can be 0- or 1-based 55 | (g(x) = (x+a^b)(x+a^(b+1))...(x+a^(b+2t-1))). 56 | In most cases it should be 1, but for QR code it is 0. 57 | */ 58 | public function __construct(private $primitive, private $size, private $generatorBase) 59 | { 60 | $x = 1; 61 | for ($i = 0; $i < $size; $i++) { 62 | $this->expTable[$i] = $x; 63 | $x *= 2; // we're assuming the generator alpha is 2 64 | if ($x >= $size) { 65 | $x ^= $primitive; 66 | $x &= $size - 1; 67 | } 68 | } 69 | for ($i = 0; $i < $size - 1; $i++) { 70 | $this->logTable[$this->expTable[$i]] = $i; 71 | } 72 | // logTable[0] == 0 but this should never be used 73 | $this->zero = new GenericGFPoly($this, [0]); 74 | $this->one = new GenericGFPoly($this, [1]); 75 | } 76 | 77 | public static function Init(): void 78 | { 79 | self::$AZTEC_DATA_12 = new GenericGF(0x1069, 4096, 1); // x^12 + x^6 + x^5 + x^3 + 1 80 | self::$AZTEC_DATA_10 = new GenericGF(0x409, 1024, 1); // x^10 + x^3 + 1 81 | self::$AZTEC_DATA_6 = new GenericGF(0x43, 64, 1); // x^6 + x + 1 82 | self::$AZTEC_PARAM = new GenericGF(0x13, 16, 1); // x^4 + x + 1 83 | self::$QR_CODE_FIELD_256 = new GenericGF(0x011D, 256, 0); // x^8 + x^4 + x^3 + x^2 + 1 84 | self::$DATA_MATRIX_FIELD_256 = new GenericGF(0x012D, 256, 1); // x^8 + x^5 + x^3 + x^2 + 1 85 | self::$AZTEC_DATA_8 = self::$DATA_MATRIX_FIELD_256; 86 | self::$MAXICODE_FIELD_64 = self::$AZTEC_DATA_6; 87 | } 88 | 89 | /** 90 | * Implements both addition and subtraction -- they are the same in GF(size). 91 | * 92 | * @return float|int sum/difference of a and b 93 | * 94 | * @param float|int|null $b 95 | */ 96 | public static function addOrSubtract(int $a, int|float|null $b) 97 | { 98 | return $a ^ $b; 99 | } 100 | 101 | public function getZero(): GenericGFPoly 102 | { 103 | return $this->zero; 104 | } 105 | 106 | public function getOne(): GenericGFPoly 107 | { 108 | return $this->one; 109 | } 110 | 111 | /** 112 | * @return GenericGFPoly the monomial representing coefficient * x^degree 113 | */ 114 | public function buildMonomial($degree, int $coefficient) 115 | { 116 | if ($degree < 0) { 117 | throw new \InvalidArgumentException(); 118 | } 119 | if ($coefficient == 0) { 120 | return $this->zero; 121 | } 122 | $coefficients = fill_array(0, $degree + 1, 0);//new int[degree + 1]; 123 | $coefficients[0] = $coefficient; 124 | 125 | return new GenericGFPoly($this, $coefficients); 126 | } 127 | 128 | /** 129 | * @return 2 to the power of a in GF(size) 130 | */ 131 | public function exp($a) 132 | { 133 | return $this->expTable[$a]; 134 | } 135 | 136 | /** 137 | * @return float base 2 log of a in GF(size) 138 | */ 139 | public function log(float|int|null $a) 140 | { 141 | if ($a == 0) { 142 | throw new \InvalidArgumentException(); 143 | } 144 | 145 | return $this->logTable[$a]; 146 | } 147 | 148 | /** 149 | * @return float multiplicative inverse of a 150 | */ 151 | public function inverse($a) 152 | { 153 | if ($a == 0) { 154 | throw new \Exception(); 155 | } 156 | 157 | return $this->expTable[$this->size - $this->logTable[$a] - 1]; 158 | } 159 | 160 | /** 161 | * @return int product of a and b in GF(size) 162 | * 163 | * @param float|int|null $b 164 | * @param float|int|null $a 165 | */ 166 | public function multiply(int|float|null $a, int|float|null $b) 167 | { 168 | if ($a == 0 || $b == 0) { 169 | return 0; 170 | } 171 | 172 | return $this->expTable[($this->logTable[$a] + $this->logTable[$b]) % ($this->size - 1)]; 173 | } 174 | 175 | public function getSize() 176 | { 177 | return $this->size; 178 | } 179 | 180 | public function getGeneratorBase() 181 | { 182 | return $this->generatorBase; 183 | } 184 | 185 | // @Override 186 | public function toString(): string 187 | { 188 | return "GF(0x" . dechex((int)($this->primitive)) . ',' . $this->size . ')'; 189 | } 190 | } 191 | 192 | GenericGF::Init(); 193 | -------------------------------------------------------------------------------- /lib/Common/Reedsolomon/GenericGFPoly.php: -------------------------------------------------------------------------------- 1 | Represents a polynomial whose coefficients are elements of a GF. 22 | * Instances of this class are immutable.

23 | * 24 | *

Much credit is due to William Rucklidge since portions of this code are an indirect 25 | * port of his C++ Reed-Solomon implementation.

26 | * 27 | * @author Sean Owen 28 | */ 29 | final class GenericGFPoly 30 | { 31 | /** 32 | * @var int[]|float[]|null 33 | */ 34 | private $coefficients; 35 | 36 | /** 37 | * @param GenericGF $field {@link GenericGF} the instance representing the field to use 38 | * to perform computations 39 | * @param array $coefficients coefficients as ints representing elements of GF(size), arranged 40 | * from most significant (highest-power term) coefficient to least significant 41 | * 42 | * @throws \InvalidArgumentException if argument is null or empty, 43 | * or if leading coefficient is 0 and this is not a 44 | * constant polynomial (that is, it is not the monomial "0") 45 | */ 46 | public function __construct(private $field, $coefficients) 47 | { 48 | if (count($coefficients) == 0) { 49 | throw new \InvalidArgumentException(); 50 | } 51 | $coefficientsLength = count($coefficients); 52 | if ($coefficientsLength > 1 && $coefficients[0] == 0) { 53 | // Leading term must be non-zero for anything except the constant polynomial "0" 54 | $firstNonZero = 1; 55 | while ($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] == 0) { 56 | $firstNonZero++; 57 | } 58 | if ($firstNonZero == $coefficientsLength) { 59 | $this->coefficients = [0]; 60 | } else { 61 | $this->coefficients = fill_array(0, $coefficientsLength - $firstNonZero, 0); 62 | $this->coefficients = arraycopy( 63 | $coefficients, 64 | $firstNonZero, 65 | $this->coefficients, 66 | 0, 67 | is_countable($this->coefficients) ? count($this->coefficients) : 0 68 | ); 69 | } 70 | } else { 71 | $this->coefficients = $coefficients; 72 | } 73 | } 74 | 75 | /** 76 | * @return (float|int)[]|null 77 | * 78 | * @psalm-return array|null 79 | */ 80 | public function getCoefficients(): array|null 81 | { 82 | return $this->coefficients; 83 | } 84 | 85 | /** 86 | * @return float|int|null evaluation of this polynomial at a given point 87 | */ 88 | public function evaluateAt($a): int|float|null 89 | { 90 | if ($a == 0) { 91 | // Just return the x^0 coefficient 92 | return $this->getCoefficient(0); 93 | } 94 | $size = is_countable($this->coefficients) ? count($this->coefficients) : 0; 95 | if ($a == 1) { 96 | // Just the sum of the coefficients 97 | $result = 0; 98 | foreach ($this->coefficients as $coefficient) { 99 | $result = GenericGF::addOrSubtract($result, $coefficient); 100 | } 101 | 102 | return $result; 103 | } 104 | $result = $this->coefficients[0]; 105 | for ($i = 1; $i < $size; $i++) { 106 | $result = GenericGF::addOrSubtract($this->field->multiply($a, $result), $this->coefficients[$i]); 107 | } 108 | 109 | return $result; 110 | } 111 | 112 | /** 113 | * @return float|int|null coefficient of x^degree term in this polynomial 114 | * 115 | * @param float|int $degree 116 | */ 117 | public function getCoefficient(int|float $degree): int|float|null 118 | { 119 | return $this->coefficients[(is_countable($this->coefficients) ? count($this->coefficients) : 0) - 1 - $degree]; 120 | } 121 | 122 | public function multiply($other): self 123 | { 124 | $aCoefficients = []; 125 | $bCoefficients = []; 126 | $aLength = null; 127 | $bLength = null; 128 | $product = []; 129 | if (is_int($other)) { 130 | return $this->multiply_($other); 131 | } 132 | if ($this->field !== $other->field) { 133 | throw new \InvalidArgumentException("GenericGFPolys do not have same GenericGF field"); 134 | } 135 | if ($this->isZero() || $other->isZero()) { 136 | return $this->field->getZero(); 137 | } 138 | $aCoefficients = $this->coefficients; 139 | $aLength = count($aCoefficients); 140 | $bCoefficients = $other->coefficients; 141 | $bLength = count($bCoefficients); 142 | $product = fill_array(0, $aLength + $bLength - 1, 0); 143 | for ($i = 0; $i < $aLength; $i++) { 144 | $aCoeff = $aCoefficients[$i]; 145 | for ($j = 0; $j < $bLength; $j++) { 146 | $product[$i + $j] = GenericGF::addOrSubtract( 147 | $product[$i + $j], 148 | $this->field->multiply($aCoeff, $bCoefficients[$j]) 149 | ); 150 | } 151 | } 152 | 153 | return new GenericGFPoly($this->field, $product); 154 | } 155 | 156 | public function multiply_(int $scalar): self 157 | { 158 | if ($scalar == 0) { 159 | return $this->field->getZero(); 160 | } 161 | if ($scalar == 1) { 162 | return $this; 163 | } 164 | $size = is_countable($this->coefficients) ? count($this->coefficients) : 0; 165 | $product = fill_array(0, $size, 0); 166 | for ($i = 0; $i < $size; $i++) { 167 | $product[$i] = $this->field->multiply($this->coefficients[$i], $scalar); 168 | } 169 | 170 | return new GenericGFPoly($this->field, $product); 171 | } 172 | 173 | /** 174 | * @return bool iff this polynomial is the monomial "0" 175 | */ 176 | public function isZero(): bool 177 | { 178 | return $this->coefficients[0] == 0; 179 | } 180 | 181 | public function multiplyByMonomial($degree, $coefficient): self 182 | { 183 | if ($degree < 0) { 184 | throw new \InvalidArgumentException(); 185 | } 186 | if ($coefficient == 0) { 187 | return $this->field->getZero(); 188 | } 189 | $size = is_countable($this->coefficients) ? count($this->coefficients) : 0; 190 | $product = fill_array(0, $size + $degree, 0); 191 | for ($i = 0; $i < $size; $i++) { 192 | $product[$i] = $this->field->multiply($this->coefficients[$i], $coefficient); 193 | } 194 | 195 | return new GenericGFPoly($this->field, $product); 196 | } 197 | 198 | /** 199 | * @psalm-return array{0: mixed, 1: mixed} 200 | */ 201 | public function divide($other): array 202 | { 203 | if ($this->field !== $other->field) { 204 | throw new \InvalidArgumentException("GenericGFPolys do not have same GenericGF field"); 205 | } 206 | if ($other->isZero()) { 207 | throw new \InvalidArgumentException("Divide by 0"); 208 | } 209 | 210 | $quotient = $this->field->getZero(); 211 | $remainder = $this; 212 | 213 | $denominatorLeadingTerm = $other->getCoefficient($other->getDegree()); 214 | $inverseDenominatorLeadingTerm = $this->field->inverse($denominatorLeadingTerm); 215 | 216 | while ($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()) { 217 | $degreeDifference = $remainder->getDegree() - $other->getDegree(); 218 | $scale = $this->field->multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm); 219 | $term = $other->multiplyByMonomial($degreeDifference, $scale); 220 | $iterationQuotient = $this->field->buildMonomial($degreeDifference, $scale); 221 | $quotient = $quotient->addOrSubtract($iterationQuotient); 222 | $remainder = $remainder->addOrSubtract($term); 223 | } 224 | 225 | return [$quotient, $remainder]; 226 | } 227 | 228 | /** 229 | * @return int of this polynomial 230 | */ 231 | public function getDegree(): int 232 | { 233 | return (is_countable($this->coefficients) ? count($this->coefficients) : 0) - 1; 234 | } 235 | 236 | public function addOrSubtract(self $other): self 237 | { 238 | $smallerCoefficients = []; 239 | $largerCoefficients = []; 240 | $sumDiff = []; 241 | $lengthDiff = null; 242 | $countLargerCoefficients = null; 243 | if ($this->field !== $other->field) { 244 | throw new \InvalidArgumentException("GenericGFPolys do not have same GenericGF field"); 245 | } 246 | if ($this->isZero()) { 247 | return $other; 248 | } 249 | if ($other->isZero()) { 250 | return $this; 251 | } 252 | 253 | $smallerCoefficients = $this->coefficients; 254 | $largerCoefficients = $other->coefficients; 255 | if (count($smallerCoefficients) > count($largerCoefficients)) { 256 | $temp = $smallerCoefficients; 257 | $smallerCoefficients = $largerCoefficients; 258 | $largerCoefficients = $temp; 259 | } 260 | $sumDiff = fill_array(0, count($largerCoefficients), 0); 261 | $lengthDiff = count($largerCoefficients) - count($smallerCoefficients); 262 | // Copy high-order terms only found in higher-degree polynomial's coefficients 263 | $sumDiff = arraycopy($largerCoefficients, 0, $sumDiff, 0, $lengthDiff); 264 | 265 | $countLargerCoefficients = count($largerCoefficients); 266 | for ($i = $lengthDiff; $i < $countLargerCoefficients; $i++) { 267 | $sumDiff[$i] = GenericGF::addOrSubtract($smallerCoefficients[$i - $lengthDiff], $largerCoefficients[$i]); 268 | } 269 | 270 | return new GenericGFPoly($this->field, $sumDiff); 271 | } 272 | 273 | 274 | 275 | public function toString(): string 276 | { 277 | $result = ''; 278 | for ($degree = $this->getDegree(); $degree >= 0; $degree--) { 279 | $coefficient = $this->getCoefficient($degree); 280 | if ($coefficient != 0) { 281 | if ($coefficient < 0) { 282 | $result .= " - "; 283 | $coefficient = -$coefficient; 284 | } else { 285 | if (strlen((string) $result) > 0) { 286 | $result .= " + "; 287 | } 288 | } 289 | if ($degree == 0 || $coefficient != 1) { 290 | $alphaPower = $this->field->log($coefficient); 291 | if ($alphaPower == 0) { 292 | $result .= '1'; 293 | } elseif ($alphaPower == 1) { 294 | $result .= 'a'; 295 | } else { 296 | $result .= "a^"; 297 | $result .= ($alphaPower); 298 | } 299 | } 300 | if ($degree != 0) { 301 | if ($degree == 1) { 302 | $result .= 'x'; 303 | } else { 304 | $result .= "x^"; 305 | $result .= $degree; 306 | } 307 | } 308 | } 309 | } 310 | 311 | return $result; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /lib/Common/Reedsolomon/ReedSolomonDecoder.php: -------------------------------------------------------------------------------- 1 | Implements Reed-Solomon decoding, as the name implies.

22 | * 23 | *

The algorithm will not be explained here, but the following references were helpful 24 | * in creating this implementation:

25 | * 26 | * 34 | * 35 | *

Much credit is due to William Rucklidge since portions of this code are an indirect 36 | * port of his C++ Reed-Solomon implementation.

37 | * 38 | * @author Sean Owen 39 | * @author William Rucklidge 40 | * @author sanfordsquires 41 | */ 42 | final class ReedSolomonDecoder 43 | { 44 | public function __construct(private $field) 45 | { 46 | } 47 | 48 | /** 49 | *

Decodes given set of received codewords, which include both data and error-correction 50 | * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place, 51 | * in the input.

52 | * 53 | * @param array $received data and error-correction codewords 54 | * @param int|float $twoS number of error-correction codewords available 55 | * 56 | * @throws ReedSolomonException if decoding fails for any reason 57 | * 58 | * @return void 59 | */ 60 | public function decode(&$received, $twoS) 61 | { 62 | $poly = new GenericGFPoly($this->field, $received); 63 | $syndromeCoefficients = fill_array(0, $twoS, 0); 64 | $noError = true; 65 | for ($i = 0; $i < $twoS; $i++) { 66 | $eval = $poly->evaluateAt($this->field->exp($i + $this->field->getGeneratorBase())); 67 | $syndromeCoefficients[(is_countable($syndromeCoefficients) ? count($syndromeCoefficients) : 0) - 1 - $i] = $eval; 68 | if ($eval != 0) { 69 | $noError = false; 70 | } 71 | } 72 | if ($noError) { 73 | return; 74 | } 75 | $syndrome = new GenericGFPoly($this->field, $syndromeCoefficients); 76 | $sigmaOmega = 77 | $this->runEuclideanAlgorithm($this->field->buildMonomial($twoS, 1), $syndrome, $twoS); 78 | $sigma = $sigmaOmega[0]; 79 | $omega = $sigmaOmega[1]; 80 | $errorLocations = $this->findErrorLocations($sigma); 81 | $errorMagnitudes = $this->findErrorMagnitudes($omega, $errorLocations); 82 | $errorLocationsCount = is_countable($errorLocations) ? count($errorLocations) : 0; 83 | for ($i = 0; $i < $errorLocationsCount; $i++) { 84 | $position = (is_countable($received) ? count($received) : 0) - 1 - $this->field->log($errorLocations[$i]); 85 | if ($position < 0) { 86 | throw new ReedSolomonException("Bad error location"); 87 | } 88 | $received[$position] = GenericGF::addOrSubtract($received[$position], $errorMagnitudes[$i]); 89 | } 90 | } 91 | 92 | /** 93 | * @psalm-return array{0: mixed, 1: mixed} 94 | */ 95 | private function runEuclideanAlgorithm($a, \Zxing\Common\Reedsolomon\GenericGFPoly $b, int|float $R): array 96 | { 97 | // Assume a's degree is >= b's 98 | if ($a->getDegree() < $b->getDegree()) { 99 | $temp = $a; 100 | $a = $b; 101 | $b = $temp; 102 | } 103 | 104 | $rLast = $a; 105 | $r = $b; 106 | $tLast = $this->field->getZero(); 107 | $t = $this->field->getOne(); 108 | 109 | // Run Euclidean algorithm until r's degree is less than R/2 110 | while ($r->getDegree() >= $R / 2) { 111 | $rLastLast = $rLast; 112 | $tLastLast = $tLast; 113 | $rLast = $r; 114 | $tLast = $t; 115 | 116 | // Divide rLastLast by rLast, with quotient in q and remainder in r 117 | if ($rLast->isZero()) { 118 | // Oops, Euclidean algorithm already terminated? 119 | throw new ReedSolomonException("r_{i-1} was zero"); 120 | } 121 | $r = $rLastLast; 122 | $q = $this->field->getZero(); 123 | $denominatorLeadingTerm = $rLast->getCoefficient($rLast->getDegree()); 124 | $dltInverse = $this->field->inverse($denominatorLeadingTerm); 125 | while ($r->getDegree() >= $rLast->getDegree() && !$r->isZero()) { 126 | $degreeDiff = $r->getDegree() - $rLast->getDegree(); 127 | $scale = $this->field->multiply($r->getCoefficient($r->getDegree()), $dltInverse); 128 | $q = $q->addOrSubtract($this->field->buildMonomial($degreeDiff, $scale)); 129 | $r = $r->addOrSubtract($rLast->multiplyByMonomial($degreeDiff, $scale)); 130 | } 131 | 132 | $t = $q->multiply($tLast)->addOrSubtract($tLastLast); 133 | 134 | if ($r->getDegree() >= $rLast->getDegree()) { 135 | throw new ReedSolomonException("Division algorithm failed to reduce polynomial?"); 136 | } 137 | } 138 | 139 | $sigmaTildeAtZero = $t->getCoefficient(0); 140 | if ($sigmaTildeAtZero == 0) { 141 | throw new ReedSolomonException("sigmaTilde(0) was zero"); 142 | } 143 | 144 | $inverse = $this->field->inverse($sigmaTildeAtZero); 145 | $sigma = $t->multiply($inverse); 146 | $omega = $r->multiply($inverse); 147 | 148 | return [$sigma, $omega]; 149 | } 150 | 151 | /** 152 | * @psalm-return array 153 | */ 154 | private function findErrorLocations($errorLocator): array 155 | { 156 | // This is a direct application of Chien's search 157 | $numErrors = $errorLocator->getDegree(); 158 | if ($numErrors == 1) { // shortcut 159 | return [$errorLocator->getCoefficient(1)]; 160 | } 161 | $result = fill_array(0, $numErrors, 0); 162 | $e = 0; 163 | for ($i = 1; $i < $this->field->getSize() && $e < $numErrors; $i++) { 164 | if ($errorLocator->evaluateAt($i) == 0) { 165 | $result[$e] = $this->field->inverse($i); 166 | $e++; 167 | } 168 | } 169 | if ($e != $numErrors) { 170 | throw new ReedSolomonException("Error locator degree does not match number of roots"); 171 | } 172 | 173 | return $result; 174 | } 175 | 176 | /** 177 | * @psalm-return array 178 | * @psalm-param array $errorLocations 179 | */ 180 | private function findErrorMagnitudes($errorEvaluator, array $errorLocations): array 181 | { 182 | // This is directly applying Forney's Formula 183 | $s = is_countable($errorLocations) ? count($errorLocations) : 0; 184 | $result = fill_array(0, $s, 0); 185 | for ($i = 0; $i < $s; $i++) { 186 | $xiInverse = $this->field->inverse($errorLocations[$i]); 187 | $denominator = 1; 188 | for ($j = 0; $j < $s; $j++) { 189 | if ($i != $j) { 190 | //denominator = field.multiply(denominator, 191 | // GenericGF.addOrSubtract(1, field.multiply(errorLocations[j], xiInverse))); 192 | // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug. 193 | // Below is a funny-looking workaround from Steven Parkes 194 | $term = $this->field->multiply($errorLocations[$j], $xiInverse); 195 | $termPlus1 = ($term & 0x1) == 0 ? $term | 1 : $term & ~1; 196 | $denominator = $this->field->multiply($denominator, $termPlus1); 197 | } 198 | } 199 | $result[$i] = $this->field->multiply( 200 | $errorEvaluator->evaluateAt($xiInverse), 201 | $this->field->inverse($denominator) 202 | ); 203 | if ($this->field->getGeneratorBase() != 0) { 204 | $result[$i] = $this->field->multiply($result[$i], $xiInverse); 205 | } 206 | } 207 | 208 | return $result; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /lib/Common/Reedsolomon/ReedSolomonException.php: -------------------------------------------------------------------------------- 1 | Thrown when an exception occurs during Reed-Solomon decoding, such as when 22 | * there are too many errors to correct.

23 | * 24 | * @author Sean Owen 25 | */ 26 | final class ReedSolomonException extends \Exception 27 | { 28 | } 29 | -------------------------------------------------------------------------------- /lib/Common/customFunctions.php: -------------------------------------------------------------------------------- 1 | >= 1; 38 | $num++; 39 | } 40 | 41 | return $num; 42 | } 43 | } 44 | 45 | if (!function_exists('uRShift')) { 46 | function uRShift($a, $b) 47 | { 48 | static $mask = (8 * PHP_INT_SIZE - 1); 49 | if ($b === 0) { 50 | return $a; 51 | } 52 | 53 | return ($a >> $b) & ~(1 << $mask >> ($b - 1)); 54 | } 55 | } 56 | 57 | /* 58 | function sdvig3($num,$count=1){//>>> 32 bit 59 | $s = decbin($num); 60 | 61 | $sarray = str_split($s,1); 62 | $sarray = array_slice($sarray,-32);//32bit 63 | 64 | for($i=0;$i<=1;$i++) { 65 | array_pop($sarray); 66 | array_unshift($sarray, '0'); 67 | } 68 | return bindec(implode($sarray)); 69 | } 70 | */ 71 | 72 | if (!function_exists('sdvig3')) { 73 | function sdvig3($a, $b): float|int 74 | { 75 | if ($a >= 0) { 76 | return bindec(decbin($a >> $b)); //simply right shift for positive number 77 | } 78 | 79 | $bin = decbin($a >> $b); 80 | 81 | $bin = substr($bin, $b); // zero fill on the left side 82 | 83 | return bindec($bin); 84 | } 85 | } 86 | 87 | if (!function_exists('floatToIntBits')) { 88 | function floatToIntBits($float_val) 89 | { 90 | $int = unpack('i', pack('f', $float_val)); 91 | 92 | return $int[1]; 93 | } 94 | } 95 | 96 | 97 | if (!function_exists('fill_array')) { 98 | /** 99 | * @psalm-return array 100 | */ 101 | function fill_array($index, $count, $value): array 102 | { 103 | if ($count <= 0) { 104 | return [0]; 105 | } 106 | 107 | return array_fill($index, $count, $value); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/FormatException.php: -------------------------------------------------------------------------------- 1 | GDLuminanceSource($gdImage, $dataWidth, $dataHeight); 42 | 43 | return; 44 | } 45 | parent::__construct($width, $height); 46 | if ($left + $width > $dataWidth || $top + $height > $dataHeight) { 47 | throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); 48 | } 49 | $this->luminances = $gdImage; 50 | $this->dataWidth = $dataWidth; 51 | $this->dataHeight = $dataHeight; 52 | $this->left = $left; 53 | $this->top = $top; 54 | } 55 | 56 | public function GDLuminanceSource($gdImage, $width, $height): void 57 | { 58 | parent::__construct($width, $height); 59 | 60 | $this->dataWidth = $width; 61 | $this->dataHeight = $height; 62 | $this->left = 0; 63 | $this->top = 0; 64 | $this->gdImage = $gdImage; 65 | 66 | 67 | // In order to measure pure decoding speed, we convert the entire image to a greyscale array 68 | // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. 69 | $this->luminances = []; 70 | //$this->luminances = $this->grayScaleToBitmap($this->grayscale()); 71 | 72 | $array = []; 73 | $rgb = []; 74 | 75 | for ($j = 0; $j < $height; $j++) { 76 | for ($i = 0; $i < $width; $i++) { 77 | $argb = imagecolorat($this->gdImage, $i, $j); 78 | $pixel = imagecolorsforindex($this->gdImage, $argb); 79 | $r = $pixel['red']; 80 | $g = $pixel['green']; 81 | $b = $pixel['blue']; 82 | if ($r == $g && $g == $b) { 83 | // Image is already greyscale, so pick any channel. 84 | 85 | $this->luminances[] = $r;//(($r + 128) % 256) - 128; 86 | } else { 87 | // Calculate luminance cheaply, favoring green. 88 | $this->luminances[] = ($r + 2 * $g + $b) / 4;//(((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; 89 | } 90 | } 91 | } 92 | 93 | /* 94 | for ($y = 0; $y < $height; $y++) { 95 | $offset = $y * $width; 96 | for ($x = 0; $x < $width; $x++) { 97 | $pixel = $pixels[$offset + $x]; 98 | $r = ($pixel >> 16) & 0xff; 99 | $g = ($pixel >> 8) & 0xff; 100 | $b = $pixel & 0xff; 101 | if ($r == $g && $g == $b) { 102 | // Image is already greyscale, so pick any channel. 103 | 104 | $this->luminances[(int)($offset + $x)] = (($r+128) % 256) - 128; 105 | } else { 106 | // Calculate luminance cheaply, favoring green. 107 | $this->luminances[(int)($offset + $x)] = (((($r + 2 * $g + $b) / 4)+128)%256) - 128; 108 | } 109 | 110 | 111 | 112 | } 113 | */ 114 | //} 115 | // $this->luminances = $this->grayScaleToBitmap($this->luminances); 116 | } 117 | 118 | 119 | public function getRow($y, $row = null) 120 | { 121 | if ($y < 0 || $y >= $this->getHeight()) { 122 | throw new \InvalidArgumentException('Requested row is outside the image: ' . $y); 123 | } 124 | $width = $this->getWidth(); 125 | if ($row == null || (is_countable($row) ? count($row) : 0) < $width) { 126 | $row = []; 127 | } 128 | $offset = ($y + $this->top) * $this->dataWidth + $this->left; 129 | $row = arraycopy($this->luminances, $offset, $row, 0, $width); 130 | 131 | return $row; 132 | } 133 | 134 | 135 | public function getMatrix() 136 | { 137 | $width = $this->getWidth(); 138 | $height = $this->getHeight(); 139 | 140 | // If the caller asks for the entire underlying image, save the copy and give them the 141 | // original data. The docs specifically warn that result.length must be ignored. 142 | if ($width == $this->dataWidth && $height == $this->dataHeight) { 143 | return $this->luminances; 144 | } 145 | 146 | $area = $width * $height; 147 | $matrix = []; 148 | $inputOffset = $this->top * $this->dataWidth + $this->left; 149 | 150 | // If the width matches the full width of the underlying data, perform a single copy. 151 | if ($width == $this->dataWidth) { 152 | $matrix = arraycopy($this->luminances, $inputOffset, $matrix, 0, $area); 153 | 154 | return $matrix; 155 | } 156 | 157 | // Otherwise copy one cropped row at a time. 158 | $rgb = $this->luminances; 159 | for ($y = 0; $y < $height; $y++) { 160 | $outputOffset = $y * $width; 161 | $matrix = arraycopy($rgb, $inputOffset, $matrix, $outputOffset, $width); 162 | $inputOffset += $this->dataWidth; 163 | } 164 | 165 | return $matrix; 166 | } 167 | 168 | 169 | public function isCropSupported(): bool 170 | { 171 | return true; 172 | } 173 | 174 | 175 | public function crop($left, $top, $width, $height): \Zxing\GDLuminanceSource 176 | { 177 | return new GDLuminanceSource( 178 | $this->luminances, 179 | $this->dataWidth, 180 | $this->dataHeight, 181 | $this->left + $left, 182 | $this->top + $top, 183 | $width, 184 | $height 185 | ); 186 | } 187 | 188 | public function rotateCounterClockwise(): void 189 | { 190 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise"); 191 | } 192 | 193 | public function rotateCounterClockwise45(): void 194 | { 195 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise45"); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/IMagickLuminanceSource.php: -------------------------------------------------------------------------------- 1 | _IMagickLuminanceSource($image, $dataWidth, $dataHeight); 35 | 36 | return; 37 | } 38 | parent::__construct($width, $height); 39 | if ($left + $width > $dataWidth || $top + $height > $dataHeight) { 40 | throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); 41 | } 42 | $this->luminances = $image; 43 | $this->dataWidth = $dataWidth; 44 | $this->dataHeight = $dataHeight; 45 | $this->left = $left; 46 | $this->top = $top; 47 | } 48 | 49 | public function rotateCounterClockwise(): void 50 | { 51 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise"); 52 | } 53 | 54 | public function rotateCounterClockwise45(): void 55 | { 56 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise45"); 57 | } 58 | 59 | /** 60 | * TODO: move to some utility class or something 61 | * Converts shorthand memory notation value to bytes 62 | * From http://php.net/manual/en/function.ini-get.php 63 | * 64 | * @param int $val Memory size shorthand notation string 65 | */ 66 | protected static function kmgStringToBytes(string $val) 67 | { 68 | $val = trim($val); 69 | $last = strtolower($val[strlen($val) - 1]); 70 | $val = substr($val, 0, -1); 71 | switch ($last) { 72 | // The 'G' modifier is available since PHP 5.1.0 73 | case 'g': 74 | $val *= 1024; 75 | // no break 76 | case 'm': 77 | $val *= 1024; 78 | // no break 79 | case 'k': 80 | $val *= 1024; 81 | } 82 | return $val; 83 | } 84 | 85 | public function _IMagickLuminanceSource(\Imagick $image, $width, $height): void 86 | { 87 | parent::__construct($width, $height); 88 | 89 | $this->dataWidth = $width; 90 | $this->dataHeight = $height; 91 | $this->left = 0; 92 | $this->top = 0; 93 | $this->image = $image; 94 | 95 | // In order to measure pure decoding speed, we convert the entire image to a greyscale array 96 | // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. 97 | $this->luminances = []; 98 | 99 | $image->setImageColorspace(\Imagick::COLORSPACE_GRAY); 100 | // Check that we actually have enough space to do it 101 | if (ini_get('memory_limit') != -1 && $width * $height * 16 * 3 > $this->kmgStringToBytes(ini_get('memory_limit'))) { 102 | throw new \RuntimeException("PHP Memory Limit does not allow pixel export."); 103 | } 104 | $pixels = $image->exportImagePixels(1, 1, $width, $height, "RGB", \Imagick::PIXEL_CHAR); 105 | 106 | $array = []; 107 | $rgb = []; 108 | 109 | $countPixels = count($pixels); 110 | for ($i = 0; $i < $countPixels; $i += 3) { 111 | $r = $pixels[$i] & 0xff; 112 | $g = $pixels[$i + 1] & 0xff; 113 | $b = $pixels[$i + 2] & 0xff; 114 | if ($r == $g && $g == $b) { 115 | // Image is already greyscale, so pick any channel. 116 | $this->luminances[] = $r; //(($r + 128) % 256) - 128; 117 | } else { 118 | // Calculate luminance cheaply, favoring green. 119 | $this->luminances[] = ($r + 2 * $g + $b) / 4; //(((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; 120 | } 121 | } 122 | } 123 | 124 | 125 | public function getRow($y, $row = null) 126 | { 127 | if ($y < 0 || $y >= $this->getHeight()) { 128 | throw new \InvalidArgumentException('Requested row is outside the image: ' . $y); 129 | } 130 | $width = $this->getWidth(); 131 | if ($row == null || (is_countable($row) ? count($row) : 0) < $width) { 132 | $row = []; 133 | } 134 | $offset = ($y + $this->top) * $this->dataWidth + $this->left; 135 | $row = arraycopy($this->luminances, $offset, $row, 0, $width); 136 | 137 | return $row; 138 | } 139 | 140 | 141 | public function getMatrix() 142 | { 143 | $width = $this->getWidth(); 144 | $height = $this->getHeight(); 145 | 146 | // If the caller asks for the entire underlying image, save the copy and give them the 147 | // original data. The docs specifically warn that result.length must be ignored. 148 | if ($width == $this->dataWidth && $height == $this->dataHeight) { 149 | return $this->luminances; 150 | } 151 | 152 | $area = $width * $height; 153 | $matrix = []; 154 | $inputOffset = $this->top * $this->dataWidth + $this->left; 155 | 156 | // If the width matches the full width of the underlying data, perform a single copy. 157 | if ($width == $this->dataWidth) { 158 | $matrix = arraycopy($this->luminances, $inputOffset, $matrix, 0, $area); 159 | 160 | return $matrix; 161 | } 162 | 163 | // Otherwise copy one cropped row at a time. 164 | $rgb = $this->luminances; 165 | for ($y = 0; $y < $height; $y++) { 166 | $outputOffset = $y * $width; 167 | $matrix = arraycopy($rgb, $inputOffset, $matrix, $outputOffset, $width); 168 | $inputOffset += $this->dataWidth; 169 | } 170 | 171 | return $matrix; 172 | } 173 | 174 | 175 | public function isCropSupported(): bool 176 | { 177 | return true; 178 | } 179 | 180 | 181 | public function crop($left, $top, $width, $height): LuminanceSource 182 | { 183 | return $this->luminances->cropImage($width, $height, $left, $top); 184 | 185 | return new GDLuminanceSource( 186 | $this->luminances, 187 | $this->dataWidth, 188 | $this->dataHeight, 189 | $this->left + $left, 190 | $this->top + $top, 191 | $width, 192 | $height 193 | ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/LuminanceSource.php: -------------------------------------------------------------------------------- 1 | width; 53 | } 54 | 55 | /** 56 | * @return float The height of the bitmap. 57 | */ 58 | final public function getHeight(): float 59 | { 60 | return $this->height; 61 | } 62 | 63 | /** 64 | * @return bool Whether this subclass supports cropping. 65 | */ 66 | public function isCropSupported(): bool 67 | { 68 | return false; 69 | } 70 | 71 | /** 72 | * Returns a new object with cropped image data. Implementations may keep a reference to the 73 | * original data rather than a copy. Only callable if isCropSupported() is true. 74 | * 75 | * @param $left The left coordinate, which must be in [0,getWidth()) 76 | * @param $top The top coordinate, which must be in [0,getHeight()) 77 | * @param $width The width of the rectangle to crop. 78 | * @param $height The height of the rectangle to crop. 79 | * 80 | * @return mixed A cropped version of this object. 81 | */ 82 | abstract public function crop($left, $top, $width, $height): LuminanceSource; 83 | 84 | /** 85 | * @return bool Whether this subclass supports counter-clockwise rotation. 86 | */ 87 | public function isRotateSupported(): bool 88 | { 89 | return false; 90 | } 91 | 92 | /** 93 | * @return a wrapper of this {@code LuminanceSource} which inverts the luminances it returns -- black becomes 94 | * white and vice versa, and each value becomes (255-value). 95 | */ 96 | // public function invert() 97 | // { 98 | // return new InvertedLuminanceSource($this); 99 | // } 100 | 101 | /** 102 | * Returns a new object with rotated image data by 90 degrees counterclockwise. 103 | * Only callable if {@link #isRotateSupported()} is true. 104 | * 105 | * @return mixed A rotated version of this object. 106 | */ 107 | abstract public function rotateCounterClockwise(): void; 108 | 109 | /** 110 | * Returns a new object with rotated image data by 45 degrees counterclockwise. 111 | * Only callable if {@link #isRotateSupported()} is true. 112 | * 113 | * @return mixed A rotated version of this object. 114 | */ 115 | abstract public function rotateCounterClockwise45(): void; 116 | 117 | final public function toString(): string 118 | { 119 | $row = []; 120 | $result = ''; 121 | for ($y = 0; $y < $this->height; $y++) { 122 | $row = $this->getRow($y, $row); 123 | for ($x = 0; $x < $this->width; $x++) { 124 | $luminance = $row[$x] & 0xFF; 125 | $c = ''; 126 | if ($luminance < 0x40) { 127 | $c = '#'; 128 | } elseif ($luminance < 0x80) { 129 | $c = '+'; 130 | } elseif ($luminance < 0xC0) { 131 | $c = '.'; 132 | } else { 133 | $c = ' '; 134 | } 135 | $result .= ($c); 136 | } 137 | $result .= ('\n'); 138 | } 139 | 140 | return $result; 141 | } 142 | 143 | /** 144 | * Fetches one row of luminance data from the underlying platform's bitmap. Values range from 145 | * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have 146 | * to bitwise and with 0xff for each value. It is preferable for implementations of this method 147 | * to only fetch this row rather than the whole image, since no 2D Readers may be installed and 148 | * getMatrix() may never be called. 149 | * 150 | * @param $y ; The row to fetch, which must be in [0,getHeight()) 151 | * @param $row ; An optional preallocated array. If null or too small, it will be ignored. 152 | * Always use the returned object, and ignore the .length of the array. 153 | * 154 | * @return array 155 | * An array containing the luminance data. 156 | */ 157 | abstract public function getRow(int $y, array $row); 158 | } 159 | -------------------------------------------------------------------------------- /lib/NotFoundException.php: -------------------------------------------------------------------------------- 1 | $dataWidth || $top + $height > $dataHeight) { 51 | throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); 52 | } 53 | $this->dataWidth = $dataWidth; 54 | $this->dataHeight = $dataHeight; 55 | $this->left = $left; 56 | $this->top = $top; 57 | if ($reverseHorizontal) { 58 | $this->reverseHorizontal($width, $height); 59 | } 60 | } 61 | 62 | public function rotateCounterClockwise(): void 63 | { 64 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise"); 65 | } 66 | 67 | public function rotateCounterClockwise45(): void 68 | { 69 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise45"); 70 | } 71 | 72 | public function getRow($y, $row = null) 73 | { 74 | if ($y < 0 || $y >= $this->getHeight()) { 75 | throw new \InvalidArgumentException("Requested row is outside the image: " + $y); 76 | } 77 | $width = $this->getWidth(); 78 | if ($row == null || (is_countable($row) ? count($row) : 0) < $width) { 79 | $row = []; //new byte[width]; 80 | } 81 | $offset = ($y + $this->top) * $this->dataWidth + $this->left; 82 | $row = arraycopy($this->yuvData, $offset, $row, 0, $width); 83 | 84 | return $row; 85 | } 86 | 87 | 88 | public function getMatrix() 89 | { 90 | $width = $this->getWidth(); 91 | $height = $this->getHeight(); 92 | 93 | // If the caller asks for the entire underlying image, save the copy and give them the 94 | // original data. The docs specifically warn that result.length must be ignored. 95 | if ($width == $this->dataWidth && $height == $this->dataHeight) { 96 | return $this->yuvData; 97 | } 98 | 99 | $area = $width * $height; 100 | $matrix = []; //new byte[area]; 101 | $inputOffset = $this->top * $this->dataWidth + $this->left; 102 | 103 | // If the width matches the full width of the underlying data, perform a single copy. 104 | if ($width == $this->dataWidth) { 105 | $matrix = arraycopy($this->yuvData, $inputOffset, $matrix, 0, $area); 106 | 107 | return $matrix; 108 | } 109 | 110 | // Otherwise copy one cropped row at a time. 111 | $yuv = $this->yuvData; 112 | for ($y = 0; $y < $height; $y++) { 113 | $outputOffset = $y * $width; 114 | $matrix = arraycopy($this->yuvData, $inputOffset, $matrix, $outputOffset, $width); 115 | $inputOffset += $this->dataWidth; 116 | } 117 | 118 | return $matrix; 119 | } 120 | 121 | // @Override 122 | public function isCropSupported(): bool 123 | { 124 | return true; 125 | } 126 | 127 | // @Override 128 | public function crop($left, $top, $width, $height): \Zxing\PlanarYUVLuminanceSource 129 | { 130 | return new PlanarYUVLuminanceSource( 131 | $this->yuvData, 132 | $this->dataWidth, 133 | $this->dataHeight, 134 | $this->left + $left, 135 | $this->top + $top, 136 | $width, 137 | $height, 138 | false 139 | ); 140 | } 141 | 142 | /** 143 | * @return int[] 144 | */ 145 | public function renderThumbnail(): array 146 | { 147 | $width = (int)($this->getWidth() / self::$THUMBNAIL_SCALE_FACTOR); 148 | $height = (int)($this->getHeight() / self::$THUMBNAIL_SCALE_FACTOR); 149 | $pixels = []; //new int[width * height]; 150 | $yuv = $this->yuvData; 151 | $inputOffset = $this->top * $this->dataWidth + $this->left; 152 | 153 | for ($y = 0; $y < $height; $y++) { 154 | $outputOffset = $y * $width; 155 | for ($x = 0; $x < $width; $x++) { 156 | $grey = ($yuv[$inputOffset + $x * self::$THUMBNAIL_SCALE_FACTOR] & 0xff); 157 | $pixels[$outputOffset + $x] = (0xFF000000 | ($grey * 0x00010101)); 158 | } 159 | $inputOffset += $this->dataWidth * self::$THUMBNAIL_SCALE_FACTOR; 160 | } 161 | 162 | return $pixels; 163 | } 164 | 165 | /** 166 | * @return width of image from {@link #renderThumbnail()} 167 | */ 168 | /* 169 | public int getThumbnailWidth() { 170 | return getWidth() / THUMBNAIL_SCALE_FACTOR; 171 | }*/ 172 | 173 | /** 174 | * @return height of image from {@link #renderThumbnail()} 175 | */ 176 | /**public function getThumbnailHeight(): int 177 | { 178 | return getHeight() / THUMBNAIL_SCALE_FACTOR; 179 | }*/ 180 | 181 | /** 182 | * 183 | * @param int $width 184 | * @param int $height 185 | * @return void 186 | */ 187 | private function reverseHorizontal(int $width, int $height): void 188 | { 189 | $yuvData = $this->yuvData; 190 | for ($y = 0, $rowStart = $this->top * $this->dataWidth + $this->left; $y < $height; $y++, $rowStart += $this->dataWidth) { 191 | $middle = (int)round($rowStart + $width / 2); 192 | for ($x1 = $rowStart, $x2 = $rowStart + $width - 1; $x1 < $middle; $x1++, $x2--) { 193 | $temp = $yuvData[$x1]; 194 | $yuvData[$x1] = $yuvData[$x2]; 195 | $yuvData[$x2] = $temp; 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/QrReader.php: -------------------------------------------------------------------------------- 1 | readImage($imgSource); 44 | } else { 45 | $image = file_get_contents($imgSource); 46 | $im = imagecreatefromstring($image); 47 | } 48 | break; 49 | 50 | case QrReader::SOURCE_TYPE_BLOB: 51 | if ($useImagickIfAvailable && extension_loaded('imagick')) { 52 | $im = new \Imagick(); 53 | $im->readImageBlob($imgSource); 54 | } else { 55 | $im = imagecreatefromstring($imgSource); 56 | } 57 | break; 58 | 59 | case QrReader::SOURCE_TYPE_RESOURCE: 60 | $im = $imgSource; 61 | if ($useImagickIfAvailable && extension_loaded('imagick')) { 62 | $useImagickIfAvailable = true; 63 | } else { 64 | $useImagickIfAvailable = false; 65 | } 66 | break; 67 | } 68 | if ($useImagickIfAvailable && extension_loaded('imagick')) { 69 | if (!$im instanceof \Imagick) { 70 | throw new \InvalidArgumentException('Invalid image source.'); 71 | } 72 | $width = $im->getImageWidth(); 73 | $height = $im->getImageHeight(); 74 | $source = new IMagickLuminanceSource($im, $width, $height); 75 | } else { 76 | if (!$im instanceof \GdImage && !is_object($im)) { 77 | throw new \InvalidArgumentException('Invalid image source.'); 78 | } 79 | $width = imagesx($im); 80 | $height = imagesy($im); 81 | $source = new GDLuminanceSource($im, $width, $height); 82 | } 83 | $histo = new HybridBinarizer($source); 84 | $this->bitmap = new BinaryBitmap($histo); 85 | $this->reader = new QRCodeReader(); 86 | } 87 | 88 | public function decode($hints = null): void 89 | { 90 | try { 91 | $this->result = $this->reader->decode($this->bitmap, $hints); 92 | } catch (NotFoundException | FormatException | ChecksumException $e) { 93 | $this->result = false; 94 | $this->error = $e; 95 | } 96 | } 97 | 98 | public function text($hints = null) 99 | { 100 | $this->decode($hints); 101 | 102 | if ($this->result !== false && method_exists($this->result, 'toString')) { 103 | return $this->result->toString(); 104 | } 105 | 106 | return $this->result; 107 | } 108 | 109 | public function getResult(): bool|Result|null 110 | { 111 | return $this->result; 112 | } 113 | 114 | public function getError(): Exception|null 115 | { 116 | return $this->error; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/BitMatrixParser.php: -------------------------------------------------------------------------------- 1 | = 21 and 1 mod 4 40 | */ 41 | public function __construct($bitMatrix) 42 | { 43 | $dimension = $bitMatrix->getHeight(); 44 | if ($dimension < 21 || ($dimension & 0x03) != 1) { 45 | throw new FormatException(); 46 | } 47 | $this->bitMatrix = $bitMatrix; 48 | } 49 | 50 | /** 51 | *

Reads the bits in the {@link BitMatrix} representing the finder pattern in the 52 | * correct order in order to reconstruct the codewords bytes contained within the 53 | * QR Code.

54 | * 55 | * @return array bytes encoded within the QR Code 56 | * @throws FormatException if the exact number of bytes expected is not read 57 | */ 58 | public function readCodewords() 59 | { 60 | $formatInfo = $this->readFormatInformation(); 61 | $version = $this->readVersion(); 62 | 63 | // Get the data mask for the format used in this QR Code. This will exclude 64 | // some bits from reading as we wind through the bit matrix. 65 | $dataMask = DataMask::forReference($formatInfo->getDataMask()); 66 | $dimension = $this->bitMatrix->getHeight(); 67 | $dataMask->unmaskBitMatrix($this->bitMatrix, $dimension); 68 | 69 | $functionPattern = $version->buildFunctionPattern(); 70 | 71 | $readingUp = true; 72 | if ($version->getTotalCodewords()) { 73 | $result = fill_array(0, $version->getTotalCodewords(), 0); 74 | } else { 75 | $result = []; 76 | } 77 | $resultOffset = 0; 78 | $currentByte = 0; 79 | $bitsRead = 0; 80 | // Read columns in pairs, from right to left 81 | for ($j = $dimension - 1; $j > 0; $j -= 2) { 82 | if ($j == 6) { 83 | // Skip whole column with vertical alignment pattern; 84 | // saves time and makes the other code proceed more cleanly 85 | $j--; 86 | } 87 | // Read alternatingly from bottom to top then top to bottom 88 | for ($count = 0; $count < $dimension; $count++) { 89 | $i = $readingUp ? $dimension - 1 - $count : $count; 90 | for ($col = 0; $col < 2; $col++) { 91 | // Ignore bits covered by the function pattern 92 | if (!$functionPattern->get($j - $col, $i)) { 93 | // Read a bit 94 | $bitsRead++; 95 | $currentByte <<= 1; 96 | if ($this->bitMatrix->get($j - $col, $i)) { 97 | $currentByte |= 1; 98 | } 99 | // If we've made a whole byte, save it off 100 | if ($bitsRead == 8) { 101 | $result[$resultOffset++] = $currentByte; //(byte) 102 | $bitsRead = 0; 103 | $currentByte = 0; 104 | } 105 | } 106 | } 107 | } 108 | $readingUp ^= true; // readingUp = !readingUp; // switch directions 109 | } 110 | if ($resultOffset != $version->getTotalCodewords()) { 111 | throw new FormatException(); 112 | } 113 | 114 | return $result; 115 | } 116 | 117 | /** 118 | *

Reads format information from one of its two locations within the QR Code.

119 | * 120 | * @return {@link FormatInformation} encapsulating the QR Code's format info 121 | * @throws FormatException if both format information locations cannot be parsed as 122 | * the valid encoding of format information 123 | */ 124 | public function readFormatInformation() 125 | { 126 | if ($this->parsedFormatInfo != null) { 127 | return $this->parsedFormatInfo; 128 | } 129 | 130 | // Read top-left format info bits 131 | $formatInfoBits1 = 0; 132 | for ($i = 0; $i < 6; $i++) { 133 | $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1); 134 | } 135 | // .. and skip a bit in the timing pattern ... 136 | $formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1); 137 | $formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1); 138 | $formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1); 139 | // .. and skip a bit in the timing pattern ... 140 | for ($j = 5; $j >= 0; $j--) { 141 | $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1); 142 | } 143 | 144 | // Read the top-right/bottom-left pattern too 145 | $dimension = $this->bitMatrix->getHeight(); 146 | $formatInfoBits2 = 0; 147 | $jMin = $dimension - 7; 148 | for ($j = $dimension - 1; $j >= $jMin; $j--) { 149 | $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2); 150 | } 151 | for ($i = $dimension - 8; $i < $dimension; $i++) { 152 | $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2); 153 | } 154 | 155 | $parsedFormatInfo = FormatInformation::decodeFormatInformation($formatInfoBits1, $formatInfoBits2); 156 | if ($parsedFormatInfo != null) { 157 | return $parsedFormatInfo; 158 | } 159 | throw new FormatException(); 160 | } 161 | 162 | /** 163 | * @psalm-param 0 $versionBits 164 | * 165 | * @psalm-return 0|1 166 | */ 167 | private function copyBit(int|float $i, int|float $j, int $versionBits): int 168 | { 169 | $bit = $this->mirror ? $this->bitMatrix->get($j, $i) : $this->bitMatrix->get($i, $j); 170 | 171 | return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1; 172 | } 173 | 174 | /** 175 | *

Reads version information from one of its two locations within the QR Code.

176 | * 177 | * @return {@link Version} encapsulating the QR Code's version 178 | * @throws FormatException if both version information locations cannot be parsed as 179 | * the valid encoding of version information 180 | */ 181 | public function readVersion() 182 | { 183 | if ($this->parsedVersion != null) { 184 | return $this->parsedVersion; 185 | } 186 | 187 | $dimension = $this->bitMatrix->getHeight(); 188 | 189 | $provisionalVersion = ($dimension - 17) / 4; 190 | if ($provisionalVersion <= 6) { 191 | return Version::getVersionForNumber($provisionalVersion); 192 | } 193 | 194 | // Read top-right version info: 3 wide by 6 tall 195 | $versionBits = 0; 196 | $ijMin = $dimension - 11; 197 | for ($j = 5; $j >= 0; $j--) { 198 | for ($i = $dimension - 9; $i >= $ijMin; $i--) { 199 | $versionBits = $this->copyBit($i, $j, $versionBits); 200 | } 201 | } 202 | 203 | $theParsedVersion = Version::decodeVersionInformation($versionBits); 204 | if ($theParsedVersion != null && $theParsedVersion->getDimensionForVersion() == $dimension) { 205 | $this->parsedVersion = $theParsedVersion; 206 | 207 | return $theParsedVersion; 208 | } 209 | 210 | // Hmm, failed. Try bottom left: 6 wide by 3 tall 211 | $versionBits = 0; 212 | for ($i = 5; $i >= 0; $i--) { 213 | for ($j = $dimension - 9; $j >= $ijMin; $j--) { 214 | $versionBits = $this->copyBit($i, $j, $versionBits); 215 | } 216 | } 217 | 218 | $theParsedVersion = Version::decodeVersionInformation($versionBits); 219 | if ($theParsedVersion != null && $theParsedVersion->getDimensionForVersion() == $dimension) { 220 | $this->parsedVersion = $theParsedVersion; 221 | 222 | return $theParsedVersion; 223 | } 224 | throw new FormatException("both version information locations cannot be parsed as the valid encoding of version information"); 225 | } 226 | 227 | /** 228 | * Revert the mask removal done while reading the code words. The bit matrix should revert to its original state. 229 | */ 230 | public function remask(): void 231 | { 232 | if ($this->parsedFormatInfo == null) { 233 | return; // We have no format information, and have no data mask 234 | } 235 | $dataMask = DataMask::forReference($this->parsedFormatInfo->getDataMask()); 236 | $dimension = $this->bitMatrix->getHeight(); 237 | $dataMask->unmaskBitMatrix($this->bitMatrix, $dimension); 238 | } 239 | 240 | /** 241 | * Prepare the parser for a mirrored operation. 242 | * This flag has effect only on the {@link #readFormatInformation()} and the 243 | * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the 244 | * {@link #mirror()} method should be called. 245 | * 246 | * @param bool $mirror Whether to read version and format information mirrored. 247 | */ 248 | public function setMirror($mirror): void 249 | { 250 | $parsedVersion = null; 251 | $parsedFormatInfo = null; 252 | $this->mirror = $mirror; 253 | } 254 | 255 | /** Mirror the bit matrix in order to attempt a second reading. */ 256 | public function mirror(): void 257 | { 258 | for ($x = 0; $x < $this->bitMatrix->getWidth(); $x++) { 259 | for ($y = $x + 1; $y < $this->bitMatrix->getHeight(); $y++) { 260 | if ($this->bitMatrix->get($x, $y) != $this->bitMatrix->get($y, $x)) { 261 | $this->bitMatrix->flip($y, $x); 262 | $this->bitMatrix->flip($x, $y); 263 | } 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/DataBlock.php: -------------------------------------------------------------------------------- 1 | Encapsulates a block of data within a QR Code. QR Codes may split their data into 22 | * multiple blocks, each of which is a unit of data and error-correction codewords. Each 23 | * is represented by an instance of this class.

24 | * 25 | * @author Sean Owen 26 | */ 27 | final class DataBlock 28 | { 29 | //byte[] 30 | 31 | private function __construct(private $numDataCodewords, private $codewords) 32 | { 33 | } 34 | 35 | /** 36 | *

When QR Codes use multiple data blocks, they are actually interleaved. 37 | * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This 38 | * method will separate the data into original blocks.

39 | * 40 | * @param array $rawCodewords as read directly from the QR Code 41 | * @param Version $version of the QR Code 42 | * @param ErrorCorrectionLevel $ecLevel error-correction level of the QR Code 43 | * 44 | * @return \Zxing\Qrcode\Decoder\DataBlock[] DataBlocks containing original bytes, "de-interleaved" from representation in the 45 | QR Code 46 | */ 47 | public static function getDataBlocks( 48 | $rawCodewords, 49 | Version $version, 50 | ErrorCorrectionLevel $ecLevel 51 | ): array { 52 | if ((is_countable($rawCodewords) ? count($rawCodewords) : 0) != $version->getTotalCodewords()) { 53 | throw new \InvalidArgumentException(); 54 | } 55 | 56 | // Figure out the number and size of data blocks used by this version and 57 | // error correction level 58 | $ecBlocks = $version->getECBlocksForLevel($ecLevel); 59 | 60 | // First count the total number of data blocks 61 | $totalBlocks = 0; 62 | $ecBlockArray = $ecBlocks->getECBlocks(); 63 | foreach ($ecBlockArray as $ecBlock) { 64 | $totalBlocks += $ecBlock->getCount(); 65 | } 66 | 67 | // Now establish DataBlocks of the appropriate size and number of data codewords 68 | $result = []; //new DataBlock[$totalBlocks]; 69 | $numResultBlocks = 0; 70 | foreach ($ecBlockArray as $ecBlock) { 71 | $ecBlockCount = $ecBlock->getCount(); 72 | for ($i = 0; $i < $ecBlockCount; $i++) { 73 | $numDataCodewords = $ecBlock->getDataCodewords(); 74 | $numBlockCodewords = $ecBlocks->getECCodewordsPerBlock() + $numDataCodewords; 75 | $result[$numResultBlocks++] = new DataBlock($numDataCodewords, fill_array(0, $numBlockCodewords, 0)); 76 | } 77 | } 78 | 79 | // All blocks have the same amount of data, except that the last n 80 | // (where n may be 0) have 1 more byte. Figure out where these start. 81 | $shorterBlocksTotalCodewords = is_countable($result[0]->codewords) ? count($result[0]->codewords) : 0; 82 | $longerBlocksStartAt = count($result) - 1; 83 | while ($longerBlocksStartAt >= 0) { 84 | $numCodewords = is_countable($result[$longerBlocksStartAt]->codewords) ? count($result[$longerBlocksStartAt]->codewords) : 0; 85 | if ($numCodewords == $shorterBlocksTotalCodewords) { 86 | break; 87 | } 88 | $longerBlocksStartAt--; 89 | } 90 | $longerBlocksStartAt++; 91 | 92 | $shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $ecBlocks->getECCodewordsPerBlock(); 93 | // The last elements of result may be 1 element longer; 94 | // first fill out as many elements as all of them have 95 | $rawCodewordsOffset = 0; 96 | for ($i = 0; $i < $shorterBlocksNumDataCodewords; $i++) { 97 | for ($j = 0; $j < $numResultBlocks; $j++) { 98 | $result[$j]->codewords[$i] = $rawCodewords[$rawCodewordsOffset++]; 99 | } 100 | } 101 | // Fill out the last data block in the longer ones 102 | for ($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++) { 103 | $result[$j]->codewords[$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++]; 104 | } 105 | // Now add in error correction blocks 106 | $max = is_countable($result[0]->codewords) ? count($result[0]->codewords) : 0; 107 | for ($i = $shorterBlocksNumDataCodewords; $i < $max; $i++) { 108 | for ($j = 0; $j < $numResultBlocks; $j++) { 109 | $iOffset = $j < $longerBlocksStartAt ? $i : $i + 1; 110 | $result[$j]->codewords[$iOffset] = $rawCodewords[$rawCodewordsOffset++]; 111 | } 112 | } 113 | 114 | return $result; 115 | } 116 | 117 | public function getNumDataCodewords() 118 | { 119 | return $this->numDataCodewords; 120 | } 121 | 122 | public function getCodewords() 123 | { 124 | return $this->codewords; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/DataMask.php: -------------------------------------------------------------------------------- 1 | Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations 24 | * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix, 25 | * including areas used for finder patterns, timing patterns, etc. These areas should be unused 26 | * after the point they are unmasked anyway.

27 | * 28 | *

Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position 29 | * and j is row position. In fact, as the text says, i is row position and j is column position.

30 | * 31 | * @author Sean Owen 32 | */ 33 | abstract class DataMask 34 | { 35 | /** 36 | * See ISO 18004:2006 6.8.1 37 | */ 38 | private static array $DATA_MASKS = []; 39 | 40 | public function __construct() 41 | { 42 | } 43 | 44 | public static function Init(): void 45 | { 46 | self::$DATA_MASKS = [ 47 | new DataMask000(), 48 | new DataMask001(), 49 | new DataMask010(), 50 | new DataMask011(), 51 | new DataMask100(), 52 | new DataMask101(), 53 | new DataMask110(), 54 | new DataMask111(), 55 | ]; 56 | } 57 | 58 | /** 59 | * @param int $reference a value between 0 and 7 indicating one of the eight possible 60 | * data mask patterns a QR Code may use 61 | * 62 | * @return DataMask encapsulating the data mask pattern 63 | */ 64 | public static function forReference($reference) 65 | { 66 | if ($reference < 0 || $reference > 7) { 67 | throw new \InvalidArgumentException(); 68 | } 69 | 70 | return self::$DATA_MASKS[$reference]; 71 | } 72 | 73 | /** 74 | *

Implementations of this method reverse the data masking process applied to a QR Code and 75 | * make its bits ready to read.

76 | * 77 | * @param BitMatrix $bits representation of QR Code bits 78 | * @param int $dimension dimension of QR Code, represented by bits, being unmasked 79 | */ 80 | final public function unmaskBitMatrix($bits, $dimension): void 81 | { 82 | for ($i = 0; $i < $dimension; $i++) { 83 | for ($j = 0; $j < $dimension; $j++) { 84 | if ($this->isMasked($i, $j)) { 85 | $bits->flip($j, $i); 86 | } 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * @psalm-param 0|positive-int $i 93 | * @psalm-param 0|positive-int $j 94 | */ 95 | abstract public function isMasked(int $i, int $j); 96 | } 97 | 98 | DataMask::Init(); 99 | 100 | /** 101 | * 000: mask bits for which (x + y) mod 2 == 0 102 | */ 103 | final class DataMask000 extends DataMask 104 | { 105 | // @Override 106 | public function isMasked($i, $j): bool 107 | { 108 | return (($i + $j) & 0x01) == 0; 109 | } 110 | } 111 | 112 | /** 113 | * 001: mask bits for which x mod 2 == 0 114 | */ 115 | final class DataMask001 extends DataMask 116 | { 117 | public function isMasked($i, $j): bool 118 | { 119 | return ($i & 0x01) == 0; 120 | } 121 | } 122 | 123 | /** 124 | * 010: mask bits for which y mod 3 == 0 125 | */ 126 | final class DataMask010 extends DataMask 127 | { 128 | public function isMasked($i, $j): bool 129 | { 130 | return $j % 3 == 0; 131 | } 132 | } 133 | 134 | /** 135 | * 011: mask bits for which (x + y) mod 3 == 0 136 | */ 137 | final class DataMask011 extends DataMask 138 | { 139 | public function isMasked($i, $j): bool 140 | { 141 | return ($i + $j) % 3 == 0; 142 | } 143 | } 144 | 145 | /** 146 | * 100: mask bits for which (x/2 + y/3) mod 2 == 0 147 | */ 148 | final class DataMask100 extends DataMask 149 | { 150 | public function isMasked($i, $j): bool 151 | { 152 | return (int)(((int)($i / 2) + (int)($j / 3)) & 0x01) == 0; 153 | } 154 | } 155 | 156 | /** 157 | * 101: mask bits for which xy mod 2 + xy mod 3 == 0 158 | */ 159 | final class DataMask101 extends DataMask 160 | { 161 | public function isMasked($i, $j): bool 162 | { 163 | $temp = $i * $j; 164 | 165 | return ($temp & 0x01) + ($temp % 3) == 0; 166 | } 167 | } 168 | 169 | /** 170 | * 110: mask bits for which (xy mod 2 + xy mod 3) mod 2 == 0 171 | */ 172 | final class DataMask110 extends DataMask 173 | { 174 | public function isMasked($i, $j): bool 175 | { 176 | $temp = $i * $j; 177 | 178 | return ((($temp & 0x01) + ($temp % 3)) & 0x01) == 0; 179 | } 180 | } 181 | 182 | /** 183 | * 111: mask bits for which ((x+y)mod 2 + xy mod 3) mod 2 == 0 184 | */ 185 | final class DataMask111 extends DataMask 186 | { 187 | public function isMasked($i, $j): bool 188 | { 189 | return (((($i + $j) & 0x01) + (($i * $j) % 3)) & 0x01) == 0; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/Decoder.php: -------------------------------------------------------------------------------- 1 | The main class which implements QR Code decoding -- as opposed to locating and extracting 30 | * the QR Code from an image.

31 | * 32 | * @author Sean Owen 33 | */ 34 | final class Decoder 35 | { 36 | private readonly \Zxing\Common\Reedsolomon\ReedSolomonDecoder $rsDecoder; 37 | 38 | public function __construct() 39 | { 40 | $this->rsDecoder = new ReedSolomonDecoder(GenericGF::$QR_CODE_FIELD_256); 41 | } 42 | 43 | public function decode(BitMatrix|BitMatrixParser $variable, array|null $hints = null): string|DecoderResult 44 | { 45 | if (is_array($variable)) { 46 | return $this->decodeImage($variable, $hints); 47 | } elseif ($variable instanceof BitMatrix) { 48 | return $this->decodeBits($variable, $hints); 49 | } elseif ($variable instanceof BitMatrixParser) { 50 | return $this->decodeParser($variable, $hints); 51 | } 52 | die('decode error Decoder.php'); 53 | } 54 | 55 | /** 56 | *

Convenience method that can decode a QR Code represented as a 2D array of booleans. 57 | * "true" is taken to mean a black module.

58 | * 59 | * @param array $image booleans representing white/black QR Code modules 60 | * @param array|null $hints decoding hints that should be used to influence decoding 61 | * 62 | * @return DecoderResult|string text and bytes encoded within the QR Code 63 | * 64 | * @throws FormatException if the QR Code cannot be decoded 65 | * @throws ChecksumException if error correction fails 66 | */ 67 | public function decodeImage(array $image, $hints = null): string|DecoderResult 68 | { 69 | $dimension = is_countable($image) ? count($image) : 0; 70 | $bits = new BitMatrix($dimension); 71 | for ($i = 0; $i < $dimension; $i++) { 72 | for ($j = 0; $j < $dimension; $j++) { 73 | if ($image[$i][$j]) { 74 | $bits->set($j, $i); 75 | } 76 | } 77 | } 78 | 79 | return $this->decode($bits, $hints); 80 | } 81 | 82 | 83 | /** 84 | *

Decodes a QR Code represented as a {@link BitMatrix}. A 1 or "true" is taken to mean a black module.

85 | * 86 | * @param BitMatrix $bits booleans representing white/black QR Code modules 87 | * @param array|null $hints decoding hints that should be used to influence decoding 88 | * 89 | * @return DecoderResult|string string text and bytes encoded within the QR Code 90 | * 91 | * @throws FormatException if the QR Code cannot be decoded 92 | * @throws ChecksumException if error correction fails 93 | */ 94 | public function decodeBits(\Zxing\Common\BitMatrix $bits, $hints = null): string|DecoderResult 95 | { 96 | 97 | // Construct a parser and read version, error-correction level 98 | $parser = new BitMatrixParser($bits); 99 | $fe = null; 100 | $ce = null; 101 | try { 102 | return $this->decode($parser, $hints); 103 | } catch (FormatException $e) { 104 | $fe = $e; 105 | } catch (ChecksumException $e) { 106 | $ce = $e; 107 | } 108 | 109 | try { 110 | 111 | // Revert the bit matrix 112 | $parser->remask(); 113 | 114 | // Will be attempting a mirrored reading of the version and format info. 115 | $parser->setMirror(true); 116 | 117 | // Preemptively read the version. 118 | $parser->readVersion(); 119 | 120 | // Preemptively read the format information. 121 | $parser->readFormatInformation(); 122 | 123 | /* 124 | * Since we're here, this means we have successfully detected some kind 125 | * of version and format information when mirrored. This is a good sign, 126 | * that the QR code may be mirrored, and we should try once more with a 127 | * mirrored content. 128 | */ 129 | // Prepare for a mirrored reading. 130 | $parser->mirror(); 131 | 132 | $result = $this->decode($parser, $hints); 133 | 134 | // Success! Notify the caller that the code was mirrored. 135 | $result->setOther(new QRCodeDecoderMetaData(true)); 136 | 137 | return $result; 138 | } catch (FormatException $e) { // catch (FormatException | ChecksumException e) { 139 | // Throw the exception from the original reading 140 | if ($fe != null) { 141 | throw $fe; 142 | } 143 | if ($ce != null) { 144 | throw $ce; 145 | } 146 | throw $e; 147 | } 148 | } 149 | 150 | private function decodeParser(\Zxing\Qrcode\Decoder\BitMatrixParser $parser, array $hints = null): DecoderResult 151 | { 152 | $version = $parser->readVersion(); 153 | $ecLevel = $parser->readFormatInformation()->getErrorCorrectionLevel(); 154 | 155 | // Read codewords 156 | $codewords = $parser->readCodewords(); 157 | // Separate into data blocks 158 | $dataBlocks = DataBlock::getDataBlocks($codewords, $version, $ecLevel); 159 | 160 | // Count total number of data bytes 161 | $totalBytes = 0; 162 | foreach ($dataBlocks as $dataBlock) { 163 | $totalBytes += $dataBlock->getNumDataCodewords(); 164 | } 165 | $resultBytes = fill_array(0, $totalBytes, 0); 166 | $resultOffset = 0; 167 | 168 | // Error-correct and copy data blocks together into a stream of bytes 169 | foreach ($dataBlocks as $dataBlock) { 170 | $codewordBytes = $dataBlock->getCodewords(); 171 | $numDataCodewords = $dataBlock->getNumDataCodewords(); 172 | $this->correctErrors($codewordBytes, $numDataCodewords); 173 | for ($i = 0; $i < $numDataCodewords; $i++) { 174 | $resultBytes[$resultOffset++] = $codewordBytes[$i]; 175 | } 176 | } 177 | 178 | // Decode the contents of that stream of bytes 179 | return DecodedBitStreamParser::decode($resultBytes, $version, $ecLevel, $hints); 180 | } 181 | 182 | /** 183 | *

Given data and error-correction codewords received, possibly corrupted by errors, attempts to 184 | * correct the errors in-place using Reed-Solomon error correction.

185 | * 186 | * @param array $codewordBytes and error correction codewords 187 | * @param int $numDataCodewords of codewords that are data bytes 188 | * 189 | * @throws ChecksumException if error correction fails 190 | */ 191 | private function correctErrors(&$codewordBytes, int $numDataCodewords): void 192 | { 193 | $numCodewords = is_countable($codewordBytes) ? count($codewordBytes) : 0; 194 | // First read into an array of ints 195 | $codewordsInts = fill_array(0, $numCodewords, 0); 196 | for ($i = 0; $i < $numCodewords; $i++) { 197 | $codewordsInts[$i] = $codewordBytes[$i] & 0xFF; 198 | } 199 | $numECCodewords = (is_countable($codewordBytes) ? count($codewordBytes) : 0) - $numDataCodewords; 200 | try { 201 | $this->rsDecoder->decode($codewordsInts, $numECCodewords); 202 | } catch (ReedSolomonException) { 203 | throw ChecksumException::getChecksumInstance(); 204 | } 205 | // Copy back into array of bytes -- only need to worry about the bytes that were data 206 | // We don't care about errors in the error-correction codewords 207 | for ($i = 0; $i < $numDataCodewords; $i++) { 208 | $codewordBytes[$i] = $codewordsInts[$i]; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/ErrorCorrectionLevel.php: -------------------------------------------------------------------------------- 1 | See ISO 18004:2006, 6.5.1. This enum encapsulates the four error correction levels 22 | * defined by the QR code standard.

23 | * 24 | * @author Sean Owen 25 | */ 26 | class ErrorCorrectionLevel 27 | { 28 | /** 29 | * @var \Zxing\Qrcode\Decoder\ErrorCorrectionLevel[]|null 30 | */ 31 | private static ?array $FOR_BITS = null; 32 | 33 | public function __construct(private $bits, private $ordinal = 0) 34 | { 35 | } 36 | 37 | public static function Init(): void 38 | { 39 | self::$FOR_BITS = [ 40 | 41 | 42 | new ErrorCorrectionLevel(0x00, 1), //M 43 | new ErrorCorrectionLevel(0x01, 0), //L 44 | new ErrorCorrectionLevel(0x02, 3), //H 45 | new ErrorCorrectionLevel(0x03, 2), //Q 46 | 47 | ]; 48 | } 49 | /** L = ~7% correction */ 50 | // self::$L = new ErrorCorrectionLevel(0x01); 51 | /** M = ~15% correction */ 52 | //self::$M = new ErrorCorrectionLevel(0x00); 53 | /** Q = ~25% correction */ 54 | //self::$Q = new ErrorCorrectionLevel(0x03); 55 | /** H = ~30% correction */ 56 | //self::$H = new ErrorCorrectionLevel(0x02); 57 | /** 58 | * @param int $bits containing the two bits encoding a QR Code's error correction level 59 | * 60 | * @return null|self representing the encoded error correction level 61 | */ 62 | public static function forBits(int $bits): self|null 63 | { 64 | if ($bits < 0 || $bits >= (is_countable(self::$FOR_BITS) ? count(self::$FOR_BITS) : 0)) { 65 | throw new \InvalidArgumentException(); 66 | } 67 | $level = self::$FOR_BITS[$bits]; 68 | 69 | // $lev = self::$$bit; 70 | return $level; 71 | } 72 | 73 | 74 | public function getBits() 75 | { 76 | return $this->bits; 77 | } 78 | 79 | public function toString() 80 | { 81 | return $this->bits; 82 | } 83 | 84 | public function getOrdinal() 85 | { 86 | return $this->ordinal; 87 | } 88 | } 89 | 90 | ErrorCorrectionLevel::Init(); 91 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/FormatInformation.php: -------------------------------------------------------------------------------- 1 | Encapsulates a QR Code's format information, including the data mask used and 22 | * error correction level.

23 | * 24 | * @author Sean Owen 25 | * @see DataMask 26 | * @see ErrorCorrectionLevel 27 | */ 28 | final class FormatInformation 29 | { 30 | public static $FORMAT_INFO_MASK_QR; 31 | 32 | /** 33 | * See ISO 18004:2006, Annex C, Table C.1 34 | */ 35 | public static $FORMAT_INFO_DECODE_LOOKUP; 36 | /** 37 | * Offset i holds the number of 1 bits in the binary representation of i 38 | * @var int[]|null 39 | */ 40 | private static ?array $BITS_SET_IN_HALF_BYTE = null; 41 | 42 | private readonly \Zxing\Qrcode\Decoder\ErrorCorrectionLevel $errorCorrectionLevel; 43 | private readonly int $dataMask; 44 | 45 | private function __construct($formatInfo) 46 | { 47 | // Bits 3,4 48 | $this->errorCorrectionLevel = ErrorCorrectionLevel::forBits(($formatInfo >> 3) & 0x03); 49 | // Bottom 3 bits 50 | $this->dataMask = ($formatInfo & 0x07);//(byte) 51 | } 52 | 53 | public static function Init(): void 54 | { 55 | self::$FORMAT_INFO_MASK_QR = 0x5412; 56 | self::$BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]; 57 | self::$FORMAT_INFO_DECODE_LOOKUP = [ 58 | [0x5412, 0x00], 59 | [0x5125, 0x01], 60 | [0x5E7C, 0x02], 61 | [0x5B4B, 0x03], 62 | [0x45F9, 0x04], 63 | [0x40CE, 0x05], 64 | [0x4F97, 0x06], 65 | [0x4AA0, 0x07], 66 | [0x77C4, 0x08], 67 | [0x72F3, 0x09], 68 | [0x7DAA, 0x0A], 69 | [0x789D, 0x0B], 70 | [0x662F, 0x0C], 71 | [0x6318, 0x0D], 72 | [0x6C41, 0x0E], 73 | [0x6976, 0x0F], 74 | [0x1689, 0x10], 75 | [0x13BE, 0x11], 76 | [0x1CE7, 0x12], 77 | [0x19D0, 0x13], 78 | [0x0762, 0x14], 79 | [0x0255, 0x15], 80 | [0x0D0C, 0x16], 81 | [0x083B, 0x17], 82 | [0x355F, 0x18], 83 | [0x3068, 0x19], 84 | [0x3F31, 0x1A], 85 | [0x3A06, 0x1B], 86 | [0x24B4, 0x1C], 87 | [0x2183, 0x1D], 88 | [0x2EDA, 0x1E], 89 | [0x2BED, 0x1F], 90 | ]; 91 | } 92 | 93 | /** 94 | * @param $maskedFormatInfo1 ; format info indicator, with mask still applied 95 | * @param $maskedFormatInfo2 ; second copy of same info; both are checked at the same time 96 | * to establish best match 97 | * 98 | * @return FormatInformation|null information about the format it specifies, or {@code null} 99 | * if doesn't seem to match any known pattern 100 | * 101 | * @psalm-param 0|1 $maskedFormatInfo1 102 | * @psalm-param 0|1 $maskedFormatInfo2 103 | */ 104 | public static function decodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2) 105 | { 106 | $formatInfo = self::doDecodeFormatInformation($maskedFormatInfo1, $maskedFormatInfo2); 107 | if ($formatInfo != null) { 108 | return $formatInfo; 109 | } 110 | // Should return null, but, some QR codes apparently 111 | // do not mask this info. Try again by actually masking the pattern 112 | // first 113 | return self::doDecodeFormatInformation( 114 | $maskedFormatInfo1 ^ self::$FORMAT_INFO_MASK_QR, 115 | $maskedFormatInfo2 ^ self::$FORMAT_INFO_MASK_QR 116 | ); 117 | } 118 | 119 | /** 120 | * @psalm-param 0|1 $maskedFormatInfo1 121 | * @psalm-param 0|1 $maskedFormatInfo2 122 | */ 123 | private static function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2): self|null 124 | { 125 | // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing 126 | $bestDifference = PHP_INT_MAX; 127 | $bestFormatInfo = 0; 128 | foreach (self::$FORMAT_INFO_DECODE_LOOKUP as $decodeInfo) { 129 | $targetInfo = $decodeInfo[0]; 130 | if ($targetInfo == $maskedFormatInfo1 || $targetInfo == $maskedFormatInfo2) { 131 | // Found an exact match 132 | return new FormatInformation($decodeInfo[1]); 133 | } 134 | $bitsDifference = self::numBitsDiffering($maskedFormatInfo1, $targetInfo); 135 | if ($bitsDifference < $bestDifference) { 136 | $bestFormatInfo = $decodeInfo[1]; 137 | $bestDifference = $bitsDifference; 138 | } 139 | if ($maskedFormatInfo1 != $maskedFormatInfo2) { 140 | // also try the other option 141 | $bitsDifference = self::numBitsDiffering($maskedFormatInfo2, $targetInfo); 142 | if ($bitsDifference < $bestDifference) { 143 | $bestFormatInfo = $decodeInfo[1]; 144 | $bestDifference = $bitsDifference; 145 | } 146 | } 147 | } 148 | // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits 149 | // differing means we found a match 150 | if ($bestDifference <= 3) { 151 | return new FormatInformation($bestFormatInfo); 152 | } 153 | 154 | return null; 155 | } 156 | 157 | /** 158 | * @psalm-param 0|1 $a 159 | */ 160 | public static function numBitsDiffering(int $a, $b): int 161 | { 162 | $a ^= $b; // a now has a 1 bit exactly where its bit differs with b's 163 | // Count bits set quickly with a series of lookups: 164 | return self::$BITS_SET_IN_HALF_BYTE[$a & 0x0F] + 165 | self::$BITS_SET_IN_HALF_BYTE[(int)(uRShift($a, 4) & 0x0F)] + 166 | self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 8) & 0x0F)] + 167 | self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 12) & 0x0F)] + 168 | self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 16) & 0x0F)] + 169 | self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 20) & 0x0F)] + 170 | self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 24) & 0x0F)] + 171 | self::$BITS_SET_IN_HALF_BYTE[(uRShift($a, 28) & 0x0F)]; 172 | } 173 | 174 | public function getErrorCorrectionLevel(): ErrorCorrectionLevel 175 | { 176 | return $this->errorCorrectionLevel; 177 | } 178 | 179 | public function getDataMask(): int 180 | { 181 | return $this->dataMask; 182 | } 183 | 184 | 185 | public function hashCode() 186 | { 187 | return ($this->errorCorrectionLevel->ordinal() << 3) | (int)($this->dataMask); 188 | } 189 | 190 | 191 | public function equals($o): bool 192 | { 193 | if (!($o instanceof FormatInformation)) { 194 | return false; 195 | } 196 | $other = $o; 197 | 198 | return $this->errorCorrectionLevel == $other->errorCorrectionLevel && 199 | $this->dataMask == $other->dataMask; 200 | } 201 | } 202 | 203 | FormatInformation::Init(); 204 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/Mode.php: -------------------------------------------------------------------------------- 1 | See ISO 18004:2006, 6.4.1, Tables 2 and 3. This enum encapsulates the various modes in which 22 | * data can be encoded to bits in the QR code standard.

23 | * 24 | * @author Sean Owen 25 | */ 26 | class Mode 27 | { 28 | public static $TERMINATOR; 29 | public static $NUMERIC; 30 | public static $ALPHANUMERIC; 31 | public static $STRUCTURED_APPEND; 32 | public static $BYTE; 33 | public static $ECI; 34 | public static $KANJI; 35 | public static $FNC1_FIRST_POSITION; 36 | public static $FNC1_SECOND_POSITION; 37 | public static $HANZI; 38 | 39 | public function __construct(private $characterCountBitsForVersions, private $bits) 40 | { 41 | } 42 | 43 | public static function Init(): void 44 | { 45 | self::$TERMINATOR = new Mode([0, 0, 0], 0x00); // Not really a mode... 46 | self::$NUMERIC = new Mode([10, 12, 14], 0x01); 47 | self::$ALPHANUMERIC = new Mode([9, 11, 13], 0x02); 48 | self::$STRUCTURED_APPEND = new Mode([0, 0, 0], 0x03); // Not supported 49 | self::$BYTE = new Mode([8, 16, 16], 0x04); 50 | self::$ECI = new Mode([0, 0, 0], 0x07); // character counts don't apply 51 | self::$KANJI = new Mode([8, 10, 12], 0x08); 52 | self::$FNC1_FIRST_POSITION = new Mode([0, 0, 0], 0x05); 53 | self::$FNC1_SECOND_POSITION = new Mode([0, 0, 0], 0x09); 54 | /** See GBT 18284-2000; "Hanzi" is a transliteration of this mode name. */ 55 | self::$HANZI = new Mode([8, 10, 12], 0x0D); 56 | } 57 | 58 | /** 59 | * @param int $bits four bits encoding a QR Code data mode 60 | * 61 | * @return Mode encoded by these bits 62 | * @throws \InvalidArgumentException if bits do not correspond to a known mode 63 | */ 64 | public static function forBits($bits) 65 | { 66 | return match ($bits) { 67 | 0x0 => self::$TERMINATOR, 68 | 0x1 => self::$NUMERIC, 69 | 0x2 => self::$ALPHANUMERIC, 70 | 0x3 => self::$STRUCTURED_APPEND, 71 | 0x4 => self::$BYTE, 72 | 0x5 => self::$FNC1_FIRST_POSITION, 73 | 0x7 => self::$ECI, 74 | 0x8 => self::$KANJI, 75 | 0x9 => self::$FNC1_SECOND_POSITION, 76 | 0xD => self::$HANZI, 77 | default => throw new \InvalidArgumentException(), 78 | }; 79 | } 80 | 81 | /** 82 | * @param version $version in question 83 | * 84 | * @return int number of bits used, in this QR Code symbol {@link Version}, to encode the 85 | * count of characters that will follow encoded in this Mode 86 | */ 87 | public function getCharacterCountBits(\Zxing\Qrcode\Decoder\version $version) 88 | { 89 | $number = $version->getVersionNumber(); 90 | $offset = 0; 91 | if ($number <= 9) { 92 | $offset = 0; 93 | } elseif ($number <= 26) { 94 | $offset = 1; 95 | } else { 96 | $offset = 2; 97 | } 98 | 99 | return $this->characterCountBitsForVersions[$offset]; 100 | } 101 | 102 | public function getBits() 103 | { 104 | return $this->bits; 105 | } 106 | } 107 | 108 | Mode::Init(); 109 | -------------------------------------------------------------------------------- /lib/Qrcode/Decoder/QRCodeDecoderMetaData.php: -------------------------------------------------------------------------------- 1 | mirrored; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/Qrcode/Detector/AlignmentPattern.php: -------------------------------------------------------------------------------- 1 | Encapsulates an alignment pattern, which are the smaller square patterns found in 24 | * all but the simplest QR Codes.

25 | * 26 | * @author Sean Owen 27 | */ 28 | final class AlignmentPattern extends ResultPoint 29 | { 30 | public function __construct($posX, $posY, private $estimatedModuleSize) 31 | { 32 | parent::__construct($posX, $posY); 33 | } 34 | 35 | /** 36 | *

Determines if this alignment pattern "about equals" an alignment pattern at the stated 37 | * position and size -- meaning, it is at nearly the same center with nearly the same size.

38 | */ 39 | public function aboutEquals($moduleSize, $i, $j): bool 40 | { 41 | if (abs($i - $this->getY()) <= $moduleSize && abs($j - $this->getX()) <= $moduleSize) { 42 | $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize); 43 | 44 | return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * Combines this object's current estimate of a finder pattern position and module size 52 | * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two. 53 | */ 54 | public function combineEstimate($i, $j, $newModuleSize): \Zxing\Qrcode\Detector\AlignmentPattern 55 | { 56 | $combinedX = ($this->getX() + $j) / 2.0; 57 | $combinedY = ($this->getY() + $i) / 2.0; 58 | $combinedModuleSize = ($this->estimatedModuleSize + $newModuleSize) / 2.0; 59 | 60 | return new AlignmentPattern($combinedX, $combinedY, $combinedModuleSize); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/Qrcode/Detector/AlignmentPatternFinder.php: -------------------------------------------------------------------------------- 1 | This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder 24 | * patterns but are smaller and appear at regular intervals throughout the image.

25 | * 26 | *

At the moment this only looks for the bottom-right alignment pattern.

27 | * 28 | *

This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied, 29 | * pasted and stripped down here for maximum performance but does unfortunately duplicate 30 | * some code.

31 | * 32 | *

This class is thread-safe but not reentrant. Each thread must allocate its own object.

33 | * 34 | * @author Sean Owen 35 | */ 36 | final class AlignmentPatternFinder 37 | { 38 | private array $possibleCenters = []; 39 | private array $crossCheckStateCount = []; 40 | 41 | /** 42 | *

Creates a finder that will look in a portion of the whole image.

43 | * 44 | * @param \Imagick image $image to search 45 | * @param int left $startX column from which to start searching 46 | * @param int top $startY row from which to start searching 47 | * @param float width $width of region to search 48 | * @param float height $height of region to search 49 | * @param float estimated $moduleSize module size so far 50 | */ 51 | public function __construct(private $image, private $startX, private $startY, private $width, private $height, private $moduleSize, private $resultPointCallback) 52 | { 53 | } 54 | 55 | /** 56 | *

This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since 57 | * it's pretty performance-critical and so is written to be fast foremost.

58 | * 59 | * @return {@link AlignmentPattern} if found 60 | * @throws NotFoundException if not found 61 | */ 62 | public function find() 63 | { 64 | $startX = $this->startX; 65 | $height = $this->height; 66 | $maxJ = $startX + $this->width; 67 | $middleI = $this->startY + ($height / 2); 68 | // We are looking for black/white/black modules in 1:1:1 ratio; 69 | // this tracks the number of black/white/black modules seen so far 70 | $stateCount = []; 71 | for ($iGen = 0; $iGen < $height; $iGen++) { 72 | // Search from middle outwards 73 | $i = $middleI + (($iGen & 0x01) == 0 ? ($iGen + 1) / 2 : -(($iGen + 1) / 2)); 74 | $i = (int)($i); 75 | $stateCount[0] = 0; 76 | $stateCount[1] = 0; 77 | $stateCount[2] = 0; 78 | $j = $startX; 79 | // Burn off leading white pixels before anything else; if we start in the middle of 80 | // a white run, it doesn't make sense to count its length, since we don't know if the 81 | // white run continued to the left of the start point 82 | while ($j < $maxJ && !$this->image->get($j, $i)) { 83 | $j++; 84 | } 85 | $currentState = 0; 86 | while ($j < $maxJ) { 87 | if ($this->image->get($j, $i)) { 88 | // Black pixel 89 | if ($currentState == 1) { // Counting black pixels 90 | $stateCount[$currentState]++; 91 | } else { // Counting white pixels 92 | if ($currentState == 2) { // A winner? 93 | if ($this->foundPatternCross($stateCount)) { // Yes 94 | $confirmed = $this->handlePossibleCenter($stateCount, $i, $j); 95 | if ($confirmed != null) { 96 | return $confirmed; 97 | } 98 | } 99 | $stateCount[0] = $stateCount[2]; 100 | $stateCount[1] = 1; 101 | $stateCount[2] = 0; 102 | $currentState = 1; 103 | } else { 104 | $stateCount[++$currentState]++; 105 | } 106 | } 107 | } else { // White pixel 108 | if ($currentState == 1) { // Counting black pixels 109 | $currentState++; 110 | } 111 | $stateCount[$currentState]++; 112 | } 113 | $j++; 114 | } 115 | if ($this->foundPatternCross($stateCount)) { 116 | $confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ); 117 | if ($confirmed != null) { 118 | return $confirmed; 119 | } 120 | } 121 | } 122 | 123 | // Hmm, nothing we saw was observed and confirmed twice. If we had 124 | // any guess at all, return it. 125 | if (count($this->possibleCenters)) { 126 | return $this->possibleCenters[0]; 127 | } 128 | 129 | throw new NotFoundException("Bottom right alignment pattern not found"); 130 | } 131 | 132 | /** 133 | * @param int $stateCount count of black/white/black pixels just read 134 | * 135 | * @return bool iff the proportions of the counts is close enough to the 1/1/1 ratios used by alignment patterns to be considered a match 136 | */ 137 | private function foundPatternCross($stateCount): bool 138 | { 139 | $moduleSize = $this->moduleSize; 140 | $maxVariance = $moduleSize / 2.0; 141 | for ($i = 0; $i < 3; $i++) { 142 | if (abs($moduleSize - $stateCount[$i]) >= $maxVariance) { 143 | return false; 144 | } 145 | } 146 | 147 | return true; 148 | } 149 | 150 | /** 151 | *

This is called when a horizontal scan finds a possible alignment pattern. It will 152 | * cross check with a vertical scan, and if successful, will see if this pattern had been 153 | * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have 154 | * found the alignment pattern.

155 | * 156 | * @param array $stateCount state module counts from horizontal scan 157 | * @param int $i where alignment pattern may be found 158 | * @param int $j number of possible alignment pattern in row 159 | * 160 | * @return {@link AlignmentPattern} if we have found the same pattern twice, or null if not 161 | */ 162 | private function handlePossibleCenter($stateCount, $i, $j) 163 | { 164 | $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2]; 165 | $centerJ = self::centerFromEnd($stateCount, $j); 166 | $centerI = $this->crossCheckVertical($i, (int)$centerJ, 2 * $stateCount[1], $stateCountTotal); 167 | if (!is_nan($centerI)) { 168 | $estimatedModuleSize = (float)($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0; 169 | foreach ($this->possibleCenters as $center) { 170 | // Look for about the same center and module size: 171 | if ($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)) { 172 | return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize); 173 | } 174 | } 175 | // Hadn't found this before; save it 176 | $point = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize); 177 | $this->possibleCenters[] = $point; 178 | if ($this->resultPointCallback != null) { 179 | $this->resultPointCallback->foundPossibleResultPoint($point); 180 | } 181 | } 182 | 183 | return null; 184 | } 185 | 186 | /** 187 | * Given a count of black/white/black pixels just seen and an end position, 188 | * figures the location of the center of this black/white/black run. 189 | */ 190 | private static function centerFromEnd(array $stateCount, int $end) 191 | { 192 | return (float)($end - $stateCount[2]) - $stateCount[1] / 2.0; 193 | } 194 | 195 | /** 196 | *

After a horizontal scan finds a potential alignment pattern, this method 197 | * "cross-checks" by scanning down vertically through the center of the possible 198 | * alignment pattern to see if the same proportion is detected.

199 | * 200 | * @param int $startI row where an alignment pattern was detected 201 | * @param float $centerJ center of the section that appears to cross an alignment pattern 202 | * @param int $maxCount maximum reasonable number of modules that should be 203 | * observed in any reading state, based on the results of the horizontal scan 204 | * 205 | * @return float vertical center of alignment pattern, or {@link Float#NaN} if not found 206 | */ 207 | private function crossCheckVertical( 208 | int $startI, 209 | int $centerJ, 210 | $maxCount, 211 | $originalStateCountTotal 212 | ) { 213 | $image = $this->image; 214 | 215 | $maxI = $image->getHeight(); 216 | $stateCount = $this->crossCheckStateCount; 217 | $stateCount[0] = 0; 218 | $stateCount[1] = 0; 219 | $stateCount[2] = 0; 220 | 221 | // Start counting up from center 222 | $i = $startI; 223 | while ($i >= 0 && $image->get($centerJ, $i) && $stateCount[1] <= $maxCount) { 224 | $stateCount[1]++; 225 | $i--; 226 | } 227 | // If already too many modules in this state or ran off the edge: 228 | if ($i < 0 || $stateCount[1] > $maxCount) { 229 | return NAN; 230 | } 231 | while ($i >= 0 && !$image->get($centerJ, $i) && $stateCount[0] <= $maxCount) { 232 | $stateCount[0]++; 233 | $i--; 234 | } 235 | if ($stateCount[0] > $maxCount) { 236 | return NAN; 237 | } 238 | 239 | // Now also count down from center 240 | $i = $startI + 1; 241 | while ($i < $maxI && $image->get($centerJ, $i) && $stateCount[1] <= $maxCount) { 242 | $stateCount[1]++; 243 | $i++; 244 | } 245 | if ($i == $maxI || $stateCount[1] > $maxCount) { 246 | return NAN; 247 | } 248 | while ($i < $maxI && !$image->get($centerJ, $i) && $stateCount[2] <= $maxCount) { 249 | $stateCount[2]++; 250 | $i++; 251 | } 252 | if ($stateCount[2] > $maxCount) { 253 | return NAN; 254 | } 255 | 256 | $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2]; 257 | if (5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal) { 258 | return NAN; 259 | } 260 | 261 | return $this->foundPatternCross($stateCount) ? self::centerFromEnd($stateCount, $i) : NAN; 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /lib/Qrcode/Detector/FinderPattern.php: -------------------------------------------------------------------------------- 1 | Encapsulates a finder pattern, which are the three square patterns found in 24 | * the corners of QR Codes. It also encapsulates a count of similar finder patterns, 25 | * as a convenience to the finder's bookkeeping.

26 | * 27 | * @author Sean Owen 28 | */ 29 | final class FinderPattern extends ResultPoint 30 | { 31 | public function __construct($posX, $posY, private $estimatedModuleSize, private $count = 1) 32 | { 33 | parent::__construct($posX, $posY); 34 | } 35 | 36 | public function getEstimatedModuleSize() 37 | { 38 | return $this->estimatedModuleSize; 39 | } 40 | 41 | public function getCount() 42 | { 43 | return $this->count; 44 | } 45 | 46 | /* 47 | void incrementCount() { 48 | this.count++; 49 | } 50 | */ 51 | 52 | /** 53 | *

Determines if this finder pattern "about equals" a finder pattern at the stated 54 | * position and size -- meaning, it is at nearly the same center with nearly the same size.

55 | */ 56 | public function aboutEquals($moduleSize, $i, $j): bool 57 | { 58 | if (abs($i - $this->getY()) <= $moduleSize && abs($j - $this->getX()) <= $moduleSize) { 59 | $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize); 60 | 61 | return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | /** 68 | * Combines this object's current estimate of a finder pattern position and module size 69 | * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average 70 | * based on count. 71 | */ 72 | public function combineEstimate($i, $j, $newModuleSize): \Zxing\Qrcode\Detector\FinderPattern 73 | { 74 | $combinedCount = $this->count + 1; 75 | $combinedX = ($this->count * $this->getX() + $j) / $combinedCount; 76 | $combinedY = ($this->count * $this->getY() + $i) / $combinedCount; 77 | $combinedModuleSize = ($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount; 78 | 79 | return new FinderPattern($combinedX, $combinedY, $combinedModuleSize, $combinedCount); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/Qrcode/Detector/FinderPatternInfo.php: -------------------------------------------------------------------------------- 1 | Encapsulates information about finder patterns in an image, including the location of 22 | * the three finder patterns, and their estimated module size.

23 | * 24 | * @author Sean Owen 25 | */ 26 | final class FinderPatternInfo 27 | { 28 | private $bottomLeft; 29 | private $topLeft; 30 | private $topRight; 31 | 32 | public function __construct($patternCenters) 33 | { 34 | $this->bottomLeft = $patternCenters[0]; 35 | $this->topLeft = $patternCenters[1]; 36 | $this->topRight = $patternCenters[2]; 37 | } 38 | 39 | public function getBottomLeft() 40 | { 41 | return $this->bottomLeft; 42 | } 43 | 44 | public function getTopLeft() 45 | { 46 | return $this->topLeft; 47 | } 48 | 49 | public function getTopRight() 50 | { 51 | return $this->topRight; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/Qrcode/QRCodeReader.php: -------------------------------------------------------------------------------- 1 | decoder = new Decoder(); 43 | } 44 | 45 | /** 46 | * @param null $hints 47 | * 48 | * @return Result 49 | * @throws \Zxing\FormatException 50 | * @throws \Zxing\NotFoundException 51 | */ 52 | public function decode(BinaryBitmap $image, $hints = null) 53 | { 54 | $decoderResult = null; 55 | if ($hints !== null && array_key_exists('PURE_BARCODE', $hints) && $hints['PURE_BARCODE']) { 56 | $bits = self::extractPureBits($image->getBlackMatrix()); 57 | $decoderResult = $this->decoder->decode($bits, $hints); 58 | $points = self::$NO_POINTS; 59 | } else { 60 | $detector = new Detector($image->getBlackMatrix()); 61 | $detectorResult = $detector->detect($hints); 62 | 63 | $decoderResult = $this->decoder->decode($detectorResult->getBits(), $hints); 64 | $points = $detectorResult->getPoints(); 65 | } 66 | $result = new Result($decoderResult->getText(), $decoderResult->getRawBytes(), $points, 'QR_CODE');//BarcodeFormat.QR_CODE 67 | 68 | $byteSegments = $decoderResult->getByteSegments(); 69 | if ($byteSegments !== null) { 70 | $result->putMetadata('BYTE_SEGMENTS', $byteSegments);//ResultMetadataType.BYTE_SEGMENTS 71 | } 72 | $ecLevel = $decoderResult->getECLevel(); 73 | if ($ecLevel !== null) { 74 | $result->putMetadata('ERROR_CORRECTION_LEVEL', $ecLevel);//ResultMetadataType.ERROR_CORRECTION_LEVEL 75 | } 76 | if ($decoderResult->hasStructuredAppend()) { 77 | $result->putMetadata( 78 | 'STRUCTURED_APPEND_SEQUENCE',//ResultMetadataType.STRUCTURED_APPEND_SEQUENCE 79 | $decoderResult->getStructuredAppendSequenceNumber() 80 | ); 81 | $result->putMetadata( 82 | 'STRUCTURED_APPEND_PARITY',//ResultMetadataType.STRUCTURED_APPEND_PARITY 83 | $decoderResult->getStructuredAppendParity() 84 | ); 85 | } 86 | 87 | return $result; 88 | } 89 | 90 | /** 91 | * Locates and decodes a QR code in an image. 92 | * This method detects a code in a "pure" image -- that is, pure monochrome image 93 | * which contains only an unrotated, unskewed, image of a code, with some white border 94 | * around it. This is a specialized method that works exceptionally fast in this special 95 | * case. 96 | * 97 | * @return BitMatrix a String representing the content encoded by the QR code 98 | * @throws NotFoundException if a QR code cannot be found 99 | * @throws FormatException if a QR code cannot be decoded 100 | * @throws ChecksumException if error correction fails 101 | * 102 | * @see com.google.zxing.datamatrix.DataMatrixReader#extractPureBits(BitMatrix) 103 | */ 104 | private static function extractPureBits(BitMatrix $image): BitMatrix 105 | { 106 | $leftTopBlack = $image->getTopLeftOnBit(); 107 | $rightBottomBlack = $image->getBottomRightOnBit(); 108 | if ($leftTopBlack === null || $rightBottomBlack == null) { 109 | throw new NotFoundException("Top left or bottom right on bit not found"); 110 | } 111 | 112 | $moduleSize = self::moduleSize($leftTopBlack, $image); 113 | 114 | $top = $leftTopBlack[1]; 115 | $bottom = $rightBottomBlack[1]; 116 | $left = $leftTopBlack[0]; 117 | $right = $rightBottomBlack[0]; 118 | 119 | // Sanity check! 120 | if ($left >= $right || $top >= $bottom) { 121 | throw new NotFoundException("Left vs. right ($left >= $right) or top vs. bottom ($top >= $bottom) sanity violated."); 122 | } 123 | 124 | if ($bottom - $top != $right - $left) { 125 | // Special case, where bottom-right module wasn't black so we found something else in the last row 126 | // Assume it's a square, so use height as the width 127 | $right = $left + ($bottom - $top); 128 | } 129 | 130 | $matrixWidth = round(($right - $left + 1) / $moduleSize); 131 | $matrixHeight = round(($bottom - $top + 1) / $moduleSize); 132 | if ($matrixWidth <= 0 || $matrixHeight <= 0) { 133 | throw new NotFoundException("Matrix dimensions <= 0 ($matrixWidth, $matrixHeight)"); 134 | } 135 | if ($matrixHeight != $matrixWidth) { 136 | // Only possibly decode square regions 137 | throw new NotFoundException("Matrix height $matrixHeight != matrix width $matrixWidth"); 138 | } 139 | 140 | // Push in the "border" by half the module width so that we start 141 | // sampling in the middle of the module. Just in case the image is a 142 | // little off, this will help recover. 143 | $nudge = (int)($moduleSize / 2.0);// $nudge = (int) ($moduleSize / 2.0f); 144 | $top += $nudge; 145 | $left += $nudge; 146 | 147 | // But careful that this does not sample off the edge 148 | // "right" is the farthest-right valid pixel location -- right+1 is not necessarily 149 | // This is positive by how much the inner x loop below would be too large 150 | $nudgedTooFarRight = $left + (int)(($matrixWidth - 1) * $moduleSize) - $right; 151 | if ($nudgedTooFarRight > 0) { 152 | if ($nudgedTooFarRight > $nudge) { 153 | // Neither way fits; abort 154 | throw new NotFoundException("Nudge too far right ($nudgedTooFarRight > $nudge), no fit found"); 155 | } 156 | $left -= $nudgedTooFarRight; 157 | } 158 | // See logic above 159 | $nudgedTooFarDown = $top + (int)(($matrixHeight - 1) * $moduleSize) - $bottom; 160 | if ($nudgedTooFarDown > 0) { 161 | if ($nudgedTooFarDown > $nudge) { 162 | // Neither way fits; abort 163 | throw new NotFoundException("Nudge too far down ($nudgedTooFarDown > $nudge), no fit found"); 164 | } 165 | $top -= $nudgedTooFarDown; 166 | } 167 | 168 | // Now just read off the bits 169 | $bits = new BitMatrix($matrixWidth, $matrixHeight); 170 | for ($y = 0; $y < $matrixHeight; $y++) { 171 | $iOffset = (int)($top + (int)($y * $moduleSize)); 172 | for ($x = 0; $x < $matrixWidth; $x++) { 173 | if ($image->get((int)($left + (int)($x * $moduleSize)), $iOffset)) { 174 | $bits->set($x, $y); 175 | } 176 | } 177 | } 178 | 179 | return $bits; 180 | } 181 | 182 | /** 183 | * @psalm-param array{0: mixed, 1: mixed} $leftTopBlack 184 | */ 185 | private static function moduleSize(array $leftTopBlack, BitMatrix $image) 186 | { 187 | $height = $image->getHeight(); 188 | $width = $image->getWidth(); 189 | $x = $leftTopBlack[0]; 190 | $y = $leftTopBlack[1]; 191 | /*$x = $leftTopBlack[0]; 192 | $y = $leftTopBlack[1];*/ 193 | $inBlack = true; 194 | $transitions = 0; 195 | while ($x < $width && $y < $height) { 196 | if ($inBlack != $image->get((int)round($x), (int)round($y))) { 197 | if (++$transitions == 5) { 198 | break; 199 | } 200 | $inBlack = !$inBlack; 201 | } 202 | $x++; 203 | $y++; 204 | } 205 | if ($x == $width || $y == $height) { 206 | throw new NotFoundException("$x == $width || $y == $height"); 207 | } 208 | 209 | return ($x - $leftTopBlack[0]) / 7.0; //return ($x - $leftTopBlack[0]) / 7.0f; 210 | } 211 | 212 | public function reset(): void 213 | { 214 | // do nothing 215 | } 216 | 217 | final protected function getDecoder(): Decoder 218 | { 219 | return $this->decoder; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /lib/RGBLuminanceSource.php: -------------------------------------------------------------------------------- 1 | RGBLuminanceSource_($pixels, $dataWidth, $dataHeight); 57 | 58 | return; 59 | } 60 | parent::__construct($width, $height); 61 | if ($left + $width > $dataWidth || $top + $height > $dataHeight) { 62 | throw new \InvalidArgumentException("Crop rectangle does not fit within image data."); 63 | } 64 | $this->luminances = $pixels; 65 | $this->dataWidth = $dataWidth; 66 | $this->dataHeight = $dataHeight; 67 | $this->left = $left; 68 | $this->top = $top; 69 | } 70 | 71 | public function rotateCounterClockwise(): void 72 | { 73 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise"); 74 | } 75 | 76 | public function rotateCounterClockwise45(): void 77 | { 78 | throw new \RuntimeException("This LuminanceSource does not support rotateCounterClockwise45"); 79 | } 80 | 81 | public function RGBLuminanceSource_($width, $height, $pixels): void 82 | { 83 | parent::__construct($width, $height); 84 | 85 | $this->dataWidth = $width; 86 | $this->dataHeight = $height; 87 | $this->left = 0; 88 | $this->top = 0; 89 | $this->pixels = $pixels; 90 | 91 | 92 | // In order to measure pure decoding speed, we convert the entire image to a greyscale array 93 | // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. 94 | $this->luminances = []; 95 | //$this->luminances = $this->grayScaleToBitmap($this->grayscale()); 96 | 97 | foreach ($pixels as $key => $pixel) { 98 | $r = $pixel['red']; 99 | $g = $pixel['green']; 100 | $b = $pixel['blue']; 101 | 102 | /* if (($pixel & 0xFF000000) == 0) { 103 | $pixel = 0xFFFFFFFF; // = white 104 | } 105 | 106 | // .229R + 0.587G + 0.114B (YUV/YIQ for PAL and NTSC) 107 | 108 | $this->luminances[$key] = 109 | (306 * (($pixel >> 16) & 0xFF) + 110 | 601 * (($pixel >> 8) & 0xFF) + 111 | 117 * ($pixel & 0xFF) + 112 | 0x200) >> 10; 113 | 114 | */ 115 | //$r = ($pixel >> 16) & 0xff; 116 | //$g = ($pixel >> 8) & 0xff; 117 | //$b = $pixel & 0xff; 118 | if ($r == $g && $g == $b) { 119 | // Image is already greyscale, so pick any channel. 120 | 121 | $this->luminances[$key] = $r;//(($r + 128) % 256) - 128; 122 | } else { 123 | // Calculate luminance cheaply, favoring green. 124 | $this->luminances[$key] = ($r + 2 * $g + $b) / 4;//(((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; 125 | } 126 | } 127 | 128 | /* 129 | 130 | for ($y = 0; $y < $height; $y++) { 131 | $offset = $y * $width; 132 | for ($x = 0; $x < $width; $x++) { 133 | $pixel = $pixels[$offset + $x]; 134 | $r = ($pixel >> 16) & 0xff; 135 | $g = ($pixel >> 8) & 0xff; 136 | $b = $pixel & 0xff; 137 | if ($r == $g && $g == $b) { 138 | // Image is already greyscale, so pick any channel. 139 | 140 | $this->luminances[(int)($offset + $x)] = (($r+128) % 256) - 128; 141 | } else { 142 | // Calculate luminance cheaply, favoring green. 143 | $this->luminances[(int)($offset + $x)] = (((($r + 2 * $g + $b) / 4)+128)%256) - 128; 144 | } 145 | 146 | 147 | 148 | } 149 | */ 150 | //} 151 | // $this->luminances = $this->grayScaleToBitmap($this->luminances); 152 | } 153 | 154 | /** 155 | * @return (int|mixed)[] 156 | * 157 | * @psalm-return array 158 | */ 159 | public function grayscale(): array 160 | { 161 | $width = $this->dataWidth; 162 | $height = $this->dataHeight; 163 | 164 | $ret = fill_array(0, $width * $height, 0); 165 | for ($y = 0; $y < $height; $y++) { 166 | for ($x = 0; $x < $width; $x++) { 167 | $gray = $this->getPixel($x, $y, $width, $height); 168 | 169 | $ret[$x + $y * $width] = $gray; 170 | } 171 | } 172 | 173 | return $ret; 174 | } 175 | 176 | public function getPixel(int $x, int $y, $width, $height): int 177 | { 178 | $image = $this->pixels; 179 | if ($width < $x) { 180 | die('error'); 181 | } 182 | if ($height < $y) { 183 | die('error'); 184 | } 185 | $point = ($x) + ($y * $width); 186 | 187 | $r = $image[$point]['red'];//($image[$point] >> 16) & 0xff; 188 | $g = $image[$point]['green'];//($image[$point] >> 8) & 0xff; 189 | $b = $image[$point]['blue'];//$image[$point] & 0xff; 190 | 191 | $p = (int)(($r * 33 + $g * 34 + $b * 33) / 100); 192 | 193 | 194 | return $p; 195 | } 196 | 197 | /** 198 | * @return (int|mixed)[] 199 | * 200 | * @psalm-return array 201 | */ 202 | public function grayScaleToBitmap($grayScale): array 203 | { 204 | $middle = $this->getMiddleBrightnessPerArea($grayScale); 205 | $sqrtNumArea = is_countable($middle) ? count($middle) : 0; 206 | $areaWidth = floor($this->dataWidth / $sqrtNumArea); 207 | $areaHeight = floor($this->dataHeight / $sqrtNumArea); 208 | $bitmap = fill_array(0, $this->dataWidth * $this->dataHeight, 0); 209 | 210 | for ($ay = 0; $ay < $sqrtNumArea; $ay++) { 211 | for ($ax = 0; $ax < $sqrtNumArea; $ax++) { 212 | for ($dy = 0; $dy < $areaHeight; $dy++) { 213 | for ($dx = 0; $dx < $areaWidth; $dx++) { 214 | $bitmap[(int)($areaWidth * $ax + $dx + ($areaHeight * $ay + $dy) * $this->dataWidth)] = ($grayScale[(int)($areaWidth * $ax + $dx + ($areaHeight * $ay + $dy) * $this->dataWidth)] < $middle[$ax][$ay]) ? 0 : 255; 215 | } 216 | } 217 | } 218 | } 219 | 220 | return $bitmap; 221 | } 222 | 223 | /** 224 | * @return float[]&mixed[][] 225 | */ 226 | public function getMiddleBrightnessPerArea($image): array 227 | { 228 | $numSqrtArea = 4; 229 | //obtain middle brightness((min + max) / 2) per area 230 | $areaWidth = floor($this->dataWidth / $numSqrtArea); 231 | $areaHeight = floor($this->dataHeight / $numSqrtArea); 232 | $minmax = fill_array(0, $numSqrtArea, 0); 233 | for ($i = 0; $i < $numSqrtArea; $i++) { 234 | $minmax[$i] = fill_array(0, $numSqrtArea, 0); 235 | for ($i2 = 0; $i2 < $numSqrtArea; $i2++) { 236 | $minmax[$i][$i2] = [0, 0]; 237 | } 238 | } 239 | for ($ay = 0; $ay < $numSqrtArea; $ay++) { 240 | for ($ax = 0; $ax < $numSqrtArea; $ax++) { 241 | $minmax[$ax][$ay][0] = 0xFF; 242 | for ($dy = 0; $dy < $areaHeight; $dy++) { 243 | for ($dx = 0; $dx < $areaWidth; $dx++) { 244 | $target = $image[(int)($areaWidth * $ax + $dx + ($areaHeight * $ay + $dy) * $this->dataWidth)]; 245 | if ($target < $minmax[$ax][$ay][0]) { 246 | $minmax[$ax][$ay][0] = $target; 247 | } 248 | if ($target > $minmax[$ax][$ay][1]) { 249 | $minmax[$ax][$ay][1] = $target; 250 | } 251 | } 252 | } 253 | //minmax[ax][ay][0] = (minmax[ax][ay][0] + minmax[ax][ay][1]) / 2; 254 | } 255 | } 256 | $middle = []; 257 | for ($i3 = 0; $i3 < $numSqrtArea; $i3++) { 258 | $middle[$i3] = []; 259 | } 260 | for ($ay = 0; $ay < $numSqrtArea; $ay++) { 261 | for ($ax = 0; $ax < $numSqrtArea; $ax++) { 262 | $middle[$ax][$ay] = floor(($minmax[$ax][$ay][0] + $minmax[$ax][$ay][1]) / 2); 263 | //Console.out.print(middle[ax][ay] + ","); 264 | } 265 | //Console.out.println(""); 266 | } 267 | 268 | //Console.out.println(""); 269 | 270 | return $middle; 271 | } 272 | 273 | 274 | public function getRow($y, $row = null) 275 | { 276 | if ($y < 0 || $y >= $this->getHeight()) { 277 | throw new \InvalidArgumentException("Requested row is outside the image: " + $y); 278 | } 279 | $width = $this->getWidth(); 280 | if ($row == null || (is_countable($row) ? count($row) : 0) < $width) { 281 | $row = []; 282 | } 283 | $offset = ($y + $this->top) * $this->dataWidth + $this->left; 284 | $row = arraycopy($this->luminances, $offset, $row, 0, $width); 285 | 286 | return $row; 287 | } 288 | 289 | 290 | public function getMatrix() 291 | { 292 | $width = $this->getWidth(); 293 | $height = $this->getHeight(); 294 | 295 | // If the caller asks for the entire underlying image, save the copy and give them the 296 | // original data. The docs specifically warn that result.length must be ignored. 297 | if ($width == $this->dataWidth && $height == $this->dataHeight) { 298 | return $this->luminances; 299 | } 300 | 301 | $area = $width * $height; 302 | $matrix = []; 303 | $inputOffset = $this->top * $this->dataWidth + $this->left; 304 | 305 | // If the width matches the full width of the underlying data, perform a single copy. 306 | if ($width == $this->dataWidth) { 307 | $matrix = arraycopy($this->luminances, $inputOffset, $matrix, 0, $area); 308 | 309 | return $matrix; 310 | } 311 | 312 | // Otherwise copy one cropped row at a time. 313 | $rgb = $this->luminances; 314 | for ($y = 0; $y < $height; $y++) { 315 | $outputOffset = $y * $width; 316 | $matrix = arraycopy($rgb, $inputOffset, $matrix, $outputOffset, $width); 317 | $inputOffset += $this->dataWidth; 318 | } 319 | 320 | return $matrix; 321 | } 322 | 323 | 324 | public function isCropSupported(): bool 325 | { 326 | return true; 327 | } 328 | 329 | 330 | public function crop($left, $top, $width, $height): \Zxing\RGBLuminanceSource 331 | { 332 | return new RGBLuminanceSource( 333 | $this->luminances, 334 | $this->dataWidth, 335 | $this->dataHeight, 336 | $this->left + $left, 337 | $this->top + $top, 338 | $width, 339 | $height 340 | ); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /lib/Reader.php: -------------------------------------------------------------------------------- 1 | Encapsulates the result of decoding a barcode within an image.

22 | * 23 | * @author Sean Owen 24 | */ 25 | final class Result 26 | { 27 | /** 28 | * @var mixed[]|mixed 29 | */ 30 | private $resultMetadata = null; 31 | private $timestamp; 32 | 33 | public function __construct( 34 | private $text, 35 | private $rawBytes, 36 | private $resultPoints, 37 | private $format, 38 | $timestamp = '' 39 | ) { 40 | $this->timestamp = $timestamp ?: time(); 41 | } 42 | 43 | /** 44 | * @return string raw text encoded by the barcode 45 | */ 46 | public function getText() 47 | { 48 | return $this->text; 49 | } 50 | 51 | /** 52 | * @return array|string raw bytes encoded by the barcode, if applicable, otherwise {@code null} 53 | */ 54 | public function getRawBytes() 55 | { 56 | return $this->rawBytes; 57 | } 58 | 59 | /** 60 | * @return array points related to the barcode in the image. These are typically points 61 | * identifying finder patterns or the corners of the barcode. The exact meaning is 62 | * specific to the type of barcode that was decoded. 63 | */ 64 | public function getResultPoints() 65 | { 66 | return $this->resultPoints; 67 | } 68 | 69 | /** 70 | * @return {@link BarcodeFormat} representing the format of the barcode that was decoded 71 | */ 72 | public function getBarcodeFormat() 73 | { 74 | return $this->format; 75 | } 76 | 77 | /** 78 | * @return {@link Map} mapping {@link ResultMetadataType} keys to values. May be 79 | * {@code null}. This contains optional metadata about what was detected about the barcode, 80 | * like orientation. 81 | */ 82 | public function getResultMetadata() 83 | { 84 | return $this->resultMetadata; 85 | } 86 | 87 | public function putMetadata(string $type, $value): void 88 | { 89 | $resultMetadata = []; 90 | if ($this->resultMetadata === null) { 91 | $this->resultMetadata = []; 92 | } 93 | $resultMetadata[$type] = $value; 94 | } 95 | 96 | public function putAllMetadata($metadata): void 97 | { 98 | if ($metadata !== null) { 99 | if ($this->resultMetadata === null) { 100 | $this->resultMetadata = $metadata; 101 | } else { 102 | $this->resultMetadata = array_merge($this->resultMetadata, $metadata); 103 | } 104 | } 105 | } 106 | 107 | public function addResultPoints($newPoints): void 108 | { 109 | $oldPoints = $this->resultPoints; 110 | if ($oldPoints === null) { 111 | $this->resultPoints = $newPoints; 112 | } elseif ($newPoints !== null && (is_countable($newPoints) ? count($newPoints) : 0) > 0) { 113 | $allPoints = fill_array(0, (is_countable($oldPoints) ? count($oldPoints) : 0) + (is_countable($newPoints) ? count($newPoints) : 0), 0); 114 | $allPoints = arraycopy($oldPoints, 0, $allPoints, 0, is_countable($oldPoints) ? count($oldPoints) : 0); 115 | $allPoints = arraycopy($newPoints, 0, $allPoints, is_countable($oldPoints) ? count($oldPoints) : 0, is_countable($newPoints) ? count($newPoints) : 0); 116 | $this->resultPoints = $allPoints; 117 | } 118 | } 119 | 120 | public function getTimestamp() 121 | { 122 | return $this->timestamp; 123 | } 124 | 125 | public function toString() 126 | { 127 | return $this->text; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/ResultPoint.php: -------------------------------------------------------------------------------- 1 | Encapsulates a point of interest in an image containing a barcode. Typically, this 24 | * would be the location of a finder pattern or the corner of the barcode, for example.

25 | * 26 | * @author Sean Owen 27 | */ 28 | class ResultPoint 29 | { 30 | private float $x; 31 | private float $y; 32 | 33 | public function __construct($x, $y) 34 | { 35 | $this->x = (float)($x); 36 | $this->y = (float)($y); 37 | } 38 | 39 | /** 40 | * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC 41 | * and BC is less than AC, and the angle between BC and BA is less than 180 degrees. 42 | * 43 | * @param array $patterns of three {@code ResultPoint} to order 44 | */ 45 | public static function orderBestPatterns(array $patterns): array 46 | { 47 | 48 | // Find distances between pattern centers 49 | $zeroOneDistance = self::distance($patterns[0], $patterns[1]); 50 | $oneTwoDistance = self::distance($patterns[1], $patterns[2]); 51 | $zeroTwoDistance = self::distance($patterns[0], $patterns[2]); 52 | 53 | $pointA = ''; 54 | $pointB = ''; 55 | $pointC = ''; 56 | // Assume one closest to other two is B; A and C will just be guesses at first 57 | if ($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance) { 58 | $pointB = $patterns[0]; 59 | $pointA = $patterns[1]; 60 | $pointC = $patterns[2]; 61 | } elseif ($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance) { 62 | $pointB = $patterns[1]; 63 | $pointA = $patterns[0]; 64 | $pointC = $patterns[2]; 65 | } else { 66 | $pointB = $patterns[2]; 67 | $pointA = $patterns[0]; 68 | $pointC = $patterns[1]; 69 | } 70 | 71 | // Use cross product to figure out whether A and C are correct or flipped. 72 | // This asks whether BC x BA has a positive z component, which is the arrangement 73 | // we want for A, B, C. If it's negative, then we've got it flipped around and 74 | // should swap A and C. 75 | if (self::crossProductZ($pointA, $pointB, $pointC) < 0.0) { 76 | $temp = $pointA; 77 | $pointA = $pointC; 78 | $pointC = $temp; 79 | } 80 | 81 | $patterns[0] = $pointA; 82 | $patterns[1] = $pointB; 83 | $patterns[2] = $pointC; 84 | 85 | return $patterns; 86 | } 87 | 88 | /** 89 | * @param ResultPoint $pattern1 first pattern 90 | * @param ResultPoint $pattern2 second pattern 91 | * 92 | * @return float distance between two points 93 | */ 94 | public static function distance($pattern1, $pattern2) 95 | { 96 | return MathUtils::distance($pattern1->x, $pattern1->y, $pattern2->x, $pattern2->y); 97 | } 98 | 99 | 100 | 101 | /** 102 | * Returns the z component of the cross product between vectors BC and BA. 103 | */ 104 | private static function crossProductZ( 105 | $pointA, 106 | $pointB, 107 | $pointC 108 | ) { 109 | $bX = $pointB->x; 110 | $bY = $pointB->y; 111 | 112 | return (($pointC->x - $bX) * ($pointA->y - $bY)) - (($pointC->y - $bY) * ($pointA->x - $bX)); 113 | } 114 | 115 | 116 | 117 | final public function getX(): float 118 | { 119 | return (float)($this->x); 120 | } 121 | 122 | 123 | 124 | final public function getY(): float 125 | { 126 | return (float)($this->y); 127 | } 128 | 129 | final public function equals($other): bool 130 | { 131 | if ($other instanceof ResultPoint) { 132 | $otherPoint = $other; 133 | 134 | return $this->x == $otherPoint->x && $this->y == $otherPoint->y; 135 | } 136 | 137 | return false; 138 | } 139 | 140 | final public function hashCode() 141 | { 142 | return 31 * floatToIntBits($this->x) + floatToIntBits($this->y); 143 | } 144 | 145 | final public function toString(): string 146 | { 147 | $result = ''; 148 | $result .= ('('); 149 | $result .= ($this->x); 150 | $result .= (','); 151 | $result .= ($this->y); 152 | $result .= (')'); 153 | 154 | return $result; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | lib 6 | 7 | 8 | 9 | tests 10 | 11 | 12 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 23 | __DIR__ . '/lib' 24 | ]); 25 | 26 | $rectorConfig->symfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml'); 27 | 28 | $rectorConfig->sets([ 29 | DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, 30 | SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, 31 | SymfonySetList::SYMFONY_60, 32 | LevelSetList::UP_TO_PHP_81 33 | ]); 34 | 35 | // register a single rule 36 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 37 | $rectorConfig->rule(AddReturnTypeDeclarationRector::class); 38 | $rectorConfig->rules([ 39 | AddMethodCallBasedStrictParamTypeRector::class, 40 | AddVoidReturnTypeWhereNoReturnRector::class, 41 | ParamTypeByMethodCallTypeRector::class, 42 | ParamTypeByParentCallTypeRector::class, 43 | RemoveUselessParamTagRector::class, 44 | ReturnTypeFromReturnNewRector::class, 45 | ReturnTypeFromStrictBoolReturnExprRector::class, 46 | ReturnTypeFromStrictNewArrayRector::class, 47 | TypedPropertyFromAssignsRector::class, 48 | ]); 49 | 50 | // define sets of rules 51 | // $rectorConfig->sets([ 52 | // LevelSetList::UP_TO_PHP_80 53 | // ]); 54 | }; 55 | -------------------------------------------------------------------------------- /tests/QrReaderTest.php: -------------------------------------------------------------------------------- 1 | assertSame("Hello world!", $qrcode->text()); 23 | } 24 | 25 | public function testNoText() 26 | { 27 | $image = __DIR__ . "/qrcodes/empty.png"; 28 | $qrcode = new QrReader($image); 29 | $this->assertSame(false, $qrcode->text()); 30 | } 31 | 32 | public function testText2() 33 | { 34 | $image = __DIR__ . "/qrcodes/139225861-398ccbbd-2bfd-4736-889b-878c10573888.png"; 35 | $qrcode = new QrReader($image); 36 | $hints = [ 37 | 'TRY_HARDER' => true, 38 | 'NR_ALLOW_SKIP_ROWS' => 0 39 | ]; 40 | $qrcode->decode($hints); 41 | $this->assertSame(null, $qrcode->getError()); 42 | $this->assertInstanceOf(Result::class, $qrcode->getResult()); 43 | $this->assertEquals("https://www.gosuslugi.ru/covid-cert/verify/9770000014233333?lang=ru&ck=733a9d218d312fe134f1c2cc06e1a800", $qrcode->getResult()->getText()); 44 | $this->assertSame("https://www.gosuslugi.ru/covid-cert/verify/9770000014233333?lang=ru&ck=733a9d218d312fe134f1c2cc06e1a800", $qrcode->text($hints)); 45 | } 46 | 47 | public function testText3() 48 | { 49 | $image = __DIR__ . "/qrcodes/test.png"; 50 | $qrcode = new QrReader($image); 51 | $qrcode->decode([ 52 | 'TRY_HARDER' => true 53 | ]); 54 | $this->assertSame(null, $qrcode->getError()); 55 | $this->assertSame("https://www.gosuslugi.ru/covid-cert/verify/9770000014233333?lang=ru&ck=733a9d218d312fe134f1c2cc06e1a800", $qrcode->text()); 56 | } 57 | 58 | // TODO: fix this test 59 | // public function testText4() 60 | // { 61 | // $image = __DIR__ . "/qrcodes/174419877-f6b5dae1-2251-4b67-95f1-5e1143e40fae.jpg"; 62 | // $qrcode = new QrReader($image); 63 | // $qrcode->decode([ 64 | // 'TRY_HARDER' => true, 65 | // 'NR_ALLOW_SKIP_ROWS' => 0, 66 | // // 'ALLOWED_DEVIATION' => 0.1, 67 | // // 'MAX_VARIANCE' => 0.7 68 | // ]); 69 | // $this->assertSame(null, $qrcode->getError()); 70 | // $this->assertSame("some text", $qrcode->text()); 71 | // } 72 | } 73 | -------------------------------------------------------------------------------- /tests/qrcodes/139225861-398ccbbd-2bfd-4736-889b-878c10573888.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanamiryan/php-qrcode-detector-decoder/7e0cbfa8490c715b1acbadc30bb89b4ca534e033/tests/qrcodes/139225861-398ccbbd-2bfd-4736-889b-878c10573888.png -------------------------------------------------------------------------------- /tests/qrcodes/174419877-f6b5dae1-2251-4b67-95f1-5e1143e40fae.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanamiryan/php-qrcode-detector-decoder/7e0cbfa8490c715b1acbadc30bb89b4ca534e033/tests/qrcodes/174419877-f6b5dae1-2251-4b67-95f1-5e1143e40fae.jpg -------------------------------------------------------------------------------- /tests/qrcodes/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanamiryan/php-qrcode-detector-decoder/7e0cbfa8490c715b1acbadc30bb89b4ca534e033/tests/qrcodes/empty.png -------------------------------------------------------------------------------- /tests/qrcodes/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanamiryan/php-qrcode-detector-decoder/7e0cbfa8490c715b1acbadc30bb89b4ca534e033/tests/qrcodes/hello_world.png -------------------------------------------------------------------------------- /tests/qrcodes/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khanamiryan/php-qrcode-detector-decoder/7e0cbfa8490c715b1acbadc30bb89b4ca534e033/tests/qrcodes/test.png --------------------------------------------------------------------------------