Skip to content

Commit b331b0e

Browse files
authored
Merge pull request #132 from GenieTim/master
Next iteration of improvements
2 parents 69c49a5 + e236a0e commit b331b0e

11 files changed

+69
-41
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
/vendor
33
.idea/
44
.phpunit.result.cache
5+
6+
tests/qrcodes/private_test.png
7+
tests/qrcodes/private_test2.png

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# QR code decoder / reader for PHP
2+
3+
[![Tests](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/tests.yml/badge.svg)](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/tests.yml)
4+
[![Static Tests](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/static_tests.yml/badge.svg)](https://github.com/khanamiryan/php-qrcode-detector-decoder/actions/workflows/static_tests.yml)
5+
26
This is a PHP library to detect and decode QR-codes.<br />This is first and only QR code reader that works without extensions.<br />
37
Ported from [ZXing library](https://github.com/zxing/zxing)
48

lib/Common/HybridBinarizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private static function calculateBlackPoints(
146146
// finish the rest of the rows quickly
147147
for ($yy++, $offset += $width; $yy < self::$BLOCK_SIZE; $yy++, $offset += $width) {
148148
for ($xx = 0; $xx < self::$BLOCK_SIZE; $xx++) {
149-
$sum += ($luminances[$offset + $xx] & 0xFF);
149+
$sum += (((int)$luminances[(int)round($offset + $xx)]) & 0xFF);
150150
}
151151
}
152152
}
@@ -266,7 +266,7 @@ private static function thresholdBlock(
266266
for ($y = 0, $offset = $yoffset * $stride + $xoffset; $y < self::$BLOCK_SIZE; $y++, $offset += $stride) {
267267
for ($x = 0; $x < self::$BLOCK_SIZE; $x++) {
268268
// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
269-
if (($luminances[$offset + $x] & 0xFF) <= $threshold) {
269+
if (($luminances[(int)round($offset + $x)] & 0xFF) <= $threshold) {
270270
$matrix->set($xoffset + $x, $yoffset + $y);
271271
}
272272
}

lib/Common/PerspectiveTransform.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,11 @@ public function transformPoints(array &$points, &$yValues = 0): void
173173
$x = $points[$i];
174174
$y = $points[$i + 1];
175175
$denominator = $a13 * $x + $a23 * $y + $a33;
176-
$points[$i] = ($a11 * $x + $a21 * $y + $a31) / $denominator;
177-
$points[$i + 1] = ($a12 * $x + $a22 * $y + $a32) / $denominator;
176+
// TODO: think what we do if $denominator == 0 (division by zero)
177+
if ($denominator != 0.0) {
178+
$points[$i] = ($a11 * $x + $a21 * $y + $a31) / $denominator;
179+
$points[$i + 1] = ($a12 * $x + $a22 * $y + $a32) / $denominator;
180+
}
178181
}
179182
}
180183

lib/IMagickLuminanceSource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function _IMagickLuminanceSource(\Imagick $image, $width, $height): void
9898

9999
$image->setImageColorspace(\Imagick::COLORSPACE_GRAY);
100100
// Check that we actually have enough space to do it
101-
if ($width * $height * 16 * 3 > $this->kmgStringToBytes(ini_get('memory_limit'))) {
101+
if (ini_get('memory_limit') != -1 && $width * $height * 16 * 3 > $this->kmgStringToBytes(ini_get('memory_limit'))) {
102102
throw new \RuntimeException("PHP Memory Limit does not allow pixel export.");
103103
}
104104
$pixels = $image->exportImagePixels(1, 1, $width, $height, "RGB", \Imagick::PIXEL_CHAR);

lib/Qrcode/Decoder/BitMatrixParser.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function __construct($bitMatrix)
4242
{
4343
$dimension = $bitMatrix->getHeight();
4444
if ($dimension < 21 || ($dimension & 0x03) != 1) {
45-
throw FormatException::getFormatInstance();
45+
throw new FormatException();
4646
}
4747
$this->bitMatrix = $bitMatrix;
4848
}
@@ -108,7 +108,7 @@ public function readCodewords()
108108
$readingUp ^= true; // readingUp = !readingUp; // switch directions
109109
}
110110
if ($resultOffset != $version->getTotalCodewords()) {
111-
throw FormatException::getFormatInstance();
111+
throw new FormatException();
112112
}
113113

114114
return $result;
@@ -156,7 +156,7 @@ public function readFormatInformation()
156156
if ($parsedFormatInfo != null) {
157157
return $parsedFormatInfo;
158158
}
159-
throw FormatException::getFormatInstance();
159+
throw new FormatException();
160160
}
161161

162162
/**
@@ -221,7 +221,7 @@ public function readVersion()
221221

222222
return $theParsedVersion;
223223
}
224-
throw FormatException::getFormatInstance("both version information locations cannot be parsed as the valid encoding of version information");
224+
throw new FormatException("both version information locations cannot be parsed as the valid encoding of version information");
225225
}
226226

227227
/**

lib/Qrcode/Decoder/DecodedBitStreamParser.php

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public static function decode(
7777
$fc1InEffect = true;
7878
} elseif ($mode == Mode::$STRUCTURED_APPEND) {
7979
if ($bits->available() < 16) {
80-
throw FormatException::getFormatInstance("Bits available < 16");
80+
throw new FormatException("Bits available < 16");
8181
}
8282
// sequence number and parity is added later to the result metadata
8383
// Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
@@ -88,7 +88,7 @@ public static function decode(
8888
$value = self::parseECIValue($bits);
8989
$currentCharacterSetECI = CharacterSetECI::getCharacterSetECIByValue($value);
9090
if ($currentCharacterSetECI == null) {
91-
throw FormatException::getFormatInstance("Current character set ECI is null");
91+
throw new FormatException("Current character set ECI is null");
9292
}
9393
} else {
9494
// First handle Hanzi mode which does not start with character count
@@ -112,15 +112,15 @@ public static function decode(
112112
} elseif ($mode == Mode::$KANJI) {
113113
self::decodeKanjiSegment($bits, $result, $count);
114114
} else {
115-
throw FormatException::getFormatInstance("Unknown mode $mode to decode");
115+
throw new FormatException("Unknown mode $mode to decode");
116116
}
117117
}
118118
}
119119
}
120120
} while ($mode != Mode::$TERMINATOR);
121121
} catch (\InvalidArgumentException $e) {
122122
// from readBits() calls
123-
throw FormatException::getFormatInstance("Invalid argument exception when formatting: " . $e->getMessage());
123+
throw new FormatException("Invalid argument exception when formatting: " . $e->getMessage());
124124
}
125125

126126
return new DecoderResult(
@@ -152,7 +152,7 @@ private static function parseECIValue(BitSource $bits): int
152152

153153
return (($firstByte & 0x1F) << 16) | $secondThirdBytes;
154154
}
155-
throw FormatException::getFormatInstance("ECI Value parsing failed.");
155+
throw new FormatException("ECI Value parsing failed.");
156156
}
157157

158158
/**
@@ -167,7 +167,7 @@ private static function decodeHanziSegment(
167167
): void {
168168
// Don't crash trying to read more bits than we have available.
169169
if ($count * 13 > $bits->available()) {
170-
throw FormatException::getFormatInstance("Trying to read more bits than we have available");
170+
throw new FormatException("Trying to read more bits than we have available");
171171
}
172172

173173
// Each character will require 2 bytes. Read the characters as 2-byte pairs
@@ -202,36 +202,36 @@ private static function decodeNumericSegment(
202202
while ($count >= 3) {
203203
// Each 10 bits encodes three digits
204204
if ($bits->available() < 10) {
205-
throw FormatException::getFormatInstance("Not enough bits available");
205+
throw new FormatException("Not enough bits available");
206206
}
207207
$threeDigitsBits = $bits->readBits(10);
208208
if ($threeDigitsBits >= 1000) {
209-
throw FormatException::getFormatInstance("Too many three digit bits");
209+
throw new FormatException("Too many three digit bits");
210210
}
211211
$result .= (self::toAlphaNumericChar($threeDigitsBits / 100));
212-
$result .= (self::toAlphaNumericChar(($threeDigitsBits / 10) % 10));
212+
$result .= (self::toAlphaNumericChar(((int)round($threeDigitsBits / 10)) % 10));
213213
$result .= (self::toAlphaNumericChar($threeDigitsBits % 10));
214214
$count -= 3;
215215
}
216216
if ($count == 2) {
217217
// Two digits left over to read, encoded in 7 bits
218218
if ($bits->available() < 7) {
219-
throw FormatException::getFormatInstance("Two digits left over to read, encoded in 7 bits, but only " . $bits->available() . ' bits available');
219+
throw new FormatException("Two digits left over to read, encoded in 7 bits, but only " . $bits->available() . ' bits available');
220220
}
221221
$twoDigitsBits = $bits->readBits(7);
222222
if ($twoDigitsBits >= 100) {
223-
throw FormatException::getFormatInstance("Too many bits: $twoDigitsBits expected < 100");
223+
throw new FormatException("Too many bits: $twoDigitsBits expected < 100");
224224
}
225225
$result .= (self::toAlphaNumericChar($twoDigitsBits / 10));
226226
$result .= (self::toAlphaNumericChar($twoDigitsBits % 10));
227227
} elseif ($count == 1) {
228228
// One digit left over to read
229229
if ($bits->available() < 4) {
230-
throw FormatException::getFormatInstance("One digit left to read, but < 4 bits available");
230+
throw new FormatException("One digit left to read, but < 4 bits available");
231231
}
232232
$digitBits = $bits->readBits(4);
233233
if ($digitBits >= 10) {
234-
throw FormatException::getFormatInstance("Too many bits: $digitBits expected < 10");
234+
throw new FormatException("Too many bits: $digitBits expected < 10");
235235
}
236236
$result .= (self::toAlphaNumericChar($digitBits));
237237
}
@@ -242,11 +242,12 @@ private static function decodeNumericSegment(
242242
*/
243243
private static function toAlphaNumericChar(int|float $value)
244244
{
245-
if ($value >= count(self::$ALPHANUMERIC_CHARS)) {
246-
throw FormatException::getFormatInstance("$value has too many alphanumeric chars");
245+
$intVal = (int) $value;
246+
if ($intVal >= count(self::$ALPHANUMERIC_CHARS)) {
247+
throw new FormatException("$intVal is too many alphanumeric chars");
247248
}
248249

249-
return self::$ALPHANUMERIC_CHARS[$value];
250+
return self::$ALPHANUMERIC_CHARS[(int)($intVal)];
250251
}
251252

252253
private static function decodeAlphanumericSegment(
@@ -259,7 +260,7 @@ private static function decodeAlphanumericSegment(
259260
$start = strlen((string) $result);
260261
while ($count > 1) {
261262
if ($bits->available() < 11) {
262-
throw FormatException::getFormatInstance("Not enough bits available to read two expected characters");
263+
throw new FormatException("Not enough bits available to read two expected characters");
263264
}
264265
$nextTwoCharsBits = $bits->readBits(11);
265266
$result .= (self::toAlphaNumericChar($nextTwoCharsBits / 45));
@@ -269,7 +270,7 @@ private static function decodeAlphanumericSegment(
269270
if ($count == 1) {
270271
// special case: one character left
271272
if ($bits->available() < 6) {
272-
throw FormatException::getFormatInstance("Not enough bits available to read one expected character");
273+
throw new FormatException("Not enough bits available to read one expected character");
273274
}
274275
$result .= self::toAlphaNumericChar($bits->readBits(6));
275276
}
@@ -300,7 +301,7 @@ private static function decodeByteSegment(
300301
): void {
301302
// Don't crash trying to read more bits than we have available.
302303
if (8 * $count > $bits->available()) {
303-
throw FormatException::getFormatInstance("Trying to read more bits than we have available");
304+
throw new FormatException("Trying to read more bits than we have available");
304305
}
305306

306307
$readBytes = fill_array(0, $count, 0);
@@ -337,7 +338,7 @@ private static function decodeKanjiSegment(
337338
): void {
338339
// Don't crash trying to read more bits than we have available.
339340
if ($count * 13 > $bits->available()) {
340-
throw FormatException::getFormatInstance("Trying to read more bits than we have available");
341+
throw new FormatException("Trying to read more bits than we have available");
341342
}
342343

343344
// Each character will require 2 bytes. Read the characters as 2-byte pairs

lib/Qrcode/Decoder/Version.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ public function getECBlocksForLevel(ErrorCorrectionLevel $ecLevel)
100100
public static function getProvisionalVersionForDimension($dimension)
101101
{
102102
if ($dimension % 4 != 1) {
103-
throw FormatException::getFormatInstance();
103+
throw new FormatException();
104104
}
105105
try {
106106
return self::getVersionForNumber(($dimension - 17) / 4);
107107
} catch (\InvalidArgumentException) {
108-
throw FormatException::getFormatInstance();
108+
throw new FormatException();
109109
}
110110
}
111111

lib/Qrcode/Detector/FinderPatternFinder.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ final public function find(array|null $hints): \Zxing\Qrcode\Detector\FinderPatt
5858
$tryHarder = $hints != null && array_key_exists('TRY_HARDER', $hints) && $hints['TRY_HARDER'];
5959
$pureBarcode = $hints != null && array_key_exists('PURE_BARCODE', $hints) && $hints['PURE_BARCODE'];
6060
$nrOfRowsSkippable = $hints != null && array_key_exists('NR_ALLOW_SKIP_ROWS', $hints) ? $hints['NR_ALLOW_SKIP_ROWS'] : ($tryHarder ? 0 : null);
61+
$allowedDeviation = $hints != null && array_key_exists('ALLOWED_DEVIATION', $hints) ? $hints['ALLOWED_DEVIATION'] : 0.05;
62+
$maxVariance = $hints != null && array_key_exists('MAX_VARIANCE', $hints) ? $hints['MAX_VARIANCE'] : 0.5;
6163
$maxI = $this->image->getHeight();
6264
$maxJ = $this->image->getWidth();
6365
// We are looking for black/white/black/white/black modules in
@@ -92,14 +94,14 @@ final public function find(array|null $hints): \Zxing\Qrcode\Detector\FinderPatt
9294
} else { // White pixel
9395
if (($currentState & 1) == 0) { // Counting black pixels
9496
if ($currentState == 4) { // A winner?
95-
if (self::foundPatternCross($stateCount)) { // Yes
97+
if (self::foundPatternCross($stateCount, $maxVariance)) { // Yes
9698
$confirmed = $this->handlePossibleCenter($stateCount, $i, $j, $pureBarcode);
9799
if ($confirmed) {
98100
// Start examining every other line. Checking each line turned out to be too
99101
// expensive and didn't improve performance.
100102
$iSkip = 3;
101103
if ($this->hasSkipped) {
102-
$done = $this->haveMultiplyConfirmedCenters();
104+
$done = $this->haveMultiplyConfirmedCenters($allowedDeviation);
103105
} else {
104106
$rowSkip = $nrOfRowsSkippable === null ? $this->findRowSkip() : $nrOfRowsSkippable;
105107
if ($rowSkip > $stateCount[2]) {
@@ -147,13 +149,13 @@ final public function find(array|null $hints): \Zxing\Qrcode\Detector\FinderPatt
147149
}
148150
}
149151
}
150-
if (self::foundPatternCross($stateCount)) {
152+
if (self::foundPatternCross($stateCount, $maxVariance)) {
151153
$confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ, $pureBarcode);
152154
if ($confirmed) {
153155
$iSkip = $stateCount[0];
154156
if ($this->hasSkipped) {
155157
// Found a third one
156-
$done = $this->haveMultiplyConfirmedCenters();
158+
$done = $this->haveMultiplyConfirmedCenters($allowedDeviation);
157159
}
158160
}
159161
}
@@ -173,7 +175,7 @@ final public function find(array|null $hints): \Zxing\Qrcode\Detector\FinderPatt
173175
*
174176
* @psalm-param array<0|positive-int, int> $stateCount
175177
*/
176-
protected static function foundPatternCross(array $stateCount): bool
178+
protected static function foundPatternCross(array $stateCount, float $maxVariance = 0.5): bool
177179
{
178180
$totalModuleSize = 0;
179181
for ($i = 0; $i < 5; $i++) {
@@ -187,7 +189,7 @@ protected static function foundPatternCross(array $stateCount): bool
187189
return false;
188190
}
189191
$moduleSize = $totalModuleSize / 7.0;
190-
$maxVariance = $moduleSize / 2.0;
192+
$maxVariance = $moduleSize * $maxVariance;
191193

192194
// Allow less than 50% variance from 1-1-3-1-1 proportions
193195
return
@@ -537,7 +539,7 @@ private function crossCheckDiagonal(int $startI, int $centerJ, $maxCount, int|fl
537539
/**
538540
* @return bool iff we have found at least 3 finder patterns that have been detected at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the candidates is "pretty similar"
539541
*/
540-
private function haveMultiplyConfirmedCenters(): bool
542+
private function haveMultiplyConfirmedCenters(?float $allowedDeviation = 0.05): bool
541543
{
542544
$confirmedCount = 0;
543545
$totalModuleSize = 0.0;
@@ -561,7 +563,7 @@ private function haveMultiplyConfirmedCenters(): bool
561563
$totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
562564
}
563565

564-
return $totalDeviation <= 0.05 * $totalModuleSize;
566+
return $totalDeviation <= $allowedDeviation * $totalModuleSize;
565567
}
566568

567569
/**
@@ -609,7 +611,7 @@ private function selectBestPatterns()
609611
$startSize = count($this->possibleCenters);
610612
if ($startSize < 3) {
611613
// Couldn't find enough finder patterns
612-
throw new NotFoundException();
614+
throw new NotFoundException("Could not find 3 finder patterns ($startSize found)");
613615
}
614616

615617
// Filter outlier possibilities whose module size is too different

tests/QrReaderTest.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class QrReaderTest extends TestCase
1111
public function setUp(): void
1212
{
1313
error_reporting(E_ALL);
14-
ini_set('memory_limit','2G');
14+
ini_set('memory_limit', '2G');
1515
}
1616

1717
public function testText1()
@@ -54,4 +54,19 @@ public function testText3()
5454
$this->assertSame(null, $qrcode->getError());
5555
$this->assertSame("https://www.gosuslugi.ru/covid-cert/verify/9770000014233333?lang=ru&ck=733a9d218d312fe134f1c2cc06e1a800", $qrcode->text());
5656
}
57+
58+
// TODO: fix this test
59+
// public function testText4()
60+
// {
61+
// $image = __DIR__ . "/qrcodes/174419877-f6b5dae1-2251-4b67-95f1-5e1143e40fae.jpg";
62+
// $qrcode = new QrReader($image);
63+
// $qrcode->decode([
64+
// 'TRY_HARDER' => true,
65+
// 'NR_ALLOW_SKIP_ROWS' => 0,
66+
// // 'ALLOWED_DEVIATION' => 0.1,
67+
// // 'MAX_VARIANCE' => 0.7
68+
// ]);
69+
// $this->assertSame(null, $qrcode->getError());
70+
// $this->assertSame("some text", $qrcode->text());
71+
// }
5772
}

0 commit comments

Comments
 (0)