Passed
Push — master ( 514bca...891b04 )
by smiley
02:57
created

QRDataAbstract::getLengthBits()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 0
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class QRDataAbstract
4
 *
5
 * @filesource   QRDataAbstract.php
6
 * @created      25.11.2015
7
 * @package      chillerlan\QRCode\Data
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2015 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\QRCode\Data;
14
15
use chillerlan\QRCode\QRCode;
16
use chillerlan\QRCode\Helpers\{BitBuffer, Polynomial};
17
use chillerlan\Settings\SettingsContainerInterface;
18
19
use function array_fill, array_merge, count, max, mb_convert_encoding, mb_detect_encoding, range, sprintf, strlen;
20
21
/**
22
 * Processes the binary data and maps it on a matrix which is then being returned
23
 */
24
abstract class QRDataAbstract implements QRDataInterface{
25
26
	/**
27
	 * the string byte count
28
	 */
29
	protected ?int $strlen = null;
30
31
	/**
32
	 * the current data mode: Num, Alphanum, Kanji, Byte
33
	 */
34
	protected int $datamode;
35
36
	/**
37
	 * mode length bits for the version breakpoints 1-9, 10-26 and 27-40
38
	 */
39
	protected array $lengthBits = [0, 0, 0];
40
41
	/**
42
	 * current QR Code version
43
	 */
44
	protected int $version;
45
46
	/**
47
	 * the raw data that's being passed to QRMatrix::mapData()
48
	 */
49
	protected array $matrixdata;
50
51
	/**
52
	 * ECC temp data
53
	 */
54
	protected array $ecdata;
55
56
	/**
57
	 * ECC temp data
58
	 */
59
	protected array $dcdata;
60
61
	/**
62
	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\QRCode\QROptions
63
	 */
64
	protected SettingsContainerInterface $options;
65
66
	/**
67
	 * a BitBuffer instance
68
	 */
69
	protected BitBuffer $bitBuffer;
70
71
	/**
72
	 * QRDataInterface constructor.
73
	 */
74
	public function __construct(SettingsContainerInterface $options, string $data = null){
75
		$this->options = $options;
76
77
		if($data !== null){
78
			$this->setData($data);
79
		}
80
	}
81
82
	/**
83
	 * @inheritDoc
84
	 */
85
	public function setData(string $data):QRDataInterface{
86
87
		if($this->datamode === QRCode::DATA_KANJI){
88
			$data = mb_convert_encoding($data, 'SJIS', mb_detect_encoding($data));
89
		}
90
91
		$this->strlen  = $this->getLength($data);
92
		$this->version = $this->options->version === QRCode::VERSION_AUTO
93
			? $this->getMinimumVersion()
94
			: $this->options->version;
95
96
		$this->writeBitBuffer($data);
97
		$this->matrixdata = $this->maskECC();
98
99
		return $this;
100
	}
101
102
	/**
103
	 * @inheritDoc
104
	 */
105
	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{
106
		return (new QRMatrix($this->version, $this->options->eccLevel))
107
			->init($maskPattern, $test)
108
			->mapData($this->matrixdata, $maskPattern)
109
		;
110
	}
111
112
	/**
113
	 * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40
114
	 *
115
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
116
	 * @codeCoverageIgnore
117
	 */
118
	protected function getLengthBits():int{
119
120
		 foreach([9, 26, 40] as $key => $breakpoint){
121
			 if($this->version <= $breakpoint){
122
				 return $this->lengthBits[$key];
123
			 }
124
		 }
125
126
		throw new QRCodeDataException(sprintf('invalid version number: %d', $this->version));
127
	}
128
129
	/**
130
	 * returns the byte count of the $data string
131
	 */
132
	protected function getLength(string $data):int{
133
		return strlen($data);
134
	}
135
136
	/**
137
	 * returns the minimum version number for the given string
138
	 *
139
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
140
	 */
141
	protected function getMinimumVersion():int{
142
		$maxlength = 0;
143
144
		// guess the version number within the given range
145
		$dataMode = QRCode::DATA_MODES[$this->datamode];
146
		$eccMode  = QRCode::ECC_MODES[$this->options->eccLevel];
147
148
		foreach(range($this->options->versionMin, $this->options->versionMax) as $version){
149
			$maxlength = $this::MAX_LENGTH[$version][$dataMode][$eccMode];
150
151
			if($this->strlen <= $maxlength){
152
				return $version;
153
			}
154
		}
155
156
		throw new QRCodeDataException(sprintf('data exceeds %d characters', $maxlength));
157
	}
158
159
	/**
160
	 * writes the actual data string to the BitBuffer
161
	 *
162
	 * @see \chillerlan\QRCode\Data\QRDataAbstract::writeBitBuffer()
163
	 */
164
	abstract protected function write(string $data):void;
165
166
	/**
167
	 * creates a BitBuffer and writes the string data to it
168
	 *
169
	 * @throws \chillerlan\QRCode\QRCodeException
170
	 */
171
	protected function writeBitBuffer(string $data):void{
172
		$this->bitBuffer = new BitBuffer;
173
174
		$MAX_BITS = $this::MAX_BITS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
175
176
		$this->bitBuffer
177
			->clear()
178
			->put($this->datamode, 4)
179
			->put($this->strlen, $this->getLengthBits())
0 ignored issues
show
Bug introduced by
It seems like $this->strlen can also be of type null; however, parameter $num of chillerlan\QRCode\Helpers\BitBuffer::put() 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

179
			->put(/** @scrutinizer ignore-type */ $this->strlen, $this->getLengthBits())
Loading history...
180
		;
181
182
		$this->write($data);
183
184
		// overflow, likely caused due to invalid version setting
185
		if($this->bitBuffer->length > $MAX_BITS){
186
			throw new QRCodeDataException(sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->length, $MAX_BITS));
187
		}
188
189
		// end code.
190
		if($this->bitBuffer->length + 4 <= $MAX_BITS){
191
			$this->bitBuffer->put(0, 4);
192
		}
193
194
		// padding
195
		while($this->bitBuffer->length % 8 !== 0){
196
			$this->bitBuffer->putBit(false);
197
		}
198
199
		// padding
200
		while(true){
201
202
			if($this->bitBuffer->length >= $MAX_BITS){
203
				break;
204
			}
205
206
			$this->bitBuffer->put(0xEC, 8);
207
208
			if($this->bitBuffer->length >= $MAX_BITS){
209
				break;
210
			}
211
212
			$this->bitBuffer->put(0x11, 8);
213
		}
214
215
	}
