Completed
Push — master ( 693e1e...7ea7b3 )
by smiley
03:06
created

QRDataAbstract   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 0
loc 314
rs 9.8
c 0
b 0
f 0
wmc 31
lcom 1
cbo 7

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A setData() 0 18 3
A initMatrix() 0 15 1
A getLengthBits() 0 10 3
A getLength() 0 3 1
A getMinimumVersion() 0 13 3
write() 0 1 ?
C writeBitBuffer() 0 46 7
C maskECC() 0 59 9
B poly() 0 24 2
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\{
16
	QRCode, QRCodeException, QROptions
17
};
18
use chillerlan\QRCode\Helpers\{
19
	BitBuffer, Polynomial
20
};
21
use chillerlan\Traits\ClassLoader;
22
23
/**
24
 *
25
 */
26
abstract class QRDataAbstract implements QRDataInterface{
27
	use ClassLoader;
28
29
	/**
30
	 * @var int
31
	 */
32
	protected $strlen;
33
34
	/**
35
	 * @var int
36
	 */
37
	protected $datamode;
38
39
	/**
40
	 * @var array
41
	 */
42
	protected $lengthBits = [0, 0, 0];
43
44
	/**
45
	 * @var int
46
	 */
47
	protected $version;
48
49
	/**
50
	 * @var array
51
	 */
52
	protected $ecdata;
53
54
	/**
55
	 * @var array
56
	 */
57
	protected $dcdata;
58
59
	/**
60
	 * @var array
61
	 */
62
	protected $matrixdata;
63
64
	/**
65
	 * @var \chillerlan\QRCode\QROptions
66
	 */
67
	protected $options;
68
69
	/**
70
	 * @var \chillerlan\QRCode\Helpers\BitBuffer
71
	 */
72
	protected $bitBuffer;
73
74
	/**
75
	 * QRDataInterface constructor.
76
	 *
77
	 * @param \chillerlan\QRCode\QROptions $options
78
	 * @param string|null                  $data
79
	 */
80
	public function __construct(QROptions $options, string $data = null){
81
		$this->options = $options;
82
83
		if($data !== null){
84
			$this->setData($data);
85
		}
86
	}
87
88
	/**
89
	 * @param string $data
90
	 *
91
	 * @return \chillerlan\QRCode\Data\QRDataInterface
92
	 */
93
	public function setData(string $data):QRDataInterface{
94
95
		if($this->datamode === QRCode::DATA_KANJI){
96
			$data = mb_convert_encoding($data, 'SJIS', mb_detect_encoding($data));
97
		}
98
99
		$this->strlen  = $this->getLength($data);
100
		$this->version = $this->options->version === QRCode::VERSION_AUTO
101
			? $this->getMinimumVersion()
102
			: $this->options->version;
103
104
		$this->matrixdata = $this
105
			->writeBitBuffer($data)
106
			->maskECC()
107
		;
108
109
		return $this;
110
	}
111
112
	/**
113
	 * @param int  $maskPattern
114
	 * @param bool $test
0 ignored issues
show
Documentation introduced by
Should the type for parameter $test not be null|boolean?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
115
	 *
116
	 * @return \chillerlan\QRCode\Data\QRMatrix
117
	 */
118
	public function initMatrix(int $maskPattern, bool $test = null):QRMatrix{
119
		/** @var \chillerlan\QRCode\Data\QRMatrix $matrix */
120
		$matrix = $this->loadClass(QRMatrix::class, null, $this->version, $this->options->eccLevel);
121
122
		return $matrix
123
			->setFinderPattern()
124
			->setSeparators()
125
			->setAlignmentPattern()
126
			->setTimingPattern()
127
			->setVersionNumber($test)
128
			->setFormatInfo($maskPattern, $test)
129
			->setDarkModule()
130
			->mapData($this->matrixdata, $maskPattern)
131
		;
132
	}
133
134
	/**
135
	 * @param int $version
136
	 *
137
	 * @return int
138
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
139
	 * @codeCoverageIgnore
140
	 */
141
	protected function getLengthBits(int $version):int {
142
143
		 foreach([9, 26, 40] as $key => $breakpoint){
144
			 if($version <= $breakpoint){
145
				 return $this->lengthBits[$key];
146
			 }
147
		 }
148
149
		throw new QRCodeDataException('invalid version number: '.$version);
150
	}
151
152
	/**
153
	 * returns the byte count of the string
154
	 *
155
	 * @param string $data
156
	 *
157
	 * @return int
158
	 */
159
	protected function getLength(string $data):int{
160
		return strlen($data);
161
	}
162
163
	/**
164
	 * @return int
165
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
166
	 */
167
	protected function getMinimumVersion():int{
168
169
		// guess the version number within the given range
170
		foreach(range(max(1, $this->options->versionMin), min($this->options->versionMax, 40)) as $version){
171
			$maxlength = self::MAX_LENGTH[$version][QRCode::DATA_MODES[$this->datamode]][QRCode::ECC_MODES[$this->options->eccLevel]];
172
173
			if($this->strlen <= $maxlength){
174
				return $version;
175
			}
176
		}
177
178
		throw new QRCodeDataException('data exceeds '.$maxlength.' characters');
179
	}
180
181
	/**
182
	 * writes the $data bits to $this->bitBuffer
183
	 *
184
	 * @param string $data
185
	 *
186
	 * @return void
187
	 */
188
	abstract protected function write(string $data);
189
190
	/**
191
	 * @param string $data
192
	 *
193
	 * @return \chillerlan\QRCode\Data\QRDataAbstract
194
	 * @throws \chillerlan\QRCode\QRCodeException
195
	 */
196
	protected function writeBitBuffer(string $data):QRDataInterface {
197
		$this->bitBuffer = new BitBuffer;
198
199
		// @todo: fixme, get real length
200
		$MAX_BITS = self::MAX_BITS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
201
202
		$this->bitBuffer
203
			->clear()
204
			->put($this->datamode, 4)
205
			->put($this->strlen, $this->getLengthBits($this->version))
206
		;
207
208
		$this->write($data);
209
210
		if($this->bitBuffer->length > $MAX_BITS){
211
			throw new QRCodeException('code length overflow. ('.$this->bitBuffer->length.' > '.$MAX_BITS.'bit)');
212
		}
213
214
		// end code.
215
		if($this->bitBuffer->length + 4 <= $MAX_BITS){
216
			$this->bitBuffer->put(0, 4);
217
		}
218
219
		// padding
220
		while($this->bitBuffer->length % 8 !== 0){
221
			$this->bitBuffer->putBit(false);
222
		}
223
224
		// padding
225
		while(true){
226
227
			if($this->bitBuffer->length >= $MAX_BITS){
228
				break;
229
			}
230
231
			$this->bitBuffer->put(0xEC, 8);
232
233
			if($this->bitBuffer->length >= $MAX_BITS){
234
				break;
235
			}
236
237
			$this->bitBuffer->put(0x11, 8);
238
		}
239
240
		return $this;
241
	}
242
243
	/**
244
	 * @link http://www.thonky.com/qr-code-tutorial/error-correction-coding
245
	 *
246
	 * @return array
247
	 */
248
	protected function maskECC():array {
249
		list($l1, $l2, $b1, $b2) = self::RSBLOCKS[$this->version][QRCode::ECC_MODES[$this->options->eccLevel]];
250
251
		$rsBlocks       = array_fill(0, $l1, [$b1, $b2]);
252
		$rsCount        = $l1 + $l2;
253
		$this->ecdata   = array_fill(0, $rsCount, null);
254
		$this->dcdata   = $this->ecdata;
255
256
		if($l2 > 0){
257
			$rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [$b1 + 1, $b2 + 1]));
258
		}
