MaskUtil.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <?php
  2. declare(strict_types = 1);
  3. namespace BaconQrCode\Encoder;
  4. use BaconQrCode\Common\BitUtils;
  5. use BaconQrCode\Exception\InvalidArgumentException;
  6. /**
  7. * Mask utility.
  8. */
  9. final class MaskUtil
  10. {
  11. /**#@+
  12. * Penalty weights from section 6.8.2.1
  13. */
  14. const N1 = 3;
  15. const N2 = 3;
  16. const N3 = 40;
  17. const N4 = 10;
  18. /**#@-*/
  19. private function __construct()
  20. {
  21. }
  22. /**
  23. * Applies mask penalty rule 1 and returns the penalty.
  24. *
  25. * Finds repetitive cells with the same color and gives penalty to them.
  26. * Example: 00000 or 11111.
  27. */
  28. public static function applyMaskPenaltyRule1(ByteMatrix $matrix) : int
  29. {
  30. return (
  31. self::applyMaskPenaltyRule1Internal($matrix, true)
  32. + self::applyMaskPenaltyRule1Internal($matrix, false)
  33. );
  34. }
  35. /**
  36. * Applies mask penalty rule 2 and returns the penalty.
  37. *
  38. * Finds 2x2 blocks with the same color and gives penalty to them. This is
  39. * actually equivalent to the spec's rule, which is to find MxN blocks and
  40. * give a penalty proportional to (M-1)x(N-1), because this is the number of
  41. * 2x2 blocks inside such a block.
  42. */
  43. public static function applyMaskPenaltyRule2(ByteMatrix $matrix) : int
  44. {
  45. $penalty = 0;
  46. $array = $matrix->getArray();
  47. $width = $matrix->getWidth();
  48. $height = $matrix->getHeight();
  49. for ($y = 0; $y < $height - 1; ++$y) {
  50. for ($x = 0; $x < $width - 1; ++$x) {
  51. $value = $array[$y][$x];
  52. if ($value === $array[$y][$x + 1]
  53. && $value === $array[$y + 1][$x]
  54. && $value === $array[$y + 1][$x + 1]
  55. ) {
  56. ++$penalty;
  57. }
  58. }
  59. }
  60. return self::N2 * $penalty;
  61. }
  62. /**
  63. * Applies mask penalty rule 3 and returns the penalty.
  64. *
  65. * Finds consecutive cells of 00001011101 or 10111010000, and gives penalty
  66. * to them. If we find patterns like 000010111010000, we give penalties
  67. * twice (i.e. 40 * 2).
  68. */
  69. public static function applyMaskPenaltyRule3(ByteMatrix $matrix) : int
  70. {
  71. $penalty = 0;
  72. $array = $matrix->getArray();
  73. $width = $matrix->getWidth();
  74. $height = $matrix->getHeight();
  75. for ($y = 0; $y < $height; ++$y) {
  76. for ($x = 0; $x < $width; ++$x) {
  77. if ($x + 6 < $width
  78. && 1 === $array[$y][$x]
  79. && 0 === $array[$y][$x + 1]
  80. && 1 === $array[$y][$x + 2]
  81. && 1 === $array[$y][$x + 3]
  82. && 1 === $array[$y][$x + 4]
  83. && 0 === $array[$y][$x + 5]
  84. && 1 === $array[$y][$x + 6]
  85. && (
  86. (
  87. $x + 10 < $width
  88. && 0 === $array[$y][$x + 7]
  89. && 0 === $array[$y][$x + 8]
  90. && 0 === $array[$y][$x + 9]
  91. && 0 === $array[$y][$x + 10]
  92. )
  93. || (
  94. $x - 4 >= 0
  95. && 0 === $array[$y][$x - 1]
  96. && 0 === $array[$y][$x - 2]
  97. && 0 === $array[$y][$x - 3]
  98. && 0 === $array[$y][$x - 4]
  99. )
  100. )
  101. ) {
  102. $penalty += self::N3;
  103. }
  104. if ($y + 6 < $height
  105. && 1 === $array[$y][$x]
  106. && 0 === $array[$y + 1][$x]
  107. && 1 === $array[$y + 2][$x]
  108. && 1 === $array[$y + 3][$x]
  109. && 1 === $array[$y + 4][$x]
  110. && 0 === $array[$y + 5][$x]
  111. && 1 === $array[$y + 6][$x]
  112. && (
  113. (
  114. $y + 10 < $height
  115. && 0 === $array[$y + 7][$x]
  116. && 0 === $array[$y + 8][$x]
  117. && 0 === $array[$y + 9][$x]
  118. && 0 === $array[$y + 10][$x]
  119. )
  120. || (
  121. $y - 4 >= 0
  122. && 0 === $array[$y - 1][$x]
  123. && 0 === $array[$y - 2][$x]
  124. && 0 === $array[$y - 3][$x]
  125. && 0 === $array[$y - 4][$x]
  126. )
  127. )
  128. ) {
  129. $penalty += self::N3;
  130. }
  131. }
  132. }
  133. return $penalty;
  134. }
  135. /**
  136. * Applies mask penalty rule 4 and returns the penalty.
  137. *
  138. * Calculates the ratio of dark cells and gives penalty if the ratio is far
  139. * from 50%. It gives 10 penalty for 5% distance.
  140. */
  141. public static function applyMaskPenaltyRule4(ByteMatrix $matrix) : int
  142. {
  143. $numDarkCells = 0;
  144. $array = $matrix->getArray();
  145. $width = $matrix->getWidth();
  146. $height = $matrix->getHeight();
  147. for ($y = 0; $y < $height; ++$y) {
  148. $arrayY = $array[$y];
  149. for ($x = 0; $x < $width; ++$x) {
  150. if (1 === $arrayY[$x]) {
  151. ++$numDarkCells;
  152. }
  153. }
  154. }
  155. $numTotalCells = $height * $width;
  156. $darkRatio = $numDarkCells / $numTotalCells;
  157. $fixedPercentVariances = (int) (abs($darkRatio - 0.5) * 20);
  158. return $fixedPercentVariances * self::N4;
  159. }
  160. /**
  161. * Returns the mask bit for "getMaskPattern" at "x" and "y".
  162. *
  163. * See 8.8 of JISX0510:2004 for mask pattern conditions.
  164. *
  165. * @throws InvalidArgumentException if an invalid mask pattern was supplied
  166. */
  167. public static function getDataMaskBit(int $maskPattern, int $x, int $y) : bool
  168. {
  169. switch ($maskPattern) {
  170. case 0:
  171. $intermediate = ($y + $x) & 0x1;
  172. break;
  173. case 1:
  174. $intermediate = $y & 0x1;
  175. break;
  176. case 2:
  177. $intermediate = $x % 3;
  178. break;
  179. case 3:
  180. $intermediate = ($y + $x) % 3;
  181. break;
  182. case 4:
  183. $intermediate = (BitUtils::unsignedRightShift($y, 1) + ($x / 3)) & 0x1;
  184. break;
  185. case 5:
  186. $temp = $y * $x;
  187. $intermediate = ($temp & 0x1) + ($temp % 3);
  188. break;
  189. case 6:
  190. $temp = $y * $x;
  191. $intermediate = (($temp & 0x1) + ($temp % 3)) & 0x1;
  192. break;
  193. case 7:
  194. $temp = $y * $x;
  195. $intermediate = (($temp % 3) + (($y + $x) & 0x1)) & 0x1;
  196. break;
  197. default:
  198. throw new InvalidArgumentException('Invalid mask pattern: ' . $maskPattern);
  199. }
  200. return 0 == $intermediate;
  201. }
  202. /**
  203. * Helper function for applyMaskPenaltyRule1.
  204. *
  205. * We need this for doing this calculation in both vertical and horizontal
  206. * orders respectively.
  207. */
  208. private static function applyMaskPenaltyRule1Internal(ByteMatrix $matrix, bool $isHorizontal) : int
  209. {
  210. $penalty = 0;
  211. $iLimit = $isHorizontal ? $matrix->getHeight() : $matrix->getWidth();
  212. $jLimit = $isHorizontal ? $matrix->getWidth() : $matrix->getHeight();
  213. $array = $matrix->getArray();
  214. for ($i = 0; $i < $iLimit; ++$i) {
  215. $numSameBitCells = 0;
  216. $prevBit = -1;
  217. for ($j = 0; $j < $jLimit; $j++) {
  218. $bit = $isHorizontal ? $array[$i][$j] : $array[$j][$i];
  219. if ($bit === $prevBit) {
  220. ++$numSameBitCells;
  221. } else {
  222. if ($numSameBitCells >= 5) {
  223. $penalty += self::N1 + ($numSameBitCells - 5);
  224. }
  225. $numSameBitCells = 1;
  226. $prevBit = $bit;
  227. }
  228. }
  229. if ($numSameBitCells >= 5) {
  230. $penalty += self::N1 + ($numSameBitCells - 5);
  231. }
  232. }
  233. return $penalty;
  234. }
  235. }