216
217
	/**
218
	 * ECC masking
219
	 *
220
	 * @link http://www.thonky.com/qr-code-tutorial/error-correction-coding
221
	 */
222
	protected function maskECC():array{
223
		[$l1, $l2, $b1, $b2] = $this::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
224
225
		$rsBlocks     = array_fill(0, $l1, [$b1, $b2]);
226
		$rsCount      = $l1 + $l2;
227
		$this->ecdata = array_fill(0, $rsCount, []);
228
		$this->dcdata = $this->ecdata;
229
230
		if($l2 > 0){
231
			$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1]));
232
		}
233
234
		$totalCodeCount = 0;
235
		$maxDcCount     = 0;
236
		$maxEcCount     = 0;
237
		$offset         = 0;
238
239
		foreach($rsBlocks as $key => $block){
240
			[$rsBlockTotal, $dcCount] = $block;
241
242
			$ecCount            = $rsBlockTotal - $dcCount;
243
			$maxDcCount         = max($maxDcCount, $dcCount);
244
			$maxEcCount         = max($maxEcCount, $ecCount);
245
			$this->dcdata[$key] = array_fill(0, $dcCount, null);
246
247
			foreach($this->dcdata[$key] as $a => $_z){
248
				$this->dcdata[$key][$a] = 0xff & $this->bitBuffer->buffer[$a + $offset];
249
			}
250
251
			[$num, $add] = $this->poly($key, $ecCount);
252
253
			foreach($this->ecdata[$key] as $c => $_z){
254
				$modIndex               = $c + $add;
255
				$this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0;
256
			}
257
258
			$offset         += $dcCount;
259
			$totalCodeCount += $rsBlockTotal;
260
		}
261
262
		$data  = array_fill(0, $totalCodeCount, null);
263
		$index = 0;
264
265
		$mask = function(array $arr, int $count) use (&$data, &$index, $rsCount){
266
			for($x = 0; $x < $count; $x++){
267
				for($y = 0; $y < $rsCount; $y++){
268
					if($x < count($arr[$y])){
269
						$data[$index] = $arr[$y][$x];
270
						$index++;
271
					}
272
				}
273
			}
274
		};
275
276
		$mask($this->dcdata, $maxDcCount);
277
		$mask($this->ecdata, $maxEcCount);
278
279
		return $data;
280
	}
281
282
	/**
283
	 *
284
	 */
285
	protected function poly(int $key, int $count):array{
286
		$rsPoly  = new Polynomial;
287
		$modPoly = new Polynomial;
288
289
		for($i = 0; $i < $count; $i++){
290
			$modPoly->setNum([1, $modPoly->gexp($i)]);
291
			$rsPoly->multiply($modPoly->getNum());
292
		}
293
294
		$rsPolyCount = count($rsPoly->getNum());
295
296
		$modPoly
297
			->setNum($this->dcdata[$key], $rsPolyCount - 1)
298
			->mod($rsPoly->getNum())
299
		;
300
301
		$this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null);
302
		$num                = $modPoly->getNum();
303
304
		return [
305
			$num,
306
			count($num) - count($this->ecdata[$key]),
307
		];
308
	}
309
310
}
311