MaskPattern.php 8.0 KB

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