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