Passed
Push — main ( a9949d...738294 )
by smiley
01:55
created

QRCode::render()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 2
b 0
f 0
nc 4
nop 2
dl 0
loc 17
rs 10
1
<?php
2
/**
3
 * Class QRCode
4
 *
5
 * @created      26.11.2015
6
 * @author       Smiley <[email protected]>
7
 * @copyright    2015 Smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\QRCode;
12
13
use chillerlan\QRCode\Common\{EccLevel, ECICharset, MaskPattern, MaskPatternTester, Mode};
14
use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Kanji, Number, QRData, QRCodeDataException, QRDataModeInterface, QRMatrix};
15
use chillerlan\QRCode\Decoder\{Decoder, DecoderResult, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface};
16
use chillerlan\QRCode\Output\{QRCodeOutputException, QRFpdf, QRImage, QRImagick, QRMarkup, QROutputInterface, QRString};
17
use chillerlan\Settings\SettingsContainerInterface;
18
use function class_exists, class_implements, in_array, mb_convert_encoding, mb_detect_encoding;
19
20
/**
21
 * Turns a text string into a Model 2 QR Code
22
 *
23
 * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
24
 * @see http://www.qrcode.com/en/codes/model12.html
25
 * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
26
 * @see https://en.wikipedia.org/wiki/QR_code
27
 * @see http://www.thonky.com/qr-code-tutorial/
28
 */
