Completed
Push — v3.2.x ( 952323...cba5a1 )
by smiley
06:51 queued 05:30
created

QRCode   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

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

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A render() 0 3 1
A getMatrix() 0 20 4
A getBestMaskPattern() 0 11 2
A initDataInterface() 0 23 5
A initOutputInterface() 0 16 6
A isNumber() 0 3 1
A isAlphaNum() 0 3 1
A checkString() 0 11 3
A isKanji() 0 16 6
A isByte() 0 3 1
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
	MaskPatternTester, QRCodeDataException, QRDataInterface, QRMatrix
17
};
18
use chillerlan\QRCode\Output\{
19
	QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString
20
};
21
use chillerlan\Settings\SettingsContainerInterface;
22
23
use function array_search, call_user_func_array, class_exists, in_array, min, ord, strlen;
24
25
/**
26
 * Turns a text string into a Model 2 QR Code
27
 *
28
 * @link https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
29
 * @link http://www.qrcode.com/en/codes/model12.html
30
 * @link http://www.thonky.com/qr-code-tutorial/
31
 */
32
class QRCode{
33
34
	/**
35
	 * API constants
36
	 */
37
	public const OUTPUT_MARKUP_HTML = 'html';
38
	public const OUTPUT_MARKUP_SVG  = 'svg';
39
	public const OUTPUT_IMAGE_PNG   = 'png';
40
	public const OUTPUT_IMAGE_JPG   = 'jpg';
41
	public const OUTPUT_IMAGE_GIF   = 'gif';
42
	public const OUTPUT_STRING_JSON = 'json';
43
	public const OUTPUT_STRING_TEXT = 'text';
44
	public const OUTPUT_IMAGICK     = 'imagick';
45
	public const OUTPUT_FPDF        = 'fpdf';
46
	public const OUTPUT_CUSTOM      = 'custom';
47
48
	public const VERSION_AUTO       = -1;
49
	public const MASK_PATTERN_AUTO  = -1;
50
51
	public const ECC_L         = 0b01; // 7%.
52
	public const ECC_M         = 0b00; // 15%.
53
	public const ECC_Q         = 0b11; // 25%.
54
	public const ECC_H         = 0b10; // 30%.
55
56
	public const DATA_NUMBER   = 0b0001;
57
	public const DATA_ALPHANUM = 0b0010;
58
	public const DATA_BYTE     = 0b0100;
59
	public const DATA_KANJI    = 0b1000;
60
61
	public const ECC_MODES = [
62
		self::ECC_L => 0,
63
		self::ECC_M => 1,
64
		self::ECC_Q => 2,
65
		self::ECC_H => 3,
66
	];
67
68
	public const DATA_MODES = [
69
		self::DATA_NUMBER   => 0,
70
		self::DATA_ALPHANUM => 1,
71
		self::DATA_BYTE     => 2,
72
		self::DATA_KANJI    => 3,
73
	];
74
75
	public const OUTPUT_MODES = [
76
		QRMarkup::class => [
77
			self::OUTPUT_MARKUP_SVG,
78
			self::OUTPUT_MARKUP_HTML,
79
		],
80
		QRImage::class => [
81
			self::OUTPUT_IMAGE_PNG,
82
			self::OUTPUT_IMAGE_GIF,
83
			self::OUTPUT_IMAGE_JPG,
84
		],
85
		QRString::class => [
86
			self::OUTPUT_STRING_JSON,
87
			self::OUTPUT_STRING_TEXT,
88
		],
89
		QRImagick::class => [
90
			self::OUTPUT_IMAGICK,
91
		],
92
		QRFpdf::class => [
93
			self::OUTPUT_FPDF
94
		]
95
	];
96
97
	/**
98
	 * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
99
	 */
100
	protected $options;
101
102
	/**
103
	 * @var \chillerlan\QRCode\Data\QRDataInterface
104
	 */
105
	protected $dataInterface;
106
107
	/**
108
	 * QRCode constructor.
109
	 *
110
	 * @param \chillerlan\Settings\SettingsContainerInterface|null $options
111
	 */
112
	public function __construct(SettingsContainerInterface $options = null){
113
		$this->options = $options ?? new QROptions;
114
	}
115
116
	/**
117
	 * Renders a QR Code for the given $data and QROptions
118
	 *
119
	 * @param string      $data
120
	 * @param string|null $file
121
	 *
122
	 * @return mixed
123
	 */
124
	public function render(string $data, string $file = null){
125
		return $this->initOutputInterface($data)->dump($file);
126
	}
127
128
	/**
129
	 * Returns a QRMatrix object for the given $data and current QROptions
130
	 *
131
	 * @param string $data
132
	 *
133
	 * @return \chillerlan\QRCode\Data\QRMatrix
134
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
135
	 */
136
	public function getMatrix(string $data):QRMatrix{
137
138
		if(empty($data)){
139
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
140
		}
141
142
		$this->dataInterface = $this->initDataInterface($data);
143
144
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
145
			? $this->getBestMaskPattern()
146
			: $this->options->maskPattern;
147
148
		$matrix = $this->dataInterface->initMatrix($maskPattern);
149
150
		if((bool)$this->options->addQuietzone){
151
			$matrix->setQuietZone($this->options->quietzoneSize);
152
		}
153
154
		return $matrix;
155
	}
156
157
	/**
158
	 * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
159
	 *
160
	 * @see \chillerlan\QRCode\Data\MaskPatternTester
161
	 *
162
	 * @return int
163
	 */
164
	protected function getBestMaskPattern():int{
165
		$penalties = [];
166
167
		for($pattern = 0; $pattern < 8; $pattern++){
168
			$tester = new MaskPatternTester($this->dataInterface->initMatrix($pattern, true));
169
170
			$penalties[$pattern] = $tester->testPattern();
171
		}
172
173
		return array_search(min($penalties), $penalties, true);
174
	}
175
176
	/**
177
	 * returns a fresh QRDataInterface for the given $data
178
	 *
179
	 * @param string                       $data
180
	 *
181
	 * @return \chillerlan\QRCode\Data\QRDataInterface
182
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
183
	 */
184
	public function initDataInterface(string $data):QRDataInterface{
185
		$dataModes     = ['Number', 'AlphaNum', 'Kanji', 'Byte'];
186
		$dataNamespace = __NAMESPACE__.'\\Data\\';
187
188
		// allow forcing the data mode
189
		// see https://github.com/chillerlan/php-qrcode/issues/39
190
		if(in_array($this->options->dataMode, $dataModes, true)){
191
			$dataInterface = $dataNamespace.$this->options->dataMode;
192
193
			return new $dataInterface($this->options, $data);
194
		}
195
196
		foreach($dataModes as $mode){
197
			$dataInterface = $dataNamespace.$mode;
198
199
			if(call_user_func_array([$this, 'is'.$mode], [$data]) && class_exists($dataInterface)){
200
				return new $dataInterface($this->options, $data);
201
			}
202
203
		}
204
205
		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
206
	}
207
208
	/**
209
	 * returns a fresh (built-in) QROutputInterface
210
	 *
211
	 * @param string $data
212
	 *
213
	 * @return \chillerlan\QRCode\Output\QROutputInterface
214
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
215
	 */
216
	protected function initOutputInterface(string $data):QROutputInterface{
217
218
		if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){
219
			return new $this->options->outputInterface($this->options, $this->getMatrix($data));
220
		}
221
222
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
223
224
			if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){
225
				return new $outputInterface($this->options, $this->getMatrix($data));
226
			}
