MaskPattern::getPattern()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 1
b 0
f 0
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
It seems like array_search(min($penalties), $penalties, true) can also be of type string; however, parameter $maskPattern of chillerlan\QRCode\Common...kPattern::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

130
		return new self(/** @scrutinizer ignore-type */ array_search(min($penalties), $penalties, true));
Loading history...
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