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)){ |
|
|
|
|
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 { |
|
|
|
|
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 { |
|
|
|
|
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
|
|
|
|
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.