259
260
		$totalCodeCount = 0;
261
		$maxDcCount     = 0;
262
		$maxEcCount     = 0;
263
		$offset         = 0;
264
265
		foreach($rsBlocks as $key => $block){
266
			list($rsBlockTotal, $dcCount) = $block;
267
268
			$ecCount            = $rsBlockTotal - $dcCount;
269
			$maxDcCount         = max($maxDcCount, $dcCount);
270
			$maxEcCount         = max($maxEcCount, $ecCount);
271
			$this->dcdata[$key] = array_fill(0, $dcCount, null);
272
273
			foreach($this->dcdata[$key] as $a => $_z){
274
				$this->dcdata[$key][$a] = 0xff & $this->bitBuffer->buffer[$a + $offset];
275
			}
276
277
			list($num, $add) = $this->poly($key, $ecCount);
278
279
			foreach($this->ecdata[$key] as $c => $_z){
280
				$modIndex               = $c + $add;
281
				$this->ecdata[$key][$c] = $modIndex >= 0 ? $num[$modIndex] : 0;
282
			}
283
284
			$offset         += $dcCount;
285
			$totalCodeCount += $rsBlockTotal;
286
		}
287
288
		$data  = array_fill(0, $totalCodeCount, null);
289
		$index = 0;
290
291
		$mask = function($arr, $count) use (&$data, &$index, $rsCount){
292
			for($x = 0; $x < $count; $x++){
293
				for($y = 0; $y < $rsCount; $y++){
294
					if($x < count($arr[$y])){
295
						$data[$index] = $arr[$y][$x];
296
						$index++;
297
					}
298
				}
299
			}
300
		};
301
302
		$mask($this->dcdata, $maxDcCount);
303
		$mask($this->ecdata, $maxEcCount);
304
305
		return $data;
306
	}
307
308
	/**
309
	 * @param int $key
310
	 * @param int $count
311
	 *
312
	 * @return int[]
313
	 */
314
	protected function poly(int $key, int $count):array{
315
		$rsPoly  = new Polynomial;
316
		$modPoly = new Polynomial;
317
318
		for($i = 0; $i < $count; $i++){
319
			$modPoly->setNum([1, $modPoly->gexp($i)]);
320
			$rsPoly->multiply($modPoly->getNum());
321
		}
322
323
		$rsPolyCount = count($rsPoly->getNum());
324
325
		$modPoly
326
			->setNum($this->dcdata[$key], $rsPolyCount - 1)
327
			->mod($rsPoly->getNum())
328
		;
329
330
		$this->ecdata[$key] = array_fill(0, $rsPolyCount - 1, null);
331
		$num                = $modPoly->getNum();
332
333
		return [
334
			$num,
335
			count($num) - count($this->ecdata[$key]),
336
		];
337
	}
338
339
}
340