From 84eb31696ca6dc7f2be9e492fdf2cf3bfe0126c6 Mon Sep 17 00:00:00 2001
From: codemasher
Date: Mon, 25 Jan 2021 00:42:26 +0100
Subject: [PATCH] :sparkles: init from
https://github.com/codemasher/php-qrcode-decoder/commit/5798a53268f14eb1d7d70098d23db0a81b25f98b
---
composer.json | 5 +-
src/Common/FormatInformation.php | 82 +++
src/Common/ReedSolomonDecoder.php | 192 ++++++
src/Common/Version.php | 2 +-
src/Common/functions.php | 58 ++
src/Decoder/Binarizer.php | 361 +++++++++++
src/Decoder/BitMatrix.php | 204 +++++++
src/Decoder/BitMatrixParser.php | 333 ++++++++++
src/Decoder/Decoder.php | 336 ++++++++++
src/Decoder/DecoderResult.php | 86 +++
src/Decoder/GDLuminanceSource.php | 71 +++
src/Decoder/IMagickLuminanceSource.php | 53 ++
src/Decoder/LuminanceSource.php | 103 ++++
src/Detector/AlignmentPattern.php | 34 ++
src/Detector/AlignmentPatternFinder.php | 287 +++++++++
src/Detector/Detector.php | 358 +++++++++++
src/Detector/FinderPattern.php | 69 +++
src/Detector/FinderPatternFinder.php | 775 ++++++++++++++++++++++++
src/Detector/GridSampler.php | 171 ++++++
src/Detector/PerspectiveTransform.php | 152 +++++
src/Detector/ResultPoint.php | 61 ++
src/QRCodeReader.php | 88 +++
src/includes.php | 16 +
tests/QRCodeReaderTest.php | 121 ++++
tests/qrcodes/alphanum.png | Bin 0 -> 383 bytes
tests/qrcodes/byte.png | Bin 0 -> 1759 bytes
tests/qrcodes/damaged.png | Bin 0 -> 8646 bytes
tests/qrcodes/hello_world.png | Bin 0 -> 3121 bytes
tests/qrcodes/hello_world_mirrored.png | Bin 0 -> 12319 bytes
tests/qrcodes/kanji.png | Bin 0 -> 383 bytes
tests/qrcodes/numeric.png | Bin 0 -> 388 bytes
tests/qrcodes/smol.png | Bin 0 -> 389 bytes
32 files changed, 4016 insertions(+), 2 deletions(-)
create mode 100644 src/Common/FormatInformation.php
create mode 100644 src/Common/ReedSolomonDecoder.php
create mode 100644 src/Common/functions.php
create mode 100644 src/Decoder/Binarizer.php
create mode 100644 src/Decoder/BitMatrix.php
create mode 100644 src/Decoder/BitMatrixParser.php
create mode 100644 src/Decoder/Decoder.php
create mode 100644 src/Decoder/DecoderResult.php
create mode 100644 src/Decoder/GDLuminanceSource.php
create mode 100644 src/Decoder/IMagickLuminanceSource.php
create mode 100644 src/Decoder/LuminanceSource.php
create mode 100644 src/Detector/AlignmentPattern.php
create mode 100644 src/Detector/AlignmentPatternFinder.php
create mode 100644 src/Detector/Detector.php
create mode 100644 src/Detector/FinderPattern.php
create mode 100644 src/Detector/FinderPatternFinder.php
create mode 100644 src/Detector/GridSampler.php
create mode 100644 src/Detector/PerspectiveTransform.php
create mode 100644 src/Detector/ResultPoint.php
create mode 100644 src/QRCodeReader.php
create mode 100644 src/includes.php
create mode 100644 tests/QRCodeReaderTest.php
create mode 100644 tests/qrcodes/alphanum.png
create mode 100644 tests/qrcodes/byte.png
create mode 100644 tests/qrcodes/damaged.png
create mode 100644 tests/qrcodes/hello_world.png
create mode 100644 tests/qrcodes/hello_world_mirrored.png
create mode 100644 tests/qrcodes/kanji.png
create mode 100644 tests/qrcodes/numeric.png
create mode 100644 tests/qrcodes/smol.png
diff --git a/composer.json b/composer.json
index 536bdf56f..311442500 100644
--- a/composer.json
+++ b/composer.json
@@ -48,7 +48,10 @@
"autoload": {
"psr-4": {
"chillerlan\\QRCode\\": "src/"
- }
+ },
+ "files": [
+ "src/includes.php"
+ ]
},
"autoload-dev": {
"psr-4": {
diff --git a/src/Common/FormatInformation.php b/src/Common/FormatInformation.php
new file mode 100644
index 000000000..4f409fc8f
--- /dev/null
+++ b/src/Common/FormatInformation.php
@@ -0,0 +1,82 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+/**
+ * Encapsulates a QR Code's format information, including the data mask used and
+ * error correction level.
+ *
+ * @author Sean Owen
+ * @see \chillerlan\QRCode\Common\ErrorCorrectionLevel
+ */
+final class FormatInformation{
+
+ public const MASK_QR = 0x5412;
+
+ /**
+ * See ISO 18004:2006, Annex C, Table C.1
+ *
+ * [data bits, sequence after masking]
+ */
+ public const DECODE_LOOKUP = [
+ [0x00, 0x5412],
+ [0x01, 0x5125],
+ [0x02, 0x5E7C],
+ [0x03, 0x5B4B],
+ [0x04, 0x45F9],
+ [0x05, 0x40CE],
+ [0x06, 0x4F97],
+ [0x07, 0x4AA0],
+ [0x08, 0x77C4],
+ [0x09, 0x72F3],
+ [0x0A, 0x7DAA],
+ [0x0B, 0x789D],
+ [0x0C, 0x662F],
+ [0x0D, 0x6318],
+ [0x0E, 0x6C41],
+ [0x0F, 0x6976],
+ [0x10, 0x1689],
+ [0x11, 0x13BE],
+ [0x12, 0x1CE7],
+ [0x13, 0x19D0],
+ [0x14, 0x0762],
+ [0x15, 0x0255],
+ [0x16, 0x0D0C],
+ [0x17, 0x083B],
+ [0x18, 0x355F],
+ [0x19, 0x3068],
+ [0x1A, 0x3F31],
+ [0x1B, 0x3A06],
+ [0x1C, 0x24B4],
+ [0x1D, 0x2183],
+ [0x1E, 0x2EDA],
+ [0x1F, 0x2BED],
+ ];
+
+ private int $errorCorrectionLevel;
+ private int $dataMask;
+
+ public function __construct(int $formatInfo){
+ $this->errorCorrectionLevel = ($formatInfo >> 3) & 0x03; // Bits 3,4
+ $this->dataMask = ($formatInfo & 0x07); // Bottom 3 bits
+ }
+
+ public function getErrorCorrectionLevel():EccLevel{
+ return new EccLevel($this->errorCorrectionLevel);
+ }
+
+ public function getDataMask():MaskPattern{
+ return new MaskPattern($this->dataMask);
+ }
+
+}
+
diff --git a/src/Common/ReedSolomonDecoder.php b/src/Common/ReedSolomonDecoder.php
new file mode 100644
index 000000000..943973860
--- /dev/null
+++ b/src/Common/ReedSolomonDecoder.php
@@ -0,0 +1,192 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use RuntimeException;
+use function array_fill, count;
+
+/**
+ * Implements Reed-Solomon decoding, as the name implies.
+ *
+ * The algorithm will not be explained here, but the following references were helpful
+ * in creating this implementation:
+ *
+ *
+ *
+ * Much credit is due to William Rucklidge since portions of this code are an indirect
+ * port of his C++ Reed-Solomon implementation.
+ *
+ * @author Sean Owen
+ * @author William Rucklidge
+ * @author sanfordsquires
+ */
+final class ReedSolomonDecoder{
+
+ /**
+ * Decodes given set of received codewords, which include both data and error-correction
+ * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place,
+ * in the input.
+ *
+ * @param array $received data and error-correction codewords
+ * @param int $numEccCodewords number of error-correction codewords available
+ *
+ * @return int[]
+ * @throws \RuntimeException if decoding fails for any reason
+ */
+ public function decode(array $received, int $numEccCodewords):array{
+ $poly = new GenericGFPoly($received);
+ $syndromeCoefficients = [];
+ $noError = true;
+
+ for($i = 0, $j = $numEccCodewords - 1; $i < $numEccCodewords; $i++, $j--){
+ $eval = $poly->evaluateAt(GF256::exp($i));
+ $syndromeCoefficients[$j] = $eval;
+
+ if($eval !== 0){
+ $noError = false;
+ }
+ }
+
+ if($noError){
+ return $received;
+ }
+
+ [$sigma, $omega] = $this->runEuclideanAlgorithm(
+ GF256::buildMonomial($numEccCodewords, 1),
+ new GenericGFPoly($syndromeCoefficients),
+ $numEccCodewords
+ );
+
+ $errorLocations = $this->findErrorLocations($sigma);
+ $errorMagnitudes = $this->findErrorMagnitudes($omega, $errorLocations);
+ $errorLocationsCount = count($errorLocations);
+ $receivedCount = count($received);
+
+ for($i = 0; $i < $errorLocationsCount; $i++){
+ $position = $receivedCount - 1 - GF256::log($errorLocations[$i]);
+
+ if($position < 0){
+ throw new RuntimeException('Bad error location');
+ }
+
+ $received[$position] ^= $errorMagnitudes[$i];
+ }
+
+ return $received;
+ }
+
+ /**
+ * @return \chillerlan\QRCode\Common\GenericGFPoly[] [sigma, omega]
+ * @throws \RuntimeException
+ */
+ private function runEuclideanAlgorithm(GenericGFPoly $a, GenericGFPoly $b, int $R):array{
+ // Assume a's degree is >= b's
+ if($a->getDegree() < $b->getDegree()){
+ $temp = $a;
+ $a = $b;
+ $b = $temp;
+ }
+
+ $rLast = $a;
+ $r = $b;
+ $tLast = new GenericGFPoly([0]);
+ $t = new GenericGFPoly([1]);
+
+ // Run Euclidean algorithm until r's degree is less than R/2
+ while($r->getDegree() >= $R / 2){
+ $rLastLast = $rLast;
+ $tLastLast = $tLast;
+ $rLast = $r;
+ $tLast = $t;
+
+ // Divide rLastLast by rLast, with quotient in q and remainder in r
+ [$q, $r] = $rLastLast->divide($rLast);
+
+ $t = $q->multiply($tLast)->addOrSubtract($tLastLast);
+
+ if($r->getDegree() >= $rLast->getDegree()){
+ throw new RuntimeException('Division algorithm failed to reduce polynomial?');
+ }
+ }
+
+ $sigmaTildeAtZero = $t->getCoefficient(0);
+
+ if($sigmaTildeAtZero === 0){
+ throw new RuntimeException('sigmaTilde(0) was zero');
+ }
+
+ $inverse = GF256::inverse($sigmaTildeAtZero);
+
+ return [$t->multiplyInt($inverse), $r->multiplyInt($inverse)];
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ private function findErrorLocations(GenericGFPoly $errorLocator):array{
+ // This is a direct application of Chien's search
+ $numErrors = $errorLocator->getDegree();
+
+ if($numErrors === 1){ // shortcut
+ return [$errorLocator->getCoefficient(1)];
+ }
+
+ $result = array_fill(0, $numErrors, 0);
+ $e = 0;
+
+ for($i = 1; $i < 256 && $e < $numErrors; $i++){
+ if($errorLocator->evaluateAt($i) === 0){
+ $result[$e] = GF256::inverse($i);
+ $e++;
+ }
+ }
+
+ if($e !== $numErrors){
+ throw new RuntimeException('Error locator degree does not match number of roots');
+ }
+
+ return $result;
+ }
+
+ private function findErrorMagnitudes(GenericGFPoly $errorEvaluator, array $errorLocations):array{
+ // This is directly applying Forney's Formula
+ $s = count($errorLocations);
+ $result = [];
+
+ for($i = 0; $i < $s; $i++){
+ $xiInverse = GF256::inverse($errorLocations[$i]);
+ $denominator = 1;
+
+ for($j = 0; $j < $s; $j++){
+ if($i !== $j){
+# $denominator = GF256::multiply($denominator, GF256::addOrSubtract(1, GF256::multiply($errorLocations[$j], $xiInverse)));
+ // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug.
+ // Below is a funny-looking workaround from Steven Parkes
+ $term = GF256::multiply($errorLocations[$j], $xiInverse);
+ $denominator = GF256::multiply($denominator, (($term & 0x1) === 0 ? $term | 1 : $term & ~1));
+ }
+ }
+
+ $result[$i] = GF256::multiply($errorEvaluator->evaluateAt($xiInverse), GF256::inverse($denominator));
+ }
+
+ return $result;
+ }
+
+}
diff --git a/src/Common/Version.php b/src/Common/Version.php
index 3b613587e..1c0ba45d4 100644
--- a/src/Common/Version.php
+++ b/src/Common/Version.php
@@ -306,7 +306,7 @@ final class Version{
* the maximum character count for the given $mode and $eccLevel
*/
public function getMaxLengthForMode(int $mode, EccLevel $eccLevel):?int{
- return self::MAX_LENGTH[$this->version][$mode][$eccLevel->getOrdinal()] ?? null;
+ return self::MAX_LENGTH[$this->version][Mode::DATA_MODES[$mode]][$eccLevel->getOrdinal()] ?? null;
}
/**
diff --git a/src/Common/functions.php b/src/Common/functions.php
new file mode 100644
index 000000000..ebfa03094
--- /dev/null
+++ b/src/Common/functions.php
@@ -0,0 +1,58 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Common;
+
+use function array_slice, array_splice, sqrt;
+use const PHP_INT_SIZE;
+
+const QRCODE_DECODER_INCLUDES = true;
+
+function arraycopy(array $srcArray, int $srcPos, array $destArray, int $destPos, int $length):array{
+ array_splice($destArray, $destPos, $length, array_slice($srcArray, $srcPos, $length));
+
+ return $destArray;
+}
+
+function uRShift(int $a, int $b):int{
+
+ if($b === 0){
+ return $a;
+ }
+
+ return ($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1));
+}
+
+function numBitsDiffering(int $a, int $b):int{
+ // a now has a 1 bit exactly where its bit differs with b's
+ $a ^= $b;
+ // Offset i holds the number of 1 bits in the binary representation of i
+ $BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
+ // Count bits set quickly with a series of lookups:
+ $count = 0;
+
+ for($i = 0; $i < 32; $i += 4){
+ $count += $BITS_SET_IN_HALF_BYTE[uRShift($a, $i) & 0x0F];
+ }
+
+ return $count;
+}
+
+function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{
+ $xDiff = $aX - $bX;
+ $yDiff = $aY - $bY;
+
+ return $xDiff * $xDiff + $yDiff * $yDiff;
+}
+
+function distance(float $aX, float $aY, float $bX, float $bY):float{
+ return sqrt(squaredDistance($aX, $aY, $bX, $bY));
+}
+
diff --git a/src/Decoder/Binarizer.php b/src/Decoder/Binarizer.php
new file mode 100644
index 000000000..d3c7fdaf3
--- /dev/null
+++ b/src/Decoder/Binarizer.php
@@ -0,0 +1,361 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use RuntimeException;
+use function array_fill, count, max;
+
+/**
+ * This class implements a local thresholding algorithm, which while slower than the
+ * GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for
+ * high frequency images of barcodes with black data on white backgrounds. For this application,
+ * it does a much better job than a global blackpoint with severe shadows and gradients.
+ * However it tends to produce artifacts on lower frequency images and is therefore not
+ * a good general purpose binarizer for uses outside ZXing.
+ *
+ * This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers,
+ * and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already
+ * inherently local, and only fails for horizontal gradients. We can revisit that problem later,
+ * but for now it was not a win to use local blocks for 1D.
+ *
+ * This Binarizer is the default for the unit tests and the recommended class for library users.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class Binarizer{
+
+ // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels.
+ // So this is the smallest dimension in each axis we can accept.
+ private const BLOCK_SIZE_POWER = 3;
+ private const BLOCK_SIZE = 8; // ...0100...00
+ private const BLOCK_SIZE_MASK = 7; // ...0011...11
+ private const MINIMUM_DIMENSION = 40;
+ private const MIN_DYNAMIC_RANGE = 24;
+
+# private const LUMINANCE_BITS = 5;
+ private const LUMINANCE_SHIFT = 3;
+ private const LUMINANCE_BUCKETS = 32;
+
+ private LuminanceSource $source;
+
+ public function __construct(LuminanceSource $source){
+ $this->source = $source;
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ private function estimateBlackPoint(array $buckets):int{
+ // Find the tallest peak in the histogram.
+ $numBuckets = count($buckets);
+ $maxBucketCount = 0;
+ $firstPeak = 0;
+ $firstPeakSize = 0;
+
+ for($x = 0; $x < $numBuckets; $x++){
+
+ if($buckets[$x] > $firstPeakSize){
+ $firstPeak = $x;
+ $firstPeakSize = $buckets[$x];
+ }
+
+ if($buckets[$x] > $maxBucketCount){
+ $maxBucketCount = $buckets[$x];
+ }
+ }
+
+ // Find the second-tallest peak which is somewhat far from the tallest peak.
+ $secondPeak = 0;
+ $secondPeakScore = 0;
+
+ for($x = 0; $x < $numBuckets; $x++){
+ $distanceToBiggest = $x - $firstPeak;
+ // Encourage more distant second peaks by multiplying by square of distance.
+ $score = $buckets[$x] * $distanceToBiggest * $distanceToBiggest;
+
+ if($score > $secondPeakScore){
+ $secondPeak = $x;
+ $secondPeakScore = $score;
+ }
+ }
+
+ // Make sure firstPeak corresponds to the black peak.
+ if($firstPeak > $secondPeak){
+ $temp = $firstPeak;
+ $firstPeak = $secondPeak;
+ $secondPeak = $temp;
+ }
+
+ // If there is too little contrast in the image to pick a meaningful black point, throw rather
+ // than waste time trying to decode the image, and risk false positives.
+ if($secondPeak - $firstPeak <= $numBuckets / 16){
+ throw new RuntimeException('no meaningful dark point found');
+ }
+
+ // Find a valley between them that is low and closer to the white peak.
+ $bestValley = $secondPeak - 1;
+ $bestValleyScore = -1;
+
+ for($x = $secondPeak - 1; $x > $firstPeak; $x--){
+ $fromFirst = $x - $firstPeak;
+ $score = $fromFirst * $fromFirst * ($secondPeak - $x) * ($maxBucketCount - $buckets[$x]);
+
+ if($score > $bestValleyScore){
+ $bestValley = $x;
+ $bestValleyScore = $score;
+ }
+ }
+
+ return $bestValley << self::LUMINANCE_SHIFT;
+ }
+
+ /**
+ * Calculates the final BitMatrix once for all requests. This could be called once from the
+ * constructor instead, but there are some advantages to doing it lazily, such as making
+ * profiling easier, and not doing heavy lifting when callers don't expect it.
+ *
+ * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive
+ * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or
+ * may not apply sharpening. Therefore, a row from this matrix may not be identical to one
+ * fetched using getBlackRow(), so don't mix and match between them.
+ *
+ * @return \chillerlan\QRCode\Decoder\BitMatrix The 2D array of bits for the image (true means black).
+ */
+ public function getBlackMatrix():BitMatrix{
+ $width = $this->source->getWidth();
+ $height = $this->source->getHeight();
+
+ if($width >= self::MINIMUM_DIMENSION && $height >= self::MINIMUM_DIMENSION){
+ $subWidth = $width >> self::BLOCK_SIZE_POWER;
+
+ if(($width & self::BLOCK_SIZE_MASK) !== 0){
+ $subWidth++;
+ }
+
+ $subHeight = $height >> self::BLOCK_SIZE_POWER;
+
+ if(($height & self::BLOCK_SIZE_MASK) !== 0){
+ $subHeight++;
+ }
+
+ return $this->calculateThresholdForBlock($subWidth, $subHeight, $width, $height);
+ }
+
+ // If the image is too small, fall back to the global histogram approach.
+ return $this->getHistogramBlackMatrix($width, $height);
+ }
+
+ public function getHistogramBlackMatrix(int $width, int $height):BitMatrix{
+ $matrix = new BitMatrix(max($width, $height));
+
+ // Quickly calculates the histogram by sampling four rows from the image. This proved to be
+ // more robust on the blackbox tests than sampling a diagonal as we used to do.
+ $buckets = array_fill(0, self::LUMINANCE_BUCKETS, 0);
+
+ for($y = 1; $y < 5; $y++){
+ $row = (int)($height * $y / 5);
+ $localLuminances = $this->source->getRow($row);
+ $right = (int)(($width * 4) / 5);
+
+ for($x = (int)($width / 5); $x < $right; $x++){
+ $pixel = $localLuminances[(int)$x] & 0xff;
+ $buckets[$pixel >> self::LUMINANCE_SHIFT]++;
+ }
+ }
+
+ $blackPoint = $this->estimateBlackPoint($buckets);
+
+ // We delay reading the entire image luminance until the black point estimation succeeds.
+ // Although we end up reading four rows twice, it is consistent with our motto of
+ // "fail quickly" which is necessary for continuous scanning.
+ $localLuminances = $this->source->getMatrix();
+
+ for($y = 0; $y < $height; $y++){
+ $offset = $y * $width;
+
+ for($x = 0; $x < $width; $x++){
+ $pixel = (int)($localLuminances[$offset + $x] & 0xff);
+
+ if($pixel < $blackPoint){
+ $matrix->set($x, $y);
+ }
+ }
+ }
+
+ return $matrix;
+ }
+
+ /**
+ * Calculates a single black point for each block of pixels and saves it away.
+ * See the following thread for a discussion of this algorithm:
+ *
+ * @see http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0
+ */
+ private function calculateBlackPoints(array $luminances, int $subWidth, int $subHeight, int $width, int $height):array{
+ $blackPoints = array_fill(0, $subHeight, 0);
+
+ foreach($blackPoints as $key => $point){
+ $blackPoints[$key] = array_fill(0, $subWidth, 0);
+ }
+
+ for($y = 0; $y < $subHeight; $y++){
+ $yoffset = ($y << self::BLOCK_SIZE_POWER);
+ $maxYOffset = $height - self::BLOCK_SIZE;
+
+ if($yoffset > $maxYOffset){
+ $yoffset = $maxYOffset;
+ }
+
+ for($x = 0; $x < $subWidth; $x++){
+ $xoffset = ($x << self::BLOCK_SIZE_POWER);
+ $maxXOffset = $width - self::BLOCK_SIZE;
+
+ if($xoffset > $maxXOffset){
+ $xoffset = $maxXOffset;
+ }
+
+ $sum = 0;
+ $min = 255;
+ $max = 0;
+
+ for($yy = 0, $offset = $yoffset * $width + $xoffset; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
+
+ for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
+ $pixel = (int)($luminances[(int)($offset + $xx)]) & 0xff;
+ $sum += $pixel;
+ // still looking for good contrast
+ if($pixel < $min){
+ $min = $pixel;
+ }
+
+ if($pixel > $max){
+ $max = $pixel;
+ }
+ }
+
+ // short-circuit min/max tests once dynamic range is met
+ if($max - $min > self::MIN_DYNAMIC_RANGE){
+ // finish the rest of the rows quickly
+ for($yy++, $offset += $width; $yy < self::BLOCK_SIZE; $yy++, $offset += $width){
+ for($xx = 0; $xx < self::BLOCK_SIZE; $xx++){
+ $sum += $luminances[$offset + $xx] & 0xff;
+ }
+ }
+ }
+ }
+
+ // The default estimate is the average of the values in the block.
+ $average = $sum >> (self::BLOCK_SIZE_POWER * 2);
+
+ if($max - $min <= self::MIN_DYNAMIC_RANGE){
+ // If variation within the block is low, assume this is a block with only light or only
+ // dark pixels. In that case we do not want to use the average, as it would divide this
+ // low contrast area into black and white pixels, essentially creating data out of noise.
+ //
+ // The default assumption is that the block is light/background. Since no estimate for
+ // the level of dark pixels exists locally, use half the min for the block.
+ $average = (int)($min / 2);
+
+ if($y > 0 && $x > 0){
+ // Correct the "white background" assumption for blocks that have neighbors by comparing
+ // the pixels in this block to the previously calculated black points. This is based on
+ // the fact that dark barcode symbology is always surrounded by some amount of light
+ // background for which reasonable black point estimates were made. The bp estimated at
+ // the boundaries is used for the interior.
+
+ // The (min < bp) is arbitrary but works better than other heuristics that were tried.
+ $averageNeighborBlackPoint = (int)(($blackPoints[$y - 1][$x] + (2 * $blackPoints[$y][$x - 1]) + $blackPoints[$y - 1][$x - 1]) / 4);
+
+ if($min < $averageNeighborBlackPoint){
+ $average = $averageNeighborBlackPoint;
+ }
+ }
+ }
+
+ $blackPoints[$y][$x] = (int)($average);
+ }
+ }
+
+ return $blackPoints;
+ }
+
+ /**
+ * For each block in the image, calculate the average black point using a 5x5 grid
+ * of the blocks around it. Also handles the corner cases (fractional blocks are computed based
+ * on the last pixels in the row/column which are also used in the previous block).
+ */
+ private function calculateThresholdForBlock(
+ int $subWidth,
+ int $subHeight,
+ int $width,
+ int $height
+ ):BitMatrix{
+ $matrix = new BitMatrix(max($width, $height));
+ $luminances = $this->source->getMatrix();
+ $blackPoints = $this->calculateBlackPoints($luminances, $subWidth, $subHeight, $width, $height);
+
+ for($y = 0; $y < $subHeight; $y++){
+ $yoffset = ($y << self::BLOCK_SIZE_POWER);
+ $maxYOffset = $height - self::BLOCK_SIZE;
+
+ if($yoffset > $maxYOffset){
+ $yoffset = $maxYOffset;
+ }
+
+ for($x = 0; $x < $subWidth; $x++){
+ $xoffset = ($x << self::BLOCK_SIZE_POWER);
+ $maxXOffset = $width - self::BLOCK_SIZE;
+
+ if($xoffset > $maxXOffset){
+ $xoffset = $maxXOffset;
+ }
+
+ $left = $this->cap($x, 2, $subWidth - 3);
+ $top = $this->cap($y, 2, $subHeight - 3);
+ $sum = 0;
+
+ for($z = -2; $z <= 2; $z++){
+ $blackRow = $blackPoints[$top + $z];
+ $sum += $blackRow[$left - 2] + $blackRow[$left - 1] + $blackRow[$left] + $blackRow[$left + 1] + $blackRow[$left + 2];
+ }
+
+ $average = (int)($sum / 25);
+
+ // Applies a single threshold to a block of pixels.
+ for($j = 0, $o = $yoffset * $width + $xoffset; $j < self::BLOCK_SIZE; $j++, $o += $width){
+ for($i = 0; $i < self::BLOCK_SIZE; $i++){
+ // Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
+ if(($luminances[$o + $i] & 0xff) <= $average){
+ $matrix->set($xoffset + $i, $yoffset + $j);
+ }
+ }
+ }
+ }
+ }
+
+ return $matrix;
+ }
+
+ private function cap(int $value, int $min, int $max):int{
+
+ if($value < $min){
+ return $min;
+ }
+
+ if($value > $max){
+ return $max;
+ }
+
+ return $value;
+ }
+
+}
diff --git a/src/Decoder/BitMatrix.php b/src/Decoder/BitMatrix.php
new file mode 100644
index 000000000..a8d41eefa
--- /dev/null
+++ b/src/Decoder/BitMatrix.php
@@ -0,0 +1,204 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use chillerlan\QRCode\Common\{MaskPattern, Version};
+use InvalidArgumentException;
+use function chillerlan\QRCode\Common\uRShift;
+use function array_fill, count;
+
+final class BitMatrix{
+
+ private int $dimension;
+ private int $rowSize;
+ private array $bits;
+
+ public function __construct(int $dimension){
+ $this->dimension = $dimension;
+ $this->rowSize = ((int)(($this->dimension + 0x1f) / 0x20));
+ $this->bits = array_fill(0, $this->rowSize * $this->dimension, 0);
+ }
+
+ /**
+ * Sets the given bit to true.
+ *
+ * @param int $x ; The horizontal component (i.e. which column)
+ * @param int $y ; The vertical component (i.e. which row)
+ */
+ public function set(int $x, int $y):void{
+ $offset = (int)($y * $this->rowSize + ($x / 0x20));
+
+ $this->bits[$offset] ??= 0;
+ $this->bits[$offset] |= ($this->bits[$offset] |= 1 << ($x & 0x1f));
+ }
+
+ /**
+ * Flips the given bit. 1 << (0xf9 & 0x1f)
+ *
+ * @param int $x ; The horizontal component (i.e. which column)
+ * @param int $y ; The vertical component (i.e. which row)
+ */
+ public function flip(int $x, int $y):void{
+ $offset = $y * $this->rowSize + (int)($x / 0x20);
+
+ $this->bits[$offset] = ($this->bits[$offset] ^ (1 << ($x & 0x1f)));
+ }
+
+ /**
+ * Sets a square region of the bit matrix to true.
+ *
+ * @param int $left ; The horizontal position to begin at (inclusive)
+ * @param int $top ; The vertical position to begin at (inclusive)
+ * @param int $width ; The width of the region
+ * @param int $height ; The height of the region
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setRegion(int $left, int $top, int $width, int $height):void{
+
+ if($top < 0 || $left < 0){
+ throw new InvalidArgumentException('Left and top must be nonnegative');
+ }
+
+ if($height < 1 || $width < 1){
+ throw new InvalidArgumentException('Height and width must be at least 1');
+ }
+
+ $right = $left + $width;
+ $bottom = $top + $height;
+
+ if($bottom > $this->dimension || $right > $this->dimension){
+ throw new InvalidArgumentException('The region must fit inside the matrix');
+ }
+
+ for($y = $top; $y < $bottom; $y++){
+ $yOffset = $y * $this->rowSize;
+
+ for($x = $left; $x < $right; $x++){
+ $xOffset = $yOffset + (int)($x / 0x20);
+ $this->bits[$xOffset] = ($this->bits[$xOffset] |= 1 << ($x & 0x1f));
+ }
+ }
+ }
+
+ /**
+ * @return int The dimension (width/height) of the matrix
+ */
+ public function getDimension():int{
+ return $this->dimension;
+ }
+
+ /**
+ * Gets the requested bit, where true means black.
+ *
+ * @param int $x The horizontal component (i.e. which column)
+ * @param int $y The vertical component (i.e. which row)
+ *
+ * @return bool value of given bit in matrix
+ */
+ public function get(int $x, int $y):bool{
+ $offset = (int)($y * $this->rowSize + ($x / 0x20));
+
+ $this->bits[$offset] ??= 0;
+
+ return (uRShift($this->bits[$offset], ($x & 0x1f)) & 1) !== 0;
+ }
+
+ /**
+ * See ISO 18004:2006 Annex E
+ */
+ public function buildFunctionPattern(Version $version):BitMatrix{
+ $dimension = $version->getDimension();
+ // @todo
+ $bitMatrix = new self($dimension);
+
+ // Top left finder pattern + separator + format
+ $bitMatrix->setRegion(0, 0, 9, 9);
+ // Top right finder pattern + separator + format
+ $bitMatrix->setRegion($dimension - 8, 0, 8, 9);
+ // Bottom left finder pattern + separator + format
+ $bitMatrix->setRegion(0, $dimension - 8, 9, 8);
+
+ // Alignment patterns
+ $apc = $version->getAlignmentPattern();
+ $max = count($apc);
+
+ for($x = 0; $x < $max; $x++){
+ $i = $apc[$x] - 2;
+
+ for($y = 0; $y < $max; $y++){
+ if(($x === 0 && ($y === 0 || $y === $max - 1)) || ($x === $max - 1 && $y === 0)){
+ // No alignment patterns near the three finder paterns
+ continue;
+ }
+
+ $bitMatrix->setRegion($apc[$y] - 2, $i, 5, 5);
+ }
+ }
+
+ // Vertical timing pattern
+ $bitMatrix->setRegion(6, 9, 1, $dimension - 17);
+ // Horizontal timing pattern
+ $bitMatrix->setRegion(9, 6, $dimension - 17, 1);
+
+ if($version->getVersionNumber() > 6){
+ // Version info, top right
+ $bitMatrix->setRegion($dimension - 11, 0, 3, 6);
+ // Version info, bottom left
+ $bitMatrix->setRegion(0, $dimension - 11, 6, 3);
+ }
+
+ return $bitMatrix;
+ }
+
+ /**
+ * Mirror the bit matrix in order to attempt a second reading.
+ */
+ public function mirror():void{
+
+ for($x = 0; $x < $this->dimension; $x++){
+ for($y = $x + 1; $y < $this->dimension; $y++){
+ if($this->get($x, $y) !== $this->get($y, $x)){
+ $this->flip($y, $x);
+ $this->flip($x, $y);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations
+ * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix,
+ * including areas used for finder patterns, timing patterns, etc. These areas should be unused
+ * after the point they are unmasked anyway.
+ *
+ * Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position
+ * and j is row position. In fact, as the text says, i is row position and j is column position.
+ *
+ * Implementations of this method reverse the data masking process applied to a QR Code and
+ * make its bits ready to read.
+ */
+ public function unmask(int $dimension, MaskPattern $maskPattern):void{
+ $mask = $maskPattern->getMask();
+
+ for($y = 0; $y < $dimension; $y++){
+ for($x = 0; $x < $dimension; $x++){
+ if($mask($x, $y) === 0){
+ $this->flip($x, $y);
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/src/Decoder/BitMatrixParser.php b/src/Decoder/BitMatrixParser.php
new file mode 100644
index 000000000..190b7ff82
--- /dev/null
+++ b/src/Decoder/BitMatrixParser.php
@@ -0,0 +1,333 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use RuntimeException;
+use chillerlan\QRCode\Common\{Version, FormatInformation};
+use function chillerlan\QRCode\Common\numBitsDiffering;
+use const PHP_INT_MAX;
+
+/**
+ * @author Sean Owen
+ */
+final class BitMatrixParser{
+
+ private BitMatrix $bitMatrix;
+ private ?Version $parsedVersion = null;
+ private ?FormatInformation $parsedFormatInfo = null;
+ private bool $mirror = false;
+
+ /**
+ * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix
+ *
+ * @throws \RuntimeException if dimension is not >= 21 and 1 mod 4
+ */
+ public function __construct(BitMatrix $bitMatrix){
+ $dimension = $bitMatrix->getDimension();
+
+ if($dimension < 21 || ($dimension % 4) !== 1){
+ throw new RuntimeException('dimension is not >= 21, dimension mod 4 not 1');
+ }
+
+ $this->bitMatrix = $bitMatrix;
+ }
+
+ /**
+ * Prepare the parser for a mirrored operation.
+ * This flag has effect only on the {@link #readFormatInformation()} and the
+ * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
+ * {@link #mirror()} method should be called.
+ *
+ * @param bool mirror Whether to read version and format information mirrored.
+ */
+ public function setMirror(bool $mirror):void{
+ $this->parsedVersion = null;
+ $this->parsedFormatInfo = null;
+ $this->mirror = $mirror;
+ }
+
+ /**
+ * Mirror the bit matrix in order to attempt a second reading.
+ */
+ public function mirror():void{
+ $this->bitMatrix->mirror();
+ }
+
+ private function copyBit(int $i, int $j, int $versionBits):int{
+
+ $bit = $this->mirror
+ ? $this->bitMatrix->get($j, $i)
+ : $this->bitMatrix->get($i, $j);
+
+ return $bit ? ($versionBits << 1) | 0x1 : $versionBits << 1;
+ }
+
+ /**
+ * Reads the bits in the {@link BitMatrix} representing the finder pattern in the
+ * correct order in order to reconstruct the codewords bytes contained within the
+ * QR Code.
+ *
+ * @return array bytes encoded within the QR Code
+ * @throws \RuntimeException if the exact number of bytes expected is not read
+ */
+ public function readCodewords():array{
+ $formatInfo = $this->readFormatInformation();
+ $version = $this->readVersion();
+
+ // Get the data mask for the format used in this QR Code. This will exclude
+ // some bits from reading as we wind through the bit matrix.
+ $dimension = $this->bitMatrix->getDimension();
+ $this->bitMatrix->unmask($dimension, $formatInfo->getDataMask());
+ $functionPattern = $this->bitMatrix->buildFunctionPattern($version);
+
+ $readingUp = true;
+ $result = [];
+ $resultOffset = 0;
+ $currentByte = 0;
+ $bitsRead = 0;
+ // Read columns in pairs, from right to left
+ for($j = $dimension - 1; $j > 0; $j -= 2){
+
+ if($j === 6){
+ // Skip whole column with vertical alignment pattern;
+ // saves time and makes the other code proceed more cleanly
+ $j--;
+ }
+ // Read alternatingly from bottom to top then top to bottom
+ for($count = 0; $count < $dimension; $count++){
+ $i = $readingUp ? $dimension - 1 - $count : $count;
+
+ for($col = 0; $col < 2; $col++){
+ // Ignore bits covered by the function pattern
+ if(!$functionPattern->get($j - $col, $i)){
+ // Read a bit
+ $bitsRead++;
+ $currentByte <<= 1;
+
+ if($this->bitMatrix->get($j - $col, $i)){
+ $currentByte |= 1;
+ }
+ // If we've made a whole byte, save it off
+ if($bitsRead === 8){
+ $result[$resultOffset++] = $currentByte; //(byte)
+ $bitsRead = 0;
+ $currentByte = 0;
+ }
+ }
+ }
+ }
+
+ $readingUp = !$readingUp; // switch directions
+ }
+
+ if($resultOffset !== $version->getTotalCodewords()){
+ throw new RuntimeException('offset differs from total codewords for version');
+ }
+
+ return $result;
+ }
+
+ /**
+ * Reads format information from one of its two locations within the QR Code.
+ *
+ * @return \chillerlan\QRCode\Common\FormatInformation encapsulating the QR Code's format info
+ * @throws \RuntimeException if both format information locations cannot be parsed as
+ * the valid encoding of format information
+ */
+ public function readFormatInformation():FormatInformation{
+
+ if($this->parsedFormatInfo !== null){
+ return $this->parsedFormatInfo;
+ }
+
+ // Read top-left format info bits
+ $formatInfoBits1 = 0;
+
+ for($i = 0; $i < 6; $i++){
+ $formatInfoBits1 = $this->copyBit($i, 8, $formatInfoBits1);
+ }
+
+ // .. and skip a bit in the timing pattern ...
+ $formatInfoBits1 = $this->copyBit(7, 8, $formatInfoBits1);
+ $formatInfoBits1 = $this->copyBit(8, 8, $formatInfoBits1);
+ $formatInfoBits1 = $this->copyBit(8, 7, $formatInfoBits1);
+ // .. and skip a bit in the timing pattern ...
+ for($j = 5; $j >= 0; $j--){
+ $formatInfoBits1 = $this->copyBit(8, $j, $formatInfoBits1);
+ }
+
+ // Read the top-right/bottom-left pattern too
+ $dimension = $this->bitMatrix->getDimension();
+ $formatInfoBits2 = 0;
+ $jMin = $dimension - 7;
+
+ for($j = $dimension - 1; $j >= $jMin; $j--){
+ $formatInfoBits2 = $this->copyBit(8, $j, $formatInfoBits2);
+ }
+
+ for($i = $dimension - 8; $i < $dimension; $i++){
+ $formatInfoBits2 = $this->copyBit($i, 8, $formatInfoBits2);
+ }
+
+ $this->parsedFormatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
+
+ if($this->parsedFormatInfo !== null){
+ return $this->parsedFormatInfo;
+ }
+
+ // Should return null, but, some QR codes apparently do not mask this info.
+ // Try again by actually masking the pattern first.
+ $this->parsedFormatInfo = $this->doDecodeFormatInformation(
+ $formatInfoBits1 ^ FormatInformation::MASK_QR,
+ $formatInfoBits2 ^ FormatInformation::MASK_QR
+ );
+
+ if($this->parsedFormatInfo !== null){
+ return $this->parsedFormatInfo;
+ }
+
+ throw new RuntimeException('failed to read format info');
+ }
+
+ /**
+ * @param int $maskedFormatInfo1 format info indicator, with mask still applied
+ * @param int $maskedFormatInfo2 second copy of same info; both are checked at the same time
+ * to establish best match
+ *
+ * @return \chillerlan\QRCode\Common\FormatInformation information about the format it specifies, or {@code null}
+ * if doesn't seem to match any known pattern
+ */
+ private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?FormatInformation{
+ // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing
+ $bestDifference = PHP_INT_MAX;
+ $bestFormatInfo = 0;
+
+ foreach(FormatInformation::DECODE_LOOKUP as $decodeInfo){
+ [$maskedBits, $dataBits] = $decodeInfo;
+
+ if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
+ // Found an exact match
+ return new FormatInformation($maskedBits);
+ }
+
+ $bitsDifference = numBitsDiffering($maskedFormatInfo1, $dataBits);
+
+ if($bitsDifference < $bestDifference){
+ $bestFormatInfo = $maskedBits;
+ $bestDifference = $bitsDifference;
+ }
+
+ if($maskedFormatInfo1 !== $maskedFormatInfo2){
+ // also try the other option
+ $bitsDifference = numBitsDiffering($maskedFormatInfo2, $dataBits);
+
+ if($bitsDifference < $bestDifference){
+ $bestFormatInfo = $maskedBits;
+ $bestDifference = $bitsDifference;
+ }
+ }
+ }
+ // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
+ if($bestDifference <= 3){
+ return new FormatInformation($bestFormatInfo);
+ }
+
+ return null;
+ }
+
+ /**
+ * Reads version information from one of its two locations within the QR Code.
+ *
+ * @return \chillerlan\QRCode\Common\Version encapsulating the QR Code's version
+ * @throws \RuntimeException if both version information locations cannot be parsed as
+ * the valid encoding of version information
+ */
+ public function readVersion():Version{
+
+ if($this->parsedVersion !== null){
+ return $this->parsedVersion;
+ }
+
+ $dimension = $this->bitMatrix->getDimension();
+ $provisionalVersion = ($dimension - 17) / 4;
+
+ if($provisionalVersion <= 6){
+ return new Version($provisionalVersion);
+ }
+
+ // Read top-right version info: 3 wide by 6 tall
+ $versionBits = 0;
+ $ijMin = $dimension - 11;
+
+ for($j = 5; $j >= 0; $j--){
+ for($i = $dimension - 9; $i >= $ijMin; $i--){
+ $versionBits = $this->copyBit($i, $j, $versionBits);
+ }
+ }
+
+ $this->parsedVersion = $this->decodeVersionInformation($versionBits);
+
+ if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
+ return $this->parsedVersion;
+ }
+
+ // Hmm, failed. Try bottom left: 6 wide by 3 tall
+ $versionBits = 0;
+
+ for($i = 5; $i >= 0; $i--){
+ for($j = $dimension - 9; $j >= $ijMin; $j--){
+ $versionBits = $this->copyBit($i, $j, $versionBits);
+ }
+ }
+
+ $this->parsedVersion = $this->decodeVersionInformation($versionBits);
+
+ if($this->parsedVersion !== null && $this->parsedVersion->getDimension() === $dimension){
+ return $this->parsedVersion;
+ }
+
+ throw new RuntimeException('failed to read version');
+ }
+
+ private function decodeVersionInformation(int $versionBits):?Version{
+ $bestDifference = PHP_INT_MAX;
+ $bestVersion = 0;
+
+ for($i = 7; $i <= 40; $i++){
+ $targetVersion = new Version($i);
+ $targetVersionPattern = $targetVersion->getVersionPattern();
+
+ // Do the version info bits match exactly? done.
+ if($targetVersionPattern === $versionBits){
+ return $targetVersion;
+ }
+
+ // Otherwise see if this is the closest to a real version info bit string
+ // we have seen so far
+ $bitsDifference = numBitsDiffering($versionBits, $targetVersionPattern);
+
+ if($bitsDifference < $bestDifference){
+ $bestVersion = $i;
+ $bestDifference = $bitsDifference;
+ }
+ }
+ // We can tolerate up to 3 bits of error since no two version info codewords will
+ // differ in less than 8 bits.
+ if($bestDifference <= 3){
+ return new Version($bestVersion);
+ }
+
+ // If we didn't find a close enough match, fail
+ return null;
+ }
+
+}
diff --git a/src/Decoder/Decoder.php b/src/Decoder/Decoder.php
new file mode 100644
index 000000000..f50f43ef1
--- /dev/null
+++ b/src/Decoder/Decoder.php
@@ -0,0 +1,336 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use Exception, InvalidArgumentException, RuntimeException;
+use chillerlan\QRCode\Common\{BitBuffer, EccLevel, ECICharset, Mode, ReedSolomonDecoder, Version};
+use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number};
+use chillerlan\QRCode\Detector\Detector;
+use function count, array_fill, mb_convert_encoding, mb_detect_encoding;
+
+/**
+ * The main class which implements QR Code decoding -- as opposed to locating and extracting
+ * the QR Code from an image.
+ *
+ * @author Sean Owen
+ */
+final class Decoder{
+
+# private const GB2312_SUBSET = 1;
+
+ /**
+ * Decodes a QR Code represented as a {@link \chillerlan\QRCode\Decoder\BitMatrix}.
+ * A 1 or "true" is taken to mean a black module.
+ *
+ * @param \chillerlan\QRCode\Decoder\LuminanceSource $source
+ *
+ * @return \chillerlan\QRCode\Decoder\DecoderResult text and bytes encoded within the QR Code
+ * @throws \Exception if the QR Code cannot be decoded
+ */
+ public function decode(LuminanceSource $source):DecoderResult{
+ $matrix = (new Binarizer($source))->getBlackMatrix();
+ $bitMatrix = (new Detector($matrix))->detect();
+
+ $fe = null;
+
+ try{
+ // Construct a parser and read version, error-correction level
+ // clone the BitMatrix to avoid errors in case we run into mirroring
+ return $this->decodeParser(new BitMatrixParser(clone $bitMatrix));
+ }
+ catch(Exception $e){
+ $fe = $e;
+ }
+
+ try{
+ $parser = new BitMatrixParser(clone $bitMatrix);
+
+ // Will be attempting a mirrored reading of the version and format info.
+ $parser->setMirror(true);
+
+ // Preemptively read the version.
+# $parser->readVersion();
+
+ // Preemptively read the format information.
+# $parser->readFormatInformation();
+
+ /*
+ * Since we're here, this means we have successfully detected some kind
+ * of version and format information when mirrored. This is a good sign,
+ * that the QR code may be mirrored, and we should try once more with a
+ * mirrored content.
+ */
+ // Prepare for a mirrored reading.
+ $parser->mirror();
+
+ return $this->decodeParser($parser);
+ }
+ catch(Exception $e){
+ // Throw the exception from the original reading
+ if($fe instanceof Exception){
+ throw $fe;
+ }
+
+ throw $e;
+ }
+
+ }
+
+ /**
+ * @param \chillerlan\QRCode\Decoder\BitMatrixParser $parser
+ *
+ * @return \chillerlan\QRCode\Decoder\DecoderResult
+ */
+ private function decodeParser(BitMatrixParser $parser):DecoderResult{
+ $version = $parser->readVersion();
+ $eccLevel = $parser->readFormatInformation()->getErrorCorrectionLevel();
+
+ // Read raw codewords
+ $rawCodewords = $parser->readCodewords();
+ // Separate into data blocks
+ $dataBlocks = $this->getDataBlocks($rawCodewords, $version, $eccLevel);
+
+ $resultBytes = [];
+ $resultOffset = 0;
+
+ // Error-correct and copy data blocks together into a stream of bytes
+ foreach($dataBlocks as $dataBlock){
+ [$numDataCodewords, $codewordBytes] = $dataBlock;
+
+ $corrected = $this->correctErrors($codewordBytes, $numDataCodewords);
+
+ for($i = 0; $i < $numDataCodewords; $i++){
+ $resultBytes[$resultOffset++] = $corrected[$i];
+ }
+ }
+
+ // Decode the contents of that stream of bytes
+ return $this->decodeBitStream($resultBytes, $version, $eccLevel);
+ }
+
+ /**
+ * When QR Codes use multiple data blocks, they are actually interleaved.
+ * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This
+ * method will separate the data into original blocks.
+ *
+ * @param array $rawCodewords bytes as read directly from the QR Code
+ * @param \chillerlan\QRCode\Common\Version $version version of the QR Code
+ * @param \chillerlan\QRCode\Common\EccLevel $eccLevel error-correction level of the QR Code
+ *
+ * @return array DataBlocks containing original bytes, "de-interleaved" from representation in the QR Code
+ * @throws \InvalidArgumentException
+ */
+ private function getDataBlocks(array $rawCodewords, Version $version, EccLevel $eccLevel):array{
+
+ if(count($rawCodewords) !== $version->getTotalCodewords()){
+ throw new InvalidArgumentException('$rawCodewords differ from total codewords for version');
+ }
+
+ // Figure out the number and size of data blocks used by this version and
+ // error correction level
+ [$numEccCodewords, $eccBlocks] = $version->getRSBlocks($eccLevel);
+
+ // Now establish DataBlocks of the appropriate size and number of data codewords
+ $result = [];//new DataBlock[$totalBlocks];
+ $numResultBlocks = 0;
+
+ foreach($eccBlocks as $blockData){
+ [$numEccBlocks, $eccPerBlock] = $blockData;
+
+ for($i = 0; $i < $numEccBlocks; $i++, $numResultBlocks++){
+ $result[$numResultBlocks] = [$eccPerBlock, array_fill(0, $numEccCodewords + $eccPerBlock, 0)];
+ }
+ }
+
+ // All blocks have the same amount of data, except that the last n
+ // (where n may be 0) have 1 more byte. Figure out where these start.
+ $shorterBlocksTotalCodewords = count($result[0][1]);
+ $longerBlocksStartAt = count($result) - 1;
+
+ while($longerBlocksStartAt >= 0){
+ $numCodewords = count($result[$longerBlocksStartAt][1]);
+
+ if($numCodewords == $shorterBlocksTotalCodewords){
+ break;
+ }
+
+ $longerBlocksStartAt--;
+ }
+
+ $longerBlocksStartAt++;
+
+ $shorterBlocksNumDataCodewords = $shorterBlocksTotalCodewords - $numEccCodewords;
+ // The last elements of result may be 1 element longer;
+ // first fill out as many elements as all of them have
+ $rawCodewordsOffset = 0;
+
+ for($i = 0; $i < $shorterBlocksNumDataCodewords; $i++){
+ for($j = 0; $j < $numResultBlocks; $j++){
+ $result[$j][1][$i] = $rawCodewords[$rawCodewordsOffset++];
+ }
+ }
+
+ // Fill out the last data block in the longer ones
+ for($j = $longerBlocksStartAt; $j < $numResultBlocks; $j++){
+ $result[$j][1][$shorterBlocksNumDataCodewords] = $rawCodewords[$rawCodewordsOffset++];
+ }
+
+ // Now add in error correction blocks
+ $max = count($result[0][1]);
+
+ for($i = $shorterBlocksNumDataCodewords; $i < $max; $i++){
+ for($j = 0; $j < $numResultBlocks; $j++){
+ $iOffset = $j < $longerBlocksStartAt ? $i : $i + 1;
+ $result[$j][1][$iOffset] = $rawCodewords[$rawCodewordsOffset++];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Given data and error-correction codewords received, possibly corrupted by errors, attempts to
+ * correct the errors in-place using Reed-Solomon error correction.
+ */
+ private function correctErrors(array $codewordBytes, int $numDataCodewords):array{
+ // First read into an array of ints
+ $codewordsInts = [];
+
+ foreach($codewordBytes as $i => $codewordByte){
+ $codewordsInts[$i] = $codewordByte & 0xFF;
+ }
+
+ $decoded = (new ReedSolomonDecoder)->decode($codewordsInts, (count($codewordBytes) - $numDataCodewords));
+
+ // Copy back into array of bytes -- only need to worry about the bytes that were data
+ // We don't care about errors in the error-correction codewords
+ for($i = 0; $i < $numDataCodewords; $i++){
+ $codewordBytes[$i] = $decoded[$i];
+ }
+
+ return $codewordBytes;
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ private function decodeBitStream(array $bytes, Version $version, EccLevel $ecLevel):DecoderResult{
+ $bits = new BitBuffer($bytes);
+ $symbolSequence = -1;
+ $parityData = -1;
+ $versionNumber = $version->getVersionNumber();
+
+ $result = '';
+ $eciCharset = null;
+# $fc1InEffect = false;
+
+ // While still another segment to read...
+ while($bits->available() >= 4){
+ $datamode = $bits->read(4); // mode is encoded by 4 bits
+
+ // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
+ if($datamode === Mode::DATA_TERMINATOR){
+ break;
+ }
+
+ if($datamode === Mode::DATA_ECI){
+ // Count doesn't apply to ECI
+ $value = ECI::parseValue($bits);
+ $eciCharset = new ECICharset($value);
+ }
+ /** @noinspection PhpStatementHasEmptyBodyInspection */
+ elseif($datamode === Mode::DATA_FNC1_FIRST || $datamode === Mode::DATA_FNC1_SECOND){
+ // We do little with FNC1 except alter the parsed result a bit according to the spec
+# $fc1InEffect = true;
+ }
+ elseif($datamode === Mode::DATA_STRCTURED_APPEND){
+ if($bits->available() < 16){
+ throw new RuntimeException('structured append: not enough bits left');
+ }
+ // sequence number and parity is added later to the result metadata
+ // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue
+ $symbolSequence = $bits->read(8);
+ $parityData = $bits->read(8);
+ }
+ else{
+ // First handle Hanzi mode which does not start with character count
+/* if($datamode === Mode::DATA_HANZI){
+ //chinese mode contains a sub set indicator right after mode indicator
+ $subset = $bits->read(4);
+ $length = $bits->read(Mode::getLengthBitsForVersion($datamode, $versionNumber));
+ if($subset === self::GB2312_SUBSET){
+ $result .= $this->decodeHanziSegment($bits, $length);
+ }
+ }*/
+# else{
+ // "Normal" QR code modes:
+ if($datamode === Mode::DATA_NUMBER){
+ $result .= Number::decodeSegment($bits, $versionNumber);
+ }
+ elseif($datamode === Mode::DATA_ALPHANUM){
+ $str = AlphaNum::decodeSegment($bits, $versionNumber);
+
+ // See section 6.4.8.1, 6.4.8.2
+/* if($fc1InEffect){
+ $start = \strlen($str);
+ // We need to massage the result a bit if in an FNC1 mode:
+ for($i = $start; $i < $start; $i++){
+ if($str[$i] === '%'){
+ if($i < $start - 1 && $str[$i + 1] === '%'){
+ // %% is rendered as %
+ $str = \substr_replace($str, '', $i + 1, 1);//deleteCharAt(i + 1);
+ }
+# else{
+ // In alpha mode, % should be converted to FNC1 separator 0x1D @todo
+# $str = setCharAt($i, \chr(0x1D)); // ???
+# }
+ }
+ }
+ }
+*/
+ $result .= $str;
+ }
+ elseif($datamode === Mode::DATA_BYTE){
+ $str = Byte::decodeSegment($bits, $versionNumber);
+
+ if($eciCharset !== null){
+ $encoding = $eciCharset->getName();
+
+ if($encoding === null){
+ // The spec isn't clear on this mode; see
+ // section 6.4.5: t does not say which encoding to assuming
+ // upon decoding. I have seen ISO-8859-1 used as well as
+ // Shift_JIS -- without anything like an ECI designator to
+ // give a hint.
+ $encoding = mb_detect_encoding($str, ['ISO-8859-1', 'SJIS', 'UTF-8']);
+ }
+
+ $eciCharset = null;
+ $str = mb_convert_encoding($str, $encoding);
+ }
+
+ $result .= $str;
+ }
+ elseif($datamode === Mode::DATA_KANJI){
+ $result .= Kanji::decodeSegment($bits, $versionNumber);
+ }
+ else{
+ throw new RuntimeException('invalid data mode');
+ }
+# }
+ }
+ }
+
+ return new DecoderResult($bytes, $result, $version, $ecLevel, $symbolSequence, $parityData);
+ }
+
+}
diff --git a/src/Decoder/DecoderResult.php b/src/Decoder/DecoderResult.php
new file mode 100644
index 000000000..fb61802e1
--- /dev/null
+++ b/src/Decoder/DecoderResult.php
@@ -0,0 +1,86 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use chillerlan\QRCode\Common\{EccLevel, Version};
+
+/**
+ * Encapsulates the result of decoding a matrix of bits. This typically
+ * applies to 2D barcode formats. For now it contains the raw bytes obtained,
+ * as well as a String interpretation of those bytes, if applicable.
+ *
+ * @author Sean Owen
+ */
+final class DecoderResult{
+
+ private array $rawBytes;
+ private string $text;
+ private Version $version;
+ private EccLevel $eccLevel;
+ private int $structuredAppendParity;
+ private int $structuredAppendSequenceNumber;
+
+ public function __construct(
+ array $rawBytes,
+ string $text,
+ Version $version,
+ EccLevel $eccLevel,
+ int $saSequence = -1,
+ int $saParity = -1
+ ){
+ $this->rawBytes = $rawBytes;
+ $this->text = $text;
+ $this->version = $version;
+ $this->eccLevel = $eccLevel;
+ $this->structuredAppendParity = $saParity;
+ $this->structuredAppendSequenceNumber = $saSequence;
+ }
+
+ /**
+ * @return int[] raw bytes encoded by the barcode, if applicable, otherwise {@code null}
+ */
+ public function getRawBytes():array{
+ return $this->rawBytes;
+ }
+
+ /**
+ * @return string raw text encoded by the barcode
+ */
+ public function getText():string{
+ return $this->text;
+ }
+
+ public function __toString():string{
+ return $this->text;
+ }
+
+ public function getVersion():Version{
+ return $this->version;
+ }
+
+ public function getEccLevel():EccLevel{
+ return $this->eccLevel;
+ }
+
+ public function hasStructuredAppend():bool{
+ return $this->structuredAppendParity >= 0 && $this->structuredAppendSequenceNumber >= 0;
+ }
+
+ public function getStructuredAppendParity():int{
+ return $this->structuredAppendParity;
+ }
+
+ public function getStructuredAppendSequenceNumber():int{
+ return $this->structuredAppendSequenceNumber;
+ }
+
+}
diff --git a/src/Decoder/GDLuminanceSource.php b/src/Decoder/GDLuminanceSource.php
new file mode 100644
index 000000000..710887aae
--- /dev/null
+++ b/src/Decoder/GDLuminanceSource.php
@@ -0,0 +1,71 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use InvalidArgumentException;
+use function get_resource_type, imagecolorat, imagecolorsforindex, imagesx, imagesy, is_resource;
+use const PHP_MAJOR_VERSION;
+
+/**
+ * This class is used to help decode images from files which arrive as GD Resource
+ * It does not support rotation.
+ */
+final class GDLuminanceSource extends LuminanceSource{
+
+ /**
+ * @var resource|\GdImage
+ * @phan-suppress PhanUndeclaredTypeProperty
+ */
+ private $gdImage;
+
+ /**
+ * GDLuminanceSource constructor.
+ *
+ * @param resource|\GdImage $gdImage
+ * @phan-suppress PhanUndeclaredTypeParameter
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($gdImage){
+
+ /**
+ * @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection
+ * @phan-suppress PhanUndeclaredClassInstanceof
+ */
+ if(
+ (PHP_MAJOR_VERSION >= 8 && !$gdImage instanceof \GdImage)
+ || (PHP_MAJOR_VERSION < 8 && (!is_resource($gdImage) || get_resource_type($gdImage) !== 'gd'))
+ ){
+ throw new InvalidArgumentException('Invalid GD image source.');
+ }
+
+ parent::__construct(imagesx($gdImage), imagesy($gdImage));
+
+ $this->gdImage = $gdImage;
+
+ $this->setLuminancePixels();
+ }
+
+ private function setLuminancePixels():void{
+ for($j = 0; $j < $this->height; $j++){
+ for($i = 0; $i < $this->width; $i++){
+ $argb = imagecolorat($this->gdImage, $i, $j);
+ $pixel = imagecolorsforindex($this->gdImage, $argb);
+
+ $this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']);
+ }
+ }
+ }
+
+}
diff --git a/src/Decoder/IMagickLuminanceSource.php b/src/Decoder/IMagickLuminanceSource.php
new file mode 100644
index 000000000..c2f71231c
--- /dev/null
+++ b/src/Decoder/IMagickLuminanceSource.php
@@ -0,0 +1,53 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use Imagick, InvalidArgumentException;
+use function count;
+
+/**
+ * This class is used to help decode images from files which arrive as Imagick Resource
+ * It does not support rotation.
+ */
+final class IMagickLuminanceSource extends LuminanceSource{
+
+ private Imagick $imagick;
+
+ /**
+ * IMagickLuminanceSource constructor.
+ *
+ * @param \Imagick $imagick
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(Imagick $imagick){
+ parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight());
+
+ $this->imagick = $imagick;
+
+ $this->setLuminancePixels();
+ }
+
+ private function setLuminancePixels():void{
+ $this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY);
+ $pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR);
+
+ $countPixels = count($pixels);
+
+ for($i = 0; $i < $countPixels; $i += 3){
+ $this->setLuminancePixel($pixels[$i] & 0xff, $pixels[$i + 1] & 0xff, $pixels[$i + 2] & 0xff);
+ }
+ }
+
+}
diff --git a/src/Decoder/LuminanceSource.php b/src/Decoder/LuminanceSource.php
new file mode 100644
index 000000000..fba88ab9e
--- /dev/null
+++ b/src/Decoder/LuminanceSource.php
@@ -0,0 +1,103 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Decoder;
+
+use InvalidArgumentException;
+use function chillerlan\QRCode\Common\arraycopy;
+
+/**
+ * The purpose of this class hierarchy is to abstract different bitmap implementations across
+ * platforms into a standard interface for requesting greyscale luminance values. The interface
+ * only provides immutable methods; therefore crop and rotation create copies. This is to ensure
+ * that one Reader does not modify the original luminance source and leave it in an unknown state
+ * for other Readers in the chain.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+abstract class LuminanceSource{
+
+ protected array $luminances;
+ protected int $width;
+ protected int $height;
+
+ public function __construct(int $width, int $height){
+ $this->width = $width;
+ $this->height = $height;
+ // In order to measure pure decoding speed, we convert the entire image to a greyscale array
+ // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app.
+ $this->luminances = [];
+ // @todo: grayscale?
+ //$this->luminances = $this->grayScaleToBitmap($this->grayscale());
+ }
+
+ /**
+ * Fetches luminance data for the underlying bitmap. Values should be fetched using:
+ * {@code int luminance = array[y * width + x] & 0xff}
+ *
+ * @return array A row-major 2D array of luminance values. Do not use result.length as it may be
+ * larger than width * height bytes on some platforms. Do not modify the contents
+ * of the result.
+ */
+ public function getMatrix():array{
+ return $this->luminances;
+ }
+
+ /**
+ * @return int The width of the bitmap.
+ */
+ public function getWidth():int{
+ return $this->width;
+ }
+
+ /**
+ * @return int The height of the bitmap.
+ */
+ public function getHeight():int{
+ return $this->height;
+ }
+
+ /**
+ * Fetches one row of luminance data from the underlying platform's bitmap. Values range from
+ * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have
+ * to bitwise and with 0xff for each value. It is preferable for implementations of this method
+ * to only fetch this row rather than the whole image, since no 2D Readers may be installed and
+ * getMatrix() may never be called.
+ *
+ * @param int $y The row to fetch, which must be in [0,getHeight())
+ *
+ * @return array An array containing the luminance data.
+ */
+ public function getRow(int $y):array{
+
+ if($y < 0 || $y >= $this->getHeight()){
+ throw new InvalidArgumentException('Requested row is outside the image: '.$y);
+ }
+
+ return arraycopy($this->luminances, $y * $this->width, [], 0, $this->width);
+ }
+
+ /**
+ * @param int $r
+ * @param int $g
+ * @param int $b
+ *
+ * @return void
+ */
+ protected function setLuminancePixel(int $r, int $g, int $b):void{
+ $this->luminances[] = $r === $g && $g === $b
+ // Image is already greyscale, so pick any channel.
+ ? $r // (($r + 128) % 256) - 128;
+ // Calculate luminance cheaply, favoring green.
+ : ($r + 2 * $g + $b) / 4; // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128;
+ }
+
+}
diff --git a/src/Detector/AlignmentPattern.php b/src/Detector/AlignmentPattern.php
new file mode 100644
index 000000000..1a8ad016a
--- /dev/null
+++ b/src/Detector/AlignmentPattern.php
@@ -0,0 +1,34 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+/**
+ * Encapsulates an alignment pattern, which are the smaller square patterns found in
+ * all but the simplest QR Codes.
+ *
+ * @author Sean Owen
+ */
+final class AlignmentPattern extends ResultPoint{
+
+ /**
+ * Combines this object's current estimate of a finder pattern position and module size
+ * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two.
+ */
+ public function combineEstimate(float $i, float $j, float $newModuleSize):AlignmentPattern{
+ return new self(
+ ($this->x + $j) / 2.0,
+ ($this->y + $i) / 2.0,
+ ($this->estimatedModuleSize + $newModuleSize) / 2.0
+ );
+ }
+
+}
diff --git a/src/Detector/AlignmentPatternFinder.php b/src/Detector/AlignmentPatternFinder.php
new file mode 100644
index 000000000..fcbb26296
--- /dev/null
+++ b/src/Detector/AlignmentPatternFinder.php
@@ -0,0 +1,287 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function abs, count;
+
+
+/**
+ * This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder
+ * patterns but are smaller and appear at regular intervals throughout the image.
+ *
+ * At the moment this only looks for the bottom-right alignment pattern.
+ *
+ * This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied,
+ * pasted and stripped down here for maximum performance but does unfortunately duplicate
+ * some code.
+ *
+ * This class is thread-safe but not reentrant. Each thread must allocate its own object.
+ *
+ * @author Sean Owen
+ */
+final class AlignmentPatternFinder{
+
+ private BitMatrix $bitMatrix;
+ private float $moduleSize;
+ /** @var \chillerlan\QRCode\Detector\AlignmentPattern[] */
+ private array $possibleCenters;
+ private array $crossCheckStateCount;
+
+ /**
+ * Creates a finder that will look in a portion of the whole image.
+ *
+ * @param \chillerlan\QRCode\Decoder\BitMatrix $image image to search
+ * @param float $moduleSize estimated module size so far
+ */
+ public function __construct(BitMatrix $image, float $moduleSize){
+ $this->bitMatrix = $image;
+ $this->moduleSize = $moduleSize;
+ $this->possibleCenters = [];
+ $this->crossCheckStateCount = [];
+ }
+
+ /**
+ * This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since
+ * it's pretty performance-critical and so is written to be fast foremost.
+ *
+ * @param int $startX left column from which to start searching
+ * @param int $startY top row from which to start searching
+ * @param int $width width of region to search
+ * @param int $height height of region to search
+ *
+ * @return \chillerlan\QRCode\Detector\AlignmentPattern|null
+ */
+ public function find(int $startX, int $startY, int $width, int $height):?AlignmentPattern{
+ $maxJ = $startX + $width;
+ $middleI = $startY + ($height / 2);
+ $stateCount = [];
+
+ // We are looking for black/white/black modules in 1:1:1 ratio;
+ // this tracks the number of black/white/black modules seen so far
+ for($iGen = 0; $iGen < $height; $iGen++){
+ // Search from middle outwards
+ $i = (int)($middleI + (($iGen & 0x01) === 0 ? ($iGen + 1) / 2 : -(($iGen + 1) / 2)));
+ $stateCount[0] = 0;
+ $stateCount[1] = 0;
+ $stateCount[2] = 0;
+ $j = $startX;
+ // Burn off leading white pixels before anything else; if we start in the middle of
+ // a white run, it doesn't make sense to count its length, since we don't know if the
+ // white run continued to the left of the start point
+ while($j < $maxJ && !$this->bitMatrix->get($j, $i)){
+ $j++;
+ }
+
+ $currentState = 0;
+
+ while($j < $maxJ){
+
+ if($this->bitMatrix->get($j, $i)){
+ // Black pixel
+ if($currentState === 1){ // Counting black pixels
+ $stateCount[$currentState]++;
+ }
+ // Counting white pixels
+ else{
+ // A winner?
+ if($currentState === 2){
+ // Yes
+ if($this->foundPatternCross($stateCount)){
+ $confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
+
+ if($confirmed !== null){
+ return $confirmed;
+ }
+ }
+
+ $stateCount[0] = $stateCount[2];
+ $stateCount[1] = 1;
+ $stateCount[2] = 0;
+ $currentState = 1;
+ }
+ else{
+ $stateCount[++$currentState]++;
+ }
+ }
+ }
+ // White pixel
+ else{
+ // Counting black pixels
+ if($currentState === 1){
+ $currentState++;
+ }
+
+ $stateCount[$currentState]++;
+ }
+
+ $j++;
+ }
+
+ if($this->foundPatternCross($stateCount)){
+ $confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ);
+
+ if($confirmed !== null){
+ return $confirmed;
+ }
+ }
+
+ }
+
+ // Hmm, nothing we saw was observed and confirmed twice. If we had
+ // any guess at all, return it.
+ if(count($this->possibleCenters)){
+ return $this->possibleCenters[0];
+ }
+
+ return null;
+ }
+
+ /**
+ * @param int[] $stateCount count of black/white/black pixels just read
+ *
+ * @return bool true if the proportions of the counts is close enough to the 1/1/1 ratios
+ * used by alignment patterns to be considered a match
+ */
+ private function foundPatternCross(array $stateCount):bool{
+ $moduleSize = $this->moduleSize;
+ $maxVariance = $moduleSize / 2.0;
+
+ for($i = 0; $i < 3; $i++){
+ if(abs($moduleSize - $stateCount[$i]) >= $maxVariance){
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * This is called when a horizontal scan finds a possible alignment pattern. It will
+ * cross check with a vertical scan, and if successful, will see if this pattern had been
+ * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have
+ * found the alignment pattern.
+ *
+ * @param int[] $stateCount reading state module counts from horizontal scan
+ * @param int $i row where alignment pattern may be found
+ * @param int $j end of possible alignment pattern in row
+ *
+ * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if we have found the same pattern twice, or null if not
+ */
+ private function handlePossibleCenter(array $stateCount, int $i, int $j):?AlignmentPattern{
+ $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2];
+ $centerJ = $this->centerFromEnd($stateCount, $j);
+ $centerI = $this->crossCheckVertical($i, (int)$centerJ, 2 * $stateCount[1], $stateCountTotal);
+
+ if($centerI !== null){
+ $estimatedModuleSize = (float)($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0;
+
+ foreach($this->possibleCenters as $center){
+ // Look for about the same center and module size:
+ if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
+ return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
+ }
+ }
+
+ // Hadn't found this before; save it
+ $point = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize);
+ $this->possibleCenters[] = $point;
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a count of black/white/black pixels just seen and an end position,
+ * figures the location of the center of this black/white/black run.
+ *
+ * @param int[] $stateCount
+ * @param int $end
+ *
+ * @return float
+ */
+ private function centerFromEnd(array $stateCount, int $end):float{
+ return (float)(($end - $stateCount[2]) - $stateCount[1] / 2.0);
+ }
+
+ /**
+ * After a horizontal scan finds a potential alignment pattern, this method
+ * "cross-checks" by scanning down vertically through the center of the possible
+ * alignment pattern to see if the same proportion is detected.
+ *
+ * @param int $startI row where an alignment pattern was detected
+ * @param int $centerJ center of the section that appears to cross an alignment pattern
+ * @param int $maxCount maximum reasonable number of modules that should be
+ * observed in any reading state, based on the results of the horizontal scan
+ * @param int $originalStateCountTotal
+ *
+ * @return float|null vertical center of alignment pattern, or null if not found
+ */
+ private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
+ $maxI = $this->bitMatrix->getDimension();
+ $stateCount = $this->crossCheckStateCount;
+ $stateCount[0] = 0;
+ $stateCount[1] = 0;
+ $stateCount[2] = 0;
+
+ // Start counting up from center
+ $i = $startI;
+ while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
+ $stateCount[1]++;
+ $i--;
+ }
+ // If already too many modules in this state or ran off the edge:
+ if($i < 0 || $stateCount[1] > $maxCount){
+ return null;
+ }
+
+ while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){
+ $stateCount[0]++;
+ $i--;
+ }
+
+ if($stateCount[0] > $maxCount){
+ return null;
+ }
+
+ // Now also count down from center
+ $i = $startI + 1;
+ while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
+ $stateCount[1]++;
+ $i++;
+ }
+
+ if($i == $maxI || $stateCount[1] > $maxCount){
+ return null;
+ }
+
+ while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[2] <= $maxCount){
+ $stateCount[2]++;
+ $i++;
+ }
+
+ if($stateCount[2] > $maxCount){
+ return null;
+ }
+
+ if(5 * abs(($stateCount[0] + $stateCount[1] + $stateCount[2]) - $originalStateCountTotal) >= 2 * $originalStateCountTotal){
+ return null;
+ }
+
+ if(!$this->foundPatternCross($stateCount)){
+ return null;
+ }
+
+ return $this->centerFromEnd($stateCount, $i);
+ }
+
+}
diff --git a/src/Detector/Detector.php b/src/Detector/Detector.php
new file mode 100644
index 000000000..ae28805b4
--- /dev/null
+++ b/src/Detector/Detector.php
@@ -0,0 +1,358 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use RuntimeException;
+use chillerlan\QRCode\Common\Version;
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function abs, is_nan, max, min, round;
+use function chillerlan\QRCode\Common\distance;
+use const NAN;
+
+/**
+ * Encapsulates logic that can detect a QR Code in an image, even if the QR Code
+ * is rotated or skewed, or partially obscured.
+ *
+ * @author Sean Owen
+ */
+final class Detector{
+
+ private BitMatrix $bitMatrix;
+
+ /**
+ * Detector constructor.
+ */
+ public function __construct(BitMatrix $image){
+ $this->bitMatrix = $image;
+ }
+
+ /**
+ * Detects a QR Code in an image.
+ */
+ public function detect():BitMatrix{
+ [$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->bitMatrix))->find();
+
+ $moduleSize = (float)$this->calculateModuleSize($topLeft, $topRight, $bottomLeft);
+ $dimension = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize);
+ $provisionalVersion = new Version((int)(($dimension - 17) / 4));
+ $alignmentPattern = null;
+
+ // Anything above version 1 has an alignment pattern
+ if(!empty($provisionalVersion->getAlignmentPattern())){
+ // Guess where a "bottom right" finder pattern would have been
+ $bottomRightX = $topRight->getX() - $topLeft->getX() + $bottomLeft->getX();
+ $bottomRightY = $topRight->getY() - $topLeft->getY() + $bottomLeft->getY();
+
+ // Estimate that alignment pattern is closer by 3 modules
+ // from "bottom right" to known top left location
+ $correctionToTopLeft = 1.0 - 3.0 / (float)($provisionalVersion->getDimension() - 7);
+ $estAlignmentX = (int)($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX()));
+ $estAlignmentY = (int)($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY()));
+
+ // Kind of arbitrary -- expand search radius before giving up
+ for($i = 4; $i <= 16; $i <<= 1){//??????????
+ $alignmentPattern = $this->findAlignmentInRegion($moduleSize, $estAlignmentX, $estAlignmentY, (float)$i);
+
+ if($alignmentPattern !== null){
+ break;
+ }
+ }
+ // If we didn't find alignment pattern... well try anyway without it
+ }
+
+ $transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern);
+
+ return (new GridSampler)->sampleGrid($this->bitMatrix, $dimension, $transform);
+ }
+
+ /**
+ * Computes an average estimated module size based on estimated derived from the positions
+ * of the three finder patterns.
+ *
+ * @throws \RuntimeException
+ */
+ private function calculateModuleSize(FinderPattern $topLeft, FinderPattern $topRight, FinderPattern $bottomLeft):float{
+ // Take the average
+ $moduleSize = (
+ $this->calculateModuleSizeOneWay($topLeft, $topRight) +
+ $this->calculateModuleSizeOneWay($topLeft, $bottomLeft)
+ ) / 2.0;
+
+ if($moduleSize < 1.0){
+ throw new RuntimeException('module size < 1.0');
+ }
+
+ return $moduleSize;
+ }
+
+ /**
+ * Estimates module size based on two finder patterns -- it uses
+ * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the
+ * width of each, measuring along the axis between their centers.
+ */
+ private function calculateModuleSizeOneWay(FinderPattern $pattern, FinderPattern $otherPattern):float{
+
+ $moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays(
+ $pattern->getX(),
+ $pattern->getY(),
+ $otherPattern->getX(),
+ $otherPattern->getY()
+ );
+
+ $moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays(
+ $otherPattern->getX(),
+ $otherPattern->getY(),
+ $pattern->getX(),
+ $pattern->getY()
+ );
+
+ if(is_nan($moduleSizeEst1)){
+ return $moduleSizeEst2 / 7.0;
+ }
+
+ if(is_nan($moduleSizeEst2)){
+ return $moduleSizeEst1 / 7.0;
+ }
+ // Average them, and divide by 7 since we've counted the width of 3 black modules,
+ // and 1 white and 1 black module on either side. Ergo, divide sum by 14.
+ return ($moduleSizeEst1 + $moduleSizeEst2) / 14.0;
+ }
+
+ /**
+ * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of
+ * a finder pattern by looking for a black-white-black run from the center in the direction
+ * of another po$(another finder pattern center), and in the opposite direction too.
+ */
+ private function sizeOfBlackWhiteBlackRunBothWays(float $fromX, float $fromY, float $toX, float $toY):float{
+ $result = $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$toX, (int)$toY);
+ $dimension = $this->bitMatrix->getDimension();
+ // Now count other way -- don't run off image though of course
+ $scale = 1.0;
+ $otherToX = $fromX - ($toX - $fromX);
+
+ if($otherToX < 0){
+ $scale = $fromX / ($fromX - $otherToX);
+ $otherToX = 0;
+ }
+ elseif($otherToX >= $dimension){
+ $scale = ($dimension - 1 - $fromX) / ($otherToX - $fromX);
+ $otherToX = $dimension - 1;
+ }
+
+ $otherToY = (int)($fromY - ($toY - $fromY) * $scale);
+ $scale = 1.0;
+
+ if($otherToY < 0){
+ $scale = $fromY / ($fromY - $otherToY);
+ $otherToY = 0;
+ }
+ elseif($otherToY >= $dimension){
+ $scale = ($dimension - 1 - $fromY) / ($otherToY - $fromY);
+ $otherToY = $dimension - 1;
+ }
+
+ $otherToX = (int)($fromX + ($otherToX - $fromX) * $scale);
+ $result += $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$otherToX, (int)$otherToY);
+
+ // Middle pixel is double-counted this way; subtract 1
+ return $result - 1.0;
+ }
+
+ /**
+ * This method traces a line from a po$in the image, in the direction towards another point.
+ * It begins in a black region, and keeps going until it finds white, then black, then white again.
+ * It reports the distance from the start to this point.
+ *
+ * This is used when figuring out how wide a finder pattern is, when the finder pattern
+ * may be skewed or rotated.
+ */
+ private function sizeOfBlackWhiteBlackRun(int $fromX, int $fromY, int $toX, int $toY):float{
+ // Mild variant of Bresenham's algorithm;
+ // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
+ $steep = abs($toY - $fromY) > abs($toX - $fromX);
+
+ if($steep){
+ $temp = $fromX;
+ $fromX = $fromY;
+ $fromY = $temp;
+ $temp = $toX;
+ $toX = $toY;
+ $toY = $temp;
+ }
+
+ $dx = abs($toX - $fromX);
+ $dy = abs($toY - $fromY);
+ $error = -$dx / 2;
+ $xstep = $fromX < $toX ? 1 : -1;
+ $ystep = $fromY < $toY ? 1 : -1;
+
+ // In black pixels, looking for white, first or second time.
+ $state = 0;
+ // Loop up until x == toX, but not beyond
+ $xLimit = $toX + $xstep;
+
+ for($x = $fromX, $y = $fromY; $x !== $xLimit; $x += $xstep){
+ $realX = $steep ? $y : $x;
+ $realY = $steep ? $x : $y;
+
+ // Does current pixel mean we have moved white to black or vice versa?
+ // Scanning black in state 0,2 and white in state 1, so if we find the wrong
+ // color, advance to next state or end if we are in state 2 already
+ if(($state === 1) === $this->bitMatrix->get($realX, $realY)){
+
+ if($state === 2){
+ return distance($x, $y, $fromX, $fromY);
+ }
+
+ $state++;
+ }
+
+ $error += $dy;
+
+ if($error > 0){
+
+ if($y === $toY){
+ break;
+ }
+
+ $y += $ystep;
+ $error -= $dx;
+ }
+ }
+
+ // Found black-white-black; give the benefit of the doubt that the next pixel outside the image
+ // is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a
+ // small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.
+ if($state === 2){
+ return distance($toX + $xstep, $toY, $fromX, $fromY);
+ }
+
+ // else we didn't find even black-white-black; no estimate is really possible
+ return NAN;
+ }
+
+ /**
+ * Computes the dimension (number of modules on a size) of the QR Code based on the position
+ * of the finder patterns and estimated module size.
+ *
+ * @throws \RuntimeException
+ */
+ private function computeDimension(
+ FinderPattern $topLeft,
+ FinderPattern $topRight,
+ FinderPattern $bottomLeft,
+ float $moduleSize
+ ):int{
+ $tltrCentersDimension = (int)round($topLeft->distance($topRight) / $moduleSize);
+ $tlblCentersDimension = (int)round($topLeft->distance($bottomLeft) / $moduleSize);
+ $dimension = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7);
+
+ switch($dimension % 4){
+ case 0:
+ $dimension++;
+ break;
+ // 1? do nothing
+ case 2:
+ $dimension--;
+ break;
+ case 3:
+ throw new RuntimeException('estimated dimension: '.$dimension);
+ }
+
+ if($dimension % 4 !== 1){
+ throw new RuntimeException('dimension mod 4 is not 1');
+ }
+
+ return $dimension;
+ }
+
+ /**
+ * Attempts to locate an alignment pattern in a limited region of the image, which is
+ * guessed to contain it.
+ *
+ * @param float $overallEstModuleSize estimated module size so far
+ * @param int $estAlignmentX x coordinate of center of area probably containing alignment pattern
+ * @param int $estAlignmentY y coordinate of above
+ * @param float $allowanceFactor number of pixels in all directions to search from the center
+ *
+ * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if found, or null otherwise
+ */
+ private function findAlignmentInRegion(
+ float $overallEstModuleSize,
+ int $estAlignmentX,
+ int $estAlignmentY,
+ float $allowanceFactor
+ ):?AlignmentPattern{
+ // Look for an alignment pattern (3 modules in size) around where it should be
+ $dimension = $this->bitMatrix->getDimension();
+ $allowance = (int)($allowanceFactor * $overallEstModuleSize);
+ $alignmentAreaLeftX = max(0, $estAlignmentX - $allowance);
+ $alignmentAreaRightX = min($dimension - 1, $estAlignmentX + $allowance);
+
+ if($alignmentAreaRightX - $alignmentAreaLeftX < $overallEstModuleSize * 3){
+ return null;
+ }
+
+ $alignmentAreaTopY = max(0, $estAlignmentY - $allowance);
+ $alignmentAreaBottomY = min($dimension - 1, $estAlignmentY + $allowance);
+
+ if($alignmentAreaBottomY - $alignmentAreaTopY < $overallEstModuleSize * 3){
+ return null;
+ }
+
+ return (new AlignmentPatternFinder($this->bitMatrix, $overallEstModuleSize))->find(
+ $alignmentAreaLeftX,
+ $alignmentAreaTopY,
+ $alignmentAreaRightX - $alignmentAreaLeftX,
+ $alignmentAreaBottomY - $alignmentAreaTopY,
+ );
+ }
+
+ /**
+ *
+ */
+ private function createTransform(
+ FinderPattern $topLeft,
+ FinderPattern $topRight,
+ FinderPattern $bottomLeft,
+ int $dimension,
+ AlignmentPattern $alignmentPattern = null
+ ):PerspectiveTransform{
+ $dimMinusThree = (float)$dimension - 3.5;
+
+ if($alignmentPattern instanceof AlignmentPattern){
+ $bottomRightX = $alignmentPattern->getX();
+ $bottomRightY = $alignmentPattern->getY();
+ $sourceBottomRightX = $dimMinusThree - 3.0;
+ $sourceBottomRightY = $sourceBottomRightX;
+ }
+ else{
+ // Don't have an alignment pattern, just make up the bottom-right point
+ $bottomRightX = ($topRight->getX() - $topLeft->getX()) + $bottomLeft->getX();
+ $bottomRightY = ($topRight->getY() - $topLeft->getY()) + $bottomLeft->getY();
+ $sourceBottomRightX = $dimMinusThree;
+ $sourceBottomRightY = $dimMinusThree;
+ }
+
+ return PerspectiveTransform::quadrilateralToQuadrilateral(
+ 3.5, 3.5,
+ $dimMinusThree, 3.5,
+ $sourceBottomRightX, $sourceBottomRightY,
+ 3.5, $dimMinusThree,
+ $topLeft->getX(), $topLeft->getY(),
+ $topRight->getX(), $topRight->getY(),
+ $bottomRightX, $bottomRightY,
+ $bottomLeft->getX(), $bottomLeft->getY()
+ );
+ }
+
+}
diff --git a/src/Detector/FinderPattern.php b/src/Detector/FinderPattern.php
new file mode 100644
index 000000000..e522d6cfd
--- /dev/null
+++ b/src/Detector/FinderPattern.php
@@ -0,0 +1,69 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use function chillerlan\QRCode\Common\{distance, squaredDistance};
+
+/**
+ * Encapsulates a finder pattern, which are the three square patterns found in
+ * the corners of QR Codes. It also encapsulates a count of similar finder patterns,
+ * as a convenience to the finder's bookkeeping.
+ *
+ * @author Sean Owen
+ */
+final class FinderPattern extends ResultPoint{
+
+ private int $count;
+
+ public function __construct(float $posX, float $posY, float $estimatedModuleSize, int $count = 1){
+ parent::__construct($posX, $posY, $estimatedModuleSize);
+
+ $this->count = $count;
+ }
+
+ public function getCount():int{
+ return $this->count;
+ }
+
+ /**
+ * @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern
+ *
+ * @return float distance between two points
+ */
+ public function distance(FinderPattern $b):float{
+ return distance($this->getX(), $this->getY(), $b->getX(), $b->getY());
+ }
+
+ /**
+ * Get square of distance between a and b.
+ */
+ public function squaredDistance(FinderPattern $b):float{
+ return squaredDistance($this->getX(), $this->getY(), $b->getX(), $b->getY());
+ }
+
+ /**
+ * Combines this object's current estimate of a finder pattern position and module size
+ * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average
+ * based on count.
+ */
+ public function combineEstimate(float $i, float $j, float $newModuleSize):FinderPattern{
+ $combinedCount = $this->count + 1;
+
+ return new self(
+ ($this->count * $this->x + $j) / $combinedCount,
+ ($this->count * $this->y + $i) / $combinedCount,
+ ($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount,
+ $combinedCount
+ );
+ }
+
+}
diff --git a/src/Detector/FinderPatternFinder.php b/src/Detector/FinderPatternFinder.php
new file mode 100644
index 000000000..374688931
--- /dev/null
+++ b/src/Detector/FinderPatternFinder.php
@@ -0,0 +1,775 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ *
+ * @phan-file-suppress PhanTypePossiblyInvalidDimOffset
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use RuntimeException;
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function abs, count, usort;
+use const PHP_FLOAT_MAX;
+
+/**
+ * This class attempts to find finder patterns in a QR Code. Finder patterns are the square
+ * markers at three corners of a QR Code.
+ *
+ * This class is thread-safe but not reentrant. Each thread must allocate its own object.
+ *
+ * @author Sean Owen
+ */
+final class FinderPatternFinder{
+
+ private const MIN_SKIP = 2;
+ private const MAX_MODULES = 177; // 1 pixel/module times 3 modules/center
+ private const CENTER_QUORUM = 2; // support up to version 10 for mobile clients
+ private BitMatrix $bitMatrix;
+ /** @var \chillerlan\QRCode\Detector\FinderPattern[] */
+ private array $possibleCenters;
+ private bool $hasSkipped = false;
+ /** @var int[] */
+ private array $crossCheckStateCount;
+
+ /**
+ *
Creates a finder that will search the image for three finder patterns.
+ *
+ * @param BitMatrix $bitMatrix image to search
+ */
+ public function __construct(BitMatrix $bitMatrix){
+ $this->bitMatrix = $bitMatrix;
+ $this->possibleCenters = [];
+ $this->crossCheckStateCount = $this->getCrossCheckStateCount();
+ }
+
+ /**
+ * @return \chillerlan\QRCode\Detector\FinderPattern[]
+ */
+ public function find():array{
+ $dimension = $this->bitMatrix->getDimension();
+
+ // We are looking for black/white/black/white/black modules in
+ // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
+ // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
+ // image, and then account for the center being 3 modules in size. This gives the smallest
+ // number of pixels the center could be, so skip this often.
+ $iSkip = (int)((3 * $dimension) / (4 * self::MAX_MODULES));
+
+ if($iSkip < self::MIN_SKIP){
+ $iSkip = self::MIN_SKIP;
+ }
+
+ $done = false;
+
+ for($i = $iSkip - 1; $i < $dimension && !$done; $i += $iSkip){
+ // Get a row of black/white values
+ $stateCount = $this->getCrossCheckStateCount();
+ $currentState = 0;
+
+ for($j = 0; $j < $dimension; $j++){
+
+ // Black pixel
+ if($this->bitMatrix->get($j, $i)){
+ // Counting white pixels
+ if(($currentState & 1) === 1){
+ $currentState++;
+ }
+
+ $stateCount[$currentState]++;
+ }
+ // White pixel
+ else{
+ // Counting black pixels
+ if(($currentState & 1) === 0){
+ // A winner?
+ if($currentState === 4){
+ // Yes
+ if($this->foundPatternCross($stateCount)){
+ $confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
+
+ if($confirmed){
+ // Start examining every other line. Checking each line turned out to be too
+ // expensive and didn't improve performance.
+ $iSkip = 3;
+
+ if($this->hasSkipped){
+ $done = $this->haveMultiplyConfirmedCenters();
+ }
+ else{
+ $rowSkip = $this->findRowSkip();
+
+ if($rowSkip > $stateCount[2]){
+ // Skip rows between row of lower confirmed center
+ // and top of presumed third confirmed center
+ // but back up a bit to get a full chance of detecting
+ // it, entire width of center of finder pattern
+
+ // Skip by rowSkip, but back off by $stateCount[2] (size of last center
+ // of pattern we saw) to be conservative, and also back off by iSkip which
+ // is about to be re-added
+ $i += $rowSkip - $stateCount[2] - $iSkip;
+ $j = $dimension - 1;
+ }
+ }
+ }
+ else{
+ $stateCount = $this->doShiftCounts2($stateCount);
+ $currentState = 3;
+
+ continue;
+ }
+ // Clear state to start looking again
+ $currentState = 0;
+ $stateCount = $this->getCrossCheckStateCount();
+ }
+ // No, shift counts back by two
+ else{
+ $stateCount = $this->doShiftCounts2($stateCount);
+ $currentState = 3;
+ }
+ }
+ else{
+ $stateCount[++$currentState]++;
+ }
+ }
+ // Counting white pixels
+ else{
+ $stateCount[$currentState]++;
+ }
+ }
+ }
+
+ if($this->foundPatternCross($stateCount)){
+ $confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
+
+ if($confirmed){
+ $iSkip = $stateCount[0];
+
+ if($this->hasSkipped){
+ // Found a third one
+ $done = $this->haveMultiplyConfirmedCenters();
+ }
+ }
+ }
+ }
+
+ return $this->orderBestPatterns($this->selectBestPatterns());
+ }
+
+ /**
+ * @return int[]
+ */
+ private function getCrossCheckStateCount():array{
+ return [0, 0, 0, 0, 0];
+ }
+
+ /**
+ * @param int[] $stateCount
+ *
+ * @return int[]
+ */
+ private function doShiftCounts2(array $stateCount):array{
+ $stateCount[0] = $stateCount[2];
+ $stateCount[1] = $stateCount[3];
+ $stateCount[2] = $stateCount[4];
+ $stateCount[3] = 1;
+ $stateCount[4] = 0;
+
+ return $stateCount;
+ }
+
+ /**
+ * Given a count of black/white/black/white/black pixels just seen and an end position,
+ * figures the location of the center of this run.
+ *
+ * @param int[] $stateCount
+ *
+ * @return float
+ */
+ private function centerFromEnd(array $stateCount, int $end):float{
+ return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0);
+ }
+
+ /**
+ * @param int[] $stateCount
+ *
+ * @return bool
+ */
+ private function foundPatternCross(array $stateCount):bool{
+ // Allow less than 50% variance from 1-1-3-1-1 proportions
+ return $this->foundPatternVariance($stateCount, 2.0);
+ }
+
+ /**
+ * @param int[] $stateCount
+ *
+ * @return bool
+ */
+ private function foundPatternDiagonal(array $stateCount):bool{
+ // Allow less than 75% variance from 1-1-3-1-1 proportions
+ return $this->foundPatternVariance($stateCount, 1.333);
+ }
+
+ /**
+ * @param int[] $stateCount count of black/white/black/white/black pixels just read
+ *
+ * @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
+ * used by finder patterns to be considered a match
+ */
+ private function foundPatternVariance(array $stateCount, float $variance):bool{
+ $totalModuleSize = 0;
+
+ for($i = 0; $i < 5; $i++){
+ $count = $stateCount[$i];
+
+ if($count === 0){
+ return false;
+ }
+
+ $totalModuleSize += $count;
+ }
+
+ if($totalModuleSize < 7){
+ return false;
+ }
+
+ $moduleSize = $totalModuleSize / 7.0;
+ $maxVariance = $moduleSize / $variance;
+
+ return
+ abs($moduleSize - $stateCount[0]) < $maxVariance
+ && abs($moduleSize - $stateCount[1]) < $maxVariance
+ && abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance
+ && abs($moduleSize - $stateCount[3]) < $maxVariance
+ && abs($moduleSize - $stateCount[4]) < $maxVariance;
+ }
+
+ /**
+ * After a vertical and horizontal scan finds a potential finder pattern, this method
+ * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
+ * finder pattern to see if the same proportion is detected.
+ *
+ * @param $centerI ; row where a finder pattern was detected
+ * @param $centerJ ; center of the section that appears to cross a finder pattern
+ *
+ * @return bool true if proportions are withing expected limits
+ */
+ private function crossCheckDiagonal(int $centerI, int $centerJ):bool{
+ $stateCount = $this->getCrossCheckStateCount();
+
+ // Start counting up, left from center finding black center mass
+ $i = 0;
+
+ while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
+ $stateCount[2]++;
+ $i++;
+ }
+
+ if($stateCount[2] === 0){
+ return false;
+ }
+
+ // Continue up, left finding white space
+ while($centerI >= $i && $centerJ >= $i && !$this->bitMatrix->get($centerJ - $i, $centerI - $i)){
+ $stateCount[1]++;
+ $i++;
+ }
+
+ if($stateCount[1] === 0){
+ return false;
+ }
+
+ // Continue up, left finding black border
+ while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
+ $stateCount[0]++;
+ $i++;
+ }
+
+ if($stateCount[0] === 0){
+ return false;
+ }
+
+ $dimension = $this->bitMatrix->getDimension();
+
+ // Now also count down, right from center
+ $i = 1;
+ while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
+ $stateCount[2]++;
+ $i++;
+ }
+
+ while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->bitMatrix->get($centerJ + $i, $centerI + $i)){
+ $stateCount[3]++;
+ $i++;
+ }
+
+ if($stateCount[3] === 0){
+ return false;
+ }
+
+ while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
+ $stateCount[4]++;
+ $i++;
+ }
+
+ if($stateCount[4] === 0){
+ return false;
+ }
+
+ return $this->foundPatternDiagonal($stateCount);
+ }
+
+ /**
+ * After a horizontal scan finds a potential finder pattern, this method
+ * "cross-checks" by scanning down vertically through the center of the possible
+ * finder pattern to see if the same proportion is detected.
+ *
+ * @param int $startI ; row where a finder pattern was detected
+ * @param int $centerJ ; center of the section that appears to cross a finder pattern
+ * @param int $maxCount ; maximum reasonable number of modules that should be
+ * observed in any reading state, based on the results of the horizontal scan
+ * @param int $originalStateCountTotal
+ *
+ * @return float|null vertical center of finder pattern, or null if not found
+ */
+ private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
+ $maxI = $this->bitMatrix->getDimension();
+ $stateCount = $this->getCrossCheckStateCount();
+
+ // Start counting up from center
+ $i = $startI;
+ while($i >= 0 && $this->bitMatrix->get($centerJ, $i)){
+ $stateCount[2]++;
+ $i--;
+ }
+
+ if($i < 0){
+ return null;
+ }
+
+ while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
+ $stateCount[1]++;
+ $i--;
+ }
+
+ // If already too many modules in this state or ran off the edge:
+ if($i < 0 || $stateCount[1] > $maxCount){
+ return null;
+ }
+
+ while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){
+ $stateCount[0]++;
+ $i--;
+ }
+
+ if($stateCount[0] > $maxCount){
+ return null;
+ }
+
+ // Now also count down from center
+ $i = $startI + 1;
+ while($i < $maxI && $this->bitMatrix->get($centerJ, $i)){
+ $stateCount[2]++;
+ $i++;
+ }
+
+ if($i === $maxI){
+ return null;
+ }
+
+ while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[3] < $maxCount){
+ $stateCount[3]++;
+ $i++;
+ }
+
+ if($i === $maxI || $stateCount[3] >= $maxCount){
+ return null;
+ }
+
+ while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[4] < $maxCount){
+ $stateCount[4]++;
+ $i++;
+ }
+
+ if($stateCount[4] >= $maxCount){
+ return null;
+ }
+
+ // If we found a finder-pattern-like section, but its size is more than 40% different than
+ // the original, assume it's a false positive
+ $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
+
+ if(5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal){
+ return null;
+ }
+
+ if(!$this->foundPatternCross($stateCount)){
+ return null;
+ }
+
+ return $this->centerFromEnd($stateCount, $i);
+ }
+
+ /**
+ * Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical,
+ * except it reads horizontally instead of vertically. This is used to cross-cross
+ * check a vertical cross check and locate the real center of the alignment pattern.
+ */
+ private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{
+ $maxJ = $this->bitMatrix->getDimension();
+ $stateCount = $this->getCrossCheckStateCount();
+
+ $j = $startJ;
+ while($j >= 0 && $this->bitMatrix->get($j, $centerI)){
+ $stateCount[2]++;
+ $j--;
+ }
+
+ if($j < 0){
+ return null;
+ }
+
+ while($j >= 0 && !$this->bitMatrix->get($j, $centerI) && $stateCount[1] <= $maxCount){
+ $stateCount[1]++;
+ $j--;
+ }
+
+ if($j < 0 || $stateCount[1] > $maxCount){
+ return null;
+ }
+
+ while($j >= 0 && $this->bitMatrix->get($j, $centerI) && $stateCount[0] <= $maxCount){
+ $stateCount[0]++;
+ $j--;
+ }
+
+ if($stateCount[0] > $maxCount){
+ return null;
+ }
+
+ $j = $startJ + 1;
+ while($j < $maxJ && $this->bitMatrix->get($j, $centerI)){
+ $stateCount[2]++;
+ $j++;
+ }
+
+ if($j === $maxJ){
+ return null;
+ }
+
+ while($j < $maxJ && !$this->bitMatrix->get($j, $centerI) && $stateCount[3] < $maxCount){
+ $stateCount[3]++;
+ $j++;
+ }
+
+ if($j === $maxJ || $stateCount[3] >= $maxCount){
+ return null;
+ }
+
+ while($j < $maxJ && $this->bitMatrix->get($j, $centerI) && $stateCount[4] < $maxCount){
+ $stateCount[4]++;
+ $j++;
+ }
+
+ if($stateCount[4] >= $maxCount){
+ return null;
+ }
+
+ // If we found a finder-pattern-like section, but its size is significantly different than
+ // the original, assume it's a false positive
+ $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
+
+ if(5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal){
+ return null;
+ }
+
+ if(!$this->foundPatternCross($stateCount)){
+ return null;
+ }
+
+ return $this->centerFromEnd($stateCount, $j);
+ }
+
+ /**
+ * This is called when a horizontal scan finds a possible alignment pattern. It will
+ * cross check with a vertical scan, and if successful, will, ah, cross-cross-check
+ * with another horizontal scan. This is needed primarily to locate the real horizontal
+ * center of the pattern in cases of extreme skew.
+ * And then we cross-cross-cross check with another diagonal scan.
+ *
+ * If that succeeds the finder pattern location is added to a list that tracks
+ * the number of times each location has been nearly-matched as a finder pattern.
+ * Each additional find is more evidence that the location is in fact a finder
+ * pattern center
+ *
+ * @param int[] $stateCount reading state module counts from horizontal scan
+ * @param int $i row where finder pattern may be found
+ * @param int $j end of possible finder pattern in row
+ *
+ * @return bool if a finder pattern candidate was found this time
+ */
+ private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{
+ $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
+ $centerJ = $this->centerFromEnd($stateCount, $j);
+ $centerI = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal);
+
+ if($centerI !== null){
+ // Re-cross check
+ $centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal);
+ if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){
+ $estimatedModuleSize = $stateCountTotal / 7.0;
+ $found = false;
+
+ for($index = 0; $index < count($this->possibleCenters); $index++){
+ $center = $this->possibleCenters[$index];
+ // Look for about the same center and module size:
+ if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
+ $this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
+ $found = true;
+ break;
+ }
+ }
+
+ if(!$found){
+ $point = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
+ $this->possibleCenters[] = $point;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return int number of rows we could safely skip during scanning, based on the first
+ * two finder patterns that have been located. In some cases their position will
+ * allow us to infer that the third pattern must lie below a certain point farther
+ * down in the image.
+ */
+ private function findRowSkip():int{
+ $max = count($this->possibleCenters);
+
+ if($max <= 1){
+ return 0;
+ }
+
+ $firstConfirmedCenter = null;
+
+ foreach($this->possibleCenters as $center){
+
+ if($center->getCount() >= self::CENTER_QUORUM){
+
+ if($firstConfirmedCenter === null){
+ $firstConfirmedCenter = $center;
+ }
+ else{
+ // We have two confirmed centers
+ // How far down can we skip before resuming looking for the next
+ // pattern? In the worst case, only the difference between the
+ // difference in the x / y coordinates of the two centers.
+ // This is the case where you find top left last.
+ $this->hasSkipped = true;
+
+ return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) -
+ abs($firstConfirmedCenter->getY() - $center->getY())) / 2);
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * @return bool true if we have found at least 3 finder patterns that have been detected
+ * at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the
+ * candidates is "pretty similar"
+ */
+ private function haveMultiplyConfirmedCenters():bool{
+ $confirmedCount = 0;
+ $totalModuleSize = 0.0;
+ $max = count($this->possibleCenters);
+
+ foreach($this->possibleCenters as $pattern){
+ if($pattern->getCount() >= self::CENTER_QUORUM){
+ $confirmedCount++;
+ $totalModuleSize += $pattern->getEstimatedModuleSize();
+ }
+ }
+
+ if($confirmedCount < 3){
+ return false;
+ }
+ // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
+ // and that we need to keep looking. We detect this by asking if the estimated module sizes
+ // vary too much. We arbitrarily say that when the total deviation from average exceeds
+ // 5% of the total module size estimates, it's too much.
+ $average = $totalModuleSize / (float)$max;
+ $totalDeviation = 0.0;
+
+ foreach($this->possibleCenters as $pattern){
+ $totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
+ }
+
+ return $totalDeviation <= 0.05 * $totalModuleSize;
+ }
+
+ /**
+ * @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best {@link FinderPattern}s from our list of candidates. The "best" are
+ * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module
+ * size differs from the average among those patterns the least
+ * @throws \RuntimeException if 3 such finder patterns do not exist
+ */
+ private function selectBestPatterns():array{
+ $startSize = count($this->possibleCenters);
+
+ if($startSize < 3){
+ throw new RuntimeException('could not find enough finder patterns');
+ }
+
+ usort(
+ $this->possibleCenters,
+ fn(FinderPattern $a, FinderPattern $b) => $a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize()
+ );
+
+ $distortion = PHP_FLOAT_MAX;
+ $bestPatterns = [];
+
+ for($i = 0; $i < $startSize - 2; $i++){
+ $fpi = $this->possibleCenters[$i];
+ $minModuleSize = $fpi->getEstimatedModuleSize();
+
+ for($j = $i + 1; $j < $startSize - 1; $j++){
+ $fpj = $this->possibleCenters[$j];
+ $squares0 = $fpi->squaredDistance($fpj);
+
+ for($k = $j + 1; $k < $startSize; $k++){
+ $fpk = $this->possibleCenters[$k];
+ $maxModuleSize = $fpk->getEstimatedModuleSize();
+
+ // module size is not similar
+ if($maxModuleSize > $minModuleSize * 1.4){
+ continue;
+ }
+
+ $a = $squares0;
+ $b = $fpj->squaredDistance($fpk);
+ $c = $fpi->squaredDistance($fpk);
+
+ // sorts ascending - inlined
+ if($a < $b){
+ if($b > $c){
+ if($a < $c){
+ $temp = $b;
+ $b = $c;
+ $c = $temp;
+ }
+ else{
+ $temp = $a;
+ $a = $c;
+ $c = $b;
+ $b = $temp;
+ }
+ }
+ }
+ else{
+ if($b < $c){
+ if($a < $c){
+ $temp = $a;
+ $a = $b;
+ $b = $temp;
+ }
+ else{
+ $temp = $a;
+ $a = $b;
+ $b = $c;
+ $c = $temp;
+ }
+ }
+ else{
+ $temp = $a;
+ $a = $c;
+ $c = $temp;
+ }
+ }
+
+ // a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
+ // Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
+ // we need to check both two equal sides separately.
+ // The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
+ // from isosceles right triangle.
+ $d = abs($c - 2 * $b) + abs($c - 2 * $a);
+
+ if($d < $distortion){
+ $distortion = $d;
+ $bestPatterns = [$fpi, $fpj, $fpk];
+ }
+ }
+ }
+ }
+
+ if($distortion === PHP_FLOAT_MAX){
+ throw new RuntimeException('finder patterns may be too distorted');
+ }
+
+ return $bestPatterns;
+ }
+
+ /**
+ * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC
+ * and BC is less than AC, and the angle between BC and BA is less than 180 degrees.
+ *
+ * @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order
+ *
+ * @return \chillerlan\QRCode\Detector\FinderPattern[]
+ */
+ private function orderBestPatterns(array $patterns):array{
+
+ // Find distances between pattern centers
+ $zeroOneDistance = $patterns[0]->distance($patterns[1]);
+ $oneTwoDistance = $patterns[1]->distance($patterns[2]);
+ $zeroTwoDistance = $patterns[0]->distance($patterns[2]);
+
+ // Assume one closest to other two is B; A and C will just be guesses at first
+ if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){
+ [$pointB, $pointA, $pointC] = $patterns;
+ }
+ elseif($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance){
+ [$pointA, $pointB, $pointC] = $patterns;
+ }
+ else{
+ [$pointA, $pointC, $pointB] = $patterns;
+ }
+
+ // Use cross product to figure out whether A and C are correct or flipped.
+ // This asks whether BC x BA has a positive z component, which is the arrangement
+ // we want for A, B, C. If it's negative, then we've got it flipped around and
+ // should swap A and C.
+ if($this->crossProductZ($pointA, $pointB, $pointC) < 0.0){
+ $temp = $pointA;
+ $pointA = $pointC;
+ $pointC = $temp;
+ }
+
+ return [$pointA, $pointB, $pointC];
+ }
+
+ /**
+ * Returns the z component of the cross product between vectors BC and BA.
+ */
+ private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC):float{
+ $bX = $pointB->getX();
+ $bY = $pointB->getY();
+
+ return (($pointC->getX() - $bX) * ($pointA->getY() - $bY)) - (($pointC->getY() - $bY) * ($pointA->getX() - $bX));
+ }
+
+}
diff --git a/src/Detector/GridSampler.php b/src/Detector/GridSampler.php
new file mode 100644
index 000000000..7f03c9436
--- /dev/null
+++ b/src/Detector/GridSampler.php
@@ -0,0 +1,171 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use Exception, RuntimeException;
+use chillerlan\QRCode\Decoder\BitMatrix;
+use function array_fill, count, sprintf;
+
+/**
+ * Implementations of this class can, given locations of finder patterns for a QR code in an
+ * image, sample the right points in the image to reconstruct the QR code, accounting for
+ * perspective distortion. It is abstracted since it is relatively expensive and should be allowed
+ * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced
+ * Imaging library, but which may not be available in other environments such as J2ME, and vice
+ * versa.
+ *
+ * The implementation used can be controlled by calling {@link #setGridSampler(GridSampler)}
+ * with an instance of a class which implements this interface.
+ *
+ * @author Sean Owen
+ */
+final class GridSampler{
+
+ /**
+ *
Checks a set of points that have been transformed to sample points on an image against
+ * the image's dimensions to see if the point are even within the image.
+ *
+ * This method will actually "nudge" the endpoints back onto the image if they are found to be
+ * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder
+ * patterns in an image where the QR Code runs all the way to the image border.
+ *
+ * For efficiency, the method will check points from either end of the line until one is found
+ * to be within the image. Because the set of points are assumed to be linear, this is valid.
+ *
+ * @param \chillerlan\QRCode\Decoder\BitMatrix $bitMatrix image into which the points should map
+ * @param float[] $points actual points in x1,y1,...,xn,yn form
+ *
+ * @throws \RuntimeException if an endpoint is lies outside the image boundaries
+ */
+ private function checkAndNudgePoints(BitMatrix $bitMatrix, array $points):void{
+ $dimension = $bitMatrix->getDimension();
+ $nudged = true;
+ $max = count($points);
+
+ // Check and nudge points from start until we see some that are OK:
+ for($offset = 0; $offset < $max && $nudged; $offset += 2){
+ $x = (int)$points[$offset];
+ $y = (int)$points[$offset + 1];
+
+ if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
+ throw new RuntimeException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension));
+ }
+
+ $nudged = false;
+
+ if($x === -1){
+ $points[$offset] = 0.0;
+ $nudged = true;
+ }
+ elseif($x === $dimension){
+ $points[$offset] = $dimension - 1;
+ $nudged = true;
+ }
+ if($y === -1){
+ $points[$offset + 1] = 0.0;
+ $nudged = true;
+ }
+ elseif($y === $dimension){
+ $points[$offset + 1] = $dimension - 1;
+ $nudged = true;
+ }
+ }
+ // Check and nudge points from end:
+ $nudged = true;
+
+ for($offset = count($points) - 2; $offset >= 0 && $nudged; $offset -= 2){
+ $x = (int)$points[$offset];
+ $y = (int)$points[$offset + 1];
+
+ if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){
+ throw new RuntimeException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension));
+ }
+
+ $nudged = false;
+
+ if($x === -1){
+ $points[$offset] = 0.0;
+ $nudged = true;
+ }
+ elseif($x === $dimension){
+ $points[$offset] = $dimension - 1;
+ $nudged = true;
+ }
+ if($y === -1){
+ $points[$offset + 1] = 0.0;
+ $nudged = true;
+ }
+ elseif($y === $dimension){
+ $points[$offset + 1] = $dimension - 1;
+ $nudged = true;
+ }
+ }
+ }
+
+ /**
+ * Samples an image for a rectangular matrix of bits of the given dimension. The sampling
+ * transformation is determined by the coordinates of 4 points, in the original and transformed
+ * image space.
+ *
+ * @return \chillerlan\QRCode\Decoder\BitMatrix representing a grid of points sampled from the image within a region
+ * defined by the "from" parameters
+ * @throws \RuntimeException if image can't be sampled, for example, if the transformation defined
+ * by the given points is invalid or results in sampling outside the image boundaries
+ */
+ public function sampleGrid(BitMatrix $image, int $dimension, PerspectiveTransform $transform):BitMatrix{
+
+ if($dimension <= 0){
+ throw new RuntimeException('invalid matrix size');
+ }
+
+ $bits = new BitMatrix($dimension);
+ $points = array_fill(0, 2 * $dimension, 0.0);
+
+ for($y = 0; $y < $dimension; $y++){
+ $max = count($points);
+ $iValue = (float)$y + 0.5;
+
+ for($x = 0; $x < $max; $x += 2){
+ $points[$x] = (float)($x / 2) + 0.5;
+ $points[$x + 1] = $iValue;
+ }
+
+ $transform->transformPoints($points);
+ // Quick check to see if points transformed to something inside the image;
+ // sufficient to check the endpoints
+ $this->checkAndNudgePoints($image, $points);
+
+ try{
+ for($x = 0; $x < $max; $x += 2){
+ if($image->get((int)$points[$x], (int)$points[$x + 1])){
+ // Black(-ish) pixel
+ $bits->set($x / 2, $y);
+ }
+ }
+ }
+ catch(Exception $aioobe){//ArrayIndexOutOfBoundsException
+ // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting
+ // transform gets "twisted" such that it maps a straight line of points to a set of points
+ // whose endpoints are in bounds, but others are not. There is probably some mathematical
+ // way to detect this about the transformation that I don't know yet.
+ // This results in an ugly runtime exception despite our clever checks above -- can't have
+ // that. We could check each point's coordinates but that feels duplicative. We settle for
+ // catching and wrapping ArrayIndexOutOfBoundsException.
+ throw new RuntimeException('ArrayIndexOutOfBoundsException');
+ }
+
+ }
+
+ return $bits;
+ }
+
+}
diff --git a/src/Detector/PerspectiveTransform.php b/src/Detector/PerspectiveTransform.php
new file mode 100644
index 000000000..cabce5911
--- /dev/null
+++ b/src/Detector/PerspectiveTransform.php
@@ -0,0 +1,152 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use function count;
+
+/**
+ * This class implements a perspective transform in two dimensions. Given four source and four
+ * destination points, it will compute the transformation implied between them. The code is based
+ * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.
+ *
+ * @author Sean Owen
+ */
+final class PerspectiveTransform{
+
+ private float $a11;
+ private float $a12;
+ private float $a13;
+ private float $a21;
+ private float $a22;
+ private float $a23;
+ private float $a31;
+ private float $a32;
+ private float $a33;
+
+ private function __construct(
+ float $a11, float $a21, float $a31,
+ float $a12, float $a22, float $a32,
+ float $a13, float $a23, float $a33
+ ){
+ $this->a11 = $a11;
+ $this->a12 = $a12;
+ $this->a13 = $a13;
+ $this->a21 = $a21;
+ $this->a22 = $a22;
+ $this->a23 = $a23;
+ $this->a31 = $a31;
+ $this->a32 = $a32;
+ $this->a33 = $a33;
+ }
+
+ public static function quadrilateralToQuadrilateral(
+ float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3,
+ float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p
+ ):PerspectiveTransform{
+
+ $qToS = self::quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3);
+ $sToQ = self::squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p);
+
+ return $sToQ->times($qToS);
+ }
+
+ public static function quadrilateralToSquare(
+ float $x0, float $y0, float $x1, float $y1,
+ float $x2, float $y2, float $x3, float $y3
+ ):PerspectiveTransform{
+ // Here, the adjoint serves as the inverse:
+ return self::squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)->buildAdjoint();
+ }
+
+ public function buildAdjoint():PerspectiveTransform{
+ // Adjoint is the transpose of the cofactor matrix:
+ return new self(
+ $this->a22 * $this->a33 - $this->a23 * $this->a32,
+ $this->a23 * $this->a31 - $this->a21 * $this->a33,
+ $this->a21 * $this->a32 - $this->a22 * $this->a31,
+ $this->a13 * $this->a32 - $this->a12 * $this->a33,
+ $this->a11 * $this->a33 - $this->a13 * $this->a31,
+ $this->a12 * $this->a31 - $this->a11 * $this->a32,
+ $this->a12 * $this->a23 - $this->a13 * $this->a22,
+ $this->a13 * $this->a21 - $this->a11 * $this->a23,
+ $this->a11 * $this->a22 - $this->a12 * $this->a21
+ );
+ }
+
+ public static function squareToQuadrilateral(
+ float $x0, float $y0, float $x1, float $y1,
+ float $x2, float $y2, float $x3, float $y3
+ ):PerspectiveTransform{
+ $dx3 = $x0 - $x1 + $x2 - $x3;
+ $dy3 = $y0 - $y1 + $y2 - $y3;
+
+ if($dx3 === 0.0 && $dy3 === 0.0){
+ // Affine
+ return new self($x1 - $x0, $x2 - $x1, $x0, $y1 - $y0, $y2 - $y1, $y0, 0.0, 0.0, 1.0);
+ }
+ else{
+ $dx1 = $x1 - $x2;
+ $dx2 = $x3 - $x2;
+ $dy1 = $y1 - $y2;
+ $dy2 = $y3 - $y2;
+ $denominator = $dx1 * $dy2 - $dx2 * $dy1;
+ $a13 = ($dx3 * $dy2 - $dx2 * $dy3) / $denominator;
+ $a23 = ($dx1 * $dy3 - $dx3 * $dy1) / $denominator;
+
+ return new self(
+ $x1 - $x0 + $a13 * $x1, $x3 - $x0 + $a23 * $x3, $x0,
+ $y1 - $y0 + $a13 * $y1, $y3 - $y0 + $a23 * $y3, $y0,
+ $a13, $a23, 1.0
+ );
+ }
+ }
+
+ public function times(PerspectiveTransform $other):PerspectiveTransform{
+ return new self(
+ $this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13,
+ $this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23,
+ $this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33,
+ $this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13,
+ $this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23,
+ $this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33,
+ $this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13,
+ $this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23,
+ $this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33
+ );
+ }
+
+ public function transformPoints(array &$xValues, array &$yValues = null):void{
+ $max = count($xValues);
+
+ if($yValues !== null){
+
+ for($i = 0; $i < $max; $i++){
+ $x = $xValues[$i];
+ $y = $yValues[$i];
+ $denominator = $this->a13 * $x + $this->a23 * $y + $this->a33;
+ $xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator;
+ $yValues[$i] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator;
+ }
+
+ return;
+ }
+
+ for($i = 0; $i < $max; $i += 2){
+ $x = $xValues[$i];
+ $y = $xValues[$i + 1];
+ $denominator = $this->a13 * $x + $this->a23 * $y + $this->a33;
+ $xValues[$i] = ($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator;
+ $xValues[$i + 1] = ($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator;
+ }
+ }
+
+}
diff --git a/src/Detector/ResultPoint.php b/src/Detector/ResultPoint.php
new file mode 100644
index 000000000..f57dbaf25
--- /dev/null
+++ b/src/Detector/ResultPoint.php
@@ -0,0 +1,61 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ */
+
+namespace chillerlan\QRCode\Detector;
+
+use function abs;
+
+/**
+ * Encapsulates a point of interest in an image containing a barcode. Typically, this
+ * would be the location of a finder pattern or the corner of the barcode, for example.
+ *
+ * @author Sean Owen
+ */
+abstract class ResultPoint{
+
+ protected float $x;
+ protected float $y;
+ protected float $estimatedModuleSize;
+
+ public function __construct(float $x, float $y, float $estimatedModuleSize){
+ $this->x = $x;
+ $this->y = $y;
+ $this->estimatedModuleSize = $estimatedModuleSize;
+ }
+
+ public function getX():float{
+ return (float)$this->x;
+ }
+
+ public function getY():float{
+ return (float)$this->y;
+ }
+
+ public function getEstimatedModuleSize():float{
+ return $this->estimatedModuleSize;
+ }
+
+ /**
+ * Determines if this finder pattern "about equals" a finder pattern at the stated
+ * position and size -- meaning, it is at nearly the same center with nearly the same size.
+ */
+ public function aboutEquals(float $moduleSize, float $i, float $j):bool{
+
+ if(abs($i - $this->y) <= $moduleSize && abs($j - $this->x) <= $moduleSize){
+ $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize);
+
+ return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize;
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/QRCodeReader.php b/src/QRCodeReader.php
new file mode 100644
index 000000000..92e53185f
--- /dev/null
+++ b/src/QRCodeReader.php
@@ -0,0 +1,88 @@
+
+ * @copyright 2021 Smiley
+ * @license Apache-2.0
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCode;
+
+use Imagick, InvalidArgumentException;
+use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource};
+use function extension_loaded, file_exists, file_get_contents, imagecreatefromstring, is_file, is_readable;
+
+final class QRCodeReader{
+
+ private bool $useImagickIfAvailable;
+
+ public function __construct(bool $useImagickIfAvailable = true){
+ $this->useImagickIfAvailable = $useImagickIfAvailable && extension_loaded('imagick');
+ }
+
+ /**
+ * @param \Imagick|\GdImage|resource $im
+ *
+ * @return \chillerlan\QRCode\Decoder\DecoderResult
+ * @phan-suppress PhanUndeclaredTypeParameter (GdImage)
+ */
+ protected function decode($im):DecoderResult{
+
+ $source = $this->useImagickIfAvailable
+ ? new IMagickLuminanceSource($im)
+ : new GDLuminanceSource($im);
+
+ return (new Decoder)->decode($source);
+ }
+
+ /**
+ * @param string $imgFilePath
+ *
+ * @return \chillerlan\QRCode\Decoder\DecoderResult
+ */
+ public function readFile(string $imgFilePath):DecoderResult{
+
+ if(!file_exists($imgFilePath) || !is_file($imgFilePath) || !is_readable($imgFilePath)){
+ throw new InvalidArgumentException('invalid file: '.$imgFilePath);
+ }
+
+ $im = $this->useImagickIfAvailable
+ ? new Imagick($imgFilePath)
+ : imagecreatefromstring(file_get_contents($imgFilePath));
+
+ return $this->decode($im);
+ }
+
+ /**
+ * @param string $imgBlob
+ *
+ * @return \chillerlan\QRCode\Decoder\DecoderResult
+ */
+ public function readBlob(string $imgBlob):DecoderResult{
+
+ if($this->useImagickIfAvailable){
+ $im = new Imagick;
+ $im->readImageBlob($imgBlob);
+ }
+ else{
+ $im = imagecreatefromstring($imgBlob);
+ }
+
+ return $this->decode($im);
+ }
+
+ /**
+ * @param \Imagick|\GdImage|resource $imgSource
+ *
+ * @return \chillerlan\QRCode\Decoder\DecoderResult
+ */
+ public function readResource($imgSource):DecoderResult{
+ return $this->decode($imgSource);
+ }
+
+}
diff --git a/src/includes.php b/src/includes.php
new file mode 100644
index 000000000..0e326fcaa
--- /dev/null
+++ b/src/includes.php
@@ -0,0 +1,16 @@
+
+ * @copyright 2021 smiley
+ * @license MIT
+ */
+
+namespace chillerlan\QRCode;
+
+// @codeCoverageIgnoreStart
+if(!\defined('QRCODE_DECODER_INCLUDES')){
+ require_once __DIR__.'/Common/functions.php';
+}
+
+// @codeCoverageIgnoreEnd
diff --git a/tests/QRCodeReaderTest.php b/tests/QRCodeReaderTest.php
new file mode 100644
index 000000000..2fcf06158
--- /dev/null
+++ b/tests/QRCodeReaderTest.php
@@ -0,0 +1,121 @@
+
+ * @copyright 2021 Smiley
+ * @license MIT
+ *
+ * @noinspection PhpComposerExtensionStubsInspection
+ */
+
+namespace chillerlan\QRCodeTest;
+
+use Exception;
+use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
+use chillerlan\QRCode\{QRCode, QROptions, QRCodeReader};
+use PHPUnit\Framework\TestCase;
+use function extension_loaded, range, str_repeat, substr;
+
+/**
+ * Tests the QR Code reader
+ */
+class QRCodeReaderTest extends TestCase{
+
+ // https://www.bobrosslipsum.com/
+ protected const loremipsum = 'Just let this happen. We just let this flow right out of our minds. '
+ .'Anyone can paint. We touch the canvas, the canvas takes what it wants. From all of us here, '
+ .'I want to wish you happy painting and God bless, my friends. A tree cannot be straight if it has a crooked trunk. '
+ .'You have to make almighty decisions when you\'re the creator. I guess that would be considered a UFO. '
+ .'A big cotton ball in the sky. I\'m gonna add just a tiny little amount of Prussian Blue. '
+ .'They say everything looks better with odd numbers of things. But sometimes I put even numbers—just '
+ .'to upset the critics. We\'ll lay all these little funky little things in there. ';
+
+ public function qrCodeProvider():array{
+ return [
+ 'helloworld' => ['hello_world.png', 'Hello world!'],
+ // covers mirroring
+ 'mirrored' => ['hello_world_mirrored.png', 'Hello world!'],
+ // data modes
+ 'byte' => ['byte.png', 'https://smiley.codes/qrcode/'],
+ 'numeric' => ['numeric.png', '123456789012345678901234567890'],
+ 'alphanum' => ['alphanum.png', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:'],
+ 'kanji' => ['kanji.png', '茗荷茗荷茗荷茗荷'],
+ // covers most of ReedSolomonDecoder
+ 'damaged' => ['damaged.png', 'https://smiley.codes/qrcode/'],
+ // covers Binarizer::getHistogramBlackMatrix()
+ 'smol' => ['smol.png', 'https://smiley.codes/qrcode/'],
+ ];
+ }
+
+ /**
+ * @dataProvider qrCodeProvider
+ */
+ public function testReaderGD(string $img, string $expected):void{
+ $reader = new QRCodeReader(false);
+
+ self::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img));
+ }
+
+ /**
+ * @dataProvider qrCodeProvider
+ */
+ public function testReaderImagick(string $img, string $expected):void{
+
+ if(!extension_loaded('imagick')){
+ self::markTestSkipped('imagick not installed');
+ }
+
+ $reader = new QRCodeReader(true);
+
+ self::assertSame($expected, (string)$reader->readFile(__DIR__.'/qrcodes/'.$img));
+ }
+
+ public function dataTestProvider():array{
+ $data = [];
+ $str = str_repeat(self::loremipsum, 5);
+
+ foreach(range(1, 40) as $v){
+ $version = new Version($v);
+
+ foreach(EccLevel::MODES as $ecc => $_){
+ $eccLevel = new EccLevel($ecc);
+
+ $data['version: '.$version->getVersionNumber().$eccLevel->__toString()] = [
+ $version,
+ $eccLevel,
+ substr($str, 0, $version->getMaxLengthForMode(Mode::DATA_BYTE, $eccLevel))
+ ];
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @dataProvider dataTestProvider
+ */
+ public function testReadData(Version $version, EccLevel $ecc, string $expected):void{
+ $options = new QROptions;
+# $options->imageTransparent = false;
+ $options->eccLevel = $ecc->getLevel();
+ $options->version = $version->getVersionNumber();
+ $options->imageBase64 = false;
+ $options->scale = 1; // what's interesting is that a smaller scale seems to produce less errors???
+
+ $imagedata = (new QRCode($options))->render($expected);
+
+ try{
+ $result = (new QRCodeReader(true))->readBlob($imagedata);
+ }
+ catch(Exception $e){
+ self::markTestSkipped($version->getVersionNumber().$ecc->__toString().': '.$e->getMessage());
+ }
+
+ self::assertSame($expected, $result->getText());
+ self::assertSame($version->getVersionNumber(), $result->getVersion()->getVersionNumber());
+ self::assertSame($ecc->getLevel(), $result->getEccLevel()->getLevel());
+ }
+
+}
diff --git a/tests/qrcodes/alphanum.png b/tests/qrcodes/alphanum.png
new file mode 100644
index 0000000000000000000000000000000000000000..fdcf4f289da99534c5e8f7b7e6b971921ca01cf6
GIT binary patch
literal 383
zcmV-_0f7FAP)4R?8B?FbE7z|NoccAn&3I>Ms}6M~
zP#{DsiY?}Wo;JIrFqYO;=A-|{3t8x59_YUE2z{p
zJy>is)F(?L=Ni~hp}kGp(36yntdjB=L5t-HE>d=aL7v0Nm
zS)1G4h&2OI56$clUSyBi$!^F$^K#j{bYIhuN+=XUKP7#Vx0!3lP$mn$WG9*APZk*d
zE9CL6uHCy!2Uo1AHnW&G=-l(}x5^FivSXe7dNf8wyx+n3s0vJx^VdnU+t^Kng;VLT
zKd^SjeoqPYnDh5(DH%)aqcxDjoX{6kuKc2S=yojnik?*}tKjq1pfNY;nfCH)39CeQ
z`9g%OIqVjcz1_4o(PT%T76dOQD3@*9La@B}tk%YKYX#ot%>fO4nrHv%vIHZNwb4Z_
zG;(&FiecUK$U4<(ESe-3D1;Ajr6(w#P8nYxc?--d3ICdp~i|md;2>D
zNf!UQSl_+z#J~^?xMw;3NA7@<9)5mo1!+LOri|8GSJCCyECCFoDNBtNpAa&)8*s-b
z7$zFq(Qq6c#l#&?*Ma%Y2y&ms%%J|yIY?KG4=n{VAdAW<<2aUfE$#P#a>fJ_mYK7%
zBsZN1h-FCX5F#o((Io$E%n>FFAta1pL|<*8Xdg=`R19CTDy?iS0bqta;tqLmUwDdC
z(M51X_g*}qV+?;Rj+HR^18Q|ja@Nv%Z>Hl*yBKLP^%?eOi%bChA1*M)bW2F^0JECu
z=5_8%X;s?*?E&wu7r+!Dc`3ahm|@
zQs~~Z8Gi2+=~ak-_v{(V!~1TCFUVFPt2~0_f=7}|>jrDNBkHhY*Ds@_B@yE)3yX#3r
z)e&9Jk+|*@VUCnGeS94IYGx7)^Q{%+9`p6wnf=;|NX+SKS!;&tT5i>sY?&O{g>nCd
zJgjnspEQVzX$z7x1y}(5wsem$zo%^XqZ`w@%sw=c(yE20!O>UgfNJs!dQmuj!#N%5
z({es>FiNUl_;Hd$(T4egw!Yq2JY0LvoDZIq&N+x0NrH`9@>~s}6$(5#S*>Txh_*fJ
zHx$~Dai&S~nsk`EUSorC*bh)Flch{6sE=?r`QpLP-c?ZpZqKfT)nvOF_ZAqw5&6W<
z1k21a1f?CqW6n`TVyExpUP@W-#=!)xUe4s@jG>5cD%Ml$f}B$|{doEcbIIWLAEs2t^$SEg9Et+;$R*bJb5))`&;UT{h|
zPCUS+H-R#duW06wm?-tp%o3^3*2^wJl)-~>pmPlxu243d|q?Jg`Uw|T<*EplA
zQn_s*J~%BlY|}u3QM;w<&Xe+HOS1)6Y^K5ou^4GD4V4e04m}+4NYiXg6tXR{D|7dp
zow|8mi*U3!FTFzTqUjUe;m`~o%Ba7OMTQKd#lCB&+;W8jJR|@B*J?VqXvQR-9lrlM
zqVO^o+4r|H_J@Crv>Cy%T|zye-yr>p!7n+Q`eFMb)Z$%l4K6bt*4>j^kHOYl!wuw>
zFr+b0S%(Q~*;r)zInVYS^|CeM=#sq+e_9StMJH8!rlvii({o_a+PYl94RL(UqgLjg
zo%Oyb9@YiK6zW5<1iOvQ@mF|A)4S)3Vn&7@)ifc-h;aB7swiVc
zU8-3|9L37_JwdlVQuZ?{bKt>ktOIWADv)bwFY3Ef&L_V5t@}NFaW!Di?FhDD;vq5C
zPyrYLG^wZ)prQNgZ+$D(v~vSriCf5N+NzGvQa78x#;G9arEF9vwlx;3yp!FEj4NIH
zT^J02^h;xb$y@xTCCuac0?UJJ)0wIt$scUhk~V>TeL=`ce%C3fdoP2a1jLEOZ{>);
z8}B+9i6g%iFlQcgM3RZziH4(DUEM><>w*6}lANc*NMR89b~a)jVd0eWzeqY0=MLEJ
mfh0<{3#iM##>PKGWL^KXm$ua&n7w*%fdYJjyvbgtFZ~5)FM?D6
literal 0
HcmV?d00001
diff --git a/tests/qrcodes/damaged.png b/tests/qrcodes/damaged.png
new file mode 100644
index 0000000000000000000000000000000000000000..feeb7443efeee44979a090d86a7e49aaa7c4ee39
GIT binary patch
literal 8646
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
G`2PS;r}*6f
literal 0
HcmV?d00001
diff --git a/tests/qrcodes/hello_world.png b/tests/qrcodes/hello_world.png
new file mode 100644
index 0000000000000000000000000000000000000000..3578ea0cc1bd88805bad82da46582cb3af13c163
GIT binary patch
literal 3121
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
AivR!s
literal 0
HcmV?d00001
diff --git a/tests/qrcodes/hello_world_mirrored.png b/tests/qrcodes/hello_world_mirrored.png
new file mode 100644
index 0000000000000000000000000000000000000000..d871f1dbf8da31bf339e3dd4fa710270807aa881
GIT binary patch
literal 12319
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
C@0E}M
literal 0
HcmV?d00001
diff --git a/tests/qrcodes/kanji.png b/tests/qrcodes/kanji.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ac7990b2661796ec8a46d5a26eaa7e01edd069d
GIT binary patch
literal 383
zcmV-_0f7FAP)t%ynzs9>4UA)WV^@_)oDn@tg`eyC<
z>riJ<>1zGSTX_g++?$Khb#yh25b_@EcZgSZF}e@VfY%((9#NCM+JiHA{nG2cUT-c&
zH#G0sbAqf|$x^$%d+oXd!K5R*j4pJ&=UF%{JF*?27~RchWJsSwPINT&qD%6g%^;4;
z*V|5HDQ9H1`$kBgN>}p)dq(qiyN-0%<_i0+wf%50x|?T)h#KxDw#~c0Q8BtV-Avtd
dGj-F=)E5Rv_Gf{2&pH4A002ovPDHLkV1lwswUqz>
literal 0
HcmV?d00001
diff --git a/tests/qrcodes/numeric.png b/tests/qrcodes/numeric.png
new file mode 100644
index 0000000000000000000000000000000000000000..24b2b88ce2870e362548c353c739684323a932a5
GIT binary patch
literal 388
zcmV-~0ek+5P)4RzVKJFbr!E|NnA0Op09FNegHk$N_a*6UTGhF(YE!
z9FMyJ{s`lV7jsVC5fO7v{KZF2mdBuHnV+SzTvoppHmk(YLiWx{R>s?UB%vg+jZ=(>
zP%*3$8s=STcc`ru-)5BG5rb+L%q#>>CL6QJ^lX>V?Z5Ame(Z)C9@HrcLeTGyghi3X!WrN&8oI+Hq|Z;`MH&3fvz=h1vctQ$t{aF5X(rCP`yGit-r
i{}$iGzx?0*1;z_Nk77}(p>JXU00004R?7~lwLsD2>-OagWmt)Sg(
z?KtklXb$%3ct~2dwwtWlc*~H;1CXGfC0(cjBNOu~$7P{_kSyQ*EXjwR+?lJ
z7