* @copyright 2021 Smiley * @license Apache-2.0 */ namespace WCPOS\Vendor\chillerlan\QRCode\Decoder; use WCPOS\Vendor\chillerlan\QRCode\Common\EccLevel; use WCPOS\Vendor\chillerlan\QRCode\Common\MaskPattern; use WCPOS\Vendor\chillerlan\QRCode\Common\Version; use WCPOS\Vendor\chillerlan\QRCode\Data\QRCodeDataException; use WCPOS\Vendor\chillerlan\QRCode\Data\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 0x762, // 0000011101100010 0x255, // 0000001001010101 0xd0c, // 0000110100001100 0x83b, // 0000100000111011 0x355f, // 0011010101011111 0x3068, // 0011000001101000 0x3f31, // 0011111100110001 0x3a06, // 0011101000000110 0x24b4, // 0010010010110100 0x2183, // 0010000110000011 0x2eda, // 0010111011011010 0x2bed, ]; 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); // 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 & 0x3); // Bits 3,4 $this->maskPattern = new MaskPattern($formatInfo & 0x7); // 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) & 0xf]; } 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'); } }