Passed
Push — main ( 48b6c2...5c2c92 )
by smiley
02:24
created

MaskPattern::testRule4()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 4
nop 3
dl 0
loc 13
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class MaskPattern
4
 *
5
 * @created      19.01.2021
6
 * @author       smiley <[email protected]>
7
 * @copyright    2021 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\QRCode\Common;
12
13
use chillerlan\QRCode\Data\QRData;
14
use chillerlan\QRCode\QRCodeException;
15
use Closure;
16
use function abs, array_search, count, min;
17
18
/**
19
 * ISO/IEC 18004:2000 Section 8.8.1
20
 * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results
21
 *
22
 * @see http://www.thonky.com/qr-code-tutorial/data-masking
23
 */
24
final class MaskPattern{
25
26
	public const PATTERN_000 = 0b000;
27
	public const PATTERN_001 = 0b001;
28
	public const PATTERN_010 = 0b010;
29
	public const PATTERN_011 = 0b011;
30
	public const PATTERN_100 = 0b100;
31
	public const PATTERN_101 = 0b101;
32
	public const PATTERN_110 = 0b110;
33
	public const PATTERN_111 = 0b111;
34
35
	/**
36
	 * @var int[]
37
	 */
38
	public const PATTERNS = [
39
		self::PATTERN_000,
40
		self::PATTERN_001,
41
		self::PATTERN_010,
42
		self::PATTERN_011,
43
		self::PATTERN_100,
44
		self::PATTERN_101,
45
		self::PATTERN_110,
46
		self::PATTERN_111,
47
	];
48
49
	/**
50
	 * The current mask pattern value (0-7)
51
	 */
52
	private int $maskPattern;
53
54
	/**
55
	 * MaskPattern constructor.
56
	 *
57
	 * @throws \chillerlan\QRCode\QRCodeException
58
	 */
59
	public function __construct(int $maskPattern){
60
61
		if((0b111 & $maskPattern) !== $maskPattern){
62
			throw new QRCodeException('invalid mask pattern');
63
		}
64
65
		$this->maskPattern = $maskPattern;
66
	}
67
68
	/**
69
	 * Returns the current mask pattern
70
	 */
71
	public function getPattern():int{
72
		return $this->maskPattern;
73
	}
74
75
	/**
76
	 * Returns a closure that applies the mask for the chosen mask pattern.
77
	 *
78
	 * Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations
79
	 * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix,
80
	 * including areas used for finder patterns, timing patterns, etc. These areas should be unused
81
	 * after the point they are unmasked anyway.
82
	 *
83
	 * Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position
84
	 * and j is row position. In fact, as the text says, i is row position and j is column position.
85
	 *
86
	 * @see https://www.thonky.com/qr-code-tutorial/mask-patterns
87
	 * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117
88
	 */
89
	public function getMask():Closure{
90
		// $x = column (width), $y = row (height)
91
		return [
92
			self::PATTERN_000 => fn(int $x, int $y):bool => (($x + $y) % 2) === 0,
93
			self::PATTERN_001 => fn(int $x, int $y):bool => ($y % 2) === 0,
94
			self::PATTERN_010 => fn(int $x, int $y):bool => ($x % 3) === 0,
95
			self::PATTERN_011 => fn(int $x, int $y):bool => (($x + $y) % 3) === 0,
96
			self::PATTERN_100 => fn(int $x, int $y):bool => (((int)($y / 2) + (int)($x / 3)) % 2) === 0,
97
			self::PATTERN_101 => fn(int $x, int $y):bool => ($x * $y) % 6 === 0, // ((($x * $y) % 2) + (($x * $y) % 3)) === 0,
98
			self::PATTERN_110 => fn(int $x, int $y):bool => (($x * $y) % 6) < 3, // (((($x * $y) % 2) + (($x * $y) % 3)) % 2) === 0,
99
			self::PATTERN_111 => fn(int $x, int $y):bool => (($x + $y + (($x * $y) % 3)) % 2) === 0, // (((($x * $y) % 3) + (($x + $y) % 2)) % 2) === 0,
100
		][$this->maskPattern];
101
	}
102
103
	/**
104
	 * Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result
105
	 */
106
	public static function getBestPattern(QRData $dataInterface):self{
107
		$penalties = [];
108
109
		foreach(self::PATTERNS as $pattern){
110
			$matrix  = $dataInterface->writeMatrix(new self($pattern))->matrix(true);
111
			$penalty = 0;
112
113
			for($level = 1; $level <= 4; $level++){
114
				$penalty += self::{'testRule'.$level}($matrix, count($matrix), count($matrix[0]));
115
			}
116
117
			$penalties[$pattern] = (int)$penalty;
118
		}
119
120
		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

120
		return new self(/** @scrutinizer ignore-type */ array_search(min($penalties), $penalties, true));
Loading history...
121
	}
122
123
	/**
124
	 * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and
125
	 * give penalty to them. Example: 00000 or 11111.
126
	 */
127
	public static function testRule1(array $matrix, int $height, int $width):int{
128
		return self::applyRule1($matrix, $height, $width, true) + self::applyRule1($matrix, $height, $width, false);
129
	}
130
131
	private static function applyRule1(array $matrix, int $height, int $width, bool $isHorizontal):int{
132
		$penalty = 0;
133
		$iLimit  = $isHorizontal ? $height : $width;
134
		$jLimit  = $isHorizontal ? $width : $height;
135
136
		for($i = 0; $i < $iLimit; $i++){
137
			$numSameBitCells = 0;
138
			$prevBit         = -1;
139
140
			for($j = 0; $j < $jLimit; $j++){
141
				$bit = $isHorizontal ? $matrix[$i][$j] : $matrix[$j][$i];
142
143
				if($bit === $prevBit){
144
					$numSameBitCells++;
145
				}
146
				else{
147
148
					if($numSameBitCells >= 5){
149
						$penalty += 3 + ($numSameBitCells - 5);
150
					}
151
152
					$numSameBitCells = 1;  // Include the cell itself.
153
					$prevBit         = $bit;
154
				}
155
			}
156
			if($numSameBitCells >= 5){
157
				$penalty += 3 + ($numSameBitCells - 5);
158
			}
159
		}
160
161
		return $penalty;
162
	}
163
164
	/**
165
	 * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give
166
	 * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a
167
	 * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block.
168
	 */
169
	public static function testRule2(array $matrix, int $height, int $width):int{
170
		$penalty = 0;
171
172
		foreach($matrix as $y => $row){
173
174
			if($y > $height - 2){
175
				break;
176
			}
177
178
			foreach($row as $x => $val){
179
180
				if($x > $width - 2){
181
					break;
182
				}
183
184
				if(
185
					$val === $row[$x + 1]
186
					&& $val === $matrix[$y + 1][$x]
187
					&& $val === $matrix[$y + 1][$x + 1]
188
				){
189
					$penalty++;
190
				}
191
			}
192
		}
193
194
		return 3 * $penalty;
195
	}
196
197
	/**
198
	 * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4
199
	 * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them.  If we
200
	 * find patterns like 000010111010000, we give penalty once.
201
	 */
202
	public static function testRule3(array $matrix, int $height, int $width):int{
203
		$penalties = 0;
204
205
		foreach($matrix as $y => $row){
206
			foreach($row as $x => $val){
207
208
				if(
209
					$x + 6 < $width
210
					&&  $val
211
					&& !$row[$x + 1]
212
					&&  $row[$x + 2]
213
					&&  $row[$x + 3]
214
					&&  $row[$x + 4]
215
					&& !$row[$x + 5]
216
					&&  $row[$x + 6]
217
					&& (
218
						self::isWhiteHorizontal($row, $width, $x - 4, $x)
219
						|| self::isWhiteHorizontal($row, $width, $x + 7, $x + 11)
220
					)
221
				){
222
					$penalties++;
223
				}
224
225
				if(
226
					$y + 6 < $height
227
					&&  $val
228
					&& !$matrix[$y + 1][$x]
229
					&&  $matrix[$y + 2][$x]
230
					&&  $matrix[$y + 3][$x]
231
					&&  $matrix[$y + 4][$x]
232
					&& !$matrix[$y + 5][$x]
233
					&&  $matrix[$y + 6][$x]
234
					&& (
235
						self::isWhiteVertical($matrix, $height, $x, $y - 4, $y)
236
						|| self::isWhiteVertical($matrix, $height, $x, $y + 7, $y + 11)
237
					)
238
				){
239
					$penalties++;
240
				}
241
242
			}
243
		}
244
245
		return $penalties * 40;
246
	}
247
248
	private static function isWhiteHorizontal(array $row, int $width, int $from, int $to):bool{
249
250
		if($from < 0 || $width < $to){
251
			return false;
252
		}
253
254
		for($x = $from; $x < $to; $x++){
255
			if($row[$x]){
256
				return false;
257
			}
258
		}
259
260
		return true;
261
	}
262
263
	private static function isWhiteVertical(array $matrix, int $height, int $x, int $from, int $to):bool{
264
265
		if($from < 0 || $height < $to){
266
			return false;
267
		}
268
269
		for($y = $from; $y < $to; $y++){
270
			if($matrix[$y][$x]){
271
				return false;
272
			}
273
		}
274
275
		return true;
276
	}
277
278
	/**
279
	 * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give
280
	 * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance.
281
	 */
282
	public static function testRule4(array $matrix, int $height, int $width):int{
283
		$darkCells  = 0;
284
		$totalCells = $height * $width;
285
286
		foreach($matrix as $row){
287
			foreach($row as $val){
288
				if($val){
289
					$darkCells++;
290
				}
291
			}
292
		}
293
294
		return (int)(abs($darkCells * 2 - $totalCells) * 10 / $totalCells) * 10;
295
	}
296
297
}
298