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