227
228
		}
229
230
		throw new QRCodeOutputException('invalid output type');
231
	}
232
233
	/**
234
	 * checks if a string qualifies as numeric
235
	 *
236
	 * @param string $string
237
	 *
238
	 * @return bool
239
	 */
240
	public function isNumber(string $string):bool{
241
		return $this->checkString($string, QRDataInterface::NUMBER_CHAR_MAP);
242
	}
243
244
	/**
245
	 * checks if a string qualifies as alphanumeric
246
	 *
247
	 * @param string $string
248
	 *
249
	 * @return bool
250
	 */
251
	public function isAlphaNum(string $string):bool{
252
		return $this->checkString($string, QRDataInterface::ALPHANUM_CHAR_MAP);
253
	}
254
255
	/**
256
	 * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
257
	 *
258
	 * @param string $string
259
	 * @param array  $charmap
260
	 *
261
	 * @return bool
262
	 */
263
	protected function checkString(string $string, array $charmap):bool{
264
		$len = strlen($string);
265
266
		for($i = 0; $i < $len; $i++){
267
			if(!in_array($string[$i], $charmap, true)){
268
				return false;
269
			}
270
		}
271
272
		return true;
273
	}
274
275
	/**
276
	 * checks if a string qualifies as Kanji
277
	 *
278
	 * @param string $string
279
	 *
280
	 * @return bool
281
	 */
282
	public function isKanji(string $string):bool{
283
		$i   = 0;
284
		$len = strlen($string);
285
286
		while($i + 1 < $len){
287
			$c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1]));
288
289
			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
290
				return false;
291
			}
292
293
			$i += 2;
294
		}
295
296
		return $i >= $len;
297
	}
298
299
	/**
300
	 * a dummy
301
	 *
302
	 * @param $data
303
	 *
304
	 * @return bool
305
	 */
306
	protected function isByte(string $data):bool{
307
		return !empty($data);
308
	}
309
310
}
311