MaskPattern.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <?php
  2. /**
  3. * Class MaskPattern
  4. *
  5. * @created 19.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\Common;
  12. use chillerlan\QRCode\QRCodeException;
  13. use chillerlan\QRCode\Data\QRMatrix;
  14. use Closure;
  15. use function abs, array_column, array_search, intdiv, min;
  16. /**
  17. * ISO/IEC 18004:2000 Section 8.8.1
  18. * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
  19. *
  20. * @see http://www.thonky.com/qr-code-tutorial/data-masking
  21. * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java
  22. */
  23. final class MaskPattern{
  24. /**
  25. * @see \chillerlan\QRCode\QROptionsTrait::$maskPattern
  26. *
  27. * @var int
  28. */
  29. public const AUTO = -1;
  30. public const PATTERN_000 = 0b000;
  31. public const PATTERN_001 = 0b001;
  32. public const PATTERN_010 = 0b010;
  33. public const PATTERN_011 = 0b011;
  34. public const PATTERN_100 = 0b100;
  35. public const PATTERN_101 = 0b101;
  36. public const PATTERN_110 = 0b110;
  37. public const PATTERN_111 = 0b111;
  38. /**
  39. * @var int[]
  40. */
  41. public const PATTERNS = [
  42. self::PATTERN_000,
  43. self::PATTERN_001,
  44. self::PATTERN_010,
  45. self::PATTERN_011,
  46. self::PATTERN_100,
  47. self::PATTERN_101,
  48. self::PATTERN_110,
  49. self::PATTERN_111,
  50. ];
  51. /*
  52. * Penalty scores
  53. *
  54. * ISO/IEC 18004:2000 Section 8.8.1 - Table 24
  55. */
  56. private const PENALTY_N1 = 3;
  57. private const PENALTY_N2 = 3;
  58. private const PENALTY_N3 = 40;
  59. private const PENALTY_N4 = 10;
  60. /**
  61. * The current mask pattern value (0-7)
  62. */
  63. private int $maskPattern;
  64. /**
  65. * MaskPattern constructor.
  66. *
  67. * @throws \chillerlan\QRCode\QRCodeException
  68. */
  69. public function __construct(int $maskPattern){
  70. if(($maskPattern & 0b111) !== $maskPattern){
  71. throw new QRCodeException('invalid mask pattern');
  72. }
  73. $this->maskPattern = $maskPattern;
  74. }
  75. /**
  76. * Returns the current mask pattern
  77. */
  78. public function getPattern():int{
  79. return $this->maskPattern;
  80. }
  81. /**
  82. * Returns a closure that applies the mask for the chosen mask pattern.
  83. *
  84. * Note that the diagram in section 6.8.1 is misleading since it indicates that $i is column position
  85. * and $j is row position. In fact, as the text says, $i is row position and $j is column position.
  86. *
  87. * @see https://www.thonky.com/qr-code-tutorial/mask-patterns
  88. * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117
  89. */
  90. public function getMask():Closure{
  91. // $x = column (width), $y = row (height)
  92. return [
  93. self::PATTERN_000 => fn(int $x, int $y):bool => (($x + $y) % 2) === 0,
  94. self::PATTERN_001 => fn(int $x, int $y):bool => ($y % 2) === 0,
  95. self::PATTERN_010 => fn(int $x, int $y):bool => ($x % 3) === 0,
  96. self::PATTERN_011 => fn(int $x, int $y):bool => (($x + $y) % 3) === 0,
  97. self::PATTERN_100 => fn(int $x, int $y):bool => ((intdiv($y, 2) + intdiv($x, 3)) % 2) === 0,
  98. self::PATTERN_101 => fn(int $x, int $y):bool => (($x * $y) % 6) === 0,
  99. self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3,
  100. self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0,
  101. ][$this->maskPattern];
  102. }
  103. /**
  104. * Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result
  105. */
  106. public static function getBestPattern(QRMatrix $QRMatrix):self{
  107. $penalties = [];
  108. $size = $QRMatrix->getSize();
  109. foreach(self::PATTERNS as $pattern){
  110. $mp = new self($pattern);
  111. $matrix = (clone $QRMatrix)->setFormatInfo($mp)->mask($mp)->getMatrix(true);
  112. $penalty = 0;
  113. for($level = 1; $level <= 4; $level++){
  114. $penalty += self::{'testRule'.$level}($matrix, $size, $size);
  115. }
  116. $penalties[$pattern] = (int)$penalty;
  117. }
  118. return new self(array_search(min($penalties), $penalties, true));
  119. }
  120. /**
  121. * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and
  122. * give penalty to them. Example: 00000 or 11111.
  123. *
  124. * @param bool[][] $matrix
  125. */
  126. public static function testRule1(array $matrix, int $height, int $width):int{
  127. $penalty = 0;
  128. // horizontal
  129. foreach($matrix as $row){
  130. $penalty += self::applyRule1($row);
  131. }
  132. // vertical
  133. for($x = 0; $x < $width; $x++){
  134. $penalty += self::applyRule1(array_column($matrix, $x));
  135. }
  136. return $penalty;
  137. }
  138. /**
  139. * @param bool[] $rc
  140. */
  141. private static function applyRule1(array $rc):int{
  142. $penalty = 0;
  143. $numSameBitCells = 0;
  144. $prevBit = null;
  145. foreach($rc as $val){
  146. if($val === $prevBit){
  147. $numSameBitCells++;
  148. }
  149. else{
  150. if($numSameBitCells >= 5){
  151. $penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
  152. }
  153. $numSameBitCells = 1; // Include the cell itself.
  154. $prevBit = $val;
  155. }
  156. }
  157. if($numSameBitCells >= 5){
  158. $penalty += (self::PENALTY_N1 + $numSameBitCells - 5);
  159. }
  160. return $penalty;
  161. }
  162. /**
  163. * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give
  164. * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a
  165. * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block.
  166. *
  167. * @param bool[][] $matrix
  168. */
  169. public static function testRule2(array $matrix, int $height, int $width):int{
  170. $penalty = 0;
  171. foreach($matrix as $y => $row){
  172. if($y > ($height - 2)){
  173. break;
  174. }
  175. foreach($row as $x => $val){
  176. if($x > ($width - 2)){
  177. break;
  178. }
  179. if(
  180. $val === $row[($x + 1)]
  181. && $val === $matrix[($y + 1)][$x]
  182. && $val === $matrix[($y + 1)][($x + 1)]
  183. ){
  184. $penalty++;
  185. }
  186. }
  187. }
  188. return (self::PENALTY_N2 * $penalty);
  189. }
  190. /**
  191. * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4
  192. * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we
  193. * find patterns like 000010111010000, we give penalty once.
  194. *
  195. * @param bool[][] $matrix
  196. */
  197. public static function testRule3(array $matrix, int $height, int $width):int{
  198. $penalties = 0;
  199. foreach($matrix as $y => $row){
  200. foreach($row as $x => $val){
  201. if(
  202. ($x + 6) < $width
  203. && $val
  204. && !$row[($x + 1)]
  205. && $row[($x + 2)]
  206. && $row[($x + 3)]
  207. && $row[($x + 4)]
  208. && !$row[($x + 5)]
  209. && $row[($x + 6)]
  210. && (
  211. self::isWhiteHorizontal($row, $width, ($x - 4), $x)
  212. || self::isWhiteHorizontal($row, $width, ($x + 7), ($x + 11))
  213. )
  214. ){
  215. $penalties++;
  216. }
  217. if(
  218. ($y + 6) < $height
  219. && $val
  220. && !$matrix[($y + 1)][$x]
  221. && $matrix[($y + 2)][$x]
  222. && $matrix[($y + 3)][$x]
  223. && $matrix[($y + 4)][$x]
  224. && !$matrix[($y + 5)][$x]
  225. && $matrix[($y + 6)][$x]
  226. && (
  227. self::isWhiteVertical($matrix, $height, $x, ($y - 4), $y)
  228. || self::isWhiteVertical($matrix, $height, $x, ($y + 7), ($y + 11))
  229. )
  230. ){
  231. $penalties++;
  232. }
  233. }
  234. }
  235. return ($penalties * self::PENALTY_N3);
  236. }
  237. /**
  238. * @param bool[] $row
  239. */
  240. private static function isWhiteHorizontal(array $row, int $width, int $from, int $to):bool{
  241. if($from < 0 || $width < $to){
  242. return false;
  243. }
  244. for($x = $from; $x < $to; $x++){
  245. if($row[$x]){
  246. return false;
  247. }
  248. }
  249. return true;
  250. }
  251. /**
  252. * @param bool[][] $matrix
  253. */
  254. private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to):bool{
  255. if($from < 0 || $height < $to){
  256. return false;
  257. }
  258. for($y = $from; $y < $to; $y++){
  259. if($matrix[$y][$x] === true){
  260. return false;
  261. }
  262. }
  263. return true;
  264. }
  265. /**
  266. * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give
  267. * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.
  268. *
  269. * @param bool[][] $matrix
  270. */
  271. public static function testRule4(array $matrix, int $height, int $width):int{
  272. $darkCells = 0;
  273. $totalCells = ($height * $width);
  274. foreach($matrix as $row){
  275. foreach($row as $val){
  276. if($val === true){
  277. $darkCells++;
  278. }
  279. }
  280. }
  281. return (intdiv((abs($darkCells * 2 - $totalCells) * 10), $totalCells) * self::PENALTY_N4);
  282. }
  283. }