MaskPatternTester.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. <?php
  2. /**
  3. * Class MaskPatternTester
  4. *
  5. * @created 22.11.2017
  6. * @author Smiley <smiley@chillerlan.net>
  7. * @copyright 2017 Smiley
  8. * @license MIT
  9. *
  10. * @noinspection PhpUnused
  11. */
  12. namespace chillerlan\QRCode\Common;
  13. use chillerlan\QRCode\Data\QRData;
  14. use function abs, array_search, call_user_func_array, min;
  15. /**
  16. * Receives a QRData object and runs the mask pattern tests on it.
  17. *
  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. */
  22. final class MaskPatternTester{
  23. /**
  24. * The data interface that contains the data matrix to test
  25. */
  26. private QRData $qrData;
  27. /**
  28. * Receives the QRData object
  29. *
  30. * @see \chillerlan\QRCode\QROptions::$maskPattern
  31. * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
  32. */
  33. public function __construct(QRData $qrData){
  34. $this->qrData = $qrData;
  35. }
  36. /**
  37. * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
  38. *
  39. * @see \chillerlan\QRCode\Data\MaskPatternTester
  40. */
  41. public function getBestMaskPattern():MaskPattern{
  42. $penalties = [];
  43. foreach(MaskPattern::PATTERNS as $pattern){
  44. $penalties[$pattern] = $this->testPattern(new MaskPattern($pattern));
  45. }
  46. return new MaskPattern(array_search(min($penalties), $penalties, true));
  47. }
  48. /**
  49. * Returns the penalty for the given mask pattern
  50. *
  51. * @see \chillerlan\QRCode\QROptions::$maskPattern
  52. * @see \chillerlan\QRCode\Data\QRMatrix::$maskPattern
  53. */
  54. public function testPattern(MaskPattern $pattern):int{
  55. $matrix = $this->qrData->writeMatrix($pattern, true);
  56. $penalty = 0;
  57. for($level = 1; $level <= 4; $level++){
  58. $penalty += call_user_func_array([$this, 'testLevel'.$level], [$matrix->matrix(true), $matrix->size()]);
  59. }
  60. return (int)$penalty;
  61. }
  62. /**
  63. * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and
  64. * give penalty to them. Example: 00000 or 11111.
  65. */
  66. private function testLevel1(array $m, int $size):int{
  67. $penalty = 0;
  68. foreach($m as $y => $row){
  69. foreach($row as $x => $val){
  70. $count = 0;
  71. for($ry = -1; $ry <= 1; $ry++){
  72. if($y + $ry < 0 || $size <= $y + $ry){
  73. continue;
  74. }
  75. for($rx = -1; $rx <= 1; $rx++){
  76. if(($ry === 0 && $rx === 0) || (($x + $rx) < 0 || $size <= ($x + $rx))){
  77. continue;
  78. }
  79. if($m[$y + $ry][$x + $rx] === $val){
  80. $count++;
  81. }
  82. }
  83. }
  84. if($count > 5){
  85. $penalty += (3 + $count - 5);
  86. }
  87. }
  88. }
  89. return $penalty;
  90. }
  91. /**
  92. * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give
  93. * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a
  94. * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block.
  95. */
  96. private function testLevel2(array $m, int $size):int{
  97. $penalty = 0;
  98. foreach($m as $y => $row){
  99. if($y > $size - 2){
  100. break;
  101. }
  102. foreach($row as $x => $val){
  103. if($x > $size - 2){
  104. break;
  105. }
  106. if(
  107. $val === $row[$x + 1]
  108. && $val === $m[$y + 1][$x]
  109. && $val === $m[$y + 1][$x + 1]
  110. ){
  111. $penalty++;
  112. }
  113. }
  114. }
  115. return 3 * $penalty;
  116. }
  117. /**
  118. * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4
  119. * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we
  120. * find patterns like 000010111010000, we give penalty once.
  121. */
  122. private function testLevel3(array $m, int $size):int{
  123. $penalties = 0;
  124. foreach($m as $y => $row){
  125. foreach($row as $x => $val){
  126. if(
  127. $x + 6 < $size
  128. && $val
  129. && !$row[$x + 1]
  130. && $row[$x + 2]
  131. && $row[$x + 3]
  132. && $row[$x + 4]
  133. && !$row[$x + 5]
  134. && $row[$x + 6]
  135. ){
  136. $penalties++;
  137. }
  138. if(
  139. $y + 6 < $size
  140. && $val
  141. && !$m[$y + 1][$x]
  142. && $m[$y + 2][$x]
  143. && $m[$y + 3][$x]
  144. && $m[$y + 4][$x]
  145. && !$m[$y + 5][$x]
  146. && $m[$y + 6][$x]
  147. ){
  148. $penalties++;
  149. }
  150. }
  151. }
  152. return $penalties * 40;
  153. }
  154. /**
  155. * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give
  156. * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.
  157. */
  158. private function testLevel4(array $m, int $size):float{
  159. $count = 0;
  160. foreach($m as $y => $row){
  161. foreach($row as $x => $val){
  162. if($val){
  163. $count++;
  164. }
  165. }
  166. }
  167. return (abs(100 * $count / $size / $size - 50) / 5) * 10;
  168. }
  169. }