├── .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 |
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 |
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