29
class QRCode{
30
31
	/** @var int */
32
	public const VERSION_AUTO       = -1;
33
	/** @var int */
34
	public const MASK_PATTERN_AUTO  = -1;
35
36
	/**
37
	 * @deprecated backward compatibility
38
	 * @see \chillerlan\QRCode\Common\EccLevel
39
	 */
40
	/** @var int */
41
	public const ECC_L     = EccLevel::L;
42
	/** @var int */
43
	public const ECC_M     = EccLevel::M;
44
	/** @var int */
45
	public const ECC_Q     = EccLevel::Q;
46
	/** @var int */
47
	public const ECC_H     = EccLevel::H;
48
	/** @var int[] */
49
	public const ECC_MODES = EccLevel::MODES;
50
51
	/** @var string */
52
	public const OUTPUT_MARKUP_HTML = 'html';
53
	/** @var string */
54
	public const OUTPUT_MARKUP_SVG  = 'svg';
55
	/** @var string */
56
	public const OUTPUT_IMAGE_PNG   = 'png';
57
	/** @var string */
58
	public const OUTPUT_IMAGE_JPG   = 'jpg';
59
	/** @var string */
60
	public const OUTPUT_IMAGE_GIF   = 'gif';
61
	/** @var string */
62
	public const OUTPUT_STRING_JSON = 'json';
63
	/** @var string */
64
	public const OUTPUT_STRING_TEXT = 'text';
65
	/** @var string */
66
	public const OUTPUT_IMAGICK     = 'imagick';
67
	/** @var string */
68
	public const OUTPUT_FPDF        = 'fpdf';
69
	/** @var string */
70
	public const OUTPUT_CUSTOM      = 'custom';
71
72
	/**
73
	 * Map of built-in output modules => capabilities
74
	 *
75
	 * @var string[][]
76
	 */
77
	public const OUTPUT_MODES = [
78
		QRMarkup::class => [
79
			self::OUTPUT_MARKUP_SVG,
80
			self::OUTPUT_MARKUP_HTML,
81
		],
82
		QRImage::class => [
83
			self::OUTPUT_IMAGE_PNG,
84
			self::OUTPUT_IMAGE_GIF,
85
			self::OUTPUT_IMAGE_JPG,
86
		],
87
		QRString::class => [
88
			self::OUTPUT_STRING_JSON,
89
			self::OUTPUT_STRING_TEXT,
90
		],
91
		QRImagick::class => [
92
			self::OUTPUT_IMAGICK,
93
		],
94
		QRFpdf::class => [
95
			self::OUTPUT_FPDF,
96
		],
97
	];
98
99
	/**
100
	 * The settings container
101
	 *
102
	 * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
103
	 */
104
	protected SettingsContainerInterface $options;
105
106
	/**
107
	 * The selected data interface (Number, AlphaNum, Kanji, Byte)
108
	 */
109
	protected QRData $dataInterface;
110
111
	/**
112
	 * A collection of one or more data segments of [classname, data] to write
113
	 *
114
	 * @see \chillerlan\QRCode\Data\QRDataModeInterface
115
	 *
116
	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
117
	 */
118
	protected array $dataSegments = [];
119
120
	/**
121
	 * The FQCN of the luminance sporce class to use in the reader (GD or Imagick)
122
	 *
123
	 * @see \chillerlan\QRCode\Decoder\LuminanceSourceInterface
124
	 */
125
	private string $luminanceSourceClass;
126
127
	/**
128
	 * QRCode constructor.
129
	 *
130
	 * Sets the options instance
131
	 */
132
	public function __construct(SettingsContainerInterface $options = null){
133
		$this->options              = $options ?? new QROptions;
134
		// i hate this
135
		$this->luminanceSourceClass = $this->options->useImagickIfAvailable
136
			? IMagickLuminanceSource::class
137
			: GDLuminanceSource::class;
138
	}
139
140
	/**
141
	 * Renders a QR Code for the given $data and QROptions
142
	 *
143
	 * @return mixed
144
	 */
145
	public function render(string $data = null, string $file = null){
146
147
		if($data !== null){
148
			/** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
149
			foreach(Mode::DATA_INTERFACES as $dataInterface){
150
151
				if($dataInterface::validateString($data)){
152
					$this->addSegment(new $dataInterface($data));
153
154
					break;
155
				}
156
157
			}
158
159
		}
160
161
		return $this->initOutputInterface()->dump($file);
162
	}
163
164
	/**
165
	 * Returns a QRMatrix object for the given $data and current QROptions
166
	 *
167
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
168
	 */
169
	public function getMatrix():QRMatrix{
170
171
		if(empty($this->dataSegments)){
172
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
173
		}
174
175
		$this->dataInterface = new QRData($this->options, $this->dataSegments);
176
177
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
178
			? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern()
179
			: new MaskPattern($this->options->maskPattern);
0 ignored issues
show
Bug introduced by
It seems like $this->options->maskPattern can also be of type null; however, parameter $maskPattern of chillerlan\QRCode\Common...kPattern::__construct() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

179
			: new MaskPattern(/** @scrutinizer ignore-type */ $this->options->maskPattern);
Loading history...
180
181
		$matrix = $this->dataInterface->writeMatrix($maskPattern);
182
183
		if($this->options->addQuietzone){
184
			$matrix->setQuietZone($this->options->quietzoneSize);
185
		}
186
187
		return $matrix;
188
	}
189
190
	/**
191
	 * returns a fresh (built-in) QROutputInterface
192
	 *
193
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
194
	 */
195
	protected function initOutputInterface():QROutputInterface{
196
197
		if($this->options->outputType === $this::OUTPUT_CUSTOM){
198
			return $this->initCustomOutputInterface();
199
		}
200
201
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
202
203
			if(in_array($this->options->outputType, $modes)){
204
				return new $outputInterface($this->options, $this->getMatrix());
205
			}
206
207
		}
208
209
		throw new QRCodeOutputException('invalid output type');
210
	}
211
212
	/**
213
	 * initializes a custom output module after checking the existence of the class and if it implemnts the required interface
214
	 *
215
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
216
	 */
217
	protected function initCustomOutputInterface():QROutputInterface{
218
219
		if(!class_exists($this->options->outputInterface)){
0 ignored issues
show
Bug introduced by
It seems like $this->options->outputInterface can also be of type null; however, parameter $class of class_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

219
		if(!class_exists(/** @scrutinizer ignore-type */ $this->options->outputInterface)){
Loading history...
220
			throw new QRCodeOutputException('invalid custom output module');
221
		}
222
223
		if(!in_array(QROutputInterface::class, class_implements($this->options->outputInterface))){
224
			throw new QRCodeOutputException('custom output module does not implement QROutputInterface');
225
		}
226
227
		/** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName */
228
		return new $this->options->outputInterface($this->options, $this->getMatrix());
229
	}
230
231
	/**
232
	 * checks if a string qualifies as numeric (convenience method)
233
	 */
234
	public function isNumber(string $string):bool{
235
		return Number::validateString($string);
236
	}
237
238
	/**
239
	 * checks if a string qualifies as alphanumeric (convenience method)
240
	 */
241
	public function isAlphaNum(string $string):bool{
242
		return AlphaNum::validateString($string);
243
	}
244
245
	/**
246
	 * checks if a string qualifies as Kanji (convenience method)
247
	 */
248
	public function isKanji(string $string):bool{
249
		return Kanji::validateString($string);
250
	}
251
252
	/**
253
	 * a dummy (convenience method)
254
	 */
255
	public function isByte(string $string):bool{
256
		return Byte::validateString($string);
257
	}
258
259
	/**
260
	 * Adds a data segment
261
	 *
262
	 * ISO/IEC 18004:2000 8.3.6 - Mixing modes
263
	 * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
264
	 */
265
	protected function addSegment(QRDataModeInterface $segment):void{
266
		$this->dataSegments[] = $segment;
267
	}
268
269
	/**
270
	 * Clears the data segments array
271
	 */
272
	public function clearSegments():self{
273
		$this->dataSegments = [];
274
275
		return $this;
276
	}
277
278
	/**
279
	 * Adds a numeric data segment
280
	 *
281
	 * ISO/IEC 18004:2000 8.3.2 - Numeric Mode
282
	 */
283
	public function addNumericSegment(string $data):self{
284
		$this->addSegment(new Number($data));
285
286
		return $this;
287
	}
288
289
	/**
290
	 * Adds an alphanumeric data segment
291
	 *
292
	 * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
293
	 */
294
	public function addAlphaNumSegment(string $data):self{
295
		$this->addSegment(new AlphaNum($data));
296
297
		return $this;
298
	}
299
300
	/**
301
	 * Adds a Kanji data segment
302
	 *
303
	 * ISO/IEC 18004:2000 8.3.5 - Kanji Mode
304
	 */
305
	public function addKanjiSegment(string $data):self{
306
		$this->addSegment(new Kanji($data));
307
308
		return $this;
309
	}
310
311
	/**
312
	 * Adds an 8-bit byte data segment
313
	 *
314
	 * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
315
	 */
316
	public function addByteSegment(string $data):self{
317
		$this->addSegment(new Byte($data));
318
319
		return $this;
320
	}
321
322
	/**
323
	 * Adds a standalone ECI designator
324
	 *
325
	 * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
326
	 */
327
	public function addEciDesignator(int $encoding):self{
328
		$this->addSegment(new ECI($encoding));
329
330
		return $this;
331
	}
332
333
	/**
334
	 * Adds an ECI data segment (including designator)
335
	 *
336
	 * i hate this somehow but i'll leave it for now
337
	 *
338
	 * @throws \chillerlan\QRCode\QRCodeException
339
	 */
340
	public function addEciSegment(int $encoding, string $data):self{
341
		// validate the encoding id
342
		$eciCharset = new ECICharset($encoding);
343
		// get charset name
344
		$eciCharsetName = $eciCharset->getName();
345
		// convert the string to the given charset
346
		if($eciCharsetName !== null){
347
			$data = mb_convert_encoding($data, $eciCharsetName, mb_detect_encoding($data));
348
			// add ECI designator
349
			$this->addSegment(new ECI($eciCharset->getID()));
350
			$this->addSegment(new Byte($data));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type array; however, parameter $data of chillerlan\QRCode\Data\Byte::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

350
			$this->addSegment(new Byte(/** @scrutinizer ignore-type */ $data));
Loading history...
351
352
			return $this;
353
		}
354
355
		throw new QRCodeException('unable to add ECI segment');
356
	}
357
358
	/**
359
	 * Reads a QR Code from a given file
360
	 */
361
	public function readFromFile(string $path):DecoderResult{
362
		/** @noinspection PhpUndefinedMethodInspection */
363
		return $this->readFromSource($this->luminanceSourceClass::fromFile($path));
364
	}
365
366
	/**
367
	 * Reads a QR Code from the given data blob
368
	 */
369
	public function readFromBlob(string $blob):DecoderResult{
370
		/** @noinspection PhpUndefinedMethodInspection */
371
		return $this->readFromSource($this->luminanceSourceClass::fromBlob($blob));
372
	}
373
374
	/**
375
	 * Reads a QR Code from the given luminance source
376
	 */
377
	public function readFromSource(LuminanceSourceInterface $source):DecoderResult{
378
		/** @noinspection PhpUndefinedMethodInspection */
379
		return (new Decoder)->decode($source);
380
	}
381
382
}
383