Passed
Push — main ( 0a0d87...5e9cf0 )
by smiley
02:11
created

QRCode::clearSegments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
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\{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};
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
	/** @var string */
37
	public const OUTPUT_MARKUP_HTML = 'html';
38
	/** @var string */
39
	public const OUTPUT_MARKUP_SVG  = 'svg';
40
	/** @var string */
41
	public const OUTPUT_IMAGE_PNG   = 'png';
42
	/** @var string */
43
	public const OUTPUT_IMAGE_JPG   = 'jpg';
44
	/** @var string */
45
	public const OUTPUT_IMAGE_GIF   = 'gif';
46
	/** @var string */
47
	public const OUTPUT_STRING_JSON = 'json';
48
	/** @var string */
49
	public const OUTPUT_STRING_TEXT = 'text';
50
	/** @var string */
51
	public const OUTPUT_IMAGICK     = 'imagick';
52
	/** @var string */
53
	public const OUTPUT_FPDF        = 'fpdf';
54
	/** @var string */
55
	public const OUTPUT_CUSTOM      = 'custom';
56
57
	/**
58
	 * Map of built-in output modules => capabilities
59
	 *
60
	 * @var string[][]
61
	 */
62
	public const OUTPUT_MODES = [
63
		QRMarkup::class => [
64
			self::OUTPUT_MARKUP_SVG,
65
			self::OUTPUT_MARKUP_HTML,
66
		],
67
		QRImage::class => [
68
			self::OUTPUT_IMAGE_PNG,
69
			self::OUTPUT_IMAGE_GIF,
70
			self::OUTPUT_IMAGE_JPG,
71
		],
72
		QRString::class => [
73
			self::OUTPUT_STRING_JSON,
74
			self::OUTPUT_STRING_TEXT,
75
		],
76
		QRImagick::class => [
77
			self::OUTPUT_IMAGICK,
78
		],
79
		QRFpdf::class => [
80
			self::OUTPUT_FPDF,
81
		],
82
	];
83
84
	/**
85
	 * The settings container
86
	 *
87
	 * @var \chillerlan\QRCode\QROptions|\chillerlan\Settings\SettingsContainerInterface
88
	 */
89
	protected SettingsContainerInterface $options;
90
91
	/**
92
	 * The selected data interface (Number, AlphaNum, Kanji, Byte)
93
	 */
94
	protected QRData $dataInterface;
95
96
	/**
97
	 * A collection of one or more data segments of [classname, data] to write
98
	 *
99
	 * @see \chillerlan\QRCode\Data\QRDataModeInterface
100
	 *
101
	 * @var \chillerlan\QRCode\Data\QRDataModeInterface[]
102
	 */
103
	protected array $dataSegments = [];
104
105
	/**
106
	 * The FQCN of the luminance sporce class to use in the reader (GD or Imagick)
107
	 *
108
	 * @see \chillerlan\QRCode\Decoder\LuminanceSourceInterface
109
	 */
110
	private string $luminanceSourceClass;
111
112
	/**
113
	 * QRCode constructor.
114
	 *
115
	 * Sets the options instance
116
	 */
117
	public function __construct(SettingsContainerInterface $options = null){
118
		$this->options              = $options ?? new QROptions;
119
		$this->luminanceSourceClass = $this->options->useImagickIfAvailable
120
			? IMagickLuminanceSource::class
121
			: GDLuminanceSource::class;
122
	}
123
124
	/**
125
	 * Renders a QR Code for the given $data and QROptions
126
	 *
127
	 * @return mixed
128
	 */
129
	public function render(string $data = null, string $file = null){
130
131
		if($data !== null){
132
			/** @var \chillerlan\QRCode\Data\QRDataModeInterface $dataInterface */
133
			foreach(Mode::DATA_INTERFACES as $dataInterface){
134
135
				if($dataInterface::validateString($data)){
136
					$this->addSegment(new $dataInterface($data));
137
138
					break;
139
				}
140
141
			}
142
143
		}
144
145
		return $this->initOutputInterface()->dump($file);
146
	}
147
148
	/**
149
	 * Returns a QRMatrix object for the given $data and current QROptions
150
	 *
151
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
152
	 */
153
	public function getMatrix():QRMatrix{
154
155
		if(empty($this->dataSegments)){
156
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
157
		}
158
159
		$this->dataInterface = new QRData($this->options, $this->dataSegments);
160
161
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
162
			? (new MaskPatternTester($this->dataInterface))->getBestMaskPattern()
163
			: 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

163
			: new MaskPattern(/** @scrutinizer ignore-type */ $this->options->maskPattern);
Loading history...
164
165
		$matrix = $this->dataInterface->writeMatrix($maskPattern);
166
167
		if($this->options->addQuietzone){
168
			$matrix->setQuietZone($this->options->quietzoneSize);
169
		}
170
171
		return $matrix;
172
	}
173
174
	/**
175
	 * returns a fresh (built-in) QROutputInterface
176
	 *
177
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
178
	 */
