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

QRCode::isAlphaNum()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 1
b 0
f 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, 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, mb_internal_encoding, 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_CUSTOM      = 'custom';
46
47
	public const VERSION_AUTO       = -1;
48
	public const MASK_PATTERN_AUTO  = -1;
49
50
	public const ECC_L         = 0b01; // 7%.
51
	public const ECC_M         = 0b00; // 15%.
52
	public const ECC_Q         = 0b11; // 25%.
53
	public const ECC_H         = 0b10; // 30%.
54
55
	public const DATA_NUMBER   = 0b0001;
56
	public const DATA_ALPHANUM = 0b0010;
57
	public const DATA_BYTE     = 0b0100;
58
	public const DATA_KANJI    = 0b1000;
59
60
	public const ECC_MODES = [
61
		self::ECC_L => 0,
62
		self::ECC_M => 1,
63
		self::ECC_Q => 2,
64
		self::ECC_H => 3,
65
	];
66
67
	public 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
	public 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
		QRImagick::class => [
89
			self::OUTPUT_IMAGICK,
90
		],
91
	];
92
93
	/**
94
	 * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
95
	 */
96
	protected SettingsContainerInterface $options;
97
98
	protected QRDataInterface $dataInterface;
99
100
	/**
101
	 * @see http://php.net/manual/function.mb-internal-encoding.php
102
	 */
103
	protected string $mbCurrentEncoding;
104
105
	/**
106
	 * QRCode constructor.
107
	 */
108
	public function __construct(SettingsContainerInterface $options = null){
109
		// save the current mb encoding (in case it differs from UTF-8)
110
		$this->mbCurrentEncoding = mb_internal_encoding();
111
		// use UTF-8 from here on
112
		mb_internal_encoding('UTF-8');
113
114
		$this->options = $options ?? new QROptions;
115
	}
116
117
	/**
118
	 * @return void
119
	 */
120
	public function __destruct(){
121
		// restore the previous mb_internal_encoding, so that we don't mess up the rest of the script
122
		mb_internal_encoding($this->mbCurrentEncoding);
123
	}
124
125
	/**
126
	 * Renders a QR Code for the given $data and QROptions
127
	 *
128
	 * @return mixed
129
	 */
130
	public function render(string $data, string $file = null){
131
		return $this->initOutputInterface($data)->dump($file);
132
	}
133
134
	/**
135
	 * Returns a QRMatrix object for the given $data and current QROptions
136
	 *
137
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
138
	 */
139
	public function getMatrix(string $data):QRMatrix{
140
141
		if(empty($data)){
142
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
143
		}
144
145
		$this->dataInterface = $this->initDataInterface($data);
146
147
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
148
			? $this->getBestMaskPattern()
149
			: $this->options->maskPattern;
150
151
		$matrix = $this->dataInterface->initMatrix($maskPattern);
152
153
		if((bool)$this->options->addQuietzone){
154
			$matrix->setQuietZone($this->options->quietzoneSize);
155
		}
156
157
		return $matrix;
158
	}
159
160
	/**
161
	 * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
162
	 *
163
	 * @see \chillerlan\QRCode\Data\MaskPatternTester
164
	 */
165
	protected function getBestMaskPattern():int{
166
		$penalties = [];
167
168
		for($pattern = 0; $pattern < 8; $pattern++){
169
			$tester = new MaskPatternTester($this->dataInterface->initMatrix($pattern, true));
170
171
			$penalties[$pattern] = $tester->testPattern();
172
		}
173
174
		return array_search(min($penalties), $penalties, true);
175
	}
176
177
	/**
178
	 * returns a fresh QRDataInterface for the given $data
179
	 *
180
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
181
	 */
182
	public function initDataInterface(string $data):QRDataInterface{
183
		$dataModes     = ['Number', 'AlphaNum', 'Kanji', 'Byte'];
184
		$dataNamespace = __NAMESPACE__.'\\Data\\';
185
186
		// allow forcing the data mode
187
		// see https://github.com/chillerlan/php-qrcode/issues/39
188
		if(in_array($this->options->dataMode, $dataModes, true)){
189
			$dataInterface = $dataNamespace.$this->options->dataMode;
190
191
			return new $dataInterface($this->options, $data);
192
		}
193
194
		foreach($dataModes as $mode){
195
			$dataInterface = $dataNamespace.$mode;
196
197
			if(call_user_func_array([$this, 'is'.$mode], [$data]) && class_exists($dataInterface)){
198
				return new $dataInterface($this->options, $data);
199
			}
200
201
		}
202
203
		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
204
	}
205
206
	/**
207
	 * returns a fresh (built-in) QROutputInterface
208
	 *
209
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
210
	 */
211
	protected function initOutputInterface(string $data):QROutputInterface{
212
213
		if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){
214
			return new $this->options->outputInterface($this->options, $this->getMatrix($data));
215
		}
216
217
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
218
219
			if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){
220
				return new $outputInterface($this->options, $this->getMatrix($data));
221
			}
222
223
		}
224
225
		throw new QRCodeOutputException('invalid output type');
226
	}
227
228
	/**
229
	 * checks if a string qualifies as numeric
230
	 */
231
	public function isNumber(string $string):bool{
232
		return $this->checkString($string, QRDataInterface::NUMBER_CHAR_MAP);
233
	}
234
235
	/**
236
	 * checks if a string qualifies as alphanumeric
237
	 */
238
	public function isAlphaNum(string $string):bool{
239
		return $this->checkString($string, QRDataInterface::ALPHANUM_CHAR_MAP);
240
	}
241
242
	/**
243
	 * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
244
	 */
245
	protected function checkString(string $string, array $charmap):bool{
246
		$len = strlen($string);
247
248
		for($i = 0; $i < $len; $i++){
249
			if(!in_array($string[$i], $charmap, true)){
250
				return false;
251
			}
252
		}
253
254
		return true;
255
	}
256
257
	/**
258
	 * checks if a string qualifies as Kanji
259
	 */
260
	public function isKanji(string $string):bool{
261
		$i   = 0;
262
		$len = strlen($string);
263
264
		while($i + 1 < $len){
265
			$c = ((0xff & ord($string[$i])) << 8) | (0xff & ord($string[$i + 1]));
266
267
			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
268
				return false;
269
			}
270
271
			$i += 2;
272
		}
273
274
		return $i >= $len;
275
	}
276
277
	/**
278
	 * a dummy
279
	 */
280
	protected function isByte(string $data):bool{
281
		return !empty($data);
282
	}
283
284
}
285