BitMatrix.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. <?php
  2. /**
  3. * Class BitMatrix
  4. *
  5. * @created 17.01.2021
  6. * @author ZXing Authors
  7. * @author Smiley <smiley@chillerlan.net>
  8. * @copyright 2021 Smiley
  9. * @license Apache-2.0
  10. */
  11. namespace chillerlan\QRCode\Decoder;
  12. use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Version};
  13. use chillerlan\QRCode\Data\{QRCodeDataException, QRMatrix};
  14. use function array_fill, array_map, array_reverse, count;
  15. use const PHP_INT_MAX, PHP_INT_SIZE;
  16. /**
  17. * Extended QRMatrix to map read data from the Binarizer
  18. */
  19. final class BitMatrix extends QRMatrix{
  20. /**
  21. * See ISO 18004:2006, Annex C, Table C.1
  22. *
  23. * [data bits, sequence after masking]
  24. */
  25. private const DECODE_LOOKUP = [
  26. 0x5412, // 0101010000010010
  27. 0x5125, // 0101000100100101
  28. 0x5E7C, // 0101111001111100
  29. 0x5B4B, // 0101101101001011
  30. 0x45F9, // 0100010111111001
  31. 0x40CE, // 0100000011001110
  32. 0x4F97, // 0100111110010111
  33. 0x4AA0, // 0100101010100000
  34. 0x77C4, // 0111011111000100
  35. 0x72F3, // 0111001011110011
  36. 0x7DAA, // 0111110110101010
  37. 0x789D, // 0111100010011101
  38. 0x662F, // 0110011000101111
  39. 0x6318, // 0110001100011000
  40. 0x6C41, // 0110110001000001
  41. 0x6976, // 0110100101110110
  42. 0x1689, // 0001011010001001
  43. 0x13BE, // 0001001110111110
  44. 0x1CE7, // 0001110011100111
  45. 0x19D0, // 0001100111010000
  46. 0x0762, // 0000011101100010
  47. 0x0255, // 0000001001010101
  48. 0x0D0C, // 0000110100001100
  49. 0x083B, // 0000100000111011
  50. 0x355F, // 0011010101011111
  51. 0x3068, // 0011000001101000
  52. 0x3F31, // 0011111100110001
  53. 0x3A06, // 0011101000000110
  54. 0x24B4, // 0010010010110100
  55. 0x2183, // 0010000110000011
  56. 0x2EDA, // 0010111011011010
  57. 0x2BED, // 0010101111101101
  58. ];
  59. private const FORMAT_INFO_MASK_QR = 0x5412; // 0101010000010010
  60. /**
  61. * This flag has effect only on the copyVersionBit() method.
  62. * Before proceeding with readCodewords() the resetInfo() method should be called.
  63. */
  64. private bool $mirror = false;
  65. /**
  66. * @noinspection PhpMissingParentConstructorInspection
  67. */
  68. public function __construct(int $dimension){
  69. $this->moduleCount = $dimension;
  70. $this->matrix = array_fill(0, $this->moduleCount, array_fill(0, $this->moduleCount, $this::M_NULL));
  71. }
  72. /**
  73. * Resets the current version info in order to attempt another reading
  74. */
  75. public function resetVersionInfo():self{
  76. $this->version = null;
  77. $this->eccLevel = null;
  78. $this->maskPattern = null;
  79. return $this;
  80. }
  81. /**
  82. * Mirror the bit matrix diagonally in order to attempt a second reading.
  83. */
  84. public function mirrorDiagonal():self{
  85. $this->mirror = !$this->mirror;
  86. // mirror vertically
  87. $matrix = array_reverse($this->matrix);
  88. // rotate by 90 degrees clockwise
  89. $this->matrix = array_map(fn(...$a) => array_reverse($a), ...$matrix);
  90. return $this;
  91. }
  92. /**
  93. * Reads the bits in the BitMatrix representing the finder pattern in the
  94. * correct order in order to reconstruct the codewords bytes contained within the
  95. * QR Code. Throws if the exact number of bytes expected is not read.
  96. *
  97. * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
  98. */
  99. public function readCodewords():array{
  100. $this
  101. ->readFormatInformation()
  102. ->readVersion()
  103. ->mask($this->maskPattern) // reverse the mask pattern
  104. ;
  105. // invoke a fresh matrix with only the function & format patterns to compare against
  106. $matrix = (new QRMatrix($this->version, $this->eccLevel))
  107. ->initFunctionalPatterns()
  108. ->setFormatInfo($this->maskPattern)
  109. ;
  110. $result = [];
  111. $byte = 0;
  112. $bitsRead = 0;
  113. $direction = true;
  114. // Read columns in pairs, from right to left
  115. for($i = ($this->moduleCount - 1); $i > 0; $i -= 2){
  116. // Skip whole column with vertical alignment pattern;
  117. // saves time and makes the other code proceed more cleanly
  118. if($i === 6){
  119. $i--;
  120. }
  121. // Read alternatingly from bottom to top then top to bottom
  122. for($count = 0; $count < $this->moduleCount; $count++){
  123. $y = ($direction) ? ($this->moduleCount - 1 - $count) : $count;
  124. for($col = 0; $col < 2; $col++){
  125. $x = ($i - $col);
  126. // Ignore bits covered by the function pattern
  127. if($matrix->get($x, $y) !== $this::M_NULL){
  128. continue;
  129. }
  130. $bitsRead++;
  131. $byte <<= 1;
  132. if($this->check($x, $y)){
  133. $byte |= 1;
  134. }
  135. // If we've made a whole byte, save it off
  136. if($bitsRead === 8){
  137. $result[] = $byte;
  138. $bitsRead = 0;
  139. $byte = 0;
  140. }
  141. }
  142. }
  143. $direction = !$direction; // switch directions
  144. }
  145. if(count($result) !== $this->version->getTotalCodewords()){
  146. throw new QRCodeDecoderException('result count differs from total codewords for version');
  147. }
  148. // bytes encoded within the QR Code
  149. return $result;
  150. }
  151. /**
  152. * Reads format information from one of its two locations within the QR Code.
  153. * Throws if both format information locations cannot be parsed as the valid encoding of format information.
  154. *
  155. * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
  156. */
  157. private function readFormatInformation():self{
  158. if($this->eccLevel !== null && $this->maskPattern !== null){
  159. return $this;
  160. }
  161. // Read top-left format info bits
  162. $formatInfoBits1 = 0;
  163. for($i = 0; $i < 6; $i++){
  164. $formatInfoBits1 = $this->copyVersionBit($i, 8, $formatInfoBits1);
  165. }
  166. // ... and skip a bit in the timing pattern ...
  167. $formatInfoBits1 = $this->copyVersionBit(7, 8, $formatInfoBits1);
  168. $formatInfoBits1 = $this->copyVersionBit(8, 8, $formatInfoBits1);
  169. $formatInfoBits1 = $this->copyVersionBit(8, 7, $formatInfoBits1);
  170. // ... and skip a bit in the timing pattern ...
  171. for($j = 5; $j >= 0; $j--){
  172. $formatInfoBits1 = $this->copyVersionBit(8, $j, $formatInfoBits1);
  173. }
  174. // Read the top-right/bottom-left pattern too
  175. $formatInfoBits2 = 0;
  176. $jMin = ($this->moduleCount - 7);
  177. for($j = ($this->moduleCount - 1); $j >= $jMin; $j--){
  178. $formatInfoBits2 = $this->copyVersionBit(8, $j, $formatInfoBits2);
  179. }
  180. for($i = ($this->moduleCount - 8); $i < $this->moduleCount; $i++){
  181. $formatInfoBits2 = $this->copyVersionBit($i, 8, $formatInfoBits2);
  182. }
  183. $formatInfo = $this->doDecodeFormatInformation($formatInfoBits1, $formatInfoBits2);
  184. if($formatInfo === null){
  185. // Should return null, but, some QR codes apparently do not mask this info.
  186. // Try again by actually masking the pattern first.
  187. $formatInfo = $this->doDecodeFormatInformation(
  188. ($formatInfoBits1 ^ $this::FORMAT_INFO_MASK_QR),
  189. ($formatInfoBits2 ^ $this::FORMAT_INFO_MASK_QR)
  190. );
  191. // still nothing???
  192. if($formatInfo === null){
  193. throw new QRCodeDecoderException('failed to read format info'); // @codeCoverageIgnore
  194. }
  195. }
  196. $this->eccLevel = new EccLevel(($formatInfo >> 3) & 0x03); // Bits 3,4
  197. $this->maskPattern = new MaskPattern($formatInfo & 0x07); // Bottom 3 bits
  198. return $this;
  199. }
  200. /**
  201. *
  202. */
  203. private function copyVersionBit(int $i, int $j, int $versionBits):int{
  204. $bit = $this->mirror
  205. ? $this->check($j, $i)
  206. : $this->check($i, $j);
  207. return ($bit) ? (($versionBits << 1) | 0x1) : ($versionBits << 1);
  208. }
  209. /**
  210. * Returns information about the format it specifies, or null if it doesn't seem to match any known pattern
  211. */
  212. private function doDecodeFormatInformation(int $maskedFormatInfo1, int $maskedFormatInfo2):?int{
  213. $bestDifference = PHP_INT_MAX;
  214. $bestFormatInfo = 0;
  215. // Find the int in FORMAT_INFO_DECODE_LOOKUP with the fewest bits differing
  216. foreach($this::DECODE_LOOKUP as $maskedBits => $dataBits){
  217. if($maskedFormatInfo1 === $dataBits || $maskedFormatInfo2 === $dataBits){
  218. // Found an exact match
  219. return $maskedBits;
  220. }
  221. $bitsDifference = $this->numBitsDiffering($maskedFormatInfo1, $dataBits);
  222. if($bitsDifference < $bestDifference){
  223. $bestFormatInfo = $maskedBits;
  224. $bestDifference = $bitsDifference;
  225. }
  226. if($maskedFormatInfo1 !== $maskedFormatInfo2){
  227. // also try the other option
  228. $bitsDifference = $this->numBitsDiffering($maskedFormatInfo2, $dataBits);
  229. if($bitsDifference < $bestDifference){
  230. $bestFormatInfo = $maskedBits;
  231. $bestDifference = $bitsDifference;
  232. }
  233. }
  234. }
  235. // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
  236. if($bestDifference <= 3){
  237. return $bestFormatInfo;
  238. }
  239. return null;
  240. }
  241. /**
  242. * Reads version information from one of its two locations within the QR Code.
  243. * Throws if both version information locations cannot be parsed as the valid encoding of version information.
  244. *
  245. * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException
  246. * @noinspection DuplicatedCode
  247. */
  248. private function readVersion():self{
  249. if($this->version !== null){
  250. return $this;
  251. }
  252. $provisionalVersion = (($this->moduleCount - 17) / 4);
  253. // no version info if v < 7
  254. if($provisionalVersion < 7){
  255. $this->version = new Version($provisionalVersion);
  256. return $this;
  257. }
  258. // Read top-right version info: 3 wide by 6 tall
  259. $versionBits = 0;
  260. $ijMin = ($this->moduleCount - 11);
  261. for($y = 5; $y >= 0; $y--){
  262. for($x = ($this->moduleCount - 9); $x >= $ijMin; $x--){
  263. $versionBits = $this->copyVersionBit($x, $y, $versionBits);
  264. }
  265. }
  266. $this->version = $this->decodeVersionInformation($versionBits);
  267. if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
  268. return $this;
  269. }
  270. // Hmm, failed. Try bottom left: 6 wide by 3 tall
  271. $versionBits = 0;
  272. for($x = 5; $x >= 0; $x--){
  273. for($y = ($this->moduleCount - 9); $y >= $ijMin; $y--){
  274. $versionBits = $this->copyVersionBit($x, $y, $versionBits);
  275. }
  276. }
  277. $this->version = $this->decodeVersionInformation($versionBits);
  278. if($this->version !== null && $this->version->getDimension() === $this->moduleCount){
  279. return $this;
  280. }
  281. throw new QRCodeDecoderException('failed to read version');
  282. }
  283. /**
  284. * Decodes the version information from the given bit sequence, returns null if no valid match is found.
  285. */
  286. private function decodeVersionInformation(int $versionBits):?Version{
  287. $bestDifference = PHP_INT_MAX;
  288. $bestVersion = 0;
  289. for($i = 7; $i <= 40; $i++){
  290. $targetVersion = new Version($i);
  291. $targetVersionPattern = $targetVersion->getVersionPattern();
  292. // Do the version info bits match exactly? done.
  293. if($targetVersionPattern === $versionBits){
  294. return $targetVersion;
  295. }
  296. // Otherwise see if this is the closest to a real version info bit string
  297. // we have seen so far
  298. /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable ($targetVersionPattern is never null here) */
  299. $bitsDifference = $this->numBitsDiffering($versionBits, $targetVersionPattern);
  300. if($bitsDifference < $bestDifference){
  301. $bestVersion = $i;
  302. $bestDifference = $bitsDifference;
  303. }
  304. }
  305. // We can tolerate up to 3 bits of error since no two version info codewords will
  306. // differ in less than 8 bits.
  307. if($bestDifference <= 3){
  308. return new Version($bestVersion);
  309. }
  310. // If we didn't find a close enough match, fail
  311. return null;
  312. }
  313. /**
  314. *
  315. */
  316. private function uRShift(int $a, int $b):int{
  317. if($b === 0){
  318. return $a;
  319. }
  320. return (($a >> $b) & ~((1 << (8 * PHP_INT_SIZE - 1)) >> ($b - 1)));
  321. }
  322. /**
  323. *
  324. */
  325. private function numBitsDiffering(int $a, int $b):int{
  326. // a now has a 1 bit exactly where its bit differs with b's
  327. $a ^= $b;
  328. // Offset $i holds the number of 1-bits in the binary representation of $i
  329. $BITS_SET_IN_HALF_BYTE = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4];
  330. // Count bits set quickly with a series of lookups:
  331. $count = 0;
  332. for($i = 0; $i < 32; $i += 4){
  333. $count += $BITS_SET_IN_HALF_BYTE[($this->uRShift($a, $i) & 0x0F)];
  334. }
  335. return $count;
  336. }
  337. /**
  338. * @codeCoverageIgnore
  339. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  340. */
  341. public function setQuietZone(int $quietZoneSize = null):self{
  342. throw new QRCodeDataException('not supported');
  343. }
  344. /**
  345. * @codeCoverageIgnore
  346. * @throws \chillerlan\QRCode\Data\QRCodeDataException
  347. */
  348. public function setLogoSpace(int $width, int $height = null, int $startX = null, int $startY = null):self{
  349. throw new QRCodeDataException('not supported');
  350. }
  351. }