179
	protected function initOutputInterface():QROutputInterface{
180
181
		if($this->options->outputType === $this::OUTPUT_CUSTOM){
182
			return $this->initCustomOutputInterface();
183
		}
184
185
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
186
187
			if(in_array($this->options->outputType, $modes)){
188
				return new $outputInterface($this->options, $this->getMatrix());
189
			}
190
191
		}
192
193
		throw new QRCodeOutputException('invalid output type');
194
	}
195
196
	/**
197
	 * initializes a custom output module after checking the existence of the class and if it implemnts the required interface
198
	 *
199
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
200
	 */
201
	protected function initCustomOutputInterface():QROutputInterface{
202
203
		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

203
		if(!class_exists(/** @scrutinizer ignore-type */ $this->options->outputInterface)){
Loading history...
204
			throw new QRCodeOutputException('invalid custom output module');
205
		}
206
207
		if(!in_array(QROutputInterface::class, class_implements($this->options->outputInterface))){
208
			throw new QRCodeOutputException('custom output module does not implement QROutputInterface');
209
		}
210
211
		/** @phan-suppress-next-line PhanTypeExpectedObjectOrClassName */
212
		return new $this->options->outputInterface($this->options, $this->getMatrix());
213
	}
214
215
	/**
216
	 * checks if a string qualifies as numeric (convenience method)
217
	 */
218
	public function isNumber(string $string):bool{
219
		return Number::validateString($string);
220
	}
221
222
	/**
223
	 * checks if a string qualifies as alphanumeric (convenience method)
224
	 */
225
	public function isAlphaNum(string $string):bool{
226
		return AlphaNum::validateString($string);
227
	}
228
229
	/**
230
	 * checks if a string qualifies as Kanji (convenience method)
231
	 */
232
	public function isKanji(string $string):bool{
233
		return Kanji::validateString($string);
234
	}
235
236
	/**
237
	 * a dummy (convenience method)
238
	 */
239
	public function isByte(string $string):bool{
240
		return Byte::validateString($string);
241
	}
242
243
	/**
244
	 * ISO/IEC 18004:2000 8.3.6 - Mixing modes
245
	 * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length
246
	 */
247
	protected function addSegment(QRDataModeInterface $segment):void{
248
		$this->dataSegments[] = $segment;
249
	}
250
251
	/**
252
	 * ISO/IEC 18004:2000 8.3.2 - Numeric Mode
253
	 */
254
	public function addNumberSegment(string $data):self{
255
		$this->addSegment(new Number($data));
256
257
		return $this;
258
	}
259
260
	/**
261
	 * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode
262
	 */
263
	public function addAlphaNumSegment(string $data):self{
264
		$this->addSegment(new AlphaNum($data));
265
266
		return $this;
267
	}
268
269
	/**
270
	 * ISO/IEC 18004:2000 8.3.5 - Kanji Mode
271
	 */
272
	public function addKanjiSegment(string $data):self{
273
		$this->addSegment(new Kanji($data));
274
275
		return $this;
276
	}
277
278
	/**
279
	 * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode
280
	 */
281
	public function addByteSegment(string $data):self{
282
		$this->addSegment(new Byte($data));
283
284
		return $this;
285
	}
286
287
	/**
288
	 * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode
289
	 */
290
	public function addEciDesignator(int $encoding):self{
291
		$this->addSegment(new ECI($encoding));
292
293
		return $this;
294
	}
295
296
	/**
297
	 * i hate this somehow but i'll leave it for now
298
	 *
299
	 * @throws \chillerlan\QRCode\QRCodeException
300
	 */
301
	public function addEciSegment(int $encoding, string $data):self{
302
		// validate the encoding id
303
		$eciCharset = new ECICharset($encoding);
304
		// get charset name
305
		$eciCharsetName = $eciCharset->getName();
306
		// convert the string to the given charset
307
		if($eciCharsetName !== null){
308
			$data = mb_convert_encoding($data, $eciCharsetName, mb_detect_encoding($data));
309
			// add ECI designator
310
			$this->addSegment(new ECI($eciCharset->getID()));
311
			$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

311
			$this->addSegment(new Byte(/** @scrutinizer ignore-type */ $data));
Loading history...
312
313
			return $this;
314
		}
315
316
		throw new QRCodeException('unable to add ECI segment');
317
	}
318
319
	/**
320
	 * Clears the data segments array
321
	 */
322
	public function clearSegments():self{
323
		$this->dataSegments = [];
324
325
		return $this;
326
	}
327
328
	/**
329
	 * Reads a QR Code from a given file
330
	 */
331
	public function readFromFile(string $path):DecoderResult{
332
		/** @noinspection PhpUndefinedMethodInspection */
333
		return (new Decoder)->decode($this->luminanceSourceClass::fromFile($path));
334
	}
335
336
	/**
337
	 * Reads a QR Code from the given data blob
338
	 */
339
	public function readFromBlob(string $blob):DecoderResult{
340
		/** @noinspection PhpUndefinedMethodInspection */
341
		return (new Decoder)->decode($this->luminanceSourceClass::fromBlob($blob));
342
	}
343
344
}
345