├── .c8rc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── general_issue.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .jsdoc.json ├── .mocharc.json ├── .npmrc ├── .nycrc.json ├── LICENSE ├── README.md ├── babel.config.json ├── doc └── Readme.md ├── eslint.config.js ├── examples ├── RoundQuietzoneOptions.js ├── RoundQuietzoneSVGoutput.js ├── browser-canvas.html ├── browser-svg-round-quietzone.html ├── browser-svg.html ├── node-json.mjs ├── node-plaintext-console.mjs └── node-svg-round-quietzone.mjs ├── lib └── Readme.md ├── package-lock.json ├── package.json ├── rollup.config.dist.js ├── rollup.config.prepublish.js ├── rollup.config.src.js ├── src ├── Common │ ├── BitBuffer.js │ ├── EccLevel.js │ ├── GF256.js │ ├── GenericGFPoly.js │ ├── MaskPattern.js │ ├── Mode.js │ ├── PHPJS.js │ ├── ReedSolomonEncoder.js │ ├── Version.js │ └── constants.js ├── Data │ ├── AlphaNum.js │ ├── Byte.js │ ├── Numeric.js │ ├── QRCodeDataException.js │ ├── QRData.js │ ├── QRDataModeAbstract.js │ ├── QRDataModeInterface.js │ └── QRMatrix.js ├── Output │ ├── QRCanvas.js │ ├── QRCodeOutputException.js │ ├── QRMarkupSVG.js │ ├── QROutputAbstract.js │ ├── QROutputInterface.js │ ├── QRStringJSON.js │ └── QRStringText.js ├── QRCode.js ├── QRCodeException.js ├── QROptions.js └── index.js └── test ├── Common ├── BitBuffer.test.js ├── EccLevel.test.js ├── MaskPattern.test.js ├── Mode.test.js ├── PHPJS.test.js └── Version.test.js ├── Data ├── QRData.test.js ├── QRDataMode.test.js └── QRMatrix.test.js ├── Output ├── QRMarkupSVG.test.js ├── QRStringJSON.test.js └── QRStringText.test.js ├── QRCode.test.js └── QROptions.test.js /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "es-modules": true, 4 | "include": ["src/**"], 5 | "reporter": ["clover"], 6 | "report-dir": "./.build/coverage", 7 | "temp-dir": "./.build/c8" 8 | } 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: codemasher 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You have found a bug? That's great (ok, not that great)! Please help us to improve! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **Steps to reproduce the behavior** 18 | - When i do ... 19 | - The code below ... 20 | - Error message: ... 21 | 22 | **Code sample** 23 | ```php 24 | // your code here 25 | ``` 26 | 27 | **Expected behavior** 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | **Environment (please complete the following information):** 34 | - PHP version/OS: [e.g. 7.4.12, Ubuntu 20.04] 35 | - Library version: [e.g. 4.3.1] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General issue 3 | about: An issue that is not a bug - please use the discussions instead! Thanks! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | 7 | 8 | 9 | ## Types of changes 10 | 11 | 12 | 13 | What types of changes does your code introduce? 14 | 15 | - [ ] Bugfix (non-breaking change which fixes an issue) 16 | - [ ] New feature (non-breaking change which adds functionality) 17 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 18 | 19 | 20 | ## Checklist: 21 | 22 | - [ ] I have checked to ensure there aren't other open [Issues](../../../issues) or [Pull Requests](../../../pulls) for the same update/change 23 | - [ ] I have added tests that prove my fix is effective or that my feature works 24 | - [ ] I have added necessary documentation (if appropriate) 25 | - [ ] Any dependent changes have been merged and published in downstream modules 26 | - [ ] Lint and unit tests pass locally with my changes 27 | 28 | 29 | ## Further comments 30 | 31 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions 2 | 3 | name: "build" 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | name: "build" 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: "checkout sources" 17 | uses: actions/checkout@v4 18 | 19 | - name: "install dependencies" 20 | run: npm install 21 | 22 | - name: "run eslint" 23 | run: npm run lint 24 | 25 | - name: "run mocha tests (with coverage)" 26 | run: npm run test-with-coverage 27 | 28 | - name: "build" 29 | run: npm run build 30 | 31 | - name: "build-src" 32 | run: npm run build-src 33 | 34 | - name: "send code coverage report to codecov.io" 35 | uses: codecov/codecov-action@v4 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | files: .build/coverage/clover.xml 39 | 40 | - name: "build docs" 41 | run: npm run jsdoc 42 | 43 | - name: "publish docs to gh-pages" 44 | uses: JamesIves/github-pages-deploy-action@v4 45 | with: 46 | branch: gh-pages 47 | folder: doc 48 | clean: true 49 | 50 | - name: "upload artifacts" 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: js-qrcode-${{ github.sha }} 54 | path: ./dist 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | .idea 3 | dist 4 | lib 5 | node_modules 6 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "encoding": "utf8", 4 | "destination": "./doc/", 5 | "recurse": true 6 | }, 7 | "source": { 8 | "include": ["src/"], 9 | "exclude": ["src/index.js"] 10 | }, 11 | "sourceType": "module" 12 | } 13 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "recursive": true, 3 | "ui": "tdd", 4 | "spec": [ 5 | "test/**/*.test.js" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-babel", 3 | "temp-dir": ".build/.nyc_output" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 chillerlan 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chillerlan/js-qrcode 2 | 3 | A javascript port of [chillerlan/php-qrcode](https://github.com/chillerlan/php-qrcode), a QR Code library based on the [implementation](https://github.com/kazuhikoarase/qrcode-generator) by Kazuhiko Arase. 4 | 5 | [![NPM version][npm-badge]][npm] 6 | [![License][license-badge]][license] 7 | [![CodeCov][coverage-badge]][coverage] 8 | [![Build][gh-action-badge]][gh-action] 9 | [![NPM downloads][npm-downloads]][npm] 10 | 11 | [npm-badge]: https://img.shields.io/npm/v/%40chillerlan%2Fqrcode?logo=npm&logoColor=ccc 12 | [npm-downloads]: https://img.shields.io/npm/dm/%40chillerlan%2Fqrcode?logo=npm&logoColor=ccc 13 | [npm]: https://www.npmjs.com/package/@chillerlan/qrcode 14 | [license-badge]: https://img.shields.io/github/license/chillerlan/js-qrcode.svg 15 | [license]: https://github.com/chillerlan/js-qrcode/blob/main/LICENSE 16 | [coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/js-qrcode?logo=codecov&logoColor=ccc 17 | [coverage]: https://codecov.io/github/chillerlan/js-qrcode 18 | [gh-action-badge]: https://img.shields.io/github/actions/workflow/status/chillerlan/js-qrcode/build.yml?branch=main&logo=github&logoColor=ccc 19 | [gh-action]: https://github.com/chillerlan/js-qrcode/actions/workflows/build.yml?query=branch%3Amain 20 | 21 | 22 | # Overview 23 | 24 | ## Features 25 | 26 | - 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) 27 | - [ECC Levels](https://www.qrcode.com/en/about/error_correction.html) L/M/Q/H supported 28 | - Mixed mode support (encoding modes can be combined within a QR symbol). Supported modes: 29 | - numeric 30 | - alphanumeric 31 | - 8-bit binary 32 | - Flexible, easily extensible output modules, built-in support for the following output formats: 33 | - Markup types: SVG, etc. 34 | - String types: JSON, plain text, etc. 35 | - Raster image types via [Canvas](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) 36 | 37 | 38 | ## Documentation 39 | 40 | The API is very similar to [the PHP version of this library](https://github.com/chillerlan/php-qrcode), and you can refer to its [documentation](https://php-qrcode.readthedocs.io/) and [examples](https://github.com/chillerlan/php-qrcode/tree/main/examples) for most parts. 41 | 42 | Key differences: 43 | 44 | - Class constants of the PHP version are regular constants here, e.g. `QRMatrix::M_DATA` becomes `M_DATA`, `Version::AUTO` is `VERSION_AUTO` and `EccLevel:L` to `ECC_L` - see [index.js](https://github.com/chillerlan/js-qrcode/blob/main/src/index.js) for the proper names of all symbols. 45 | - No multimode support for Kanji and Hanzi character sets (handling/converting non-UTF8 strings in javascript is a mess), no [ECI](https://en.wikipedia.org/wiki/Extended_Channel_Interpretation) support for the same reason. 46 | - save-to-file is not supported. You can re-implement the method `QROutputAbstract::saveToFile()` in case you need it. It's called internally, but it has no function. 47 | - No QR Code reader included 48 | - The usual javascript quirks: 49 | - the internal structure of some classes may deviate from their PHP counterparts 50 | - key-value arrays become objects, causing inconsistent return values in some cases, not to mention the inconsistent loop types for each (pun intended) 51 | - numbers may act strange 52 | - magic getters and setters come with downsides, [see this comment](https://github.com/chillerlan/js-qrcode/blob/76a7a8db1b6e39d09a09f2091a830dcd7d98e9ff/src/QROptions.js#L352-L412) 53 | 54 | An API documentation created with [jsdoc](https://github.com/jsdoc/jsdoc) can be found at https://chillerlan.github.io/js-qrcode/ (WIP). 55 | 56 | ### Installation 57 | 58 | Via terminal: `npm i @chillerlan/qrcode` 59 | 60 | In `package.json`: 61 | 62 | ```json 63 | { 64 | "dependencies": { 65 | "@chillerlan/qrcode": "^1.0" 66 | } 67 | } 68 | ``` 69 | 70 | ### Quickstart 71 | 72 | Server-side, in nodejs: 73 | ```js 74 | import {QRCode} from './dist/js-qrcode-node-src.cjs'; 75 | 76 | let data = 'otpauth://totp/test?secret=B3JX4VCVJDVNXNZ5&issuer=chillerlan.net'; 77 | let qrcode = (new QRCode()).render(data); 78 | 79 | // do stuff 80 | console.log(qrcode); 81 | ``` 82 | 83 | Client-side, in a webbrowser: 84 | ```html 85 |
86 | 99 | ``` 100 |

101 | QR codes are awesome! 102 |

103 | 104 | Have a look [in the examples folder](https://github.com/chillerlan/js-qrcode/tree/main/examples) for some more usage examples. 105 | 106 | 107 | #### License notice 108 | Parts of this code are ported to js (via php) from the [ZXing project](https://github.com/zxing/zxing) and licensed under the [Apache License, Version 2.0](./NOTICE). 109 | 110 | 111 | #### Trademark Notice 112 | 113 | The word "QR Code" is a registered trademark of *DENSO WAVE INCORPORATED*
114 | https://www.qrcode.com/en/faq.html#patentH2Title 115 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": "node_modules/**", 3 | "plugins": [ 4 | "istanbul" 5 | ], 6 | "presets": [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | "targets": { 11 | "browsers": [ 12 | "last 2 versions", 13 | "ie >= 11" 14 | ] 15 | }, 16 | "useBuiltIns": "entry", 17 | "corejs": "3.37" 18 | } 19 | ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /doc/Readme.md: -------------------------------------------------------------------------------- 1 | # Auto generated API documentation 2 | 3 | The API documentation can be auto generated with [jsdoc](https://github.com/jsdoc/jsdoc). 4 | There is an [online version available](https://chillerlan.github.io/js-qrcode/) via the [gh-pages branch](https://github.com/chillerlan/js-qrcode/tree/gh-pages) that is [automatically deployed](https://github.com/chillerlan/js-qrcode/deployments) on each push to main. 5 | 6 | Locally created docs will appear in this directory. If you'd like to create local docs, please follow these steps: 7 | 8 | - run `npm run jsdoc` 9 | - open [index.html](./index.html) in a browser 10 | - profit! 11 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import babelParser from "@babel/eslint-parser"; 2 | 3 | export default [ 4 | { 5 | files: ['**/*.js'], 6 | ignores: [ 7 | '**/dist/**', 8 | '**/lib/**', 9 | '**/node_modules/**', 10 | ], 11 | languageOptions: { 12 | parser: babelParser, 13 | parserOptions: { 14 | sourceType: 'module', 15 | requireConfigFile: false, 16 | babelOptions: { 17 | configFile: './babel.config.json', 18 | }, 19 | 20 | }, 21 | ecmaVersion: 2022 22 | }, 23 | rules: { 24 | 'no-console': 'off', 25 | 'no-debugger': 'off', 26 | 'no-unused-vars': 'off', 27 | 'eqeqeq': 'error', 28 | 'no-useless-escape': 'off', 29 | 'quotes': [ 30 | 'error', 31 | 'single', 32 | { 33 | 'avoidEscape': false 34 | } 35 | ], 36 | 'max-len': [ 37 | 2, 38 | { 39 | 'code': 130, 40 | 'tabWidth': 4, 41 | 'ignoreUrls': true, 42 | 'ignoreComments': true 43 | } 44 | ], 45 | 'curly': [ 46 | 'error', 47 | 'all' 48 | ] 49 | 50 | } 51 | } 52 | ]; 53 | -------------------------------------------------------------------------------- /examples/RoundQuietzoneOptions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 10.06.2024 3 | * @author smiley 4 | * @copyright 2024 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {QROptions} from '../src/index.js'; 9 | 10 | /** 11 | * The options class for RoundQuietzoneSVGoutput 12 | */ 13 | export default class RoundQuietzoneOptions extends QROptions{ 14 | 15 | /** 16 | * we need to add the constructor with a parent call here, 17 | * otherwise the additional properties will not be recognized 18 | * 19 | * @inheritDoc 20 | */ 21 | constructor($options = null){ 22 | super(); 23 | // this.__workaround__.push('myMagicProp'); 24 | this._fromIterable($options) 25 | } 26 | 27 | /** 28 | * The amount of additional modules to be used in the circle diameter calculation 29 | * 30 | * Note that the middle of the circle stroke goes through the (assumed) outer corners 31 | * or centers of the QR Code (excluding quiet zone) 32 | * 33 | * Example: 34 | * 35 | * - a value of -1 would go through the center of the outer corner modules of the finder patterns 36 | * - a value of 0 would go through the corner of the outer modules of the finder patterns 37 | * - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45-degree angle 38 | * 39 | * @type {number|int} 40 | */ 41 | additionalModules = 0; 42 | 43 | /** 44 | * the logo as SVG string (e.g. from simple-icons) 45 | * 46 | * @type {string} 47 | */ 48 | svgLogo = ''; 49 | 50 | /** 51 | * an optional css class for the logo container 52 | * 53 | * @type {string} 54 | */ 55 | svgLogoCssClass = ''; 56 | 57 | /** 58 | * logo scale in % of QR Code size, internally clamped to 5%-25% 59 | * 60 | * @type {number|float} 61 | */ 62 | svgLogoScale = 0.20; 63 | 64 | /** 65 | * the IDs for the several colored layers, translates to css class "qr-123" which can be used in the stylesheet 66 | * 67 | * note that the layer id has to be an integer value, ideally outside the several bitmask values 68 | * 69 | * @type {int[]} 70 | * @see QRMarkupSVG.getCssClass() 71 | */ 72 | dotColors = []; 73 | } 74 | -------------------------------------------------------------------------------- /examples/RoundQuietzoneSVGoutput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 10.06.2024 3 | * @author smiley 4 | * @copyright 2024 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {IS_DARK, M_DATA, M_LOGO, M_QUIETZONE, M_QUIETZONE_DARK, QRMarkupSVG} from '../src/index.js'; 9 | 10 | /** 11 | * A custom SVG output class 12 | * 13 | * @see https://github.com/chillerlan/php-qrcode/discussions/137 14 | */ 15 | export default class RoundQuietzoneSVGoutput extends QRMarkupSVG{ 16 | 17 | radius; 18 | center; 19 | logoScale; 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | createMarkup($saveToFile){ 25 | 26 | // some Pythagorean magick 27 | let $diameter = Math.sqrt(2 * Math.pow((this.moduleCount + this.options.additionalModules), 2)); 28 | this.radius = ($diameter / 2).toFixed(3); 29 | 30 | // clamp the logo scale 31 | this.logoScale = Math.max(0.05, Math.min(0.25, this.options.svgLogoScale)); 32 | 33 | // calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it 34 | let $quietzoneSize = (Math.ceil(($diameter - this.moduleCount) / 2) + 1); 35 | 36 | // add the quiet zone to fill the circle 37 | this.matrix.setQuietZone($quietzoneSize); 38 | 39 | // update the matrix dimensions to avoid errors in subsequent calculations 40 | // the moduleCount is now QR Code matrix + 2x quiet zone 41 | this.setMatrixDimensions(); 42 | this.center = (this.moduleCount / 2); 43 | 44 | // clear the logo space 45 | this.clearLogoSpace(); 46 | 47 | // color the quiet zone 48 | this.colorQuietzone($quietzoneSize, this.radius); 49 | 50 | // start SVG output 51 | let $svg = this.header(); 52 | let $eol = this.options.eol; 53 | 54 | if(this.options.svgDefs !== ''){ 55 | $svg += `${this.options.svgDefs}${$eol}${$eol}`; 56 | } 57 | 58 | $svg += this.paths(); 59 | $svg += this.addCircle(this.radius); 60 | $svg += this.getLogo(); 61 | 62 | // close svg 63 | $svg += `${$eol}${$eol}`; 64 | 65 | return $svg; 66 | } 67 | 68 | /** 69 | * Clears a circular area for the logo 70 | */ 71 | clearLogoSpace(){ 72 | let $logoSpaceSize = (Math.ceil(this.moduleCount * this.logoScale) + 1); 73 | // set a rectangular space instead 74 | // this.matrix.setLogoSpace($logoSpaceSize); 75 | 76 | let $r = ($logoSpaceSize / 2) + this.options.circleRadius; 77 | 78 | for(let $y = 0; $y < this.moduleCount; $y++){ 79 | for(let $x = 0; $x < this.moduleCount; $x++){ 80 | 81 | if(this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, $r)){ 82 | this.matrix.set($x, $y, false, M_LOGO); 83 | 84 | } 85 | 86 | } 87 | } 88 | 89 | } 90 | 91 | /** 92 | * Sets random modules of the quiet zone to dark 93 | * 94 | * @param {number|int} $quietzoneSize 95 | * @param {number|float} $radius 96 | */ 97 | colorQuietzone($quietzoneSize, $radius){ 98 | let $l1 = ($quietzoneSize - 1); 99 | let $l2 = (this.moduleCount - $quietzoneSize); 100 | // substract 1/2 stroke width and module radius from the circle radius to not cut off modules 101 | let $r = ($radius - this.options.circleRadius * 2); 102 | 103 | for(let $y = 0; $y < this.moduleCount; $y++){ 104 | for(let $x = 0; $x < this.moduleCount; $x++){ 105 | 106 | // skip anything that's not quiet zone 107 | if(!this.matrix.checkType($x, $y, M_QUIETZONE)){ 108 | continue; 109 | } 110 | 111 | // leave one row of quiet zone around the matrix 112 | if( 113 | ($x === $l1 && $y >= $l1 && $y <= $l2) 114 | || ($x === $l2 && $y >= $l1 && $y <= $l2) 115 | || ($y === $l1 && $x >= $l1 && $x <= $l2) 116 | || ($y === $l2 && $x >= $l1 && $x <= $l2) 117 | ){ 118 | continue; 119 | } 120 | 121 | // we need to add 0.5 units to the check values since we're calculating the element centers 122 | // ($x/$y is the element's assumed top left corner) 123 | if(this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, $r)){ 124 | let randomBoolean = (Math.random() < 0.5); 125 | 126 | this.matrix.set($x, $y, randomBoolean, M_QUIETZONE); 127 | } 128 | 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * @see https://stackoverflow.com/a/7227057 135 | * 136 | * @param {number|float} $x 137 | * @param {number|float} $y 138 | * @param {number|float} $centerX 139 | * @param {number|float} $centerY 140 | * @param {number|float} $radius 141 | */ 142 | checkIfInsideCircle($x, $y, $centerX, $centerY, $radius){ 143 | let $dx = Math.abs($x - $centerX); 144 | let $dy = Math.abs($y - $centerY); 145 | 146 | if(($dx + $dy) <= $radius){ 147 | return true; 148 | } 149 | 150 | if($dx > $radius || $dy > $radius){ 151 | return false; 152 | } 153 | 154 | return (Math.pow($dx, 2) + Math.pow($dy, 2)) <= Math.pow($radius, 2); 155 | } 156 | 157 | /** 158 | * add a solid circle around the matrix 159 | * 160 | * @param {number|float} $radius 161 | */ 162 | addCircle($radius){ 163 | let pos = this.center.toFixed(3); 164 | let stroke = (this.options.circleRadius * 2).toFixed(3); 165 | 166 | return `${this.options.eol}`; 167 | } 168 | 169 | /** 170 | * returns the SVG logo wrapped in a container with a transform that scales it proportionally 171 | */ 172 | getLogo(){ 173 | let eol = this.options.eol; 174 | let pos = (this.moduleCount - this.moduleCount * this.logoScale) / 2; 175 | 176 | return `${eol}${this.options.svgLogo}${eol}` 177 | } 178 | 179 | /** 180 | * @inheritDoc 181 | */ 182 | collectModules($transform){ 183 | let $paths = {}; 184 | let $matrix = this.matrix.getMatrix(); 185 | let $y = 0; 186 | 187 | // collect the modules for each type 188 | for(let $row of $matrix){ 189 | let $x = 0; 190 | 191 | for(let $M_TYPE of $row){ 192 | let $M_TYPE_LAYER = $M_TYPE; 193 | 194 | if(this.options.connectPaths && !this.matrix.checkTypeIn($x, $y, this.options.excludeFromConnect)){ 195 | // to connect paths we'll redeclare the $M_TYPE_LAYER to data only 196 | $M_TYPE_LAYER = M_DATA; 197 | 198 | if(this.matrix.check($x, $y)){ 199 | $M_TYPE_LAYER |= IS_DARK; 200 | } 201 | } 202 | 203 | // randomly assign another $M_TYPE_LAYER for the given types 204 | if($M_TYPE_LAYER === M_QUIETZONE_DARK){ 205 | let key = Math.floor(Math.random() * this.options.dotColors.length); 206 | 207 | $M_TYPE_LAYER = this.options.dotColors[key]; 208 | } 209 | 210 | // collect the modules per $M_TYPE 211 | let $module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER); 212 | 213 | if($module){ 214 | if(!$paths[$M_TYPE_LAYER]){ 215 | $paths[$M_TYPE_LAYER] = []; 216 | } 217 | 218 | $paths[$M_TYPE_LAYER].push($module); 219 | } 220 | $x++; 221 | } 222 | $y++; 223 | } 224 | 225 | // beautify output 226 | 227 | 228 | return $paths; 229 | } 230 | 231 | /** 232 | * @inheritDoc 233 | */ 234 | module($x, $y, $M_TYPE){ 235 | 236 | // we'll ignore anything outside the circle 237 | if(!this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, this.radius)){ 238 | return ''; 239 | } 240 | 241 | if((!this.options.drawLightModules && !this.matrix.check($x, $y))){ 242 | return ''; 243 | } 244 | 245 | if(this.options.drawCircularModules && !this.matrix.checkTypeIn($x, $y, this.options.keepAsSquare)){ 246 | let r = parseFloat(this.options.circleRadius); 247 | let d = (r * 2); 248 | let ix = ($x + 0.5 - r); 249 | let iy = ($y + 0.5); 250 | 251 | if(ix < 1){ 252 | ix = ix.toPrecision(3); 253 | } 254 | 255 | if(iy < 1){ 256 | iy = iy.toPrecision(3); 257 | } 258 | 259 | return `M${ix} ${iy} a${r} ${r} 0 1 0 ${d} 0 a${r} ${r} 0 1 0 -${d} 0Z`; 260 | } 261 | 262 | return `M${$x} ${$y} h1 v1 h-1Z`; 263 | } 264 | 265 | } 266 | 267 | -------------------------------------------------------------------------------- /examples/browser-canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QRCode canvas example 7 | 13 | 14 | 15 | QR Code container 16 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/browser-svg-round-quietzone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QRCode SVG example 7 | 54 | 55 | 56 |
57 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /examples/browser-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | QRCode SVG example 7 | 20 | 21 | 22 |
23 |
24 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/node-json.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 14.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import { 9 | QRCode, QROptions, QRStringJSON, 10 | } from '../src/index.js'; 11 | 12 | 13 | let $options = new QROptions; 14 | 15 | $options.outputInterface = QRStringJSON; 16 | $options.version = 5; 17 | $options.drawLightModules = false; 18 | 19 | let $qrcode = new QRCode($options); 20 | 21 | let jsonString = $qrcode.render('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); 22 | 23 | // object per schema https://raw.githubusercontent.com/chillerlan/php-qrcode/main/src/Output/qrcode.schema.json 24 | console.log(JSON.parse(jsonString)); 25 | -------------------------------------------------------------------------------- /examples/node-plaintext-console.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 14.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import { 9 | QRCode, QROptions, QRStringText, 10 | M_FINDER_DARK, M_FINDER, M_FINDER_DOT, M_ALIGNMENT_DARK, M_ALIGNMENT, M_TIMING_DARK, M_TIMING, 11 | M_FORMAT_DARK, M_FORMAT, M_VERSION_DARK, M_VERSION, M_DARKMODULE, M_DATA_DARK, M_DATA, M_QUIETZONE, M_SEPARATOR, 12 | } from '../src/index.js'; 13 | 14 | 15 | let mv = {}; 16 | 17 | mv[M_FINDER_DARK] = QRStringText.ansi8('██', 124); 18 | mv[M_FINDER] = QRStringText.ansi8('░░', 124); 19 | mv[M_FINDER_DOT] = QRStringText.ansi8('██', 124); 20 | mv[M_ALIGNMENT_DARK] = QRStringText.ansi8('██', 2); 21 | mv[M_ALIGNMENT] = QRStringText.ansi8('░░', 2); 22 | mv[M_TIMING_DARK] = QRStringText.ansi8('██', 184); 23 | mv[M_TIMING] = QRStringText.ansi8('░░', 184); 24 | mv[M_FORMAT_DARK] = QRStringText.ansi8('██', 200); 25 | mv[M_FORMAT] = QRStringText.ansi8('░░', 200); 26 | mv[M_VERSION_DARK] = QRStringText.ansi8('██', 21); 27 | mv[M_VERSION] = QRStringText.ansi8('░░', 21); 28 | mv[M_DARKMODULE] = QRStringText.ansi8('██', 53); 29 | mv[M_DATA_DARK] = QRStringText.ansi8('██', 166); 30 | mv[M_DATA] = QRStringText.ansi8('░░', 166); 31 | mv[M_QUIETZONE] = QRStringText.ansi8('░░', 253); 32 | mv[M_SEPARATOR] = QRStringText.ansi8('░░', 253); 33 | 34 | 35 | let $options = new QROptions; 36 | 37 | $options.outputInterface = QRStringText; 38 | $options.version = 3; 39 | $options.quietzoneSize = 2; 40 | $options.moduleValues = mv; 41 | 42 | let $qrcode = new QRCode($options); 43 | 44 | console.log($qrcode.render('https://www.youtube.com/watch?v=dQw4w9WgXcQ')); 45 | -------------------------------------------------------------------------------- /examples/node-svg-round-quietzone.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 10.06.2024 3 | * @author smiley 4 | * @copyright 2024 smiley 5 | * @license MIT 6 | */ 7 | 8 | import * as qrc from '../src/index.js'; 9 | import RoundQuietzoneSVGoutput from './RoundQuietzoneSVGoutput.js'; 10 | import RoundQuietzoneOptions from './RoundQuietzoneOptions.js'; 11 | import * as fs from 'node:fs'; 12 | 13 | /* 14 | * run the example 15 | */ 16 | 17 | // use the extended options class 18 | let options = new RoundQuietzoneOptions({ 19 | // custom dot options (see extended options class) 20 | additionalModules : 5, 21 | // logo from: https://github.com/simple-icons/simple-icons 22 | svgLogo : '', 23 | svgLogoCssClass : 'logo', 24 | svgLogoScale : 0.2, 25 | dotColors : [111, 222, 333, 444, 555, 666], 26 | // load our own output class 27 | outputInterface : RoundQuietzoneSVGoutput, 28 | version : 7, 29 | eccLevel : qrc.ECC_H, 30 | // we're not adding a quiet zone, this is done internally in our own module 31 | addQuietzone : false, 32 | // toggle base64 data URI 33 | outputBase64 : false, 34 | // DOM is not available here 35 | returnAsDomElement : false, 36 | svgUseFillAttributes: false, 37 | // if set to false, the light modules won't be rendered 38 | drawLightModules : false, 39 | // draw the modules as circles isntead of squares 40 | drawCircularModules : true, 41 | circleRadius : 0.4, 42 | // connect paths 43 | connectPaths : true, 44 | excludeFromConnect : [ 45 | qrc.M_LOGO, 46 | qrc.M_QUIETZONE, 47 | ], 48 | // keep modules of these types as square 49 | keepAsSquare : [ 50 | qrc.M_FINDER_DARK, 51 | qrc.M_FINDER_DOT, 52 | qrc.M_ALIGNMENT_DARK, 53 | ], 54 | svgDefs : 55 | '\n\n' + 56 | '\n' + 57 | '\n' + 58 | '\n' + 59 | '\n' + 60 | '\n' + 61 | '\n' + 62 | '\n' + 63 | '\n' + 64 | '\n' + 65 | '\n' + 66 | '\n' + 67 | '\n' + 68 | '', 81 | }); 82 | 83 | 84 | let data = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; 85 | let qrcode = (new qrc.QRCode(options)).render(data); 86 | 87 | // write the data to an svg file 88 | fs.writeFile('./qrcode.svg', qrcode, (err) => { 89 | if(err){ 90 | console.error(err); 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /lib/Readme.md: -------------------------------------------------------------------------------- 1 | # Auto generated source 2 | 3 | The content of this directory is automatically transpiled to CommonJS from [the ES6 source](../src) 4 | during the build process in order to publish it to NPM.. 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chillerlan/qrcode", 3 | "version": "1.0.1", 4 | "description": "A QR Code generator - ported from php-qrcode (https://github.com/chillerlan/php-qrcode)", 5 | "license": "MIT", 6 | "homepage": "https://github.com/chillerlan/js-qrcode", 7 | "keywords": [ 8 | "qr code", 9 | "qrcode", 10 | "qr", 11 | "qrcode-generator", 12 | "jsqrcode", 13 | "svg" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/chillerlan/js-qrcode.git" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Kazuhiko Arase", 22 | "homepage": "https://github.com/kazuhikoarase/qrcode-generator" 23 | }, 24 | { 25 | "name": "Smiley", 26 | "email": "smiley@chillerlan.net", 27 | "homepage": "https://github.com/codemasher" 28 | } 29 | ], 30 | "contributors": [ 31 | { 32 | "name": "Contributors", 33 | "homepage": "https://github.com/chillerlan/js-qrcode/graphs/contributors" 34 | }, 35 | { 36 | "name": "ZXing Authors", 37 | "homepage": "https://github.com/zxing/zxing" 38 | } 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/chillerlan/js-qrcode/issues" 42 | }, 43 | "funding": [ 44 | { 45 | "type": "ko-fi", 46 | "url": "https://ko-fi.com/codemasher" 47 | } 48 | ], 49 | "type": "module", 50 | "main": "lib/main.cjs", 51 | "browser": "./lib/browser.js", 52 | "files": [ 53 | "dist/*", 54 | "lib/*", 55 | "src/*", 56 | "LICENSE", 57 | "README.md" 58 | ], 59 | "dependencies": {}, 60 | "devDependencies": { 61 | "@babel/eslint-parser": "~7.24.6", 62 | "@babel/preset-env": "~7.24.6", 63 | "@istanbuljs/nyc-config-babel": "~3.0.0", 64 | "@rollup/plugin-babel": "~6.0.4", 65 | "@rollup/plugin-terser": "~0.4.4", 66 | "c8": "~8.0.1", 67 | "chai": "~5.1.1", 68 | "core-js": "~3.37.1", 69 | "eslint": "~9.4.0", 70 | "jsdoc": "~4.0.3", 71 | "mocha": "~10.4.0", 72 | "node": "~22.2.0", 73 | "nyc": "~15.1.0", 74 | "rollup": "~4.18.0", 75 | "util": "~0.12.5" 76 | }, 77 | "scripts": { 78 | "lint": "eslint ./src ./test", 79 | "build": "rollup -c rollup.config.dist.js", 80 | "build-src": "rollup -c rollup.config.src.js", 81 | "test": "mocha", 82 | "test-with-coverage": "c8 mocha", 83 | "jsdoc": "jsdoc -c .jsdoc.json", 84 | "prepublishOnly": "npm run lint && npm run test && rollup -c rollup.config.prepublish.js" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rollup.config.dist.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | /** 5 | * @type {import('rollup').RollupOptions} 6 | */ 7 | export default { 8 | input: 'src/index.js', 9 | output: [ 10 | { 11 | file: 'dist/js-qrcode-es6.js', 12 | format: 'es', 13 | sourcemap: false, 14 | }, 15 | { 16 | file: 'dist/js-qrcode-amd.js', 17 | format: 'amd', 18 | sourcemap: false, 19 | }, 20 | { 21 | file: 'dist/js-qrcode-iife.js', 22 | format: 'iife', 23 | sourcemap: false, 24 | name: 'jsqrcode', 25 | }, 26 | { 27 | file: 'dist/js-qrcode-node.cjs', 28 | format: 'cjs', 29 | sourcemap: false, 30 | }, 31 | ], 32 | plugins: [ 33 | babel({ 34 | babelHelpers: 'bundled', 35 | configFile: './babel.config.json', 36 | }), 37 | terser({ 38 | format: { 39 | comments: false, 40 | keep_quoted_props: true, 41 | // max_line_len: 130, 42 | quote_style: 1, 43 | preamble: 44 | '/*\n' 45 | + ' * js-qrcode - a javascript port of chillerlan/php-qrcode\n' 46 | + ' *\n' 47 | + ' * @copyright 2022 smiley\n' 48 | + ' * @license MIT\n' 49 | + ' * @link https://github.com/chillerlan/js-qrcode\n' 50 | + ' * @link https://github.com/chillerlan/php-qrcode\n' 51 | + ' */', 52 | }, 53 | }), 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /rollup.config.prepublish.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/57718411 2 | 3 | import babel from '@rollup/plugin-babel'; 4 | 5 | /** 6 | * @type {import('rollup').RollupOptions} 7 | */ 8 | export default { 9 | input: 'src/index.js', 10 | output: [ 11 | { 12 | file: 'lib/browser.js', 13 | format: 'es', 14 | sourcemap: true, 15 | }, 16 | { 17 | file: 'lib/main.cjs', 18 | format: 'cjs', 19 | sourcemap: true, 20 | }, 21 | ], 22 | plugins: [ 23 | babel({ 24 | babelHelpers: 'bundled', 25 | configFile: './babel.config.json', 26 | }), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /rollup.config.src.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('rollup').RollupOptions} 3 | */ 4 | export default { 5 | input: 'src/index.js', 6 | output: [ 7 | { 8 | file: 'dist/js-qrcode-es6-src.js', 9 | format: 'es', 10 | sourcemap: true, 11 | }, 12 | { 13 | file: 'dist/js-qrcode-node-src.cjs', 14 | format: 'cjs', 15 | sourcemap: true, 16 | }, 17 | ], 18 | plugins: [], 19 | }; 20 | -------------------------------------------------------------------------------- /src/Common/BitBuffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import PHPJS from './PHPJS.js'; 9 | 10 | /** 11 | * Holds the raw binary data 12 | */ 13 | export default class BitBuffer{ 14 | 15 | /** 16 | * The buffer content 17 | * 18 | * @type {number[]|int[]} 19 | */ 20 | buffer = []; 21 | 22 | /** 23 | * Length of the content (bits) 24 | * 25 | * @type {number|int} 26 | */ 27 | length = 0; 28 | 29 | /** 30 | * BitBuffer constructor. 31 | * 32 | * @param {number[]|int[]|null} $bytes 33 | */ 34 | constructor($bytes = null){ 35 | this.buffer = $bytes || []; 36 | this.length = this.buffer.length; 37 | } 38 | 39 | /** 40 | * appends a sequence of bits 41 | * 42 | * @param {number|int} $bits 43 | * @param {number|int} $length 44 | * 45 | * @returns {BitBuffer} 46 | */ 47 | put($bits, $length){ 48 | 49 | for(let $i = 0; $i < $length; $i++){ 50 | this.putBit((($bits >> ($length - $i - 1)) & 1) === 1); 51 | } 52 | 53 | return this; 54 | } 55 | 56 | /** 57 | * appends a single bit 58 | * 59 | * @param {boolean} $bit 60 | * 61 | * @returns {BitBuffer} 62 | */ 63 | putBit($bit){ 64 | let $bufIndex = PHPJS.intval(Math.floor(this.length / 8)); 65 | 66 | if(this.buffer.length <= $bufIndex){ 67 | this.buffer.push(0); 68 | } 69 | 70 | if($bit === true){ 71 | this.buffer[$bufIndex] |= (0x80 >> (this.length % 8)); 72 | } 73 | 74 | this.length++; 75 | 76 | return this; 77 | } 78 | 79 | /** 80 | * returns the current buffer length 81 | * 82 | * @returns {number|int} 83 | */ 84 | getLength(){ 85 | return this.length; 86 | } 87 | 88 | /** 89 | * returns the buffer content 90 | * 91 | * @returns {number[]|int[]} 92 | */ 93 | getBuffer(){ 94 | return this.buffer; 95 | } 96 | 97 | /** 98 | * clears the buffer 99 | * 100 | * @returns {BitBuffer} 101 | */ 102 | clear(){ 103 | this.buffer = []; 104 | this.length = 0; 105 | 106 | return this; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/Common/EccLevel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QRCodeException from '../QRCodeException.js'; 9 | import PHPJS from './PHPJS.js'; 10 | import {ECC_H, ECC_L, ECC_M, ECC_Q} from './constants.js'; 11 | 12 | const ECC_LEVELS = [ECC_L, ECC_M, ECC_Q, ECC_H]; 13 | 14 | /** 15 | * ISO/IEC 18004:2000 Tables 7-11 - Number of symbol characters and input data capacity for versions 1 to 40 16 | * 17 | * @type {number[][]|int[][]} 18 | * 19 | * @private 20 | */ 21 | const MAX_BITS = [ 22 | // version => [L, M, Q, H] // modules 23 | [ 0, 0, 0, 0], // empty element, count starts at 1 24 | [ 152, 128, 104, 72], // 21 25 | [ 272, 224, 176, 128], // 25 26 | [ 440, 352, 272, 208], // 29 27 | [ 640, 512, 384, 288], // 33 28 | [ 864, 688, 496, 368], // 37 29 | [ 1088, 864, 608, 480], // 41 30 | [ 1248, 992, 704, 528], // 45 31 | [ 1552, 1232, 880, 688], // 49 32 | [ 1856, 1456, 1056, 800], // 53 33 | [ 2192, 1728, 1232, 976], // 57 34 | [ 2592, 2032, 1440, 1120], // 61 35 | [ 2960, 2320, 1648, 1264], // 65 36 | [ 3424, 2672, 1952, 1440], // 69 NICE! 37 | [ 3688, 2920, 2088, 1576], // 73 38 | [ 4184, 3320, 2360, 1784], // 77 39 | [ 4712, 3624, 2600, 2024], // 81 40 | [ 5176, 4056, 2936, 2264], // 85 41 | [ 5768, 4504, 3176, 2504], // 89 42 | [ 6360, 5016, 3560, 2728], // 93 43 | [ 6888, 5352, 3880, 3080], // 97 44 | [ 7456, 5712, 4096, 3248], // 101 45 | [ 8048, 6256, 4544, 3536], // 105 46 | [ 8752, 6880, 4912, 3712], // 109 47 | [ 9392, 7312, 5312, 4112], // 113 48 | [10208, 8000, 5744, 4304], // 117 49 | [10960, 8496, 6032, 4768], // 121 50 | [11744, 9024, 6464, 5024], // 125 51 | [12248, 9544, 6968, 5288], // 129 52 | [13048, 10136, 7288, 5608], // 133 53 | [13880, 10984, 7880, 5960], // 137 54 | [14744, 11640, 8264, 6344], // 141 55 | [15640, 12328, 8920, 6760], // 145 56 | [16568, 13048, 9368, 7208], // 149 57 | [17528, 13800, 9848, 7688], // 153 58 | [18448, 14496, 10288, 7888], // 157 59 | [19472, 15312, 10832, 8432], // 161 60 | [20528, 15936, 11408, 8768], // 165 61 | [21616, 16816, 12016, 9136], // 169 62 | [22496, 17728, 12656, 9776], // 173 63 | [23648, 18672, 13328, 10208], // 177 64 | ]; 65 | 66 | /** 67 | * ISO/IEC 18004:2000 Section 8.9 - Format Information 68 | * 69 | * ECC level -> mask pattern 70 | * 71 | * @type {number[][]|int[][]} 72 | * 73 | * @private 74 | */ 75 | const FORMAT_PATTERN = [ 76 | [ // L 77 | 0b111011111000100, 78 | 0b111001011110011, 79 | 0b111110110101010, 80 | 0b111100010011101, 81 | 0b110011000101111, 82 | 0b110001100011000, 83 | 0b110110001000001, 84 | 0b110100101110110, 85 | ], 86 | [ // M 87 | 0b101010000010010, 88 | 0b101000100100101, 89 | 0b101111001111100, 90 | 0b101101101001011, 91 | 0b100010111111001, 92 | 0b100000011001110, 93 | 0b100111110010111, 94 | 0b100101010100000, 95 | ], 96 | [ // Q 97 | 0b011010101011111, 98 | 0b011000001101000, 99 | 0b011111100110001, 100 | 0b011101000000110, 101 | 0b010010010110100, 102 | 0b010000110000011, 103 | 0b010111011011010, 104 | 0b010101111101101, 105 | ], 106 | [ // H 107 | 0b001011010001001, 108 | 0b001001110111110, 109 | 0b001110011100111, 110 | 0b001100111010000, 111 | 0b000011101100010, 112 | 0b000001001010101, 113 | 0b000110100001100, 114 | 0b000100000111011, 115 | ], 116 | ]; 117 | 118 | 119 | /** 120 | * This class encapsulates the four error correction levels defined by the QR code standard. 121 | */ 122 | export default class EccLevel{ 123 | 124 | /** 125 | * The current ECC level value 126 | * 127 | * L: 0b01 128 | * M: 0b00 129 | * Q: 0b11 130 | * H: 0b10 131 | * 132 | * @type {number|int} 133 | * @private 134 | */ 135 | eccLevel; 136 | 137 | /** 138 | * @param {number|int} $eccLevel containing the two bits encoding a QR Code's error correction level 139 | * 140 | * @throws QRCodeException 141 | */ 142 | constructor($eccLevel){ 143 | 144 | if((0b11 & $eccLevel) !== $eccLevel){ 145 | throw new QRCodeException('invalid ECC level'); 146 | } 147 | 148 | this.eccLevel = $eccLevel; 149 | } 150 | 151 | /** 152 | * returns the string representation of the current ECC level 153 | * 154 | * @returns {string} 155 | */ 156 | toString(){ 157 | return PHPJS.array_combine(ECC_LEVELS, ['L', 'M', 'Q', 'H'])[this.eccLevel]; 158 | } 159 | 160 | /** 161 | * returns the current ECC level 162 | * 163 | * @returns {number|int} 164 | */ 165 | getLevel(){ 166 | return this.eccLevel; 167 | } 168 | 169 | /** 170 | * returns the ordinal value of the current ECC level 171 | * 172 | * references to the keys of the following tables: 173 | * 174 | * @see MAX_BITS 175 | * @see FORMAT_PATTERN 176 | * @see RSBLOCKS 177 | */ 178 | getOrdinal(){ 179 | return PHPJS.array_combine(ECC_LEVELS, [0, 1, 2, 3])[this.eccLevel]; 180 | } 181 | 182 | /** 183 | * returns the format pattern for the given $eccLevel and $maskPattern 184 | * 185 | * @param {MaskPattern} $maskPattern 186 | * 187 | * @returns {number|int} 188 | */ 189 | getformatPattern($maskPattern){ 190 | return FORMAT_PATTERN[this.getOrdinal()][$maskPattern.getPattern()]; 191 | } 192 | 193 | /** 194 | * @returns {number[]|int[]} an array with the max bit lengths for version 1-40 and the current ECC level 195 | */ 196 | getMaxBits(){ 197 | let $v = []; 198 | let $c = this.getOrdinal(); 199 | 200 | for(let $k in MAX_BITS){ 201 | $v.push(MAX_BITS[$k][$c]); 202 | } 203 | 204 | return $v; 205 | } 206 | 207 | /** 208 | * Returns the maximum bit length for the given version and current ECC level 209 | * 210 | * @param {Version} $version 211 | * 212 | * @returns {number|int} 213 | */ 214 | getMaxBitsForVersion($version){ 215 | return MAX_BITS[$version.getVersionNumber()][this.getOrdinal()]; 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /src/Common/GF256.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author ZXing Authors 4 | * @author smiley 5 | * @copyright 2022 smiley 6 | * @license Apache-2.0 7 | */ 8 | 9 | import QRCodeException from '../QRCodeException.js'; 10 | /** 11 | * @type {number[]|int[]} 12 | * @private 13 | */ 14 | const logTable = [ 15 | null, // the first value is never returned, index starts at 1 16 | 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 17 | 4, 100, 224, 14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 18 | 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218, 240, 18, 130, 69, 19 | 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 20 | 6, 191, 139, 98, 102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 21 | 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92, 131, 56, 70, 64, 22 | 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 23 | 202, 94, 155, 159, 10, 21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 24 | 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49, 197, 254, 24, 25 | 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 26 | 55, 63, 209, 91, 149, 188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 27 | 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71, 109, 65, 162, 28 | 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 29 | 108, 161, 59, 82, 41, 157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 30 | 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122, 117, 44, 215, 31 | 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175, 32 | ]; 33 | 34 | /** 35 | * @type {number[]|int[]} 36 | * @private 37 | */ 38 | const expTable = [ 39 | 1, 2, 4, 8, 16, 32, 64, 128, 29, 58, 116, 232, 205, 135, 19, 38, 40 | 76, 152, 45, 90, 180, 117, 234, 201, 143, 3, 6, 12, 24, 48, 96, 192, 41 | 157, 39, 78, 156, 37, 74, 148, 53, 106, 212, 181, 119, 238, 193, 159, 35, 42 | 70, 140, 5, 10, 20, 40, 80, 160, 93, 186, 105, 210, 185, 111, 222, 161, 43 | 95, 190, 97, 194, 153, 47, 94, 188, 101, 202, 137, 15, 30, 60, 120, 240, 44 | 253, 231, 211, 187, 107, 214, 177, 127, 254, 225, 223, 163, 91, 182, 113, 226, 45 | 217, 175, 67, 134, 17, 34, 68, 136, 13, 26, 52, 104, 208, 189, 103, 206, 46 | 129, 31, 62, 124, 248, 237, 199, 147, 59, 118, 236, 197, 151, 51, 102, 204, 47 | 133, 23, 46, 92, 184, 109, 218, 169, 79, 158, 33, 66, 132, 21, 42, 84, 48 | 168, 77, 154, 41, 82, 164, 85, 170, 73, 146, 57, 114, 228, 213, 183, 115, 49 | 230, 209, 191, 99, 198, 145, 63, 126, 252, 229, 215, 179, 123, 246, 241, 255, 50 | 227, 219, 171, 75, 150, 49, 98, 196, 149, 55, 110, 220, 165, 87, 174, 65, 51 | 130, 25, 50, 100, 200, 141, 7, 14, 28, 56, 112, 224, 221, 167, 83, 166, 52 | 81, 162, 89, 178, 121, 242, 249, 239, 195, 155, 43, 86, 172, 69, 138, 9, 53 | 18, 36, 72, 144, 61, 122, 244, 245, 247, 243, 251, 235, 203, 139, 11, 22, 54 | 44, 88, 176, 125, 250, 233, 207, 131, 27, 54, 108, 216, 173, 71, 142, 1, 55 | ]; 56 | 57 | 58 | /** 59 | * This class contains utility methods for performing mathematical operations over 60 | * the Galois Fields. Operations use a given primitive polynomial in calculations. 61 | * 62 | * Throughout this package, elements of the GF are represented as an int 63 | * for convenience and speed (but at the cost of memory). 64 | * 65 | * 66 | * @author Sean Owen 67 | * @author David Olivier 68 | */ 69 | export default class GF256{ 70 | 71 | /** 72 | * @param {number|int} $a 73 | * 74 | * @returns {number|int} 2 to the power of a in GF(size) 75 | */ 76 | static exp($a){ 77 | 78 | if($a < 0){ 79 | $a += 255; 80 | } 81 | else if($a >= 256){ 82 | $a -= 255; 83 | } 84 | 85 | return expTable[$a]; 86 | } 87 | 88 | /** 89 | * @param {number|int} $a 90 | * 91 | * @returns {number|int} base 2 log of a in GF(size) 92 | * @throws QRCodeException 93 | */ 94 | static log($a){ 95 | 96 | if($a < 1){ 97 | throw new QRCodeException('$a < 1'); 98 | } 99 | 100 | return logTable[$a]; 101 | } 102 | 103 | /** 104 | * @param {number|int} $a 105 | * @param {number|int} $b 106 | * 107 | * @return int product of a and b in GF(size) 108 | */ 109 | static multiply($a, $b){ 110 | 111 | if($a === 0 || $b === 0){ 112 | return 0; 113 | } 114 | 115 | return expTable[(logTable[$a] + logTable[$b]) % 255]; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Common/GenericGFPoly.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @author ZXing Authors 5 | * @copyright 2022 smiley 6 | * @license Apache-2.0 7 | */ 8 | 9 | import QRCodeException from '../QRCodeException.js'; 10 | import PHPJS from './PHPJS.js'; 11 | import GF256 from './GF256.js'; 12 | 13 | /** 14 | * Represents a polynomial whose coefficients are elements of a GF. 15 | * Instances of this class are immutable. 16 | * 17 | * Much credit is due to William Rucklidge since portions of this code are an indirect 18 | * port of his C++ Reed-Solomon implementation. 19 | * 20 | * @author Sean Owen 21 | */ 22 | export default class GenericGFPoly{ 23 | 24 | /** 25 | * @type {number[]|int[]} 26 | * @private 27 | */ 28 | coefficients = []; 29 | 30 | /** 31 | * @param {number[]|int[]} $coefficients array coefficients as ints representing elements of GF(size), arranged 32 | * from most significant (highest-power term) coefficient to least significant 33 | * @param {number|int|null} $degree 34 | * 35 | * @throws QRCodeException if argument is null or empty, or if leading coefficient is 0 and this 36 | * is not a constant polynomial (that is, it is not the monomial "0") 37 | */ 38 | constructor($coefficients, $degree = null){ 39 | $degree ??= 0; 40 | 41 | if(!$coefficients || !$coefficients.length){ 42 | throw new QRCodeException('arg $coefficients is empty'); 43 | } 44 | 45 | if($degree < 0){ 46 | throw new QRCodeException('negative degree'); 47 | } 48 | 49 | let $coefficientsLength = $coefficients.length; 50 | 51 | // Leading term must be non-zero for anything except the constant polynomial "0" 52 | let $firstNonZero = 0; 53 | 54 | while($firstNonZero < $coefficientsLength && $coefficients[$firstNonZero] === 0){ 55 | $firstNonZero++; 56 | } 57 | 58 | if($firstNonZero === $coefficientsLength){ 59 | this.coefficients = [0]; 60 | } 61 | else{ 62 | this.coefficients = PHPJS.array_fill($coefficientsLength - $firstNonZero + $degree, 0); 63 | 64 | for(let $i = 0; $i < $coefficientsLength - $firstNonZero; $i++){ 65 | this.coefficients[$i] = $coefficients[$i + $firstNonZero]; 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * @param {number|int} $degree 72 | * 73 | * @returns {number|int} $coefficient of x^degree term in this polynomial 74 | */ 75 | getCoefficient($degree){ 76 | return this.coefficients[this.coefficients.length - 1 - $degree]; 77 | } 78 | 79 | /** 80 | * @returns {number[]|int[]} 81 | */ 82 | getCoefficients(){ 83 | return this.coefficients; 84 | } 85 | 86 | /** 87 | * @returns {number|int} $degree of this polynomial 88 | */ 89 | getDegree(){ 90 | return this.coefficients.length - 1; 91 | } 92 | 93 | /** 94 | * @returns {boolean} true if this polynomial is the monomial "0" 95 | */ 96 | isZero(){ 97 | return this.coefficients[0] === 0; 98 | } 99 | 100 | /** 101 | * @param {GenericGFPoly} $other 102 | * 103 | * @returns {GenericGFPoly} 104 | */ 105 | multiply($other){ 106 | 107 | if(this.isZero() || $other.isZero()){ 108 | return new GenericGFPoly([0]); 109 | } 110 | 111 | let $product = PHPJS.array_fill(this.coefficients.length + $other.coefficients.length - 1, 0); 112 | 113 | for(let $i = 0; $i < this.coefficients.length; $i++){ 114 | for(let $j = 0; $j < $other.coefficients.length; $j++){ 115 | $product[$i + $j] ^= GF256.multiply(this.coefficients[$i], $other.coefficients[$j]); 116 | 117 | } 118 | } 119 | 120 | return new GenericGFPoly($product); 121 | } 122 | 123 | /** 124 | * @param {GenericGFPoly} $other 125 | * 126 | * @returns {GenericGFPoly} 127 | */ 128 | mod($other){ 129 | 130 | if(this.coefficients.length - $other.coefficients.length < 0){ 131 | return this; 132 | } 133 | 134 | let $ratio = GF256.log(this.coefficients[0]) - GF256.log($other.coefficients[0]); 135 | 136 | for(let $i = 0; $i < $other.coefficients.length; $i++){ 137 | this.coefficients[$i] ^= GF256.exp(GF256.log($other.coefficients[$i]) + $ratio); 138 | } 139 | 140 | return (new GenericGFPoly(this.coefficients)).mod($other); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/Common/MaskPattern.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author ZXing Authors 4 | * @author smiley 5 | * @copyright 2022 smiley 6 | * @license Apache-2.0 7 | */ 8 | 9 | import PHPJS from './PHPJS.js'; 10 | import QRCodeException from '../QRCodeException.js'; 11 | import {PATTERNS} from './constants.js'; 12 | 13 | /* 14 | * Penalty scores 15 | * 16 | * ISO/IEC 18004:2000 Section 8.8.1 - Table 24 17 | */ 18 | const PENALTY_N1 = 3; 19 | const PENALTY_N2 = 3; 20 | const PENALTY_N3 = 40; 21 | const PENALTY_N4 = 10; 22 | 23 | 24 | /** 25 | * ISO/IEC 18004:2000 Section 8.8.1 26 | * ISO/IEC 18004:2000 Section 8.8.2 - Evaluation of masking results 27 | * 28 | * @see http://www.thonky.com/qr-code-tutorial/data-masking 29 | * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java 30 | */ 31 | export default class MaskPattern{ 32 | 33 | /** 34 | * The current mask pattern value (0-7) 35 | * 36 | * @type {number|int} 37 | * @private 38 | */ 39 | maskPattern; 40 | 41 | /** 42 | * MaskPattern constructor. 43 | * 44 | * @param {number|int} $maskPattern 45 | * 46 | * @throws QRCodeException 47 | */ 48 | constructor($maskPattern){ 49 | $maskPattern = PHPJS.intval($maskPattern); 50 | 51 | if(($maskPattern & 0b111) !== $maskPattern){ 52 | throw new QRCodeException(`invalid mask pattern: "${$maskPattern}"`); 53 | } 54 | 55 | this.maskPattern = $maskPattern; 56 | } 57 | 58 | /** 59 | * Returns the current mask pattern 60 | * 61 | * @returns {number|int} 62 | */ 63 | getPattern(){ 64 | return this.maskPattern; 65 | } 66 | 67 | /** 68 | * Returns a closure that applies the mask for the chosen mask pattern. 69 | * 70 | * Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position 71 | * and j is row position. In fact, as the text says, i is row position and j is column position. 72 | * 73 | * @see https://www.thonky.com/qr-code-tutorial/mask-patterns 74 | * @see https://github.com/zxing/zxing/blob/e9e2bd280bcaeabd59d0f955798384fe6c018a6c/core/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java#L32-L117 75 | * 76 | * @returns {Function} 77 | */ 78 | getMask(){ 79 | // $x = column (width), $y = row (height) 80 | return PHPJS.array_combine(PATTERNS, [ 81 | ($x, $y) => (($x + $y) % 2) === 0, 82 | ($x, $y) => ($y % 2) === 0, 83 | ($x, $y) => ($x % 3) === 0, 84 | ($x, $y) => (($x + $y) % 3) === 0, 85 | ($x, $y) => ((PHPJS.intval($y / 2) + PHPJS.intval($x / 3)) % 2) === 0, 86 | ($x, $y) => ($x * $y) % 6 === 0, 87 | ($x, $y) => (($x * $y) % 6) < 3, 88 | ($x, $y) => (($x + $y + (($x * $y) % 3)) % 2) === 0, 89 | ])[this.maskPattern]; 90 | } 91 | 92 | /** 93 | * Evaluates the matrix of the given data interface and returns a new mask pattern instance for the best result 94 | * 95 | * @param {QRMatrix} $QRMatrix 96 | * 97 | * @returns {MaskPattern} 98 | */ 99 | static getBestPattern($QRMatrix){ 100 | let $penalties = []; 101 | let $size = $QRMatrix.getSize(); 102 | 103 | for(let $pattern of PATTERNS){ 104 | let $penalty = 0; 105 | let $mp = new MaskPattern($pattern); 106 | let $matrix = PHPJS.clone($QRMatrix); 107 | // because js is fucking dumb, it can't even properly clone THAT ONE FUCKING ARRAY WITHOUT LEAVING REFERENCES 108 | $matrix._matrix = structuredClone($QRMatrix._matrix); 109 | let $m = $matrix.setFormatInfo($mp).mask($mp).getMatrix(true); 110 | 111 | for(let $level = 1; $level <= 4; $level++){ 112 | $penalty += this['testRule' + $level]($m, $size, $size); 113 | } 114 | 115 | $penalties[$pattern] = PHPJS.intval($penalty); 116 | } 117 | 118 | return new MaskPattern($penalties.indexOf(Math.min(...$penalties))); 119 | } 120 | 121 | /** 122 | * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and 123 | * give penalty to them. Example: 00000 or 11111. 124 | * 125 | * @param {Array} $matrix 126 | * @param {number|int} $height 127 | * @param {number|int} $width 128 | * 129 | * @returns {number|int} 130 | */ 131 | static testRule1($matrix, $height, $width){ 132 | let $penalty = 0; 133 | 134 | // horizontal 135 | for(let $y = 0; $y < $height; $y++){ 136 | $penalty += MaskPattern.applyRule1($matrix[$y]); 137 | } 138 | 139 | // vertical 140 | for(let $x = 0; $x < $width; $x++){ 141 | $penalty += MaskPattern.applyRule1($matrix.map(y => y[$x])); 142 | } 143 | 144 | return $penalty; 145 | } 146 | 147 | /** 148 | * @param {Array} $rc 149 | * 150 | * @returns {number|int} 151 | * @private 152 | */ 153 | static applyRule1($rc){ 154 | let $penalty = 0; 155 | let $numSameBitCells = 0; 156 | let $prevBit = null; 157 | 158 | for(let $val of $rc){ 159 | 160 | if($val === $prevBit){ 161 | $numSameBitCells++; 162 | } 163 | else{ 164 | 165 | if($numSameBitCells >= 5){ 166 | $penalty += (PENALTY_N1 + $numSameBitCells - 5); 167 | } 168 | 169 | $numSameBitCells = 1; // Include the cell itself. 170 | $prevBit = $val; 171 | } 172 | } 173 | 174 | if($numSameBitCells >= 5){ 175 | $penalty += (PENALTY_N1 + $numSameBitCells - 5); 176 | } 177 | 178 | return $penalty; 179 | } 180 | 181 | /** 182 | * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give 183 | * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a 184 | * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block. 185 | * 186 | * @param {Array} $matrix 187 | * @param {number|int} $height 188 | * @param {number|int} $width 189 | * 190 | * @returns {number|int} 191 | */ 192 | static testRule2($matrix, $height, $width){ 193 | let $penalty = 0; 194 | 195 | for(let $y = 0; $y < $height; $y++){ 196 | 197 | if($y > $height - 2){ 198 | break; 199 | } 200 | 201 | for(let $x = 0; $x < $width; $x++){ 202 | 203 | if($x > $width - 2){ 204 | break; 205 | } 206 | 207 | let $val = $matrix[$y][$x]; 208 | 209 | if( 210 | $val === $matrix[$y][$x + 1] 211 | && $val === $matrix[$y + 1][$x] 212 | && $val === $matrix[$y + 1][$x + 1] 213 | ){ 214 | $penalty++; 215 | } 216 | } 217 | } 218 | 219 | return PENALTY_N2 * $penalty; 220 | } 221 | 222 | /** 223 | * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4 224 | * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we 225 | * find patterns like 000010111010000, we give penalty once. 226 | * 227 | * @param {Array} $matrix 228 | * @param {number|int} $height 229 | * @param {number|int} $width 230 | * 231 | * @returns {number|int} 232 | */ 233 | static testRule3($matrix, $height, $width){ 234 | let $penalties = 0; 235 | 236 | for(let $y = 0; $y < $height; $y++){ 237 | for(let $x = 0; $x < $width; $x++){ 238 | 239 | if( 240 | $x + 6 < $width 241 | && $matrix[$y][$x] === true 242 | && !$matrix[$y][($x + 1)] 243 | && $matrix[$y][($x + 2)] 244 | && $matrix[$y][($x + 3)] 245 | && $matrix[$y][($x + 4)] 246 | && !$matrix[$y][($x + 5)] 247 | && $matrix[$y][($x + 6)] 248 | && ( 249 | MaskPattern.isWhiteHorizontal($matrix, $width, $y, $x - 4, $x) 250 | || MaskPattern.isWhiteHorizontal($matrix, $width, $y, $x + 7, $x + 11) 251 | ) 252 | ){ 253 | $penalties++; 254 | } 255 | 256 | if( 257 | $y + 6 < $height 258 | && $matrix[$y][$x] === true 259 | && !$matrix[($y + 1)][$x] 260 | && $matrix[($y + 2)][$x] 261 | && $matrix[($y + 3)][$x] 262 | && $matrix[($y + 4)][$x] 263 | && !$matrix[($y + 5)][$x] 264 | && $matrix[($y + 6)][$x] 265 | && ( 266 | MaskPattern.isWhiteVertical($matrix, $height, $x, $y - 4, $y) 267 | || MaskPattern.isWhiteVertical($matrix, $height, $x, $y + 7, $y + 11) 268 | ) 269 | ){ 270 | $penalties++; 271 | } 272 | 273 | } 274 | } 275 | 276 | return $penalties * PENALTY_N3; 277 | } 278 | 279 | /** 280 | * @param {Array} $matrix 281 | * @param {number|int} $width 282 | * @param {number|int} $y 283 | * @param {number|int} $from 284 | * @param {number|int} $to 285 | * 286 | * @returns {boolean} 287 | * @private 288 | */ 289 | static isWhiteHorizontal($matrix, $width, $y, $from, $to){ 290 | 291 | if($from < 0 || $width < $to){ 292 | return false; 293 | } 294 | 295 | for(let $x = $from; $x < $to; $x++){ 296 | if($matrix[$y][$x] === true){ 297 | return false; 298 | } 299 | } 300 | 301 | return true; 302 | } 303 | 304 | /** 305 | * @param {Array} $matrix 306 | * @param {number|int} $height 307 | * @param {number|int} $x 308 | * @param {number|int} $from 309 | * @param {number|int} $to 310 | * 311 | * @returns {boolean} 312 | * @private 313 | */ 314 | static isWhiteVertical($matrix, $height, $x, $from, $to){ 315 | 316 | if($from < 0 || $height < $to){ 317 | return false; 318 | } 319 | 320 | for(let $y = $from; $y < $to; $y++){ 321 | if($matrix[$y][$x] === true){ 322 | return false; 323 | } 324 | } 325 | 326 | return true; 327 | } 328 | 329 | /** 330 | * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give 331 | * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance. 332 | * 333 | * @param {Array} $matrix 334 | * @param {number|int} $height 335 | * @param {number|int} $width 336 | * 337 | * @returns {number|int} 338 | */ 339 | static testRule4($matrix, $height, $width){ 340 | let $darkCells = 0; 341 | let $totalCells = $height * $width; 342 | 343 | for(let $row of $matrix){ 344 | for(let $val of $row){ 345 | if($val === true){ 346 | $darkCells++; 347 | } 348 | } 349 | } 350 | 351 | return PHPJS.intval((Math.abs($darkCells * 2 - $totalCells) * 10 / $totalCells)) * PENALTY_N4; 352 | } 353 | 354 | } 355 | -------------------------------------------------------------------------------- /src/Common/Mode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import PHPJS from './PHPJS.js'; 9 | import QRCodeException from '../QRCodeException.js'; 10 | import {MODES} from './constants.js'; 11 | 12 | /** 13 | * mode length bits for the version breakpoints 1-9, 10-26 and 27-40 14 | * 15 | * ISO/IEC 18004:2000 Table 3 - Number of bits in Character Count Indicator 16 | */ 17 | const MODE_LENGTH_BITS = PHPJS.array_combine(MODES, [ 18 | [10, 12, 14], // Numeric 19 | [9, 11, 13], // AlphaNum 20 | [8, 16, 16], // Byte 21 | ]); 22 | 23 | /** 24 | * ISO 18004:2006, 6.4.1, Tables 2 and 3 25 | */ 26 | export default class Mode{ 27 | 28 | /** 29 | * returns the length bits for the version breakpoints 1-9, 10-26 and 27-40 30 | * 31 | * @param {number|int} $mode 32 | * @param {number|int} $version 33 | * 34 | * @returns {number|int} 35 | * @throws QRCodeException 36 | */ 37 | static getLengthBitsForVersion($mode, $version){ 38 | 39 | if(!MODE_LENGTH_BITS[$mode]){ 40 | throw new QRCodeException('invalid mode given'); 41 | } 42 | 43 | let $minVersion = 0; 44 | let $breakpoints = [9, 26, 40]; 45 | 46 | for(let $key = 0; $key < $breakpoints.length; $key++){ 47 | let $breakpoint = $breakpoints[$key]; 48 | 49 | if($version > $minVersion && $version <= $breakpoint){ 50 | return MODE_LENGTH_BITS[$mode][$key]; 51 | } 52 | 53 | $minVersion = $breakpoint; 54 | } 55 | 56 | throw new QRCodeException('invalid version number: ' + $version); 57 | } 58 | 59 | /** 60 | * returns the array of length bits for the given mode 61 | * 62 | * @param {number|int} $mode 63 | */ 64 | static getLengthBitsForMode($mode){ 65 | 66 | if(PHPJS.isset(() => MODE_LENGTH_BITS[$mode])){ 67 | return MODE_LENGTH_BITS[$mode]; 68 | } 69 | 70 | throw new QRCodeException('invalid mode given'); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/Common/PHPJS.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | export default class PHPJS{ 9 | 10 | /** 11 | * not an exact implementation, we're ignoring the $start_index parameter, which is always 0 here 12 | * 13 | * @param {number|int} $count 14 | * @param {*} $value 15 | * @returns {Array} 16 | */ 17 | static array_fill($count, $value){ 18 | let arr = []; 19 | 20 | for(let key = 0; key < $count; key++){ 21 | arr[key] = structuredClone($value); 22 | } 23 | 24 | return arr; 25 | } 26 | 27 | /** 28 | * Checks to see if a value in a nested array is set. 29 | * isset(() => some.nested.value) 30 | * 31 | * @link https://stackoverflow.com/a/46256973 32 | * 33 | * @param {Function} $accessor Function that returns our value 34 | */ 35 | static isset($accessor){ 36 | try{ 37 | return typeof $accessor() !== 'undefined'; 38 | } 39 | catch(e){ 40 | return false; 41 | } 42 | } 43 | 44 | /** 45 | * @link http://locutus.io/php/var/intval/ 46 | * 47 | * @param {*} $var 48 | * @param {number|null} $base 49 | * @returns {number|int} 50 | */ 51 | static intval($var, $base = null){ 52 | let tmp; 53 | let type = typeof($var); 54 | 55 | if(type === 'boolean'){ 56 | return +$var; 57 | } 58 | 59 | if(type === 'string'){ 60 | tmp = parseInt($var, $base || 10); 61 | 62 | return (isNaN(tmp) || !isFinite(tmp)) ? 0 : tmp; 63 | } 64 | 65 | if(type === 'number' && isFinite($var)){ 66 | return $var|0; 67 | } 68 | 69 | return 0; 70 | } 71 | 72 | /** 73 | * @link https://locutus.io/php/array/array_combine/ 74 | * 75 | * @param {Array} keys 76 | * @param {Array} values 77 | * @returns {Object<{}>|boolean} 78 | */ 79 | static array_combine(keys, values){ 80 | let newArray = {}; 81 | let i = 0; 82 | // input sanitation 83 | if( 84 | // Only accept arrays or array-like objects 85 | typeof keys !== 'object' 86 | || typeof values !== 'object' 87 | // Require arrays to have a count 88 | || typeof keys.length !== 'number' 89 | || typeof values.length !== 'number' 90 | || !keys.length 91 | // number of elements does not match 92 | || keys.length !== values.length 93 | ){ 94 | return false; 95 | } 96 | 97 | for(i = 0; i < keys.length; i++){ 98 | newArray[keys[i]] = values[i]; 99 | } 100 | 101 | return newArray; 102 | } 103 | 104 | /** 105 | * @link https://locutus.io/php/strings/ord 106 | * 107 | * @param {string} $string 108 | * @returns {number|int} 109 | */ 110 | static ord($string){ 111 | $string += ''; // make sure we have a string 112 | let code = $string.charCodeAt(0); 113 | 114 | if(code >= 0xD800 && code <= 0xDBFF){ 115 | // High surrogate (could change last hex to 0xDB7F to treat 116 | // high private surrogates as single characters) 117 | let hi = code; 118 | 119 | if($string.length === 1){ 120 | // This is just a high surrogate with no following low surrogate, 121 | // so we return its value; 122 | return code; 123 | // we could also throw an error as it is not a complete character, 124 | // but someone may want to know 125 | } 126 | 127 | return ((hi - 0xD800) * 0x400) + ($string.charCodeAt(1) - 0xDC00) + 0x10000; 128 | } 129 | 130 | if(code >= 0xDC00 && code <= 0xDFFF){ 131 | // Low surrogate 132 | // This is just a low surrogate with no preceding high surrogate, 133 | // so we return its value; 134 | return code; 135 | // we could also throw an error as it is not a complete character, 136 | // but someone may want to know 137 | } 138 | 139 | return code; 140 | } 141 | 142 | /** 143 | * @link https://www.php.net/manual/en/language.oop5.cloning.php 144 | * 145 | * because javascript is dumb (have I mentioned it yet??) we still cannot properly 1:1 clone objects in 2024. 146 | * structuredClone() suggest that but in fact it does not. so we have to invoke a new instance of the class, 147 | * and copy over the properties from the object we want to clone - could have done that by hand entirely... 148 | * 149 | * @param {Object.<*>} $object 150 | * @returns {Object.<*>} 151 | */ 152 | static clone($object){ 153 | let $dummy = Object.create(Object.getPrototypeOf($object)); 154 | 155 | return Object.assign($dummy, $object); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/Common/ReedSolomonEncoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import PHPJS from './PHPJS.js'; 9 | import GenericGFPoly from './GenericGFPoly.js'; 10 | import GF256 from './GF256.js'; 11 | 12 | /** 13 | * ISO/IEC 18004:2000 Section 8.5 ff 14 | * 15 | * @see http://www.thonky.com/qr-code-tutorial/error-correction-coding 16 | */ 17 | export default class ReedSolomonEncoder{ 18 | 19 | /** 20 | * @type {Object<{}>} 21 | * @private 22 | */ 23 | interleavedData; 24 | 25 | /** 26 | * @type {number|int} 27 | * @private 28 | */ 29 | interleavedDataIndex; 30 | 31 | /** 32 | * @type {Version} $version 33 | */ 34 | version; 35 | 36 | /** 37 | * @type {EccLevel} $eccLevel 38 | */ 39 | eccLevel; 40 | 41 | /** 42 | * @param {Version} $version 43 | * @param {EccLevel} $eccLevel 44 | */ 45 | constructor($version, $eccLevel){ 46 | this.version = $version; 47 | this.eccLevel = $eccLevel; 48 | } 49 | 50 | /** 51 | * ECC interleaving 52 | * 53 | * @param {BitBuffer} $bitBuffer 54 | * 55 | * @returns {Object<{}>} 56 | * @throws QRCodeException 57 | */ 58 | interleaveEcBytes($bitBuffer){ 59 | let $rsblockData = this.version.getRSBlocks(this.eccLevel); 60 | let $numEccCodewords = $rsblockData[0]; 61 | let $l1 = $rsblockData[1][0][0]; 62 | let $b1 = $rsblockData[1][0][1]; 63 | let $l2 = $rsblockData[1][1][0]; 64 | let $b2 = $rsblockData[1][1][1]; 65 | let $rsBlocks = PHPJS.array_fill($l1, [$numEccCodewords + $b1, $b1]); 66 | 67 | if($l2 > 0){ 68 | $rsBlocks = $rsBlocks.concat(PHPJS.array_fill($l2, [$numEccCodewords + $b2, $b2])); 69 | } 70 | 71 | let $bitBufferData = $bitBuffer.getBuffer(); 72 | let $dataBytes = []; 73 | let $ecBytes = []; 74 | let $maxDataBytes = 0; 75 | let $maxEcBytes = 0; 76 | let $dataByteOffset = 0; 77 | 78 | for(let $key in $rsBlocks){ 79 | let $rsBlockTotal = $rsBlocks[$key][0]; 80 | let $dataByteCount = $rsBlocks[$key][1]; 81 | 82 | $dataBytes[$key] = []; 83 | 84 | for(let $i = 0; $i < $dataByteCount; $i++){ 85 | $dataBytes[$key][$i] = $bitBufferData[$i + $dataByteOffset] & 0xff; 86 | } 87 | 88 | let $ecByteCount = $rsBlockTotal - $dataByteCount; 89 | $ecBytes[$key] = this.encode($dataBytes[$key], $ecByteCount); 90 | $maxDataBytes = Math.max($maxDataBytes, $dataByteCount); 91 | $maxEcBytes = Math.max($maxEcBytes, $ecByteCount); 92 | $dataByteOffset += $dataByteCount; 93 | } 94 | 95 | this.interleavedData = PHPJS.array_fill(this.version.getTotalCodewords(), 0); 96 | this.interleavedDataIndex = 0; 97 | let $numRsBlocks = $l1 + $l2; 98 | 99 | this.interleave($dataBytes, $maxDataBytes, $numRsBlocks); 100 | this.interleave($ecBytes, $maxEcBytes, $numRsBlocks); 101 | 102 | return this.interleavedData; 103 | } 104 | 105 | /** 106 | * @param {Array} $dataBytes 107 | * @param {number|int} $ecByteCount 108 | * 109 | * @returns {Object<{}>} 110 | * @private 111 | */ 112 | encode($dataBytes, $ecByteCount){ 113 | let $rsPoly = new GenericGFPoly([1]); 114 | 115 | for(let $i = 0; $i < $ecByteCount; $i++){ 116 | $rsPoly = $rsPoly.multiply(new GenericGFPoly([1, GF256.exp($i)])); 117 | } 118 | 119 | let $rsPolyDegree = $rsPoly.getDegree(); 120 | let $modCoefficients = (new GenericGFPoly($dataBytes, $rsPolyDegree)) 121 | .mod($rsPoly) 122 | .getCoefficients() 123 | ; 124 | 125 | let $ecBytes = PHPJS.array_fill($rsPolyDegree, 0); 126 | let $count = $modCoefficients.length - $rsPolyDegree; 127 | 128 | for(let $i = 0; $i < $ecBytes.length; $i++){ 129 | let $modIndex = $i + $count; 130 | $ecBytes[$i] = $modIndex >= 0 ? $modCoefficients[$modIndex] : 0; 131 | } 132 | 133 | return $ecBytes; 134 | } 135 | 136 | /** 137 | * @param {Object<{}>} $byteArray 138 | * @param {number|int} $maxBytes 139 | * @param {number|int} $numRsBlocks 140 | * 141 | * @returns {void} 142 | * @private 143 | */ 144 | interleave($byteArray, $maxBytes, $numRsBlocks){ 145 | for(let $x = 0; $x < $maxBytes; $x++){ 146 | for(let $y = 0; $y < $numRsBlocks; $y++){ 147 | if($x < $byteArray[$y].length){ 148 | this.interleavedData[this.interleavedDataIndex++] = $byteArray[$y][$x]; 149 | } 150 | } 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/Common/Version.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QRCodeException from '../QRCodeException.js'; 9 | 10 | /** 11 | * ISO/IEC 18004:2000 Annex E, Table E.1 - Row/column coordinates of center module of Alignment Patterns 12 | * 13 | * version -> pattern 14 | * 15 | * @type {number[][]|int[][]} 16 | * @private 17 | */ 18 | const ALIGNMENT_PATTERN = [ 19 | null, // version count starts at 1 20 | [], 21 | [6, 18], 22 | [6, 22], 23 | [6, 26], 24 | [6, 30], 25 | [6, 34], 26 | [6, 22, 38], 27 | [6, 24, 42], 28 | [6, 26, 46], 29 | [6, 28, 50], 30 | [6, 30, 54], 31 | [6, 32, 58], 32 | [6, 34, 62], 33 | [6, 26, 46, 66], 34 | [6, 26, 48, 70], 35 | [6, 26, 50, 74], 36 | [6, 30, 54, 78], 37 | [6, 30, 56, 82], 38 | [6, 30, 58, 86], 39 | [6, 34, 62, 90], 40 | [6, 28, 50, 72, 94], 41 | [6, 26, 50, 74, 98], 42 | [6, 30, 54, 78, 102], 43 | [6, 28, 54, 80, 106], 44 | [6, 32, 58, 84, 110], 45 | [6, 30, 58, 86, 114], 46 | [6, 34, 62, 90, 118], 47 | [6, 26, 50, 74, 98, 122], 48 | [6, 30, 54, 78, 102, 126], 49 | [6, 26, 52, 78, 104, 130], 50 | [6, 30, 56, 82, 108, 134], 51 | [6, 34, 60, 86, 112, 138], 52 | [6, 30, 58, 86, 114, 142], 53 | [6, 34, 62, 90, 118, 146], 54 | [6, 30, 54, 78, 102, 126, 150], 55 | [6, 24, 50, 76, 102, 128, 154], 56 | [6, 28, 54, 80, 106, 132, 158], 57 | [6, 32, 58, 84, 110, 136, 162], 58 | [6, 26, 54, 82, 110, 138, 166], 59 | [6, 30, 58, 86, 114, 142, 170], 60 | ]; 61 | 62 | /** 63 | * ISO/IEC 18004:2000 Annex D, Table D.1 - Version information bit stream for each version 64 | * 65 | * no version pattern for QR Codes < 7 66 | * 67 | * @type {number[]|int[]} 68 | * @private 69 | */ 70 | const VERSION_PATTERN = [ 71 | null, null, null, null, null, null, null, // no version info patterns < version 7 72 | 0b000111110010010100, 73 | 0b001000010110111100, 74 | 0b001001101010011001, 75 | 0b001010010011010011, 76 | 0b001011101111110110, 77 | 0b001100011101100010, 78 | 0b001101100001000111, 79 | 0b001110011000001101, 80 | 0b001111100100101000, 81 | 0b010000101101111000, 82 | 0b010001010001011101, 83 | 0b010010101000010111, 84 | 0b010011010100110010, 85 | 0b010100100110100110, 86 | 0b010101011010000011, 87 | 0b010110100011001001, 88 | 0b010111011111101100, 89 | 0b011000111011000100, 90 | 0b011001000111100001, 91 | 0b011010111110101011, 92 | 0b011011000010001110, 93 | 0b011100110000011010, 94 | 0b011101001100111111, 95 | 0b011110110101110101, 96 | 0b011111001001010000, 97 | 0b100000100111010101, 98 | 0b100001011011110000, 99 | 0b100010100010111010, 100 | 0b100011011110011111, 101 | 0b100100101100001011, 102 | 0b100101010000101110, 103 | 0b100110101001100100, 104 | 0b100111010101000001, 105 | 0b101000110001101001, 106 | ]; 107 | 108 | /** 109 | * ISO/IEC 18004:2000 Tables 13-22 110 | * 111 | * @see http://www.thonky.com/qr-code-tutorial/error-correction-table 112 | * 113 | * @type {Array} 114 | * @private 115 | */ 116 | const RSBLOCKS = [ 117 | null, // version count starts at 1 118 | [[ 7, [[ 1, 19], [ 0, 0]]], [10, [[ 1, 16], [ 0, 0]]], [13, [[ 1, 13], [ 0, 0]]], [17, [[ 1, 9], [ 0, 0]]]], 119 | [[10, [[ 1, 34], [ 0, 0]]], [16, [[ 1, 28], [ 0, 0]]], [22, [[ 1, 22], [ 0, 0]]], [28, [[ 1, 16], [ 0, 0]]]], 120 | [[15, [[ 1, 55], [ 0, 0]]], [26, [[ 1, 44], [ 0, 0]]], [18, [[ 2, 17], [ 0, 0]]], [22, [[ 2, 13], [ 0, 0]]]], 121 | [[20, [[ 1, 80], [ 0, 0]]], [18, [[ 2, 32], [ 0, 0]]], [26, [[ 2, 24], [ 0, 0]]], [16, [[ 4, 9], [ 0, 0]]]], 122 | [[26, [[ 1, 108], [ 0, 0]]], [24, [[ 2, 43], [ 0, 0]]], [18, [[ 2, 15], [ 2, 16]]], [22, [[ 2, 11], [ 2, 12]]]], 123 | [[18, [[ 2, 68], [ 0, 0]]], [16, [[ 4, 27], [ 0, 0]]], [24, [[ 4, 19], [ 0, 0]]], [28, [[ 4, 15], [ 0, 0]]]], 124 | [[20, [[ 2, 78], [ 0, 0]]], [18, [[ 4, 31], [ 0, 0]]], [18, [[ 2, 14], [ 4, 15]]], [26, [[ 4, 13], [ 1, 14]]]], 125 | [[24, [[ 2, 97], [ 0, 0]]], [22, [[ 2, 38], [ 2, 39]]], [22, [[ 4, 18], [ 2, 19]]], [26, [[ 4, 14], [ 2, 15]]]], 126 | [[30, [[ 2, 116], [ 0, 0]]], [22, [[ 3, 36], [ 2, 37]]], [20, [[ 4, 16], [ 4, 17]]], [24, [[ 4, 12], [ 4, 13]]]], 127 | [[18, [[ 2, 68], [ 2, 69]]], [26, [[ 4, 43], [ 1, 44]]], [24, [[ 6, 19], [ 2, 20]]], [28, [[ 6, 15], [ 2, 16]]]], 128 | [[20, [[ 4, 81], [ 0, 0]]], [30, [[ 1, 50], [ 4, 51]]], [28, [[ 4, 22], [ 4, 23]]], [24, [[ 3, 12], [ 8, 13]]]], 129 | [[24, [[ 2, 92], [ 2, 93]]], [22, [[ 6, 36], [ 2, 37]]], [26, [[ 4, 20], [ 6, 21]]], [28, [[ 7, 14], [ 4, 15]]]], 130 | [[26, [[ 4, 107], [ 0, 0]]], [22, [[ 8, 37], [ 1, 38]]], [24, [[ 8, 20], [ 4, 21]]], [22, [[12, 11], [ 4, 12]]]], 131 | [[30, [[ 3, 115], [ 1, 116]]], [24, [[ 4, 40], [ 5, 41]]], [20, [[11, 16], [ 5, 17]]], [24, [[11, 12], [ 5, 13]]]], 132 | [[22, [[ 5, 87], [ 1, 88]]], [24, [[ 5, 41], [ 5, 42]]], [30, [[ 5, 24], [ 7, 25]]], [24, [[11, 12], [ 7, 13]]]], 133 | [[24, [[ 5, 98], [ 1, 99]]], [28, [[ 7, 45], [ 3, 46]]], [24, [[15, 19], [ 2, 20]]], [30, [[ 3, 15], [13, 16]]]], 134 | [[28, [[ 1, 107], [ 5, 108]]], [28, [[10, 46], [ 1, 47]]], [28, [[ 1, 22], [15, 23]]], [28, [[ 2, 14], [17, 15]]]], 135 | [[30, [[ 5, 120], [ 1, 121]]], [26, [[ 9, 43], [ 4, 44]]], [28, [[17, 22], [ 1, 23]]], [28, [[ 2, 14], [19, 15]]]], 136 | [[28, [[ 3, 113], [ 4, 114]]], [26, [[ 3, 44], [11, 45]]], [26, [[17, 21], [ 4, 22]]], [26, [[ 9, 13], [16, 14]]]], 137 | [[28, [[ 3, 107], [ 5, 108]]], [26, [[ 3, 41], [13, 42]]], [30, [[15, 24], [ 5, 25]]], [28, [[15, 15], [10, 16]]]], 138 | [[28, [[ 4, 116], [ 4, 117]]], [26, [[17, 42], [ 0, 0]]], [28, [[17, 22], [ 6, 23]]], [30, [[19, 16], [ 6, 17]]]], 139 | [[28, [[ 2, 111], [ 7, 112]]], [28, [[17, 46], [ 0, 0]]], [30, [[ 7, 24], [16, 25]]], [24, [[34, 13], [ 0, 0]]]], 140 | [[30, [[ 4, 121], [ 5, 122]]], [28, [[ 4, 47], [14, 48]]], [30, [[11, 24], [14, 25]]], [30, [[16, 15], [14, 16]]]], 141 | [[30, [[ 6, 117], [ 4, 118]]], [28, [[ 6, 45], [14, 46]]], [30, [[11, 24], [16, 25]]], [30, [[30, 16], [ 2, 17]]]], 142 | [[26, [[ 8, 106], [ 4, 107]]], [28, [[ 8, 47], [13, 48]]], [30, [[ 7, 24], [22, 25]]], [30, [[22, 15], [13, 16]]]], 143 | [[28, [[10, 114], [ 2, 115]]], [28, [[19, 46], [ 4, 47]]], [28, [[28, 22], [ 6, 23]]], [30, [[33, 16], [ 4, 17]]]], 144 | [[30, [[ 8, 122], [ 4, 123]]], [28, [[22, 45], [ 3, 46]]], [30, [[ 8, 23], [26, 24]]], [30, [[12, 15], [28, 16]]]], 145 | [[30, [[ 3, 117], [10, 118]]], [28, [[ 3, 45], [23, 46]]], [30, [[ 4, 24], [31, 25]]], [30, [[11, 15], [31, 16]]]], 146 | [[30, [[ 7, 116], [ 7, 117]]], [28, [[21, 45], [ 7, 46]]], [30, [[ 1, 23], [37, 24]]], [30, [[19, 15], [26, 16]]]], 147 | [[30, [[ 5, 115], [10, 116]]], [28, [[19, 47], [10, 48]]], [30, [[15, 24], [25, 25]]], [30, [[23, 15], [25, 16]]]], 148 | [[30, [[13, 115], [ 3, 116]]], [28, [[ 2, 46], [29, 47]]], [30, [[42, 24], [ 1, 25]]], [30, [[23, 15], [28, 16]]]], 149 | [[30, [[17, 115], [ 0, 0]]], [28, [[10, 46], [23, 47]]], [30, [[10, 24], [35, 25]]], [30, [[19, 15], [35, 16]]]], 150 | [[30, [[17, 115], [ 1, 116]]], [28, [[14, 46], [21, 47]]], [30, [[29, 24], [19, 25]]], [30, [[11, 15], [46, 16]]]], 151 | [[30, [[13, 115], [ 6, 116]]], [28, [[14, 46], [23, 47]]], [30, [[44, 24], [ 7, 25]]], [30, [[59, 16], [ 1, 17]]]], 152 | [[30, [[12, 121], [ 7, 122]]], [28, [[12, 47], [26, 48]]], [30, [[39, 24], [14, 25]]], [30, [[22, 15], [41, 16]]]], 153 | [[30, [[ 6, 121], [14, 122]]], [28, [[ 6, 47], [34, 48]]], [30, [[46, 24], [10, 25]]], [30, [[ 2, 15], [64, 16]]]], 154 | [[30, [[17, 122], [ 4, 123]]], [28, [[29, 46], [14, 47]]], [30, [[49, 24], [10, 25]]], [30, [[24, 15], [46, 16]]]], 155 | [[30, [[ 4, 122], [18, 123]]], [28, [[13, 46], [32, 47]]], [30, [[48, 24], [14, 25]]], [30, [[42, 15], [32, 16]]]], 156 | [[30, [[20, 117], [ 4, 118]]], [28, [[40, 47], [ 7, 48]]], [30, [[43, 24], [22, 25]]], [30, [[10, 15], [67, 16]]]], 157 | [[30, [[19, 118], [ 6, 119]]], [28, [[18, 47], [31, 48]]], [30, [[34, 24], [34, 25]]], [30, [[20, 15], [61, 16]]]], 158 | ]; 159 | 160 | /** 161 | * @type {number[]|int[]} 162 | */ 163 | const TOTAL_CODEWORDS = [ 164 | null, // version count starts at 1 165 | 26, 166 | 44, 167 | 70, 168 | 100, 169 | 134, 170 | 172, 171 | 196, 172 | 242, 173 | 292, 174 | 346, 175 | 404, 176 | 466, 177 | 532, 178 | 581, 179 | 655, 180 | 733, 181 | 815, 182 | 901, 183 | 991, 184 | 1085, 185 | 1156, 186 | 1258, 187 | 1364, 188 | 1474, 189 | 1588, 190 | 1706, 191 | 1828, 192 | 1921, 193 | 2051, 194 | 2185, 195 | 2323, 196 | 2465, 197 | 2611, 198 | 2761, 199 | 2876, 200 | 3034, 201 | 3196, 202 | 3362, 203 | 3532, 204 | 3706, 205 | ]; 206 | 207 | /** 208 | * 209 | */ 210 | export default class Version{ 211 | 212 | /** 213 | * QR Code version number 214 | * 215 | * @type {number|int} 216 | * @private 217 | */ 218 | version; 219 | 220 | /** 221 | * Version constructor. 222 | * 223 | * @param {number|int} $version 224 | * 225 | * @throws QRCodeException 226 | */ 227 | constructor($version){ 228 | 229 | if($version < 1 || $version > 40){ 230 | throw new QRCodeException('invalid version given'); 231 | } 232 | 233 | this.version = $version; 234 | } 235 | 236 | /** 237 | * returns the current version number as string 238 | * 239 | * @returns {string} 240 | */ 241 | toString(){ 242 | return this.version + ''; 243 | } 244 | 245 | /** 246 | * returns the current version number 247 | * 248 | * @returns {number|int} 249 | */ 250 | getVersionNumber(){ 251 | return this.version; 252 | } 253 | 254 | /** 255 | * the matrix size for the given version 256 | * 257 | * @returns {number|int} 258 | */ 259 | getDimension(){ 260 | return this.version * 4 + 17; 261 | } 262 | 263 | /** 264 | * the version pattern for the given version 265 | * 266 | * @returns {number|int|null} 267 | */ 268 | getVersionPattern(){ 269 | return VERSION_PATTERN[this.version] ?? null; 270 | } 271 | 272 | /** 273 | * the alignment patterns for the current version 274 | * 275 | * @returns {number[]|int[]} 276 | */ 277 | getAlignmentPattern(){ 278 | return ALIGNMENT_PATTERN[this.version]; 279 | } 280 | 281 | /** 282 | * returns ECC block information for the given $version and $eccLevel 283 | * 284 | * @param {EccLevel} $eccLevel 285 | * 286 | * @returns {Array} 287 | */ 288 | getRSBlocks($eccLevel){ 289 | return RSBLOCKS[this.version][$eccLevel.getOrdinal()]; 290 | } 291 | 292 | /** 293 | * returns the maximum codewords for the current version 294 | * 295 | * @returns {number|int} 296 | */ 297 | getTotalCodewords(){ 298 | return TOTAL_CODEWORDS[this.version]; 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /src/Common/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 24.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | * 7 | * @see https://jamie.build/const 8 | */ 9 | 10 | import PHPJS from './PHPJS.js'; 11 | 12 | /** 13 | * from EccLevel 14 | */ 15 | 16 | // ISO/IEC 18004:2000 Tables 12, 25 17 | const ECC_L = 0b01; // 7%. 18 | const ECC_M = 0b00; // 15%. 19 | const ECC_Q = 0b11; // 25%. 20 | const ECC_H = 0b10; // 30%. 21 | 22 | /** 23 | * from MaskPattern 24 | */ 25 | 26 | /** @type {number|int} */ 27 | const MASK_PATTERN_AUTO = -1; 28 | 29 | const PATTERN_000 = 0b000; 30 | const PATTERN_001 = 0b001; 31 | const PATTERN_010 = 0b010; 32 | const PATTERN_011 = 0b011; 33 | const PATTERN_100 = 0b100; 34 | const PATTERN_101 = 0b101; 35 | const PATTERN_110 = 0b110; 36 | const PATTERN_111 = 0b111; 37 | 38 | /** 39 | * @type {number[]|int[]} 40 | * @private 41 | */ 42 | const PATTERNS = [ 43 | PATTERN_000, 44 | PATTERN_001, 45 | PATTERN_010, 46 | PATTERN_011, 47 | PATTERN_100, 48 | PATTERN_101, 49 | PATTERN_110, 50 | PATTERN_111, 51 | ]; 52 | 53 | /** 54 | * from Mode 55 | */ 56 | 57 | // ISO/IEC 18004:2000 Table 2 58 | const MODE_NUMBER = 0b0001; 59 | const MODE_ALPHANUM = 0b0010; 60 | const MODE_BYTE = 0b0100; 61 | 62 | const MODES = [ 63 | MODE_NUMBER, 64 | MODE_ALPHANUM, 65 | MODE_BYTE, 66 | ]; 67 | 68 | /** 69 | * from Version 70 | */ 71 | 72 | /** @type {number|int} */ 73 | const VERSION_AUTO = -1; 74 | 75 | /** 76 | * from QRMatrix 77 | */ 78 | 79 | /* 80 | * special values 81 | */ 82 | 83 | /** @type {number|int} */ 84 | const IS_DARK = 0b100000000000; 85 | /** @type {number|int} */ 86 | const M_NULL = 0b000000000000; 87 | /** @type {number|int} */ 88 | const M_LOGO = 0b001000000000; 89 | /** @type {number|int} */ 90 | const M_LOGO_DARK = 0b101000000000; 91 | 92 | /* 93 | * light values 94 | */ 95 | 96 | /** @type {number|int} */ 97 | const M_DATA = 0b000000000010; 98 | /** @type {number|int} */ 99 | const M_FINDER = 0b000000000100; 100 | /** @type {number|int} */ 101 | const M_SEPARATOR = 0b000000001000; 102 | /** @type {number|int} */ 103 | const M_ALIGNMENT = 0b000000010000; 104 | /** @type {number|int} */ 105 | const M_TIMING = 0b000000100000; 106 | /** @type {number|int} */ 107 | const M_FORMAT = 0b000001000000; 108 | /** @type {number|int} */ 109 | const M_VERSION = 0b000010000000; 110 | /** @type {number|int} */ 111 | const M_QUIETZONE = 0b000100000000; 112 | 113 | /* 114 | * dark values 115 | */ 116 | 117 | /** @type {number|int} */ 118 | const M_DARKMODULE = 0b100000000001; 119 | /** @type {number|int} */ 120 | const M_DATA_DARK = 0b100000000010; 121 | /** @type {number|int} */ 122 | const M_FINDER_DARK = 0b100000000100; 123 | /** @type {number|int} */ 124 | const M_ALIGNMENT_DARK = 0b100000010000; 125 | /** @type {number|int} */ 126 | const M_TIMING_DARK = 0b100000100000; 127 | /** @type {number|int} */ 128 | const M_FORMAT_DARK = 0b100001000000; 129 | /** @type {number|int} */ 130 | const M_VERSION_DARK = 0b100010000000; 131 | /** @type {number|int} */ 132 | const M_FINDER_DOT = 0b110000000000; 133 | 134 | /* 135 | * values used for reversed reflectance 136 | */ 137 | 138 | /** @type {number|int} */ 139 | const M_DARKMODULE_LIGHT = 0b000000000001; 140 | /** @type {number|int} */ 141 | const M_FINDER_DOT_LIGHT = 0b010000000000; 142 | /** @type {number|int} */ 143 | const M_SEPARATOR_DARK = 0b100000001000; 144 | /** @type {number|int} */ 145 | const M_QUIETZONE_DARK = 0b100100000000; 146 | 147 | /** @type {number[]|int[]} */ 148 | const MATRIX_NEIGHBOUR_FLAGS = [ 149 | 0b00000001, 150 | 0b00000010, 151 | 0b00000100, 152 | 0b00001000, 153 | 0b00010000, 154 | 0b00100000, 155 | 0b01000000, 156 | 0b10000000, 157 | ]; 158 | 159 | /** 160 | * Map of flag => coord 161 | * 162 | * @see QRMatrix::checkNeighbours() 163 | * 164 | * @type {number[][]|int[][]} 165 | * @protected 166 | */ 167 | const MATRIX_NEIGHBOURS = PHPJS.array_combine(MATRIX_NEIGHBOUR_FLAGS, [ 168 | [-1, -1], 169 | [ 0, -1], 170 | [ 1, -1], 171 | [ 1, 0], 172 | [ 1, 1], 173 | [ 0, 1], 174 | [-1, 1], 175 | [-1, 0], 176 | ]); 177 | 178 | /** 179 | * @type {number[]|int[]} 180 | * @internal 181 | */ 182 | const MODULE_VALUES_KEYS = [ 183 | // light 184 | M_NULL, 185 | M_DARKMODULE_LIGHT, 186 | M_DATA, 187 | M_FINDER, 188 | M_SEPARATOR, 189 | M_ALIGNMENT, 190 | M_TIMING, 191 | M_FORMAT, 192 | M_VERSION, 193 | M_QUIETZONE, 194 | M_LOGO, 195 | M_FINDER_DOT_LIGHT, 196 | // dark 197 | M_DARKMODULE, 198 | M_DATA_DARK, 199 | M_FINDER_DARK, 200 | M_SEPARATOR_DARK, 201 | M_ALIGNMENT_DARK, 202 | M_TIMING_DARK, 203 | M_FORMAT_DARK, 204 | M_VERSION_DARK, 205 | M_QUIETZONE_DARK, 206 | M_LOGO_DARK, 207 | M_FINDER_DOT, 208 | ]; 209 | 210 | const DEFAULT_MODULE_VALUES = PHPJS.array_combine(MODULE_VALUES_KEYS, [ 211 | // light 212 | false, 213 | false, 214 | false, 215 | false, 216 | false, 217 | false, 218 | false, 219 | false, 220 | false, 221 | false, 222 | false, 223 | false, 224 | // dark 225 | true, 226 | true, 227 | true, 228 | true, 229 | true, 230 | true, 231 | true, 232 | true, 233 | true, 234 | true, 235 | true, 236 | ]); 237 | 238 | const LAYERNAMES = PHPJS.array_combine(MODULE_VALUES_KEYS, [ 239 | // light 240 | 'null', 241 | 'darkmodule-light', 242 | 'data', 243 | 'finder', 244 | 'separator', 245 | 'alignment', 246 | 'timing', 247 | 'format', 248 | 'version', 249 | 'quietzone', 250 | 'logo', 251 | 'finder-dot-light', 252 | // dark 253 | 'darkmodule', 254 | 'data-dark', 255 | 'finder-dark', 256 | 'separator-dark', 257 | 'alignment-dark', 258 | 'timing-dark', 259 | 'format-dark', 260 | 'version-dark', 261 | 'quietzone-dark', 262 | 'logo-dark', 263 | 'finder-dot', 264 | ]); 265 | 266 | export { 267 | ECC_L, ECC_M, ECC_Q, ECC_H, 268 | MASK_PATTERN_AUTO, PATTERNS, PATTERN_000, PATTERN_001, PATTERN_010, 269 | PATTERN_011, PATTERN_100, PATTERN_101, PATTERN_110, PATTERN_111, 270 | MODES, MODE_NUMBER, MODE_ALPHANUM, MODE_BYTE, 271 | VERSION_AUTO, 272 | M_NULL, M_DARKMODULE, M_DARKMODULE_LIGHT, M_DATA, M_FINDER, M_SEPARATOR, M_ALIGNMENT, M_TIMING, 273 | M_FORMAT, M_VERSION, M_QUIETZONE, M_LOGO, M_FINDER_DOT, M_FINDER_DOT_LIGHT, IS_DARK, 274 | M_DATA_DARK, M_FINDER_DARK, M_SEPARATOR_DARK, M_ALIGNMENT_DARK, M_TIMING_DARK, 275 | M_FORMAT_DARK, M_VERSION_DARK, M_QUIETZONE_DARK, M_LOGO_DARK, 276 | MATRIX_NEIGHBOUR_FLAGS, MATRIX_NEIGHBOURS, 277 | DEFAULT_MODULE_VALUES, LAYERNAMES, 278 | }; 279 | -------------------------------------------------------------------------------- /src/Data/AlphaNum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QRDataModeAbstract from './QRDataModeAbstract.js'; 9 | import {MODE_ALPHANUM} from '../Common/constants.js'; 10 | 11 | /** 12 | * ISO/IEC 18004:2000 Table 5 13 | * 14 | * @type {Object<{}>} 15 | */ 16 | const CHAR_TO_ORD = { 17 | '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, 18 | '8': 8, '9': 9, 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 19 | 'G': 16, 'H': 17, 'I': 18, 'J': 19, 'K': 20, 'L': 21, 'M': 22, 'N': 23, 20 | 'O': 24, 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, 'U': 30, 'V': 31, 21 | 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, ' ': 36, '$': 37, '%': 38, '*': 39, 22 | '+': 40, '-': 41, '.': 42, '/': 43, ':': 44, 23 | }; 24 | 25 | /** 26 | * Alphanumeric mode: 0 to 9, A to Z, space, $ % * + - . / : 27 | * 28 | * ISO/IEC 18004:2000 Section 8.3.3 29 | * ISO/IEC 18004:2000 Section 8.4.3 30 | */ 31 | export default class AlphaNum extends QRDataModeAbstract{ 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | datamode = MODE_ALPHANUM; 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | getLengthInBits(){ 42 | return Math.ceil(this.getCharCount() * (11 / 2)); 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | static validateString($string){ 49 | 50 | if(typeof $string !== 'string' || !$string.length){ 51 | return false; 52 | } 53 | 54 | let $chars = $string.split(''); 55 | 56 | for(let $chr of $chars){ 57 | if(typeof CHAR_TO_ORD[$chr] === 'undefined'){ 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | write($bitBuffer, $versionNumber){ 69 | let $len = this.getCharCount(); 70 | let $i; 71 | 72 | $bitBuffer 73 | .put(this.datamode, 4) 74 | .put($len, this.getLengthBitsForVersion($versionNumber)) 75 | ; 76 | 77 | // encode 2 characters in 11 bits 78 | for($i = 0; $i + 1 < $len; $i += 2){ 79 | $bitBuffer.put(CHAR_TO_ORD[this.data[$i]] * 45 + CHAR_TO_ORD[this.data[$i + 1]], 11); 80 | } 81 | 82 | // encode a remaining character in 6 bits 83 | if($i < $len){ 84 | $bitBuffer.put(CHAR_TO_ORD[this.data[$i]], 6); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Data/Byte.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QRDataModeAbstract from './QRDataModeAbstract.js'; 9 | import PHPJS from '../Common/PHPJS.js'; 10 | import {MODE_BYTE} from '../Common/constants.js'; 11 | 12 | /** 13 | * Byte mode, ISO-8859-1 or UTF-8 14 | * 15 | * ISO/IEC 18004:2000 Section 8.3.4 16 | * ISO/IEC 18004:2000 Section 8.4.4 17 | */ 18 | export default class Byte extends QRDataModeAbstract{ 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | datamode = MODE_BYTE; 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | getLengthInBits(){ 29 | return this.getCharCount() * 8; 30 | } 31 | 32 | /** 33 | * @inheritDoc 34 | */ 35 | static validateString($string){ 36 | return typeof $string === 'string' && !!$string.length; 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | write($bitBuffer, $versionNumber){ 43 | let $len = this.getCharCount(); 44 | let $data = this.data.split(''); 45 | 46 | $bitBuffer 47 | .put(this.datamode, 4) 48 | .put($len, this.getLengthBitsForVersion($versionNumber)) 49 | ; 50 | 51 | let $i = 0; 52 | 53 | while($i < $len){ 54 | $bitBuffer.put(PHPJS.ord($data[$i]), 8); 55 | $i++; 56 | } 57 | 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Data/Numeric.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QRDataModeAbstract from './QRDataModeAbstract.js'; 9 | import PHPJS from '../Common/PHPJS.js'; 10 | import {MODE_NUMBER} from '../Common/constants.js'; 11 | 12 | const NUMBER_TO_ORD = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}; 13 | 14 | /** 15 | * Numeric mode: decimal digits 0 to 9 16 | * 17 | * ISO/IEC 18004:2000 Section 8.3.2 18 | * ISO/IEC 18004:2000 Section 8.4.2 19 | */ 20 | export default class Numeric extends QRDataModeAbstract{ 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | datamode = MODE_NUMBER; 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | getLengthInBits(){ 31 | return Math.ceil(this.getCharCount() * (10 / 3)); 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | static validateString($string){ 38 | 39 | if(typeof $string !== 'string' || !$string.length){ 40 | return false; 41 | } 42 | 43 | let $chars = $string.split(''); 44 | 45 | for(let $chr of $chars){ 46 | if(typeof NUMBER_TO_ORD[$chr] === 'undefined'){ 47 | return false; 48 | } 49 | } 50 | 51 | return true; 52 | } 53 | 54 | /** 55 | * @inheritDoc 56 | */ 57 | write($bitBuffer, $versionNumber){ 58 | let $len = this.getCharCount(); 59 | 60 | $bitBuffer 61 | .put(this.datamode, 4) 62 | .put($len, this.getLengthBitsForVersion($versionNumber)) 63 | ; 64 | 65 | let $i = 0; 66 | 67 | // encode numeric triplets in 10 bits 68 | while($i + 2 < $len){ 69 | $bitBuffer.put(this.parseInt(this.data.substring($i, 3)), 10); 70 | $i += 3; 71 | } 72 | 73 | if($i < $len){ 74 | 75 | // encode 2 remaining numbers in 7 bits 76 | if(($len - $i) === 2){ 77 | $bitBuffer.put(this.parseInt(this.data.substring($i, 2)), 7); 78 | } 79 | // encode one remaining number in 4 bits 80 | else if(($len - $i) === 1){ 81 | $bitBuffer.put(this.parseInt(this.data.substring($i, 1)), 4); 82 | } 83 | 84 | } 85 | 86 | } 87 | 88 | /** 89 | * get the code for the given numeric string 90 | * 91 | * @param {string} $string 92 | * @returns {number|int} 93 | * @private 94 | */ 95 | parseInt($string){ 96 | let $num = 0; 97 | let $chars = $string.split(''); 98 | 99 | for(let $chr of $chars){ 100 | $num = $num * 10 + PHPJS.ord($chr) - 48; 101 | } 102 | 103 | return $num; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/Data/QRCodeDataException.js: -------------------------------------------------------------------------------- 1 | import QRCodeException from '../QRCodeException.js'; 2 | 3 | export default class QRCodeDataException extends QRCodeException{ 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Data/QRData.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import BitBuffer from '../Common/BitBuffer.js'; 9 | import EccLevel from '../Common/EccLevel.js'; 10 | import Version from '../Common/Version.js'; 11 | import QRMatrix from './QRMatrix.js'; 12 | import Mode from '../Common/Mode.js'; 13 | import QRCodeDataException from './QRCodeDataException.js'; 14 | import {VERSION_AUTO} from '../Common/constants.js'; 15 | 16 | /** 17 | * Processes the binary data and maps it on a matrix which is then being returned 18 | */ 19 | export default class QRData{ 20 | 21 | /** 22 | * the options instance 23 | * 24 | * @type {QROptions} 25 | * @private 26 | */ 27 | options; 28 | 29 | /** 30 | * a BitBuffer instance 31 | * 32 | * @type {BitBuffer} 33 | * @private 34 | */ 35 | bitBuffer; 36 | 37 | /** 38 | * an EccLevel instance 39 | * 40 | * @type {EccLevel} 41 | * @private 42 | */ 43 | eccLevel; 44 | 45 | /** 46 | * current QR Code version 47 | * 48 | * @type {Version} 49 | * @private 50 | */ 51 | version; 52 | 53 | /** 54 | * @type {QRDataModeAbstract[]|Array} 55 | * @private 56 | */ 57 | dataSegments = []; 58 | 59 | /** 60 | * Max bits for the current ECC mode 61 | * 62 | * @type {number[]|int[]} 63 | * @private 64 | */ 65 | maxBitsForEcc; 66 | 67 | /** 68 | * QRData constructor. 69 | * 70 | * @param {QROptions} $options 71 | * @param {QRDataModeAbstract[]} $dataSegments 72 | */ 73 | constructor($options, $dataSegments = []){ 74 | this.options = $options; 75 | this.bitBuffer = new BitBuffer; 76 | this.eccLevel = new EccLevel(this.options.eccLevel); 77 | this.maxBitsForEcc = this.eccLevel.getMaxBits(); 78 | 79 | this.setData($dataSegments); 80 | } 81 | 82 | /** 83 | * Sets the data string (internally called by the constructor) 84 | * 85 | * @param {QRDataModeAbstract[]} $dataSegments 86 | * 87 | * @returns {QRData} 88 | */ 89 | setData($dataSegments){ 90 | this.dataSegments = $dataSegments; 91 | this.version = this.getMinimumVersion(); 92 | 93 | this.bitBuffer.clear(); 94 | this.writeBitBuffer(); 95 | 96 | return this; 97 | } 98 | 99 | /** 100 | * returns a fresh matrix object with the data written and masked with the given $maskPattern 101 | * 102 | * @returns {QRMatrix} 103 | */ 104 | writeMatrix(){ 105 | return (new QRMatrix(this.version, this.eccLevel)) 106 | .initFunctionalPatterns() 107 | .writeCodewords(this.bitBuffer) 108 | ; 109 | } 110 | 111 | /** 112 | * estimates the total length of the several mode segments in order to guess the minimum version 113 | * 114 | * @returns {number|int} 115 | * @throws {QRCodeDataException} 116 | * @private 117 | */ 118 | estimateTotalBitLength(){ 119 | let $length = 0; 120 | let $segment; 121 | 122 | for($segment of this.dataSegments){ 123 | // data length of the current segment 124 | $length += $segment.getLengthInBits(); 125 | // +4 bits for the mode descriptor 126 | $length += 4; 127 | } 128 | 129 | let $provisionalVersion = null; 130 | 131 | for(let $version in this.maxBitsForEcc){ 132 | 133 | if($version === 0){ // JS array/object weirdness vs php arrays... 134 | continue; 135 | } 136 | 137 | if($length <= this.maxBitsForEcc[$version]){ 138 | $provisionalVersion = $version; 139 | } 140 | 141 | } 142 | 143 | if($provisionalVersion !== null){ 144 | 145 | // add character count indicator bits for the provisional version 146 | for($segment of this.dataSegments){ 147 | $length += Mode.getLengthBitsForVersion($segment.datamode, $provisionalVersion); 148 | } 149 | 150 | // it seems that in some cases the estimated total length is not 100% accurate, 151 | // so we substract 4 bits from the total when not in mixed mode 152 | if(this.dataSegments.length <= 1){ 153 | $length -= 4; 154 | } 155 | 156 | // we've got a match! 157 | // or let's see if there's a higher version number available 158 | if($length <= this.maxBitsForEcc[$provisionalVersion] || this.maxBitsForEcc[($provisionalVersion + 1)]){ 159 | return $length; 160 | } 161 | 162 | } 163 | 164 | throw new QRCodeDataException(`estimated data exceeds ${$length} bits`); 165 | } 166 | 167 | /** 168 | * returns the minimum version number for the given string 169 | * 170 | * @return {Version} 171 | * @throws {QRCodeDataException} 172 | * @private 173 | */ 174 | getMinimumVersion(){ 175 | 176 | if(this.options.version !== VERSION_AUTO){ 177 | return new Version(this.options.version); 178 | } 179 | 180 | let $total = this.estimateTotalBitLength(); 181 | 182 | // guess the version number within the given range 183 | for(let $version = this.options.versionMin; $version <= this.options.versionMax; $version++){ 184 | if($total <= (this.maxBitsForEcc[$version] - 4)){ 185 | return new Version($version); 186 | } 187 | } 188 | /* c8 ignore next 2 */ 189 | // it's almost impossible to run into this one as $this::estimateTotalBitLength() would throw first 190 | throw new QRCodeDataException('failed to guess minimum version'); 191 | } 192 | 193 | /** 194 | * creates a BitBuffer and writes the string data to it 195 | * 196 | * @returns {void} 197 | * @throws {QRCodeException} on data overflow 198 | * @private 199 | */ 200 | writeBitBuffer(){ 201 | let $MAX_BITS = this.eccLevel.getMaxBitsForVersion(this.version); 202 | 203 | for(let $i = 0; $i < this.dataSegments.length; $i++){ 204 | this.dataSegments[$i].write(this.bitBuffer, this.version.getVersionNumber()); 205 | } 206 | 207 | // overflow, likely caused due to invalid version setting 208 | if(this.bitBuffer.getLength() > $MAX_BITS){ 209 | throw new QRCodeDataException(`code length overflow. (${this.bitBuffer.getLength()} > ${$MAX_BITS} bit)`); 210 | } 211 | 212 | // add terminator (ISO/IEC 18004:2000 Table 2) 213 | if(this.bitBuffer.getLength() + 4 <= $MAX_BITS){ 214 | this.bitBuffer.put(0, 4); 215 | } 216 | 217 | // Padding: ISO/IEC 18004:2000 8.4.9 Bit stream to codeword conversion 218 | 219 | // if the final codeword is not exactly 8 bits in length, it shall be made 8 bits long 220 | // by the addition of padding bits with binary value 0 221 | while(this.bitBuffer.getLength() % 8 !== 0){ 222 | 223 | if(this.bitBuffer.getLength() === $MAX_BITS){ 224 | break; 225 | } 226 | 227 | this.bitBuffer.putBit(false); 228 | } 229 | 230 | // The message bit stream shall then be extended to fill the data capacity of the symbol 231 | // corresponding to the Version and Error Correction Level, by the addition of the Pad 232 | // Codewords 11101100 and 00010001 alternately. 233 | let $alternate = false; 234 | 235 | while(this.bitBuffer.getLength() <= $MAX_BITS){ 236 | this.bitBuffer.put($alternate ? 0b00010001 : 0b11101100, 8); 237 | $alternate = !$alternate; 238 | } 239 | 240 | // In certain versions of symbol, it may be necessary to add 3, 4 or 7 Remainder Bits (all zeros) 241 | // to the end of the message in order exactly to fill the symbol capacity 242 | while(this.bitBuffer.getLength() <= $MAX_BITS){ 243 | this.bitBuffer.putBit(false); 244 | } 245 | 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /src/Data/QRDataModeAbstract.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import Mode from '../Common/Mode.js'; 9 | import QRCodeDataException from './QRCodeDataException.js'; 10 | import QRDataModeInterface from './QRDataModeInterface.js'; 11 | 12 | /** 13 | * @abstract 14 | */ 15 | export default class QRDataModeAbstract extends QRDataModeInterface{ 16 | 17 | /** 18 | * the current data mode: Num, Alphanum, Kanji, Byte 19 | * 20 | * @type {number|int} 21 | * @abstract 22 | */ 23 | datamode; 24 | 25 | /** 26 | * The data to write 27 | * 28 | * @type {string} 29 | */ 30 | data; 31 | 32 | /** 33 | * QRDataModeAbstract constructor. 34 | * 35 | * @param {string} $data 36 | * 37 | * @throws {QRCodeDataException} 38 | */ 39 | constructor($data){ 40 | super(); // JS dum 41 | 42 | if(!this.constructor.validateString($data)){ 43 | throw new QRCodeDataException('invalid data'); 44 | } 45 | 46 | this.data = $data; 47 | } 48 | 49 | /** 50 | * returns the character count of the $data string 51 | * 52 | * @returns {number|int} 53 | * @protected 54 | */ 55 | getCharCount(){ 56 | return this.data.length; 57 | } 58 | 59 | /** 60 | * returns the current data mode constant 61 | * 62 | * @inheritDoc 63 | * 64 | * @returns {number|int} 65 | */ 66 | getDataMode(){ 67 | return this.datamode; 68 | } 69 | 70 | /** 71 | * @param {number|int} $versionNumber 72 | * 73 | * @returns {number|int} 74 | * @protected 75 | */ 76 | getLengthBitsForVersion($versionNumber){ 77 | return Mode.getLengthBitsForVersion(this.datamode, $versionNumber) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Data/QRDataModeInterface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 14.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | /** 9 | * @interface 10 | */ 11 | export default class QRDataModeInterface{ 12 | 13 | /** 14 | * returns the current data mode constant 15 | * 16 | * @inheritDoc 17 | * 18 | * @returns {number|int} 19 | * @abstract 20 | */ 21 | getDataMode(){} 22 | 23 | /** 24 | * retruns the length in bits of the data string 25 | * 26 | * @returns {number|int} 27 | * @abstract 28 | */ 29 | getLengthInBits(){} 30 | 31 | /** 32 | * writes the actual data string to the BitBuffer, uses the given version to determine the length bits 33 | * 34 | * @see QRData::writeBitBuffer() 35 | * 36 | * @param {BitBuffer} $bitBuffer 37 | * @param {number|int} $versionNumber 38 | * 39 | * @returns {void} 40 | * @abstract 41 | */ 42 | write($bitBuffer, $versionNumber){} 43 | 44 | /** 45 | * checks if the given string qualifies for the encoder module 46 | * 47 | * @param {string} $string 48 | * 49 | * @returns {boolean} 50 | * @abstract 51 | */ 52 | static validateString($string){} 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Output/QRCanvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QROutputAbstract from './QROutputAbstract.js'; 9 | import QRCodeOutputException from './QRCodeOutputException.js'; 10 | 11 | /** 12 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API 13 | * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas 14 | */ 15 | export default class QRCanvas extends QROutputAbstract{ 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | mimeType = 'image/png'; 21 | 22 | /** 23 | * @type {HTMLCanvasElement} 24 | * @protected 25 | */ 26 | canvas; 27 | 28 | /** 29 | * @type {CanvasRenderingContext2D} 30 | * @protected 31 | */ 32 | context; 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | static moduleValueIsValid($value){ 38 | 39 | if(typeof $value !== 'string'){ 40 | return false; 41 | } 42 | 43 | $value = $value.trim(); 44 | 45 | // hex notation 46 | // #rgb(a) 47 | // #rrggbb(aa) 48 | if($value.match(/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i)){ 49 | return true; 50 | } 51 | 52 | // css: hsla/rgba(...values) 53 | if($value.match(/^(hsla?|rgba?)\([\d .,%\/]+\)$/i)){ 54 | return true; 55 | } 56 | 57 | // predefined css color 58 | if($value.match(/^[a-z]+$/i)){ 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | prepareModuleValue($value){ 69 | return $value.trim(); 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | getDefaultModuleValue($isDark){ 76 | return $isDark ? '#000' : '#fff'; 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | getOutputDimensions(){ 83 | return [this.length, this.length]; 84 | } 85 | 86 | /** 87 | * @param {string} $data (ignored) 88 | * @param {string} $mime (ignored) 89 | * @returns {string} 90 | * @protected 91 | */ 92 | toBase64DataURI($data, $mime){ 93 | $mime = this.options.canvasMimeType.trim(); 94 | 95 | if($mime === ''){ 96 | $mime = this.mimeType; 97 | } 98 | 99 | return this.canvas.toDataURL($mime, this.options.canvasImageQuality) 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | * 105 | * @returns {HTMLCanvasElement|string|*} 106 | * @throws {QRCodeOutputException} 107 | */ 108 | dump($file = null){ 109 | this.canvas = (this.options.canvasElement || document.createElement('canvas')); 110 | 111 | // @todo: test if instance check also works with nodejs canvas modules etc. 112 | if(!this.canvas || !(this.canvas instanceof HTMLCanvasElement) || (typeof this.canvas.getContext !== 'function')){ 113 | throw new QRCodeOutputException('invalid canvas element'); 114 | } 115 | 116 | this.drawImage(); 117 | 118 | if(this.options.returnAsDomElement){ 119 | return this.canvas; 120 | } 121 | 122 | let base64DataURI = this.toBase64DataURI(); 123 | let rawImage = atob(base64DataURI.split(',')[1]); 124 | 125 | this.saveToFile(rawImage, $file); 126 | 127 | if(this.options.outputBase64){ 128 | return base64DataURI; 129 | } 130 | 131 | return rawImage; 132 | } 133 | 134 | /** 135 | * @returns {void} 136 | * @protected 137 | */ 138 | drawImage(){ 139 | this.canvas.width = this.length; 140 | this.canvas.height = this.length; 141 | this.context = this.canvas.getContext('2d', {alpha: this.options.imageTransparent}) 142 | 143 | if(this.options.bgcolor && this.constructor.moduleValueIsValid(this.options.bgcolor)){ 144 | this.context.fillStyle = this.options.bgcolor; 145 | this.context.fillRect(0, 0, this.length, this.length); 146 | } 147 | 148 | for(let $y = 0; $y < this.moduleCount; $y++){ 149 | for(let $x = 0; $x < this.moduleCount; $x++){ 150 | this.module($x, $y, this.matrix.get($x, $y)) 151 | } 152 | } 153 | 154 | } 155 | 156 | /** 157 | * @returns {void} 158 | * @protected 159 | */ 160 | module($x, $y, $M_TYPE){ 161 | 162 | if(!this.options.drawLightModules && !this.matrix.check($x, $y)){ 163 | return; 164 | } 165 | 166 | this.context.fillStyle = this.getModuleValue($M_TYPE); 167 | 168 | if(this.options.drawCircularModules && !this.matrix.checkTypeIn($x, $y, this.options.keepAsSquare)){ 169 | this.context.beginPath(); 170 | 171 | this.context.arc( 172 | ($x + 0.5) * this.scale, 173 | ($y + 0.5) * this.scale, 174 | (this.options.circleRadius * this.scale), 175 | 0, 176 | 2 * Math.PI 177 | ) 178 | 179 | this.context.fill(); 180 | 181 | return; 182 | } 183 | 184 | this.context.fillRect($x * this.scale, $y * this.scale, this.scale, this.scale); 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/Output/QRCodeOutputException.js: -------------------------------------------------------------------------------- 1 | import QRCodeException from '../QRCodeException.js'; 2 | 3 | export default class QRCodeOutputException extends QRCodeException{ 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Output/QRMarkupSVG.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QROutputAbstract from './QROutputAbstract.js'; 9 | import {LAYERNAMES} from '../Common/constants.js'; 10 | 11 | /** 12 | * SVG output 13 | * 14 | * @see https://github.com/codemasher/php-qrcode/pull/5 15 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg 16 | * @see https://www.sarasoueidan.com/demos/interactive-svg-coordinate-system/ 17 | * @see https://web.archive.org/web/20200220211445/http://apex.infogridpacific.com/SVG/svg-tutorial-contents.html 18 | */ 19 | export default class QRMarkupSVG extends QROutputAbstract{ 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | mimeType = 'image/svg+xml'; 25 | 26 | /** 27 | * @protected 28 | */ 29 | getCssClass($M_TYPE){ 30 | return [ 31 | `qr-${LAYERNAMES[$M_TYPE] ?? $M_TYPE}`, 32 | this.matrix.isDark($M_TYPE) ? 'dark' : 'light', 33 | this.options.cssClass, 34 | ].join(' '); 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | * 40 | * @returns {HTMLElement|SVGElement|ChildNode|string|*} 41 | */ 42 | dump($file = null){ 43 | let $data = this.createMarkup($file !== null); 44 | 45 | this.saveToFile($data, $file); 46 | 47 | if(this.options.returnAsDomElement){ 48 | let doc = new DOMParser().parseFromString($data.trim(), this.mimeType); 49 | 50 | return doc.firstChild; 51 | } 52 | 53 | if(this.options.outputBase64){ 54 | $data = this.toBase64DataURI($data, this.mimeType); 55 | } 56 | 57 | return $data; 58 | } 59 | 60 | /** 61 | * @protected 62 | */ 63 | createMarkup($saveToFile){ 64 | let $svg = this.header(); 65 | let $eol = this.options.eol; 66 | 67 | if(this.options.svgDefs){ 68 | let $s1 = this.options.svgDefs; 69 | 70 | $svg += `${$s1}${$eol}${$eol}`; 71 | } 72 | 73 | $svg += this.paths(); 74 | 75 | // close svg 76 | $svg += `${$eol}${$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 | * @returns {string} 88 | * @protected 89 | */ 90 | getViewBox(){ 91 | let [$width, $height] = this.getOutputDimensions(); 92 | 93 | return `0 0 ${$width} ${$height}`; 94 | } 95 | 96 | /** 97 | * returns the header with the given options parsed 98 | * 99 | * @returns {string} 100 | * @protected 101 | */ 102 | header(){ 103 | 104 | let $header = `${this.options.eol}`; 106 | 107 | if(this.options.svgAddXmlHeader){ 108 | $header = `${this.options.eol}${$header}`; 109 | } 110 | 111 | return $header; 112 | } 113 | 114 | /** 115 | * returns one or more SVG elements 116 | * 117 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path 118 | * 119 | * @returns {string} 120 | * @protected 121 | */ 122 | paths(){ 123 | let $paths = this.collectModules(($x, $y, $M_TYPE) => this.module($x, $y, $M_TYPE)); 124 | let $svg = []; 125 | 126 | // create the path elements 127 | for(let $M_TYPE in $paths){ 128 | // limit the total line length 129 | let $chunkSize = 100; 130 | let $chonks = []; 131 | 132 | for(let $i = 0; $i < $paths[$M_TYPE].length; $i += $chunkSize){ 133 | $chonks.push($paths[$M_TYPE].slice($i, $i + $chunkSize).join(' ')); 134 | } 135 | 136 | let $path = $chonks.join(this.options.eol); 137 | 138 | if($path.trim() === ''){ 139 | continue; 140 | } 141 | 142 | $svg.push(this.path($path, $M_TYPE)); 143 | } 144 | 145 | return $svg.join(this.options.eol); 146 | } 147 | 148 | /** 149 | * renders and returns a single element 150 | * 151 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path 152 | * 153 | * @param {string} $path 154 | * @param {number|int} $M_TYPE 155 | * @returns {string} 156 | * @protected 157 | */ 158 | path($path, $M_TYPE){ 159 | let $cssClass = this.getCssClass($M_TYPE); 160 | 161 | if(this.options.svgUseFillAttributes){ 162 | return ``; 164 | } 165 | 166 | return ``; 167 | } 168 | 169 | /** 170 | * returns a path segment for a single module 171 | * 172 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d 173 | * 174 | * @param {number|int} $x 175 | * @param {number|int} $y 176 | * @param {number|int} $M_TYPE 177 | * 178 | * @returns {string} 179 | * @protected 180 | */ 181 | module($x, $y, $M_TYPE){ 182 | 183 | if(!this.options.drawLightModules && !this.matrix.check($x, $y)){ 184 | return ''; 185 | } 186 | 187 | if(this.options.drawCircularModules && !this.matrix.checkTypeIn($x, $y, this.options.keepAsSquare)){ 188 | // some values come with the usual JS float fun and i won't do shit about it 189 | let r = parseFloat(this.options.circleRadius); 190 | let d = (r * 2); 191 | let ix = ($x + 0.5 - r); 192 | let iy = ($y + 0.5); 193 | 194 | if(ix < 1){ 195 | ix = ix.toPrecision(3); 196 | } 197 | 198 | if(iy < 1){ 199 | iy = iy.toPrecision(3); 200 | } 201 | 202 | return `M${ix} ${iy} a${r} ${r} 0 1 0 ${d} 0 a${r} ${r} 0 1 0 -${d} 0Z`; 203 | } 204 | 205 | return `M${$x} ${$y} h1 v1 h-1Z`; 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/Output/QROutputAbstract.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QROutputInterface from './QROutputInterface.js'; 9 | import PHPJS from '../Common/PHPJS.js'; 10 | import {IS_DARK, M_DATA, DEFAULT_MODULE_VALUES} from '../Common/constants.js'; 11 | import QRCodeOutputException from './QRCodeOutputException.js'; 12 | 13 | /** 14 | * common output abstract 15 | * @abstract 16 | */ 17 | export default class QROutputAbstract extends QROutputInterface{ 18 | 19 | /** 20 | * the current size of the QR matrix 21 | * 22 | * @see QRMatrix.getSize() 23 | * @type {number|int} 24 | * @protected 25 | */ 26 | moduleCount; 27 | 28 | /** 29 | * an (optional) array of color values for the several QR matrix parts 30 | * @type {Object.} 31 | * @protected 32 | */ 33 | moduleValues = {}; 34 | 35 | /** 36 | * the current scaling for a QR pixel 37 | * 38 | * @see QROptions.$scale 39 | * @type {number|int} 40 | * @protected 41 | */ 42 | scale; 43 | 44 | /** 45 | * the side length of the QR image (modules * scale) 46 | * @type {number|int} 47 | * @protected 48 | */ 49 | length; 50 | 51 | /** 52 | * the (filled) data matrix object 53 | * @type {QRMatrix} 54 | * @protected 55 | */ 56 | matrix; 57 | 58 | /** 59 | * @type {QROptions} 60 | * @protected 61 | */ 62 | options; 63 | 64 | /** 65 | * QROutputAbstract constructor. 66 | * @param {QROptions} $options 67 | * @param {QRMatrix} $matrix 68 | */ 69 | constructor($options, $matrix){ 70 | super(); 71 | 72 | this.options = $options; 73 | this.matrix = $matrix; 74 | 75 | this.setMatrixDimensions(); 76 | this.setModuleValues(); 77 | } 78 | 79 | /** 80 | * Sets/updates the matrix dimensions 81 | * 82 | * Call this method if you modify the matrix from within your custom module in case the dimensions have been changed 83 | * 84 | * @returns {void} 85 | * @protected 86 | */ 87 | setMatrixDimensions(){ 88 | this.moduleCount = this.matrix.getSize(); 89 | this.scale = this.options.scale; 90 | this.length = this.moduleCount * this.scale; 91 | } 92 | 93 | /** 94 | * Returns a 2 element array with the current output width and height 95 | * 96 | * The type and units of the values depend on the output class. The default value is the current module count. 97 | * 98 | * @returna {array} 99 | * @protected 100 | */ 101 | getOutputDimensions(){ 102 | return [this.moduleCount, this.moduleCount]; 103 | } 104 | 105 | /** 106 | * Sets the initial module values 107 | * 108 | * @returns {void} 109 | * @protected 110 | */ 111 | setModuleValues(){ 112 | let $M_TYPE; 113 | 114 | // first fill the map with the default values 115 | for($M_TYPE in DEFAULT_MODULE_VALUES){ 116 | this.moduleValues[$M_TYPE] = this.getDefaultModuleValue(DEFAULT_MODULE_VALUES[$M_TYPE]); 117 | } 118 | 119 | // now loop over the options values to replace defaults and add extra values 120 | for($M_TYPE in this.options.moduleValues){ 121 | let $value = this.options.moduleValues[$M_TYPE]; 122 | 123 | if(this.constructor.moduleValueIsValid($value)){ 124 | this.moduleValues[$M_TYPE] = this.prepareModuleValue($value); 125 | } 126 | } 127 | 128 | } 129 | 130 | /** 131 | * Returns the final value for the given input (return value depends on the output module) 132 | * 133 | * @param {*} $value 134 | * @returna {*} 135 | * @protected 136 | */ 137 | prepareModuleValue($value){ 138 | return $value.replace(/(<([^>]+)>)/gi, '').replace(/([ '"\r\n\t]+)/g, ''); 139 | } 140 | 141 | /** 142 | * Returns a defualt value for either dark or light modules (return value depends on the output module) 143 | * 144 | * @param {boolean} $isDark 145 | * @returna {*} 146 | * @protected 147 | */ 148 | getDefaultModuleValue($isDark){ 149 | return $isDark ? '#000' : '#fff'; 150 | } 151 | 152 | /** 153 | * @inheritDoc 154 | */ 155 | static moduleValueIsValid($value){ 156 | 157 | if(typeof $value !== 'string'){ 158 | return false; 159 | } 160 | 161 | $value = $value.trim(); 162 | 163 | // hex notation 164 | // #rgb(a) 165 | // #rrggbb(aa) 166 | if($value.match(/^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i)){ 167 | return true; 168 | } 169 | 170 | // css: hsla/rgba(...values) 171 | if($value.match(/^(hsla?|rgba?)\([\d .,%\/]+\)$/i)){ 172 | return true; 173 | } 174 | 175 | // url(...) 176 | if($value.match(/^url\([-\/#a-z\d]+\)$/i)){ 177 | return true; 178 | } 179 | 180 | // predefined css color 181 | if($value.match(/^[a-z]+$/i)){ 182 | return true; 183 | } 184 | 185 | return false; 186 | } 187 | 188 | /** 189 | * Returns the prepared value for the given $M_TYPE 190 | * 191 | * @throws {QRCodeOutputException} if $moduleValues[$M_TYPE] doesn't exist 192 | * @param {number|int} $M_TYPE 193 | * @returna {*} 194 | * @protected 195 | */ 196 | getModuleValue($M_TYPE){ 197 | 198 | if(!PHPJS.isset(() => this.moduleValues[$M_TYPE])){ 199 | throw new QRCodeOutputException(`$M_TYPE "${$M_TYPE.toString(2).padStart(12, '0')}" not found in module values map`); 200 | } 201 | 202 | return this.moduleValues[$M_TYPE]; 203 | } 204 | 205 | /** 206 | * Returns the prepared module value at the given coordinate [$x, $y] (convenience) 207 | * 208 | * @param {number|int} $x 209 | * @param {number|int} $y 210 | * @returna {*} 211 | * @protected 212 | */ 213 | getModuleValueAt($x, $y){ 214 | return this.getModuleValue(this.matrix.get($x, $y)); 215 | } 216 | 217 | /** 218 | * Returns a base64 data URI for the given string and mime type 219 | * 220 | * @param {string} $data 221 | * @param {string} $mime 222 | * @returna {string} 223 | * @throws {QRCodeOutputException} 224 | * @protected 225 | */ 226 | toBase64DataURI($data, $mime){ 227 | $mime = ($mime ?? this.mimeType).trim(); 228 | 229 | if($mime === ''){ 230 | throw new QRCodeOutputException('invalid mime type given'); 231 | } 232 | 233 | return `data:${$mime};base64,${btoa($data)}`; 234 | } 235 | 236 | /** 237 | * saves the qr data to a file 238 | * 239 | * @see file_put_contents() 240 | * @see QROptions.cachefile 241 | * 242 | * @param {string} $data 243 | * @param {string} $file 244 | * @returns {void} 245 | * @throws QRCodeOutputException 246 | * @protected 247 | */ 248 | saveToFile($data, $file){ 249 | 250 | if($file === null){ 251 | return; 252 | } 253 | 254 | // @todo 255 | } 256 | 257 | /** 258 | * collects the modules per QRMatrix.M_* type and runs a $transform functio on each module and 259 | * returns an array with the transformed modules 260 | * 261 | * The transform callback is called with the following parameters: 262 | * 263 | * $x - current column 264 | * $y - current row 265 | * $M_TYPE - field value 266 | * $M_TYPE_LAYER - (possibly modified) field value that acts as layer id 267 | * 268 | * @param {function} $transform 269 | * @returns {Object<{}>} 270 | * @protected 271 | */ 272 | collectModules($transform){ 273 | let $paths = {}; 274 | let $matrix = this.matrix.getMatrix(); 275 | let $y = 0; 276 | 277 | // collect the modules for each type 278 | for(let $row of $matrix){ 279 | let $x = 0; 280 | 281 | for(let $M_TYPE of $row){ 282 | let $M_TYPE_LAYER = $M_TYPE; 283 | 284 | if(this.options.connectPaths && !this.matrix.checkTypeIn($x, $y, this.options.excludeFromConnect)){ 285 | // to connect paths we'll redeclare the $M_TYPE_LAYER to data only 286 | $M_TYPE_LAYER = M_DATA; 287 | 288 | if(this.matrix.check($x, $y)){ 289 | $M_TYPE_LAYER |= IS_DARK; 290 | } 291 | } 292 | 293 | // collect the modules per $M_TYPE 294 | let $module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER); 295 | 296 | if($module){ 297 | if(!$paths[$M_TYPE_LAYER]){ 298 | $paths[$M_TYPE_LAYER] = []; 299 | } 300 | 301 | $paths[$M_TYPE_LAYER].push($module); 302 | } 303 | $x++; 304 | } 305 | $y++; 306 | } 307 | 308 | // beautify output 309 | // ksort($paths); 310 | 311 | return $paths; 312 | } 313 | 314 | } 315 | -------------------------------------------------------------------------------- /src/Output/QROutputInterface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 14.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | /** 9 | * @interface 10 | */ 11 | export default class QROutputInterface{ 12 | 13 | /** 14 | * @type {string} 15 | * @protected 16 | * @see QROutputAbstract.toBase64DataURI() 17 | */ 18 | mimeType; 19 | 20 | 21 | /** 22 | * Determines whether the given value is valid 23 | * 24 | * @param {*} $value 25 | * @returns {boolean} 26 | * @abstract 27 | */ 28 | static moduleValueIsValid($value){} 29 | 30 | /** 31 | * generates the output, optionally dumps it to a file, and returns it 32 | * 33 | * @param {string|null} $file 34 | * @return {*} 35 | * @abstract 36 | */ 37 | dump($file = null){} 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Output/QRStringJSON.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QROutputAbstract from './QROutputAbstract.js'; 9 | import PHPJS from '../Common/PHPJS.js'; 10 | import {LAYERNAMES} from '../Common/constants.js'; 11 | 12 | /** 13 | * Converts the matrix data into string types 14 | */ 15 | export default class QRStringJSON extends QROutputAbstract{ 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | mimeType = 'application/json'; 21 | 22 | /** 23 | * the json schema 24 | * 25 | * @type {string} 26 | * @protected 27 | */ 28 | schema = 'https://raw.githubusercontent.com/chillerlan/php-qrcode/main/src/Output/qrcode.schema.json'; 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | dump($file){ 34 | let [width, height] = this.getOutputDimensions(); 35 | let version = this.matrix.getVersion(); 36 | let dimension = version.getDimension(); 37 | 38 | 39 | let $json = { 40 | $schema: this.schema, 41 | qrcode: { 42 | version: version.getVersionNumber(), 43 | eccLevel: this.matrix.getEccLevel().toString(), 44 | matrix: { 45 | size: dimension, 46 | quietzoneSize: PHPJS.intval((this.moduleCount - dimension) / 2), 47 | maskPattern: this.matrix.getMaskPattern().getPattern(), 48 | width: width, 49 | height: height, 50 | rows: [], 51 | } 52 | } 53 | }; 54 | 55 | let matrix = this.matrix.getMatrix(); 56 | 57 | for(let y in matrix){ 58 | let matrixRow = this.row(y, matrix[y]); 59 | 60 | if(matrixRow !== null){ 61 | $json.qrcode.matrix.rows.push(matrixRow); 62 | } 63 | } 64 | 65 | let $data = JSON.stringify($json); 66 | 67 | this.saveToFile($data, $file); 68 | 69 | return $data; 70 | } 71 | 72 | /** 73 | * Creates an array element for a matrix row 74 | * 75 | * @returns {*} 76 | * @protected 77 | */ 78 | row($y, $row){ 79 | let matrixRow = {y: $y, modules: []}; 80 | 81 | for(let x in $row){ 82 | let module = this.module(x, $y, $row[x]); 83 | 84 | if(module !== null){ 85 | matrixRow.modules.push(module); 86 | } 87 | } 88 | 89 | if(matrixRow.modules.length){ 90 | return matrixRow; 91 | } 92 | 93 | // skip empty rows 94 | return null; 95 | } 96 | 97 | /** 98 | * Creates an array element for a single module 99 | * 100 | * @returns {*} 101 | * @protected 102 | */ 103 | module($x, $y, $M_TYPE){ 104 | let isDark = this.matrix.isDark($M_TYPE); 105 | 106 | if(!this.options.drawLightModules && !isDark){ 107 | return null; 108 | } 109 | 110 | return { 111 | x: $x, 112 | dark: isDark, 113 | layer: (LAYERNAMES[$M_TYPE] ?? ''), 114 | value: this.getModuleValue($M_TYPE), 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Output/QRStringText.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QROutputAbstract from './QROutputAbstract.js'; 9 | 10 | /** 11 | * Converts the matrix data into string types 12 | */ 13 | export default class QRStringText extends QROutputAbstract{ 14 | 15 | /** 16 | * @inheritDoc 17 | */ 18 | mimeType = 'text/plain'; 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | getDefaultModuleValue($isDark){ 24 | return $isDark ? '██' : '░░'; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | prepareModuleValue($value){ 31 | return $value; 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | static moduleValueIsValid($value){ 38 | return typeof $value === 'string'; 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | dump($file){ 45 | let $str = []; 46 | 47 | for(let $y = 0; $y < this.moduleCount; $y++){ 48 | let $row = []; 49 | 50 | for(let $x = 0; $x < this.moduleCount; $x++){ 51 | $row.push(this.moduleValues[this.matrix.get($x, $y)]); 52 | } 53 | 54 | $str.push($row.join('')); 55 | } 56 | 57 | let $data = $str.join(this.options.eol); 58 | 59 | this.saveToFile($data, $file); 60 | 61 | return $data; 62 | } 63 | 64 | /** 65 | * 66 | * @param {string} $str 67 | * @param {number|int} $color 68 | * @param {boolean} $background 69 | * @returns {string} 70 | */ 71 | static ansi8($str, $color, $background = null){ 72 | $color = Math.max(0, Math.min($color, 255)); 73 | 74 | return `\x1b[${($background === true ? 48 : 38)};5;${$color}m${$str}\x1b[0m`; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/QRCode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QROptions from './QROptions.js'; 9 | import MaskPattern from './Common/MaskPattern.js'; 10 | import AlphaNum from './Data/AlphaNum.js'; 11 | import Byte from './Data/Byte.js'; 12 | import Numeric from './Data/Numeric.js'; 13 | import QRData from './Data/QRData.js'; 14 | import QRCodeOutputException from './Output/QRCodeOutputException.js'; 15 | import QROutputInterface from './Output/QROutputInterface.js'; 16 | import PHPJS from './Common/PHPJS.js'; 17 | import { 18 | MASK_PATTERN_AUTO, MODE_ALPHANUM, MODE_BYTE, MODE_NUMBER 19 | } from './Common/constants.js'; 20 | 21 | /** 22 | * Map of data mode => interface (detection order) 23 | * 24 | * @type {string[]} 25 | */ 26 | const MODE_INTERFACES = PHPJS.array_combine([MODE_NUMBER, MODE_ALPHANUM, MODE_BYTE], [Numeric, AlphaNum, Byte]); 27 | 28 | /** 29 | * Turns a text string into a Model 2 QR Code 30 | * 31 | * @see https://github.com/chillerlan/php-qrcode 32 | * @see https://github.com/kazuhikoarase/qrcode-generator/tree/master/php 33 | * @see http://www.qrcode.com/en/codes/model12.html 34 | * @see https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf 35 | * @see https://en.wikipedia.org/wiki/QR_code 36 | * @see http://www.thonky.com/qr-code-tutorial/ 37 | * @see https://jamie.build/const 38 | */ 39 | export default class QRCode{ 40 | 41 | /** 42 | * @type {QROptions} 43 | * @protected 44 | */ 45 | options; 46 | 47 | /** 48 | * @type {Array} 49 | * @protected 50 | */ 51 | dataSegments = []; 52 | 53 | /** 54 | * QRCode constructor. 55 | * 56 | * @param {QROptions} $options 57 | */ 58 | constructor($options){ 59 | this.setOptions($options); 60 | } 61 | 62 | /** 63 | * Sets an options instance 64 | * 65 | * @param {QROptions} $options 66 | * 67 | * @returns {QRCode} 68 | */ 69 | setOptions($options){ 70 | 71 | if(!($options instanceof QROptions)){ 72 | $options = new QROptions(); 73 | } 74 | 75 | this.options = $options; 76 | 77 | return this; 78 | } 79 | 80 | /** 81 | * Renders a QR Code for the given $data and QROptions 82 | * 83 | * @returns {*} 84 | */ 85 | render($data = null, $file = null){ 86 | 87 | if($data !== null){ 88 | for(let $mode in MODE_INTERFACES){ 89 | let $dataInterface = MODE_INTERFACES[$mode]; 90 | 91 | if($dataInterface.validateString($data)){ 92 | this.addSegment(new $dataInterface($data)); 93 | 94 | break; 95 | } 96 | } 97 | } 98 | 99 | return this.renderMatrix(this.getQRMatrix(), $file); 100 | } 101 | 102 | /** 103 | * Renders a QR Code for the given QRMatrix and QROptions, saves $file optionally 104 | * 105 | * @param {QRMatrix} $QRMatrix 106 | * @param {string|null} $file 107 | * @returns {*} 108 | */ 109 | renderMatrix($QRMatrix, $file = null){ 110 | return this.initOutputInterface($QRMatrix).dump($file ?? this.options.cachefile); 111 | } 112 | 113 | /** 114 | * Returns a QRMatrix object for the given $data and current QROptions 115 | * 116 | * @returns {QRMatrix} 117 | * @throws {QRCodeDataException} 118 | */ 119 | getQRMatrix(){ 120 | let $QRMatrix = new QRData(this.options, this.dataSegments).writeMatrix(); 121 | 122 | let $maskPattern = this.options.maskPattern === MASK_PATTERN_AUTO 123 | ? MaskPattern.getBestPattern($QRMatrix) 124 | : new MaskPattern(this.options.maskPattern); 125 | 126 | $QRMatrix.setFormatInfo($maskPattern).mask($maskPattern); 127 | 128 | return this.addMatrixModifications($QRMatrix); 129 | } 130 | 131 | /** 132 | * add matrix modifications after mask pattern evaluation and before handing over to output 133 | * 134 | * @param {QRMatrix} $QRMatrix 135 | * @returns {QRMatrix} 136 | * @protected 137 | */ 138 | addMatrixModifications($QRMatrix){ 139 | 140 | if(this.options.addLogoSpace){ 141 | // check whether one of the dimensions was omitted 142 | let $logoSpaceWidth = (this.options.logoSpaceWidth ?? this.options.logoSpaceHeight ?? 0); 143 | let $logoSpaceHeight = (this.options.logoSpaceHeight ?? $logoSpaceWidth); 144 | 145 | $QRMatrix.setLogoSpace( 146 | $logoSpaceWidth, 147 | $logoSpaceHeight, 148 | this.options.logoSpaceStartX, 149 | this.options.logoSpaceStartY 150 | ); 151 | } 152 | 153 | if(this.options.addQuietzone){ 154 | $QRMatrix.setQuietZone(this.options.quietzoneSize); 155 | } 156 | 157 | return $QRMatrix; 158 | } 159 | 160 | /** 161 | * initializes a fresh built-in or custom QROutputInterface 162 | * 163 | * @param {QRMatrix} $QRMatrix 164 | * @returns {QROutputAbstract} 165 | * @throws {QRCodeOutputException} 166 | * @protected 167 | */ 168 | initOutputInterface($QRMatrix){ 169 | 170 | if(typeof this.options.outputInterface !== 'function'){ 171 | throw new QRCodeOutputException('invalid output class'); 172 | } 173 | 174 | let $outputInterface = new this.options.outputInterface(this.options, $QRMatrix); 175 | 176 | if(!($outputInterface instanceof QROutputInterface)){ 177 | throw new QRCodeOutputException('output class does not implement QROutputInterface'); 178 | } 179 | 180 | return $outputInterface 181 | } 182 | 183 | /** 184 | * Adds a data segment 185 | * 186 | * ISO/IEC 18004:2000 8.3.6 - Mixing modes 187 | * ISO/IEC 18004:2000 Annex H - Optimisation of bit stream length 188 | * 189 | * @returns {QRCode} 190 | */ 191 | addSegment($segment){ 192 | this.dataSegments.push($segment); 193 | 194 | return this; 195 | } 196 | 197 | /** 198 | * Clears the data segments array 199 | * 200 | * @returns {QRCode} 201 | */ 202 | clearSegments(){ 203 | this.dataSegments = []; 204 | 205 | return this; 206 | } 207 | 208 | /** 209 | * Adds a numeric data segment 210 | * 211 | * ISO/IEC 18004:2000 8.3.2 - Numeric Mode 212 | * 213 | * @returns {QRCode} 214 | */ 215 | addNumericSegment($data){ 216 | this.addSegment(new Numeric($data)); 217 | 218 | return this; 219 | } 220 | 221 | /** 222 | * Adds an alphanumeric data segment 223 | * 224 | * ISO/IEC 18004:2000 8.3.3 - Alphanumeric Mode 225 | * 226 | * @returns {QRCode} 227 | */ 228 | addAlphaNumSegment($data){ 229 | this.addSegment(new AlphaNum($data)); 230 | 231 | return this; 232 | } 233 | 234 | /** 235 | * Adds an 8-bit byte data segment 236 | * 237 | * ISO/IEC 18004:2000 8.3.4 - 8-bit Byte Mode 238 | * 239 | * @returns {QRCode} 240 | */ 241 | addByteSegment($data){ 242 | this.addSegment(new Byte($data)); 243 | 244 | return this; 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/QRCodeException.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | export default class QRCodeException extends Error{ 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/QROptions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import QRCodeException from './QRCodeException.js'; 9 | import PHPJS from './Common/PHPJS.js'; 10 | import { 11 | ECC_H, ECC_L, ECC_M, ECC_Q, MASK_PATTERN_AUTO, VERSION_AUTO, 12 | } from './Common/constants.js'; 13 | import QRMarkupSVG from './Output/QRMarkupSVG.js'; 14 | 15 | /** 16 | * The QRCode plug-in settings & setter functionality 17 | */ 18 | export default class QROptions{ 19 | 20 | /** 21 | * QR Code version number 22 | * 23 | * [1 ... 40] or QRCode.VERSION_AUTO 24 | * 25 | * @type {number|int} 26 | * @protected 27 | */ 28 | _version = VERSION_AUTO; 29 | 30 | /** 31 | * Minimum QR version 32 | * 33 | * if $version = QRCode.VERSION_AUTO 34 | * 35 | * @type {number|int} 36 | * @protected 37 | */ 38 | _versionMin = 1; 39 | 40 | /** 41 | * Maximum QR version 42 | * 43 | * @type {number|int} 44 | * @protected 45 | */ 46 | _versionMax = 40; 47 | 48 | /** 49 | * Error correct level 50 | * 51 | * QRCode::ECC_X where X is: 52 | * 53 | * - L => 7% 54 | * - M => 15% 55 | * - Q => 25% 56 | * - H => 30% 57 | * 58 | * @type {number|int} 59 | * @protected 60 | */ 61 | _eccLevel = ECC_L; 62 | 63 | /** 64 | * Mask Pattern to use (no value in using, mostly for unit testing purposes) 65 | * 66 | * [0...7] or QRCode::MASK_PATTERN_AUTO 67 | * 68 | * @type {number|int} 69 | * @protected 70 | */ 71 | _maskPattern = MASK_PATTERN_AUTO; 72 | 73 | /** 74 | * Add a "quiet zone" (margin) according to the QR code spec 75 | * 76 | * @see https://www.qrcode.com/en/howto/code.html 77 | * 78 | * @type {boolean} 79 | */ 80 | addQuietzone = true; 81 | 82 | /** 83 | * Size of the quiet zone 84 | * 85 | * internally clamped to [0 ... $moduleCount / 2], defaults to 4 modules 86 | * 87 | * @type {number|int} 88 | * @protected 89 | */ 90 | _quietzoneSize = 4; 91 | 92 | /** 93 | * the FQCN of the custom QROutputInterface if $outputType is set to QRCode::OUTPUT_CUSTOM 94 | * 95 | * @type {string|null} 96 | */ 97 | outputInterface = QRMarkupSVG; 98 | 99 | /** 100 | * /path/to/cache.file 101 | * 102 | * @type {string|null} 103 | */ 104 | cachefile = null; 105 | 106 | /** 107 | * newline string [HTML, SVG, TEXT] 108 | * 109 | * @type {string} 110 | */ 111 | eol = '\n'; 112 | 113 | /** 114 | * size of a QR code module in pixels [SVG, IMAGE_*], HTML via CSS 115 | * 116 | * @type {number|int} 117 | */ 118 | scale = 5; 119 | 120 | /** 121 | * a common css class 122 | * 123 | * @type {string} 124 | */ 125 | cssClass = 'qrcode'; 126 | 127 | /** 128 | * SVG opacity 129 | * 130 | * @type {number|float} 131 | */ 132 | svgOpacity = 1.0; 133 | 134 | /** 135 | * anything between 136 | * 137 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs 138 | * 139 | * @type {string} 140 | */ 141 | svgDefs = ''; 142 | 143 | /** 144 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio 145 | * 146 | * @type {string} 147 | */ 148 | svgPreserveAspectRatio = 'xMidYMid'; 149 | 150 | /** 151 | * Whether to add an XML header line or not, e.g. to embed the SVG directly in HTML 152 | * 153 | * `` 154 | * 155 | * @type {boolean} 156 | */ 157 | svgAddXmlHeader = false; 158 | 159 | /** 160 | * Whether to use the SVG `fill` attributes 161 | * 162 | * If set to `true` (default), the `fill` attribute will be set with the module value for the `` element's `$M_TYPE`. 163 | * When set to `false`, the module values map will be ignored and the QR Code may be styled via CSS. 164 | * 165 | * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill 166 | * 167 | * @type {boolean} 168 | */ 169 | svgUseFillAttributes = true; 170 | 171 | /** 172 | * whether to connect the paths for the several module types to avoid weird glitches when using gradients etc. 173 | * 174 | * @see https://github.com/chillerlan/php-qrcode/issues/57 175 | * 176 | * @type {boolean} 177 | */ 178 | connectPaths = false; 179 | 180 | /** 181 | * specify which paths/patterns to exclude from connecting if $svgConnectPaths is set to true 182 | * 183 | * @type {number[]|int[]} 184 | */ 185 | excludeFromConnect = []; 186 | 187 | /** 188 | * specify whether to draw the modules as filled circles 189 | * 190 | * a note for GDImage output: 191 | * 192 | * if QROptions::$scale is less or equal than 20, the image will be upscaled internally, then the modules will be drawn 193 | * using imagefilledellipse() and then scaled back to the expected size using IMG_BICUBIC which in turn produces 194 | * unexpected outcomes in combination with transparency - to avoid this, set scale to a value greater than 20. 195 | * 196 | * @see https://github.com/chillerlan/php-qrcode/issues/23 197 | * @see https://github.com/chillerlan/php-qrcode/discussions/122 198 | * 199 | * @type {boolean} 200 | */ 201 | drawCircularModules = false; 202 | 203 | /** 204 | * specifies the radius of the modules when $svgDrawCircularModules is set to true 205 | * 206 | * @type {number|float} 207 | * @protected 208 | */ 209 | _circleRadius = 0.45; 210 | 211 | /** 212 | * specifies which module types to exclude when $svgDrawCircularModules is set to true 213 | * 214 | * @type {number[]|int[]} 215 | */ 216 | keepAsSquare = []; 217 | 218 | /** 219 | * toggle base64 or raw image data 220 | * 221 | * @type {boolean} 222 | */ 223 | outputBase64 = true; 224 | 225 | /** 226 | * toggle background transparency 227 | * 228 | * if transparency is disabled, a background color should be specified to avoid unexpected outcomes 229 | * 230 | * @see QROptions.bgcolor 231 | * 232 | * @type {boolean} 233 | */ 234 | imageTransparent = true; 235 | 236 | /** 237 | * whether to draw the light (false) modules 238 | * 239 | * @type {boolean} 240 | */ 241 | drawLightModules = true; 242 | 243 | /** 244 | * Module values map 245 | * 246 | * - HTML, IMAGICK: #ABCDEF, cssname, rgb(), rgba()... 247 | * - IMAGE: [63, 127, 255] // R, G, B 248 | * 249 | * @type {Object.|null} 250 | */ 251 | moduleValues = null; 252 | 253 | /** 254 | * Toggles logo space creation 255 | * 256 | * @type {boolean} 257 | */ 258 | addLogoSpace = false; 259 | 260 | /** 261 | * width of the logo space 262 | * 263 | * @type {number|int|null} 264 | * @protected 265 | */ 266 | _logoSpaceWidth = null; 267 | 268 | /** 269 | * height of the logo space 270 | * 271 | * @type {number|int|null} 272 | * @protected 273 | */ 274 | _logoSpaceHeight = null; 275 | 276 | /** 277 | * optional horizontal start position of the logo space (top left corner) 278 | * 279 | * @type {number|int|null} 280 | * @protected 281 | */ 282 | _logoSpaceStartX = null; 283 | 284 | /** 285 | * optional vertical start position of the logo space (top left corner) 286 | * 287 | * @type {number|int|null} 288 | * @protected 289 | */ 290 | _logoSpaceStartY = null; 291 | 292 | /** 293 | * whether to return the markup as DOM element 294 | * 295 | * @type {boolean} 296 | */ 297 | returnAsDomElement = true; 298 | 299 | /** 300 | * background color 301 | * 302 | * supported in: 303 | * 304 | * - OUTPUT_CANVAS 305 | * 306 | * @type {*|null} 307 | */ 308 | bgcolor = null; 309 | 310 | /** 311 | * the canvas HTML element (canvas output only) 312 | * 313 | * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement 314 | * 315 | * @type {HTMLCanvasElement} 316 | */ 317 | canvasElement = null; 318 | 319 | /** 320 | * canvas image mime type for bas64/file output 321 | * 322 | * the value may be one of the following (depends on browser/engine): 323 | * 324 | * - png 325 | * - jpeg 326 | * - bmp 327 | * - webp 328 | * 329 | * the "image/" is prepended internally 330 | * 331 | * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL 332 | * 333 | * @type {string} 334 | * @protected 335 | */ 336 | _canvasMimeType = 'image/png'; 337 | 338 | /** 339 | * canvas image quality 340 | * 341 | * "A number between 0 and 1 indicating the image quality to be used when creating images 342 | * using file formats that support lossy compression (such as image/jpeg or image/webp). 343 | * A user agent will use its default quality value if this option is not specified, 344 | * or if the number is outside the allowed range." 345 | * 346 | * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL 347 | * 348 | * @type {number|float} 349 | */ 350 | canvasImageQuality = 0.85; 351 | 352 | /** 353 | * because javascript is dumb, and we can't call getters and setters directly we have to do this silly workaround. 354 | * if your inherited options class uses magic getters and setters, add the relevant property names to this array 355 | * and call _fromIterable() afterwards: 356 | * 357 | * constructor($options = null){ 358 | * super(); 359 | * this.__workaround__.push('myMagicProp'); 360 | * this._fromIterable($options) 361 | * } 362 | * 363 | * 364 | * let o = new MyExtendedOptions({myMagicProp: 'foo', ...}); 365 | * 366 | * note; for some reason we need to add the constructor with a parent call in extended classes even without 367 | * the aforementioned workaround, otherwise the additional properties will not be recognized. wtfjs??? 368 | * 369 | * @protected 370 | */ 371 | __workaround__ = [ 372 | 'canvasMimeType', 373 | 'circleRadius', 374 | 'eccLevel', 375 | 'logoSpaceHeight', 376 | 'logoSpaceWidth', 377 | 'logoSpaceStartX', 378 | 'logoSpaceStartY', 379 | 'maskPattern', 380 | 'quietzoneSize', 381 | 'version', 382 | 'versionMin', 383 | 'versionMax', 384 | ]; 385 | 386 | /** 387 | * @param {Object<{}>|null} $options 388 | */ 389 | constructor($options = null){ 390 | this._fromIterable($options); 391 | } 392 | 393 | /** 394 | * @param {Object<{}>} $options 395 | * @returns {void} 396 | * @protected 397 | */ 398 | _fromIterable($options){ 399 | 400 | if(Object.prototype.toString.call($options) !== '[object Object]'){ 401 | return; 402 | } 403 | 404 | Object.keys($options).forEach($property => { 405 | if(this.__workaround__.includes($property)){ 406 | this['_set_'+$property]($options[$property]); 407 | } 408 | // since Object.prototype.hasOwnProperty.call(this, $property) will cause issues with extended classes, 409 | // we'll just check if the property is defined. have i mentioned yet how much i loathe javascript? 410 | else if(this[$property] !== undefined){ 411 | this[$property] = $options[$property]; 412 | } 413 | }); 414 | 415 | } 416 | 417 | /** 418 | * clamp min/max version number 419 | * 420 | * @param {number|int} $versionMin 421 | * @param {number|int} $versionMax 422 | * 423 | * @returns {void} 424 | * 425 | * @protected 426 | */ 427 | setMinMaxVersion($versionMin, $versionMax){ 428 | let $min = Math.max(1, Math.min(40, $versionMin)); 429 | let $max = Math.max(1, Math.min(40, $versionMax)); 430 | 431 | this._versionMin = Math.min($min, $max); 432 | this._versionMax = Math.max($min, $max); 433 | } 434 | 435 | /** 436 | * sets the minimum version number 437 | * 438 | * @param {number|int} $versionMin 439 | * 440 | * @returns {void} 441 | * @protected 442 | */ 443 | _set_versionMin($versionMin){ 444 | this.setMinMaxVersion($versionMin, this._versionMax); 445 | } 446 | 447 | set versionMin($versionMin){ 448 | this._set_versionMin($versionMin); 449 | } 450 | 451 | get versionMin(){ 452 | return this._versionMin; 453 | } 454 | 455 | /** 456 | * sets the maximum version number 457 | * 458 | * @param {number|int} $versionMax 459 | * 460 | * @returns {void} 461 | * @protected 462 | */ 463 | _set_versionMax($versionMax){ 464 | this.setMinMaxVersion(this._versionMin, $versionMax); 465 | } 466 | 467 | set versionMax($versionMax){ 468 | this._set_versionMax($versionMax); 469 | } 470 | 471 | get versionMax(){ 472 | return this._versionMax; 473 | } 474 | 475 | /** 476 | * sets/clamps the version number 477 | * 478 | * @param {number|int} $version 479 | * 480 | * @returns {void} 481 | * @protected 482 | */ 483 | _set_version($version){ 484 | this._version = $version !== VERSION_AUTO ? Math.max(1, Math.min(40, $version)) : VERSION_AUTO; 485 | } 486 | 487 | set version($version){ 488 | this._set_version($version); 489 | } 490 | 491 | get version(){ 492 | return this._version; 493 | } 494 | 495 | /** 496 | * sets the error correction level 497 | * 498 | * @param {number|int} $eccLevel 499 | * 500 | * @returns {void} 501 | * @throws QRCodeException 502 | * @protected 503 | */ 504 | _set_eccLevel($eccLevel){ 505 | 506 | if(![ECC_L, ECC_M, ECC_Q, ECC_H].includes($eccLevel)){ 507 | throw new QRCodeException(`Invalid error correct level: ${$eccLevel}`); 508 | } 509 | 510 | this._eccLevel = $eccLevel; 511 | } 512 | 513 | set eccLevel($eccLevel){ 514 | this._set_eccLevel($eccLevel); 515 | } 516 | 517 | get eccLevel(){ 518 | return this._eccLevel; 519 | } 520 | 521 | /** 522 | * sets/clamps the mask pattern 523 | * 524 | * @param {number|int} $maskPattern 525 | * 526 | * @returns {void} 527 | * @protected 528 | */ 529 | _set_maskPattern($maskPattern){ 530 | 531 | if($maskPattern !== MASK_PATTERN_AUTO){ 532 | this._maskPattern = Math.max(0, Math.min(7, $maskPattern)); 533 | } 534 | 535 | } 536 | 537 | set maskPattern($maskPattern){ 538 | this._set_maskPattern($maskPattern); 539 | } 540 | 541 | get maskPattern(){ 542 | return this._maskPattern; 543 | } 544 | 545 | /** 546 | * sets/clamps the quiet zone size 547 | * 548 | * @param {number|int} $quietzoneSize 549 | * 550 | * @returns {void} 551 | * @protected 552 | */ 553 | _set_quietzoneSize($quietzoneSize){ 554 | this._quietzoneSize = Math.max(0, Math.min($quietzoneSize, 75)); 555 | } 556 | 557 | set quietzoneSize($quietzoneSize){ 558 | this._set_quietzoneSize($quietzoneSize) ; 559 | } 560 | 561 | get quietzoneSize(){ 562 | return this._quietzoneSize; 563 | } 564 | 565 | /** 566 | * clamp the logo space values between 0 and maximum length (177 modules at version 40) 567 | * 568 | * @param {number|int} $value 569 | * 570 | * @returns {number|int} 571 | * @protected 572 | */ 573 | clampLogoSpaceValue($value){ 574 | $value = PHPJS.intval($value); 575 | 576 | return Math.max(0, Math.min(177, $value)); 577 | } 578 | 579 | /** 580 | * clamp/set logo space width 581 | * 582 | * @param {number|int} $width 583 | * 584 | * @returns {void} 585 | * @protected 586 | */ 587 | _set_logoSpaceWidth($width){ 588 | this._logoSpaceWidth = this.clampLogoSpaceValue($width); 589 | } 590 | 591 | set logoSpaceWidth($width){ 592 | this._set_logoSpaceWidth($width); 593 | } 594 | 595 | get logoSpaceWidth(){ 596 | return this._logoSpaceWidth; 597 | } 598 | 599 | /** 600 | * clamp/set logo space height 601 | * 602 | * @param {number|int} $height 603 | * 604 | * @returns {void} 605 | * @protected 606 | */ 607 | _set_logoSpaceHeight($height){ 608 | this._logoSpaceHeight = this.clampLogoSpaceValue($height); 609 | } 610 | 611 | set logoSpaceHeight($height){ 612 | this._set_logoSpaceHeight($height); 613 | } 614 | 615 | get logoSpaceHeight(){ 616 | return this._logoSpaceHeight; 617 | } 618 | 619 | /** 620 | * clamp/set horizontal logo space start 621 | * 622 | * @param {number|int|null} $startX 623 | * 624 | * @returns {void} 625 | * @protected 626 | */ 627 | _set_logoSpaceStartX($startX){ 628 | this._logoSpaceStartX = (typeof $startX === 'undefined' || $startX === null) ? null : this.clampLogoSpaceValue($startX); 629 | } 630 | 631 | set logoSpaceStartX($startX){ 632 | this._set_logoSpaceStartX($startX); 633 | } 634 | 635 | get logoSpaceStartX(){ 636 | return this._logoSpaceStartX; 637 | } 638 | 639 | /** 640 | * clamp/set vertical logo space start 641 | * 642 | * @param {number|int|null} $startY 643 | * 644 | * @returns {void} 645 | * @protected 646 | */ 647 | _set_logoSpaceStartY($startY){ 648 | this._logoSpaceStartY = (typeof $startY === 'undefined' || $startY === null) ? null : this.clampLogoSpaceValue($startY); 649 | } 650 | 651 | set logoSpaceStartY($startY){ 652 | this._set_logoSpaceStartY($startY); 653 | } 654 | 655 | get logoSpaceStartY(){ 656 | return this._logoSpaceStartY; 657 | } 658 | 659 | /** 660 | * clamp/set SVG circle radius 661 | * 662 | * @param {number|float} $circleRadius 663 | * 664 | * @returns {void} 665 | * @protected 666 | */ 667 | _set_circleRadius($circleRadius){ 668 | this._circleRadius = Math.max(0.1, Math.min(0.75, $circleRadius)); 669 | } 670 | 671 | set circleRadius($circleRadius){ 672 | this._set_circleRadius($circleRadius); 673 | } 674 | 675 | get circleRadius(){ 676 | return this._circleRadius; 677 | } 678 | 679 | /** 680 | * set canvas image type 681 | * 682 | * @param {string} $canvasImageType 683 | * 684 | * @returns {void} 685 | * @protected 686 | */ 687 | _set_canvasMimeType($canvasImageType){ 688 | $canvasImageType = $canvasImageType.toLowerCase(); 689 | 690 | if(!['bmp', 'jpeg', 'png', 'webp'].includes($canvasImageType)){ 691 | throw new QRCodeException(`Invalid canvas image type: ${$canvasImageType}`); 692 | } 693 | 694 | this._canvasMimeType = `image/${$canvasImageType}`; 695 | } 696 | 697 | set canvasMimeType($canvasImageType){ 698 | this._set_canvasMimeType($canvasImageType); 699 | } 700 | 701 | get canvasMimeType(){ 702 | return this._canvasMimeType; 703 | } 704 | 705 | } 706 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 11.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | 'use strict'; 9 | 10 | // root 11 | import QRCode from './QRCode.js'; 12 | import QRCodeException from './QRCodeException.js'; 13 | import QROptions from './QROptions.js'; 14 | // Common 15 | import { 16 | ECC_L, ECC_M, ECC_Q, ECC_H, 17 | MASK_PATTERN_AUTO, PATTERNS, PATTERN_000, PATTERN_001, PATTERN_010, 18 | PATTERN_011, PATTERN_100, PATTERN_101, PATTERN_110, PATTERN_111, 19 | MODES, MODE_NUMBER, MODE_ALPHANUM, MODE_BYTE, 20 | VERSION_AUTO, 21 | M_NULL, M_DARKMODULE, M_DARKMODULE_LIGHT, M_DATA, M_FINDER, M_SEPARATOR, M_ALIGNMENT, M_TIMING, 22 | M_FORMAT, M_VERSION, M_QUIETZONE, M_LOGO, M_FINDER_DOT, M_FINDER_DOT_LIGHT, IS_DARK, 23 | M_DATA_DARK, M_FINDER_DARK, M_SEPARATOR_DARK, M_ALIGNMENT_DARK, M_TIMING_DARK, 24 | M_FORMAT_DARK, M_VERSION_DARK, M_QUIETZONE_DARK, M_LOGO_DARK, 25 | MATRIX_NEIGHBOUR_FLAGS, MATRIX_NEIGHBOURS, 26 | DEFAULT_MODULE_VALUES, LAYERNAMES, 27 | } from './Common/constants.js'; 28 | import BitBuffer from './Common/BitBuffer.js'; 29 | import EccLevel from './Common/EccLevel.js'; 30 | import MaskPattern from './Common/MaskPattern.js'; 31 | import Mode from './Common/Mode.js'; 32 | import Version from './Common/Version.js'; 33 | // Data 34 | import AlphaNum from './Data/AlphaNum.js'; 35 | import Byte from './Data/Byte.js'; 36 | import Numeric from './Data/Numeric.js'; 37 | import QRCodeDataException from './Data/QRCodeDataException.js'; 38 | import QRData from './Data/QRData.js'; 39 | import QRDataModeInterface from './Data/QRDataModeInterface.js'; 40 | import QRMatrix from './Data/QRMatrix.js'; 41 | // Output 42 | import QRCanvas from './Output/QRCanvas.js'; 43 | import QRCodeOutputException from './Output/QRCodeOutputException.js'; 44 | import QRMarkupSVG from './Output/QRMarkupSVG.js'; 45 | import QROutputAbstract from './Output/QROutputAbstract.js'; 46 | import QROutputInterface from './Output/QROutputInterface.js'; 47 | import QRStringText from './Output/QRStringText.js'; 48 | import QRStringJSON from './Output/QRStringJSON.js'; 49 | 50 | 51 | export { 52 | QRCode, 53 | QRCodeException, 54 | QROptions, 55 | BitBuffer, 56 | EccLevel, ECC_L, ECC_M, ECC_Q, ECC_H, 57 | MaskPattern, MASK_PATTERN_AUTO, PATTERNS, PATTERN_000, PATTERN_001, PATTERN_010, 58 | PATTERN_011, PATTERN_100, PATTERN_101, PATTERN_110, PATTERN_111, 59 | Mode, MODES, MODE_NUMBER, MODE_ALPHANUM, MODE_BYTE, 60 | Version, VERSION_AUTO, 61 | AlphaNum, 62 | Byte, 63 | Numeric, 64 | QRCodeDataException, 65 | QRData, 66 | QRDataModeInterface, 67 | QRMatrix, M_NULL, M_DARKMODULE, M_DARKMODULE_LIGHT, M_DATA, M_FINDER, M_SEPARATOR, M_ALIGNMENT, M_TIMING, 68 | M_FORMAT, M_VERSION, M_QUIETZONE, M_LOGO, M_FINDER_DOT, M_FINDER_DOT_LIGHT, IS_DARK, 69 | M_DATA_DARK, M_FINDER_DARK, M_SEPARATOR_DARK, M_ALIGNMENT_DARK, M_TIMING_DARK, 70 | M_FORMAT_DARK, M_VERSION_DARK, M_QUIETZONE_DARK, M_LOGO_DARK, MATRIX_NEIGHBOUR_FLAGS, MATRIX_NEIGHBOURS, 71 | QRCanvas, 72 | QRCodeOutputException, 73 | QRMarkupSVG, 74 | QROutputAbstract, 75 | QROutputInterface, DEFAULT_MODULE_VALUES, LAYERNAMES, 76 | QRStringText, 77 | QRStringJSON, 78 | }; 79 | -------------------------------------------------------------------------------- /test/Common/BitBuffer.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 22.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {BitBuffer, MODE_ALPHANUM, MODE_BYTE, MODE_NUMBER} from '../../src/index.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | suite('BitBufferTest', function(){ 14 | 15 | suite('BitBuffer test', function(){ 16 | 17 | let bitProvider = [ 18 | {$bits: MODE_NUMBER, expected: 16, desc: 'number'}, 19 | {$bits: MODE_ALPHANUM, expected: 32, desc: 'alphanum'}, 20 | {$bits: MODE_BYTE, expected: 64, desc: 'byte'}, 21 | ]; 22 | 23 | bitProvider.forEach(({$bits, expected}) => { 24 | test(`write data ${$bits}, expect ${expected}` , function(){ 25 | let bitBuffer = new BitBuffer() 26 | bitBuffer.put($bits, 4); 27 | 28 | assert.strictEqual(bitBuffer.getBuffer()[0], expected); 29 | assert.strictEqual(bitBuffer.getLength(), 4); 30 | }); 31 | }); 32 | 33 | }); 34 | 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /test/Common/EccLevel.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 29.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {EccLevel, MaskPattern, ECC_L, ECC_Q} from '../../src/index.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | /** 14 | * EccLevel coverage test 15 | */ 16 | suite('EccLevelTest', function(){ 17 | 18 | test('testConstructInvalidEccException', function(){ 19 | assert.throws(() => new EccLevel(69), 'invalid ECC level'); 20 | }); 21 | 22 | test('testToString', function(){ 23 | let ecc = new EccLevel(ECC_L); 24 | 25 | assert.strictEqual((ecc + ''), 'L'); 26 | }); 27 | 28 | test('testGetLevel', function(){ 29 | let ecc = new EccLevel(ECC_L); 30 | 31 | assert.strictEqual(ecc.getLevel(), ECC_L); 32 | }); 33 | 34 | test('testGetOrdinal', function(){ 35 | let ecc = new EccLevel(ECC_L); 36 | 37 | assert.strictEqual(ecc.getOrdinal(), 0); 38 | }); 39 | 40 | test('testGetOrdinal', function(){ 41 | let ecc = new EccLevel(ECC_Q); 42 | 43 | assert.strictEqual(ecc.getformatPattern(new MaskPattern(4)), 0b010010010110100); 44 | }); 45 | 46 | test('getMaxBits', function(){ 47 | let ecc = new EccLevel(ECC_Q); 48 | 49 | assert.strictEqual(ecc.getMaxBits()[21], 4096); 50 | }); 51 | 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /test/Common/MaskPattern.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /** 3 | * @created 22.07.2022 4 | * @author smiley 5 | * @copyright 2022 smiley 6 | * @license MIT 7 | */ 8 | 9 | import { 10 | MaskPattern, PATTERN_000, PATTERN_001, PATTERN_010, PATTERN_011, PATTERN_100, PATTERN_101, PATTERN_110, PATTERN_111, 11 | } from '../../src/index.js'; 12 | 13 | import {suite, test} from 'mocha'; 14 | import {assert} from 'chai'; 15 | 16 | suite('MaskPatternTest', function(){ 17 | 18 | /** 19 | * Tests if the mask function generates the correct pattern 20 | */ 21 | suite('testMask', function(){ 22 | 23 | // See mask patterns on the page 43 of JISX0510:2004. 24 | let maskPatternProvider = [ 25 | {desc: 'PATTERN_000', $pattern: PATTERN_000, expected: [ 26 | [1, 0, 1, 0, 1, 0], 27 | [0, 1, 0, 1, 0, 1], 28 | [1, 0, 1, 0, 1, 0], 29 | [0, 1, 0, 1, 0, 1], 30 | [1, 0, 1, 0, 1, 0], 31 | [0, 1, 0, 1, 0, 1], 32 | ]}, 33 | {desc: 'PATTERN_001', $pattern: PATTERN_001, expected: [ 34 | [1, 1, 1, 1, 1, 1], 35 | [0, 0, 0, 0, 0, 0], 36 | [1, 1, 1, 1, 1, 1], 37 | [0, 0, 0, 0, 0, 0], 38 | [1, 1, 1, 1, 1, 1], 39 | [0, 0, 0, 0, 0, 0], 40 | ]}, 41 | {desc: 'PATTERN_010', $pattern: PATTERN_010, expected: [ 42 | [1, 0, 0, 1, 0, 0], 43 | [1, 0, 0, 1, 0, 0], 44 | [1, 0, 0, 1, 0, 0], 45 | [1, 0, 0, 1, 0, 0], 46 | [1, 0, 0, 1, 0, 0], 47 | [1, 0, 0, 1, 0, 0], 48 | ]}, 49 | {desc: 'PATTERN_011', $pattern: PATTERN_011, expected: [ 50 | [1, 0, 0, 1, 0, 0], 51 | [0, 0, 1, 0, 0, 1], 52 | [0, 1, 0, 0, 1, 0], 53 | [1, 0, 0, 1, 0, 0], 54 | [0, 0, 1, 0, 0, 1], 55 | [0, 1, 0, 0, 1, 0], 56 | ]}, 57 | {desc: 'PATTERN_100', $pattern: PATTERN_100, expected: [ 58 | [1, 1, 1, 0, 0, 0], 59 | [1, 1, 1, 0, 0, 0], 60 | [0, 0, 0, 1, 1, 1], 61 | [0, 0, 0, 1, 1, 1], 62 | [1, 1, 1, 0, 0, 0], 63 | [1, 1, 1, 0, 0, 0], 64 | ]}, 65 | {desc: 'PATTERN_101', $pattern: PATTERN_101, expected: [ 66 | [1, 1, 1, 1, 1, 1], 67 | [1, 0, 0, 0, 0, 0], 68 | [1, 0, 0, 1, 0, 0], 69 | [1, 0, 1, 0, 1, 0], 70 | [1, 0, 0, 1, 0, 0], 71 | [1, 0, 0, 0, 0, 0], 72 | ]}, 73 | {desc: 'PATTERN_110', $pattern: PATTERN_110, expected: [ 74 | [1, 1, 1, 1, 1, 1], 75 | [1, 1, 1, 0, 0, 0], 76 | [1, 1, 0, 1, 1, 0], 77 | [1, 0, 1, 0, 1, 0], 78 | [1, 0, 1, 1, 0, 1], 79 | [1, 0, 0, 0, 1, 1], 80 | ]}, 81 | {desc: 'PATTERN_111', $pattern: PATTERN_111, expected: [ 82 | [1, 0, 1, 0, 1, 0], 83 | [0, 0, 0, 1, 1, 1], 84 | [1, 0, 0, 0, 1, 1], 85 | [0, 1, 0, 1, 0, 1], 86 | [1, 1, 1, 0, 0, 0], 87 | [0, 1, 1, 1, 0, 0], 88 | ]}, 89 | ]; 90 | 91 | /** 92 | * @param {function} $mask 93 | * @param {array} $expected 94 | */ 95 | let assertMask = function($mask, $expected){ 96 | 97 | for(let $x = 0; $x < 6; $x++){ 98 | for(let $y = 0; $y < 6; $y++){ 99 | if($mask($x, $y) !== ($expected[$y][$x] === 1)){ 100 | return false; 101 | } 102 | } 103 | } 104 | 105 | return true; 106 | } 107 | 108 | maskPatternProvider.forEach(({$pattern, expected, desc}) => { 109 | test(`testing ${desc}`, function(){ 110 | let $maskPattern = new MaskPattern($pattern); 111 | 112 | assert.isTrue(assertMask($maskPattern.getMask(), expected)); 113 | }); 114 | }); 115 | 116 | }); 117 | 118 | /** 119 | * Tests if an exception is thrown on an incorrect mask pattern 120 | */ 121 | test('testInvalidMaskPatternException', function(){ 122 | assert.throws(() => { 123 | new MaskPattern(42); 124 | }, 'invalid mask pattern') 125 | }); 126 | 127 | suite('testPenaltyRule', function(){ 128 | 129 | test('testPenaltyRule1', function(){ 130 | // horizontal 131 | assert.strictEqual(MaskPattern.testRule1([[false, false, false, false]], 1, 4), 0); 132 | assert.strictEqual(MaskPattern.testRule1([[false, false, false, false, false, true]], 1, 6), 3); 133 | assert.strictEqual(MaskPattern.testRule1([[false, false, false, false, false, false]], 1, 6), 4); 134 | // vertical 135 | assert.strictEqual(MaskPattern.testRule1([[false], [false], [false], [false]], 4, 1), 0); 136 | assert.strictEqual(MaskPattern.testRule1([[false], [false], [false], [false], [false], [true]], 6, 1), 3); 137 | assert.strictEqual(MaskPattern.testRule1([[false], [false], [false], [false], [false], [false]], 6, 1), 4); 138 | }); 139 | 140 | test('testPenaltyRule2', function(){ 141 | assert.strictEqual(MaskPattern.testRule2([[false]], 1, 1), 0); 142 | assert.strictEqual(MaskPattern.testRule2([[false, false], [false, true]], 2, 2), 0); 143 | assert.strictEqual(MaskPattern.testRule2([[false, false], [false, false]], 2, 2), 3); 144 | assert.strictEqual(MaskPattern.testRule2([[false, false, false], [false, false, false], [false, false, false]], 3, 3), 12); 145 | }); 146 | 147 | test('testPenaltyRule3', function(){ 148 | // horizontal 149 | assert.strictEqual(MaskPattern.testRule3([[false, false, false, false, true, false, true, true, true, false, true]], 1, 11), 40); 150 | assert.strictEqual(MaskPattern.testRule3([[true, false, true, true, true, false, true, false, false, false, false]], 1, 11), 40); 151 | assert.strictEqual(MaskPattern.testRule3([[true, false, true, true, true, false, true]], 1, 7), 0); 152 | // vertical 153 | assert.strictEqual(MaskPattern.testRule3([[false], [false], [false], [false], [true], [false], [true], [true], [true], [false], [true]], 11, 1), 40); 154 | assert.strictEqual(MaskPattern.testRule3([[true], [false], [true], [true], [true], [false], [true], [false], [false], [false], [false]], 11, 1), 40); 155 | assert.strictEqual(MaskPattern.testRule3([[true], [false], [true], [true], [true], [false], [true]], 7, 1), 0); 156 | }); 157 | 158 | test('testPenaltyRule4', function(){ 159 | assert.strictEqual(MaskPattern.testRule4([[false]], 1, 1), 100); 160 | assert.strictEqual(MaskPattern.testRule4([[false, true]], 1, 2), 0); 161 | assert.strictEqual(MaskPattern.testRule4([[false, true, true, true, true, false]], 1, 6), 30); 162 | }); 163 | 164 | }); 165 | 166 | }); 167 | 168 | -------------------------------------------------------------------------------- /test/Common/Mode.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 29.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {Mode, MODE_BYTE, MODE_NUMBER} from '../../src/index.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | /** 14 | * Mode coverage test 15 | */ 16 | suite('ModeTest', function(){ 17 | 18 | suite('testGetLengthBitsForVersionBreakpoints', function(){ 19 | 20 | let versionProvider = [ 21 | {$version: 1, expected: 10}, 22 | {$version: 9, expected: 10}, 23 | {$version: 10, expected: 12}, 24 | {$version: 26, expected: 12}, 25 | {$version: 27, expected: 14}, 26 | {$version: 40, expected: 14}, 27 | ]; 28 | 29 | versionProvider.forEach(({$version, expected}) => { 30 | test(`version: ${$version}, bits: ${expected}`, function(){ 31 | assert.strictEqual(Mode.getLengthBitsForVersion(MODE_NUMBER, $version), expected); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | test('testGetLengthBitsForVersionInvalidModeException', function(){ 38 | assert.throws(() => Mode.getLengthBitsForVersion(42, 69), 'invalid mode given'); 39 | }); 40 | 41 | test('testGetLengthBitsForVersionInvalidVersionException', function(){ 42 | assert.throws(() => Mode.getLengthBitsForVersion(MODE_BYTE, 69), 'invalid version number'); 43 | }); 44 | 45 | test('testGetLengthBitsForModeInvalidModeException', function(){ 46 | assert.throws(() => Mode.getLengthBitsForMode(42), 'invalid mode given'); 47 | }); 48 | 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /test/Common/PHPJS.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 29.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import PHPJS from '../../src/Common/PHPJS.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | /** 14 | * PHPJS coverage test 15 | */ 16 | suite('PHPJSTest', function(){ 17 | 18 | test('testFillArray', function(){ 19 | assert.sameOrderedMembers(PHPJS.array_fill(3, 1), [1, 1, 1]); 20 | assert.sameOrderedMembers(PHPJS.array_fill(3, null), [null, null, null]); 21 | assert.sameOrderedMembers(PHPJS.array_fill(3, undefined), [undefined, undefined, undefined]); 22 | 23 | // special case: object cloning 24 | 25 | let $valObj = {val: 1}; 26 | let $arr1 = PHPJS.array_fill(3, $valObj); 27 | // without proper cloning this would change the values of each copy of $val in $arr 28 | $valObj['val'] = 2; 29 | 30 | assert.sameDeepMembers($arr1, [{val: 1}, {val: 1}, {val: 1}]); 31 | 32 | let $valArr = [1]; 33 | let $arr2 = PHPJS.array_fill(3, $valArr); 34 | $valArr[0] = 2; 35 | 36 | assert.sameDeepMembers($arr2, [[1], [1], [1]]); 37 | }); 38 | 39 | test('testIsset', function(){ 40 | let $obj = {a: { b: {c: 'c'}}}; 41 | 42 | assert.isTrue(PHPJS.isset(() => $obj.a.b.c)); 43 | assert.isFalse(PHPJS.isset(() => $obj.a.b.d)); 44 | }); 45 | 46 | suite('testIntval', function(){ 47 | // examples from https://www.php.net/manual/function.intval.php 48 | let intvalProvider = [ 49 | {$val: 'not an int', expected: 0}, 50 | {$val: 42, expected: 42}, 51 | {$val: 4.2, expected: 4}, 52 | {$val: '42', expected: 42}, 53 | {$val: '+42', expected: 42}, 54 | {$val: '-42', expected: -42}, 55 | {$val: 0o42, expected: 34}, 56 | {$val: '042', expected: 42}, 57 | {$val: 1e10, expected: 1410065408}, 58 | {$val: '1e10', expected: 1}, 59 | {$val: 0x1A, expected: 26}, 60 | {$val: 42000000, expected: 42000000}, 61 | {$val: 420000000000000000000000000000000000, expected: 0}, // had to add some 0s because 64bit 62 | {$val: '4200000000000000000000000000000000000000000', expected: 4.2e+42}, // differs from PHP, which returns PHP_INT_MAX 63 | {$val: 42, $base: 8, expected: 42}, 64 | {$val: '42', $base: 8, expected: 34}, 65 | {$val: [], expected: 0}, 66 | {$val: ['foo', 'bar'], expected: 0}, // PHP returns 1 here 67 | {$val: false, expected: 0}, 68 | {$val: true, expected: 1}, 69 | ]; 70 | 71 | intvalProvider.forEach(({$val, $base, expected}) => { 72 | test(`value: ${$val}`, function(){ 73 | assert.strictEqual(PHPJS.intval($val, $base), expected); 74 | }); 75 | }); 76 | }); 77 | 78 | test('testArrayCombine', function(){ 79 | let arr = PHPJS.array_combine(['a','b','c'], [1,2,3]); 80 | 81 | assert.sameDeepMembers([arr], [{a: 1, b: 2, c: 3}]) 82 | assert.isFalse(PHPJS.array_combine('foo', [])) 83 | assert.isFalse(PHPJS.array_combine([], [])) 84 | }); 85 | 86 | test('testOrd', function(){ 87 | assert.strictEqual(PHPJS.ord('K'), 75); 88 | // surrogate pair to create a single Unicode character 89 | assert.strictEqual(PHPJS.ord('\uD800\uDC00'), 65536); 90 | // just a high surrogate with no following low surrogate 91 | assert.strictEqual(PHPJS.ord('\uD800'), 55296); 92 | // low surrogate 93 | assert.strictEqual(PHPJS.ord('\uDC00'), 56320); 94 | }); 95 | 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /test/Common/Version.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 29.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {EccLevel, Version, ECC_Q} from '../../src/index.js'; 9 | 10 | import {beforeEach, suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | /** 14 | * Version coverage test 15 | */ 16 | suite('VersionTest', function(){ 17 | 18 | let _version; 19 | 20 | beforeEach(function(){ 21 | _version = new Version(7); 22 | }); 23 | 24 | test('testToString', function(){ 25 | assert.strictEqual((_version + ''), '7'); 26 | }); 27 | 28 | test('testGetVersionNumber', function(){ 29 | assert.strictEqual(_version.getVersionNumber(), 7); 30 | }); 31 | 32 | test('testGetDimension', function(){ 33 | assert.strictEqual(_version.getDimension(), 45); 34 | }); 35 | 36 | test('testGetVersionPattern', function(){ 37 | assert.strictEqual(_version.getVersionPattern(), 0b000111110010010100); 38 | // no pattern for version < 7 39 | assert.isNull((new Version(6).getVersionPattern())); 40 | }); 41 | 42 | test('testGetAlignmentPattern', function(){ 43 | assert.sameOrderedMembers(_version.getAlignmentPattern(), [6, 22, 38]); 44 | }); 45 | 46 | test('testGetRSBlocks', function(){ 47 | assert.sameDeepMembers(_version.getRSBlocks(new EccLevel(ECC_Q)), [18, [[2, 14], [4, 15]]]); 48 | }); 49 | 50 | test('testGetTotalCodewords', function(){ 51 | assert.strictEqual(_version.getTotalCodewords(), 196); 52 | }); 53 | 54 | test('testConstructInvalidVersion', function(){ 55 | assert.throws(() => new Version(69), 'invalid version given'); 56 | }); 57 | 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /test/Data/QRData.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 08.06.2024 3 | * @author smiley 4 | * @copyright 2024 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {Byte, ECC_H, QRData, QROptions} from '../../src/index.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | suite('QRDataTest', function(){ 14 | 15 | test('testEstimateTotalBitLength', function(){ 16 | 17 | let $options = new QROptions({ 18 | versionMin: 10, 19 | eccLevel : ECC_H, 20 | }); 21 | 22 | // version 10H has a maximum of 976 bits, which is the exact length of the string below 23 | // QRData::estimateTotalBitLength() used to substract 4 bits for a hypothetical data mode indicator 24 | // we're now going the safe route and do not do that anymore... 25 | let $str = 'otpauth://totp/user?secret=P2SXMJFJ7DJGHLVEQYBNH2EYM4FH66CR' + 26 | '&issuer=phpMyAdmin%20%28%29&digits=6&algorithm=SHA1&period=30'; 27 | 28 | let $qrData = new QRData($options, [new Byte($str)]); 29 | 30 | assert.strictEqual(976, $qrData.estimateTotalBitLength()); 31 | assert.strictEqual(11, $qrData.getMinimumVersion().getVersionNumber()) 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/Data/QRDataMode.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 23.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import { 9 | AlphaNum, Byte, Numeric, QRData, QRDataModeInterface, QRMatrix, QROptions, 10 | } from '../../src/index.js'; 11 | 12 | import {beforeEach, suite, test} from 'mocha'; 13 | import {assert} from 'chai'; 14 | 15 | suite('QRDataModeTest', function(){ 16 | 17 | let _qrdata; 18 | 19 | beforeEach(function(){ 20 | _qrdata = new QRData(new QROptions()); 21 | }); 22 | 23 | /** 24 | * Verifies the QRData instance 25 | */ 26 | test('testInstance', function(){ 27 | assert.instanceOf(_qrdata, QRData); 28 | }); 29 | 30 | 31 | suite('QRDataModeInterfaceTest', function(){ 32 | 33 | let datamodeProvider = [ 34 | [AlphaNum, 'AlphaNum'], 35 | [Byte, 'Byte'], 36 | [Numeric, 'Numeric'], 37 | ]; 38 | 39 | datamodeProvider.forEach(([$fqn, desc]) => { 40 | 41 | // sample strings that pass for the respective data mode 42 | let testData = { 43 | AlphaNum: '0 $%*+-./:', 44 | Byte: '[¯\\_(ツ)_/¯]', 45 | Kanji: '茗荷茗荷茗荷茗荷茗荷', 46 | Numeric: '0123456789', 47 | }[desc]; 48 | 49 | // samples for the string validation test 50 | let stringValidateProvider = { 51 | // isAlphaNum() should pass on the 45 defined characters and fail on anything else (e.g. lowercase) 52 | AlphaNum: [ 53 | {$string: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', expected: true}, 54 | {$string: 'abc', expected: false}, 55 | ], 56 | // isByte() passses any binary string and only fails on zero-length strings 57 | Byte: [ 58 | {$string: '\t\x01\x02\x03\r\n', expected: true}, 59 | {$string: ' ', expected: true}, // not empty! 60 | {$string: '0', expected: true}, // should survive !empty() 61 | {$string: '', expected: false}, 62 | {$string: [], expected: false}, 63 | ], 64 | // isKanji() should pass on SJIS Kanji characters and fail on everything else 65 | Kanji: [ 66 | {$string: '茗荷', expected: true}, 67 | {$string: 'Ã', expected: false}, 68 | {$string: 'ABC', expected: false}, 69 | {$string: '123', expected: false}, 70 | ], 71 | // isNumber() should pass on any number and fail on anything else 72 | Numeric: [ 73 | {$string: '0123456789', expected: true}, 74 | {$string: 'ABC123', expected: false}, 75 | ], 76 | }[desc]; 77 | 78 | 79 | suite(desc, function(){ 80 | 81 | /** 82 | * Verifies the QRDataModeInterface instance 83 | */ 84 | test(`testDataModeInstance (${testData})`, function(){ 85 | let datamode = new $fqn(testData); 86 | 87 | assert.instanceOf(datamode, QRDataModeInterface); 88 | }); 89 | 90 | /** 91 | * Tests if a string is properly validated for the respective data mode 92 | */ 93 | suite('testValidateString', function(){ 94 | stringValidateProvider.forEach(({$string, expected}) => { 95 | test(`${desc}: ${$string}`, function(){ 96 | assert.strictEqual($fqn.validateString($string), expected); 97 | }); 98 | }); 99 | 100 | }); 101 | 102 | /** 103 | * Tests initializing the data matrix 104 | */ 105 | test('writeMatrix', function(){ 106 | _qrdata.setData([new $fqn(testData)]); 107 | 108 | let matrix = _qrdata.writeMatrix(); 109 | 110 | assert.instanceOf(matrix, QRMatrix); 111 | }); 112 | 113 | /** 114 | * Tests getting the minimum QR version for the given data 115 | */ 116 | test('testGetMinimumVersion', function(){ 117 | _qrdata.setData([new $fqn(testData)]); 118 | 119 | assert.strictEqual(_qrdata.getMinimumVersion().getVersionNumber(), 1); 120 | }); 121 | 122 | /** 123 | * Tests if an exception is thrown when the data exceeds the maximum version while auto detecting 124 | */ 125 | test('testGetMinimumVersionException', function(){ 126 | assert.throws(() => { 127 | _qrdata.setData([new $fqn(testData.repeat(1337))]); 128 | }, 'estimated data exceeds'); 129 | }); 130 | 131 | /** 132 | * Tests if an exception is thrown on data overflow 133 | */ 134 | test('testCodeLengthOverflowException', function(){ 135 | assert.throws(() => { 136 | new QRData( 137 | new QROptions({version: 4}), 138 | [new $fqn(testData.repeat(42))] 139 | ); 140 | }, 'code length overflow'); 141 | }); 142 | 143 | /** 144 | * Tests if an exception is thrown when an invalid character is encountered 145 | */ 146 | test('testInvalidDataException', function(){ 147 | 148 | if($fqn === Byte){ 149 | // console.log('N/A (binary mode)'); 150 | this.skip(); 151 | } 152 | 153 | assert.throws(() => _qrdata.setData([new $fqn('##')]), 'invalid data'); 154 | }); 155 | 156 | /** 157 | * Tests if an exception is thrown if the given string is empty 158 | */ 159 | test('testInvalidDataOnEmptyException', function(){ 160 | assert.throws(() => _qrdata.setData([new $fqn('')]), 'invalid data'); 161 | }); 162 | 163 | }); 164 | 165 | }); 166 | 167 | }); 168 | 169 | }); 170 | -------------------------------------------------------------------------------- /test/Data/QRMatrix.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 22.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import { 9 | EccLevel, MaskPattern, QRCode, QRMatrix, QROptions, Version, 10 | ECC_H, ECC_L, M_ALIGNMENT, M_ALIGNMENT_DARK, M_DARKMODULE, M_DARKMODULE_LIGHT, M_FINDER, M_FINDER_DARK, M_FINDER_DOT, 11 | M_FORMAT, M_LOGO, M_LOGO_DARK, M_QUIETZONE, M_SEPARATOR, M_DATA, M_DATA_DARK, M_TIMING_DARK, M_VERSION, PATTERN_100, 12 | } from '../../src/index.js'; 13 | 14 | import {beforeEach, suite, test} from 'mocha'; 15 | import {assert} from 'chai'; 16 | 17 | suite('QRMatrixTest', function(){ 18 | 19 | /** 20 | * @type {QRMatrix} 21 | * @private 22 | */ 23 | let _matrix; 24 | /** 25 | * @type {number} 26 | * @private 27 | */ 28 | let _version = 40; 29 | 30 | /** 31 | * invokes a QRMatrix object 32 | */ 33 | beforeEach(function(){ 34 | _matrix = new QRMatrix( 35 | new Version(_version), 36 | new EccLevel(ECC_L) 37 | ) 38 | }); 39 | 40 | /** 41 | * Validates the QRMatrix instance 42 | */ 43 | test('testInstance', function(){ 44 | assert.instanceOf(_matrix, QRMatrix); 45 | }); 46 | 47 | /** 48 | * Tests if size() returns the actual matrix size/count 49 | */ 50 | test('testGetSize', function(){ 51 | assert.lengthOf(_matrix.getMatrix(), _matrix.getSize()); 52 | }); 53 | 54 | /** 55 | * Tests if version() returns the current (given) version 56 | */ 57 | test('testGetVersion', function(){ 58 | assert.strictEqual(_matrix.getVersion().getVersionNumber(), _version); 59 | }); 60 | 61 | /** 62 | * Tests if eccLevel() returns the current (given) ECC level 63 | */ 64 | test('testGetECC', function(){ 65 | assert.strictEqual(_matrix.getEccLevel().getLevel(), ECC_L); 66 | }); 67 | 68 | /** 69 | * Tests if maskPattern() returns the current (or default) mask pattern 70 | */ 71 | test('testGetMaskPattern', function(){ 72 | let $matrix = (new QRCode).addByteSegment('testdata').getQRMatrix(); 73 | 74 | assert.instanceOf($matrix.getMaskPattern(), MaskPattern); 75 | assert.strictEqual($matrix.getMaskPattern().getPattern(), PATTERN_100); 76 | }); 77 | 78 | /** 79 | * Tests the set(), get() and check() methods 80 | */ 81 | test('testGetSetCheck', function(){ 82 | _matrix.set(10, 10, true, M_LOGO); 83 | assert.strictEqual(_matrix.get(10, 10), M_LOGO_DARK); 84 | assert.isTrue(_matrix.check(10, 10)); 85 | 86 | _matrix.set(20, 20, false, M_LOGO); 87 | assert.strictEqual(_matrix.get(20, 20), M_LOGO); 88 | assert.isFalse(_matrix.check(20, 20)); 89 | 90 | // get proper results when using a *_DARK constant 91 | _matrix.set(30, 30, true, M_LOGO_DARK); 92 | assert.strictEqual(_matrix.get(30, 30), M_LOGO_DARK); 93 | 94 | _matrix.set(40, 40, false, M_LOGO_DARK); 95 | assert.strictEqual(_matrix.get(40, 40), M_LOGO); 96 | 97 | // out of range 98 | assert.isFalse(_matrix.check(-1, -1)); 99 | assert.strictEqual(_matrix.get(-1, -1), -1); 100 | }); 101 | 102 | /** 103 | * runs several tests over versions 1-40 104 | */ 105 | suite('version iteration', function(){ 106 | 107 | let matrixProvider = function*(){ 108 | let ecc = new EccLevel(ECC_L); 109 | 110 | for(let version = 1; version <= 40; version++){ 111 | yield ({ 112 | $matrix: new QRMatrix(new Version(version), ecc), 113 | desc : `version ${version}`, 114 | }); 115 | } 116 | }; 117 | 118 | /** 119 | * Tests setting the dark module and verifies its position 120 | */ 121 | suite('testSetDarkModule', function(){ 122 | for(let {$matrix, desc} of matrixProvider()){ 123 | test(`${desc}`, function(){ 124 | $matrix.setDarkModule(); 125 | 126 | assert.strictEqual($matrix.get(8, $matrix.getSize() - 8), M_DARKMODULE); 127 | }); 128 | } 129 | }); 130 | 131 | /** 132 | * Tests setting the finder patterns and verifies their positions 133 | */ 134 | suite('testSetFinderPattern', function(){ 135 | for(let {$matrix, desc} of matrixProvider()){ 136 | test(`${desc}`, function(){ 137 | $matrix.setFinderPattern(); 138 | 139 | assert.strictEqual($matrix.get(0, 0), M_FINDER_DARK); 140 | assert.strictEqual($matrix.get(0, $matrix.getSize() - 1), M_FINDER_DARK); 141 | assert.strictEqual($matrix.get($matrix.getSize() - 1, 0), M_FINDER_DARK); 142 | }); 143 | } 144 | }); 145 | 146 | /** 147 | * Tests the separator patterns and verifies their positions 148 | */ 149 | suite('testSetSeparators', function(){ 150 | for(let {$matrix, desc} of matrixProvider()){ 151 | test(`${desc}`, function(){ 152 | $matrix.setSeparators(); 153 | 154 | assert.strictEqual($matrix.get(7, 0), M_SEPARATOR); 155 | assert.strictEqual($matrix.get(0, 7), M_SEPARATOR); 156 | assert.strictEqual($matrix.get(0, $matrix.getSize() - 8), M_SEPARATOR); 157 | assert.strictEqual($matrix.get($matrix.getSize() - 8, 0), M_SEPARATOR); 158 | }); 159 | } 160 | }); 161 | 162 | /** 163 | * Tests the alignment patterns and verifies their positions - version 1 (no pattern) skipped 164 | */ 165 | suite('testSetAlignmentPattern', function(){ 166 | for(let {$matrix, desc} of matrixProvider()){ 167 | test(`${desc}`, function(){ 168 | let $version = $matrix.getVersion(); 169 | 170 | if($version.getVersionNumber() === 1){ 171 | console.log('N/A (Version 1 has no alignment pattern)'); 172 | this.skip(); 173 | } 174 | 175 | $matrix 176 | .setFinderPattern() 177 | .setAlignmentPattern() 178 | ; 179 | 180 | let $alignmentPattern = $version.getAlignmentPattern(); 181 | 182 | for(let $py in $alignmentPattern){ 183 | for(let $px in $alignmentPattern){ 184 | // skip finder pattern 185 | if(!$matrix.checkTypeIn($px, $py, [M_FINDER, M_FINDER_DOT])){ 186 | assert.strictEqual($matrix.get($px, $py), M_ALIGNMENT_DARK); 187 | } 188 | } 189 | } 190 | 191 | }); 192 | } 193 | }); 194 | 195 | /** 196 | * Tests the timing patterns and verifies their positions 197 | */ 198 | suite('testSetTimingPattern', function(){ 199 | for(let {$matrix, desc} of matrixProvider()){ 200 | test(`${desc}`, function(){ 201 | 202 | $matrix 203 | .setFinderPattern() 204 | .setAlignmentPattern() 205 | .setTimingPattern() 206 | ; 207 | 208 | let $size = $matrix.getSize(); 209 | 210 | for(let $i = 7; $i < $size - 7; $i++){ 211 | // skip alignment pattern 212 | if($i % 2 === 0 && !$matrix.checkTypeIn(6, $i, [M_ALIGNMENT])){ 213 | assert.strictEqual($matrix.get(6, $i), M_TIMING_DARK); 214 | assert.strictEqual($matrix.get($i, 6), M_TIMING_DARK); 215 | } 216 | } 217 | 218 | }); 219 | } 220 | }); 221 | 222 | /** 223 | * Tests the version patterns and verifies their positions - version < 7 skipped 224 | */ 225 | suite('testSetVersionNumber', function(){ 226 | for(let {$matrix, desc} of matrixProvider()){ 227 | test(`${desc}`, function(){ 228 | 229 | if($matrix.getVersion().getVersionNumber() < 7){ 230 | console.log('N/A (Version < 7)'); 231 | this.skip(); 232 | } 233 | 234 | $matrix.setVersionNumber(); 235 | 236 | assert.isTrue($matrix.checkType($matrix.getSize() - 9, 0, M_VERSION)); 237 | assert.isTrue($matrix.checkType($matrix.getSize() - 11, 5, M_VERSION)); 238 | assert.isTrue($matrix.checkType(0, $matrix.getSize() - 9, M_VERSION)); 239 | assert.isTrue($matrix.checkType(5, $matrix.getSize() - 11, M_VERSION)); 240 | }); 241 | } 242 | }); 243 | 244 | /** 245 | * Tests the format patterns and verifies their positions 246 | */ 247 | suite('testSetFormatInfo', function(){ 248 | for(let {$matrix, desc} of matrixProvider()){ 249 | test(`${desc}`, function(){ 250 | $matrix.setFormatInfo(); 251 | 252 | assert.isTrue($matrix.checkType(8, 0, M_FORMAT)); 253 | assert.isTrue($matrix.checkType(0, 8, M_FORMAT)); 254 | assert.isTrue($matrix.checkType($matrix.getSize() - 1, 8, M_FORMAT)); 255 | assert.isTrue($matrix.checkType($matrix.getSize() - 8, 8, M_FORMAT)); 256 | }); 257 | } 258 | }); 259 | 260 | /** 261 | * Tests the quiet zone pattern and verifies its position 262 | */ 263 | suite('testSetQuietZone', function(){ 264 | for(let {$matrix, desc} of matrixProvider()){ 265 | test(`${desc}`, function(){ 266 | let $size = $matrix.getSize(); 267 | let $quietZoneSize = 5; 268 | 269 | $matrix.set(0, 0, true, M_LOGO); 270 | $matrix.set($size - 1, $size - 1, true, M_LOGO); 271 | 272 | $matrix.setQuietZone($quietZoneSize); 273 | 274 | let $s = ($size + 2 * $quietZoneSize); 275 | 276 | assert.lengthOf($matrix.getMatrix(), $s); 277 | assert.lengthOf($matrix.getMatrix()[$size - 1], $s); 278 | 279 | $size = $matrix.getSize(); 280 | 281 | assert.isTrue($matrix.checkType(0, 0, M_QUIETZONE)); 282 | assert.isTrue($matrix.checkType($size - 1, $size - 1, M_QUIETZONE)); 283 | 284 | $s = ($size - 1 - $quietZoneSize); 285 | 286 | assert.strictEqual($matrix.get($quietZoneSize, $quietZoneSize), M_LOGO_DARK); 287 | assert.strictEqual($matrix.get($s, $s), M_LOGO_DARK); 288 | }); 289 | } 290 | }); 291 | 292 | /** 293 | * Tests rotating the matrix by 90 degrees CW 294 | */ 295 | suite('testRotate90', function(){ 296 | for(let {$matrix, desc} of matrixProvider()){ 297 | test(`${desc}`, function(){ 298 | $matrix.initFunctionalPatterns(); 299 | 300 | // matrix size 301 | let $size = $matrix.getSize(); 302 | // quiet zone size 303 | let $qz = (($size - $matrix.getVersion().getDimension()) / 2); 304 | 305 | // initial dark module position 306 | assert.strictEqual($matrix.get((8 + $qz), ($size - 8 - $qz)), M_DARKMODULE); 307 | 308 | // first rotation 309 | $matrix.rotate90(); 310 | assert.strictEqual($matrix.get((7 + $qz), (8 + $qz)), M_DARKMODULE); 311 | 312 | // second rotation 313 | $matrix.rotate90(); 314 | assert.strictEqual($matrix.get(($size - 9 - $qz), (7 + $qz)), M_DARKMODULE); 315 | 316 | // third rotation 317 | $matrix.rotate90(); 318 | assert.strictEqual($matrix.get(($size - 8 - $qz), ($size - 9 - $qz)), M_DARKMODULE); 319 | 320 | // fourth rotation 321 | $matrix.rotate90(); 322 | assert.strictEqual($matrix.get((8 + $qz), ($size - 8 - $qz)), M_DARKMODULE); 323 | }); 324 | 325 | } 326 | }); 327 | 328 | }); 329 | 330 | /** 331 | * Tests if an exception is thrown in an attempt to create the quiet zone before data was written 332 | */ 333 | test('testSetQuietZoneException', function(){ 334 | assert.throws(() => { 335 | _matrix.setQuietZone(42); 336 | }, 'use only after writing data') 337 | }); 338 | 339 | /** 340 | * Tests if the logo space is drawn square if one of the dimensions is omitted 341 | */ 342 | test('testSetLogoSpaceOmitDimension', function(){ 343 | let $o = new QROptions; 344 | $o.version = 2; 345 | $o.eccLevel = ECC_H; 346 | $o.addQuietzone = false; 347 | $o.addLogoSpace = true; 348 | $o.logoSpaceHeight = 5; 349 | 350 | let $matrix = (new QRCode($o)).addByteSegment('testdata').getQRMatrix(); 351 | 352 | assert.isFalse($matrix.checkType(9, 9, M_LOGO)); 353 | assert.isTrue($matrix.checkType(10, 10, M_LOGO)); 354 | 355 | assert.isTrue($matrix.checkType(14, 14, M_LOGO)); 356 | assert.isFalse($matrix.checkType(15, 15, M_LOGO)); 357 | }); 358 | 359 | /** 360 | * Tests the auto orientation of the logo space 361 | */ 362 | test('testSetLogoSpaceOrientation', function(){ 363 | let $o = new QROptions(); 364 | $o.version = 10; 365 | $o.eccLevel = ECC_H; 366 | $o.addQuietzone = false; 367 | 368 | let $matrix = (new QRCode($o)).addByteSegment('testdata').getQRMatrix(); 369 | // also testing size adjustment to uneven numbers 370 | $matrix.setLogoSpace(20, 14); 371 | 372 | // NW corner 373 | assert.isFalse($matrix.checkType(17, 20, M_LOGO)); 374 | assert.isTrue($matrix.checkType(18, 21, M_LOGO)); 375 | 376 | // SE corner 377 | assert.isTrue($matrix.checkType(38, 35, M_LOGO)); 378 | assert.isFalse($matrix.checkType(39, 36, M_LOGO)); 379 | }); 380 | 381 | /** 382 | * Tests the manual positioning of the logo space 383 | */ 384 | test('testSetLogoSpacePosition', function(){ 385 | let $o = new QROptions; 386 | $o.version = 10; 387 | $o.eccLevel = ECC_H; 388 | $o.addQuietzone = true; 389 | $o.quietzoneSize = 10; 390 | 391 | let $matrix = (new QRCode($o)).addByteSegment('testdata').getQRMatrix(); 392 | 393 | $matrix.setLogoSpace(21, 21, -10, -10); 394 | 395 | assert.strictEqual($matrix.get(9, 9), M_QUIETZONE); 396 | assert.strictEqual($matrix.get(10, 10), M_LOGO); 397 | assert.strictEqual($matrix.get(20, 20), M_LOGO); 398 | assert.notStrictEqual($matrix.get(21, 21), M_LOGO); 399 | 400 | // I just realized that setLogoSpace() could be called multiple times 401 | // on the same instance, and I'm not going to do anything about it :P 402 | $matrix.setLogoSpace(21, 21, 45, 45); 403 | 404 | assert.notStrictEqual($matrix.get(54, 54), M_LOGO); 405 | assert.strictEqual($matrix.get(55, 55), M_LOGO); 406 | assert.strictEqual($matrix.get(67, 67), M_QUIETZONE); 407 | }); 408 | 409 | /** 410 | * Tests whether an exception is thrown when an ECC level other than "H" is set when attempting to add logo space 411 | */ 412 | test('testSetLogoSpaceInvalidEccException', function(){ 413 | assert.throws(() => { 414 | (new QRCode()).addByteSegment('testdata').getQRMatrix().setLogoSpace(50, 50); 415 | }, 'ECC level "H" required to add logo space') 416 | }); 417 | 418 | /** 419 | * Tests whether an exception is thrown when width or height exceed the matrix size 420 | */ 421 | test('testSetLogoSpaceExceedsException', function(){ 422 | assert.throws(() => { 423 | let $o = new QROptions(); 424 | $o.version = 5; 425 | $o.eccLevel = ECC_H; 426 | 427 | (new QRCode($o)).addByteSegment('testdata').getQRMatrix().setLogoSpace(69, 1); 428 | }, 'logo dimensions exceed matrix size') 429 | }); 430 | 431 | /** 432 | * Tests whether an exception is thrown when the logo space size exceeds the maximum ECC capacity 433 | */ 434 | test('testSetLogoSpaceMaxSizeException', function(){ 435 | assert.throws(() => { 436 | let $o = new QROptions(); 437 | $o.version = 5; 438 | $o.eccLevel = ECC_H; 439 | 440 | (new QRCode($o)).addByteSegment('testdata').getQRMatrix().setLogoSpace(37, 37); 441 | }, 'logo space exceeds the maximum error correction capacity') 442 | }); 443 | 444 | /** 445 | * Tests flipping the value of a module 446 | */ 447 | test('testFlip', function(){ 448 | _matrix.set(20, 20, true, M_LOGO); 449 | 450 | // cover checkType() 451 | assert.isTrue(_matrix.checkType(20, 20, M_LOGO)); 452 | // verify the current state (dark) 453 | assert.strictEqual(_matrix.get(20, 20), M_LOGO_DARK); 454 | // flip 455 | _matrix.flip(20, 20); 456 | // verify flip 457 | assert.strictEqual(_matrix.get(20, 20), M_LOGO); 458 | // flip again 459 | _matrix.flip(20, 20); 460 | // verify flip 461 | assert.strictEqual(_matrix.get(20, 20), M_LOGO_DARK); 462 | }); 463 | 464 | /** 465 | * Tests checking whether the M_TYPE of a module is not one of an array of M_TYPES 466 | */ 467 | test('testCheckTypeIn', function(){ 468 | _matrix.set(10, 10, true, M_QUIETZONE); 469 | 470 | assert.isFalse(_matrix.checkTypeIn(10, 10, [M_DATA, M_FINDER])); 471 | assert.isTrue(_matrix.checkTypeIn(10, 10, [M_QUIETZONE, M_FINDER])); 472 | }); 473 | 474 | test('testCheckNeighbours', function(){ 475 | _matrix 476 | .setFinderPattern() 477 | .setAlignmentPattern() 478 | ; 479 | 480 | /* 481 | * center of finder pattern (surrounded by all dark) 482 | * 483 | * # # # # # # # 484 | * # # 485 | * # # # # # 486 | * # # 0 # # 487 | * # # # # # 488 | * # # 489 | * # # # # # # # 490 | */ 491 | assert.strictEqual(_matrix.checkNeighbours(3, 3), 0b11111111); 492 | 493 | /* 494 | * center of alignment pattern (surrounded by all light) 495 | * 496 | * # # # # # 497 | * # # 498 | * # 0 # 499 | * # # 500 | * # # # # # 501 | */ 502 | assert.strictEqual(_matrix.checkNeighbours(30, 30), 0b00000000); 503 | 504 | /* 505 | * top left light block of finder pattern 506 | * 507 | * # # # 508 | * # 0 509 | * # # 510 | */ 511 | assert.strictEqual(_matrix.checkNeighbours(1, 1), 0b11010111); 512 | 513 | /* 514 | * bottom left light block of finder pattern 515 | * 516 | * # # 517 | * # 0 518 | * # # # 519 | */ 520 | assert.strictEqual(_matrix.checkNeighbours(1, 5), 0b11110101); 521 | 522 | /* 523 | * top right light block of finder pattern 524 | * 525 | * # # # 526 | * 0 # 527 | * # # 528 | */ 529 | assert.strictEqual(_matrix.checkNeighbours(5, 1), 0b01011111); 530 | 531 | /* 532 | * bottom right light block of finder pattern 533 | * 534 | * # # 535 | * 0 # 536 | * # # # 537 | */ 538 | assert.strictEqual(_matrix.checkNeighbours(5, 5), 0b01111101); 539 | 540 | /* 541 | * M_TYPE check 542 | * 543 | * # # # 544 | * 0 545 | * X X X 546 | */ 547 | assert.strictEqual(_matrix.checkNeighbours(3, 1, M_FINDER), 0b00000111); 548 | assert.strictEqual(_matrix.checkNeighbours(3, 1, M_FINDER_DOT), 0b01110000); 549 | }); 550 | 551 | }); 552 | -------------------------------------------------------------------------------- /test/Output/QRMarkupSVG.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 09.06.2024 3 | * @author smiley 4 | * @copyright 2024 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {beforeEach, suite, test} from 'mocha'; 9 | import {assert} from 'chai'; 10 | import { 11 | M_DATA, M_DATA_DARK, QRCode, QRMarkupSVG, QROptions, QROutputAbstract, QROutputInterface, QRStringText, 12 | } from '../../src/index.js'; 13 | 14 | suite('QRMarkupSVGTest', function(){ 15 | 16 | let _options; 17 | let _matrix; 18 | let _outputInterface; 19 | 20 | beforeEach(function(){ 21 | _options = new QROptions(); 22 | _matrix = (new QRCode(_options)).addByteSegment('testdata').getQRMatrix(); 23 | 24 | _options.outputInterface = QRMarkupSVG; 25 | 26 | _outputInterface = new QRMarkupSVG(_options, _matrix); 27 | }); 28 | 29 | /** 30 | * Validate the instance of the QROutputInterface 31 | */ 32 | test('testInstance', function(){ 33 | assert.instanceOf(_outputInterface, QROutputAbstract); 34 | assert.instanceOf(_outputInterface, QROutputInterface); 35 | }); 36 | 37 | let moduleValueProvider = [ 38 | // css colors from parent 39 | ['#abc', true], 40 | ['#abcd', true], 41 | ['#aabbcc', true], 42 | ['#aabbccdd', true], 43 | ['#aabbcxyz', false], 44 | ['#aa', false], 45 | ['#aabbc', false], 46 | ['#aabbccd', false], 47 | ['rgb(100.0%, 0.0%, 0.0%)', true], 48 | [' rgba(255, 0, 0, 1.0) ', true], 49 | ['hsl(120, 60%, 50%)', true], 50 | ['hsla(120, 255, 191.25, 1.0)', true], 51 | ['rgba(255, 0, whatever, 0, 1.0)', false], 52 | ['rgba(255, 0, 0, 1.0);', false], 53 | ['purple', true], 54 | ['c5sc0lor', false], 55 | 56 | // SVG 57 | ['url(#fillGradient)', true], 58 | ['url(https://example.com/noop)', false], 59 | ]; 60 | 61 | moduleValueProvider.forEach(([$value, $expected]) => { 62 | test('testValidateModuleValues', function(){ 63 | assert.strictEqual(_outputInterface.constructor.moduleValueIsValid($value), $expected); 64 | }); 65 | }); 66 | 67 | /** 68 | * covers the module values settings 69 | */ 70 | test('testSetModuleValues', function(){ 71 | let mv = {}; 72 | 73 | mv[M_DATA] = '#4A6000'; 74 | mv[M_DATA_DARK] = '#ECF9BE'; 75 | 76 | _options.moduleValues = mv; 77 | _outputInterface = new QRStringText(_options, _matrix); 78 | 79 | let data = _outputInterface.dump(); 80 | 81 | assert.isTrue(data.includes('#4A6000')); 82 | assert.isTrue(data.includes('#ECF9BE')); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/Output/QRStringJSON.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 23.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import { 9 | QRCode, QROptions, QROutputAbstract, QROutputInterface, QRStringJSON, M_DATA, M_DATA_DARK, 10 | } from '../../src/index.js'; 11 | 12 | import {beforeEach, suite, test} from 'mocha'; 13 | import {assert} from 'chai'; 14 | 15 | /** 16 | * Tests the QRString output module 17 | */ 18 | suite('QRStringJSONTest', function(){ 19 | 20 | let _options; 21 | let _matrix; 22 | let _outputInterface; 23 | 24 | beforeEach(function(){ 25 | _options = new QROptions(); 26 | _matrix = (new QRCode(_options)).addByteSegment('testdata').getQRMatrix(); 27 | 28 | _options.outputInterface = QRStringJSON; 29 | 30 | _outputInterface = new QRStringJSON(_options, _matrix); 31 | }); 32 | 33 | /** 34 | * Validate the instance of the QROutputInterface 35 | */ 36 | test('testInstance', function(){ 37 | assert.instanceOf(_outputInterface, QROutputAbstract); 38 | assert.instanceOf(_outputInterface, QROutputInterface); 39 | }); 40 | 41 | 42 | test('testSetModuleValues', function(){ 43 | let mv = {}; 44 | 45 | mv[M_DATA_DARK] = '#AAA' 46 | mv[M_DATA] = '#BBB' 47 | 48 | _options.moduleValues = mv; 49 | 50 | _outputInterface = new QRStringJSON(_options, _matrix); 51 | 52 | let data = _outputInterface.dump(); 53 | 54 | assert.isTrue(data.includes('"layer":"data-dark","value":"#AAA"')); 55 | assert.isTrue(data.includes('"layer":"data","value":"#BBB"')); 56 | }); 57 | 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /test/Output/QRStringText.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 23.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import { 9 | QRCode, QROptions, QROutputAbstract, QROutputInterface, QRStringText, M_DATA, M_DATA_DARK, 10 | } from '../../src/index.js'; 11 | 12 | import {beforeEach, suite, test} from 'mocha'; 13 | import {assert} from 'chai'; 14 | 15 | /** 16 | * Tests the QRString output module 17 | */ 18 | suite('QRStringTextTest', function(){ 19 | 20 | let _options; 21 | let _matrix; 22 | let _outputInterface; 23 | 24 | beforeEach(function(){ 25 | _options = new QROptions(); 26 | _matrix = (new QRCode(_options)).addByteSegment('testdata').getQRMatrix(); 27 | 28 | _options.outputInterface = QRStringText; 29 | 30 | _outputInterface = new QRStringText(_options, _matrix); 31 | }); 32 | 33 | /** 34 | * Validate the instance of the QROutputInterface 35 | */ 36 | test('testInstance', function(){ 37 | assert.instanceOf(_outputInterface, QROutputAbstract); 38 | assert.instanceOf(_outputInterface, QROutputInterface); 39 | }); 40 | 41 | /** 42 | * covers the module values settings 43 | */ 44 | test('testSetModuleValues', function(){ 45 | let mv = {}; 46 | 47 | mv[M_DATA] = 'A'; 48 | mv[M_DATA_DARK] = 'B'; 49 | 50 | _options.moduleValues = mv; 51 | _outputInterface = new QRStringText(_options, _matrix); 52 | 53 | let data = _outputInterface.dump(); 54 | 55 | assert.isTrue(data.includes('A')); 56 | assert.isTrue(data.includes('B')); 57 | }); 58 | 59 | }); 60 | 61 | -------------------------------------------------------------------------------- /test/QRCode.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 23.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {QRCode, QROptions} from '../src/index.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | /** 14 | * Tests basic functions of the QRCode class 15 | */ 16 | suite('QRCodeTest', function(){ 17 | 18 | /** 19 | * tests if an exception is thrown when an invalid (built-in) output type is specified 20 | */ 21 | test('testInitOutputInterfaceException', function(){ 22 | assert.throws(() => (new QRCode(new QROptions({outputInterface: 'foo'}))).render('test')); 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/QROptions.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @created 21.07.2022 3 | * @author smiley 4 | * @copyright 2022 smiley 5 | * @license MIT 6 | */ 7 | 8 | import {MASK_PATTERN_AUTO, VERSION_AUTO, QROptions} from '../src/index.js'; 9 | 10 | import {suite, test} from 'mocha'; 11 | import {assert} from 'chai'; 12 | 13 | suite('QROptionsTest', function(){ 14 | 15 | /** 16 | * Tests the $version clamping 17 | */ 18 | suite('testVersionClamp', function(){ 19 | 20 | let versionProvider = [ 21 | {$version: 42, expected: 40}, 22 | {$version: -42, expected: 1}, 23 | {$version: 21, expected: 21}, 24 | {$version: VERSION_AUTO, expected: -1}, 25 | ]; 26 | 27 | versionProvider.forEach(({$version, expected}) => { 28 | test(`version ${$version} should be clamped to ${expected}`, function(){ 29 | let options = new QROptions({version: $version}); 30 | 31 | assert.strictEqual(options.version, expected); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | /** 38 | * Tests the $versionMin/$versionMax clamping 39 | */ 40 | suite('testVersionMinMaxClamp', function(){ 41 | 42 | let versionMinMaxProvider = [ 43 | {$versionMin: 5, $versionMax: 10, expectedMin: 5, expectedMax: 10, desc: 'normal clamp'}, 44 | {$versionMin: -42, $versionMax: 42, expectedMin: 1, expectedMax: 40, desc: 'exceeding values'}, 45 | {$versionMin: 10, $versionMax: 5, expectedMin: 5, expectedMax: 10, desc: 'min > max'}, 46 | {$versionMin: 42, $versionMax: -42, expectedMin: 1, expectedMax: 40, desc: 'min > max, exceeding'}, 47 | ]; 48 | 49 | versionMinMaxProvider.forEach(({$versionMin, $versionMax, expectedMin, expectedMax, desc}) => { 50 | test(`clamp versionMin ${$versionMin} to ${expectedMin} and versionMax ${$versionMax} to ${expectedMax} (${desc})`, 51 | function(){ 52 | 53 | let options = new QROptions({versionMin: $versionMin, versionMax: $versionMax}); 54 | 55 | assert.strictEqual(options.versionMin, expectedMin); 56 | assert.strictEqual(options.versionMax, expectedMax); 57 | }); 58 | }); 59 | 60 | }); 61 | 62 | /** 63 | * Tests the $maskPattern clamping 64 | */ 65 | suite('testMaskPatternClamp', function(){ 66 | 67 | let maskPatternProvider = [ 68 | {$maskPattern: 5, expected: 5}, 69 | {$maskPattern: 42, expected: 7}, 70 | {$maskPattern: -42, expected: 0}, 71 | {$maskPattern: MASK_PATTERN_AUTO, expected: -1}, 72 | ]; 73 | 74 | maskPatternProvider.forEach(({$maskPattern, expected}) => { 75 | test(`maskPattern ${$maskPattern} should be clamped to ${expected}`, function(){ 76 | let options = new QROptions({maskPattern: $maskPattern}); 77 | 78 | assert.strictEqual(options.maskPattern, expected); 79 | }); 80 | }); 81 | 82 | }); 83 | 84 | /** 85 | * Tests if an exception is thrown on an incorrect ECC level 86 | */ 87 | test('testInvalidEccLevelException', function(){ 88 | assert.throws(() => new QROptions({eccLevel: 42}), 'Invalid error correct level: 42') 89 | }); 90 | 91 | /** 92 | * Tests the clamping (between 0 and 177) of the logo space values 93 | * 94 | * @dataProvider logoSpaceValueProvider 95 | */ 96 | suite('testClampLogoSpaceValue', function(){ 97 | 98 | let logoSpaceValueProvider = [ 99 | {$value: -1, expected: 0, desc: 'negative'}, 100 | {$value: 0, expected: 0, desc: 'zero'}, 101 | {$value: 69, expected: 69, desc: 'normal'}, 102 | {$value: 177, expected: 177, desc: 'max'}, 103 | {$value: 178, expected: 177, desc: 'exceed'}, 104 | ]; 105 | 106 | for(let prop of ['logoSpaceWidth', 'logoSpaceHeight', 'logoSpaceStartX', 'logoSpaceStartY']){ 107 | logoSpaceValueProvider.forEach(({$value, expected, desc}) => { 108 | test(`${prop} ${$value} should be clamped to ${expected} (${desc})`, function(){ 109 | let options = new QROptions(); 110 | 111 | options[prop] = $value; 112 | assert.strictEqual(options[prop], expected); 113 | }); 114 | }); 115 | } 116 | 117 | }); 118 | 119 | /** 120 | * Tests if the optional logo space start values are nullable 121 | */ 122 | test('testLogoSpaceStartNullable' , function(){ 123 | let options = new QROptions({ 124 | logoSpaceStartX: 42, 125 | logoSpaceStartY: 42, 126 | }); 127 | 128 | assert.strictEqual(options.logoSpaceStartX, 42); 129 | assert.strictEqual(options.logoSpaceStartY, 42); 130 | 131 | options.logoSpaceStartX = null; 132 | options.logoSpaceStartY = null; 133 | 134 | assert.strictEqual(options.logoSpaceStartX, null); 135 | assert.strictEqual(options.logoSpaceStartY, null); 136 | }); 137 | 138 | /** 139 | * Tests clamping of the circle radius 140 | */ 141 | suite('testClampCircleRadius', function(){ 142 | 143 | let circleRadiusProvider = [ 144 | {$circleRadius: 0.0, expected: 0.1}, 145 | {$circleRadius: 0.5, expected: 0.5}, 146 | {$circleRadius: 1.5, expected: 0.75}, 147 | ]; 148 | 149 | circleRadiusProvider.forEach(({$circleRadius, expected}) => { 150 | test(`circleRadius ${$circleRadius} should be clamped to ${expected}`, function(){ 151 | let options = new QROptions({circleRadius: $circleRadius}); 152 | 153 | assert.strictEqual(options.circleRadius, expected); 154 | }); 155 | }); 156 | 157 | }); 158 | 159 | }); 160 | --------------------------------------------------------------------------------