├── LICENSE-ASL-2.0 ├── LICENSE-MIT ├── NOTICE ├── README.md ├── composer.json └── src ├── Common ├── BitBuffer.php ├── ECICharset.php ├── EccLevel.php ├── GDLuminanceSource.php ├── GF256.php ├── GenericGFPoly.php ├── IMagickLuminanceSource.php ├── LuminanceSourceAbstract.php ├── LuminanceSourceInterface.php ├── MaskPattern.php ├── Mode.php └── Version.php ├── Data ├── AlphaNum.php ├── Byte.php ├── ECI.php ├── Hanzi.php ├── Kanji.php ├── Number.php ├── QRCodeDataException.php ├── QRData.php ├── QRDataModeAbstract.php ├── QRDataModeInterface.php ├── QRMatrix.php └── ReedSolomonEncoder.php ├── Decoder ├── Binarizer.php ├── BitMatrix.php ├── Decoder.php ├── DecoderResult.php ├── QRCodeDecoderException.php └── ReedSolomonDecoder.php ├── Detector ├── AlignmentPattern.php ├── AlignmentPatternFinder.php ├── Detector.php ├── FinderPattern.php ├── FinderPatternFinder.php ├── GridSampler.php ├── PerspectiveTransform.php ├── QRCodeDetectorException.php └── ResultPoint.php ├── Output ├── CssColorModuleValueTrait.php ├── QRCodeOutputException.php ├── QREps.php ├── QRFpdf.php ├── QRGdImage.php ├── QRGdImageAVIF.php ├── QRGdImageBMP.php ├── QRGdImageGIF.php ├── QRGdImageJPEG.php ├── QRGdImagePNG.php ├── QRGdImageWEBP.php ├── QRImagick.php ├── QRInterventionImage.php ├── QRMarkup.php ├── QRMarkupHTML.php ├── QRMarkupSVG.php ├── QRMarkupXML.php ├── QROutputAbstract.php ├── QROutputInterface.php ├── QRStringJSON.php ├── QRStringText.php ├── RGBArrayModuleValueTrait.php ├── qrcode.schema.json └── qrcode.schema.xsd ├── QRCode.php ├── QRCodeException.php ├── QRCodeReaderOptionsTrait.php ├── QROptions.php └── QROptionsTrait.php /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Smiley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Parts of this code are ported to php from the ZXing project 2 | and licensed under the Apache License, Version 2.0. 3 | 4 | Copyright 2007 ZXing authors (https://github.com/zxing/zxing), 5 | Copyright (c) Ashot Khanamiryan (https://github.com/khanamiryan/php-qrcode-detector-decoder) 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | 20 | List of affected files: 21 | 22 | src/Common/ECICharset.php 23 | src/Common/GenericGFPoly.php 24 | src/Common/GF256.php 25 | src/Common/LuminanceSourceAbstract.php 26 | src/Common/MaskPattern.php 27 | src/Decoder/Binarizer.php 28 | src/Decoder/BitMatrix.php 29 | src/Decoder/Decoder.php 30 | src/Decoder/DecoderResult.php 31 | src/Decoder/ReedSolomonDecoder.php 32 | src/Detector/AlignmentPattern.php 33 | src/Detector/AlignmentPatternFinder.php 34 | src/Detector/Detector.php 35 | src/Detector/FinderPattern.php 36 | src/Detector/FinderPatternFinder.php 37 | src/Detector/GridSampler.php 38 | src/Detector/PerspectiveTransform.php 39 | src/Detector/ResultPoint.php 40 | tests/Common/MaskPatternTest.php 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chillerlan/php-qrcode 2 | 3 | A PHP QR Code generator based on the [implementation by Kazuhiko Arase](https://github.com/kazuhikoarase/qrcode-generator), namespaced, cleaned up, improved and other stuff.
4 | It also features a QR Code reader based on a [PHP port](https://github.com/khanamiryan/php-qrcode-detector-decoder) of the [ZXing library](https://github.com/zxing/zxing). 5 | 6 | **Attention:** there is now also a javascript port on NPM: [@chillerlan/qrcode](https://www.npmjs.com/package/@chillerlan/qrcode). 7 | 8 | [![PHP Version Support][php-badge]][php] 9 | [![Packagist version][packagist-badge]][packagist] 10 | [![Continuous Integration][gh-action-badge]][gh-action] 11 | [![CodeCov][coverage-badge]][coverage] 12 | [![Codacy][codacy-badge]][codacy] 13 | [![Packagist downloads][downloads-badge]][downloads] 14 | [![Documentation][readthedocs-badge]][readthedocs] 15 | 16 | [php-badge]: https://img.shields.io/packagist/php-v/chillerlan/php-qrcode?logo=php&color=8892BF&logoColor=fff 17 | [php]: https://www.php.net/supported-versions.php 18 | [packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-qrcode.svg?logo=packagist&logoColor=fff 19 | [packagist]: https://packagist.org/packages/chillerlan/php-qrcode 20 | [gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/php-qrcode/ci.yml?branch=main&logo=github&logoColor=fff 21 | [gh-action]: https://github.com/chillerlan/php-qrcode/actions/workflows/ci.yml?query=branch%3Amain 22 | [coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-qrcode/main?logo=codecov&logoColor=fff 23 | [coverage]: https://app.codecov.io/gh/chillerlan/php-qrcode/tree/main 24 | [codacy-badge]: https://img.shields.io/codacy/grade/edccfc4fe5a34b74b1c53ee03f097b8d/main?logo=codacy&logoColor=fff 25 | [codacy]: https://app.codacy.com/gh/chillerlan/php-qrcode/dashboard?branch=main 26 | [downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-qrcode?logo=packagist&logoColor=fff 27 | [downloads]: https://packagist.org/packages/chillerlan/php-qrcode/stats 28 | [readthedocs-badge]: https://img.shields.io/readthedocs/php-qrcode/main?logo=readthedocs&logoColor=fff 29 | [readthedocs]: https://php-qrcode.readthedocs.io/en/main/ 30 | 31 | # Overview 32 | 33 | ## Features 34 | 35 | - Creation of [Model 2 QR Codes](https://www.qrcode.com/en/codes/model12.html), [Version 1 to 40](https://www.qrcode.com/en/about/version.html) 36 | - [ECC Levels](https://www.qrcode.com/en/about/error_correction.html) L/M/Q/H supported 37 | - Mixed mode support (encoding modes can be combined within a QR symbol). Supported modes: 38 | - numeric 39 | - alphanumeric 40 | - 8-bit binary 41 | - [ECI support](https://en.wikipedia.org/wiki/Extended_Channel_Interpretation) 42 | - 13-bit double-byte: 43 | - kanji (Japanese, Shift-JIS) 44 | - hanzi (simplified Chinese, GB2312/GB18030) as [defined in GBT18284-2000](https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000) 45 | - Flexible, easily extensible output modules, built-in support for the following output formats: 46 | - [GdImage](https://www.php.net/manual/book.image) (raster graphics: avif, bmp, gif, jpeg, png, webp) 47 | - [ImageMagick](https://www.php.net/manual/book.imagick) ([multiple supported image formats](https://imagemagick.org/script/formats.php)) 48 | - Markup types: SVG, HTML, etc. 49 | - String types: JSON, plain text, etc. 50 | - Encapsulated Postscript (EPS) 51 | - PDF via [FPDF](https://github.com/setasign/fpdf) 52 | - QR Code reader (via GD and ImageMagick) 53 | 54 | 55 | ## Requirements 56 | 57 | - PHP 8.2+ 58 | - [`ext-mbstring`](https://www.php.net/manual/book.mbstring.php) 59 | - optional: 60 | - [`ext-gd`](https://www.php.net/manual/book.image) for `QRGdImage` based output 61 | - [`ext-imagick`](https://github.com/Imagick/imagick) with [ImageMagick](https://imagemagick.org) installed 62 | - [`ext-fileinfo`](https://www.php.net/manual/book.fileinfo.php) required by `QRImagick` output 63 | - [`setasign/fpdf`](https://github.com/setasign/fpdf) for the PDF output module 64 | - [`intervention/image`](https://github.com/Intervention/image) for alternative GD/ImageMagick output 65 | 66 | For the QR Code reader, either `ext-gd` or `ext-imagick` is required! 67 | 68 | 69 | # Documentation 70 | 71 | - The user manual is at https://php-qrcode.readthedocs.io/ ([sources](https://github.com/chillerlan/php-qrcode/tree/main/docs)) 72 | - An API documentation created with [phpDocumentor](https://www.phpdoc.org/) can be found at https://chillerlan.github.io/php-qrcode/ 73 | - The documentation for the `QROptions` container is here: [chillerlan/php-settings-container](https://github.com/chillerlan/php-settings-container#readme) 74 | - Benchmark results can be found in the [`benchmark` branch](https://github.com/chillerlan/php-qrcode/tree/benchmark/markdown) 75 | 76 | **Important: Please use the examples from the branch that matches your installed php-qrcode version ( 77 | [v4.x](https://github.com/chillerlan/php-qrcode/tree/v4.3.x/examples), 78 | [v5.x](https://github.com/chillerlan/php-qrcode/tree/v5.0.x/examples), 79 | [dev-main](https://github.com/chillerlan/php-qrcode/tree/main/examples) 80 | )!**
81 | The `main` (default) branch is the active development for future major versions, and it is most likely incompatible with the latest release versions. 82 | 83 | ## Installation with [composer](https://getcomposer.org) 84 | 85 | See [the installation guide](https://php-qrcode.readthedocs.io/en/main/Usage/Installation.html) for more info! 86 | 87 | 88 | ### Terminal 89 | 90 | ``` 91 | composer require chillerlan/php-qrcode 92 | ``` 93 | 94 | 95 | ### composer.json 96 | 97 | ```json 98 | { 99 | "require": { 100 | "php": "^8.2", 101 | "chillerlan/php-qrcode": "dev-main#" 102 | } 103 | } 104 | ``` 105 | 106 | Note: replace `dev-main` with a [version constraint](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints), e.g. `^5.0` - see [releases](https://github.com/chillerlan/php-qrcode/releases) for valid versions. 107 | 108 | 109 | ## Quickstart 110 | 111 | We want to encode this URI for a mobile authenticator into a QRcode image: 112 | 113 | ```php 114 | $data = 'otpauth://totp/test?secret=B3JX4VCVJDVNXNZ5&issuer=chillerlan.net'; 115 | 116 | // quick and simple: 117 | echo 'QR Code'; 118 | ``` 119 | 120 | Wait, what was that? Please again, slower! See [Advanced usage](https://php-qrcode.readthedocs.io/en/main/Usage/Advanced-usage.html) in the manual. 121 | Also, have a look [in the examples folder](https://github.com/chillerlan/php-qrcode/tree/main/examples) for some more usage examples. 122 | 123 |

124 | QR codes are awesome! 125 |

126 | 127 | 128 | ## Reading QR Codes 129 | 130 | Using the built-in QR Code reader is pretty straight-forward: 131 | 132 | ```php 133 | // it's generally a good idea to wrap the reader in a try/catch block because it WILL throw eventually 134 | try{ 135 | $result = (new QRCode)->readFromFile('path/to/file.png'); // -> DecoderResult 136 | 137 | // you can now use the result instance... 138 | $content = $result->data; 139 | $matrix = $result->getMatrix(); // -> QRMatrix 140 | 141 | // ...or simply cast it to string to get the content: 142 | $content = (string)$result; 143 | } 144 | catch(Throwable $e){ 145 | // oopsies! 146 | } 147 | ``` 148 | 149 | 150 | # Shameless advertising 151 | 152 | Hi, please check out some of my other projects that are way cooler than qrcodes! 153 | 154 | - [js-qrcode](https://github.com/chillerlan/js-qrcode) - a javascript port of this library 155 | - [php-authenticator](https://github.com/chillerlan/php-authenticator) - a Google Authenticator implementation (see [authenticator example](https://github.com/chillerlan/php-qrcode/blob/main/examples/authenticator.php)) 156 | - [php-httpinterface](https://github.com/chillerlan/php-httpinterface) - a PSR-7/15/17/18 implemetation 157 | - [php-oauth](https://github.com/chillerlan/php-oauth) - an OAuth 1/2 client library, fully PSR-7/PSR-17/PSR-18 compatible 158 | - [php-database](https://github.com/chillerlan/php-database) - a database client & querybuilder for MySQL, Postgres, SQLite, MSSQL, Firebird 159 | - [php-tootbot](https://github.com/php-tootbot/tootbot-template) - a Mastodon bot library (see [@dwil](https://github.com/php-tootbot/dwil)) 160 | 161 | 162 | # Disclaimer! 163 | 164 | I don't take responsibility for molten CPUs, misled applications, failed log-ins etc.. Use at your own risk! 165 | 166 | 167 | ## License notice 168 | 169 | - Parts of this code are [ported to PHP](https://github.com/codemasher/php-qrcode-decoder) from the [ZXing project](https://github.com/zxing/zxing) and licensed under the [Apache License, Version 2.0](./NOTICE). 170 | - [The documentation](https://github.com/chillerlan/php-qrcode/tree/main/docs) is licensed under the [Creative Commons Attribution 4.0 International (CC BY 4.0) License](https://creativecommons.org/licenses/by/4.0/). 171 | 172 | 173 | ## Trademark Notice 174 | 175 | The word "QR Code" is a registered trademark of *DENSO WAVE INCORPORATED*
176 | https://www.qrcode.com/en/faq.html#patentH2Title 177 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://getcomposer.org/schema.json", 3 | "name": "chillerlan/php-qrcode", 4 | "description": "A QR Code generator and reader with a user-friendly API. PHP 8.2+", 5 | "homepage": "https://github.com/chillerlan/php-qrcode", 6 | "license": [ 7 | "MIT", "Apache-2.0" 8 | ], 9 | "type": "library", 10 | "keywords": [ 11 | "QR code", "qrcode", "qr", "qrcode-generator", "phpqrcode", "qrcode-reader", "qr-reader" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Kazuhiko Arase", 16 | "homepage": "https://github.com/kazuhikoarase/qrcode-generator" 17 | }, 18 | { 19 | "name":"ZXing Authors", 20 | "homepage": "https://github.com/zxing/zxing" 21 | }, 22 | { 23 | "name": "Ashot Khanamiryan", 24 | "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder" 25 | }, 26 | { 27 | "name": "Smiley", 28 | "email": "smiley@chillerlan.net", 29 | "homepage": "https://github.com/codemasher" 30 | }, 31 | { 32 | "name": "Contributors", 33 | "homepage":"https://github.com/chillerlan/php-qrcode/graphs/contributors" 34 | } 35 | ], 36 | "funding": [ 37 | { 38 | "type": "Ko-Fi", 39 | "url": "https://ko-fi.com/codemasher" 40 | } 41 | ], 42 | "support": { 43 | "docs": "https://php-qrcode.readthedocs.io", 44 | "issues": "https://github.com/chillerlan/php-qrcode/issues", 45 | "source": "https://github.com/chillerlan/php-qrcode" 46 | }, 47 | "minimum-stability": "stable", 48 | "prefer-stable": true, 49 | "require": { 50 | "php": "^8.2", 51 | "ext-mbstring": "*", 52 | "chillerlan/php-settings-container": "^3.2.1" 53 | }, 54 | "require-dev": { 55 | "ext-fileinfo": "*", 56 | "chillerlan/php-authenticator": "^5.2.1", 57 | "intervention/image": "^3.11", 58 | "phpbench/phpbench": "^1.4", 59 | "phpunit/phpunit": "^11.5", 60 | "phpmd/phpmd": "^2.15", 61 | "phpstan/phpstan": "^2.1.13", 62 | "phpstan/phpstan-deprecation-rules": "^2.0", 63 | "setasign/fpdf": "^1.8.6", 64 | "slevomat/coding-standard": "^8.15", 65 | "squizlabs/php_codesniffer": "^3.12" 66 | }, 67 | "suggest": { 68 | "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", 69 | "intervention/image": "More advanced GD and ImageMagick output.", 70 | "setasign/fpdf": "Required to use the QR FPDF output.", 71 | "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" 72 | }, 73 | "autoload": { 74 | "psr-4": { 75 | "chillerlan\\QRCode\\": "src" 76 | } 77 | }, 78 | "autoload-dev": { 79 | "psr-4": { 80 | "chillerlan\\QRCodeBenchmark\\": "benchmark", 81 | "chillerlan\\QRCodeTest\\": "tests" 82 | } 83 | }, 84 | "scripts": { 85 | "phpbench":[ 86 | "Composer\\Config::disableProcessTimeout", 87 | "@php vendor/bin/phpbench run" 88 | ], 89 | "phpcs": "@php vendor/bin/phpcs", 90 | "phpmd": "@php vendor/bin/phpmd src text ./phpmd.xml.dist", 91 | "phpstan": "@php vendor/bin/phpstan", 92 | "phpstan-baseline": "@php vendor/bin/phpstan --generate-baseline", 93 | "phpunit": "@php vendor/bin/phpunit" 94 | }, 95 | "config": { 96 | "lock": false, 97 | "sort-packages": true, 98 | "platform-check": true, 99 | "allow-plugins": { 100 | "dealerdirect/phpcodesniffer-composer-installer": true 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Common/BitBuffer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Common; 13 | 14 | use chillerlan\QRCode\QRCodeException; 15 | use function count, floor, min; 16 | 17 | /** 18 | * Holds the raw binary data 19 | */ 20 | final class BitBuffer{ 21 | 22 | /** 23 | * The buffer content 24 | * 25 | * @var int[] 26 | */ 27 | private array $buffer; 28 | 29 | /** 30 | * Length of the content (bits) 31 | */ 32 | private int $length; 33 | 34 | /** 35 | * Read count (bytes) 36 | */ 37 | private int $bytesRead = 0; 38 | 39 | /** 40 | * Read count (bits) 41 | */ 42 | private int $bitsRead = 0; 43 | 44 | /** 45 | * BitBuffer constructor. 46 | * 47 | * @param int[] $bytes 48 | */ 49 | public function __construct(array $bytes = []){ 50 | $this->buffer = $bytes; 51 | $this->length = count($this->buffer); 52 | } 53 | 54 | /** 55 | * appends a sequence of bits 56 | */ 57 | public function put(int $bits, int $length):self{ 58 | 59 | for($i = 0; $i < $length; $i++){ 60 | $this->putBit((($bits >> ($length - $i - 1)) & 1) === 1); 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * appends a single bit 68 | */ 69 | public function putBit(bool $bit):self{ 70 | $bufIndex = (int)floor($this->length / 8); 71 | 72 | if(count($this->buffer) <= $bufIndex){ 73 | $this->buffer[] = 0; 74 | } 75 | 76 | if($bit === true){ 77 | $this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8)); 78 | } 79 | 80 | $this->length++; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * returns the current buffer length 87 | */ 88 | public function getLength():int{ 89 | return $this->length; 90 | } 91 | 92 | /** 93 | * returns the buffer content 94 | * 95 | * to debug: `array_map(fn($v) => sprintf('%08b', $v), $bitBuffer->getBuffer())` 96 | * 97 | * @return int[] 98 | */ 99 | public function getBuffer():array{ 100 | return $this->buffer; 101 | } 102 | 103 | /** 104 | * Returns the number of bits that can be read successfully 105 | */ 106 | public function available():int{ 107 | return ((8 * ($this->length - $this->bytesRead)) - $this->bitsRead); 108 | } 109 | 110 | /** 111 | * @author Sean Owen, ZXing 112 | * 113 | * @param int $numBits number of bits to read 114 | * 115 | * @return int representing the bits read. The bits will appear as the least-significant bits of the int 116 | * @throws \chillerlan\QRCode\QRCodeException if numBits isn't in [1,32] or more than is available 117 | */ 118 | public function read(int $numBits):int{ 119 | 120 | if($numBits < 1 || $numBits > $this->available()){ 121 | throw new QRCodeException('invalid $numBits: '.$numBits); 122 | } 123 | 124 | $result = 0; 125 | 126 | // First, read remainder from current byte 127 | if($this->bitsRead > 0){ 128 | $bitsLeft = (8 - $this->bitsRead); 129 | $toRead = min($numBits, $bitsLeft); 130 | $bitsToNotRead = ($bitsLeft - $toRead); 131 | $mask = ((0xff >> (8 - $toRead)) << $bitsToNotRead); 132 | $result = (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead); 133 | $numBits -= $toRead; 134 | $this->bitsRead += $toRead; 135 | 136 | if($this->bitsRead === 8){ 137 | $this->bitsRead = 0; 138 | $this->bytesRead++; 139 | } 140 | } 141 | 142 | // Next read whole bytes 143 | if($numBits > 0){ 144 | 145 | while($numBits >= 8){ 146 | $result = (($result << 8) | ($this->buffer[$this->bytesRead] & 0xff)); 147 | $this->bytesRead++; 148 | $numBits -= 8; 149 | } 150 | 151 | // Finally read a partial byte 152 | if($numBits > 0){ 153 | $bitsToNotRead = (8 - $numBits); 154 | $mask = ((0xff >> $bitsToNotRead) << $bitsToNotRead); 155 | $result = (($result << $numBits) | (($this->buffer[$this->bytesRead] & $mask) >> $bitsToNotRead)); 156 | $this->bitsRead += $numBits; 157 | } 158 | } 159 | 160 | return $result; 161 | } 162 | 163 | /** 164 | * Clears the buffer and resets the stats 165 | */ 166 | public function clear():self{ 167 | $this->buffer = []; 168 | $this->length = 0; 169 | 170 | return $this->rewind(); 171 | } 172 | 173 | /** 174 | * Resets the read-counters 175 | */ 176 | public function rewind():self{ 177 | $this->bytesRead = 0; 178 | $this->bitsRead = 0; 179 | 180 | return $this; 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/Common/ECICharset.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Common; 14 | 15 | use chillerlan\QRCode\QRCodeException; 16 | use function sprintf; 17 | 18 | /** 19 | * ISO/IEC 18004:2000 - 8.4.1 Extended Channel Interpretation (ECI) Mode 20 | */ 21 | final class ECICharset{ 22 | 23 | public const CP437 = 0; // Code page 437, DOS Latin US 24 | public const ISO_IEC_8859_1_GLI = 1; // GLI encoding with characters 0 to 127 identical to ISO/IEC 646 and characters 128 to 255 identical to ISO 8859-1 25 | public const CP437_WO_GLI = 2; // An equivalent code table to CP437, without the return-to-GLI 0 logic 26 | public const ISO_IEC_8859_1 = 3; // Latin-1 (Default) 27 | public const ISO_IEC_8859_2 = 4; // Latin-2 28 | public const ISO_IEC_8859_3 = 5; // Latin-3 29 | public const ISO_IEC_8859_4 = 6; // Latin-4 30 | public const ISO_IEC_8859_5 = 7; // Latin/Cyrillic 31 | public const ISO_IEC_8859_6 = 8; // Latin/Arabic 32 | public const ISO_IEC_8859_7 = 9; // Latin/Greek 33 | public const ISO_IEC_8859_8 = 10; // Latin/Hebrew 34 | public const ISO_IEC_8859_9 = 11; // Latin-5 35 | public const ISO_IEC_8859_10 = 12; // Latin-6 36 | public const ISO_IEC_8859_11 = 13; // Latin/Thai 37 | // 14 reserved 38 | public const ISO_IEC_8859_13 = 15; // Latin-7 (Baltic Rim) 39 | public const ISO_IEC_8859_14 = 16; // Latin-8 (Celtic) 40 | public const ISO_IEC_8859_15 = 17; // Latin-9 41 | public const ISO_IEC_8859_16 = 18; // Latin-10 42 | // 19 reserved 43 | public const SHIFT_JIS = 20; // JIS X 0208 Annex 1 + JIS X 0201 44 | public const WINDOWS_1250_LATIN_2 = 21; // Superset of Latin-2, Central Europe 45 | public const WINDOWS_1251_CYRILLIC = 22; // Latin/Cyrillic 46 | public const WINDOWS_1252_LATIN_1 = 23; // Superset of Latin-1 47 | public const WINDOWS_1256_ARABIC = 24; 48 | public const ISO_IEC_10646_UCS_2 = 25; // High order byte first (UTF-16BE) 49 | public const ISO_IEC_10646_UTF_8 = 26; // UTF-8 50 | public const ISO_IEC_646_1991 = 27; // International Reference Version of ISO 7-bit coded character set (US-ASCII) 51 | public const BIG5 = 28; // Big 5 (Taiwan) Chinese Character Set 52 | public const GB18030 = 29; // GB (PRC) Chinese Character Set 53 | public const EUC_KR = 30; // Korean Character Set 54 | 55 | /** 56 | * map of charset id -> name 57 | * 58 | * @see \mb_list_encodings() 59 | */ 60 | public const MB_ENCODINGS = [ 61 | self::CP437 => null, 62 | self::ISO_IEC_8859_1_GLI => null, 63 | self::CP437_WO_GLI => null, 64 | self::ISO_IEC_8859_1 => 'ISO-8859-1', 65 | self::ISO_IEC_8859_2 => 'ISO-8859-2', 66 | self::ISO_IEC_8859_3 => 'ISO-8859-3', 67 | self::ISO_IEC_8859_4 => 'ISO-8859-4', 68 | self::ISO_IEC_8859_5 => 'ISO-8859-5', 69 | self::ISO_IEC_8859_6 => 'ISO-8859-6', 70 | self::ISO_IEC_8859_7 => 'ISO-8859-7', 71 | self::ISO_IEC_8859_8 => 'ISO-8859-8', 72 | self::ISO_IEC_8859_9 => 'ISO-8859-9', 73 | self::ISO_IEC_8859_10 => 'ISO-8859-10', 74 | self::ISO_IEC_8859_11 => null, 75 | self::ISO_IEC_8859_13 => 'ISO-8859-13', 76 | self::ISO_IEC_8859_14 => 'ISO-8859-14', 77 | self::ISO_IEC_8859_15 => 'ISO-8859-15', 78 | self::ISO_IEC_8859_16 => 'ISO-8859-16', 79 | self::SHIFT_JIS => 'SJIS', 80 | self::WINDOWS_1250_LATIN_2 => null, // @see https://www.php.net/manual/en/function.mb-convert-encoding.php#112547 81 | self::WINDOWS_1251_CYRILLIC => 'Windows-1251', 82 | self::WINDOWS_1252_LATIN_1 => 'Windows-1252', 83 | self::WINDOWS_1256_ARABIC => null, // @see https://stackoverflow.com/a/8592995 84 | self::ISO_IEC_10646_UCS_2 => 'UTF-16BE', 85 | self::ISO_IEC_10646_UTF_8 => 'UTF-8', 86 | self::ISO_IEC_646_1991 => 'ASCII', 87 | self::BIG5 => 'BIG-5', 88 | self::GB18030 => 'GB18030', 89 | self::EUC_KR => 'EUC-KR', 90 | ]; 91 | 92 | /** 93 | * The current ECI character set ID 94 | */ 95 | private int $charsetID; 96 | 97 | /** 98 | * @throws \chillerlan\QRCode\QRCodeException 99 | */ 100 | public function __construct(int $charsetID){ 101 | 102 | if($charsetID < 0 || $charsetID > 999999){ 103 | throw new QRCodeException(sprintf('invalid charset id: "%s"', $charsetID)); 104 | } 105 | 106 | $this->charsetID = $charsetID; 107 | } 108 | 109 | /** 110 | * Returns the current character set ID 111 | */ 112 | public function getID():int{ 113 | return $this->charsetID; 114 | } 115 | 116 | /** 117 | * Returns the name of the current character set or null if no name is available 118 | * 119 | * @see \mb_convert_encoding() 120 | * @see \iconv() 121 | */ 122 | public function getName():string|null{ 123 | return (self::MB_ENCODINGS[$this->charsetID] ?? null); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/Common/EccLevel.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2020 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Common; 13 | 14 | use chillerlan\QRCode\QRCodeException; 15 | use function array_column; 16 | 17 | /** 18 | * This class encapsulates the four error correction levels defined by the QR code standard. 19 | */ 20 | final class EccLevel{ 21 | 22 | // ISO/IEC 18004:2000 Tables 12, 25 23 | 24 | /** @var int */ 25 | public const L = 0b01; // 7%. 26 | /** @var int */ 27 | public const M = 0b00; // 15%. 28 | /** @var int */ 29 | public const Q = 0b11; // 25%. 30 | /** @var int */ 31 | public const H = 0b10; // 30%. 32 | 33 | /** 34 | * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 35 | * 36 | * @var int[][] 37 | */ 38 | private const MAX_BITS = [ 39 | // [ L, M, Q, H] // v => modules 40 | [ 0, 0, 0, 0], // 0 => will be ignored, index starts at 1 41 | [ 152, 128, 104, 72], // 1 => 21 42 | [ 272, 224, 176, 128], // 2 => 25 43 | [ 440, 352, 272, 208], // 3 => 29 44 | [ 640, 512, 384, 288], // 4 => 33 45 | [ 864, 688, 496, 368], // 5 => 37 46 | [ 1088, 864, 608, 480], // 6 => 41 47 | [ 1248, 992, 704, 528], // 7 => 45 48 | [ 1552, 1232, 880, 688], // 8 => 49 49 | [ 1856, 1456, 1056, 800], // 9 => 53 50 | [ 2192, 1728, 1232, 976], // 10 => 57 51 | [ 2592, 2032, 1440, 1120], // 11 => 61 52 | [ 2960, 2320, 1648, 1264], // 12 => 65 53 | [ 3424, 2672, 1952, 1440], // 13 => 69 NICE! 54 | [ 3688, 2920, 2088, 1576], // 14 => 73 55 | [ 4184, 3320, 2360, 1784], // 15 => 77 56 | [ 4712, 3624, 2600, 2024], // 16 => 81 57 | [ 5176, 4056, 2936, 2264], // 17 => 85 58 | [ 5768, 4504, 3176, 2504], // 18 => 89 59 | [ 6360, 5016, 3560, 2728], // 19 => 93 60 | [ 6888, 5352, 3880, 3080], // 20 => 97 61 | [ 7456, 5712, 4096, 3248], // 21 => 101 62 | [ 8048, 6256, 4544, 3536], // 22 => 105 63 | [ 8752, 6880, 4912, 3712], // 23 => 109 64 | [ 9392, 7312, 5312, 4112], // 24 => 113 65 | [10208, 8000, 5744, 4304], // 25 => 117 66 | [10960, 8496, 6032, 4768], // 26 => 121 67 | [11744, 9024, 6464, 5024], // 27 => 125 68 | [12248, 9544, 6968, 5288], // 28 => 129 69 | [13048, 10136, 7288, 5608], // 29 => 133 70 | [13880, 10984, 7880, 5960], // 30 => 137 71 | [14744, 11640, 8264, 6344], // 31 => 141 72 | [15640, 12328, 8920, 6760], // 32 => 145 73 | [16568, 13048, 9368, 7208], // 33 => 149 74 | [17528, 13800, 9848, 7688], // 34 => 153 75 | [18448, 14496, 10288, 7888], // 35 => 157 76 | [19472, 15312, 10832, 8432], // 36 => 161 77 | [20528, 15936, 11408, 8768], // 37 => 165 78 | [21616, 16816, 12016, 9136], // 38 => 169 79 | [22496, 17728, 12656, 9776], // 39 => 173 80 | [23648, 18672, 13328, 10208], // 40 => 177 81 | ]; 82 | 83 | /** 84 | * ISO/IEC 18004:2000 Section 8.9 - Format Information 85 | * 86 | * ECC level -> mask pattern 87 | * 88 | * @var int[][] 89 | */ 90 | private const FORMAT_PATTERN = [ 91 | [ // L 92 | 0b111011111000100, 93 | 0b111001011110011, 94 | 0b111110110101010, 95 | 0b111100010011101, 96 | 0b110011000101111, 97 | 0b110001100011000, 98 | 0b110110001000001, 99 | 0b110100101110110, 100 | ], 101 | [ // M 102 | 0b101010000010010, 103 | 0b101000100100101, 104 | 0b101111001111100, 105 | 0b101101101001011, 106 | 0b100010111111001, 107 | 0b100000011001110, 108 | 0b100111110010111, 109 | 0b100101010100000, 110 | ], 111 | [ // Q 112 | 0b011010101011111, 113 | 0b011000001101000, 114 | 0b011111100110001, 115 | 0b011101000000110, 116 | 0b010010010110100, 117 | 0b010000110000011, 118 | 0b010111011011010, 119 | 0b010101111101101, 120 | ], 121 | [ // H 122 | 0b001011010001001, 123 | 0b001001110111110, 124 | 0b001110011100111, 125 | 0b001100111010000, 126 | 0b000011101100010, 127 | 0b000001001010101, 128 | 0b000110100001100, 129 | 0b000100000111011, 130 | ], 131 | ]; 132 | 133 | /** 134 | * The current ECC level value 135 | * 136 | * L: 0b01 137 | * M: 0b00 138 | * Q: 0b11 139 | * H: 0b10 140 | */ 141 | private int $eccLevel; 142 | 143 | /** 144 | * @param int $eccLevel containing the two bits encoding a QR Code's error correction level 145 | * 146 | * @throws \chillerlan\QRCode\QRCodeException 147 | */ 148 | public function __construct(int $eccLevel){ 149 | 150 | if((0b11 & $eccLevel) !== $eccLevel){ 151 | throw new QRCodeException('invalid ECC level'); 152 | } 153 | 154 | $this->eccLevel = $eccLevel; 155 | } 156 | 157 | /** 158 | * returns the string representation of the current ECC level 159 | */ 160 | public function __toString():string{ 161 | return [ 162 | self::L => 'L', 163 | self::M => 'M', 164 | self::Q => 'Q', 165 | self::H => 'H', 166 | ][$this->eccLevel]; 167 | } 168 | 169 | /** 170 | * returns the current ECC level 171 | */ 172 | public function getLevel():int{ 173 | return $this->eccLevel; 174 | } 175 | 176 | /** 177 | * returns the ordinal value of the current ECC level 178 | * 179 | * references to the keys of the following tables: 180 | * 181 | * @see \chillerlan\QRCode\Common\EccLevel::MAX_BITS 182 | * @see \chillerlan\QRCode\Common\EccLevel::FORMAT_PATTERN 183 | * @see \chillerlan\QRCode\Common\Version::RSBLOCKS 184 | */ 185 | public function getOrdinal():int{ 186 | return [ 187 | self::L => 0, 188 | self::M => 1, 189 | self::Q => 2, 190 | self::H => 3, 191 | ][$this->eccLevel]; 192 | } 193 | 194 | /** 195 | * returns the format pattern for the given $eccLevel and $maskPattern 196 | */ 197 | public function getformatPattern(MaskPattern $maskPattern):int{ 198 | return self::FORMAT_PATTERN[$this->getOrdinal()][$maskPattern->getPattern()]; 199 | } 200 | 201 | /** 202 | * returns an array with the max bit lengths for version 1-40 and the current ECC level 203 | * 204 | * @return int[] 205 | */ 206 | public function getMaxBits():array{ 207 | $col = array_column(self::MAX_BITS, $this->getOrdinal()); 208 | 209 | unset($col[0]); // remove the inavlid index 0 210 | 211 | return $col; 212 | } 213 | 214 | /** 215 | * Returns the maximum bit length for the given version and current ECC level 216 | */ 217 | public function getMaxBitsForVersion(Version $version):int{ 218 | return self::MAX_BITS[$version->getVersionNumber()][$this->getOrdinal()]; 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/Common/GDLuminanceSource.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license MIT 10 | * 11 | * @noinspection PhpComposerExtensionStubsInspection 12 | */ 13 | declare(strict_types=1); 14 | 15 | namespace chillerlan\QRCode\Common; 16 | 17 | use chillerlan\QRCode\QROptions; 18 | use chillerlan\Settings\SettingsContainerInterface; 19 | use GdImage; 20 | use function file_get_contents, imagecolorat, imagecolorsforindex, 21 | imagecreatefromstring, imagefilter, imagesx, imagesy; 22 | use const IMG_FILTER_BRIGHTNESS, IMG_FILTER_CONTRAST, IMG_FILTER_GRAYSCALE, IMG_FILTER_NEGATE; 23 | 24 | /** 25 | * This class is used to help decode images from files which arrive as GD Resource 26 | * It does not support rotation. 27 | */ 28 | final class GDLuminanceSource extends LuminanceSourceAbstract{ 29 | 30 | private GdImage $gdImage; 31 | 32 | /** 33 | * GDLuminanceSource constructor. 34 | * 35 | * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException 36 | */ 37 | public function __construct(GdImage $gdImage, SettingsContainerInterface|QROptions $options = new QROptions){ 38 | parent::__construct(imagesx($gdImage), imagesy($gdImage), $options); 39 | 40 | $this->gdImage = $gdImage; 41 | 42 | if($this->options->readerGrayscale){ 43 | imagefilter($this->gdImage, IMG_FILTER_GRAYSCALE); 44 | } 45 | 46 | if($this->options->readerInvertColors){ 47 | imagefilter($this->gdImage, IMG_FILTER_NEGATE); 48 | } 49 | 50 | if($this->options->readerIncreaseContrast){ 51 | imagefilter($this->gdImage, IMG_FILTER_BRIGHTNESS, -100); 52 | imagefilter($this->gdImage, IMG_FILTER_CONTRAST, -100); 53 | } 54 | 55 | $this->setLuminancePixels(); 56 | } 57 | 58 | private function setLuminancePixels():void{ 59 | 60 | for($j = 0; $j < $this->height; $j++){ 61 | for($i = 0; $i < $this->width; $i++){ 62 | $argb = imagecolorat($this->gdImage, $i, $j); 63 | $pixel = imagecolorsforindex($this->gdImage, $argb); 64 | 65 | $this->setLuminancePixel($pixel['red'], $pixel['green'], $pixel['blue']); 66 | } 67 | } 68 | 69 | } 70 | 71 | public static function fromFile(string $path, SettingsContainerInterface|QROptions $options = new QROptions):static{ 72 | return new self(imagecreatefromstring(file_get_contents(self::checkFile($path))), $options); 73 | } 74 | 75 | public static function fromBlob(string $blob, SettingsContainerInterface|QROptions $options = new QROptions):static{ 76 | return new self(imagecreatefromstring($blob), $options); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Common/GF256.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Common; 14 | 15 | use chillerlan\QRCode\QRCodeException; 16 | 17 | use function array_fill; 18 | 19 | /** 20 | * This class contains utility methods for performing mathematical operations over 21 | * the Galois Fields. Operations use a given primitive polynomial in calculations. 22 | * 23 | * Throughout this package, elements of the GF are represented as an int 24 | * for convenience and speed (but at the cost of memory). 25 | * 26 | * 27 | * @author Sean Owen 28 | * @author David Olivier 29 | */ 30 | final class GF256{ 31 | 32 | /** 33 | * irreducible polynomial whose coefficients are represented by the bits of an int, 34 | * where the least-significant bit represents the constant coefficient 35 | */ 36 | # private int $primitive = 0x011D; 37 | 38 | private const logTable = [ 39 | 0, // the first value is never returned, index starts at 1 40 | 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 41 | 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 42 | 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 43 | 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 44 | 6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 45 | 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 46 | 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 47 | 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 48 | 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 49 | 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 50 | 55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 51 | 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 52 | 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 53 | 108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 54 | 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 55 | 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175, 56 | ]; 57 | 58 | private const expTable = [ 59 | 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 60 | 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 61 | 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 62 | 70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 63 | 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 64 | 253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 65 | 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 66 | 129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 67 | 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 68 | 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 69 | 230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 70 | 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 71 | 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 72 | 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 73 | 18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 74 | 44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1, 75 | ]; 76 | 77 | /** 78 | * Implements both addition and subtraction -- they are the same in GF(size). 79 | * 80 | * @return int sum/difference of a and b 81 | */ 82 | public static function addOrSubtract(int $a, int $b):int{ 83 | return ($a ^ $b); 84 | } 85 | 86 | /** 87 | * @return GenericGFPoly the monomial representing coefficient * x^degree 88 | * @throws \chillerlan\QRCode\QRCodeException 89 | */ 90 | public static function buildMonomial(int $degree, int $coefficient):GenericGFPoly{ 91 | 92 | if($degree < 0){ 93 | throw new QRCodeException('degree < 0'); 94 | } 95 | 96 | $coefficients = array_fill(0, ($degree + 1), 0); 97 | $coefficients[0] = $coefficient; 98 | 99 | return new GenericGFPoly($coefficients); 100 | } 101 | 102 | /** 103 | * @return int 2 to the power of $a in GF(size) 104 | */ 105 | public static function exp(int $a):int{ 106 | 107 | if($a < 0){ 108 | $a += 255; 109 | } 110 | elseif($a >= 256){ 111 | $a -= 255; 112 | } 113 | 114 | return self::expTable[$a]; 115 | } 116 | 117 | /** 118 | * @return int base 2 log of $a in GF(size) 119 | * @throws \chillerlan\QRCode\QRCodeException 120 | */ 121 | public static function log(int $a):int{ 122 | 123 | if($a < 1){ 124 | throw new QRCodeException('$a < 1'); 125 | } 126 | 127 | return self::logTable[$a]; 128 | } 129 | 130 | /** 131 | * @return int multiplicative inverse of a 132 | * @throws \chillerlan\QRCode\QRCodeException 133 | */ 134 | public static function inverse(int $a):int{ 135 | 136 | if($a === 0){ 137 | throw new QRCodeException('$a === 0'); 138 | } 139 | 140 | return self::expTable[(256 - self::logTable[$a] - 1)]; 141 | } 142 | 143 | /** 144 | * @return int product of a and b in GF(size) 145 | */ 146 | public static function multiply(int $a, int $b):int{ 147 | 148 | if($a === 0 || $b === 0){ 149 | return 0; 150 | } 151 | 152 | return self::expTable[((self::logTable[$a] + self::logTable[$b]) % 255)]; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/Common/GenericGFPoly.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Common; 14 | 15 | use chillerlan\QRCode\QRCodeException; 16 | use function array_fill, array_slice, array_splice, count; 17 | 18 | /** 19 | * Represents a polynomial whose coefficients are elements of a GF. 20 | * Instances of this class are immutable. 21 | * 22 | * Much credit is due to William Rucklidge since portions of this code are an indirect 23 | * port of his C++ Reed-Solomon implementation. 24 | * 25 | * @author Sean Owen 26 | */ 27 | final class GenericGFPoly{ 28 | 29 | /** @var int[] */ 30 | private array $coefficients; 31 | 32 | /** 33 | * @param int[] $coefficients array coefficients as ints representing elements of GF(size), arranged 34 | * from most significant (highest-power term) coefficient to the least significant 35 | * 36 | * @throws \chillerlan\QRCode\QRCodeException if argument is null or empty, or if leading coefficient is 0 and this 37 | * is not a constant polynomial (that is, it is not the monomial "0") 38 | */ 39 | public function __construct(array $coefficients, int|null $degree = null){ 40 | $degree ??= 0; 41 | 42 | if(empty($coefficients)){ 43 | throw new QRCodeException('arg $coefficients is empty'); 44 | } 45 | 46 | if($degree < 0){ 47 | throw new QRCodeException('negative degree'); 48 | } 49 | 50 | $coefficientsLength = count($coefficients); 51 | 52 | // Leading term must be non-zero for anything except the constant polynomial "0" 53 | $firstNonZero = 0; 54 | 55 | while($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] === 0){ 56 | $firstNonZero++; 57 | } 58 | 59 | $this->coefficients = [0]; 60 | 61 | if($firstNonZero !== $coefficientsLength){ 62 | $this->coefficients = array_fill(0, ($coefficientsLength - $firstNonZero + $degree), 0); 63 | 64 | for($i = 0; $i < ($coefficientsLength - $firstNonZero); $i++){ 65 | $this->coefficients[$i] = $coefficients[($i + $firstNonZero)]; 66 | } 67 | } 68 | 69 | } 70 | 71 | /** 72 | * @return int $coefficient of x^degree term in this polynomial 73 | */ 74 | public function getCoefficient(int $degree):int{ 75 | return $this->coefficients[(count($this->coefficients) - 1 - $degree)]; 76 | } 77 | 78 | /** 79 | * @return int[] 80 | */ 81 | public function getCoefficients():array{ 82 | return $this->coefficients; 83 | } 84 | 85 | /** 86 | * @return int $degree of this polynomial 87 | */ 88 | public function getDegree():int{ 89 | return (count($this->coefficients) - 1); 90 | } 91 | 92 | /** 93 | * @return bool true if this polynomial is the monomial "0" 94 | */ 95 | public function isZero():bool{ 96 | return $this->coefficients[0] === 0; 97 | } 98 | 99 | /** 100 | * @return int evaluation of this polynomial at a given point 101 | */ 102 | public function evaluateAt(int $a):int{ 103 | 104 | if($a === 0){ 105 | // Just return the x^0 coefficient 106 | return $this->getCoefficient(0); 107 | } 108 | 109 | $result = 0; 110 | 111 | foreach($this->coefficients as $c){ 112 | // if $a === 1 just the sum of the coefficients 113 | $result = GF256::addOrSubtract((($a === 1) ? $result : GF256::multiply($a, $result)), $c); 114 | } 115 | 116 | return $result; 117 | } 118 | 119 | public function multiply(GenericGFPoly $other):self{ 120 | 121 | if($this->isZero() || $other->isZero()){ 122 | return new self([0]); 123 | } 124 | 125 | $product = array_fill(0, (count($this->coefficients) + count($other->coefficients) - 1), 0); 126 | 127 | foreach($this->coefficients as $i => $aCoeff){ 128 | foreach($other->coefficients as $j => $bCoeff){ 129 | $product[($i + $j)] ^= GF256::multiply($aCoeff, $bCoeff); 130 | } 131 | } 132 | 133 | return new self($product); 134 | } 135 | 136 | /** 137 | * @return \chillerlan\QRCode\Common\GenericGFPoly[] [quotient, remainder] 138 | * @throws \chillerlan\QRCode\QRCodeException 139 | */ 140 | public function divide(GenericGFPoly $other):array{ 141 | 142 | if($other->isZero()){ 143 | throw new QRCodeException('Division by 0'); 144 | } 145 | 146 | $quotient = new self([0]); 147 | $remainder = clone $this; 148 | 149 | $denominatorLeadingTerm = $other->getCoefficient($other->getDegree()); 150 | $inverseDenominatorLeadingTerm = GF256::inverse($denominatorLeadingTerm); 151 | 152 | while($remainder->getDegree() >= $other->getDegree() && !$remainder->isZero()){ 153 | $scale = GF256::multiply($remainder->getCoefficient($remainder->getDegree()), $inverseDenominatorLeadingTerm); 154 | $diff = ($remainder->getDegree() - $other->getDegree()); 155 | $quotient = $quotient->addOrSubtract(GF256::buildMonomial($diff, $scale)); 156 | $remainder = $remainder->addOrSubtract($other->multiplyByMonomial($diff, $scale)); 157 | } 158 | 159 | return [$quotient, $remainder]; 160 | 161 | } 162 | 163 | public function multiplyInt(int $scalar):self{ 164 | 165 | if($scalar === 0){ 166 | return new self([0]); 167 | } 168 | 169 | if($scalar === 1){ 170 | return $this; 171 | } 172 | 173 | $product = array_fill(0, count($this->coefficients), 0); 174 | 175 | foreach($this->coefficients as $i => $c){ 176 | $product[$i] = GF256::multiply($c, $scalar); 177 | } 178 | 179 | return new self($product); 180 | } 181 | 182 | /** 183 | * @throws \chillerlan\QRCode\QRCodeException 184 | */ 185 | public function multiplyByMonomial(int $degree, int $coefficient):self{ 186 | 187 | if($degree < 0){ 188 | throw new QRCodeException('degree < 0'); 189 | } 190 | 191 | if($coefficient === 0){ 192 | return new self([0]); 193 | } 194 | 195 | $product = array_fill(0, (count($this->coefficients) + $degree), 0); 196 | 197 | foreach($this->coefficients as $i => $c){ 198 | $product[$i] = GF256::multiply($c, $coefficient); 199 | } 200 | 201 | return new self($product); 202 | } 203 | 204 | public function mod(GenericGFPoly $other):self{ 205 | 206 | if((count($this->coefficients) - count($other->coefficients)) < 0){ 207 | return $this; 208 | } 209 | 210 | $ratio = (GF256::log($this->coefficients[0]) - GF256::log($other->coefficients[0])); 211 | 212 | foreach($other->coefficients as $i => $c){ 213 | $this->coefficients[$i] ^= GF256::exp(GF256::log($c) + $ratio); 214 | } 215 | 216 | return (new self($this->coefficients))->mod($other); 217 | } 218 | 219 | public function addOrSubtract(GenericGFPoly $other):self{ 220 | 221 | if($this->isZero()){ 222 | return $other; 223 | } 224 | 225 | if($other->isZero()){ 226 | return $this; 227 | } 228 | 229 | $smallerCoefficients = $this->coefficients; 230 | $largerCoefficients = $other->coefficients; 231 | 232 | if(count($smallerCoefficients) > count($largerCoefficients)){ 233 | $temp = $smallerCoefficients; 234 | $smallerCoefficients = $largerCoefficients; 235 | $largerCoefficients = $temp; 236 | } 237 | 238 | $sumDiff = array_fill(0, count($largerCoefficients), 0); 239 | $lengthDiff = (count($largerCoefficients) - count($smallerCoefficients)); 240 | // Copy high-order terms only found in higher-degree polynomial's coefficients 241 | array_splice($sumDiff, 0, $lengthDiff, array_slice($largerCoefficients, 0, $lengthDiff)); 242 | 243 | $countLargerCoefficients = count($largerCoefficients); 244 | 245 | for($i = $lengthDiff; $i < $countLargerCoefficients; $i++){ 246 | $sumDiff[$i] = GF256::addOrSubtract($smallerCoefficients[($i - $lengthDiff)], $largerCoefficients[$i]); 247 | } 248 | 249 | return new self($sumDiff); 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /src/Common/IMagickLuminanceSource.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license MIT 10 | * 11 | * @noinspection PhpComposerExtensionStubsInspection 12 | */ 13 | declare(strict_types=1); 14 | 15 | namespace chillerlan\QRCode\Common; 16 | 17 | use chillerlan\QRCode\QROptions; 18 | use chillerlan\Settings\SettingsContainerInterface; 19 | use Imagick; 20 | use function count; 21 | 22 | /** 23 | * This class is used to help decode images from files which arrive as Imagick Resource 24 | * It does not support rotation. 25 | */ 26 | final class IMagickLuminanceSource extends LuminanceSourceAbstract{ 27 | 28 | private Imagick $imagick; 29 | 30 | /** 31 | * IMagickLuminanceSource constructor. 32 | */ 33 | public function __construct(Imagick $imagick, SettingsContainerInterface|QROptions $options = new QROptions){ 34 | parent::__construct($imagick->getImageWidth(), $imagick->getImageHeight(), $options); 35 | 36 | $this->imagick = $imagick; 37 | 38 | if($this->options->readerGrayscale){ 39 | $this->imagick->setImageColorspace(Imagick::COLORSPACE_GRAY); 40 | } 41 | 42 | if($this->options->readerInvertColors){ 43 | $this->imagick->negateImage($this->options->readerGrayscale); 44 | } 45 | 46 | if($this->options->readerIncreaseContrast){ 47 | for($i = 0; $i < 10; $i++){ 48 | $this->imagick->contrastImage(false); // misleading docs 49 | } 50 | } 51 | 52 | $this->setLuminancePixels(); 53 | } 54 | 55 | private function setLuminancePixels():void{ 56 | $pixels = $this->imagick->exportImagePixels(1, 1, $this->width, $this->height, 'RGB', Imagick::PIXEL_CHAR); 57 | $count = count($pixels); 58 | 59 | for($i = 0; $i < $count; $i += 3){ 60 | $this->setLuminancePixel(($pixels[$i] & 0xff), ($pixels[($i + 1)] & 0xff), ($pixels[($i + 2)] & 0xff)); 61 | } 62 | } 63 | 64 | public static function fromFile(string $path, SettingsContainerInterface|QROptions $options = new QROptions):static{ 65 | return new self(new Imagick(self::checkFile($path)), $options); 66 | } 67 | 68 | public static function fromBlob(string $blob, SettingsContainerInterface|QROptions $options = new QROptions):static{ 69 | $im = new Imagick; 70 | $im->readImageBlob($blob); 71 | 72 | return new self($im, $options); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Common/LuminanceSourceAbstract.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2021 Smiley 10 | * @license Apache-2.0 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Common; 15 | 16 | use chillerlan\QRCode\QROptions; 17 | use chillerlan\QRCode\Decoder\QRCodeDecoderException; 18 | use chillerlan\Settings\SettingsContainerInterface; 19 | use function array_slice, array_splice, file_exists, is_file, is_readable, realpath; 20 | 21 | /** 22 | * The purpose of this class hierarchy is to abstract different bitmap implementations across 23 | * platforms into a standard interface for requesting greyscale luminance values. 24 | * 25 | * @author dswitkin@google.com (Daniel Switkin) 26 | */ 27 | abstract class LuminanceSourceAbstract implements LuminanceSourceInterface{ 28 | 29 | protected SettingsContainerInterface|QROptions $options; 30 | /** @var int[] */ 31 | protected array $luminances; 32 | protected int $width; 33 | protected int $height; 34 | 35 | public function __construct(int $width, int $height, SettingsContainerInterface|QROptions $options = new QROptions){ 36 | $this->width = $width; 37 | $this->height = $height; 38 | $this->options = $options; 39 | 40 | $this->luminances = []; 41 | } 42 | 43 | public function getLuminances():array{ 44 | return $this->luminances; 45 | } 46 | 47 | public function getWidth():int{ 48 | return $this->width; 49 | } 50 | 51 | public function getHeight():int{ 52 | return $this->height; 53 | } 54 | 55 | public function getRow(int $y):array{ 56 | 57 | if($y < 0 || $y >= $this->getHeight()){ 58 | throw new QRCodeDecoderException('Requested row is outside the image: '.$y); 59 | } 60 | 61 | $arr = []; 62 | 63 | array_splice($arr, 0, $this->width, array_slice($this->luminances, ($y * $this->width), $this->width)); 64 | 65 | return $arr; 66 | } 67 | 68 | protected function setLuminancePixel(int $r, int $g, int $b):void{ 69 | $this->luminances[] = ($r === $g && $g === $b) 70 | // Image is already greyscale, so pick any channel. 71 | ? $r // (($r + 128) % 256) - 128; 72 | // Calculate luminance cheaply, favoring green. 73 | : (($r + 2 * $g + $b) / 4); // (((($r + 2 * $g + $b) / 4) + 128) % 256) - 128; 74 | } 75 | 76 | /** 77 | * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException 78 | */ 79 | protected static function checkFile(string $path):string{ 80 | $path = trim($path); 81 | 82 | if(!file_exists($path) || !is_file($path) || !is_readable($path)){ 83 | throw new QRCodeDecoderException('invalid file: '.$path); 84 | } 85 | 86 | $realpath = realpath($path); 87 | 88 | if($realpath === false){ 89 | throw new QRCodeDecoderException('unable to resolve path: '.$path); 90 | } 91 | 92 | return $realpath; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Common/LuminanceSourceInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2021 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Common; 13 | 14 | use chillerlan\QRCode\QROptions; 15 | use chillerlan\Settings\SettingsContainerInterface; 16 | 17 | /** 18 | * Interface for the luminance sources 19 | */ 20 | interface LuminanceSourceInterface{ 21 | 22 | /** 23 | * Fetches luminance data for the underlying bitmap. Values should be fetched using: 24 | * `int luminance = array[y * width + x] & 0xff` 25 | * 26 | * @return int[] A row-major 2D array of luminance values. Do not use result $length as it may be 27 | * larger than $width * $height bytes on some platforms. Do not modify the contents 28 | * of the result. 29 | */ 30 | public function getLuminances():array; 31 | 32 | /** 33 | * @return int The width of the bitmap. 34 | */ 35 | public function getWidth():int; 36 | 37 | /** 38 | * @return int The height of the bitmap. 39 | */ 40 | public function getHeight():int; 41 | 42 | /** 43 | * Fetches one row of luminance data from the underlying platform's bitmap. Values range from 44 | * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have 45 | * to bitwise and with 0xff for each value. It is preferable for implementations of this method 46 | * to only fetch this row rather than the whole image, since no 2D Readers may be installed and 47 | * getLuminances() may never be called. 48 | * 49 | * @param int $y The row to fetch, which must be in [0,getHeight()) 50 | * 51 | * @return int[] An array containing the luminance data. 52 | * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException 53 | */ 54 | public function getRow(int $y):array; 55 | 56 | /** 57 | * Creates a LuminanceSource instance from the given file 58 | */ 59 | public static function fromFile(string $path, SettingsContainerInterface|QROptions $options = new QROptions):static; 60 | 61 | /** 62 | * Creates a LuminanceSource instance from the given data blob 63 | */ 64 | public static function fromBlob(string $blob, SettingsContainerInterface|QROptions $options = new QROptions):static; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/Common/Mode.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2020 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Common; 13 | 14 | use chillerlan\QRCode\Data\{AlphaNum, Byte, Hanzi, Kanji, Number}; 15 | use chillerlan\QRCode\QRCodeException; 16 | 17 | /** 18 | * Data mode information - ISO 18004:2006, 6.4.1, Tables 2 and 3 19 | */ 20 | final class Mode{ 21 | 22 | // ISO/IEC 18004:2000 Table 2 23 | 24 | /** @var int */ 25 | public const TERMINATOR = 0b0000; 26 | /** @var int */ 27 | public const NUMBER = 0b0001; 28 | /** @var int */ 29 | public const ALPHANUM = 0b0010; 30 | /** @var int */ 31 | public const BYTE = 0b0100; 32 | /** @var int */ 33 | public const KANJI = 0b1000; 34 | /** @var int */ 35 | public const HANZI = 0b1101; 36 | /** @var int */ 37 | public const STRCTURED_APPEND = 0b0011; 38 | /** @var int */ 39 | public const FNC1_FIRST = 0b0101; 40 | /** @var int */ 41 | public const FNC1_SECOND = 0b1001; 42 | /** @var int */ 43 | public const ECI = 0b0111; 44 | 45 | /** 46 | * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 47 | * 48 | * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator 49 | */ 50 | public const LENGTH_BITS = [ 51 | self::NUMBER => [10, 12, 14], 52 | self::ALPHANUM => [ 9, 11, 13], 53 | self::BYTE => [ 8, 16, 16], 54 | self::KANJI => [ 8, 10, 12], 55 | self::HANZI => [ 8, 10, 12], 56 | self::ECI => [ 0, 0, 0], 57 | ]; 58 | 59 | /** 60 | * Map of data mode => interface (detection order) 61 | * 62 | * @var array 63 | */ 64 | public const INTERFACES = [ 65 | self::NUMBER => Number::class, 66 | self::ALPHANUM => AlphaNum::class, 67 | self::KANJI => Kanji::class, 68 | self::HANZI => Hanzi::class, 69 | self::BYTE => Byte::class, 70 | ]; 71 | 72 | /** 73 | * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 74 | * 75 | * @throws \chillerlan\QRCode\QRCodeException 76 | */ 77 | public static function getLengthBitsForVersion(int $mode, int $version):int{ 78 | 79 | if(!isset(self::LENGTH_BITS[$mode])){ 80 | throw new QRCodeException('invalid mode given'); 81 | } 82 | 83 | $minVersion = 0; 84 | 85 | foreach([9, 26, 40] as $key => $breakpoint){ 86 | 87 | if($version > $minVersion && $version <= $breakpoint){ 88 | return self::LENGTH_BITS[$mode][$key]; 89 | } 90 | 91 | $minVersion = $breakpoint; 92 | } 93 | 94 | throw new QRCodeException(sprintf('invalid version number: %d', $version)); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/Data/AlphaNum.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, Mode}; 15 | use function ceil, intdiv, preg_match, strpos; 16 | 17 | /** 18 | * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / : 19 | * 20 | * ISO/IEC 18004:2000 Section 8.3.3 21 | * ISO/IEC 18004:2000 Section 8.4.3 22 | */ 23 | final class AlphaNum extends QRDataModeAbstract{ 24 | 25 | /** 26 | * ISO/IEC 18004:2000 Table 5 27 | * 28 | * @var string 29 | */ 30 | private const CHAR_MAP = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; 31 | 32 | public const DATAMODE = Mode::ALPHANUM; 33 | 34 | public function getLengthInBits():int{ 35 | return (int)ceil($this->getCharCount() * (11 / 2)); 36 | } 37 | 38 | public static function validateString(string $string):bool{ 39 | return (bool)preg_match('/^[A-Z\d %$*+-.:\/]+$/', $string); 40 | } 41 | 42 | public function write(BitBuffer $bitBuffer, int $versionNumber):static{ 43 | $len = $this->getCharCount(); 44 | 45 | $bitBuffer 46 | ->put(self::DATAMODE, 4) 47 | ->put($len, $this::getLengthBits($versionNumber)) 48 | ; 49 | 50 | // encode 2 characters in 11 bits 51 | for($i = 0; ($i + 1) < $len; $i += 2){ 52 | $bitBuffer->put( 53 | ($this->ord($this->data[$i]) * 45 + $this->ord($this->data[($i + 1)])), 54 | 11, 55 | ); 56 | } 57 | 58 | // encode a remaining character in 6 bits 59 | if($i < $len){ 60 | $bitBuffer->put($this->ord($this->data[$i]), 6); 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @inheritDoc 68 | * 69 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 70 | */ 71 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ 72 | $length = $bitBuffer->read(self::getLengthBits($versionNumber)); 73 | $result = ''; 74 | // Read two characters at a time 75 | while($length > 1){ 76 | 77 | if($bitBuffer->available() < 11){ 78 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 79 | } 80 | 81 | $nextTwoCharsBits = $bitBuffer->read(11); 82 | $result .= self::chr(intdiv($nextTwoCharsBits, 45)); 83 | $result .= self::chr($nextTwoCharsBits % 45); 84 | $length -= 2; 85 | } 86 | 87 | if($length === 1){ 88 | // special case: one character left 89 | if($bitBuffer->available() < 6){ 90 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 91 | } 92 | 93 | $result .= self::chr($bitBuffer->read(6)); 94 | } 95 | 96 | return $result; 97 | } 98 | 99 | /** 100 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 101 | */ 102 | private function ord(string $chr):int{ 103 | $ord = strpos(self::CHAR_MAP, $chr); 104 | 105 | if($ord === false){ 106 | throw new QRCodeDataException('invalid character'); // @codeCoverageIgnore 107 | } 108 | 109 | return $ord; 110 | } 111 | 112 | /** 113 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 114 | */ 115 | private static function chr(int $ord):string{ 116 | 117 | if($ord < 0 || $ord > 44){ 118 | throw new QRCodeDataException('invalid character code'); // @codeCoverageIgnore 119 | } 120 | 121 | return self::CHAR_MAP[$ord]; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/Data/Byte.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, Mode}; 15 | use function chr, ord; 16 | 17 | /** 18 | * 8-bit Byte mode, ISO-8859-1 or UTF-8 19 | * 20 | * ISO/IEC 18004:2000 Section 8.3.4 21 | * ISO/IEC 18004:2000 Section 8.4.4 22 | */ 23 | final class Byte extends QRDataModeAbstract{ 24 | 25 | public const DATAMODE = Mode::BYTE; 26 | 27 | public function getLengthInBits():int{ 28 | return ($this->getCharCount() * 8); 29 | } 30 | 31 | public static function validateString(string $string):bool{ 32 | return $string !== ''; 33 | } 34 | 35 | public function write(BitBuffer $bitBuffer, int $versionNumber):static{ 36 | $len = $this->getCharCount(); 37 | 38 | $bitBuffer 39 | ->put(self::DATAMODE, 4) 40 | ->put($len, $this::getLengthBits($versionNumber)) 41 | ; 42 | 43 | $i = 0; 44 | 45 | while($i < $len){ 46 | $bitBuffer->put(ord($this->data[$i]), 8); 47 | $i++; 48 | } 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @inheritDoc 55 | * 56 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 57 | */ 58 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ 59 | $length = $bitBuffer->read(self::getLengthBits($versionNumber)); 60 | 61 | if($bitBuffer->available() < (8 * $length)){ 62 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 63 | } 64 | 65 | $readBytes = ''; 66 | 67 | for($i = 0; $i < $length; $i++){ 68 | $readBytes .= chr($bitBuffer->read(8)); 69 | } 70 | 71 | return $readBytes; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Data/ECI.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2020 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, ECICharset, Mode}; 15 | use function mb_convert_encoding, mb_detect_encoding, mb_internal_encoding, sprintf; 16 | 17 | /** 18 | * Adds an ECI Designator 19 | * 20 | * ISO/IEC 18004:2000 8.4.1.1 21 | * 22 | * Please note that you have to take care for the correct data encoding when adding with QRCode::add*Segment() 23 | */ 24 | final class ECI extends QRDataModeAbstract{ 25 | 26 | public const DATAMODE = Mode::ECI; 27 | 28 | /** 29 | * The current ECI encoding id 30 | */ 31 | private int $encoding; 32 | 33 | /** 34 | * @inheritDoc 35 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 36 | * @noinspection PhpMissingParentConstructorInspection 37 | */ 38 | public function __construct(int $encoding){ 39 | 40 | if($encoding < 0 || $encoding > 999999){ 41 | throw new QRCodeDataException(sprintf('invalid encoding id: "%s"', $encoding)); 42 | } 43 | 44 | $this->encoding = $encoding; 45 | } 46 | 47 | public function getLengthInBits():int{ 48 | 49 | if($this->encoding < 128){ 50 | return 8; 51 | } 52 | 53 | if($this->encoding < 16384){ 54 | return 16; 55 | } 56 | 57 | return 24; 58 | } 59 | 60 | /** 61 | * Writes an ECI designator to the bitbuffer 62 | * 63 | * @inheritDoc 64 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 65 | */ 66 | public function write(BitBuffer $bitBuffer, int $versionNumber):static{ 67 | $bitBuffer->put(self::DATAMODE, 4); 68 | 69 | if($this->encoding < 128){ 70 | $bitBuffer->put($this->encoding, 8); 71 | } 72 | elseif($this->encoding < 16384){ 73 | $bitBuffer->put(($this->encoding | 0x8000), 16); 74 | } 75 | elseif($this->encoding < 1000000){ 76 | $bitBuffer->put(($this->encoding | 0xC00000), 24); 77 | } 78 | else{ 79 | throw new QRCodeDataException('invalid ECI ID'); 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Reads and parses the value of an ECI designator 87 | * 88 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 89 | */ 90 | public static function parseValue(BitBuffer $bitBuffer):ECICharset{ 91 | $firstByte = $bitBuffer->read(8); 92 | 93 | // just one byte 94 | if(($firstByte & 0b10000000) === 0){ 95 | $id = ($firstByte & 0b01111111); 96 | } 97 | // two bytes 98 | elseif(($firstByte & 0b11000000) === 0b10000000){ 99 | $id = ((($firstByte & 0b00111111) << 8) | $bitBuffer->read(8)); 100 | } 101 | // three bytes 102 | elseif(($firstByte & 0b11100000) === 0b11000000){ 103 | $id = ((($firstByte & 0b00011111) << 16) | $bitBuffer->read(16)); 104 | } 105 | else{ 106 | throw new QRCodeDataException(sprintf('error decoding ECI value first byte: %08b', $firstByte));// @codeCoverageIgnore 107 | } 108 | 109 | return new ECICharset($id); 110 | } 111 | 112 | /** 113 | * @codeCoverageIgnore Unused, but required as per interface 114 | */ 115 | public static function validateString(string $string):bool{ 116 | return true; 117 | } 118 | 119 | /** 120 | * Reads and decodes the ECI designator including the following byte sequence 121 | * 122 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 123 | */ 124 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ 125 | $eciCharset = self::parseValue($bitBuffer); 126 | $nextMode = $bitBuffer->read(4); 127 | $encoding = $eciCharset->getName(); 128 | 129 | // this is definitely weird, but there are QR Codes out in the wild 130 | // that have ECI followed by numeric and alphanum segments 131 | // @see https://github.com/chillerlan/php-qrcode/discussions/289 132 | $data = match($nextMode){ 133 | Mode::NUMBER => Number::decodeSegment($bitBuffer, $versionNumber), 134 | Mode::ALPHANUM => AlphaNum::decodeSegment($bitBuffer, $versionNumber), 135 | Mode::BYTE => Byte::decodeSegment($bitBuffer, $versionNumber), 136 | default => throw new QRCodeDataException( 137 | sprintf('ECI designator followed by invalid mode: "%04b"', $nextMode), 138 | ), 139 | }; 140 | 141 | if($encoding === null){ 142 | // The spec isn't clear on this mode; see 143 | // section 6.4.5: it does not say which encoding to assuming 144 | // upon decoding. I have seen ISO-8859-1 used as well as 145 | // Shift_JIS -- without anything like an ECI designator to 146 | // give a hint. 147 | $encoding = mb_detect_encoding($data, ['ISO-8859-1', 'Windows-1252', 'SJIS', 'UTF-8'], true); 148 | 149 | if($encoding === false){ 150 | throw new QRCodeDataException('could not determine encoding in ECI mode'); // @codeCoverageIgnore 151 | } 152 | } 153 | 154 | return mb_convert_encoding($data, mb_internal_encoding(), $encoding); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/Data/Hanzi.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2020 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, Mode}; 15 | use Throwable; 16 | use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding, 17 | mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen; 18 | 19 | /** 20 | * Hanzi (simplified Chinese) mode, GBT18284-2000: 13-bit double-byte characters from the GB2312/GB18030 character set 21 | * 22 | * Please note that this is not part of the QR Code specification and may not be supported by all readers (ZXing-based ones do). 23 | * 24 | * @see https://en.wikipedia.org/wiki/GB_2312 25 | * @see http://www.herongyang.com/GB2312/Introduction-of-GB2312.html 26 | * @see https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding 27 | * @see https://gist.github.com/codemasher/91da33c44bfb48a81a6c1426bb8e4338 28 | * @see https://github.com/zxing/zxing/blob/dfb06fa33b17a9e68321be151c22846c7b78048f/core/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java#L172-L209 29 | * @see https://www.chinesestandard.net/PDF/English.aspx/GBT18284-2000 30 | */ 31 | final class Hanzi extends QRDataModeAbstract{ 32 | 33 | /** 34 | * possible values: GB2312, GB18030 35 | * 36 | * @var string 37 | */ 38 | public const ENCODING = 'GB18030'; 39 | 40 | /** 41 | * @todo: other subsets??? 42 | * 43 | * @var int 44 | */ 45 | public const GB2312_SUBSET = 0b0001; 46 | 47 | public const DATAMODE = Mode::HANZI; 48 | 49 | protected function getCharCount():int{ 50 | return mb_strlen($this->data, self::ENCODING); 51 | } 52 | 53 | public function getLengthInBits():int{ 54 | return ($this->getCharCount() * 13); 55 | } 56 | 57 | public static function convertEncoding(string $string):string{ 58 | mb_detect_order([mb_internal_encoding(), 'UTF-8', 'GB2312', 'GB18030', 'CP936', 'EUC-CN', 'HZ']); 59 | 60 | $detected = mb_detect_encoding($string, null, true); 61 | 62 | if($detected === false){ 63 | throw new QRCodeDataException('mb_detect_encoding error'); 64 | } 65 | 66 | if($detected === self::ENCODING){ 67 | return $string; 68 | } 69 | 70 | $string = mb_convert_encoding($string, self::ENCODING, $detected); 71 | 72 | if(!is_string($string)){ 73 | throw new QRCodeDataException('mb_convert_encoding error'); 74 | } 75 | 76 | return $string; 77 | } 78 | 79 | /** 80 | * checks if a string qualifies as Hanzi/GB2312 81 | */ 82 | public static function validateString(string $string):bool{ 83 | 84 | try{ 85 | $string = self::convertEncoding($string); 86 | } 87 | catch(Throwable){ 88 | return false; 89 | } 90 | 91 | $len = strlen($string); 92 | 93 | if($len < 2 || ($len % 2) !== 0){ 94 | return false; 95 | } 96 | 97 | for($i = 0; $i < $len; $i += 2){ 98 | $byte1 = ord($string[$i]); 99 | $byte2 = ord($string[($i + 1)]); 100 | 101 | // byte 1 unused ranges 102 | if($byte1 < 0xa1 || ($byte1 > 0xa9 && $byte1 < 0xb0) || $byte1 > 0xf7){ 103 | return false; 104 | } 105 | 106 | // byte 2 unused ranges 107 | if($byte2 < 0xa1 || $byte2 > 0xfe){ 108 | return false; 109 | } 110 | 111 | } 112 | 113 | return true; 114 | } 115 | 116 | /** 117 | * @inheritDoc 118 | * 119 | * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence 120 | */ 121 | public function write(BitBuffer $bitBuffer, int $versionNumber):static{ 122 | 123 | $bitBuffer 124 | ->put(self::DATAMODE, 4) 125 | ->put($this::GB2312_SUBSET, 4) 126 | ->put($this->getCharCount(), $this::getLengthBits($versionNumber)) 127 | ; 128 | 129 | $len = strlen($this->data); 130 | 131 | for($i = 0; ($i + 1) < $len; $i += 2){ 132 | $c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)]))); 133 | 134 | if($c >= 0xa1a1 && $c <= 0xaafe){ 135 | $c -= 0x0a1a1; 136 | } 137 | elseif($c >= 0xb0a1 && $c <= 0xfafe){ 138 | $c -= 0x0a6a1; 139 | } 140 | else{ 141 | throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c)); 142 | } 143 | 144 | $bitBuffer->put((((($c >> 8) & 0xff) * 0x060) + ($c & 0xff)), 13); 145 | } 146 | 147 | if($i < $len){ 148 | throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1))); 149 | } 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * See specification GBT 18284-2000 156 | * 157 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 158 | */ 159 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ 160 | 161 | // Hanzi mode contains a subset indicator right after mode indicator 162 | if($bitBuffer->read(4) !== self::GB2312_SUBSET){ 163 | throw new QRCodeDataException('ecpected subset indicator for Hanzi mode'); 164 | } 165 | 166 | $length = $bitBuffer->read(self::getLengthBits($versionNumber)); 167 | 168 | if($bitBuffer->available() < ($length * 13)){ 169 | throw new QRCodeDataException('not enough bits available'); 170 | } 171 | 172 | // Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as GB2312 afterwards 173 | $buffer = []; 174 | $offset = 0; 175 | 176 | while($length > 0){ 177 | // Each 13 bits encodes a 2-byte character 178 | $twoBytes = $bitBuffer->read(13); 179 | $assembledTwoBytes = ((intdiv($twoBytes, 0x060) << 8) | ($twoBytes % 0x060)); 180 | 181 | $assembledTwoBytes += ($assembledTwoBytes < 0x00a00) // 0x003BF 182 | ? 0x0a1a1 // In the 0xA1A1 to 0xAAFE range 183 | : 0x0a6a1; // In the 0xB0A1 to 0xFAFE range 184 | 185 | $buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8)); 186 | $buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes); 187 | $offset += 2; 188 | $length--; 189 | } 190 | 191 | return mb_convert_encoding(implode('', $buffer), mb_internal_encoding(), self::ENCODING); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /src/Data/Kanji.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, Mode}; 15 | use Throwable; 16 | use function chr, implode, intdiv, is_string, mb_convert_encoding, mb_detect_encoding, 17 | mb_detect_order, mb_internal_encoding, mb_strlen, ord, sprintf, strlen; 18 | 19 | /** 20 | * Kanji mode: 13-bit double-byte characters from the Shift-JIS character set 21 | * 22 | * ISO/IEC 18004:2000 Section 8.3.5 23 | * ISO/IEC 18004:2000 Section 8.4.5 24 | * 25 | * @see https://en.wikipedia.org/wiki/Shift_JIS#As_defined_in_JIS_X_0208:1997 26 | * @see http://www.rikai.com/library/kanjitables/kanji_codes.sjis.shtml 27 | * @see https://gist.github.com/codemasher/d07d3e6e9346c08e7a41b8b978784952 28 | */ 29 | final class Kanji extends QRDataModeAbstract{ 30 | 31 | /** 32 | * possible values: SJIS, SJIS-2004 33 | * 34 | * SJIS-2004 may produce errors in PHP < 8 35 | * 36 | * @var string 37 | */ 38 | public const ENCODING = 'SJIS'; 39 | 40 | public const DATAMODE = Mode::KANJI; 41 | 42 | protected function getCharCount():int{ 43 | return mb_strlen($this->data, self::ENCODING); 44 | } 45 | 46 | public function getLengthInBits():int{ 47 | return ($this->getCharCount() * 13); 48 | } 49 | 50 | public static function convertEncoding(string $string):string{ 51 | mb_detect_order([mb_internal_encoding(), 'UTF-8', 'SJIS', 'SJIS-2004']); 52 | 53 | $detected = mb_detect_encoding($string, null, true); 54 | 55 | if($detected === false){ 56 | throw new QRCodeDataException('mb_detect_encoding error'); 57 | } 58 | 59 | if($detected === self::ENCODING){ 60 | return $string; 61 | } 62 | 63 | $string = mb_convert_encoding($string, self::ENCODING, $detected); 64 | 65 | if(!is_string($string)){ 66 | throw new QRCodeDataException(sprintf('invalid encoding: %s', $detected)); 67 | } 68 | 69 | return $string; 70 | } 71 | 72 | /** 73 | * checks if a string qualifies as SJIS Kanji 74 | */ 75 | public static function validateString(string $string):bool{ 76 | 77 | try{ 78 | $string = self::convertEncoding($string); 79 | } 80 | catch(Throwable){ 81 | return false; 82 | } 83 | 84 | $len = strlen($string); 85 | 86 | if($len < 2 || ($len % 2) !== 0){ 87 | return false; 88 | } 89 | 90 | for($i = 0; $i < $len; $i += 2){ 91 | $byte1 = ord($string[$i]); 92 | $byte2 = ord($string[($i + 1)]); 93 | 94 | // byte 1 unused and vendor ranges 95 | if($byte1 < 0x81 || ($byte1 > 0x84 && $byte1 < 0x88) || ($byte1 > 0x9f && $byte1 < 0xe0) || $byte1 > 0xea){ 96 | return false; 97 | } 98 | 99 | // byte 2 unused ranges 100 | if($byte2 < 0x40 || $byte2 === 0x7f || $byte2 > 0xfc){ 101 | return false; 102 | } 103 | 104 | } 105 | 106 | return true; 107 | } 108 | 109 | /** 110 | * @inheritDoc 111 | * 112 | * @throws \chillerlan\QRCode\Data\QRCodeDataException on an illegal character occurence 113 | */ 114 | public function write(BitBuffer $bitBuffer, int $versionNumber):static{ 115 | 116 | $bitBuffer 117 | ->put(self::DATAMODE, 4) 118 | ->put($this->getCharCount(), $this::getLengthBits($versionNumber)) 119 | ; 120 | 121 | $len = strlen($this->data); 122 | 123 | for($i = 0; ($i + 1) < $len; $i += 2){ 124 | $c = (((0xff & ord($this->data[$i])) << 8) | (0xff & ord($this->data[($i + 1)]))); 125 | 126 | if($c >= 0x8140 && $c <= 0x9ffc){ 127 | $c -= 0x8140; 128 | } 129 | elseif($c >= 0xe040 && $c <= 0xebbf){ 130 | $c -= 0xc140; 131 | } 132 | else{ 133 | throw new QRCodeDataException(sprintf('illegal char at %d [%d]', ($i + 1), $c)); 134 | } 135 | 136 | $bitBuffer->put((((($c >> 8) & 0xff) * 0xc0) + ($c & 0xff)), 13); 137 | } 138 | 139 | if($i < $len){ 140 | throw new QRCodeDataException(sprintf('illegal char at %d', ($i + 1))); 141 | } 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * @inheritDoc 148 | * 149 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 150 | */ 151 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ 152 | $length = $bitBuffer->read(self::getLengthBits($versionNumber)); 153 | 154 | if($bitBuffer->available() < ($length * 13)){ 155 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 156 | } 157 | 158 | // Each character will require 2 bytes. Read the characters as 2-byte pairs and decode as SJIS afterwards 159 | $buffer = []; 160 | $offset = 0; 161 | 162 | while($length > 0){ 163 | // Each 13 bits encodes a 2-byte character 164 | $twoBytes = $bitBuffer->read(13); 165 | $assembledTwoBytes = ((intdiv($twoBytes, 0x0c0) << 8) | ($twoBytes % 0x0c0)); 166 | 167 | $assembledTwoBytes += ($assembledTwoBytes < 0x01f00) 168 | ? 0x08140 // In the 0x8140 to 0x9FFC range 169 | : 0x0c140; // In the 0xE040 to 0xEBBF range 170 | 171 | $buffer[$offset] = chr(0xff & ($assembledTwoBytes >> 8)); 172 | $buffer[($offset + 1)] = chr(0xff & $assembledTwoBytes); 173 | $offset += 2; 174 | $length--; 175 | } 176 | 177 | return mb_convert_encoding(implode('', $buffer), mb_internal_encoding(), self::ENCODING); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/Data/Number.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, Mode}; 15 | use function ceil, intdiv, preg_match, substr, unpack; 16 | 17 | /** 18 | * Numeric mode: decimal digits 0 to 9 19 | * 20 | * ISO/IEC 18004:2000 Section 8.3.2 21 | * ISO/IEC 18004:2000 Section 8.4.2 22 | */ 23 | final class Number extends QRDataModeAbstract{ 24 | 25 | public const DATAMODE = Mode::NUMBER; 26 | 27 | public function getLengthInBits():int{ 28 | return (int)ceil($this->getCharCount() * (10 / 3)); 29 | } 30 | 31 | public static function validateString(string $string):bool{ 32 | return (bool)preg_match('/^\d+$/', $string); 33 | } 34 | 35 | public function write(BitBuffer $bitBuffer, int $versionNumber):static{ 36 | $len = $this->getCharCount(); 37 | 38 | $bitBuffer 39 | ->put(self::DATAMODE, 4) 40 | ->put($len, $this::getLengthBits($versionNumber)) 41 | ; 42 | 43 | $i = 0; 44 | 45 | // encode numeric triplets in 10 bits 46 | while(($i + 2) < $len){ 47 | $bitBuffer->put($this->parseInt(substr($this->data, $i, 3)), 10); 48 | $i += 3; 49 | } 50 | 51 | if($i < $len){ 52 | 53 | // encode 2 remaining numbers in 7 bits 54 | if(($len - $i) === 2){ 55 | $bitBuffer->put($this->parseInt(substr($this->data, $i, 2)), 7); 56 | } 57 | // encode one remaining number in 4 bits 58 | elseif(($len - $i) === 1){ 59 | $bitBuffer->put($this->parseInt(substr($this->data, $i, 1)), 4); 60 | } 61 | 62 | } 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * get the code for the given numeric string 69 | * 70 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 71 | */ 72 | private function parseInt(string $string):int{ 73 | $num = 0; 74 | 75 | $ords = unpack('C*', $string); 76 | 77 | if($ords === false){ 78 | throw new QRCodeDataException('unpack() error'); 79 | } 80 | 81 | foreach($ords as $ord){ 82 | $num = ($num * 10 + $ord - 48); 83 | } 84 | 85 | return $num; 86 | } 87 | 88 | /** 89 | * @inheritDoc 90 | * 91 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 92 | */ 93 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string{ 94 | $length = $bitBuffer->read(self::getLengthBits($versionNumber)); 95 | $result = ''; 96 | // Read three digits at a time 97 | while($length >= 3){ 98 | // Each 10 bits encodes three digits 99 | if($bitBuffer->available() < 10){ 100 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 101 | } 102 | 103 | $threeDigitsBits = $bitBuffer->read(10); 104 | 105 | if($threeDigitsBits >= 1000){ 106 | throw new QRCodeDataException('error decoding numeric value'); 107 | } 108 | 109 | $result .= intdiv($threeDigitsBits, 100); 110 | $result .= (intdiv($threeDigitsBits, 10) % 10); 111 | $result .= ($threeDigitsBits % 10); 112 | 113 | $length -= 3; 114 | } 115 | 116 | if($length === 2){ 117 | // Two digits left over to read, encoded in 7 bits 118 | if($bitBuffer->available() < 7){ 119 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 120 | } 121 | 122 | $twoDigitsBits = $bitBuffer->read(7); 123 | 124 | if($twoDigitsBits >= 100){ 125 | throw new QRCodeDataException('error decoding numeric value'); 126 | } 127 | 128 | $result .= intdiv($twoDigitsBits, 10); 129 | $result .= ($twoDigitsBits % 10); 130 | } 131 | elseif($length === 1){ 132 | // One digit left over to read 133 | if($bitBuffer->available() < 4){ 134 | throw new QRCodeDataException('not enough bits available'); // @codeCoverageIgnore 135 | } 136 | 137 | $digitBits = $bitBuffer->read(4); 138 | 139 | if($digitBits >= 10){ 140 | throw new QRCodeDataException('error decoding numeric value'); 141 | } 142 | 143 | $result .= $digitBits; 144 | } 145 | 146 | return $result; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/Data/QRCodeDataException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\QRCodeException; 15 | 16 | /** 17 | * An exception container 18 | */ 19 | final class QRCodeDataException extends QRCodeException{ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Data/QRData.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\QROptions; 15 | use chillerlan\QRCode\Common\{BitBuffer, EccLevel, Mode, Version}; 16 | use chillerlan\Settings\SettingsContainerInterface; 17 | use function sprintf; 18 | 19 | /** 20 | * Processes the binary data and maps it on a QRMatrix which is then being returned 21 | */ 22 | final class QRData{ 23 | 24 | /** 25 | * the options instance 26 | */ 27 | private SettingsContainerInterface|QROptions $options; 28 | 29 | /** 30 | * a BitBuffer instance 31 | */ 32 | private BitBuffer $bitBuffer; 33 | 34 | /** 35 | * an EccLevel instance 36 | */ 37 | private EccLevel $eccLevel; 38 | 39 | /** 40 | * current QR Code version 41 | */ 42 | private Version $version; 43 | 44 | /** 45 | * @var \chillerlan\QRCode\Data\QRDataModeInterface[] 46 | */ 47 | private array $dataSegments = []; 48 | 49 | /** 50 | * Max bits for the current ECC mode 51 | * 52 | * @var int[] 53 | */ 54 | private array $maxBitsForEcc; 55 | 56 | /** 57 | * QRData constructor. 58 | * 59 | * @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments 60 | */ 61 | public function __construct(SettingsContainerInterface|QROptions $options, array $dataSegments = []){ 62 | $this->options = $options; 63 | $this->bitBuffer = new BitBuffer; 64 | $this->eccLevel = new EccLevel($this->options->eccLevel); 65 | $this->maxBitsForEcc = $this->eccLevel->getMaxBits(); 66 | 67 | $this->setData($dataSegments); 68 | } 69 | 70 | /** 71 | * Sets the data string (internally called by the constructor) 72 | * 73 | * Subsequent calls will overwrite the current state - use the QRCode::add*Segement() method instead 74 | * 75 | * @param \chillerlan\QRCode\Data\QRDataModeInterface[] $dataSegments 76 | */ 77 | public function setData(array $dataSegments):self{ 78 | $this->dataSegments = $dataSegments; 79 | $this->version = $this->getMinimumVersion(); 80 | 81 | $this->bitBuffer->clear(); 82 | $this->writeBitBuffer(); 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Returns the current BitBuffer instance 89 | * 90 | * @codeCoverageIgnore 91 | */ 92 | public function getBitBuffer():BitBuffer{ 93 | return $this->bitBuffer; 94 | } 95 | 96 | /** 97 | * Sets a BitBuffer object 98 | * 99 | * This can be used instead of setData(), however, the version auto-detection is not available in this case. 100 | * The version needs to match the length bits range for the data mode the data has been encoded with, 101 | * additionally the bit array needs to contain enough pad bits. 102 | * 103 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 104 | */ 105 | public function setBitBuffer(BitBuffer $bitBuffer):self{ 106 | 107 | if($this->options->version === Version::AUTO){ 108 | throw new QRCodeDataException('version auto detection is not available'); 109 | } 110 | 111 | if($bitBuffer->getLength() === 0){ 112 | throw new QRCodeDataException('the given BitBuffer is empty'); 113 | } 114 | 115 | $this->dataSegments = []; 116 | $this->bitBuffer = $bitBuffer; 117 | $this->version = new Version($this->options->version); 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * returns a fresh matrix object with the data written and masked with the given $maskPattern 124 | */ 125 | public function writeMatrix():QRMatrix{ 126 | return (new QRMatrix($this->version, $this->eccLevel)) 127 | ->initFunctionalPatterns() 128 | ->writeCodewords($this->bitBuffer) 129 | ; 130 | } 131 | 132 | /** 133 | * estimates the total length of the several mode segments in order to guess the minimum version 134 | * 135 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 136 | */ 137 | public function estimateTotalBitLength():int{ 138 | $length = 0; 139 | 140 | foreach($this->dataSegments as $segment){ 141 | // data length of the current segment 142 | $length += $segment->getLengthInBits(); 143 | // +4 bits for the mode descriptor 144 | $length += 4; 145 | // Hanzi mode sets an additional 4 bit long subset identifier 146 | if($segment instanceof Hanzi){ 147 | $length += 4; 148 | } 149 | } 150 | 151 | $provisionalVersion = null; 152 | /** @var int $version */ 153 | foreach($this->maxBitsForEcc as $version => $maxBits){ 154 | 155 | if($length <= $maxBits){ 156 | $provisionalVersion = $version; 157 | } 158 | 159 | } 160 | 161 | if($provisionalVersion !== null){ 162 | 163 | // add character count indicator bits for the provisional version 164 | foreach($this->dataSegments as $segment){ 165 | $length += Mode::getLengthBitsForVersion($segment::DATAMODE, $provisionalVersion); 166 | } 167 | 168 | // it seems that in some cases the estimated total length is not 100% accurate, 169 | // so we substract 4 bits from the total when not in mixed mode 170 | if(count($this->dataSegments) <= 1){ 171 | $length -= 4; 172 | } 173 | 174 | // we've got a match! 175 | // or let's see if there's a higher version number available 176 | if($length <= $this->maxBitsForEcc[$provisionalVersion] || isset($this->maxBitsForEcc[($provisionalVersion + 1)])){ 177 | return $length; 178 | } 179 | 180 | } 181 | 182 | throw new QRCodeDataException(sprintf('estimated data exceeds %d bits', $length)); 183 | } 184 | 185 | /** 186 | * returns the minimum version number for the given string 187 | * 188 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 189 | */ 190 | public function getMinimumVersion():Version{ 191 | 192 | if($this->options->version !== Version::AUTO){ 193 | return new Version($this->options->version); 194 | } 195 | 196 | $total = $this->estimateTotalBitLength(); 197 | 198 | // guess the version number within the given range 199 | for($version = $this->options->versionMin; $version <= $this->options->versionMax; $version++){ 200 | if($total <= ($this->maxBitsForEcc[$version] - 4)){ 201 | return new Version($version); 202 | } 203 | } 204 | 205 | // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first 206 | throw new QRCodeDataException('failed to guess minimum version'); // @codeCoverageIgnore 207 | } 208 | 209 | /** 210 | * creates a BitBuffer and writes the string data to it 211 | * 212 | * @throws \chillerlan\QRCode\Data\QRCodeDataException on data overflow 213 | */ 214 | private function writeBitBuffer():void{ 215 | $MAX_BITS = $this->eccLevel->getMaxBitsForVersion($this->version); 216 | 217 | foreach($this->dataSegments as $segment){ 218 | $segment->write($this->bitBuffer, $this->version->getVersionNumber()); 219 | } 220 | 221 | // overflow, likely caused due to invalid version setting 222 | if($this->bitBuffer->getLength() > $MAX_BITS){ 223 | throw new QRCodeDataException( 224 | sprintf('code length overflow. (%d > %d bit)', $this->bitBuffer->getLength(), $MAX_BITS), 225 | ); 226 | } 227 | 228 | // add terminator (ISO/IEC 18004:2000 Table 2) 229 | if(($this->bitBuffer->getLength() + 4) <= $MAX_BITS){ 230 | $this->bitBuffer->put(Mode::TERMINATOR, 4); 231 | } 232 | 233 | // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion 234 | 235 | // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long 236 | // by the addition of padding bits with binary value 0 237 | while(($this->bitBuffer->getLength() % 8) !== 0){ 238 | 239 | if($this->bitBuffer->getLength() === $MAX_BITS){ 240 | break; 241 | } 242 | 243 | $this->bitBuffer->putBit(false); 244 | } 245 | 246 | // The message bit stream shall then be extended to fill the data capacity of the symbol 247 | // corresponding to the Version and Error Correction Level, by the addition of the Pad 248 | // Codewords 11101100 and 00010001 alternately. 249 | $alternate = false; 250 | 251 | while(($this->bitBuffer->getLength() + 8) <= $MAX_BITS){ 252 | $this->bitBuffer->put(($alternate) ? 0b00010001 : 0b11101100, 8); 253 | 254 | $alternate = !$alternate; 255 | } 256 | 257 | // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros) 258 | // to the end of the message in order exactly to fill the symbol capacity 259 | while($this->bitBuffer->getLength() <= $MAX_BITS){ 260 | $this->bitBuffer->putBit(false); 261 | } 262 | 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/Data/QRDataModeAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2020 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\Mode; 15 | 16 | /** 17 | * abstract methods for the several data modes 18 | */ 19 | abstract class QRDataModeAbstract implements QRDataModeInterface{ 20 | 21 | /** 22 | * The data to write 23 | */ 24 | protected string $data; 25 | 26 | /** 27 | * QRDataModeAbstract constructor. 28 | * 29 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 30 | */ 31 | public function __construct(string $data){ 32 | $data = $this::convertEncoding($data); 33 | 34 | if(!$this::validateString($data)){ 35 | throw new QRCodeDataException('invalid data'); 36 | } 37 | 38 | $this->data = $data; 39 | } 40 | 41 | /** 42 | * returns the character count of the $data string 43 | */ 44 | protected function getCharCount():int{ 45 | return strlen($this->data); 46 | } 47 | 48 | public static function convertEncoding(string $string):string{ 49 | return $string; 50 | } 51 | 52 | /** 53 | * shortcut 54 | */ 55 | protected static function getLengthBits(int $versionNumber):int{ 56 | return Mode::getLengthBitsForVersion(static::DATAMODE, $versionNumber); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/Data/QRDataModeInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\BitBuffer; 15 | 16 | /** 17 | * Specifies the methods reqired for the data modules (Number, Alphanum, Byte and Kanji) 18 | */ 19 | interface QRDataModeInterface{ 20 | 21 | /** 22 | * the current data mode: Number, Alphanum, Kanji, Hanzi, Byte, ECI 23 | * 24 | * Note: do not call this constant from the interface, but rather from one of the child classes 25 | * 26 | * @var int 27 | * @see \chillerlan\QRCode\Common\Mode 28 | */ 29 | public const DATAMODE = -1; 30 | 31 | /** 32 | * retruns the length in bits of the data string 33 | */ 34 | public function getLengthInBits():int; 35 | 36 | /** 37 | * encoding conversion helper 38 | * 39 | * @throws \chillerlan\QRCode\Data\QRCodeDataException 40 | */ 41 | public static function convertEncoding(string $string):string; 42 | 43 | /** 44 | * checks if the given string qualifies for the encoder module 45 | */ 46 | public static function validateString(string $string):bool; 47 | 48 | /** 49 | * writes the actual data string to the BitBuffer, uses the given version to determine the length bits 50 | * 51 | * @see \chillerlan\QRCode\Data\QRData::writeBitBuffer() 52 | */ 53 | public function write(BitBuffer $bitBuffer, int $versionNumber):static; 54 | 55 | /** 56 | * reads a segment from the BitBuffer and decodes in the current data mode 57 | */ 58 | public static function decodeSegment(BitBuffer $bitBuffer, int $versionNumber):string; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Data/ReedSolomonEncoder.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2021 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Data; 13 | 14 | use chillerlan\QRCode\Common\{BitBuffer, EccLevel, GenericGFPoly, GF256, Version}; 15 | use function array_fill, array_merge, count, max; 16 | 17 | /** 18 | * Reed-Solomon encoding - ISO/IEC 18004:2000 Section 8.5 ff 19 | * 20 | * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding 21 | */ 22 | final class ReedSolomonEncoder{ 23 | 24 | private Version $version; 25 | private EccLevel $eccLevel; 26 | /** @var int[] */ 27 | private array $interleavedData; 28 | private int $interleavedDataIndex; 29 | 30 | /** 31 | * ReedSolomonDecoder constructor 32 | */ 33 | public function __construct(Version $version, EccLevel $eccLevel){ 34 | $this->version = $version; 35 | $this->eccLevel = $eccLevel; 36 | } 37 | 38 | /** 39 | * ECC encoding and interleaving 40 | * 41 | * @return int[] 42 | * @throws \chillerlan\QRCode\QRCodeException 43 | */ 44 | public function interleaveEcBytes(BitBuffer $bitBuffer):array{ 45 | [$numEccCodewords, [[$l1, $b1], [$l2, $b2]]] = $this->version->getRSBlocks($this->eccLevel); 46 | 47 | $rsBlocks = array_fill(0, $l1, [($numEccCodewords + $b1), $b1]); 48 | 49 | if($l2 > 0){ 50 | $rsBlocks = array_merge($rsBlocks, array_fill(0, $l2, [($numEccCodewords + $b2), $b2])); 51 | } 52 | 53 | $bitBufferData = $bitBuffer->getBuffer(); 54 | $dataBytes = []; 55 | $ecBytes = []; 56 | $maxDataBytes = 0; 57 | $maxEcBytes = 0; 58 | $dataByteOffset = 0; 59 | 60 | foreach($rsBlocks as $key => [$rsBlockTotal, $dataByteCount]){ 61 | $dataBytes[$key] = []; 62 | 63 | for($i = 0; $i < $dataByteCount; $i++){ 64 | $dataBytes[$key][$i] = ($bitBufferData[($i + $dataByteOffset)] & 0xff); 65 | } 66 | 67 | $ecByteCount = ($rsBlockTotal - $dataByteCount); 68 | $ecBytes[$key] = $this->encode($dataBytes[$key], $ecByteCount); 69 | $maxDataBytes = max($maxDataBytes, $dataByteCount); 70 | $maxEcBytes = max($maxEcBytes, $ecByteCount); 71 | $dataByteOffset += $dataByteCount; 72 | } 73 | 74 | $this->interleavedData = array_fill(0, $this->version->getTotalCodewords(), 0); 75 | $this->interleavedDataIndex = 0; 76 | $numRsBlocks = ($l1 + $l2); 77 | 78 | $this->interleave($dataBytes, $maxDataBytes, $numRsBlocks); 79 | $this->interleave($ecBytes, $maxEcBytes, $numRsBlocks); 80 | 81 | return $this->interleavedData; 82 | } 83 | 84 | /** 85 | * @param int[] $dataBytes 86 | * @return int[] 87 | */ 88 | private function encode(array $dataBytes, int $ecByteCount):array{ 89 | $rsPoly = new GenericGFPoly([1]); 90 | 91 | for($i = 0; $i < $ecByteCount; $i++){ 92 | $rsPoly = $rsPoly->multiply(new GenericGFPoly([1, GF256::exp($i)])); 93 | } 94 | 95 | $rsPolyDegree = $rsPoly->getDegree(); 96 | 97 | $modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree)) 98 | ->mod($rsPoly) 99 | ->getCoefficients() 100 | ; 101 | 102 | $ecBytes = array_fill(0, $rsPolyDegree, 0); 103 | $count = (count($modCoefficients) - $rsPolyDegree); 104 | 105 | foreach($ecBytes as $i => &$val){ 106 | $modIndex = ($i + $count); 107 | $val = 0; 108 | 109 | if($modIndex >= 0){ 110 | $val = $modCoefficients[$modIndex]; 111 | } 112 | } 113 | 114 | return $ecBytes; 115 | } 116 | 117 | /** 118 | * @param int[][] $byteArray 119 | */ 120 | private function interleave(array $byteArray, int $maxBytes, int $numRsBlocks):void{ 121 | for($x = 0; $x < $maxBytes; $x++){ 122 | for($y = 0; $y < $numRsBlocks; $y++){ 123 | if($x < count($byteArray[$y])){ 124 | $this->interleavedData[$this->interleavedDataIndex++] = $byteArray[$y][$x]; 125 | } 126 | } 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/Decoder/Decoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Decoder; 14 | 15 | use chillerlan\QRCode\QROptions; 16 | use chillerlan\QRCode\Common\{BitBuffer, EccLevel, LuminanceSourceInterface, MaskPattern, Mode, Version}; 17 | use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number}; 18 | use chillerlan\QRCode\Detector\Detector; 19 | use chillerlan\Settings\SettingsContainerInterface; 20 | use Throwable; 21 | use function chr, str_replace; 22 | 23 | /** 24 | * The main class which implements QR Code decoding -- as opposed to locating and extracting 25 | * the QR Code from an image. 26 | * 27 | * @author Sean Owen 28 | */ 29 | final class Decoder{ 30 | 31 | private SettingsContainerInterface|QROptions $options; 32 | private Version|null $version = null; 33 | private EccLevel|null $eccLevel = null; 34 | private MaskPattern|null $maskPattern = null; 35 | private BitBuffer $bitBuffer; 36 | 37 | public function __construct(SettingsContainerInterface|QROptions $options = new QROptions){ 38 | $this->options = $options; 39 | } 40 | 41 | /** 42 | * Decodes a QR Code represented as a BitMatrix. 43 | * A 1 or "true" is taken to mean a black module. 44 | * 45 | * @throws \Throwable|\chillerlan\QRCode\Decoder\QRCodeDecoderException 46 | */ 47 | public function decode(LuminanceSourceInterface $source):DecoderResult{ 48 | $matrix = (new Detector($source))->detect(); 49 | 50 | try{ 51 | // clone the BitMatrix to avoid errors in case we run into mirroring 52 | return $this->decodeMatrix(clone $matrix); 53 | } 54 | catch(Throwable $e){ 55 | 56 | try{ 57 | /* 58 | * Prepare for a mirrored reading. 59 | * 60 | * Since we're here, this means we have successfully detected some kind 61 | * of version and format information when mirrored. This is a good sign, 62 | * that the QR code may be mirrored, and we should try once more with a 63 | * mirrored content. 64 | */ 65 | return $this->decodeMatrix($matrix->resetVersionInfo()->mirrorDiagonal()); 66 | } 67 | catch(Throwable){ 68 | // Throw the exception from the original reading 69 | throw $e; 70 | } 71 | 72 | } 73 | 74 | } 75 | 76 | /** 77 | * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException 78 | */ 79 | private function decodeMatrix(BitMatrix $matrix):DecoderResult{ 80 | // Read raw codewords 81 | $rawCodewords = $matrix->readCodewords(); 82 | $this->version = $matrix->getVersion(); 83 | $this->eccLevel = $matrix->getEccLevel(); 84 | $this->maskPattern = $matrix->getMaskPattern(); 85 | 86 | if($this->version === null || $this->eccLevel === null || $this->maskPattern === null){ 87 | throw new QRCodeDecoderException('unable to read version or format info'); // @codeCoverageIgnore 88 | } 89 | 90 | $resultBytes = (new ReedSolomonDecoder($this->version, $this->eccLevel))->decode($rawCodewords); 91 | 92 | return $this->decodeBitStream($resultBytes); 93 | } 94 | 95 | /** 96 | * Decode the contents of that stream of bytes 97 | * 98 | * @throws \chillerlan\QRCode\Decoder\QRCodeDecoderException 99 | */ 100 | private function decodeBitStream(BitBuffer $bitBuffer):DecoderResult{ 101 | $this->bitBuffer = $bitBuffer; 102 | $versionNumber = $this->version->getVersionNumber(); 103 | $symbolSequence = -1; 104 | $parityData = -1; 105 | $fc1InEffect = false; 106 | $result = ''; 107 | 108 | // While still another segment to read... 109 | while($this->bitBuffer->available() >= 4){ 110 | $datamode = $this->bitBuffer->read(4); // mode is encoded by 4 bits 111 | 112 | // OK, assume we're done 113 | if($datamode === Mode::TERMINATOR){ 114 | break; 115 | } 116 | elseif($datamode === Mode::NUMBER){ 117 | $result .= Number::decodeSegment($this->bitBuffer, $versionNumber); 118 | } 119 | elseif($datamode === Mode::ALPHANUM){ 120 | $result .= $this->decodeAlphanumSegment($versionNumber, $fc1InEffect); 121 | } 122 | elseif($datamode === Mode::BYTE){ 123 | $result .= Byte::decodeSegment($this->bitBuffer, $versionNumber); 124 | } 125 | elseif($datamode === Mode::KANJI){ 126 | $result .= Kanji::decodeSegment($this->bitBuffer, $versionNumber); 127 | } 128 | elseif($datamode === Mode::STRCTURED_APPEND){ 129 | 130 | if($this->bitBuffer->available() < 16){ 131 | throw new QRCodeDecoderException('structured append: not enough bits left'); 132 | } 133 | // sequence number and parity is added later to the result metadata 134 | // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue 135 | $symbolSequence = $this->bitBuffer->read(8); 136 | $parityData = $this->bitBuffer->read(8); 137 | } 138 | elseif($datamode === Mode::FNC1_FIRST || $datamode === Mode::FNC1_SECOND){ 139 | // We do little with FNC1 except alter the parsed result a bit according to the spec 140 | $fc1InEffect = true; 141 | } 142 | elseif($datamode === Mode::ECI){ 143 | $result .= ECI::decodeSegment($this->bitBuffer, $versionNumber); 144 | } 145 | elseif($datamode === Mode::HANZI){ 146 | $result .= Hanzi::decodeSegment($this->bitBuffer, $versionNumber); 147 | } 148 | else{ 149 | throw new QRCodeDecoderException('invalid data mode'); 150 | } 151 | 152 | } 153 | 154 | return new DecoderResult([ 155 | 'rawBytes' => $this->bitBuffer, 156 | 'data' => $result, 157 | 'version' => $this->version, 158 | 'eccLevel' => $this->eccLevel, 159 | 'maskPattern' => $this->maskPattern, 160 | 'structuredAppendParity' => $parityData, 161 | 'structuredAppendSequence' => $symbolSequence, 162 | ]); 163 | } 164 | 165 | private function decodeAlphanumSegment(int $versionNumber, bool $fc1InEffect):string{ 166 | $str = AlphaNum::decodeSegment($this->bitBuffer, $versionNumber); 167 | 168 | // See section 6.4.8.1, 6.4.8.2 169 | if($fc1InEffect){ // ??? 170 | // We need to massage the result a bit if in an FNC1 mode: 171 | $str = str_replace(chr(0x1d), '%', $str); 172 | $str = str_replace('%%', '%', $str); 173 | } 174 | 175 | return $str; 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/Decoder/DecoderResult.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Decoder; 14 | 15 | use chillerlan\QRCode\Common\{BitBuffer, EccLevel, MaskPattern, Version}; 16 | use chillerlan\QRCode\Data\QRMatrix; 17 | use function property_exists; 18 | 19 | /** 20 | * Encapsulates the result of decoding a matrix of bits. This typically 21 | * applies to 2D barcode formats. For now, it contains the raw bytes obtained 22 | * as well as a String interpretation of those bytes, if applicable. 23 | * 24 | * @property \chillerlan\QRCode\Common\BitBuffer $rawBytes 25 | * @property string $data 26 | * @property \chillerlan\QRCode\Common\Version $version 27 | * @property \chillerlan\QRCode\Common\EccLevel $eccLevel 28 | * @property \chillerlan\QRCode\Common\MaskPattern $maskPattern 29 | * @property int $structuredAppendParity 30 | * @property int $structuredAppendSequence 31 | */ 32 | final class DecoderResult{ 33 | 34 | private BitBuffer $rawBytes; 35 | private Version $version; 36 | private EccLevel $eccLevel; 37 | private MaskPattern $maskPattern; 38 | private string $data = ''; 39 | private int $structuredAppendParity = -1; 40 | private int $structuredAppendSequence = -1; 41 | 42 | /** 43 | * DecoderResult constructor. 44 | * 45 | * @phpstan-param array $properties 46 | */ 47 | public function __construct(iterable|null $properties = null){ 48 | 49 | if(!empty($properties)){ 50 | 51 | foreach($properties as $property => $value){ 52 | 53 | if(!property_exists($this, $property)){ 54 | continue; 55 | } 56 | 57 | $this->{$property} = $value; 58 | } 59 | 60 | } 61 | 62 | } 63 | 64 | public function __get(string $property):mixed{ 65 | 66 | if(property_exists($this, $property)){ 67 | return $this->{$property}; 68 | } 69 | 70 | return null; 71 | } 72 | 73 | public function __toString():string{ 74 | return $this->data; 75 | } 76 | 77 | public function hasStructuredAppend():bool{ 78 | return $this->structuredAppendParity >= 0 && $this->structuredAppendSequence >= 0; 79 | } 80 | 81 | /** 82 | * Returns a QRMatrix instance with the settings and data of the reader result 83 | */ 84 | public function getQRMatrix():QRMatrix{ 85 | return (new QRMatrix($this->version, $this->eccLevel)) 86 | ->initFunctionalPatterns() 87 | ->writeCodewords($this->rawBytes) 88 | ->setFormatInfo($this->maskPattern) 89 | ->mask($this->maskPattern) 90 | ; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/Decoder/QRCodeDecoderException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2021 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Decoder; 13 | 14 | use chillerlan\QRCode\QRCodeException; 15 | 16 | /** 17 | * An exception container 18 | */ 19 | final class QRCodeDecoderException extends QRCodeException{ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Detector/AlignmentPattern.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Detector; 14 | 15 | /** 16 | * Encapsulates an alignment pattern, which are the smaller square patterns found in 17 | * all but the simplest QR Codes. 18 | * 19 | * @author Sean Owen 20 | */ 21 | final class AlignmentPattern extends ResultPoint{ 22 | 23 | /** 24 | * Combines this object's current estimate of a finder pattern position and module size 25 | * with a new estimate. It returns a new FinderPattern containing an average of the two. 26 | */ 27 | public function combineEstimate(float $i, float $j, float $newModuleSize):static{ 28 | return new self( 29 | (($this->x + $j) / 2.0), 30 | (($this->y + $i) / 2.0), 31 | (($this->estimatedModuleSize + $newModuleSize) / 2.0), 32 | ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Detector/FinderPattern.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Detector; 14 | 15 | use function sqrt; 16 | 17 | /** 18 | * Encapsulates a finder pattern, which are the three square patterns found in 19 | * the corners of QR Codes. It also encapsulates a count of similar finder patterns, 20 | * as a convenience to the finder's bookkeeping. 21 | * 22 | * @author Sean Owen 23 | */ 24 | final class FinderPattern extends ResultPoint{ 25 | 26 | private int $count; 27 | 28 | public function __construct(float $posX, float $posY, float $estimatedModuleSize, int|null $count = null){ 29 | parent::__construct($posX, $posY, $estimatedModuleSize); 30 | 31 | $this->count = ($count ?? 1); 32 | } 33 | 34 | public function getCount():int{ 35 | return $this->count; 36 | } 37 | 38 | /** 39 | * @param \chillerlan\QRCode\Detector\FinderPattern $b second pattern 40 | * 41 | * @return float distance between two points 42 | */ 43 | public function getDistance(FinderPattern $b):float{ 44 | return self::distance($this->x, $this->y, $b->x, $b->y); 45 | } 46 | 47 | /** 48 | * Get square of distance between a and b. 49 | */ 50 | public function getSquaredDistance(FinderPattern $b):float{ 51 | return self::squaredDistance($this->x, $this->y, $b->x, $b->y); 52 | } 53 | 54 | /** 55 | * Combines this object's current estimate of a finder pattern position and module size 56 | * with a new estimate. It returns a new FinderPattern containing a weighted average 57 | * based on count. 58 | */ 59 | public function combineEstimate(float $i, float $j, float $newModuleSize):static{ 60 | $combinedCount = ($this->count + 1); 61 | 62 | return new self( 63 | ($this->count * $this->x + $j) / $combinedCount, 64 | ($this->count * $this->y + $i) / $combinedCount, 65 | ($this->count * $this->estimatedModuleSize + $newModuleSize) / $combinedCount, 66 | $combinedCount, 67 | ); 68 | } 69 | 70 | private static function squaredDistance(float $aX, float $aY, float $bX, float $bY):float{ 71 | $xDiff = ($aX - $bX); 72 | $yDiff = ($aY - $bY); 73 | 74 | return ($xDiff * $xDiff + $yDiff * $yDiff); 75 | } 76 | 77 | public static function distance(float $aX, float $aY, float $bX, float $bY):float{ 78 | return sqrt(self::squaredDistance($aX, $aY, $bX, $bY)); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Detector/GridSampler.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Detector; 14 | 15 | use chillerlan\QRCode\Data\QRMatrix; 16 | use chillerlan\QRCode\Decoder\BitMatrix; 17 | use function array_fill, count, intdiv, sprintf; 18 | 19 | /** 20 | * Implementations of this class can, given locations of finder patterns for a QR code in an 21 | * image, sample the right points in the image to reconstruct the QR code, accounting for 22 | * perspective distortion. It is abstracted since it is relatively expensive and should be allowed 23 | * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced 24 | * Imaging library, but which may not be available in other environments such as J2ME, and vice 25 | * versa. 26 | * 27 | * The implementation used can be controlled by calling #setGridSampler(GridSampler) 28 | * with an instance of a class which implements this interface. 29 | * 30 | * @author Sean Owen 31 | */ 32 | final class GridSampler{ 33 | 34 | /** @var float[] */ 35 | private array $points; 36 | 37 | /** 38 | * Checks a set of points that have been transformed to sample points on an image against 39 | * the image's dimensions to see if the point are even within the image. 40 | * 41 | * This method will actually "nudge" the endpoints back onto the image if they are found to be 42 | * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder 43 | * patterns in an image where the QR Code runs all the way to the image border. 44 | * 45 | * For efficiency, the method will check points from either end of the line until one is found 46 | * to be within the image. Because the set of points are assumed to be linear, this is valid. 47 | * 48 | * @param int $dimension matrix width/height 49 | * 50 | * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if an endpoint is lies outside the image boundaries 51 | */ 52 | private function checkAndNudgePoints(int $dimension):void{ 53 | $nudged = true; 54 | $max = count($this->points); 55 | 56 | // Check and nudge points from start until we see some that are OK: 57 | for($offset = 0; $offset < $max && $nudged; $offset += 2){ 58 | $x = (int)$this->points[$offset]; 59 | $y = (int)$this->points[($offset + 1)]; 60 | 61 | if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){ 62 | throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 1, x: %s, y: %s, d: %s', $x, $y, $dimension)); 63 | } 64 | 65 | $nudged = false; 66 | 67 | if($x === -1){ 68 | $this->points[$offset] = 0.0; 69 | $nudged = true; 70 | } 71 | elseif($x === $dimension){ 72 | $this->points[$offset] = ($dimension - 1); 73 | $nudged = true; 74 | } 75 | 76 | if($y === -1){ 77 | $this->points[($offset + 1)] = 0.0; 78 | $nudged = true; 79 | } 80 | elseif($y === $dimension){ 81 | $this->points[($offset + 1)] = ($dimension - 1); 82 | $nudged = true; 83 | } 84 | 85 | } 86 | 87 | // Check and nudge points from end: 88 | $nudged = true; 89 | 90 | for($offset = ($max - 2); $offset >= 0 && $nudged; $offset -= 2){ 91 | $x = (int)$this->points[$offset]; 92 | $y = (int)$this->points[($offset + 1)]; 93 | 94 | if($x < -1 || $x > $dimension || $y < -1 || $y > $dimension){ 95 | throw new QRCodeDetectorException(sprintf('checkAndNudgePoints 2, x: %s, y: %s, d: %s', $x, $y, $dimension)); 96 | } 97 | 98 | $nudged = false; 99 | 100 | if($x === -1){ 101 | $this->points[$offset] = 0.0; 102 | $nudged = true; 103 | } 104 | elseif($x === $dimension){ 105 | $this->points[$offset] = ($dimension - 1); 106 | $nudged = true; 107 | } 108 | 109 | if($y === -1){ 110 | $this->points[($offset + 1)] = 0.0; 111 | $nudged = true; 112 | } 113 | elseif($y === $dimension){ 114 | $this->points[($offset + 1)] = ($dimension - 1); 115 | $nudged = true; 116 | } 117 | 118 | } 119 | 120 | } 121 | 122 | /** 123 | * Samples an image for a rectangular matrix of bits of the given dimension. The sampling 124 | * transformation is determined by the coordinates of 4 points, in the original and transformed 125 | * image space. 126 | * 127 | * @return \chillerlan\QRCode\Decoder\BitMatrix representing a grid of points sampled from the image within a region 128 | * defined by the "from" parameters 129 | * @throws \chillerlan\QRCode\Detector\QRCodeDetectorException if image can't be sampled, for example, if the transformation defined 130 | * by the given points is invalid or results in sampling outside the image boundaries 131 | */ 132 | public function sampleGrid(BitMatrix $matrix, int $dimension, PerspectiveTransform $transform):BitMatrix{ 133 | 134 | if($dimension <= 0){ 135 | throw new QRCodeDetectorException('invalid matrix size'); 136 | } 137 | 138 | $bits = new BitMatrix($dimension); 139 | $this->points = array_fill(0, (2 * $dimension), 0.0); 140 | 141 | for($y = 0; $y < $dimension; $y++){ 142 | $max = count($this->points); 143 | $iValue = ($y + 0.5); 144 | 145 | for($x = 0; $x < $max; $x += 2){ 146 | $this->points[$x] = (($x / 2) + 0.5); 147 | $this->points[($x + 1)] = $iValue; 148 | } 149 | // phpcs:ignore 150 | [$this->points, ] = $transform->transformPoints($this->points); 151 | // Quick check to see if points transformed to something inside the image; 152 | // sufficient to check the endpoints 153 | $this->checkAndNudgePoints($matrix->getSize()); 154 | 155 | // no need to try/catch as QRMatrix::set() will silently discard out of bounds values 156 | # try{ 157 | for($x = 0; $x < $max; $x += 2){ 158 | // Black(-ish) pixel 159 | $bits->set( 160 | intdiv($x, 2), 161 | $y, 162 | $matrix->check((int)$this->points[$x], (int)$this->points[($x + 1)]), 163 | QRMatrix::M_DATA, 164 | ); 165 | } 166 | # } 167 | # catch(\Throwable $aioobe){//ArrayIndexOutOfBoundsException 168 | // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting 169 | // transform gets "twisted" such that it maps a straight line of points to a set of points 170 | // whose endpoints are in bounds, but others are not. There is probably some mathematical 171 | // way to detect this about the transformation that I don't know yet. 172 | // This results in an ugly runtime exception despite our clever checks above -- can't have 173 | // that. We could check each point's coordinates but that feels duplicative. We settle for 174 | // catching and wrapping ArrayIndexOutOfBoundsException. 175 | # throw new QRCodeDetectorException('ArrayIndexOutOfBoundsException'); 176 | # } 177 | 178 | } 179 | 180 | return $bits; 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/Detector/PerspectiveTransform.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Detector; 14 | 15 | use function count; 16 | 17 | /** 18 | * This class implements a perspective transform in two dimensions. Given four source and four 19 | * destination points, it will compute the transformation implied between them. The code is based 20 | * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56. 21 | * 22 | * @author Sean Owen 23 | */ 24 | final class PerspectiveTransform{ 25 | 26 | private float $a11; 27 | private float $a12; 28 | private float $a13; 29 | private float $a21; 30 | private float $a22; 31 | private float $a23; 32 | private float $a31; 33 | private float $a32; 34 | private float $a33; 35 | 36 | private function set( 37 | float $a11, float $a21, float $a31, 38 | float $a12, float $a22, float $a32, 39 | float $a13, float $a23, float $a33, 40 | ):self{ 41 | $this->a11 = $a11; 42 | $this->a12 = $a12; 43 | $this->a13 = $a13; 44 | $this->a21 = $a21; 45 | $this->a22 = $a22; 46 | $this->a23 = $a23; 47 | $this->a31 = $a31; 48 | $this->a32 = $a32; 49 | $this->a33 = $a33; 50 | 51 | return $this; 52 | } 53 | 54 | public function quadrilateralToQuadrilateral( 55 | float $x0, float $y0, float $x1, float $y1, float $x2, float $y2, float $x3, float $y3, 56 | float $x0p, float $y0p, float $x1p, float $y1p, float $x2p, float $y2p, float $x3p, float $y3p, 57 | ):self{ 58 | return (new self) 59 | ->squareToQuadrilateral($x0p, $y0p, $x1p, $y1p, $x2p, $y2p, $x3p, $y3p) 60 | ->times($this->quadrilateralToSquare($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)); 61 | } 62 | 63 | private function quadrilateralToSquare( 64 | float $x0, float $y0, float $x1, float $y1, 65 | float $x2, float $y2, float $x3, float $y3, 66 | ):self{ 67 | // Here, the adjoint serves as the inverse: 68 | return $this 69 | ->squareToQuadrilateral($x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3) 70 | ->buildAdjoint(); 71 | } 72 | 73 | private function buildAdjoint():self{ 74 | // Adjoint is the transpose of the cofactor matrix: 75 | return $this->set( 76 | ($this->a22 * $this->a33 - $this->a23 * $this->a32), 77 | ($this->a23 * $this->a31 - $this->a21 * $this->a33), 78 | ($this->a21 * $this->a32 - $this->a22 * $this->a31), 79 | ($this->a13 * $this->a32 - $this->a12 * $this->a33), 80 | ($this->a11 * $this->a33 - $this->a13 * $this->a31), 81 | ($this->a12 * $this->a31 - $this->a11 * $this->a32), 82 | ($this->a12 * $this->a23 - $this->a13 * $this->a22), 83 | ($this->a13 * $this->a21 - $this->a11 * $this->a23), 84 | ($this->a11 * $this->a22 - $this->a12 * $this->a21), 85 | ); 86 | } 87 | 88 | private function squareToQuadrilateral( 89 | float $x0, float $y0, float $x1, float $y1, 90 | float $x2, float $y2, float $x3, float $y3, 91 | ):self{ 92 | $dx3 = ($x0 - $x1 + $x2 - $x3); 93 | $dy3 = ($y0 - $y1 + $y2 - $y3); 94 | 95 | if($dx3 === 0.0 && $dy3 === 0.0){ 96 | // Affine 97 | return $this->set(($x1 - $x0), ($x2 - $x1), $x0, ($y1 - $y0), ($y2 - $y1), $y0, 0.0, 0.0, 1.0); 98 | } 99 | 100 | $dx1 = ($x1 - $x2); 101 | $dx2 = ($x3 - $x2); 102 | $dy1 = ($y1 - $y2); 103 | $dy2 = ($y3 - $y2); 104 | $denominator = ($dx1 * $dy2 - $dx2 * $dy1); 105 | $a13 = (($dx3 * $dy2 - $dx2 * $dy3) / $denominator); 106 | $a23 = (($dx1 * $dy3 - $dx3 * $dy1) / $denominator); 107 | 108 | return $this->set( 109 | ($x1 - $x0 + $a13 * $x1), 110 | ($x3 - $x0 + $a23 * $x3), 111 | $x0, 112 | ($y1 - $y0 + $a13 * $y1), 113 | ($y3 - $y0 + $a23 * $y3), 114 | $y0, 115 | $a13, 116 | $a23, 117 | 1.0, 118 | ); 119 | } 120 | 121 | private function times(PerspectiveTransform $other):self{ 122 | return $this->set( 123 | ($this->a11 * $other->a11 + $this->a21 * $other->a12 + $this->a31 * $other->a13), 124 | ($this->a11 * $other->a21 + $this->a21 * $other->a22 + $this->a31 * $other->a23), 125 | ($this->a11 * $other->a31 + $this->a21 * $other->a32 + $this->a31 * $other->a33), 126 | ($this->a12 * $other->a11 + $this->a22 * $other->a12 + $this->a32 * $other->a13), 127 | ($this->a12 * $other->a21 + $this->a22 * $other->a22 + $this->a32 * $other->a23), 128 | ($this->a12 * $other->a31 + $this->a22 * $other->a32 + $this->a32 * $other->a33), 129 | ($this->a13 * $other->a11 + $this->a23 * $other->a12 + $this->a33 * $other->a13), 130 | ($this->a13 * $other->a21 + $this->a23 * $other->a22 + $this->a33 * $other->a23), 131 | ($this->a13 * $other->a31 + $this->a23 * $other->a32 + $this->a33 * $other->a33), 132 | ); 133 | } 134 | 135 | /** 136 | * @param float[] $xValues 137 | * @param float[]|null $yValues 138 | * 139 | * @return float[][] [$xValues, $yValues] 140 | */ 141 | public function transformPoints(array $xValues, array|null $yValues = null):array{ 142 | $max = count($xValues); 143 | 144 | if($yValues !== null){ // unused 145 | 146 | for($i = 0; $i < $max; $i++){ 147 | $x = $xValues[$i]; 148 | $y = $yValues[$i]; 149 | $denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33); 150 | $xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator); 151 | $yValues[$i] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator); 152 | } 153 | 154 | return [$xValues, $yValues]; 155 | } 156 | 157 | for($i = 0; $i < $max; $i += 2){ 158 | $x = $xValues[$i]; 159 | $y = $xValues[($i + 1)]; 160 | $denominator = ($this->a13 * $x + $this->a23 * $y + $this->a33); 161 | $xValues[$i] = (($this->a11 * $x + $this->a21 * $y + $this->a31) / $denominator); 162 | $xValues[($i + 1)] = (($this->a12 * $x + $this->a22 * $y + $this->a32) / $denominator); 163 | } 164 | 165 | return [$xValues, []]; 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/Detector/QRCodeDetectorException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2021 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Detector; 13 | 14 | use chillerlan\QRCode\QRCodeException; 15 | 16 | /** 17 | * An exception container 18 | */ 19 | final class QRCodeDetectorException extends QRCodeException{ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Detector/ResultPoint.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright 2021 Smiley 9 | * @license Apache-2.0 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace chillerlan\QRCode\Detector; 14 | 15 | use function abs; 16 | 17 | /** 18 | * Encapsulates a point of interest in an image containing a barcode. Typically, this 19 | * would be the location of a finder pattern or the corner of the barcode, for example. 20 | * 21 | * @author Sean Owen 22 | */ 23 | abstract class ResultPoint{ 24 | 25 | protected float $x; 26 | protected float $y; 27 | protected float $estimatedModuleSize; 28 | 29 | public function __construct(float $x, float $y, float $estimatedModuleSize){ 30 | $this->x = $x; 31 | $this->y = $y; 32 | $this->estimatedModuleSize = $estimatedModuleSize; 33 | } 34 | 35 | public function getX():float{ 36 | return $this->x; 37 | } 38 | 39 | public function getY():float{ 40 | return $this->y; 41 | } 42 | 43 | public function getEstimatedModuleSize():float{ 44 | return $this->estimatedModuleSize; 45 | } 46 | 47 | /** 48 | * Determines if this finder pattern "about equals" a finder pattern at the stated 49 | * position and size -- meaning, it is at nearly the same center with nearly the same size. 50 | */ 51 | public function aboutEquals(float $moduleSize, float $i, float $j):bool{ 52 | 53 | if(abs($i - $this->y) <= $moduleSize && abs($j - $this->x) <= $moduleSize){ 54 | $moduleSizeDiff = abs($moduleSize - $this->estimatedModuleSize); 55 | 56 | return $moduleSizeDiff <= 1.0 || $moduleSizeDiff <= $this->estimatedModuleSize; 57 | } 58 | 59 | return false; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Output/CssColorModuleValueTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2024 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use function is_string, preg_match, strip_tags, trim; 15 | 16 | /** 17 | * Module value checks for output classes that use CSS colors 18 | */ 19 | trait CssColorModuleValueTrait{ 20 | 21 | /** 22 | * note: we're not necessarily validating the several values, just checking the general syntax 23 | * note: css4 colors are not included 24 | * 25 | * implements \chillerlan\QRCode\Output\QROutputInterface::moduleValueIsValid() 26 | * 27 | * @todo: XSS proof 28 | * 29 | * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value 30 | * 31 | * @param string $value 32 | */ 33 | public static function moduleValueIsValid(mixed $value):bool{ 34 | 35 | if(!is_string($value)){ 36 | return false; 37 | } 38 | 39 | $value = trim(strip_tags($value), " '\"\r\n\t"); 40 | 41 | // hex notation 42 | // #rgb(a) 43 | // #rrggbb(aa) 44 | if(preg_match('/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i', $value)){ 45 | return true; 46 | } 47 | 48 | // css: hsla/rgba(...values) 49 | if(preg_match('#^(hsla?|rgba?)\([\d .,%/]+\)$#i', $value)){ 50 | return true; 51 | } 52 | 53 | // predefined css color 54 | if(preg_match('/^[a-z]+$/i', $value)){ 55 | return true; 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /** 62 | * implements \chillerlan\QRCode\Output\QROutputAbstract::prepareModuleValue() 63 | * 64 | * @param string $value 65 | */ 66 | protected function prepareModuleValue(mixed $value):string{ 67 | return trim(strip_tags($value), " '\"\r\n\t"); 68 | } 69 | 70 | /** 71 | * implements \chillerlan\QRCode\Output\QROutputAbstract::getDefaultModuleValue() 72 | */ 73 | protected function getDefaultModuleValue(bool $isDark):string{ 74 | return ($isDark) ? '#000' : '#fff'; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/Output/QRCodeOutputException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use chillerlan\QRCode\QRCodeException; 15 | 16 | /** 17 | * An exception container 18 | */ 19 | final class QRCodeOutputException extends QRCodeException{ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Output/QREps.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2022 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use function array_values, count, date, implode, is_array, is_numeric, max, min, round, sprintf; 15 | 16 | /** 17 | * Encapsulated Postscript (EPS) output 18 | * 19 | * @see https://github.com/t0k4rt/phpqrcode/blob/bb29e6eb77e0a2a85bb0eb62725e0adc11ff5a90/qrvect.php#L52-L137 20 | * @see https://web.archive.org/web/20170818010030/http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/postscript/pdfs/5002.EPSF_Spec.pdf 21 | * @see https://web.archive.org/web/20210419003859/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf 22 | * @see https://github.com/chillerlan/php-qrcode/discussions/148 23 | */ 24 | class QREps extends QROutputAbstract{ 25 | 26 | final public const MIME_TYPE = 'application/postscript'; 27 | 28 | public static function moduleValueIsValid(mixed $value):bool{ 29 | 30 | if(!is_array($value) || count($value) < 3){ 31 | return false; 32 | } 33 | 34 | // check the first values of the array 35 | foreach(array_values($value) as $i => $val){ 36 | 37 | if($i > 3){ 38 | break; 39 | } 40 | 41 | if(!is_numeric($val)){ 42 | return false; 43 | } 44 | 45 | } 46 | 47 | return true; 48 | } 49 | 50 | protected function prepareModuleValue(mixed $value):string{ 51 | $values = []; 52 | 53 | foreach(array_values($value) as $i => $val){ 54 | 55 | if($i > 3){ 56 | break; 57 | } 58 | 59 | // clamp value and convert from int 0-255 to float 0-1 RGB/CMYK range 60 | $values[] = round((max(0, min(255, intval($val))) / 255), 6); 61 | } 62 | 63 | return $this->formatColor($values); 64 | } 65 | 66 | protected function getDefaultModuleValue(bool $isDark):string{ 67 | return $this->formatColor(($isDark) ? [0.0, 0.0, 0.0] : [1.0, 1.0, 1.0]); 68 | } 69 | 70 | /** 71 | * Set the color format string 72 | * 73 | * 4 values in the color array will be interpreted as CMYK, 3 as RGB 74 | * 75 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 76 | * 77 | * @param float[] $values 78 | */ 79 | protected function formatColor(array $values):string{ 80 | $count = count($values); 81 | 82 | if($count < 3){ 83 | throw new QRCodeOutputException('invalid color value'); 84 | } 85 | 86 | // the EPS functions "C" and "R" are defined in the header below 87 | $format = ($count === 4) 88 | ? '%f %f %f %f C' // CMYK 89 | : '%f %f %f R'; // RGB 90 | 91 | return sprintf($format, ...$values); 92 | } 93 | 94 | public function dump(string|null $file = null):string{ 95 | 96 | $eps = implode("\n", [ 97 | // initialize header 98 | $this->header(), 99 | // create the path elements 100 | $this->paths(), 101 | // end file 102 | '%%EOF', 103 | ]); 104 | 105 | $this->saveToFile($eps, $file); 106 | 107 | return $eps; 108 | } 109 | 110 | /** 111 | * Returns the main header for the EPS file, including function definitions and background 112 | */ 113 | protected function header():string{ 114 | [$width, $height] = $this->getOutputDimensions(); 115 | 116 | $header = [ 117 | // main header 118 | '%!PS-Adobe-3.0 EPSF-3.0', 119 | '%%Creator: php-qrcode (https://github.com/chillerlan/php-qrcode)', 120 | '%%Title: QR Code', 121 | sprintf('%%%%CreationDate: %1$s', date('c')), 122 | '%%DocumentData: Clean7Bit', 123 | '%%LanguageLevel: 3', 124 | sprintf('%%%%BoundingBox: 0 0 %s %s', $width, $height), 125 | '%%EndComments', 126 | // function definitions 127 | '%%BeginProlog', 128 | '/F { rectfill } def', 129 | '/R { setrgbcolor } def', 130 | '/C { setcmykcolor } def', 131 | '%%EndProlog', 132 | ]; 133 | 134 | if($this::moduleValueIsValid($this->options->bgColor)){ 135 | $header[] = $this->prepareModuleValue($this->options->bgColor); 136 | $header[] = sprintf('0 0 %s %s F', $width, $height); 137 | } 138 | 139 | return implode("\n", $header); 140 | } 141 | 142 | /** 143 | * returns one or more EPS path blocks 144 | */ 145 | protected function paths():string{ 146 | $paths = $this->collectModules($this->module(...)); 147 | $eps = []; 148 | 149 | foreach($paths as $M_TYPE => $path){ 150 | 151 | if(empty($path)){ 152 | continue; 153 | } 154 | 155 | $eps[] = $this->getModuleValue($M_TYPE); 156 | $eps[] = implode("\n", $path); 157 | } 158 | 159 | return implode("\n", $eps); 160 | } 161 | 162 | /** 163 | * Returns a path segment for a single module 164 | */ 165 | protected function module(int $x, int $y, int $M_TYPE):string{ 166 | 167 | if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ 168 | return ''; 169 | } 170 | 171 | $outputX = ($x * $this->scale); 172 | // Actual size - one block = Topmost y pos. 173 | $top = ($this->length - $this->scale); 174 | // Apparently y-axis is inverted (y0 is at bottom and not top) in EPS, so we have to switch the y-axis here 175 | $outputY = ($top - ($y * $this->scale)); 176 | 177 | return sprintf('%d %d %d %d F', $outputX, $outputY, $this->scale, $this->scale); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/Output/QRFpdf.php: -------------------------------------------------------------------------------- 1 | options->fpdfMeasureUnit, $this->getOutputDimensions()); 62 | $fpdf->AddPage(); 63 | 64 | return $fpdf; 65 | } 66 | 67 | public function dump(string|null $file = null, FPDF|null $fpdf = null):string|FPDF{ 68 | $this->fpdf = ($fpdf ?? $this->initFPDF()); 69 | 70 | if($this::moduleValueIsValid($this->options->bgColor)){ 71 | $bgColor = $this->prepareModuleValue($this->options->bgColor); 72 | [$width, $height] = $this->getOutputDimensions(); 73 | 74 | $this->fpdf->SetFillColor(...$bgColor); 75 | $this->fpdf->Rect(0, 0, $width, $height, 'F'); 76 | } 77 | 78 | $this->prevColor = null; 79 | 80 | foreach($this->matrix->getMatrix() as $y => $row){ 81 | foreach($row as $x => $M_TYPE){ 82 | $this->module($x, $y, $M_TYPE); 83 | } 84 | } 85 | 86 | if($this->options->returnResource){ 87 | return $this->fpdf; 88 | } 89 | 90 | $pdfData = $this->fpdf->Output('S'); 91 | 92 | $this->saveToFile($pdfData, $file); 93 | 94 | if($this->options->outputBase64){ 95 | $pdfData = $this->toBase64DataURI($pdfData); 96 | } 97 | 98 | return $pdfData; 99 | } 100 | 101 | /** 102 | * Renders a single module 103 | */ 104 | protected function module(int $x, int $y, int $M_TYPE):void{ 105 | 106 | if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ 107 | return; 108 | } 109 | 110 | $color = $this->getModuleValue($M_TYPE); 111 | 112 | if($color !== null && $color !== $this->prevColor){ 113 | $this->fpdf->SetFillColor(...$color); 114 | $this->prevColor = $color; 115 | } 116 | 117 | $this->fpdf->Rect(($x * $this->scale), ($y * $this->scale), $this->scale, $this->scale, 'F'); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/Output/QRGdImage.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use chillerlan\QRCode\QROptions; 17 | use chillerlan\QRCode\Data\QRMatrix; 18 | use chillerlan\Settings\SettingsContainerInterface; 19 | use GdImage; 20 | use function extension_loaded, imagecolorallocate, imagecolortransparent, 21 | imagecreatetruecolor, imagedestroy, imagefilledellipse, imagefilledrectangle, 22 | imagescale, imagetypes, intdiv, intval, max, min, ob_end_clean, ob_get_contents, ob_start, 23 | sprintf; 24 | use const IMG_AVIF, IMG_BMP, IMG_GIF, IMG_JPG, IMG_PNG, IMG_WEBP; 25 | 26 | /** 27 | * Converts the matrix into GD images, raw or base64 output (requires ext-gd) 28 | * 29 | * @see https://php.net/manual/book.image.php 30 | * @see https://github.com/chillerlan/php-qrcode/issues/223 31 | */ 32 | abstract class QRGdImage extends QROutputAbstract{ 33 | use RGBArrayModuleValueTrait; 34 | 35 | /** 36 | * The GD image resource 37 | * 38 | * @see imagecreatetruecolor() 39 | */ 40 | protected GdImage $image; 41 | 42 | /** 43 | * The allocated background color 44 | * 45 | * @see \imagecolorallocate() 46 | */ 47 | protected int $background; 48 | 49 | /** 50 | * Whether we're running in upscale mode (scale < 20) 51 | * 52 | * @see \chillerlan\QRCode\QROptions::$drawCircularModules 53 | */ 54 | protected bool $upscaled = false; 55 | 56 | /** 57 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 58 | * @noinspection PhpMissingParentConstructorInspection 59 | */ 60 | public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix){ 61 | $this->options = $options; 62 | $this->matrix = $matrix; 63 | 64 | $this->checkGD(); 65 | 66 | if($this->options->invertMatrix){ 67 | $this->matrix->invert(); 68 | } 69 | 70 | $this->copyVars(); 71 | $this->setMatrixDimensions(); 72 | } 73 | 74 | /** 75 | * Checks whether GD is installed and if the given mode is supported 76 | * 77 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 78 | * @codeCoverageIgnore 79 | */ 80 | protected function checkGD():void{ 81 | 82 | if(!extension_loaded('gd')){ 83 | throw new QRCodeOutputException('ext-gd not loaded'); 84 | } 85 | 86 | $modes = [ 87 | QRGdImageAVIF::class => IMG_AVIF, 88 | QRGdImageBMP::class => IMG_BMP, 89 | QRGdImageGIF::class => IMG_GIF, 90 | QRGdImageJPEG::class => IMG_JPG, 91 | QRGdImagePNG::class => IMG_PNG, 92 | QRGdImageWEBP::class => IMG_WEBP, 93 | ]; 94 | 95 | // likely using custom output/manual invocation 96 | if(!isset($modes[$this->options->outputInterface])){ 97 | return; 98 | } 99 | 100 | $mode = $modes[$this->options->outputInterface]; 101 | 102 | if((imagetypes() & $mode) !== $mode){ 103 | throw new QRCodeOutputException(sprintf('output mode "%s" not supported', $this->options->outputInterface)); 104 | } 105 | 106 | } 107 | 108 | /** 109 | * @inheritDoc 110 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 111 | */ 112 | protected function prepareModuleValue(mixed $value):int{ 113 | $values = []; 114 | 115 | foreach(array_values($value) as $i => $val){ 116 | 117 | if($i > 2){ 118 | break; 119 | } 120 | 121 | $values[] = max(0, min(255, intval($val))); 122 | } 123 | 124 | $color = imagecolorallocate($this->image, ...$values); 125 | 126 | if($color === false){ 127 | throw new QRCodeOutputException('could not set color: imagecolorallocate() error'); 128 | } 129 | 130 | return $color; 131 | } 132 | 133 | protected function getDefaultModuleValue(bool $isDark):int{ 134 | return $this->prepareModuleValue(($isDark) ? [0, 0, 0] : [255, 255, 255]); 135 | } 136 | 137 | /** 138 | * @inheritDoc 139 | * 140 | * @throws \ErrorException|\chillerlan\QRCode\Output\QRCodeOutputException 141 | */ 142 | public function dump(string|null $file = null):string|GdImage{ 143 | $this->image = $this->createImage(); 144 | // set module values after image creation because we need the GdImage instance 145 | $this->setModuleValues(); 146 | $this->setBgColor(); 147 | 148 | if(imagefilledrectangle($this->image, 0, 0, $this->length, $this->length, $this->background) === false){ 149 | throw new QRCodeOutputException('imagefilledrectangle() error'); 150 | } 151 | 152 | $this->drawImage(); 153 | 154 | if($this->upscaled){ 155 | // scale down to the expected size 156 | $scaled = imagescale($this->image, ($this->length / 10), ($this->length / 10)); 157 | 158 | if($scaled === false){ 159 | throw new QRCodeOutputException('imagescale() error'); 160 | } 161 | 162 | $this->image = $scaled; 163 | $this->upscaled = false; 164 | // Reset scaled and length values after rescaling image to prevent issues with subclasses that use the output from dump() 165 | $this->setMatrixDimensions(); 166 | } 167 | 168 | // set transparency after scaling, otherwise it would be undone 169 | // @see https://www.php.net/manual/en/function.imagecolortransparent.php#77099 170 | $this->setTransparencyColor(); 171 | 172 | if($this->options->returnResource){ 173 | return $this->image; 174 | } 175 | 176 | $imageData = $this->dumpImage(); 177 | 178 | $this->saveToFile($imageData, $file); 179 | 180 | if($this->options->outputBase64){ 181 | $imageData = $this->toBase64DataURI($imageData); 182 | } 183 | 184 | return $imageData; 185 | } 186 | 187 | /** 188 | * Creates a new GdImage resource and scales it if necessary 189 | * 190 | * we're scaling the image up in order to draw crisp round circles, otherwise they appear square-y on small scales 191 | * 192 | * @see https://github.com/chillerlan/php-qrcode/issues/23 193 | * 194 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 195 | */ 196 | protected function createImage():GdImage{ 197 | 198 | if($this->drawCircularModules && $this->options->gdImageUseUpscale && $this->options->scale < 20){ 199 | // increase the initial image size by 10 200 | $this->length *= 10; 201 | $this->scale *= 10; 202 | $this->upscaled = true; 203 | } 204 | 205 | $im = imagecreatetruecolor($this->length, $this->length); 206 | 207 | if($im === false){ 208 | throw new QRCodeOutputException('imagecreatetruecolor() error'); 209 | } 210 | 211 | return $im; 212 | } 213 | 214 | /** 215 | * Sets the background color 216 | */ 217 | protected function setBgColor():void{ 218 | 219 | if(isset($this->background)){ 220 | return; 221 | } 222 | 223 | if($this::moduleValueIsValid($this->options->bgColor)){ 224 | $this->background = $this->prepareModuleValue($this->options->bgColor); 225 | 226 | return; 227 | } 228 | 229 | $this->background = $this->prepareModuleValue([255, 255, 255]); 230 | } 231 | 232 | /** 233 | * Sets the transparency color, returns the identifier of the new transparent color 234 | */ 235 | protected function setTransparencyColor():int{ 236 | 237 | if(!$this->options->imageTransparent){ 238 | return -1; 239 | } 240 | 241 | $transparencyColor = $this->background; 242 | 243 | if($this::moduleValueIsValid($this->options->transparencyColor)){ 244 | $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor); 245 | } 246 | 247 | return imagecolortransparent($this->image, $transparencyColor); 248 | } 249 | 250 | /** 251 | * Returns the image quality value for the current GdImage output child class (defaults to -1 ... 100) 252 | */ 253 | protected function getQuality():int{ 254 | return max(-1, min(100, $this->options->quality)); 255 | } 256 | 257 | /** 258 | * Draws the QR image 259 | */ 260 | protected function drawImage():void{ 261 | foreach($this->matrix->getMatrix() as $y => $row){ 262 | foreach($row as $x => $M_TYPE){ 263 | $this->module($x, $y, $M_TYPE); 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Creates a single QR pixel with the given settings 270 | */ 271 | protected function module(int $x, int $y, int $M_TYPE):void{ 272 | 273 | if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ 274 | return; 275 | } 276 | 277 | $color = $this->getModuleValue($M_TYPE); 278 | 279 | if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ 280 | imagefilledellipse( 281 | $this->image, 282 | (($x * $this->scale) + intdiv($this->scale, 2)), 283 | (($y * $this->scale) + intdiv($this->scale, 2)), 284 | (int)($this->circleDiameter * $this->scale), 285 | (int)($this->circleDiameter * $this->scale), 286 | $color, 287 | ); 288 | 289 | return; 290 | } 291 | 292 | imagefilledrectangle( 293 | $this->image, 294 | ($x * $this->scale), 295 | ($y * $this->scale), 296 | (($x + 1) * $this->scale), 297 | (($y + 1) * $this->scale), 298 | $color, 299 | ); 300 | } 301 | 302 | /** 303 | * Renders the image with the gdimage function for the desired output 304 | * 305 | * @see https://github.com/chillerlan/php-qrcode/issues/223 306 | */ 307 | abstract protected function renderImage():void; 308 | 309 | /** 310 | * Creates the final image by calling the desired GD output function 311 | * 312 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 313 | */ 314 | protected function dumpImage():string{ 315 | ob_start(); 316 | 317 | $this->renderImage(); 318 | 319 | $imageData = ob_get_contents(); 320 | 321 | if($imageData === false){ 322 | throw new QRCodeOutputException('ob_get_contents() error'); 323 | } 324 | 325 | imagedestroy($this->image); 326 | 327 | ob_end_clean(); 328 | 329 | return $imageData; 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /src/Output/QRGdImageAVIF.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use function imageavif, max, min; 17 | 18 | /** 19 | * GDImage avif output 20 | * 21 | * @see \imageavif() 22 | */ 23 | class QRGdImageAVIF extends QRGdImage{ 24 | 25 | final public const MIME_TYPE = 'image/avif'; 26 | 27 | /** 28 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 29 | */ 30 | protected function renderImage():void{ 31 | if(imageavif(image: $this->image, quality: $this->getQuality()) === false){ 32 | throw new QRCodeOutputException('imageavif() error'); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Output/QRGdImageBMP.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use function imagebmp; 17 | 18 | /** 19 | * GdImage bmp output 20 | * 21 | * @see \imagebmp() 22 | */ 23 | class QRGdImageBMP extends QRGdImage{ 24 | 25 | final public const MIME_TYPE = 'image/bmp'; 26 | 27 | /** 28 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 29 | */ 30 | protected function renderImage():void{ 31 | // the $compressed parameter is boolean here 32 | if(imagebmp(image: $this->image, compressed: ($this->options->quality > 0)) === false){ 33 | throw new QRCodeOutputException('imagebmp() error'); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Output/QRGdImageGIF.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use function imagegif; 17 | 18 | /** 19 | * GdImage gif output 20 | * 21 | * @see \imagegif() 22 | */ 23 | class QRGdImageGIF extends QRGdImage{ 24 | 25 | final public const MIME_TYPE = 'image/gif'; 26 | 27 | /** 28 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 29 | */ 30 | protected function renderImage():void{ 31 | if(imagegif(image: $this->image) === false){ 32 | throw new QRCodeOutputException('imagegif() error'); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Output/QRGdImageJPEG.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use function imagejpeg, max, min; 17 | 18 | /** 19 | * GdImage jpeg output 20 | * 21 | * @see \imagejpeg() 22 | */ 23 | class QRGdImageJPEG extends QRGdImage{ 24 | 25 | final public const MIME_TYPE = 'image/jpg'; 26 | 27 | protected function setTransparencyColor():int{ 28 | // noop - transparency is not supported 29 | return -1; 30 | } 31 | 32 | /** 33 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 34 | */ 35 | protected function renderImage():void{ 36 | if(imagejpeg(image: $this->image, quality: $this->getQuality()) === false){ 37 | throw new QRCodeOutputException('imagejpeg() error'); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Output/QRGdImagePNG.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use function imagepng, max, min; 17 | 18 | /** 19 | * GdImage png output 20 | * 21 | * @see \imagepng() 22 | */ 23 | class QRGdImagePNG extends QRGdImage{ 24 | 25 | final public const MIME_TYPE = 'image/png'; 26 | 27 | protected function getQuality():int{ 28 | return max(-1, min(9, $this->options->quality)); 29 | } 30 | 31 | /** 32 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 33 | */ 34 | protected function renderImage():void{ 35 | if(imagepng(image: $this->image, quality: $this->getQuality()) === false){ 36 | throw new QRCodeOutputException('imagepng() error'); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/Output/QRGdImageWEBP.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use function imagewebp, max, min; 17 | 18 | /** 19 | * GdImage webp output 20 | * 21 | * @see \imagewebp() 22 | */ 23 | class QRGdImageWEBP extends QRGdImage{ 24 | 25 | final public const MIME_TYPE = 'image/webp'; 26 | 27 | /** 28 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 29 | */ 30 | protected function renderImage():void{ 31 | if(imagewebp(image: $this->image, quality: $this->getQuality()) === false){ 32 | throw new QRCodeOutputException('imagewebp() error'); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Output/QRImagick.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2018 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use chillerlan\QRCode\QROptions; 17 | use chillerlan\QRCode\Data\QRMatrix; 18 | use chillerlan\Settings\SettingsContainerInterface; 19 | use Imagick, ImagickDraw, ImagickPixel; 20 | use function extension_loaded, in_array, is_string, max, min, preg_match, sprintf, strlen; 21 | 22 | /** 23 | * ImageMagick output module (requires ext-imagick) 24 | * 25 | * @see https://php.net/manual/book.imagick.php 26 | * @see https://phpimagick.com 27 | */ 28 | class QRImagick extends QROutputAbstract{ 29 | 30 | /** 31 | * The main image instance 32 | */ 33 | protected Imagick $imagick; 34 | 35 | /** 36 | * The main draw instance 37 | */ 38 | protected ImagickDraw $imagickDraw; 39 | 40 | /** 41 | * The allocated background color 42 | */ 43 | protected ImagickPixel $backgroundColor; 44 | 45 | /** 46 | * @inheritDoc 47 | * 48 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 49 | */ 50 | public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix){ 51 | 52 | foreach(['fileinfo', 'imagick'] as $ext){ 53 | if(!extension_loaded($ext)){ 54 | throw new QRCodeOutputException(sprintf('ext-%s not loaded', $ext)); // @codeCoverageIgnore 55 | } 56 | } 57 | 58 | parent::__construct($options, $matrix); 59 | } 60 | 61 | /** 62 | * note: we're not necessarily validating the several values, just checking the general syntax 63 | * 64 | * @see https://www.php.net/manual/imagickpixel.construct.php 65 | * @inheritDoc 66 | */ 67 | public static function moduleValueIsValid(mixed $value):bool{ 68 | 69 | if(!is_string($value)){ 70 | return false; 71 | } 72 | 73 | $value = trim($value); 74 | 75 | // hex notation 76 | // #rgb(a) 77 | // #rrggbb(aa) 78 | // #rrrrggggbbbb(aaaa) 79 | // ... 80 | if(preg_match('/^#[a-f\d]+$/i', $value) && in_array((strlen($value) - 1), [3, 4, 6, 8, 9, 12, 16, 24, 32], true)){ 81 | return true; 82 | } 83 | 84 | // css (-like) func(...values) 85 | if(preg_match('#^(graya?|hs(b|la?)|rgba?)\([\d .,%]+\)$#i', $value)){ 86 | return true; 87 | } 88 | 89 | // predefined css color 90 | if(preg_match('/^[a-z]+$/i', $value)){ 91 | return true; 92 | } 93 | 94 | return false; 95 | } 96 | 97 | protected function prepareModuleValue(mixed $value):ImagickPixel{ 98 | return new ImagickPixel($value); 99 | } 100 | 101 | protected function getDefaultModuleValue(bool $isDark):ImagickPixel{ 102 | return $this->prepareModuleValue(($isDark) ? '#000' : '#fff'); 103 | } 104 | 105 | public function dump(string|null $file = null):string|Imagick{ 106 | $this->setBgColor(); 107 | 108 | $this->imagick = $this->createImage(); 109 | 110 | $this->drawImage(); 111 | // set transparency color after all operations 112 | $this->setTransparencyColor(); 113 | 114 | if($this->options->returnResource){ 115 | return $this->imagick; 116 | } 117 | 118 | $imageData = $this->imagick->getImageBlob(); 119 | 120 | $this->imagick->destroy(); 121 | 122 | $this->saveToFile($imageData, $file); 123 | 124 | if($this->options->outputBase64){ 125 | $imageData = $this->toBase64DataURI($imageData); 126 | } 127 | 128 | return $imageData; 129 | } 130 | 131 | /** 132 | * Sets the background color 133 | */ 134 | protected function setBgColor():void{ 135 | 136 | if($this::moduleValueIsValid($this->options->bgColor)){ 137 | $this->backgroundColor = $this->prepareModuleValue($this->options->bgColor); 138 | 139 | return; 140 | } 141 | 142 | $this->backgroundColor = $this->prepareModuleValue('white'); 143 | } 144 | 145 | /** 146 | * Creates a new Imagick instance 147 | */ 148 | protected function createImage():Imagick{ 149 | $imagick = new Imagick; 150 | [$width, $height] = $this->getOutputDimensions(); 151 | 152 | $imagick->newImage($width, $height, $this->backgroundColor, $this->options->imagickFormat); 153 | 154 | if($this->options->quality > -1){ 155 | $imagick->setImageCompressionQuality(max(0, min(100, $this->options->quality))); 156 | } 157 | 158 | return $imagick; 159 | } 160 | 161 | /** 162 | * Sets the transparency color 163 | */ 164 | protected function setTransparencyColor():void{ 165 | 166 | if(!$this->options->imageTransparent){ 167 | return; 168 | } 169 | 170 | $transparencyColor = $this->backgroundColor; 171 | 172 | if($this::moduleValueIsValid($this->options->transparencyColor)){ 173 | $transparencyColor = $this->prepareModuleValue($this->options->transparencyColor); 174 | } 175 | 176 | $this->imagick->transparentPaintImage($transparencyColor, 0.0, 10, false); 177 | } 178 | 179 | /** 180 | * Creates the QR image via ImagickDraw 181 | */ 182 | protected function drawImage():void{ 183 | $this->imagickDraw = new ImagickDraw; 184 | $this->imagickDraw->setStrokeWidth(0); 185 | 186 | foreach($this->matrix->getMatrix() as $y => $row){ 187 | foreach($row as $x => $M_TYPE){ 188 | $this->module($x, $y, $M_TYPE); 189 | } 190 | } 191 | 192 | $this->imagick->drawImage($this->imagickDraw); 193 | } 194 | 195 | /** 196 | * draws a single pixel at the given position 197 | */ 198 | protected function module(int $x, int $y, int $M_TYPE):void{ 199 | 200 | if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ 201 | return; 202 | } 203 | 204 | $this->imagickDraw->setFillColor($this->getModuleValue($M_TYPE)); 205 | 206 | if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ 207 | $this->imagickDraw->circle( 208 | (($x + 0.5) * $this->scale), 209 | (($y + 0.5) * $this->scale), 210 | (($x + 0.5 + $this->circleRadius) * $this->scale), 211 | (($y + 0.5) * $this->scale), 212 | ); 213 | 214 | return; 215 | } 216 | 217 | $this->imagickDraw->rectangle( 218 | ($x * $this->scale), 219 | ($y * $this->scale), 220 | ((($x + 1) * $this->scale) - 1), 221 | ((($y + 1) * $this->scale) - 1), 222 | ); 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /src/Output/QRInterventionImage.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2024 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use chillerlan\QRCode\Data\QRMatrix; 15 | use chillerlan\QRCode\QROptions; 16 | use chillerlan\Settings\SettingsContainerInterface; 17 | use Intervention\Image\Drivers\Gd\Driver as GdDriver; 18 | use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver; 19 | use Intervention\Image\Geometry\Factories\CircleFactory; 20 | use Intervention\Image\Geometry\Factories\RectangleFactory; 21 | use Intervention\Image\ImageManager; 22 | use Intervention\Image\Interfaces\DriverInterface; 23 | use Intervention\Image\Interfaces\ImageInterface; 24 | use Intervention\Image\Interfaces\ImageManagerInterface; 25 | use UnhandledMatchError; 26 | use function class_exists; 27 | use function extension_loaded; 28 | use function intdiv; 29 | 30 | /** 31 | * intervention/image (GD/ImageMagick) output 32 | * 33 | * note: this output class works very slow compared to the native GD/Imagick output classes for obvious reasons. 34 | * use only if you must. 35 | * 36 | * @see https://github.com/Intervention/image 37 | * @see https://image.intervention.io/ 38 | */ 39 | class QRInterventionImage extends QROutputAbstract{ 40 | use CssColorModuleValueTrait; 41 | 42 | protected DriverInterface $driver; 43 | protected ImageManagerInterface $manager; 44 | protected ImageInterface $image; 45 | 46 | /** 47 | * QRInterventionImage constructor. 48 | * 49 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 50 | */ 51 | public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix){ 52 | 53 | if(!class_exists(ImageManager::class)){ 54 | // @codeCoverageIgnoreStart 55 | throw new QRCodeOutputException( 56 | 'The QRInterventionImage output requires Intervention/image (https://github.com/Intervention/image)'. 57 | ' as dependency but the class "\\Intervention\\Image\\ImageManager" could not be found.', 58 | ); 59 | // @codeCoverageIgnoreEnd 60 | } 61 | 62 | try{ 63 | $this->driver = match(true){ 64 | extension_loaded('gd') => new GdDriver, 65 | extension_loaded('imagick') => new ImagickDriver, 66 | }; 67 | 68 | $this->setDriver($this->driver); 69 | } 70 | catch(UnhandledMatchError){ 71 | throw new QRCodeOutputException('no image processing extension loaded (gd, imagick)'); // @codeCoverageIgnore 72 | } 73 | 74 | parent::__construct($options, $matrix); 75 | } 76 | 77 | /** 78 | * Sets a DriverInterface 79 | */ 80 | public function setDriver(DriverInterface $driver):static{ 81 | $this->manager = new ImageManager($driver); 82 | 83 | return $this; 84 | } 85 | 86 | public function dump(string|null $file = null):string|ImageInterface{ 87 | [$width, $height] = $this->getOutputDimensions(); 88 | 89 | $this->image = $this->manager->create($width, $height); 90 | 91 | $this->image->fill($this->getDefaultModuleValue(false)); 92 | 93 | if($this->options->imageTransparent && $this::moduleValueIsValid($this->options->transparencyColor)){ 94 | $this->manager 95 | ->driver() 96 | ->config() 97 | ->setOptions(blendingColor: $this->prepareModuleValue($this->options->transparencyColor)) 98 | ; 99 | } 100 | 101 | if($this::moduleValueIsValid($this->options->bgColor)){ 102 | $this->image->fill($this->prepareModuleValue($this->options->bgColor)); 103 | } 104 | 105 | foreach($this->matrix->getMatrix() as $y => $row){ 106 | foreach($row as $x => $M_TYPE){ 107 | $this->module($x, $y, $M_TYPE); 108 | } 109 | } 110 | 111 | if($this->options->returnResource){ 112 | return $this->image; 113 | } 114 | 115 | $image = $this->image->toPng(); 116 | $imageData = $image->toString(); 117 | 118 | $this->saveToFile($imageData, $file); 119 | 120 | if($this->options->outputBase64){ 121 | return $image->toDataUri(); 122 | } 123 | 124 | return $imageData; 125 | } 126 | 127 | /** 128 | * draws a single pixel at the given position 129 | */ 130 | protected function module(int $x, int $y, int $M_TYPE):void{ 131 | 132 | if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ 133 | return; 134 | } 135 | 136 | $color = $this->getModuleValue($M_TYPE); 137 | 138 | if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ 139 | 140 | $this->image->drawCircle( 141 | (($x * $this->scale) + intdiv($this->scale, 2)), 142 | (($y * $this->scale) + intdiv($this->scale, 2)), 143 | function(CircleFactory $circle) use ($color):void{ 144 | $circle->radius((int)($this->circleRadius * $this->scale)); 145 | $circle->background($color); 146 | }, 147 | ); 148 | 149 | return; 150 | } 151 | 152 | $this->image->drawRectangle( 153 | ($x * $this->scale), 154 | ($y * $this->scale), 155 | function(RectangleFactory $rectangle) use ($color):void{ 156 | $rectangle->size($this->scale, $this->scale); 157 | $rectangle->background($color); 158 | }, 159 | ); 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/Output/QRMarkup.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | /** 15 | * Abstract for markup types: HTML, SVG, ... XML anyone? 16 | */ 17 | abstract class QRMarkup extends QROutputAbstract{ 18 | use CssColorModuleValueTrait; 19 | 20 | public function dump(string|null $file = null):string{ 21 | $saveToFile = $file !== null; 22 | $data = $this->createMarkup($saveToFile); 23 | 24 | $this->saveToFile($data, $file); 25 | 26 | // transform to data URI only when not saving to file 27 | if(!$saveToFile && $this->options->outputBase64){ 28 | return $this->toBase64DataURI($data); 29 | } 30 | 31 | return $data; 32 | } 33 | 34 | /** 35 | * returns a string with all css classes for the current element 36 | */ 37 | protected function getCssClass(int $M_TYPE = 0):string{ 38 | return $this->options->cssClass; 39 | } 40 | 41 | /** 42 | * returns the fully parsed and rendered markup string for the given input 43 | */ 44 | abstract protected function createMarkup(bool $saveToFile):string; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Output/QRMarkupHTML.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2022 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use function implode, sprintf; 15 | 16 | /** 17 | * HTML output (a cheap markup substitute when SVG is not available or not an option) 18 | */ 19 | class QRMarkupHTML extends QRMarkup{ 20 | 21 | final public const MIME_TYPE = 'text/html'; 22 | 23 | protected function createMarkup(bool $saveToFile):string{ 24 | $rows = []; 25 | $cssClass = $this->getCssClass(); 26 | 27 | foreach($this->matrix->getMatrix() as $row){ 28 | $element = ''; 29 | $modules = array_map(fn(int $M_TYPE):string => sprintf($element, $this->getModuleValue($M_TYPE)), $row); 30 | 31 | $rows[] = sprintf('
%s
%s', implode('', $modules), $this->eol); 32 | } 33 | 34 | $html = sprintf('
%3$s%2$s
%3$s', $cssClass, implode('', $rows), $this->eol); 35 | 36 | // wrap the snippet into a body when saving to file 37 | if($saveToFile){ 38 | $html = sprintf( 39 | '%2$s%2$s%2$s'. 40 | 'QR Code%2$s%1$s%2$s', 41 | $html, 42 | $this->eol, 43 | ); 44 | } 45 | 46 | return $html; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Output/QRMarkupSVG.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2022 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use function array_chunk, implode, is_string, preg_match, sprintf, trim; 15 | 16 | /** 17 | * SVG output 18 | * 19 | * @see https://github.com/codemasher/php-qrcode/pull/5 20 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG 21 | * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/ 22 | * @see https://lea.verou.me/blog/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/ 23 | * @see https://codepen.io/leaverou/full/RmwzKv 24 | * @see https://jakearchibald.github.io/svgomg/ 25 | * @see https://web.archive.org/web/20200220211445/http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html 26 | */ 27 | class QRMarkupSVG extends QRMarkup{ 28 | 29 | final public const MIME_TYPE = 'image/svg+xml'; 30 | 31 | /** 32 | * @todo: XSS proof 33 | * 34 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill 35 | * @inheritDoc 36 | */ 37 | public static function moduleValueIsValid(mixed $value):bool{ 38 | 39 | if(!is_string($value)){ 40 | return false; 41 | } 42 | 43 | $value = trim($value); 44 | 45 | // url(...) 46 | if(preg_match('~^url\([-/#a-z\d]+\)$~i', $value)){ 47 | return true; 48 | } 49 | 50 | // otherwise check for standard css notation 51 | return parent::moduleValueIsValid($value); 52 | } 53 | 54 | protected function getOutputDimensions():array{ 55 | return [$this->moduleCount, $this->moduleCount]; 56 | } 57 | 58 | protected function getCssClass(int $M_TYPE = 0):string{ 59 | return implode(' ', [ 60 | 'qr-'.($this::LAYERNAMES[$M_TYPE] ?? $M_TYPE), 61 | $this->matrix->isDark($M_TYPE) ? 'dark' : 'light', 62 | $this->options->cssClass, 63 | ]); 64 | } 65 | 66 | protected function createMarkup(bool $saveToFile):string{ 67 | $svg = $this->header(); 68 | 69 | if(!empty($this->options->svgDefs)){ 70 | $svg .= sprintf('%1$s%2$s%2$s', $this->options->svgDefs, $this->eol); 71 | } 72 | 73 | $svg .= $this->paths(); 74 | 75 | // close svg 76 | $svg .= sprintf('%1$s%1$s', $this->eol); 77 | 78 | return $svg; 79 | } 80 | 81 | /** 82 | * returns the value for the SVG viewBox attribute 83 | * 84 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox 85 | * @see https://css-tricks.com/scale-svg/#article-header-id-3 86 | */ 87 | protected function getViewBox():string{ 88 | [$width, $height] = $this->getOutputDimensions(); 89 | 90 | return sprintf('0 0 %s %s', $width, $height); 91 | } 92 | 93 | /** 94 | * returns the header with the given options parsed 95 | * 96 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg 97 | */ 98 | protected function header():string{ 99 | 100 | $header = sprintf( 101 | '%4$s', 102 | $this->options->cssClass, 103 | $this->getViewBox(), 104 | $this->options->svgPreserveAspectRatio, 105 | $this->eol, 106 | ); 107 | 108 | if($this->options->svgAddXmlHeader){ 109 | $header = sprintf('%s%s', $this->eol, $header); 110 | } 111 | 112 | return $header; 113 | } 114 | 115 | /** 116 | * returns one or more SVG elements 117 | */ 118 | protected function paths():string{ 119 | $paths = $this->collectModules($this->module(...)); 120 | $svg = []; 121 | 122 | // create the path elements 123 | foreach($paths as $M_TYPE => $modules){ 124 | // limit the total line length 125 | $chunks = array_chunk($modules, 100); 126 | $chonks = []; 127 | 128 | foreach($chunks as $chunk){ 129 | $chonks[] = implode(' ', $chunk); 130 | } 131 | 132 | $path = implode($this->eol, $chonks); 133 | 134 | if(empty($path)){ 135 | continue; 136 | } 137 | 138 | $svg[] = $this->path($path, $M_TYPE); 139 | } 140 | 141 | return implode($this->eol, $svg); 142 | } 143 | 144 | /** 145 | * renders and returns a single element 146 | * 147 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path 148 | */ 149 | protected function path(string $path, int $M_TYPE):string{ 150 | 151 | if($this->options->svgUseFillAttributes){ 152 | return sprintf( 153 | '', 154 | $this->getCssClass($M_TYPE), 155 | $this->getModuleValue($M_TYPE), 156 | $path, 157 | ); 158 | } 159 | 160 | return sprintf('', $this->getCssClass($M_TYPE), $path); 161 | } 162 | 163 | /** 164 | * returns a path segment for a single module 165 | * 166 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d 167 | */ 168 | protected function module(int $x, int $y, int $M_TYPE):string{ 169 | 170 | if(!$this->drawLightModules && !$this->matrix->isDark($M_TYPE)){ 171 | return ''; 172 | } 173 | 174 | if($this->drawCircularModules && !$this->matrix->checkTypeIn($x, $y, $this->keepAsSquare)){ 175 | // string interpolation: ugly and fast 176 | $ix = ($x + 0.5 - $this->circleRadius); 177 | $iy = ($y + 0.5); 178 | 179 | // phpcs:ignore 180 | return "M$ix $iy a$this->circleRadius $this->circleRadius 0 1 0 $this->circleDiameter 0 a$this->circleRadius $this->circleRadius 0 1 0 -$this->circleDiameter 0Z"; 181 | } 182 | 183 | // phpcs:ignore 184 | return "M$x $y h1 v1 h-1Z"; 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/Output/QRMarkupXML.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2024 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use DOMDocument; 17 | use DOMElement; 18 | use function sprintf; 19 | 20 | /** 21 | * XML/XSLT output 22 | */ 23 | class QRMarkupXML extends QRMarkup{ 24 | 25 | final public const MIME_TYPE = 'application/xml'; 26 | final public const SCHEMA = 'https://raw.githubusercontent.com/chillerlan/php-qrcode/main/src/Output/qrcode.schema.xsd'; 27 | 28 | protected DOMDocument $dom; 29 | 30 | /** 31 | * @inheritDoc 32 | * @return int[] 33 | */ 34 | protected function getOutputDimensions():array{ 35 | return [$this->moduleCount, $this->moduleCount]; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 41 | */ 42 | protected function createMarkup(bool $saveToFile):string{ 43 | /** @noinspection PhpComposerExtensionStubsInspection */ 44 | $this->dom = new DOMDocument(encoding: 'UTF-8'); 45 | $this->dom->formatOutput = true; 46 | 47 | if($this->options->xmlStylesheet !== null){ 48 | $stylesheet = sprintf('type="text/xsl" href="%s"', $this->options->xmlStylesheet); 49 | $xslt = $this->dom->createProcessingInstruction('xml-stylesheet', $stylesheet); 50 | 51 | $this->dom->appendChild($xslt); 52 | } 53 | 54 | $root = $this->dom->createElement('qrcode'); 55 | 56 | $root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); 57 | $root->setAttribute('xsi:noNamespaceSchemaLocation', $this::SCHEMA); 58 | $root->setAttribute('version', (string)$this->matrix->getVersion()); 59 | $root->setAttribute('eccLevel', (string)$this->matrix->getEccLevel()); 60 | $root->appendChild($this->createMatrix()); 61 | 62 | $this->dom->appendChild($root); 63 | 64 | $xml = $this->dom->saveXML(); 65 | 66 | if($xml === false){ 67 | throw new QRCodeOutputException('XML error'); 68 | } 69 | 70 | return $xml; 71 | } 72 | 73 | /** 74 | * Creates the matrix element 75 | */ 76 | protected function createMatrix():DOMElement{ 77 | [$width, $height] = $this->getOutputDimensions(); 78 | $matrix = $this->dom->createElement('matrix'); 79 | $dimension = $this->matrix->getVersion()->getDimension(); 80 | 81 | $matrix->setAttribute('size', (string)$dimension); 82 | $matrix->setAttribute('quietzoneSize', (string)(int)(($this->moduleCount - $dimension) / 2)); 83 | $matrix->setAttribute('maskPattern', (string)$this->matrix->getMaskPattern()->getPattern()); 84 | $matrix->setAttribute('width', (string)$width); 85 | $matrix->setAttribute('height', (string)$height); 86 | 87 | foreach($this->matrix->getMatrix() as $y => $row){ 88 | $matrixRow = $this->row($y, $row); 89 | 90 | if($matrixRow !== null){ 91 | $matrix->appendChild($matrixRow); 92 | } 93 | } 94 | 95 | return $matrix; 96 | } 97 | 98 | /** 99 | * Creates a DOM element for a matrix row 100 | * 101 | * @param int[] $row 102 | */ 103 | protected function row(int $y, array $row):DOMElement|null{ 104 | $matrixRow = $this->dom->createElement('row'); 105 | 106 | $matrixRow->setAttribute('y', (string)$y); 107 | 108 | foreach($row as $x => $M_TYPE){ 109 | $module = $this->module($x, $y, $M_TYPE); 110 | 111 | if($module !== null){ 112 | $matrixRow->appendChild($module); 113 | } 114 | 115 | } 116 | 117 | if($matrixRow->childElementCount > 0){ 118 | return $matrixRow; 119 | } 120 | 121 | // skip empty rows 122 | return null; 123 | } 124 | 125 | /** 126 | * Creates a DOM element for a single module 127 | */ 128 | protected function module(int $x, int $y, int $M_TYPE):DOMElement|null{ 129 | $isDark = $this->matrix->isDark($M_TYPE); 130 | 131 | if(!$this->drawLightModules && !$isDark){ 132 | return null; 133 | } 134 | 135 | $module = $this->dom->createElement('module'); 136 | 137 | $module->setAttribute('x', (string)$x); 138 | $module->setAttribute('dark', (($isDark) ? 'true' : 'false')); 139 | $module->setAttribute('layer', ($this::LAYERNAMES[$M_TYPE] ?? '')); 140 | $module->setAttribute('value', (string)$this->getModuleValue($M_TYPE)); 141 | 142 | return $module; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/Output/QROutputAbstract.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use chillerlan\QRCode\QROptions; 17 | use chillerlan\QRCode\Data\QRMatrix; 18 | use chillerlan\Settings\SettingsContainerInterface; 19 | use Closure, finfo; 20 | use function base64_encode, dirname, extension_loaded, file_put_contents, is_writable, ksort, sprintf; 21 | use const FILEINFO_MIME_TYPE; 22 | 23 | /** 24 | * common output abstract 25 | */ 26 | abstract class QROutputAbstract implements QROutputInterface{ 27 | 28 | /** 29 | * the current size of the QR matrix 30 | * 31 | * @see \chillerlan\QRCode\Data\QRMatrix::getSize() 32 | */ 33 | protected int $moduleCount; 34 | 35 | /** 36 | * the side length of the QR image (modules * scale) 37 | */ 38 | protected int $length; 39 | 40 | /** 41 | * an (optional) array of color values for the several QR matrix parts 42 | * 43 | * @phpstan-var array 44 | */ 45 | protected array $moduleValues; 46 | 47 | /** 48 | * the (filled) data matrix object 49 | */ 50 | protected QRMatrix $matrix; 51 | 52 | /** 53 | * the options instance 54 | */ 55 | protected SettingsContainerInterface|QROptions $options; 56 | 57 | /** 58 | * @see \chillerlan\QRCode\QROptions::$excludeFromConnect 59 | * @var int[] 60 | */ 61 | protected array $excludeFromConnect; 62 | /** 63 | * @see \chillerlan\QRCode\QROptions::$keepAsSquare 64 | * @var int[] 65 | */ 66 | protected array $keepAsSquare; 67 | /** @see \chillerlan\QRCode\QROptions::$scale */ 68 | protected int $scale; 69 | /** @see \chillerlan\QRCode\QROptions::$connectPaths */ 70 | protected bool $connectPaths; 71 | /** @see \chillerlan\QRCode\QROptions::$eol */ 72 | protected string $eol; 73 | /** @see \chillerlan\QRCode\QROptions::$drawLightModules */ 74 | protected bool $drawLightModules; 75 | /** @see \chillerlan\QRCode\QROptions::$drawCircularModules */ 76 | protected bool $drawCircularModules; 77 | /** @see \chillerlan\QRCode\QROptions::$circleRadius */ 78 | protected float $circleRadius; 79 | protected float $circleDiameter; 80 | 81 | /** 82 | * QROutputAbstract constructor. 83 | */ 84 | public function __construct(SettingsContainerInterface|QROptions $options, QRMatrix $matrix){ 85 | $this->options = $options; 86 | $this->matrix = $matrix; 87 | 88 | if($this->options->invertMatrix){ 89 | $this->matrix->invert(); 90 | } 91 | 92 | $this->copyVars(); 93 | $this->setMatrixDimensions(); 94 | $this->setModuleValues(); 95 | } 96 | 97 | /** 98 | * Creates copies of several QROptions values to avoid calling the magic getters 99 | * in long loops for a significant performance increase. 100 | * 101 | * These variables are usually used in the "module" methods and are called up to 31329 times (at version 40). 102 | */ 103 | protected function copyVars():void{ 104 | 105 | $vars = [ 106 | 'connectPaths', 107 | 'excludeFromConnect', 108 | 'eol', 109 | 'drawLightModules', 110 | 'drawCircularModules', 111 | 'keepAsSquare', 112 | 'circleRadius', 113 | ]; 114 | 115 | foreach($vars as $property){ 116 | $this->{$property} = $this->options->{$property}; 117 | } 118 | 119 | $this->circleDiameter = ($this->circleRadius * 2); 120 | } 121 | 122 | /** 123 | * Sets/updates the matrix dimensions 124 | * 125 | * Call this method if you modify the matrix from within your custom module in case the dimensions have been changed 126 | */ 127 | protected function setMatrixDimensions():void{ 128 | $this->moduleCount = $this->matrix->getSize(); 129 | $this->scale = $this->options->scale; 130 | $this->length = ($this->moduleCount * $this->scale); 131 | } 132 | 133 | /** 134 | * Returns a 2 element array with the current output width and height 135 | * 136 | * The type and units of the values depend on the output class. The default value is the current module count * scale. 137 | * 138 | * @return int[] 139 | */ 140 | protected function getOutputDimensions():array{ 141 | return [$this->length, $this->length]; 142 | } 143 | 144 | /** 145 | * Sets the initial module values 146 | */ 147 | protected function setModuleValues():void{ 148 | 149 | // first fill the map with the default values 150 | foreach($this::DEFAULT_MODULE_VALUES as $M_TYPE => $defaultValue){ 151 | $this->moduleValues[$M_TYPE] = $this->getDefaultModuleValue($defaultValue); 152 | } 153 | 154 | // now loop over the options values to replace defaults and add extra values 155 | /** @var int $M_TYPE */ 156 | foreach($this->options->moduleValues as $M_TYPE => $value){ 157 | if($this::moduleValueIsValid($value)){ 158 | $this->moduleValues[$M_TYPE] = $this->prepareModuleValue($value); 159 | } 160 | } 161 | 162 | } 163 | 164 | /** 165 | * Prepares the value for the given input (return value depends on the output class) 166 | */ 167 | abstract protected function prepareModuleValue(mixed $value):mixed; 168 | 169 | /** 170 | * Returns a default value for either dark or light modules (return value depends on the output class) 171 | */ 172 | abstract protected function getDefaultModuleValue(bool $isDark):mixed; 173 | 174 | /** 175 | * Returns the prepared value for the given $M_TYPE 176 | * 177 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException if $moduleValues[$M_TYPE] doesn't exist 178 | */ 179 | protected function getModuleValue(int $M_TYPE):mixed{ 180 | 181 | if(!isset($this->moduleValues[$M_TYPE])){ 182 | throw new QRCodeOutputException(sprintf('$M_TYPE %012b not found in module values map', $M_TYPE)); 183 | } 184 | 185 | return $this->moduleValues[$M_TYPE]; 186 | } 187 | 188 | /** 189 | * Returns the prepared module value at the given coordinate [$x, $y] (convenience) 190 | */ 191 | protected function getModuleValueAt(int $x, int $y):mixed{ 192 | return $this->getModuleValue($this->matrix->get($x, $y)); 193 | } 194 | 195 | /** 196 | * Returns a base64 data URI for the given string and mime type 197 | * 198 | * The mime type can be set via class constant MIME_TYPE in child classes, 199 | * or given via $mime, otherwise it is guessed from the image $data. 200 | */ 201 | protected function toBase64DataURI(string $data, string|null $mime = null):string{ 202 | $mime ??= static::MIME_TYPE; 203 | 204 | if($mime === ''){ 205 | $mime = $this->guessMimeType($data); 206 | } 207 | 208 | return sprintf('data:%s;base64,%s', $mime, base64_encode($data)); 209 | } 210 | 211 | /** 212 | * Guesses the mime type from the given $imageData 213 | * 214 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 215 | */ 216 | protected function guessMimeType(string $imageData):string{ 217 | 218 | if(!extension_loaded('fileinfo')){ 219 | throw new QRCodeOutputException('ext-fileinfo not loaded, cannot guess mime type'); 220 | } 221 | 222 | $mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($imageData); 223 | 224 | if($mime === false){ 225 | throw new QRCodeOutputException('unable to detect mime type'); 226 | } 227 | 228 | return $mime; 229 | } 230 | 231 | /** 232 | * Saves the qr $data to a $file. If $file is null, nothing happens. 233 | * 234 | * @see file_put_contents() 235 | * @see \chillerlan\QRCode\QROptions::$cachefile 236 | * 237 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 238 | */ 239 | protected function saveToFile(string $data, string|null $file = null):void{ 240 | 241 | if($file === null){ 242 | return; 243 | } 244 | 245 | if(!is_writable(dirname($file))){ 246 | throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s', $file)); 247 | } 248 | 249 | if(file_put_contents($file, $data) === false){ 250 | throw new QRCodeOutputException(sprintf('Cannot write data to cache file: %s (file_put_contents error)', $file)); 251 | } 252 | } 253 | 254 | /** 255 | * collects the modules per QRMatrix::M_* type and runs a $transform function on each module and 256 | * returns an array with the transformed modules 257 | * 258 | * The transform callback is called with the following parameters: 259 | * 260 | * $x - current column 261 | * $y - current row 262 | * $M_TYPE - field value 263 | * $M_TYPE_LAYER - (possibly modified) field value that acts as layer id 264 | * 265 | * @return array 266 | */ 267 | protected function collectModules(Closure $transform):array{ 268 | $paths = []; 269 | 270 | // collect the modules for each type 271 | foreach($this->matrix->getMatrix() as $y => $row){ 272 | foreach($row as $x => $M_TYPE){ 273 | $M_TYPE_LAYER = $M_TYPE; 274 | 275 | if($this->connectPaths && !$this->matrix->checkTypeIn($x, $y, $this->excludeFromConnect)){ 276 | // to connect paths we'll redeclare the $M_TYPE_LAYER to data only 277 | $M_TYPE_LAYER = QRMatrix::M_DATA; 278 | 279 | if($this->matrix->isDark($M_TYPE)){ 280 | $M_TYPE_LAYER = QRMatrix::M_DATA_DARK; 281 | } 282 | } 283 | 284 | // collect the modules per $M_TYPE 285 | $module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER); 286 | 287 | if(!empty($module)){ 288 | $paths[$M_TYPE_LAYER][] = $module; 289 | } 290 | } 291 | } 292 | 293 | // beautify output 294 | ksort($paths); 295 | 296 | return $paths; 297 | } 298 | 299 | } 300 | -------------------------------------------------------------------------------- /src/Output/QROutputInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use chillerlan\QRCode\Data\QRMatrix; 15 | 16 | /** 17 | * Converts the data matrix into readable output 18 | */ 19 | interface QROutputInterface{ 20 | 21 | /** 22 | * Map of built-in output class FQN 23 | * 24 | * @var string[] 25 | * @see https://github.com/chillerlan/php-qrcode/issues/223 26 | */ 27 | public const MODES = [ 28 | QREps::class, 29 | QRFpdf::class, 30 | QRGdImageAVIF::class, 31 | QRGdImageBMP::class, 32 | QRGdImageGIF::class, 33 | QRGdImageJPEG::class, 34 | QRGdImagePNG::class, 35 | QRGdImageWEBP::class, 36 | QRImagick::class, 37 | QRInterventionImage::class, 38 | QRMarkupHTML::class, 39 | QRMarkupSVG::class, 40 | QRMarkupXML::class, 41 | QRStringJSON::class, 42 | QRStringText::class, 43 | ]; 44 | 45 | /** 46 | * Map of module type => default value 47 | * 48 | * @var bool[] 49 | */ 50 | public const DEFAULT_MODULE_VALUES = [ 51 | // light 52 | QRMatrix::M_NULL => false, 53 | QRMatrix::M_DARKMODULE_LIGHT => false, 54 | QRMatrix::M_DATA => false, 55 | QRMatrix::M_FINDER => false, 56 | QRMatrix::M_SEPARATOR => false, 57 | QRMatrix::M_ALIGNMENT => false, 58 | QRMatrix::M_TIMING => false, 59 | QRMatrix::M_FORMAT => false, 60 | QRMatrix::M_VERSION => false, 61 | QRMatrix::M_QUIETZONE => false, 62 | QRMatrix::M_LOGO => false, 63 | QRMatrix::M_FINDER_DOT_LIGHT => false, 64 | // dark 65 | QRMatrix::M_DARKMODULE => true, 66 | QRMatrix::M_DATA_DARK => true, 67 | QRMatrix::M_FINDER_DARK => true, 68 | QRMatrix::M_SEPARATOR_DARK => true, 69 | QRMatrix::M_ALIGNMENT_DARK => true, 70 | QRMatrix::M_TIMING_DARK => true, 71 | QRMatrix::M_FORMAT_DARK => true, 72 | QRMatrix::M_VERSION_DARK => true, 73 | QRMatrix::M_QUIETZONE_DARK => true, 74 | QRMatrix::M_LOGO_DARK => true, 75 | QRMatrix::M_FINDER_DOT => true, 76 | ]; 77 | 78 | /** 79 | * Map of module type => readable name (for CSS etc.) 80 | * 81 | * @var string[] 82 | */ 83 | public const LAYERNAMES = [ 84 | // light 85 | QRMatrix::M_NULL => 'null', 86 | QRMatrix::M_DARKMODULE_LIGHT => 'darkmodule-light', 87 | QRMatrix::M_DATA => 'data', 88 | QRMatrix::M_FINDER => 'finder', 89 | QRMatrix::M_SEPARATOR => 'separator', 90 | QRMatrix::M_ALIGNMENT => 'alignment', 91 | QRMatrix::M_TIMING => 'timing', 92 | QRMatrix::M_FORMAT => 'format', 93 | QRMatrix::M_VERSION => 'version', 94 | QRMatrix::M_QUIETZONE => 'quietzone', 95 | QRMatrix::M_LOGO => 'logo', 96 | QRMatrix::M_FINDER_DOT_LIGHT => 'finder-dot-light', 97 | // dark 98 | QRMatrix::M_DARKMODULE => 'darkmodule', 99 | QRMatrix::M_DATA_DARK => 'data-dark', 100 | QRMatrix::M_FINDER_DARK => 'finder-dark', 101 | QRMatrix::M_SEPARATOR_DARK => 'separator-dark', 102 | QRMatrix::M_ALIGNMENT_DARK => 'alignment-dark', 103 | QRMatrix::M_TIMING_DARK => 'timing-dark', 104 | QRMatrix::M_FORMAT_DARK => 'format-dark', 105 | QRMatrix::M_VERSION_DARK => 'version-dark', 106 | QRMatrix::M_QUIETZONE_DARK => 'quietzone-dark', 107 | QRMatrix::M_LOGO_DARK => 'logo-dark', 108 | QRMatrix::M_FINDER_DOT => 'finder-dot', 109 | ]; 110 | 111 | /** 112 | * Note: do not call this constant from the interface, but rather from one of the child classes 113 | * 114 | * @var string 115 | * @see \chillerlan\QRCode\Output\QROutputAbstract::toBase64DataURI() 116 | */ 117 | public const MIME_TYPE = ''; 118 | 119 | /** 120 | * Determines whether the given value is valid 121 | */ 122 | public static function moduleValueIsValid(mixed $value):bool; 123 | 124 | /** 125 | * Generates the output, optionally dumps it to a file, and returns it 126 | * 127 | * please note that the value of QROptions::$cachefile is already evaluated at this point. 128 | * if the output module is invoked manually, it has no effect at all. 129 | * you need to supply the $file parameter here in that case (or handle the option value in your custom output module). 130 | * 131 | * @see \chillerlan\QRCode\QRCode::renderMatrix() 132 | */ 133 | public function dump(string|null $file = null):mixed; 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/Output/QRStringJSON.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | * 10 | * @noinspection PhpComposerExtensionStubsInspection 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace chillerlan\QRCode\Output; 15 | 16 | use JsonException; 17 | use function json_encode; 18 | 19 | /** 20 | * JSON Output 21 | * 22 | * @method string getModuleValue(int $M_TYPE) 23 | * 24 | * @phpstan-type Module array{x: int, dark: bool, layer: string, value: string} 25 | */ 26 | class QRStringJSON extends QROutputAbstract{ 27 | use CssColorModuleValueTrait; 28 | 29 | final public const MIME_TYPE = 'application/json'; 30 | final public const SCHEMA = 'https://raw.githubusercontent.com/chillerlan/php-qrcode/main/src/Output/qrcode.schema.json'; 31 | 32 | /** 33 | * @inheritDoc 34 | * 35 | * @return int[] 36 | */ 37 | protected function getOutputDimensions():array{ 38 | return [$this->moduleCount, $this->moduleCount]; 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | * @throws \JsonException 44 | */ 45 | public function dump(string|null $file = null):string{ 46 | [$width, $height] = $this->getOutputDimensions(); 47 | $version = $this->matrix->getVersion(); 48 | $dimension = $version->getDimension(); 49 | 50 | $json = [ 51 | '$schema' => $this::SCHEMA, 52 | 'qrcode' => [ 53 | 'version' => $version->getVersionNumber(), 54 | 'eccLevel' => (string)$this->matrix->getEccLevel(), 55 | 'matrix' => [ 56 | 'size' => $dimension, 57 | 'quietzoneSize' => (int)(($this->moduleCount - $dimension) / 2), 58 | 'maskPattern' => $this->matrix->getMaskPattern()->getPattern(), 59 | 'width' => $width, 60 | 'height' => $height, 61 | 'rows' => [], 62 | ], 63 | ], 64 | ]; 65 | 66 | foreach($this->matrix->getMatrix() as $y => $row){ 67 | $matrixRow = $this->row($y, $row); 68 | 69 | if($matrixRow !== null){ 70 | $json['qrcode']['matrix']['rows'][] = $matrixRow; 71 | } 72 | } 73 | 74 | $data = json_encode($json, $this->options->jsonFlags); 75 | 76 | if($data === false){ 77 | throw new JsonException('error while encoding JSON'); 78 | } 79 | 80 | $this->saveToFile($data, $file); 81 | 82 | return $data; 83 | } 84 | 85 | /** 86 | * Creates an array element for a matrix row 87 | * 88 | * @param int[] $row 89 | * @phpstan-return array{y: int, modules: array} 90 | */ 91 | protected function row(int $y, array $row):array|null{ 92 | $matrixRow = ['y' => $y, 'modules' => []]; 93 | 94 | foreach($row as $x => $M_TYPE){ 95 | $module = $this->module($x, $y, $M_TYPE); 96 | 97 | if($module !== null){ 98 | $matrixRow['modules'][] = $module; 99 | } 100 | } 101 | 102 | if(!empty($matrixRow['modules'])){ 103 | return $matrixRow; 104 | } 105 | 106 | // skip empty rows 107 | return null; 108 | } 109 | 110 | /** 111 | * Creates an array element for a single module 112 | * 113 | * @phpstan-return Module 114 | */ 115 | protected function module(int $x, int $y, int $M_TYPE):array|null{ 116 | $isDark = $this->matrix->isDark($M_TYPE); 117 | 118 | if(!$this->drawLightModules && !$isDark){ 119 | return null; 120 | } 121 | 122 | return [ 123 | 'x' => $x, 124 | 'dark' => $isDark, 125 | 'layer' => ($this::LAYERNAMES[$M_TYPE] ?? ''), 126 | 'value' => $this->getModuleValue($M_TYPE), 127 | ]; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/Output/QRStringText.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2023 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use function array_map, implode, is_string, max, min, sprintf; 15 | 16 | /** 17 | * String/plaintext output (for CLI etc.) 18 | */ 19 | class QRStringText extends QROutputAbstract{ 20 | 21 | final public const MIME_TYPE = 'text/plain'; 22 | 23 | /** 24 | * @inheritDoc 25 | * 26 | * @param string $value 27 | */ 28 | public static function moduleValueIsValid(mixed $value):bool{ 29 | return is_string($value); 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | * 35 | * @param string $value 36 | */ 37 | protected function prepareModuleValue(mixed $value):string{ 38 | return $value; 39 | } 40 | 41 | protected function getDefaultModuleValue(bool $isDark):string{ 42 | return ($isDark) ? '██' : '░░'; 43 | } 44 | 45 | public function dump(string|null $file = null):string{ 46 | $lines = []; 47 | $linestart = $this->options->textLineStart; 48 | 49 | foreach($this->matrix->getMatrix() as $row){ 50 | $lines[] = $linestart.implode('', array_map($this->getModuleValue(...), $row)); 51 | } 52 | 53 | $data = implode($this->eol, $lines); 54 | 55 | $this->saveToFile($data, $file); 56 | 57 | return $data; 58 | } 59 | 60 | /** 61 | * a little helper to create a proper ANSI 8-bit color escape sequence 62 | * 63 | * @see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit 64 | * @see https://en.wikipedia.org/wiki/Block_Elements 65 | * 66 | * @codeCoverageIgnore 67 | */ 68 | public static function ansi8(string $str, int $color, bool|null $background = null):string{ 69 | $color = max(0, min($color, 255)); 70 | $background = ($background === true) ? 48 : 38; 71 | 72 | return sprintf("\x1b[%s;5;%sm%s\x1b[0m", $background, $color, $str); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Output/RGBArrayModuleValueTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2024 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode\Output; 13 | 14 | use function array_values, count, intval, is_array, is_numeric, max, min; 15 | 16 | /** 17 | * Module value checks for output classes that use RGB color arrays 18 | */ 19 | trait RGBArrayModuleValueTrait{ 20 | 21 | /** 22 | * implements \chillerlan\QRCode\Output\QROutputInterface::moduleValueIsValid() 23 | * 24 | * @param int[] $value 25 | */ 26 | public static function moduleValueIsValid(mixed $value):bool{ 27 | 28 | if(!is_array($value) || count($value) < 3){ 29 | return false; 30 | } 31 | 32 | // check the first 3 values of the array 33 | foreach(array_values($value) as $i => $val){ 34 | 35 | if($i > 2){ 36 | break; 37 | } 38 | 39 | if(!is_numeric($val)){ 40 | return false; 41 | } 42 | 43 | } 44 | 45 | return true; 46 | } 47 | 48 | /** 49 | * implements \chillerlan\QRCode\Output\QROutputAbstract::prepareModuleValue() 50 | * 51 | * @param int[] $value 52 | * @return int[] 53 | * 54 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 55 | */ 56 | protected function prepareModuleValue(mixed $value):array{ 57 | $values = []; 58 | 59 | foreach(array_values($value) as $i => $val){ 60 | 61 | if($i > 2){ 62 | break; 63 | } 64 | 65 | $values[] = max(0, min(255, intval($val))); 66 | } 67 | 68 | if(count($values) !== 3){ 69 | throw new QRCodeOutputException('invalid color value'); 70 | } 71 | 72 | return $values; 73 | } 74 | 75 | /** 76 | * implements \chillerlan\QRCode\Output\QROutputAbstract::getDefaultModuleValue() 77 | * 78 | * @return int[] 79 | */ 80 | protected function getDefaultModuleValue(bool $isDark):array{ 81 | return ($isDark) ? [0, 0, 0] : [255, 255, 255]; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Output/qrcode.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "title": "chillerlan php-qrcode schema", 4 | "type": "object", 5 | "required": [ 6 | "qrcode" 7 | ], 8 | "properties": { 9 | "qrcode": { 10 | "$ref": "#/$defs/qrcode" 11 | } 12 | }, 13 | "$defs": { 14 | "qrcode": { 15 | "description": "QR Code root element", 16 | "type": "object", 17 | "required": [ 18 | "eccLevel", 19 | "matrix", 20 | "version" 21 | ], 22 | "properties": { 23 | "version": { 24 | "description": "The QR Code version: [1...40]", 25 | "type": "integer", 26 | "minimum": 1, 27 | "maximum": 40 28 | }, 29 | "eccLevel": { 30 | "description": "The ECC level: [L, M, Q, H]", 31 | "enum": [ 32 | "L", 33 | "M", 34 | "Q", 35 | "H" 36 | ] 37 | }, 38 | "matrix": { 39 | "$ref": "#/$defs/matrix" 40 | } 41 | } 42 | }, 43 | "matrix": { 44 | "description": "The matrix holds the encoded data in a 2-dimensional array of modules.", 45 | "type": "object", 46 | "required": [ 47 | "size", 48 | "quietzoneSize", 49 | "maskPattern", 50 | "width", 51 | "height" 52 | ], 53 | "properties": { 54 | "size": { 55 | "description": "The side length of the QR symbol, excluding the quiet zone (version * 4 + 17). [21...177]", 56 | "type": "integer", 57 | "minimum": 21, 58 | "maximum": 177 59 | }, 60 | "quietzoneSize": { 61 | "description": "The size of the quiet zone (margin around the QR symbol).", 62 | "type": "integer", 63 | "minimum": 0 64 | }, 65 | "maskPattern": { 66 | "description": "The detected mask pattern that was used to mask this matrix. [0...7].", 67 | "type": "integer", 68 | "minimum": 0, 69 | "maximum": 7 70 | }, 71 | "width": { 72 | "description": "The total width of the matrix, including the quiet zone.", 73 | "type": "integer", 74 | "minimum": 21 75 | }, 76 | "height": { 77 | "description": "The total height of the matrix, including the quiet zone.", 78 | "type": "integer", 79 | "minimum": 21 80 | }, 81 | "rows": { 82 | "type": "array", 83 | "items": { 84 | "$ref": "#/$defs/row" 85 | }, 86 | "minItems": 0 87 | } 88 | } 89 | }, 90 | "row": { 91 | "description": "A row holds an array of modules", 92 | "type": "object", 93 | "required": [ 94 | "y", 95 | "modules" 96 | ], 97 | "properties": { 98 | "y": { 99 | "description": "The 'y' (vertical) coordinate of this row.", 100 | "type": "integer", 101 | "minimum": 0 102 | }, 103 | "modules": { 104 | "type": "array", 105 | "items": { 106 | "$ref": "#/$defs/module" 107 | }, 108 | "minItems": 0 109 | } 110 | } 111 | }, 112 | "module": { 113 | "description": "Represents a single module (pixel) of a QR symbol.", 114 | "type": "object", 115 | "required": [ 116 | "dark", 117 | "layer", 118 | "value", 119 | "x" 120 | ], 121 | "properties": { 122 | "dark": { 123 | "description": "Indicates whether this module is dark.", 124 | "type": "boolean" 125 | }, 126 | "layer": { 127 | "description": "The layer (functional pattern) this module belongs to.", 128 | "type": "string" 129 | }, 130 | "value": { 131 | "description": "The value for this module.", 132 | "type": "string" 133 | }, 134 | "x": { 135 | "description": "The 'x' (horizontal) coordinate of this module.", 136 | "type": "integer", 137 | "minimum": 0 138 | } 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Output/qrcode.schema.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QR Code root element 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | The ECC level: [L, M, Q, H] 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | The QR Code version: [1...40] 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | The matrix holds the encoded data in a 2-dimensional array of modules. 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | The total height of the matrix, including the quiet zone. 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | The detected mask pattern that was used to mask this matrix. [0...7] 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | The size of the quiet zone (margin around the QR symbol). 68 | 69 | 70 | 71 | 72 | The side length of the QR symbol, excluding the quiet zone (version * 4 + 17). [21...177] 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | The total width of the matrix, including the quiet zone. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | A row holds an array of modules. 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | The "y" (vertical) coordinate of this row. 104 | 105 | 106 | 107 | 108 | 109 | 110 | Represents a single module (pixel) of a QR symbol. 111 | 112 | 113 | 114 | 115 | Indicates whether this module is dark. 116 | 117 | 118 | 119 | 120 | The layer (functional pattern) this module belongs to. 121 | 122 | 123 | 124 | 125 | The value for this module (CSS color). 126 | 127 | 128 | 129 | 130 | The "x" (horizontal) coordinate of this module. 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/QRCode.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode; 13 | 14 | use chillerlan\QRCode\Common\{ 15 | ECICharset, GDLuminanceSource, IMagickLuminanceSource, LuminanceSourceInterface, MaskPattern, Mode 16 | }; 17 | use chillerlan\QRCode\Data\{AlphaNum, Byte, ECI, Hanzi, Kanji, Number, QRData, QRDataModeInterface, QRMatrix}; 18 | use chillerlan\QRCode\Decoder\{Decoder, DecoderResult}; 19 | use chillerlan\QRCode\Output\{QRCodeOutputException, QROutputInterface}; 20 | use chillerlan\Settings\SettingsContainerInterface; 21 | use function class_exists, class_implements, in_array, is_iterable, mb_convert_encoding, mb_internal_encoding; 22 | 23 | /** 24 | * Turns a text string into a Model 2 QR Code 25 | * 26 | * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php 27 | * @see https://www.qrcode.com/en/codes/model12.html 28 | * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf 29 | * @see https://en.wikipedia.org/wiki/QR_code 30 | * @see https://www.thonky.com/qr-code-tutorial/ 31 | */ 32 | class QRCode{ 33 | 34 | /** 35 | * The settings container 36 | */ 37 | protected SettingsContainerInterface|QROptions $options; 38 | 39 | /** 40 | * A collection of one or more data segments of QRDataModeInterface instances to write 41 | * 42 | * @var \chillerlan\QRCode\Data\QRDataModeInterface[] 43 | */ 44 | protected array $dataSegments = []; 45 | 46 | /** 47 | * The luminance source for the reader 48 | */ 49 | protected string $luminanceSourceFQN = GDLuminanceSource::class; 50 | 51 | /** 52 | * QRCode constructor. 53 | * 54 | * @phpstan-param array $options 55 | */ 56 | public function __construct(SettingsContainerInterface|QROptions|iterable $options = new QROptions){ 57 | $this->setOptions($options); 58 | } 59 | 60 | /** 61 | * Sets an options instance 62 | * 63 | * @phpstan-param array $options 64 | */ 65 | public function setOptions(SettingsContainerInterface|QROptions|iterable $options):static{ 66 | 67 | if(is_iterable($options)){ 68 | $options = new QROptions($options); 69 | } 70 | 71 | $this->options = $options; 72 | 73 | if($this->options->readerUseImagickIfAvailable){ 74 | $this->luminanceSourceFQN = IMagickLuminanceSource::class; 75 | } 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Renders a QR Code for the given $data and QROptions, saves $file optionally 82 | */ 83 | public function render(string|null $data = null, string|null $file = null):mixed{ 84 | 85 | if($data !== null){ 86 | foreach(Mode::INTERFACES as $dataInterface){ 87 | 88 | if($dataInterface::validateString($data)){ 89 | $this->addSegment(new $dataInterface($data)); 90 | 91 | break; 92 | } 93 | } 94 | } 95 | 96 | return $this->renderMatrix($this->getQRMatrix(), $file); 97 | } 98 | 99 | /** 100 | * Renders a QR Code for the given QRMatrix and QROptions, saves $file optionally 101 | */ 102 | public function renderMatrix(QRMatrix $matrix, string|null $file = null):mixed{ 103 | return $this->initOutputInterface($matrix)->dump($file ?? $this->options->cachefile); 104 | } 105 | 106 | /** 107 | * Returns a QRMatrix object for the given $data and current QROptions 108 | */ 109 | public function getQRMatrix():QRMatrix{ 110 | $matrix = (new QRData($this->options, $this->dataSegments))->writeMatrix(); 111 | 112 | $maskPattern = $this->options->maskPattern === MaskPattern::AUTO 113 | ? MaskPattern::getBestPattern($matrix) 114 | : new MaskPattern($this->options->maskPattern); 115 | 116 | $matrix->setFormatInfo($maskPattern)->mask($maskPattern); 117 | 118 | return $this->addMatrixModifications($matrix); 119 | } 120 | 121 | /** 122 | * add matrix modifications after mask pattern evaluation and before handing over to output 123 | */ 124 | protected function addMatrixModifications(QRMatrix $matrix):QRMatrix{ 125 | 126 | if($this->options->addLogoSpace){ 127 | // check whether one of the dimensions was omitted 128 | $logoSpaceWidth = ($this->options->logoSpaceWidth ?? $this->options->logoSpaceHeight ?? 0); 129 | $logoSpaceHeight = ($this->options->logoSpaceHeight ?? $logoSpaceWidth); 130 | 131 | $matrix->setLogoSpace( 132 | $logoSpaceWidth, 133 | $logoSpaceHeight, 134 | $this->options->logoSpaceStartX, 135 | $this->options->logoSpaceStartY, 136 | ); 137 | } 138 | 139 | if($this->options->addQuietzone){ 140 | $matrix->setQuietZone($this->options->quietzoneSize); 141 | } 142 | 143 | return $matrix; 144 | } 145 | 146 | /** 147 | * initializes a fresh built-in or custom QROutputInterface 148 | * 149 | * @throws \chillerlan\QRCode\Output\QRCodeOutputException 150 | */ 151 | protected function initOutputInterface(QRMatrix $matrix):QROutputInterface{ 152 | $outputInterface = $this->options->outputInterface; 153 | 154 | if(empty($outputInterface) || !class_exists($outputInterface)){ 155 | throw new QRCodeOutputException('invalid output class'); 156 | } 157 | 158 | if(!in_array(QROutputInterface::class, class_implements($outputInterface), true)){ 159 | throw new QRCodeOutputException('output class does not implement QROutputInterface'); 160 | } 161 | 162 | /** @var \chillerlan\QRCode\Output\QROutputInterface $instance */ 163 | $instance = new $outputInterface($this->options, $matrix); 164 | 165 | return $instance; 166 | } 167 | 168 | /** 169 | * Adds a data segment 170 | * 171 | * ISO/IEC 18004:2000 8.3.6 - Mixing modes 172 | * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length 173 | */ 174 | public function addSegment(QRDataModeInterface $segment):static{ 175 | $this->dataSegments[] = $segment; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * Clears the data segments array 182 | * 183 | * @codeCoverageIgnore 184 | */ 185 | public function clearSegments():static{ 186 | $this->dataSegments = []; 187 | 188 | return $this; 189 | } 190 | 191 | /** 192 | * Adds a numeric data segment 193 | * 194 | * ISO/IEC 18004:2000 8.3.2 - Numeric Mode 195 | */ 196 | public function addNumericSegment(string $data):static{ 197 | return $this->addSegment(new Number($data)); 198 | } 199 | 200 | /** 201 | * Adds an alphanumeric data segment 202 | * 203 | * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode 204 | */ 205 | public function addAlphaNumSegment(string $data):static{ 206 | return $this->addSegment(new AlphaNum($data)); 207 | } 208 | 209 | /** 210 | * Adds a Kanji data segment (Japanese 13-bit double-byte characters, Shift-JIS) 211 | * 212 | * ISO/IEC 18004:2000 8.3.5 - Kanji Mode 213 | */ 214 | public function addKanjiSegment(string $data):static{ 215 | return $this->addSegment(new Kanji($data)); 216 | } 217 | 218 | /** 219 | * Adds a Hanzi data segment (simplified Chinese 13-bit double-byte characters, GB2312/GB18030) 220 | * 221 | * GBT18284-2000 Hanzi Mode 222 | */ 223 | public function addHanziSegment(string $data):static{ 224 | return $this->addSegment(new Hanzi($data)); 225 | } 226 | 227 | /** 228 | * Adds an 8-bit byte data segment 229 | * 230 | * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode 231 | */ 232 | public function addByteSegment(string $data):static{ 233 | return $this->addSegment(new Byte($data)); 234 | } 235 | 236 | /** 237 | * Adds a standalone ECI designator 238 | * 239 | * The ECI designator must be followed by a Byte segment that contains the string encoded according to the given ECI charset 240 | * 241 | * ISO/IEC 18004:2000 8.3.1 - Extended Channel Interpretation (ECI) Mode 242 | */ 243 | public function addEciDesignator(int $encoding):static{ 244 | return $this->addSegment(new ECI($encoding)); 245 | } 246 | 247 | /** 248 | * Adds an ECI data segment (including designator) 249 | * 250 | * The given string will be encoded from mb_internal_encoding() to the given ECI character set 251 | * 252 | * I hate this somehow, but I'll leave it for now 253 | * 254 | * @throws \chillerlan\QRCode\QRCodeException 255 | */ 256 | public function addEciSegment(int $encoding, string $data):static{ 257 | // validate the encoding id 258 | $eciCharset = new ECICharset($encoding); 259 | // get charset name 260 | $eciCharsetName = $eciCharset->getName(); 261 | // convert the string to the given charset 262 | if($eciCharsetName !== null){ 263 | $data = mb_convert_encoding($data, $eciCharsetName, mb_internal_encoding()); 264 | 265 | return $this 266 | ->addEciDesignator($eciCharset->getID()) 267 | ->addByteSegment($data) 268 | ; 269 | } 270 | 271 | throw new QRCodeException('unable to add ECI segment'); 272 | } 273 | 274 | /** 275 | * Reads a QR Code from a given file 276 | */ 277 | public function readFromFile(string $path):DecoderResult{ 278 | return $this->readFromSource($this->luminanceSourceFQN::fromFile($path, $this->options)); 279 | } 280 | 281 | /** 282 | * Reads a QR Code from the given data blob 283 | */ 284 | public function readFromBlob(string $blob):DecoderResult{ 285 | return $this->readFromSource($this->luminanceSourceFQN::fromBlob($blob, $this->options)); 286 | } 287 | 288 | /** 289 | * Reads a QR Code from the given luminance source 290 | */ 291 | public function readFromSource(LuminanceSourceInterface $source):DecoderResult{ 292 | return (new Decoder($this->options))->decode($source); 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /src/QRCodeException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode; 13 | 14 | use Exception; 15 | 16 | /** 17 | * An exception container 18 | */ 19 | class QRCodeException extends Exception{ 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/QRCodeReaderOptionsTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2024 smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode; 13 | 14 | use function extension_loaded; 15 | 16 | /** 17 | * Trait QRCodeReaderOptionsTrait 18 | * 19 | * @property bool $readerUseImagickIfAvailable 20 | * @property bool $readerGrayscale 21 | * @property bool $readerInvertColors 22 | * @property bool $readerIncreaseContrast 23 | */ 24 | trait QRCodeReaderOptionsTrait{ 25 | 26 | /** 27 | * Use Imagick (if available) when reading QR Codes 28 | */ 29 | protected bool $readerUseImagickIfAvailable = false; 30 | 31 | /** 32 | * Grayscale the image before reading 33 | */ 34 | protected bool $readerGrayscale = false; 35 | 36 | /** 37 | * Invert the colors of the image 38 | */ 39 | protected bool $readerInvertColors = false; 40 | 41 | /** 42 | * Increase the contrast before reading 43 | * 44 | * note that applying contrast works different in GD and Imagick, so mileage may vary 45 | */ 46 | protected bool $readerIncreaseContrast = false; 47 | 48 | /** 49 | * enables Imagick for the QR Code reader if the extension is available 50 | */ 51 | protected function set_readerUseImagickIfAvailable(bool $useImagickIfAvailable):void{ 52 | $this->readerUseImagickIfAvailable = ($useImagickIfAvailable && extension_loaded('imagick')); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/QROptions.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2015 Smiley 8 | * @license MIT 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace chillerlan\QRCode; 13 | 14 | use chillerlan\Settings\SettingsContainerAbstract; 15 | 16 | /** 17 | * The QRCode settings container 18 | */ 19 | class QROptions extends SettingsContainerAbstract{ 20 | use QROptionsTrait, QRCodeReaderOptionsTrait; 21 | } 22 | --------------------------------------------------------------------------------