Completed
Push — master ( 7ea7b3...88f800 )
by smiley
01:49
created

QRCode::getBestMaskPattern()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 0
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class QRCode
4
 *
5
 * @filesource   QRCode.php
6
 * @created      26.11.2015
7
 * @package      chillerlan\QRCode
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2015 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\QRCode;
14
15
use chillerlan\QRCode\Data\{
16
	AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix
17
};
18
use chillerlan\QRCode\Output\{
19
	QRCodeOutputException, QRImage, QRMarkup, QROutputInterface, QRString
20
};
21
use chillerlan\Traits\ClassLoader;
22
23
/**
24
 * Turns a text string into a Model 2 QR Code
25
 *
26
 * @link https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
27
 * @link http://www.qrcode.com/en/codes/model12.html
28
 * @link http://www.thonky.com/qr-code-tutorial/
29
 */
30
class QRCode{
31
	use ClassLoader;
32
33
	/**
34
	 * API constants
35
	 */
36
	const OUTPUT_MARKUP_HTML  = 'html';
37
	const OUTPUT_MARKUP_SVG   = 'svg';
38
#	const OUTPUT_MARKUP_XML   = 'xml'; // anyone?
39
40
	const OUTPUT_IMAGE_PNG    = 'png';
41
	const OUTPUT_IMAGE_JPG    = 'jpg';
42
	const OUTPUT_IMAGE_GIF    = 'gif';
43
44
	const OUTPUT_STRING_JSON  = 'json';
45
	const OUTPUT_STRING_TEXT  = 'text';
46
47
	const VERSION_AUTO        = -1;
48
	const MASK_PATTERN_AUTO   = -1;
49
50
	const ECC_L         = 0b01; // 7%.
51
	const ECC_M         = 0b00; // 15%.
52
	const ECC_Q         = 0b11; // 25%.
53
	const ECC_H         = 0b10; // 30%.
54
55
	const DATA_NUMBER   = 0b0001;
56
	const DATA_ALPHANUM = 0b0010;
57
	const DATA_BYTE     = 0b0100;
58
	const DATA_KANJI    = 0b1000;
59
60
	const ECC_MODES = [
61
		QRCode::ECC_L => 0,
62
		QRCode::ECC_M => 1,
63
		QRCode::ECC_Q => 2,
64
		QRCode::ECC_H => 3,
65
	];
66
67
	const DATA_MODES = [
68
		self::DATA_NUMBER   => 0,
69
		self::DATA_ALPHANUM => 1,
70
		self::DATA_BYTE     => 2,
71
		self::DATA_KANJI    => 3,
72
	];
73
74
	const OUTPUT_MODES = [
75
		QRMarkup::class => [
76
			self::OUTPUT_MARKUP_SVG,
77
			self::OUTPUT_MARKUP_HTML,
78
		],
79
		QRImage::class => [
80
			self::OUTPUT_IMAGE_PNG,
81
			self::OUTPUT_IMAGE_GIF,
82
			self::OUTPUT_IMAGE_JPG,
83
		],
84
		QRString::class => [
85
			self::OUTPUT_STRING_JSON,
86
			self::OUTPUT_STRING_TEXT,
87
		]
88
	];
89
90
	/**
91
	 * @var \chillerlan\QRCode\QROptions
92
	 */
93
	protected $options;
94
95
	/**
96
	 * @var \chillerlan\QRCode\Data\QRDataInterface
97
	 */
98
	protected $dataInterface;
99
100
	/**
101
	 * QRCode constructor.
102
	 *
103
	 * @param \chillerlan\QRCode\QROptions|null $options
104
	 */
105
	public function __construct(QROptions $options = null){
106
		mb_internal_encoding('UTF-8');
107
108
		$this->setOptions($options ?? new QROptions);
109
	}
110
111
	/**
112
	 * Sets the options, called internally by the constructor
113
	 *
114
	 * @param \chillerlan\QRCode\QROptions $options
115
	 *
116
	 * @return \chillerlan\QRCode\QRCode
117
	 * @throws \chillerlan\QRCode\QRCodeException
118
	 */
119
	public function setOptions(QROptions $options):QRCode{
120
121
		if(!array_key_exists(QRCode::ECC_MODES[$options->eccLevel], QRCode::ECC_MODES)){
122
			throw new QRCodeException('Invalid error correct level: '.$options->eccLevel);
123
		}
124
125
		if(!is_array($options->imageTransparencyBG || count($options->imageTransparencyBG) < 3)){
0 ignored issues
show
Bug Best Practice introduced by
The expression $options->imageTransparencyBG of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
126
			$options->imageTransparencyBG = [255, 255, 255];
127
		}
128
129
		$options->version = (int)$options->version;
130
131
		// clamp min/max version number
132
		$options->versionMin = (int)min($options->versionMin, $options->versionMax);
133
		$options->versionMax = (int)max($options->versionMin, $options->versionMax);
134
135
		$this->options = $options;
136
137
		return $this;
138
	}
139
140
	/**
141
	 * Renders a QR Code for the given $data and QROptions
142
	 *
143
	 * @param string $data
144
	 *
145
	 * @return mixed
146
	 */
147
	public function render(string $data){
148
		return $this->initOutputInterface($data)->dump();
149
	}
150
151
	/**
152
	 * Returns a QRMatrix object for the given $data and current QROptions
153
	 *
154
	 * @param string $data
155
	 *
156
	 * @return \chillerlan\QRCode\Data\QRMatrix
157
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
158
	 */
159
	public function getMatrix(string $data):QRMatrix {
160
		$data = trim($data);
161
162
		if(empty($data)){
163
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
164
		}
165
166
		$this->dataInterface = $this->initDataInterface($data);
167
168
		$maskPattern = $this->options->maskPattern === self::MASK_PATTERN_AUTO
169
			? $this->getBestMaskPattern()
170
			: max(7, min(0, (int)$this->options->maskPattern));
171
172
		$matrix = $this
173
			->dataInterface
174
			->initMatrix($maskPattern)
175
		;
176
177
		if((bool)$this->options->addQuietzone){
178
			$matrix->setQuietZone($this->options->quietzoneSize);
179
		}
180
181
		return $matrix;
182
	}
183
184
	/**
185
	 * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
186
	 *
187
	 * @see \chillerlan\QRCode\Data\MaskPatternTester
188
	 *
189
	 * @return int
190
	 */
191
	protected function getBestMaskPattern():int{
192
		$penalties = [];
193
194
		$tester = new MaskPatternTester;
195
196
		for($testPattern = 0; $testPattern < 8; $testPattern++){
197
			$matrix = $this
198
				->dataInterface
199
				->initMatrix($testPattern, true);
200
201
			$tester->setMatrix($matrix);
202
203
			$penalties[$testPattern] = $tester->testPattern();
204
		}
205
206
		return array_search(min($penalties), $penalties, true);
207
	}
208
209
	/**
210
	 * returns a fresh QRDataInterface for the given $data
211
	 *
212
	 * @param string                       $data
213
	 *
214
	 * @return \chillerlan\QRCode\Data\QRDataInterface
215
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
216
	 */
217
	public function initDataInterface(string $data):QRDataInterface{
218
219
		$DATA_MODES = [
220
			Number::class   => 'Number',
221
			AlphaNum::class => 'AlphaNum',
222
			Kanji::class    => 'Kanji',
223
			Byte::class     => 'Byte',
224
		];
225
226
		foreach($DATA_MODES as $dataInterface => $mode){
227
228
			if(call_user_func_array([$this, 'is'.$mode], [$data]) === true){
229
				return $this->loadClass($dataInterface, QRDataInterface::class, $this->options, $data);
230
			}
231
232
		}
233
234
		throw new QRCodeDataException('invalid data type');
235
	}
236
237
	/**
238
	 * returns a fresh (built-in) QROutputInterface
239
	 *
240
	 * @param string $data
241
	 *
242
	 * @return \chillerlan\QRCode\Output\QROutputInterface
243
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
244
	 */
245
	protected function initOutputInterface(string $data):QROutputInterface{
246
247
		foreach(self::OUTPUT_MODES as $outputInterface => $modes){
248
249
			if(in_array($this->options->outputType, $modes, true)){
250
				return $this->loadClass($outputInterface, QROutputInterface::class, $this->options, $this->getMatrix($data));
251
			}
252
253
		}
254
255
		throw new QRCodeOutputException('invalid output type');
256
	}
257
258
	/**
259
	 * checks of a string qualifies as numeric
260
	 *
261
	 * @param string $string
262
	 *
263
	 * @return bool
264
	 */
265 View Code Duplication
	public function isNumber(string $string):bool {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
		$len = strlen($string);
267
		$map = str_split('0123456789');
268
269
		for($i = 0; $i < $len; $i++){
270
			if(!in_array($string[$i], $map, true)){
271
				return false;
272
			}
273
		}
274
275
		return true;
276
	}
277
278
	/**
279
	 * checks of a string qualifies as alphanumeric
280
	 *
281
	 * @param string $string
282
	 *
283
	 * @return bool
284
	 */
285 View Code Duplication
	public function isAlphaNum(string $string):bool {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
		$len = strlen($string);
287
288
		for($i = 0; $i < $len; $i++){
289
			if(!in_array($string[$i], AlphaNum::CHAR_MAP, true)){
290
				return false;
291
			}
292
		}
293
294
		return true;
295
	}
296
297
	/**
298
	 * checks of a string qualifies as Kanji
299
	 *
300
	 * @param string $string
301
	 *
302
	 * @return bool
303
	 */
304
	public function isKanji(string $string):bool {
305
		$i   = 0;
306
		$len = strlen($string);
307
308
		while($i + 1 < $len){
309
			$c = ((0xff&ord($string[$i])) << 8)|(0xff&ord($string[$i + 1]));
310
311
			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
312
				return false;
313
			}
314
315
			$i += 2;
316
		}
317
318
		return !($i < $len);
319
	}
320
321
	/**
322
	 * a dummy
323
	 *
324
	 * @param $data
325
	 *
326
	 * @return bool
327
	 */
328
	protected function isByte(string $data):bool{
329
		return !empty($data);
330
	}
331
332
}
333