MaskPattern.php 8.1 KB

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