├── .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 | [](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/tests.yml)
4 | [](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.
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 arrayThroughout 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 arrayThe algorithm will not be explained here, but the following references were helpful 24 | * in creating this implementation:
25 | * 26 | *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 arrayReads 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