431 lines
12 KiB
PHP
431 lines
12 KiB
PHP
<?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');
|
|
}
|
|
|
|
}
|