1 | <?php |
||
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 |