├── .DS_Store
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierrc.js
├── .releaserc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
└── release.sh
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── assets
│ ├── .DS_Store
│ ├── facebook_example_new.png
│ ├── instagram_example.png
│ ├── qr_code_example.png
│ ├── telegram_example_new.png
│ └── test
│ │ ├── image_from_readme.png
│ │ ├── image_from_readme.svg
│ │ ├── rounded_dots.png
│ │ ├── simple_dots.png
│ │ ├── simple_qr.png
│ │ ├── simple_qr_with_image.png
│ │ ├── simple_qr_with_image_margin.png
│ │ ├── simple_qr_with_margin_canvas.png
│ │ └── simple_square_dot.png
├── constants
│ ├── cornerDotTypes.ts
│ ├── cornerSquareTypes.ts
│ ├── dotTypes.test.js
│ ├── dotTypes.ts
│ ├── drawTypes.ts
│ ├── errorCorrectionLevels.test.js
│ ├── errorCorrectionLevels.ts
│ ├── errorCorrectionPercents.test.js
│ ├── errorCorrectionPercents.ts
│ ├── gradientTypes.ts
│ ├── modes.test.js
│ ├── modes.ts
│ ├── qrTypes.test.js
│ └── qrTypes.ts
├── core
│ ├── QRCanvas.test.js
│ ├── QRCanvas.ts
│ ├── QRCodeStyling.test.js
│ ├── QRCodeStyling.ts
│ ├── QROptions.test.js
│ ├── QROptions.ts
│ └── QRSVG.ts
├── figures
│ ├── cornerDot
│ │ ├── canvas
│ │ │ └── QRCornerDot.ts
│ │ └── svg
│ │ │ └── QRCornerDot.ts
│ ├── cornerSquare
│ │ ├── canvas
│ │ │ └── QRCornerSquare.ts
│ │ └── svg
│ │ │ └── QRCornerSquare.ts
│ └── dot
│ │ ├── canvas
│ │ ├── QRDot.test.js
│ │ └── QRDot.ts
│ │ └── svg
│ │ └── QRDot.ts
├── index.html
├── index.test.js
├── index.ts
├── tools
│ ├── calculateImageSize.test.js
│ ├── calculateImageSize.ts
│ ├── downloadURI.ts
│ ├── getMode.test.js
│ ├── getMode.ts
│ ├── merge.test.js
│ ├── merge.ts
│ └── sanitizeOptions.ts
└── types
│ └── index.ts
├── tsconfig.json
├── webpack.config.build.js
├── webpack.config.common.js
└── webpack.config.dev-server.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/.DS_Store
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js, ts}]
2 | indent_style = space
3 | indent_size = 2
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | coverage
4 | /*.*
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | node: true
4 | },
5 | parser: '@typescript-eslint/parser',
6 | extends: [
7 | "eslint:recommended",
8 | "plugin:prettier/recommended",
9 | "plugin:jest/recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:@typescript-eslint/eslint-recommended"
12 | ],
13 | parserOptions: {
14 | sourceType: "module"
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-18.04
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 | with:
14 | fetch-depth: 0
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: 10
19 | cache: 'npm'
20 | registry-url: 'https://npm.pkg.github.com'
21 | - name: Install dependencies
22 | run: npm install
23 | env:
24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
25 | - name: Release
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | run: npx semantic-release
30 |
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IDEA
2 | .idea
3 |
4 | # Lib folder
5 | /lib
6 |
7 | # npm modules
8 | /node_modules
9 |
10 | # Tests coverage results
11 | /coverage
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | coverage
2 | src
3 | .editorconfig
4 | .eslintignore
5 | .eslintrc.js
6 | .gitignore
7 | .prettierrc.js
8 | jest.config.js
9 | tsconfig.json
10 | webpack.config.build.js
11 | webpack.config.common.js
12 | webpack.config.dev-server.js
13 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | @oblakstudio:registry=https://npm.pkg.github.com
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: "none",
4 | singleQuote: false,
5 | printWidth: 120,
6 | tabWidth: 2
7 | };
8 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": ["master", "next"],
3 | "plugins": [
4 | ["@semantic-release/changelog", {
5 | "changelogFile": "CHANGELOG.md"
6 | }],
7 | "@semantic-release/commit-analyzer",
8 | "@semantic-release/release-notes-generator",
9 | ["@semantic-release/exec", {
10 | "prepareCmd": "sh ./build/release.sh ${nextRelease.version}"
11 | }],
12 | ["@semantic-release/git", {
13 | "assets": ["CHANGELOG.md", "package.json"],
14 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
15 | }],
16 | "@semantic-release/npm"
17 | ],
18 | "publishConfig": {
19 | "registry":"https://npm.pkg.github.com",
20 | "tag": "latest"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [1.1.0](https://github.com/oblakstudio/qr-code-styling/compare/v1.0.0...v1.1.0) (2021-12-31)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * readded semantic-release ([dc6b543](https://github.com/oblakstudio/qr-code-styling/commit/dc6b543a872ab8541116e202579a727378419e89))
7 | * **build:** Fixed package name ([01ee666](https://github.com/oblakstudio/qr-code-styling/commit/01ee666521aabfa4ff066c692cc7d2e35cbd89cc))
8 |
9 |
10 | ### Features
11 |
12 | * Updated from another repo ([436c034](https://github.com/oblakstudio/qr-code-styling/commit/436c034bcbcebc89becde20ca17d757c239859a0))
13 |
14 | # 1.0.0 (2021-12-31)
15 |
16 |
17 | ### Features
18 |
19 | * Just release it ([5d9a2fa](https://github.com/oblakstudio/qr-code-styling/commit/5d9a2fadb1db1ac245e08b2df00fe1211b561d38))
20 | * Just release it ([c931f2c](https://github.com/oblakstudio/qr-code-styling/commit/c931f2ccff816146849062acd60855ec8d39acf9))
21 | * Just release it ([470d75b](https://github.com/oblakstudio/qr-code-styling/commit/470d75b085eb2290071da02afc9b7e55005a8c2f))
22 | * Just release it ([050d97d](https://github.com/oblakstudio/qr-code-styling/commit/050d97d7316247bb58856495df04f6ab88b34cda))
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Denys Kozak
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 | # QR Code Styling
2 | [](https://www.npmjs.org/package/qr-code-styling)
3 |
4 | JavaScript library for generating QR codes with a logo and styling.
5 |
6 | Try it here https://qr-code-styling.com
7 |
8 | If you have issues / suggestions / notes / questions, please open an issue or contact me. Let's create a cool library together.
9 | ### Examples
10 |
11 |
12 |
13 |
14 |
15 |
16 | ### Installation
17 |
18 | ```
19 | npm install qr-code-styling
20 | ```
21 |
22 | ### Usage
23 |
24 | ```HTML
25 |
26 |
27 |
28 |
29 | QR Code Styling
30 |
31 |
32 |
33 |
34 |
58 |
59 |
60 | ```
61 | ---
62 |
63 | [**React example (Codesandbox)**](https://codesandbox.io/s/qr-code-styling-react-example-l8rwl?file=/src/App.js)
64 |
65 | [**Angular example (Codesandbox)**](https://codesandbox.io/s/agitated-panini-tpgb2?file=/src/app/app.component.ts)
66 |
67 | ---
68 |
69 | [**React example (source)**](https://github.com/kozakdenys/qr-code-styling-examples/tree/master/examples/react)
70 |
71 | [**Angular example (source)**](https://github.com/kozakdenys/qr-code-styling-examples/tree/master/examples/angular)
72 |
73 | [**Vue example (source)**](https://github.com/kozakdenys/qr-code-styling-examples/tree/master/examples/vue)
74 |
75 | ---
76 |
77 | ### API Documentation
78 |
79 | #### QRCodeStyling instance
80 | `new QRCodeStyling(options) => QRCodeStyling`
81 |
82 | Param |Type |Description
83 | -------|------|------------
84 | options|object|Init object
85 |
86 | `options` structure
87 |
88 | Property |Type |Default Value|Description
89 | -----------------------|-------------------------|-------------|-----------------------------------------------------
90 | width |number |`300` |Size of canvas
91 | height |number |`300` |Size of canvas
92 | type |string (`'canvas' 'svg'`)|`canvas` |The type of the element that will be rendered
93 | data |string | |The date will be encoded to the QR code
94 | image |string | |The image will be copied to the center of the QR code
95 | margin |number |`0` |Margin around canvas
96 | qrOptions |object | |Options will be passed to `qrcode-generator` lib
97 | imageOptions |object | |Specific image options, details see below
98 | dotsOptions |object | |Dots styling options
99 | cornersSquareOptions |object | |Square in the corners styling options
100 | cornersDotOptions |object | |Dots in the corners styling options
101 | backgroundOptions |object | |QR background styling options
102 | nodeCanvas |node-canvas | |Only specify when running on a node server for canvas type, please refer to node section below
103 | jsDom |jsdom | |Only specify when running on a node server for svg type, please refer to node section below
104 |
105 | `options.qrOptions` structure
106 |
107 | Property |Type |Default Value
108 | --------------------|--------------------------------------------------|-------------
109 | typeNumber |number (`0 - 40`) |`0`
110 | mode |string (`'Numeric' 'Alphanumeric' 'Byte' 'Kanji'`)|
111 | errorCorrectionLevel|string (`'L' 'M' 'Q' 'H'`) |`'Q'`
112 |
113 | `options.imageOptions` structure
114 |
115 | Property |Type |Default Value|Description
116 | ------------------|---------------------------------------|-------------|------------------------------------------------------------------------------
117 | hideBackgroundDots|boolean |`true` |Hide all dots covered by the image
118 | imageSize |number |`0.4` |Coefficient of the image size. Not recommended to use ove 0.5. Lower is better
119 | margin |number |`0` |Margin of the image in px
120 | crossOrigin |string(`'anonymous' 'use-credentials'`)| |Set "anonymous" if you want to download QR code from other origins.
121 | saveAsBlob |boolean |`false` |Saves image as base64 blob in svg type, see bellow
122 |
123 | When QR type is svg, the image may not load in certain applications as it is saved as a url, and some svg applications will not render url images for security reasons. Setting `saveAsBlob` to true will instead save the image as a blob, allowing it to render correctly in more places, but will also increase the file size.
124 |
125 | `options.dotsOptions` structure
126 |
127 | Property|Type |Default Value|Description
128 | --------|------------------------------------------------------------------------------|-------------|-------------------
129 | color |string |`'#000'` |Color of QR dots
130 | gradient|object | |Gradient of QR dots
131 | type |string (`'rounded' 'dots' 'classy' 'classy-rounded' 'square' 'extra-rounded'`)|`'square'` |Style of QR dots
132 |
133 | `options.backgroundOptions` structure
134 |
135 | Property|Type |Default Value
136 | --------|------|-------------
137 | color |string|`'#fff'`
138 | gradient|object|
139 |
140 | `options.cornersSquareOptions` structure
141 |
142 | Property|Type |Default Value|Description
143 | --------|-----------------------------------------|-------------|-----------------
144 | color |string | |Color of Corners Square
145 | gradient|object | |Gradient of Corners Square
146 | type |string (`'dot' 'square' 'extra-rounded'`)| |Style of Corners Square
147 |
148 | `options.cornersDotOptions` structure
149 |
150 | Property|Type |Default Value|Description
151 | --------|-------------------------|-------------|-----------------
152 | color |string | |Color of Corners Dot
153 | gradient|object | |Gradient of Corners Dot
154 | type |string (`'dot' 'square'`)| |Style of Corners Dot
155 |
156 | Gradient structure
157 |
158 | `options.dotsOptions.gradient`
159 |
160 | `options.backgroundOptions.gradient`
161 |
162 | `options.cornersSquareOptions.gradient`
163 |
164 | `options.cornersDotOptions.gradient`
165 |
166 | Property |Type |Default Value|Description
167 | ----------|----------------------------|-------------|---------------------------------------------------------
168 | type |string (`'linear' 'radial'`)|"linear" |Type of gradient spread
169 | rotation |number |0 |Rotation of gradient in radians (Math.PI === 180 degrees)
170 | colorStops|array of objects | |Gradient colors. Example `[{ offset: 0, color: 'blue' }, { offset: 1, color: 'red' }]`
171 |
172 | Gradient colorStops structure
173 |
174 | `options.dotsOptions.gradient.colorStops[]`
175 |
176 | `options.backgroundOptions.gradient.colorStops[]`
177 |
178 | `options.cornersSquareOptions.gradient.colorStops[]`
179 |
180 | `options.cornersDotOptions.gradient.colorStops[]`
181 |
182 | Property|Type |Default Value|Description
183 | --------|----------------|-------------|-----------------------------------
184 | offset |number (`0 - 1`)| |Position of color in gradient range
185 | color |string | |Color of stop in gradient range
186 |
187 | #### QRCodeStyling methods
188 | `QRCodeStyling.append(container) => void`
189 |
190 | Param |Type |Description
191 | ---------|-----------|-----------
192 | container|DOM element|This container will be used for appending of the QR code
193 |
194 | `QRCodeStyling.getRawData(extension) => Promise`
195 |
196 | Param |Type |Default Value|Description
197 | ---------|------------------------------------|-------------|------------
198 | extension|string (`'png' 'jpeg' 'webp' 'svg'`)|`'png'` |Blob type on browser, Buffer type on Node
199 |
200 | `QRCodeStyling.update(options) => void`
201 |
202 | Param |Type |Description
203 | -------|------|--------------------------------------
204 | options|object|The same options as for initialization
205 |
206 | `QRCodeStyling.download(downloadOptions) => Promise`
207 |
208 | Param |Type |Description
209 | ---------------|------|------------
210 | downloadOptions|object|Options with extension and name of file (not required)
211 |
212 | Promise returned will resolve into the data URI of the QR code image.
213 |
214 | `downloadOptions` structure
215 |
216 | Property |Type |Default Value|Description
217 | ---------|------------------------------------|-------------|-----------------------------------------------------
218 | name |string |`'qr'` |Name of the downloaded file
219 | extension|string (`'png' 'jpeg' 'webp' 'svg'`)|`'png'` |File extension
220 |
221 | ### Node Support
222 | You can use this on a node server by passing through the node-canvas or jsdom object depending if your creating a non-svg or svg respectively. You must pass both if using `imageOptions.saveAsBlob`.
223 |
224 | Calling `getRawData` in node will return a Buffer instead of a Blob.
225 |
226 | ```js
227 | const { QRCodeStyling } = require("qr-code-styling/lib/qr-code-styling.common.js");
228 | const nodeCanvas = require("canvas");
229 | const { JSDOM } = require("jsdom");
230 | const fs = require("fs");
231 |
232 | const options = {
233 | width: 300,
234 | height: 300,
235 | data: "https://www.facebook.com/",
236 | image: "https://upload.wikimedia.org/wikipedia/commons/5/51/Facebook_f_logo_%282019%29.svg",
237 | dotsOptions: {
238 | color: "#4267b2",
239 | type: "rounded"
240 | },
241 | backgroundOptions: {
242 | color: "#e9ebee",
243 | },
244 | imageOptions: {
245 | crossOrigin: "anonymous",
246 | margin: 20
247 | }
248 | }
249 |
250 | // For canvas type
251 | const qrCodeImage = new QRCodeStyling({
252 | nodeCanvas, // this is required
253 | ...options
254 | });
255 |
256 | qrCodeImage.getRawData("png").then((buffer) => {
257 | fs.writeFileSync("test.png", buffer);
258 | });
259 |
260 | // For svg type
261 | const qrCodeSvg = new QRCodeStyling({
262 | jsdom: JSDOM, // this is required
263 | type: "svg",
264 | ...options
265 | });
266 |
267 | qrCodeSvg.getRawData("svg").then((buffer) => {
268 | fs.writeFileSync("test.svg", buffer);
269 | });
270 |
271 | // For svg type with the inner-image saved as a blob
272 | // (inner-image will render in more places but file will be larger)
273 | const qrCodeSvgWithBlobImage = new QRCodeStyling({
274 | jsdom: JSDOM, // this is required
275 | nodeCanvas, // this is required
276 | type: "svg",
277 | ...options,
278 | imageOptions: {
279 | saveAsBlob: true,
280 | crossOrigin: "anonymous",
281 | margin: 20
282 | }
283 | });
284 |
285 | qrCodeSvgWithBlobImage.getRawData("svg").then((buffer) => {
286 | fs.writeFileSync("test_blob.svg", buffer);
287 | });
288 |
289 | ```
290 |
291 | ### License
292 |
293 | [MIT License](https://raw.githubusercontent.com/kozakdenys/qr-code-styling/master/LICENSE). Copyright (c) 2021 Denys Kozak
294 |
295 |
--------------------------------------------------------------------------------
/build/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | NEXT_VERSION=$1
4 | CURRENT_VERSION=$(cat package.json | grep version | head -1 | awk -F= "{ print $2 }" | sed 's/[version:,\",]//g' | tr -d '[[:space:]]')
5 |
6 | sed -i "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEXT_VERSION\"/g" package.json
7 |
8 | npm run build
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { defaults: tsjPreset } = require("ts-jest/presets");
2 |
3 | // For a detailed explanation regarding each configuration property, visit:
4 | // https://jestjs.io/docs/en/configuration.html
5 |
6 | module.exports = {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // Respect "browser" field in package.json when resolving modules
14 | // browser: false,
15 |
16 | // The directory where Jest should store its cached dependency information
17 | // cacheDirectory: "/tmp/jest_rs",
18 |
19 | // Automatically clear mock calls and instances between every test
20 | // clearMocks: false,
21 |
22 | // Indicates whether the coverage information should be collected while executing the test
23 | collectCoverage: true,
24 |
25 | // An array of glob patterns indicating a set of files for which coverage information should be collected
26 | collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
27 |
28 | // The directory where Jest should output its coverage files
29 | coverageDirectory: "coverage",
30 |
31 | // An array of regexp pattern strings used to skip coverage collection
32 | // coveragePathIgnorePatterns: [
33 | // "/node_modules/"
34 | // ],
35 |
36 | // A list of reporter names that Jest uses when writing coverage reports
37 | // coverageReporters: [
38 | // "json",
39 | // "text",
40 | // "lcov",
41 | // "clover"
42 | // ],
43 |
44 | // An object that configures minimum threshold enforcement for coverage results
45 | // coverageThreshold: null,
46 |
47 | // A path to a custom dependency extractor
48 | // dependencyExtractor: null,
49 |
50 | // Make calling deprecated APIs throw helpful error messages
51 | // errorOnDeprecated: false,
52 |
53 | // Force coverage collection from ignored files using an array of glob patterns
54 | // forceCoverageMatch: [],
55 |
56 | // A path to a module which exports an async function that is triggered once before all test suites
57 | // globalSetup: null,
58 |
59 | // A path to a module which exports an async function that is triggered once after all test suites
60 | // globalTeardown: null,
61 |
62 | // A set of global variables that need to be available in all test environments
63 | // globals: {},
64 |
65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
66 | // maxWorkers: "50%",
67 |
68 | // An array of directory names to be searched recursively up from the requiring module's location
69 | // moduleDirectories: [
70 | // "node_modules"
71 | // ],
72 |
73 | // An array of file extensions your modules use
74 | moduleFileExtensions: ["ts", "js", "json"],
75 |
76 | // A map from regular expressions to module names that allow to stub out resources with a single module
77 | // moduleNameMapper: {},
78 |
79 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
80 | // modulePathIgnorePatterns: [],
81 |
82 | // Activates notifications for test results
83 | // notify: false,
84 |
85 | // An enum that specifies notification mode. Requires { notify: true }
86 | // notifyMode: "failure-change",
87 |
88 | // A preset that is used as a base for Jest's configuration
89 | preset: "ts-jest",
90 |
91 | // Run tests from one or more projects
92 | // projects: null,
93 |
94 | // Use this configuration option to add custom reporters to Jest
95 | // reporters: undefined,
96 |
97 | // Automatically reset mock state between every test
98 | // resetMocks: false,
99 |
100 | // Reset the module registry before running each individual test
101 | // resetModules: false,
102 |
103 | // A path to a custom resolver
104 | // resolver: null,
105 |
106 | // Automatically restore mock state between every test
107 | // restoreMocks: false,
108 |
109 | // The root directory that Jest should scan for tests and modules within
110 | // rootDir: null,
111 |
112 | // A list of paths to directories that Jest should use to search for files in
113 | roots: ["src"],
114 |
115 | // Allows you to use a custom runner instead of Jest's default test runner
116 | // runner: "jest-runner",
117 |
118 | // The paths to modules that run some code to configure or set up the testing environment before each test
119 | // setupFiles: [],
120 |
121 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
122 | // setupFilesAfterEnv: [],
123 |
124 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
125 | // snapshotSerializers: [],
126 |
127 | // The test environment that will be used for testing
128 | testEnvironment: "jest-environment-jsdom-fifteen",
129 |
130 | // Options that will be passed to the testEnvironment
131 | testEnvironmentOptions: {
132 | resources: "usable"
133 | },
134 |
135 | // Adds a location field to test results
136 | // testLocationInResults: false,
137 |
138 | // The glob patterns Jest uses to detect test files
139 | // testMatch: [
140 | // "**/__tests__/**/*.[jt]s?(x)",
141 | // "**/?(*.)+(spec|test).[tj]s?(x)"
142 | // ],
143 |
144 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
145 | // testPathIgnorePatterns: [
146 | // "/node_modules/"
147 | // ],
148 |
149 | // The regexp pattern or array of patterns that Jest uses to detect test files
150 | // testRegex: [],
151 |
152 | // This option allows the use of a custom results processor
153 | // testResultsProcessor: null,
154 |
155 | // This option allows use of a custom test runner
156 | // testRunner: "jasmine2",
157 |
158 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
159 | // testURL: "http://localhost",
160 |
161 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
162 | // timers: "real",
163 |
164 | // A map from regular expressions to paths to transformers
165 | transform: {
166 | "^.+\\.(js|ts)$": "ts-jest"
167 | }
168 |
169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
170 | // transformIgnorePatterns: [
171 | // "/node_modules/"
172 | // ],
173 |
174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
175 | // unmockedModulePathPatterns: undefined,
176 |
177 | // Indicates whether each individual test should be reported during the run
178 | // verbose: null,
179 |
180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
181 | // watchPathIgnorePatterns: [],
182 |
183 | // Whether to use watchman for file crawling
184 | // watchman: true,
185 | };
186 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@oblakstudio/qr-code-styling",
3 | "version": "1.1.0",
4 | "description": "Add a style and an image to your QR code",
5 | "main": "lib/qr-code-styling.js",
6 | "types": "lib/index.d.ts",
7 | "files": [
8 | "lib"
9 | ],
10 | "dependencies": {
11 | "qrcode-generator": "^1.4.3"
12 | },
13 | "devDependencies": {
14 | "@semantic-release/changelog": "^5.0.1",
15 | "@semantic-release/exec": "^5.0.0",
16 | "@semantic-release/git": "^9.0.0",
17 | "@semantic-release/github": "^7.2.3",
18 | "@semantic-release/npm": "^7.1.3",
19 | "@typescript-eslint/eslint-plugin": "^4.13.0",
20 | "@typescript-eslint/parser": "^4.13.0",
21 | "canvas": "^2.7.0",
22 | "clean-webpack-plugin": "^3.0.0",
23 | "eslint": "^7.17.0",
24 | "eslint-config-prettier": "^7.1.0",
25 | "eslint-loader": "^4.0.2",
26 | "eslint-plugin-jest": "^24.1.3",
27 | "eslint-plugin-prettier": "^3.3.1",
28 | "filemanager-webpack-plugin": "^4.0.0",
29 | "html-webpack-plugin": "^4.5.1",
30 | "jest": "^26.6.3",
31 | "jest-environment-jsdom-fifteen": "^1.0.0",
32 | "jsdom": "^16.6.0",
33 | "prettier": "^2.2.1",
34 | "semantic-release": "^17.4.3",
35 | "ts-jest": "^26.4.4",
36 | "ts-loader": "^8.0.14",
37 | "typescript": "^4.1.3",
38 | "webpack": "^5.12.3",
39 | "webpack-cli": "^4.3.1",
40 | "webpack-dev-server": "^3.11.1",
41 | "webpack-merge": "^5.7.3"
42 | },
43 | "scripts": {
44 | "build": "webpack --mode=production --config webpack.config.build.js",
45 | "build:dev": "webpack --mode=development --config webpack.config.build.js",
46 | "test": "jest",
47 | "start": "webpack serve --open --config webpack.config.dev-server.js",
48 | "release": "semantic-release"
49 | },
50 | "repository": {
51 | "type": "git",
52 | "url": "git+https://github.com/oblakstudio/qr-code-styling.git"
53 | },
54 | "keywords": [
55 | "qr",
56 | "qrcode",
57 | "qr-code",
58 | "js",
59 | "qrjs",
60 | "qrstyling",
61 | "styling",
62 | "qrbranding",
63 | "branding",
64 | "qrimage",
65 | "image",
66 | "qrlogo",
67 | "logo",
68 | "design"
69 | ],
70 | "author": "Denys Kozak ",
71 | "license": "MIT",
72 | "bugs": {
73 | "url": "https://github.com/oblakstudio/qr-code-styling/issues"
74 | },
75 | "homepage": "https://qr-code-styling.com"
76 | }
77 |
--------------------------------------------------------------------------------
/src/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/.DS_Store
--------------------------------------------------------------------------------
/src/assets/facebook_example_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/facebook_example_new.png
--------------------------------------------------------------------------------
/src/assets/instagram_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/instagram_example.png
--------------------------------------------------------------------------------
/src/assets/qr_code_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/qr_code_example.png
--------------------------------------------------------------------------------
/src/assets/telegram_example_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/telegram_example_new.png
--------------------------------------------------------------------------------
/src/assets/test/image_from_readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/image_from_readme.png
--------------------------------------------------------------------------------
/src/assets/test/image_from_readme.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/test/rounded_dots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/rounded_dots.png
--------------------------------------------------------------------------------
/src/assets/test/simple_dots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/simple_dots.png
--------------------------------------------------------------------------------
/src/assets/test/simple_qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/simple_qr.png
--------------------------------------------------------------------------------
/src/assets/test/simple_qr_with_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/simple_qr_with_image.png
--------------------------------------------------------------------------------
/src/assets/test/simple_qr_with_image_margin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/simple_qr_with_image_margin.png
--------------------------------------------------------------------------------
/src/assets/test/simple_qr_with_margin_canvas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/simple_qr_with_margin_canvas.png
--------------------------------------------------------------------------------
/src/assets/test/simple_square_dot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oblakstudio/qr-code-styling/29fc080550e3dc1146c50a4d0838f4a3a3ccb87b/src/assets/test/simple_square_dot.png
--------------------------------------------------------------------------------
/src/constants/cornerDotTypes.ts:
--------------------------------------------------------------------------------
1 | import { CornerDotTypes } from "../types";
2 |
3 | export default {
4 | dot: "dot",
5 | square: "square"
6 | } as CornerDotTypes;
7 |
--------------------------------------------------------------------------------
/src/constants/cornerSquareTypes.ts:
--------------------------------------------------------------------------------
1 | import { CornerSquareTypes } from "../types";
2 |
3 | export default {
4 | dot: "dot",
5 | square: "square",
6 | extraRounded: "extra-rounded"
7 | } as CornerSquareTypes;
8 |
--------------------------------------------------------------------------------
/src/constants/dotTypes.test.js:
--------------------------------------------------------------------------------
1 | import dotTypes from "./dotTypes";
2 |
3 | describe("Dot Types", () => {
4 | it("The export of the module should be an object", () => {
5 | expect(typeof dotTypes).toBe("object");
6 | });
7 |
8 | it.each(Object.values(dotTypes))("Values should be strings", value => {
9 | expect(typeof value).toBe("string");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/constants/dotTypes.ts:
--------------------------------------------------------------------------------
1 | import { DotTypes } from "../types";
2 |
3 | export default {
4 | dots: "dots",
5 | rounded: "rounded",
6 | classy: "classy",
7 | classyRounded: "classy-rounded",
8 | square: "square",
9 | extraRounded: "extra-rounded"
10 | } as DotTypes;
11 |
--------------------------------------------------------------------------------
/src/constants/drawTypes.ts:
--------------------------------------------------------------------------------
1 | import { DrawTypes } from "../types";
2 |
3 | export default {
4 | canvas: "canvas",
5 | svg: "svg"
6 | } as DrawTypes;
7 |
--------------------------------------------------------------------------------
/src/constants/errorCorrectionLevels.test.js:
--------------------------------------------------------------------------------
1 | import errorCorrectionLevels from "./errorCorrectionLevels";
2 |
3 | describe("Error Correction Levels", () => {
4 | it("The export of the module should be an object", () => {
5 | expect(typeof errorCorrectionLevels).toBe("object");
6 | });
7 |
8 | it.each(Object.values(errorCorrectionLevels))("Values should be strings", value => {
9 | expect(typeof value).toBe("string");
10 | });
11 |
12 | it.each(Object.keys(errorCorrectionLevels))("A key of the object should be the same as a value", key => {
13 | expect(key).toBe(errorCorrectionLevels[key]);
14 | });
15 |
16 | it.each(Object.keys(errorCorrectionLevels))("Allowed only particular keys", key => {
17 | expect(["L", "M", "Q", "H"]).toContain(key);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/constants/errorCorrectionLevels.ts:
--------------------------------------------------------------------------------
1 | import { ErrorCorrectionLevel } from "../types";
2 |
3 | interface ErrorCorrectionLevels {
4 | [key: string]: ErrorCorrectionLevel;
5 | }
6 |
7 | export default {
8 | L: "L",
9 | M: "M",
10 | Q: "Q",
11 | H: "H"
12 | } as ErrorCorrectionLevels;
13 |
--------------------------------------------------------------------------------
/src/constants/errorCorrectionPercents.test.js:
--------------------------------------------------------------------------------
1 | import errorCorrectionPercents from "./errorCorrectionPercents";
2 |
3 | describe("Error Correction Percents", () => {
4 | it("The export of the module should be an object", () => {
5 | expect(typeof errorCorrectionPercents).toBe("object");
6 | });
7 |
8 | it.each(Object.values(errorCorrectionPercents))("Values should be numbers", value => {
9 | expect(typeof value).toBe("number");
10 | });
11 |
12 | it.each(Object.keys(errorCorrectionPercents))("Allowed only particular keys", key => {
13 | expect(["L", "M", "Q", "H"]).toContain(key);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/constants/errorCorrectionPercents.ts:
--------------------------------------------------------------------------------
1 | interface ErrorCorrectionPercents {
2 | [key: string]: number;
3 | }
4 |
5 | export default {
6 | L: 0.07,
7 | M: 0.15,
8 | Q: 0.25,
9 | H: 0.3
10 | } as ErrorCorrectionPercents;
11 |
--------------------------------------------------------------------------------
/src/constants/gradientTypes.ts:
--------------------------------------------------------------------------------
1 | import { GradientTypes } from "../types";
2 |
3 | export default {
4 | radial: "radial",
5 | linear: "linear"
6 | } as GradientTypes;
7 |
--------------------------------------------------------------------------------
/src/constants/modes.test.js:
--------------------------------------------------------------------------------
1 | import modes from "./modes";
2 |
3 | describe("Modes", () => {
4 | it("The export of the module should be an object", () => {
5 | expect(typeof modes).toBe("object");
6 | });
7 |
8 | it.each(Object.values(modes))("Values should be strings", value => {
9 | expect(typeof value).toBe("string");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/constants/modes.ts:
--------------------------------------------------------------------------------
1 | import { Mode } from "../types";
2 |
3 | interface Modes {
4 | [key: string]: Mode;
5 | }
6 |
7 | export default {
8 | numeric: "Numeric",
9 | alphanumeric: "Alphanumeric",
10 | byte: "Byte",
11 | kanji: "Kanji"
12 | } as Modes;
13 |
--------------------------------------------------------------------------------
/src/constants/qrTypes.test.js:
--------------------------------------------------------------------------------
1 | import qrTypes from "./qrTypes";
2 |
3 | describe("QR Types", () => {
4 | it("The export of the module should be an object", () => {
5 | expect(typeof qrTypes).toBe("object");
6 | });
7 |
8 | it.each(Object.values(qrTypes))("Values should be number", value => {
9 | expect(typeof value).toBe("number");
10 | });
11 |
12 | it.each(Object.keys(qrTypes))("A key of the object should be the same as a value", key => {
13 | expect(key).toBe(qrTypes[key].toString());
14 | });
15 |
16 | it("The object should contain 41 keys", () => {
17 | expect(Object.keys(qrTypes).length).toBe(41);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/constants/qrTypes.ts:
--------------------------------------------------------------------------------
1 | import { TypeNumber } from "../types";
2 |
3 | interface TypesMap {
4 | [key: number]: TypeNumber;
5 | }
6 |
7 | const qrTypes: TypesMap = {};
8 |
9 | for (let type = 0; type <= 40; type++) {
10 | qrTypes[type] = type as TypeNumber;
11 | }
12 |
13 | // 0 types is autodetect
14 |
15 | // types = {
16 | // 0: 0,
17 | // 1: 1,
18 | // ...
19 | // 40: 40
20 | // }
21 |
22 | export default qrTypes;
23 |
--------------------------------------------------------------------------------
/src/core/QRCanvas.test.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import qrcode from "qrcode-generator";
4 | import QRCanvas from "./QRCanvas";
5 | import modes from "../constants/modes";
6 | import mergeDeep from "../tools/merge";
7 | import defaultQRCodeStylingOptions from "./QROptions";
8 |
9 | describe("Test QRCanvas class", () => {
10 | let qr;
11 | const defaultOptions = mergeDeep(defaultQRCodeStylingOptions, {
12 | width: 100,
13 | height: 100,
14 | data: "TEST",
15 | qrOptions: {
16 | mode: modes.alphanumeric
17 | }
18 | });
19 | const defaultImage =
20 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=";
21 |
22 | beforeAll(() => {
23 | qr = qrcode(defaultOptions.qrOptions.typeNumber, defaultOptions.qrOptions.errorCorrectionLevel);
24 | qr.addData(defaultOptions.data, defaultOptions.qrOptions.mode);
25 | qr.make();
26 | });
27 |
28 | it("Should draw simple qr code", () => {
29 | const expectedQRCodeFile = fs.readFileSync(path.resolve(__dirname, "../assets/test/simple_qr.png"), "base64");
30 | const canvas = new QRCanvas(defaultOptions);
31 |
32 | canvas.drawQR(qr);
33 | expect(canvas.getCanvas().toDataURL()).toEqual(expect.stringContaining(expectedQRCodeFile));
34 | });
35 | it("Should draw a qr code with image", done => {
36 | const expectedQRCodeFile = fs.readFileSync(
37 | path.resolve(__dirname, "../assets/test/simple_qr_with_image.png"),
38 | "base64"
39 | );
40 | const canvas = new QRCanvas({
41 | ...defaultOptions,
42 | image: defaultImage
43 | });
44 | canvas.drawQR(qr);
45 | //TODO remove setTimout
46 | setTimeout(() => {
47 | canvas._image.onload();
48 | expect(canvas.getCanvas().toDataURL()).toEqual(expect.stringContaining(expectedQRCodeFile));
49 | done();
50 | });
51 | });
52 | it("Should draw a qr code with image margin", done => {
53 | const expectedQRCodeFile = fs.readFileSync(
54 | path.resolve(__dirname, "../assets/test/simple_qr_with_image_margin.png"),
55 | "base64"
56 | );
57 | const canvas = new QRCanvas({
58 | ...defaultOptions,
59 | image: defaultImage,
60 | imageOptions: {
61 | ...defaultOptions.imageOptions,
62 | margin: 2
63 | }
64 | });
65 | canvas.drawQR(qr);
66 | //TODO remove setTimout
67 | setTimeout(() => {
68 | canvas._image.onload();
69 | expect(canvas.getCanvas().toDataURL()).toEqual(expect.stringContaining(expectedQRCodeFile));
70 | done();
71 | });
72 | });
73 | it("Should draw a qr code with image without dots hiding", done => {
74 | const expectedQRCodeFile = fs.readFileSync(
75 | path.resolve(__dirname, "../assets/test/simple_qr_with_image.png"),
76 | "base64"
77 | );
78 | const canvas = new QRCanvas({
79 | ...defaultOptions,
80 | image: defaultImage,
81 | imageOptions: {
82 | ...defaultOptions.imageOptions,
83 | hideBackgroundDots: false
84 | }
85 | });
86 | canvas.drawQR(qr);
87 | //TODO remove setTimout
88 | setTimeout(() => {
89 | canvas._image.onload();
90 | expect(canvas.getCanvas().toDataURL()).toEqual(expect.stringContaining(expectedQRCodeFile));
91 | done();
92 | });
93 | });
94 | it("Should draw a qr code with margin around canvas", () => {
95 | const expectedQRCodeFile = fs.readFileSync(
96 | path.resolve(__dirname, "../assets/test/simple_qr_with_margin_canvas.png"),
97 | "base64"
98 | );
99 | const canvas = new QRCanvas({
100 | ...defaultOptions,
101 | margin: 20
102 | });
103 | canvas.drawQR(qr);
104 | expect(canvas.getCanvas().toDataURL()).toEqual(expect.stringContaining(expectedQRCodeFile));
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/src/core/QRCanvas.ts:
--------------------------------------------------------------------------------
1 | import calculateImageSize from "../tools/calculateImageSize";
2 | import errorCorrectionPercents from "../constants/errorCorrectionPercents";
3 | import QRDot from "../figures/dot/canvas/QRDot";
4 | import QRCornerSquare from "../figures/cornerSquare/canvas/QRCornerSquare";
5 | import QRCornerDot from "../figures/cornerDot/canvas/QRCornerDot";
6 | import { RequiredOptions } from "./QROptions";
7 | import gradientTypes from "../constants/gradientTypes";
8 | import { QRCode, Gradient, FilterFunction, Canvas } from "../types";
9 |
10 | const squareMask = [
11 | [1, 1, 1, 1, 1, 1, 1],
12 | [1, 0, 0, 0, 0, 0, 1],
13 | [1, 0, 0, 0, 0, 0, 1],
14 | [1, 0, 0, 0, 0, 0, 1],
15 | [1, 0, 0, 0, 0, 0, 1],
16 | [1, 0, 0, 0, 0, 0, 1],
17 | [1, 1, 1, 1, 1, 1, 1]
18 | ];
19 |
20 | const dotMask = [
21 | [0, 0, 0, 0, 0, 0, 0],
22 | [0, 0, 0, 0, 0, 0, 0],
23 | [0, 0, 1, 1, 1, 0, 0],
24 | [0, 0, 1, 1, 1, 0, 0],
25 | [0, 0, 1, 1, 1, 0, 0],
26 | [0, 0, 0, 0, 0, 0, 0],
27 | [0, 0, 0, 0, 0, 0, 0]
28 | ];
29 |
30 | export default class QRCanvas {
31 | _canvas: Canvas;
32 | _options: RequiredOptions;
33 | _qr?: QRCode;
34 | _image?: HTMLImageElement;
35 |
36 | //TODO don't pass all options to this class
37 | constructor(options: RequiredOptions) {
38 | if (options.nodeCanvas?.createCanvas) {
39 | this._canvas = options.nodeCanvas.createCanvas(options.width, options.height);
40 | } else {
41 | this._canvas = document.createElement("canvas");
42 | }
43 | this._canvas.width = options.width;
44 | this._canvas.height = options.height;
45 | this._options = options;
46 | }
47 |
48 | get context(): CanvasRenderingContext2D | null {
49 | return this._canvas.getContext("2d");
50 | }
51 |
52 | get width(): number {
53 | return this._canvas.width;
54 | }
55 |
56 | get height(): number {
57 | return this._canvas.height;
58 | }
59 |
60 | getCanvas(): Canvas {
61 | return this._canvas;
62 | }
63 |
64 | clear(): void {
65 | const canvasContext = this.context;
66 |
67 | if (canvasContext) {
68 | canvasContext.clearRect(0, 0, this._canvas.width, this._canvas.height);
69 | }
70 | }
71 |
72 | async drawQR(qr: QRCode): Promise {
73 | const count = qr.getModuleCount();
74 | const minSize = Math.min(this._options.width, this._options.height) - this._options.margin * 2;
75 | const dotSize = Math.floor(minSize / count);
76 | let drawImageSize = {
77 | hideXDots: 0,
78 | hideYDots: 0,
79 | width: 0,
80 | height: 0
81 | };
82 |
83 | this._qr = qr;
84 |
85 | if (this._options.image) {
86 | await this.loadImage();
87 | if (!this._image) return;
88 | const { imageOptions, qrOptions } = this._options;
89 | const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel];
90 | const maxHiddenDots = Math.floor(coverLevel * count * count);
91 |
92 | drawImageSize = calculateImageSize({
93 | originalWidth: this._image.width,
94 | originalHeight: this._image.height,
95 | maxHiddenDots,
96 | maxHiddenAxisDots: count - 14,
97 | dotSize
98 | });
99 | }
100 |
101 | this.clear();
102 | this.drawBackground();
103 | this.drawDots((i: number, j: number): boolean => {
104 | if (this._options.imageOptions.hideBackgroundDots) {
105 | if (
106 | i >= (count - drawImageSize.hideXDots) / 2 &&
107 | i < (count + drawImageSize.hideXDots) / 2 &&
108 | j >= (count - drawImageSize.hideYDots) / 2 &&
109 | j < (count + drawImageSize.hideYDots) / 2
110 | ) {
111 | return false;
112 | }
113 | }
114 |
115 | if (squareMask[i]?.[j] || squareMask[i - count + 7]?.[j] || squareMask[i]?.[j - count + 7]) {
116 | return false;
117 | }
118 |
119 | if (dotMask[i]?.[j] || dotMask[i - count + 7]?.[j] || dotMask[i]?.[j - count + 7]) {
120 | return false;
121 | }
122 |
123 | return true;
124 | });
125 | this.drawCorners();
126 |
127 | if (this._options.image) {
128 | this.drawImage({ width: drawImageSize.width, height: drawImageSize.height, count, dotSize });
129 | }
130 | }
131 |
132 | drawBackground(): void {
133 | const canvasContext = this.context;
134 | const options = this._options;
135 |
136 | if (canvasContext) {
137 | if (options.backgroundOptions.gradient) {
138 | const gradientOptions = options.backgroundOptions.gradient;
139 | const gradient = this._createGradient({
140 | context: canvasContext,
141 | options: gradientOptions,
142 | additionalRotation: 0,
143 | x: 0,
144 | y: 0,
145 | size: this._canvas.width > this._canvas.height ? this._canvas.width : this._canvas.height
146 | });
147 |
148 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
149 | gradient.addColorStop(offset, color);
150 | });
151 |
152 | canvasContext.fillStyle = gradient;
153 | } else if (options.backgroundOptions.color) {
154 | canvasContext.fillStyle = options.backgroundOptions.color;
155 | }
156 | canvasContext.fillRect(0, 0, this._canvas.width, this._canvas.height);
157 | }
158 | }
159 |
160 | drawDots(filter?: FilterFunction): void {
161 | if (!this._qr) {
162 | throw "QR code is not defined";
163 | }
164 |
165 | const canvasContext = this.context;
166 |
167 | if (!canvasContext) {
168 | throw "QR code is not defined";
169 | }
170 |
171 | const options = this._options;
172 | const count = this._qr.getModuleCount();
173 |
174 | if (count > options.width || count > options.height) {
175 | throw "The canvas is too small.";
176 | }
177 |
178 | const minSize = Math.min(options.width, options.height) - options.margin * 2;
179 | const dotSize = Math.floor(minSize / count);
180 | const xBeginning = Math.floor((options.width - count * dotSize) / 2);
181 | const yBeginning = Math.floor((options.height - count * dotSize) / 2);
182 | const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type });
183 |
184 | canvasContext.beginPath();
185 |
186 | for (let i = 0; i < count; i++) {
187 | for (let j = 0; j < count; j++) {
188 | if (filter && !filter(i, j)) {
189 | continue;
190 | }
191 | if (!this._qr.isDark(i, j)) {
192 | continue;
193 | }
194 | dot.draw(
195 | xBeginning + i * dotSize,
196 | yBeginning + j * dotSize,
197 | dotSize,
198 | (xOffset: number, yOffset: number): boolean => {
199 | if (i + xOffset < 0 || j + yOffset < 0 || i + xOffset >= count || j + yOffset >= count) return false;
200 | if (filter && !filter(i + xOffset, j + yOffset)) return false;
201 | return !!this._qr && this._qr.isDark(i + xOffset, j + yOffset);
202 | }
203 | );
204 | }
205 | }
206 |
207 | if (options.dotsOptions.gradient) {
208 | const gradientOptions = options.dotsOptions.gradient;
209 | const gradient = this._createGradient({
210 | context: canvasContext,
211 | options: gradientOptions,
212 | additionalRotation: 0,
213 | x: xBeginning,
214 | y: yBeginning,
215 | size: count * dotSize
216 | });
217 |
218 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
219 | gradient.addColorStop(offset, color);
220 | });
221 |
222 | canvasContext.fillStyle = canvasContext.strokeStyle = gradient;
223 | } else if (options.dotsOptions.color) {
224 | canvasContext.fillStyle = canvasContext.strokeStyle = options.dotsOptions.color;
225 | }
226 |
227 | canvasContext.fill("evenodd");
228 | }
229 |
230 | drawCorners(filter?: FilterFunction): void {
231 | if (!this._qr) {
232 | throw "QR code is not defined";
233 | }
234 |
235 | const canvasContext = this.context;
236 |
237 | if (!canvasContext) {
238 | throw "QR code is not defined";
239 | }
240 |
241 | const options = this._options;
242 |
243 | const count = this._qr.getModuleCount();
244 | const minSize = Math.min(options.width, options.height) - options.margin * 2;
245 | const dotSize = Math.floor(minSize / count);
246 | const cornersSquareSize = dotSize * 7;
247 | const cornersDotSize = dotSize * 3;
248 | const xBeginning = Math.floor((options.width - count * dotSize) / 2);
249 | const yBeginning = Math.floor((options.height - count * dotSize) / 2);
250 |
251 | [
252 | [0, 0, 0],
253 | [1, 0, Math.PI / 2],
254 | [0, 1, -Math.PI / 2]
255 | ].forEach(([column, row, rotation]) => {
256 | if (filter && !filter(column, row)) {
257 | return;
258 | }
259 |
260 | const x = xBeginning + column * dotSize * (count - 7);
261 | const y = yBeginning + row * dotSize * (count - 7);
262 |
263 | if (options.cornersSquareOptions?.type) {
264 | const cornersSquare = new QRCornerSquare({ context: canvasContext, type: options.cornersSquareOptions?.type });
265 |
266 | canvasContext.beginPath();
267 | cornersSquare.draw(x, y, cornersSquareSize, rotation);
268 | } else {
269 | const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type });
270 |
271 | canvasContext.beginPath();
272 |
273 | for (let i = 0; i < squareMask.length; i++) {
274 | for (let j = 0; j < squareMask[i].length; j++) {
275 | if (!squareMask[i]?.[j]) {
276 | continue;
277 | }
278 |
279 | dot.draw(
280 | x + i * dotSize,
281 | y + j * dotSize,
282 | dotSize,
283 | (xOffset: number, yOffset: number): boolean => !!squareMask[i + xOffset]?.[j + yOffset]
284 | );
285 | }
286 | }
287 | }
288 |
289 | if (options.cornersSquareOptions?.gradient) {
290 | const gradientOptions = options.cornersSquareOptions.gradient;
291 | const gradient = this._createGradient({
292 | context: canvasContext,
293 | options: gradientOptions,
294 | additionalRotation: rotation,
295 | x,
296 | y,
297 | size: cornersSquareSize
298 | });
299 |
300 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
301 | gradient.addColorStop(offset, color);
302 | });
303 |
304 | canvasContext.fillStyle = canvasContext.strokeStyle = gradient;
305 | } else if (options.cornersSquareOptions?.color) {
306 | canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersSquareOptions.color;
307 | }
308 |
309 | canvasContext.fill("evenodd");
310 |
311 | if (options.cornersDotOptions?.type) {
312 | const cornersDot = new QRCornerDot({ context: canvasContext, type: options.cornersDotOptions?.type });
313 |
314 | canvasContext.beginPath();
315 | cornersDot.draw(x + dotSize * 2, y + dotSize * 2, cornersDotSize, rotation);
316 | } else {
317 | const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type });
318 |
319 | canvasContext.beginPath();
320 |
321 | for (let i = 0; i < dotMask.length; i++) {
322 | for (let j = 0; j < dotMask[i].length; j++) {
323 | if (!dotMask[i]?.[j]) {
324 | continue;
325 | }
326 |
327 | dot.draw(
328 | x + i * dotSize,
329 | y + j * dotSize,
330 | dotSize,
331 | (xOffset: number, yOffset: number): boolean => !!dotMask[i + xOffset]?.[j + yOffset]
332 | );
333 | }
334 | }
335 | }
336 |
337 | if (options.cornersDotOptions?.gradient) {
338 | const gradientOptions = options.cornersDotOptions.gradient;
339 | const gradient = this._createGradient({
340 | context: canvasContext,
341 | options: gradientOptions,
342 | additionalRotation: rotation,
343 | x: x + dotSize * 2,
344 | y: y + dotSize * 2,
345 | size: cornersDotSize
346 | });
347 |
348 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
349 | gradient.addColorStop(offset, color);
350 | });
351 |
352 | canvasContext.fillStyle = canvasContext.strokeStyle = gradient;
353 | } else if (options.cornersDotOptions?.color) {
354 | canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersDotOptions.color;
355 | }
356 |
357 | canvasContext.fill("evenodd");
358 | });
359 | }
360 |
361 | loadImage(): Promise {
362 | return new Promise((resolve, reject) => {
363 | const options = this._options;
364 |
365 | if (!options.image) {
366 | return reject("Image is not defined");
367 | }
368 |
369 | if (options.nodeCanvas?.loadImage) {
370 | options.nodeCanvas
371 | .loadImage(options.image)
372 | .then((image: HTMLImageElement) => {
373 | // fix blurry svg
374 | if (/(\.svg$)|(^data:image\/svg)/.test(options.image ?? "")) {
375 | image.width = this._options.width;
376 | image.height = this._options.height;
377 | }
378 | this._image = image;
379 | resolve();
380 | })
381 | .catch(reject);
382 | } else {
383 | const image = new Image();
384 |
385 | if (typeof options.imageOptions.crossOrigin === "string") {
386 | image.crossOrigin = options.imageOptions.crossOrigin;
387 | }
388 |
389 | this._image = image;
390 | image.onload = (): void => {
391 | resolve();
392 | };
393 | image.src = options.image;
394 | }
395 | });
396 | }
397 |
398 | drawImage({
399 | width,
400 | height,
401 | count,
402 | dotSize
403 | }: {
404 | width: number;
405 | height: number;
406 | count: number;
407 | dotSize: number;
408 | }): void {
409 | const canvasContext = this.context;
410 |
411 | if (!canvasContext) {
412 | throw "canvasContext is not defined";
413 | }
414 |
415 | if (!this._image) {
416 | throw "image is not defined";
417 | }
418 |
419 | const options = this._options;
420 | const xBeginning = Math.floor((options.width - count * dotSize) / 2);
421 | const yBeginning = Math.floor((options.height - count * dotSize) / 2);
422 | const dx = xBeginning + options.imageOptions.margin + (count * dotSize - width) / 2;
423 | const dy = yBeginning + options.imageOptions.margin + (count * dotSize - height) / 2;
424 | const dw = width - options.imageOptions.margin * 2;
425 | const dh = height - options.imageOptions.margin * 2;
426 |
427 | canvasContext.drawImage(this._image, dx, dy, dw < 0 ? 0 : dw, dh < 0 ? 0 : dh);
428 | }
429 |
430 | _createGradient({
431 | context,
432 | options,
433 | additionalRotation,
434 | x,
435 | y,
436 | size
437 | }: {
438 | context: CanvasRenderingContext2D;
439 | options: Gradient;
440 | additionalRotation: number;
441 | x: number;
442 | y: number;
443 | size: number;
444 | }): CanvasGradient {
445 | let gradient;
446 |
447 | if (options.type === gradientTypes.radial) {
448 | gradient = context.createRadialGradient(x + size / 2, y + size / 2, 0, x + size / 2, y + size / 2, size / 2);
449 | } else {
450 | const rotation = ((options.rotation || 0) + additionalRotation) % (2 * Math.PI);
451 | const positiveRotation = (rotation + 2 * Math.PI) % (2 * Math.PI);
452 | let x0 = x + size / 2;
453 | let y0 = y + size / 2;
454 | let x1 = x + size / 2;
455 | let y1 = y + size / 2;
456 |
457 | if (
458 | (positiveRotation >= 0 && positiveRotation <= 0.25 * Math.PI) ||
459 | (positiveRotation > 1.75 * Math.PI && positiveRotation <= 2 * Math.PI)
460 | ) {
461 | x0 = x0 - size / 2;
462 | y0 = y0 - (size / 2) * Math.tan(rotation);
463 | x1 = x1 + size / 2;
464 | y1 = y1 + (size / 2) * Math.tan(rotation);
465 | } else if (positiveRotation > 0.25 * Math.PI && positiveRotation <= 0.75 * Math.PI) {
466 | y0 = y0 - size / 2;
467 | x0 = x0 - size / 2 / Math.tan(rotation);
468 | y1 = y1 + size / 2;
469 | x1 = x1 + size / 2 / Math.tan(rotation);
470 | } else if (positiveRotation > 0.75 * Math.PI && positiveRotation <= 1.25 * Math.PI) {
471 | x0 = x0 + size / 2;
472 | y0 = y0 + (size / 2) * Math.tan(rotation);
473 | x1 = x1 - size / 2;
474 | y1 = y1 - (size / 2) * Math.tan(rotation);
475 | } else if (positiveRotation > 1.25 * Math.PI && positiveRotation <= 1.75 * Math.PI) {
476 | y0 = y0 + size / 2;
477 | x0 = x0 + size / 2 / Math.tan(rotation);
478 | y1 = y1 - size / 2;
479 | x1 = x1 - size / 2 / Math.tan(rotation);
480 | }
481 |
482 | gradient = context.createLinearGradient(Math.round(x0), Math.round(y0), Math.round(x1), Math.round(y1));
483 | }
484 |
485 | return gradient;
486 | }
487 | }
488 |
--------------------------------------------------------------------------------
/src/core/QRCodeStyling.test.js:
--------------------------------------------------------------------------------
1 | import QRCodeStyling from "./QRCodeStyling";
2 | import fs from "fs";
3 | import path from "path";
4 | import nodeCanvas from "canvas";
5 | import { JSDOM } from "jsdom";
6 |
7 | describe("Test QRCodeStyling class", () => {
8 | beforeAll(() => {
9 | global.document.body.innerHTML = "";
10 | });
11 |
12 | it("The README example should work correctly", (done) => {
13 | const expectedQRCodeFile = fs.readFileSync(
14 | path.resolve(__dirname, "../assets/test/image_from_readme.png"),
15 | "base64"
16 | );
17 | const qrCode = new QRCodeStyling({
18 | width: 300,
19 | height: 300,
20 | data: "TEST",
21 | image:
22 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=",
23 | dotsOptions: {
24 | color: "#4267b2",
25 | type: "rounded"
26 | },
27 | backgroundOptions: {
28 | color: "#e9ebee"
29 | }
30 | });
31 | global.document.body.innerHTML = "";
32 |
33 | const container = global.document.getElementById("container");
34 |
35 | qrCode.append(container);
36 | //TODO remove setTimout
37 | setTimeout(() => {
38 | expect(qrCode._canvas.getCanvas().toDataURL()).toEqual(expect.stringContaining(expectedQRCodeFile));
39 | done();
40 | });
41 | });
42 |
43 | it("Compatible with node-canvas", (done) => {
44 | const expectedQRCodeFile = fs.readFileSync(
45 | path.resolve(__dirname, "../assets/test/image_from_readme.png"),
46 | "base64"
47 | );
48 | const qrCode = new QRCodeStyling({
49 | nodeCanvas,
50 | width: 300,
51 | height: 300,
52 | data: "TEST",
53 | image:
54 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=",
55 | dotsOptions: {
56 | color: "#4267b2",
57 | type: "rounded"
58 | },
59 | backgroundOptions: {
60 | color: "#e9ebee"
61 | }
62 | });
63 | qrCode.getRawData("png").then((buffer) => {
64 | const uri = `data:image/png;base64,${buffer.toString("base64")}`;
65 | expect(uri).toEqual(expect.stringContaining(expectedQRCodeFile));
66 | done();
67 | });
68 | });
69 |
70 | it("Compatible with jsdom", (done) => {
71 | const expectedQRCodeFile = fs.readFileSync(
72 | path.resolve(__dirname, "../assets/test/image_from_readme.svg"),
73 | "base64"
74 | );
75 | const qrCode = new QRCodeStyling({
76 | jsdom: JSDOM,
77 | type: "svg",
78 | width: 300,
79 | height: 300,
80 | data: "TEST",
81 | image:
82 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=",
83 | dotsOptions: {
84 | color: "#4267b2",
85 | type: "rounded"
86 | },
87 | backgroundOptions: {
88 | color: "#e9ebee"
89 | }
90 | });
91 | qrCode.getRawData("svg").then((buffer) => {
92 | const svgString = buffer.toString("base64");
93 | expect(svgString).toEqual(expect.stringContaining(expectedQRCodeFile));
94 | done();
95 | });
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/core/QRCodeStyling.ts:
--------------------------------------------------------------------------------
1 | import getMode from "../tools/getMode";
2 | import mergeDeep from "../tools/merge";
3 | import downloadURI from "../tools/downloadURI";
4 | import QRCanvas from "./QRCanvas";
5 | import QRSVG from "./QRSVG";
6 | import drawTypes from "../constants/drawTypes";
7 |
8 | import defaultOptions, { RequiredOptions } from "./QROptions";
9 | import sanitizeOptions from "../tools/sanitizeOptions";
10 | import { Extension, QRCode, Options, DownloadOptions } from "../types";
11 | import qrcode from "qrcode-generator";
12 |
13 | export default class QRCodeStyling {
14 | _options: RequiredOptions;
15 | _container?: HTMLElement;
16 | _canvas?: QRCanvas;
17 | _svg?: QRSVG;
18 | _qr?: QRCode;
19 | _canvasDrawingPromise?: Promise;
20 | _svgDrawingPromise?: Promise;
21 |
22 | constructor(options?: Partial) {
23 | this._options = options ? sanitizeOptions(mergeDeep(defaultOptions, options) as RequiredOptions) : defaultOptions;
24 | this.update();
25 | }
26 |
27 | static _clearContainer(container?: HTMLElement): void {
28 | if (container) {
29 | container.innerHTML = "";
30 | }
31 | }
32 |
33 | async _getQRStylingElement(extension: Extension = "png"): Promise {
34 | if (!this._qr) throw "QR code is empty";
35 |
36 | if (extension.toLowerCase() === "svg") {
37 | let promise, svg: QRSVG;
38 |
39 | if (this._svg && this._svgDrawingPromise) {
40 | svg = this._svg;
41 | promise = this._svgDrawingPromise;
42 | } else {
43 | svg = new QRSVG(this._options);
44 | promise = svg.drawQR(this._qr);
45 | }
46 |
47 | await promise;
48 |
49 | return svg;
50 | } else {
51 | let promise, canvas: QRCanvas;
52 |
53 | if (this._canvas && this._canvasDrawingPromise) {
54 | canvas = this._canvas;
55 | promise = this._canvasDrawingPromise;
56 | } else {
57 | canvas = new QRCanvas(this._options);
58 | promise = canvas.drawQR(this._qr);
59 | }
60 |
61 | await promise;
62 |
63 | return canvas;
64 | }
65 | }
66 |
67 | update(options?: Partial): void {
68 | QRCodeStyling._clearContainer(this._container);
69 | this._options = options ? sanitizeOptions(mergeDeep(this._options, options) as RequiredOptions) : this._options;
70 |
71 | if (!this._options.data) {
72 | return;
73 | }
74 |
75 | this._qr = qrcode(this._options.qrOptions.typeNumber, this._options.qrOptions.errorCorrectionLevel);
76 | this._qr.addData(this._options.data, this._options.qrOptions.mode || getMode(this._options.data));
77 | this._qr.make();
78 |
79 | if (this._options.type === drawTypes.canvas) {
80 | this._canvas = new QRCanvas(this._options);
81 | this._canvasDrawingPromise = this._canvas.drawQR(this._qr);
82 | this._svgDrawingPromise = undefined;
83 | this._svg = undefined;
84 | } else {
85 | this._svg = new QRSVG(this._options);
86 | this._svgDrawingPromise = this._svg.drawQR(this._qr);
87 | this._canvasDrawingPromise = undefined;
88 | this._canvas = undefined;
89 | }
90 |
91 | this.append(this._container);
92 | }
93 |
94 | append(container?: HTMLElement): void {
95 | if (!container) {
96 | return;
97 | }
98 |
99 | if (typeof container.appendChild !== "function") {
100 | throw "Container should be a single DOM node";
101 | }
102 |
103 | if (this._options.type === drawTypes.canvas) {
104 | if (this._canvas) {
105 | container.appendChild(this._canvas.getCanvas());
106 | }
107 | } else {
108 | if (this._svg) {
109 | container.appendChild(this._svg.getElement());
110 | }
111 | }
112 |
113 | this._container = container;
114 | }
115 |
116 | async getRawData(extension: Extension = "png"): Promise {
117 | if (!this._qr) throw "QR code is empty";
118 | const element = await this._getQRStylingElement(extension);
119 |
120 | if (extension.toLowerCase() === "svg") {
121 | const serializer = new ((element as unknown) as QRSVG)._window.XMLSerializer();
122 | const source = serializer.serializeToString(((element as unknown) as QRSVG).getElement());
123 | const svgString = `\r\n${source}`;
124 |
125 | if (typeof Blob !== "undefined" && !this._options.jsdom) {
126 | return new Blob([svgString], { type: "image/svg+xml" });
127 | } else {
128 | return Buffer.from(svgString);
129 | }
130 | } else {
131 | return new Promise((resolve) => {
132 | const canvas = ((element as unknown) as QRCanvas).getCanvas();
133 | if (canvas.toBuffer) {
134 | resolve(canvas.toBuffer(`image/${extension}`));
135 | } else {
136 | canvas.toBlob(resolve, `image/${extension}`, 1);
137 | }
138 | });
139 | }
140 | }
141 |
142 | async download(downloadOptions?: Partial | string): Promise {
143 | if (!this._qr) throw "QR code is empty";
144 | if (typeof Blob === "undefined") throw "Cannot download in Node.js, call getRawData instead.";
145 | let extension = "png" as Extension;
146 | let name = "qr";
147 |
148 | //TODO remove deprecated code in the v2
149 | if (typeof downloadOptions === "string") {
150 | extension = downloadOptions as Extension;
151 | console.warn(
152 | "Extension is deprecated as argument for 'download' method, please pass object { name: '...', extension: '...' } as argument"
153 | );
154 | } else if (typeof downloadOptions === "object" && downloadOptions !== null) {
155 | if (downloadOptions.name) {
156 | name = downloadOptions.name;
157 | }
158 | if (downloadOptions.extension) {
159 | extension = downloadOptions.extension;
160 | }
161 | }
162 |
163 | const element = await this._getQRStylingElement(extension);
164 |
165 | if (extension.toLowerCase() === "svg") {
166 | const serializer = new XMLSerializer();
167 | let source = serializer.serializeToString(((element as unknown) as QRSVG).getElement());
168 |
169 | source = '\r\n' + source;
170 | const url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
171 | downloadURI(url, `${name}.svg`);
172 | } else {
173 | const url = ((element as unknown) as QRCanvas).getCanvas().toDataURL(`image/${extension}`);
174 | downloadURI(url, `${name}.${extension}`);
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/core/QROptions.test.js:
--------------------------------------------------------------------------------
1 | import QROptions from "./QROptions";
2 |
3 | describe("Test default QROptions", () => {
4 | it("The export of the module should be an object", () => {
5 | expect(typeof QROptions).toBe("object");
6 | });
7 |
8 | describe("Test the content of options", () => {
9 | const optionsKeys = [
10 | "width",
11 | "height",
12 | "data",
13 | "margin",
14 | "qrOptions",
15 | "imageOptions",
16 | "dotsOptions",
17 | "backgroundOptions"
18 | ];
19 | it.each(optionsKeys)("The options should contain particular keys", (key) => {
20 | expect(Object.keys(QROptions)).toContain(key);
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/core/QROptions.ts:
--------------------------------------------------------------------------------
1 | import qrTypes from "../constants/qrTypes";
2 | import drawTypes from "../constants/drawTypes";
3 | import errorCorrectionLevels from "../constants/errorCorrectionLevels";
4 | import { DotType, Options, TypeNumber, ErrorCorrectionLevel, Mode, DrawType, Gradient } from "../types";
5 |
6 | export interface RequiredOptions extends Options {
7 | type: DrawType;
8 | width: number;
9 | height: number;
10 | margin: number;
11 | data: string;
12 | qrOptions: {
13 | typeNumber: TypeNumber;
14 | mode?: Mode;
15 | errorCorrectionLevel: ErrorCorrectionLevel;
16 | };
17 | imageOptions: {
18 | saveAsBlob: boolean;
19 | hideBackgroundDots: boolean;
20 | imageSize: number;
21 | crossOrigin?: string;
22 | margin: number;
23 | };
24 | dotsOptions: {
25 | type: DotType;
26 | color: string;
27 | gradient?: Gradient;
28 | };
29 | backgroundOptions: {
30 | color: string;
31 | gradient?: Gradient;
32 | };
33 | }
34 |
35 | const defaultOptions: RequiredOptions = {
36 | type: drawTypes.canvas,
37 | width: 300,
38 | height: 300,
39 | data: "",
40 | margin: 0,
41 | qrOptions: {
42 | typeNumber: qrTypes[0],
43 | mode: undefined,
44 | errorCorrectionLevel: errorCorrectionLevels.Q
45 | },
46 | imageOptions: {
47 | saveAsBlob: false,
48 | hideBackgroundDots: true,
49 | imageSize: 0.4,
50 | crossOrigin: undefined,
51 | margin: 0
52 | },
53 | dotsOptions: {
54 | type: "square",
55 | color: "#000"
56 | },
57 | backgroundOptions: {
58 | color: "#fff"
59 | }
60 | };
61 |
62 | export default defaultOptions;
63 |
--------------------------------------------------------------------------------
/src/core/QRSVG.ts:
--------------------------------------------------------------------------------
1 | import calculateImageSize from "../tools/calculateImageSize";
2 | import errorCorrectionPercents from "../constants/errorCorrectionPercents";
3 | import QRDot from "../figures/dot/svg/QRDot";
4 | import QRCornerSquare from "../figures/cornerSquare/svg/QRCornerSquare";
5 | import QRCornerDot from "../figures/cornerDot/svg/QRCornerDot";
6 | import { RequiredOptions } from "./QROptions";
7 | import gradientTypes from "../constants/gradientTypes";
8 | import { QRCode, FilterFunction, Gradient, Window, Canvas } from "../types";
9 |
10 | declare const window: Window;
11 |
12 | const squareMask = [
13 | [1, 1, 1, 1, 1, 1, 1],
14 | [1, 0, 0, 0, 0, 0, 1],
15 | [1, 0, 0, 0, 0, 0, 1],
16 | [1, 0, 0, 0, 0, 0, 1],
17 | [1, 0, 0, 0, 0, 0, 1],
18 | [1, 0, 0, 0, 0, 0, 1],
19 | [1, 1, 1, 1, 1, 1, 1]
20 | ];
21 |
22 | const dotMask = [
23 | [0, 0, 0, 0, 0, 0, 0],
24 | [0, 0, 0, 0, 0, 0, 0],
25 | [0, 0, 1, 1, 1, 0, 0],
26 | [0, 0, 1, 1, 1, 0, 0],
27 | [0, 0, 1, 1, 1, 0, 0],
28 | [0, 0, 0, 0, 0, 0, 0],
29 | [0, 0, 0, 0, 0, 0, 0]
30 | ];
31 |
32 | export default class QRSVG {
33 | _canvas?: Canvas;
34 | _window: Window;
35 | _element: SVGElement;
36 | _defs: SVGElement;
37 | _dotsClipPath?: SVGElement;
38 | _cornersSquareClipPath?: SVGElement;
39 | _cornersDotClipPath?: SVGElement;
40 | _options: RequiredOptions;
41 | _qr?: QRCode;
42 | _image?: HTMLImageElement;
43 | _imageUri?: string;
44 |
45 | //TODO don't pass all options to this class
46 | constructor(options: RequiredOptions) {
47 | if (options.jsdom) {
48 | this._window = new options.jsdom("", { resources: "usable" }).window;
49 | } else {
50 | this._window = window;
51 | }
52 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "svg");
53 | this._element.setAttribute("width", String(options.width));
54 | this._element.setAttribute("height", String(options.height));
55 | this._defs = this._window.document.createElementNS("http://www.w3.org/2000/svg", "defs");
56 | this._element.appendChild(this._defs);
57 |
58 | if (options.imageOptions.saveAsBlob) {
59 | if (options.nodeCanvas?.createCanvas) {
60 | this._canvas = options.nodeCanvas.createCanvas(options.width, options.height);
61 | } else {
62 | this._canvas = document.createElement("canvas");
63 | }
64 | this._canvas.width = options.width;
65 | this._canvas.height = options.height;
66 | }
67 | this._imageUri = options.image;
68 |
69 | this._options = options;
70 | }
71 |
72 | get width(): number {
73 | return this._options.width;
74 | }
75 |
76 | get height(): number {
77 | return this._options.height;
78 | }
79 |
80 | getElement(): SVGElement {
81 | return this._element;
82 | }
83 |
84 | clear(): void {
85 | const oldElement = this._element;
86 | this._element = oldElement.cloneNode(false) as SVGElement;
87 | oldElement?.parentNode?.replaceChild(this._element, oldElement);
88 | this._defs = this._window.document.createElementNS("http://www.w3.org/2000/svg", "defs");
89 | this._element.appendChild(this._defs);
90 | }
91 |
92 | async drawQR(qr: QRCode): Promise {
93 | const count = qr.getModuleCount();
94 | const minSize = Math.min(this._options.width, this._options.height) - this._options.margin * 2;
95 | const dotSize = Math.floor(minSize / count);
96 | let drawImageSize = {
97 | hideXDots: 0,
98 | hideYDots: 0,
99 | width: 0,
100 | height: 0
101 | };
102 |
103 | this._qr = qr;
104 |
105 | if (this._options.image) {
106 | //We need it to get image size
107 | await this.loadImage();
108 | if (!this._image) return;
109 | this.imageToBlob();
110 | const { imageOptions, qrOptions } = this._options;
111 | const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel];
112 | const maxHiddenDots = Math.floor(coverLevel * count * count);
113 |
114 | drawImageSize = calculateImageSize({
115 | originalWidth: this._image.width,
116 | originalHeight: this._image.height,
117 | maxHiddenDots,
118 | maxHiddenAxisDots: count - 14,
119 | dotSize
120 | });
121 | }
122 |
123 | this.clear();
124 | this.drawBackground();
125 | this.drawDots((i: number, j: number): boolean => {
126 | if (this._options.imageOptions.hideBackgroundDots) {
127 | if (
128 | i >= (count - drawImageSize.hideXDots) / 2 &&
129 | i < (count + drawImageSize.hideXDots) / 2 &&
130 | j >= (count - drawImageSize.hideYDots) / 2 &&
131 | j < (count + drawImageSize.hideYDots) / 2
132 | ) {
133 | return false;
134 | }
135 | }
136 |
137 | if (squareMask[i]?.[j] || squareMask[i - count + 7]?.[j] || squareMask[i]?.[j - count + 7]) {
138 | return false;
139 | }
140 |
141 | if (dotMask[i]?.[j] || dotMask[i - count + 7]?.[j] || dotMask[i]?.[j - count + 7]) {
142 | return false;
143 | }
144 |
145 | return true;
146 | });
147 | this.drawCorners();
148 |
149 | if (this._options.image) {
150 | this.drawImage({ width: drawImageSize.width, height: drawImageSize.height, count, dotSize });
151 | }
152 | }
153 |
154 | drawBackground(): void {
155 | const element = this._element;
156 | const options = this._options;
157 |
158 | if (element) {
159 | const gradientOptions = options.backgroundOptions?.gradient;
160 | const color = options.backgroundOptions?.color;
161 |
162 | if (gradientOptions || color) {
163 | this._createColor({
164 | options: gradientOptions,
165 | color: color,
166 | additionalRotation: 0,
167 | x: 0,
168 | y: 0,
169 | height: options.height,
170 | width: options.width,
171 | name: "background-color"
172 | });
173 | }
174 | }
175 | }
176 |
177 | drawDots(filter?: FilterFunction): void {
178 | if (!this._qr) {
179 | throw "QR code is not defined";
180 | }
181 |
182 | const options = this._options;
183 | const count = this._qr.getModuleCount();
184 |
185 | if (count > options.width || count > options.height) {
186 | throw "The canvas is too small.";
187 | }
188 |
189 | const minSize = Math.min(options.width, options.height) - options.margin * 2;
190 | const dotSize = Math.floor(minSize / count);
191 | const xBeginning = Math.floor((options.width - count * dotSize) / 2);
192 | const yBeginning = Math.floor((options.height - count * dotSize) / 2);
193 | const dot = new QRDot({
194 | svg: this._element,
195 | type: options.dotsOptions.type,
196 | window: this._window
197 | });
198 |
199 | this._dotsClipPath = this._window.document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
200 | this._dotsClipPath.setAttribute("id", "clip-path-dot-color");
201 | this._defs.appendChild(this._dotsClipPath);
202 |
203 | this._createColor({
204 | options: options.dotsOptions?.gradient,
205 | color: options.dotsOptions.color,
206 | additionalRotation: 0,
207 | x: xBeginning,
208 | y: yBeginning,
209 | height: count * dotSize,
210 | width: count * dotSize,
211 | name: "dot-color"
212 | });
213 |
214 | for (let i = 0; i < count; i++) {
215 | for (let j = 0; j < count; j++) {
216 | if (filter && !filter(i, j)) {
217 | continue;
218 | }
219 | if (!this._qr?.isDark(i, j)) {
220 | continue;
221 | }
222 |
223 | dot.draw(
224 | xBeginning + i * dotSize,
225 | yBeginning + j * dotSize,
226 | dotSize,
227 | (xOffset: number, yOffset: number): boolean => {
228 | if (i + xOffset < 0 || j + yOffset < 0 || i + xOffset >= count || j + yOffset >= count) return false;
229 | if (filter && !filter(i + xOffset, j + yOffset)) return false;
230 | return !!this._qr && this._qr.isDark(i + xOffset, j + yOffset);
231 | }
232 | );
233 |
234 | if (dot._element && this._dotsClipPath) {
235 | this._dotsClipPath.appendChild(dot._element);
236 | }
237 | }
238 | }
239 | }
240 |
241 | drawCorners(): void {
242 | if (!this._qr) {
243 | throw "QR code is not defined";
244 | }
245 |
246 | const element = this._element;
247 | const options = this._options;
248 |
249 | if (!element) {
250 | throw "Element code is not defined";
251 | }
252 |
253 | const count = this._qr.getModuleCount();
254 | const minSize = Math.min(options.width, options.height) - options.margin * 2;
255 | const dotSize = Math.floor(minSize / count);
256 | const cornersSquareSize = dotSize * 7;
257 | const cornersDotSize = dotSize * 3;
258 | const xBeginning = Math.floor((options.width - count * dotSize) / 2);
259 | const yBeginning = Math.floor((options.height - count * dotSize) / 2);
260 |
261 | [
262 | [0, 0, 0],
263 | [1, 0, Math.PI / 2],
264 | [0, 1, -Math.PI / 2]
265 | ].forEach(([column, row, rotation]) => {
266 | const x = xBeginning + column * dotSize * (count - 7);
267 | const y = yBeginning + row * dotSize * (count - 7);
268 | let cornersSquareClipPath = this._dotsClipPath;
269 | let cornersDotClipPath = this._dotsClipPath;
270 |
271 | if (options.cornersSquareOptions?.gradient || options.cornersSquareOptions?.color) {
272 | cornersSquareClipPath = this._window.document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
273 | cornersSquareClipPath.setAttribute("id", `clip-path-corners-square-color-${column}-${row}`);
274 | this._defs.appendChild(cornersSquareClipPath);
275 | this._cornersSquareClipPath = this._cornersDotClipPath = cornersDotClipPath = cornersSquareClipPath;
276 |
277 | this._createColor({
278 | options: options.cornersSquareOptions?.gradient,
279 | color: options.cornersSquareOptions?.color,
280 | additionalRotation: rotation,
281 | x,
282 | y,
283 | height: cornersSquareSize,
284 | width: cornersSquareSize,
285 | name: `corners-square-color-${column}-${row}`
286 | });
287 | }
288 |
289 | if (options.cornersSquareOptions?.type) {
290 | const cornersSquare = new QRCornerSquare({
291 | svg: this._element,
292 | type: options.cornersSquareOptions.type,
293 | window: this._window
294 | });
295 |
296 | cornersSquare.draw(x, y, cornersSquareSize, rotation);
297 |
298 | if (cornersSquare._element && cornersSquareClipPath) {
299 | cornersSquareClipPath.appendChild(cornersSquare._element);
300 | }
301 | } else {
302 | const dot = new QRDot({
303 | svg: this._element,
304 | type: options.dotsOptions.type,
305 | window: this._window
306 | });
307 |
308 | for (let i = 0; i < squareMask.length; i++) {
309 | for (let j = 0; j < squareMask[i].length; j++) {
310 | if (!squareMask[i]?.[j]) {
311 | continue;
312 | }
313 |
314 | dot.draw(
315 | x + i * dotSize,
316 | y + j * dotSize,
317 | dotSize,
318 | (xOffset: number, yOffset: number): boolean => !!squareMask[i + xOffset]?.[j + yOffset]
319 | );
320 |
321 | if (dot._element && cornersSquareClipPath) {
322 | cornersSquareClipPath.appendChild(dot._element);
323 | }
324 | }
325 | }
326 | }
327 |
328 | if (options.cornersDotOptions?.gradient || options.cornersDotOptions?.color) {
329 | cornersDotClipPath = this._window.document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
330 | cornersDotClipPath.setAttribute("id", `clip-path-corners-dot-color-${column}-${row}`);
331 | this._defs.appendChild(cornersDotClipPath);
332 | this._cornersDotClipPath = cornersDotClipPath;
333 |
334 | this._createColor({
335 | options: options.cornersDotOptions?.gradient,
336 | color: options.cornersDotOptions?.color,
337 | additionalRotation: rotation,
338 | x: x + dotSize * 2,
339 | y: y + dotSize * 2,
340 | height: cornersDotSize,
341 | width: cornersDotSize,
342 | name: `corners-dot-color-${column}-${row}`
343 | });
344 | }
345 |
346 | if (options.cornersDotOptions?.type) {
347 | const cornersDot = new QRCornerDot({
348 | svg: this._element,
349 | type: options.cornersDotOptions.type,
350 | window: this._window
351 | });
352 |
353 | cornersDot.draw(x + dotSize * 2, y + dotSize * 2, cornersDotSize, rotation);
354 |
355 | if (cornersDot._element && cornersDotClipPath) {
356 | cornersDotClipPath.appendChild(cornersDot._element);
357 | }
358 | } else {
359 | const dot = new QRDot({
360 | svg: this._element,
361 | type: options.dotsOptions.type,
362 | window: this._window
363 | });
364 |
365 | for (let i = 0; i < dotMask.length; i++) {
366 | for (let j = 0; j < dotMask[i].length; j++) {
367 | if (!dotMask[i]?.[j]) {
368 | continue;
369 | }
370 |
371 | dot.draw(
372 | x + i * dotSize,
373 | y + j * dotSize,
374 | dotSize,
375 | (xOffset: number, yOffset: number): boolean => !!dotMask[i + xOffset]?.[j + yOffset]
376 | );
377 |
378 | if (dot._element && cornersDotClipPath) {
379 | cornersDotClipPath.appendChild(dot._element);
380 | }
381 | }
382 | }
383 | }
384 | });
385 | }
386 |
387 | imageToBlob(): void {
388 | if (!this._image) return;
389 | // fix blurry svg
390 | if (/(\.svg$)|(^data:image\/svg)/.test(this._options.image ?? "")) {
391 | this._image.width = this._options.width;
392 | this._image.height = this._options.height;
393 | }
394 | if (this._options.imageOptions.saveAsBlob && this._canvas) {
395 | const ctx = this._canvas.getContext("2d");
396 | if (ctx) {
397 | ctx.drawImage(this._image, 0, 0, this._canvas.width, this._canvas.height);
398 | this._imageUri = this._canvas.toDataURL("image/png");
399 | console.log(this._imageUri);
400 | }
401 | }
402 | }
403 |
404 | loadImage(): Promise {
405 | return new Promise((resolve, reject) => {
406 | const options = this._options;
407 |
408 | if (options.nodeCanvas?.loadImage && options.image) {
409 | options.nodeCanvas
410 | .loadImage(options.image)
411 | .then((image: HTMLImageElement) => {
412 | this._image = image;
413 | resolve();
414 | })
415 | .catch(reject);
416 | return;
417 | }
418 | const image = new this._window.Image();
419 |
420 | if (!options.image) {
421 | return reject("Image is not defined");
422 | }
423 |
424 | if (typeof options.imageOptions.crossOrigin === "string") {
425 | image.crossOrigin = options.imageOptions.crossOrigin;
426 | }
427 |
428 | this._image = image;
429 | image.onload = (): void => {
430 | resolve();
431 | };
432 | image.src = options.image;
433 | });
434 | }
435 |
436 | drawImage({
437 | width,
438 | height,
439 | count,
440 | dotSize
441 | }: {
442 | width: number;
443 | height: number;
444 | count: number;
445 | dotSize: number;
446 | }): void {
447 | const options = this._options;
448 | const xBeginning = Math.floor((options.width - count * dotSize) / 2);
449 | const yBeginning = Math.floor((options.height - count * dotSize) / 2);
450 | const dx = xBeginning + options.imageOptions.margin + (count * dotSize - width) / 2;
451 | const dy = yBeginning + options.imageOptions.margin + (count * dotSize - height) / 2;
452 | const dw = width - options.imageOptions.margin * 2;
453 | const dh = height - options.imageOptions.margin * 2;
454 |
455 | const image = this._window.document.createElementNS("http://www.w3.org/2000/svg", "image");
456 | image.setAttribute("href", this._imageUri || "");
457 | image.setAttribute("x", String(dx));
458 | image.setAttribute("y", String(dy));
459 | image.setAttribute("width", `${dw}px`);
460 | image.setAttribute("height", `${dh}px`);
461 |
462 | this._element.appendChild(image);
463 | }
464 |
465 | _createColor({
466 | options,
467 | color,
468 | additionalRotation,
469 | x,
470 | y,
471 | height,
472 | width,
473 | name
474 | }: {
475 | options?: Gradient;
476 | color?: string;
477 | additionalRotation: number;
478 | x: number;
479 | y: number;
480 | height: number;
481 | width: number;
482 | name: string;
483 | }): void {
484 | const size = width > height ? width : height;
485 | const rect = this._window.document.createElementNS("http://www.w3.org/2000/svg", "rect");
486 | rect.setAttribute("x", String(x));
487 | rect.setAttribute("y", String(y));
488 | rect.setAttribute("height", String(height));
489 | rect.setAttribute("width", String(width));
490 | rect.setAttribute("clip-path", `url('#clip-path-${name}')`);
491 |
492 | if (options) {
493 | let gradient: SVGElement;
494 | if (options.type === gradientTypes.radial) {
495 | gradient = this._window.document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
496 | gradient.setAttribute("id", name);
497 | gradient.setAttribute("gradientUnits", "userSpaceOnUse");
498 | gradient.setAttribute("fx", String(x + width / 2));
499 | gradient.setAttribute("fy", String(y + height / 2));
500 | gradient.setAttribute("cx", String(x + width / 2));
501 | gradient.setAttribute("cy", String(y + height / 2));
502 | gradient.setAttribute("r", String(size / 2));
503 | } else {
504 | const rotation = ((options.rotation || 0) + additionalRotation) % (2 * Math.PI);
505 | const positiveRotation = (rotation + 2 * Math.PI) % (2 * Math.PI);
506 | let x0 = x + width / 2;
507 | let y0 = y + height / 2;
508 | let x1 = x + width / 2;
509 | let y1 = y + height / 2;
510 |
511 | if (
512 | (positiveRotation >= 0 && positiveRotation <= 0.25 * Math.PI) ||
513 | (positiveRotation > 1.75 * Math.PI && positiveRotation <= 2 * Math.PI)
514 | ) {
515 | x0 = x0 - width / 2;
516 | y0 = y0 - (height / 2) * Math.tan(rotation);
517 | x1 = x1 + width / 2;
518 | y1 = y1 + (height / 2) * Math.tan(rotation);
519 | } else if (positiveRotation > 0.25 * Math.PI && positiveRotation <= 0.75 * Math.PI) {
520 | y0 = y0 - height / 2;
521 | x0 = x0 - width / 2 / Math.tan(rotation);
522 | y1 = y1 + height / 2;
523 | x1 = x1 + width / 2 / Math.tan(rotation);
524 | } else if (positiveRotation > 0.75 * Math.PI && positiveRotation <= 1.25 * Math.PI) {
525 | x0 = x0 + width / 2;
526 | y0 = y0 + (height / 2) * Math.tan(rotation);
527 | x1 = x1 - width / 2;
528 | y1 = y1 - (height / 2) * Math.tan(rotation);
529 | } else if (positiveRotation > 1.25 * Math.PI && positiveRotation <= 1.75 * Math.PI) {
530 | y0 = y0 + height / 2;
531 | x0 = x0 + width / 2 / Math.tan(rotation);
532 | y1 = y1 - height / 2;
533 | x1 = x1 - width / 2 / Math.tan(rotation);
534 | }
535 |
536 | gradient = this._window.document.createElementNS("http://www.w3.org/2000/svg", "linearGradient");
537 | gradient.setAttribute("id", name);
538 | gradient.setAttribute("gradientUnits", "userSpaceOnUse");
539 | gradient.setAttribute("x1", String(Math.round(x0)));
540 | gradient.setAttribute("y1", String(Math.round(y0)));
541 | gradient.setAttribute("x2", String(Math.round(x1)));
542 | gradient.setAttribute("y2", String(Math.round(y1)));
543 | }
544 |
545 | options.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => {
546 | const stop = this._window.document.createElementNS("http://www.w3.org/2000/svg", "stop");
547 | stop.setAttribute("offset", `${100 * offset}%`);
548 | stop.setAttribute("stop-color", color);
549 | gradient.appendChild(stop);
550 | });
551 |
552 | rect.setAttribute("fill", `url('#${name}')`);
553 | this._defs.appendChild(gradient);
554 | } else if (color) {
555 | rect.setAttribute("fill", color);
556 | }
557 |
558 | this._element.appendChild(rect);
559 | }
560 | }
561 |
--------------------------------------------------------------------------------
/src/figures/cornerDot/canvas/QRCornerDot.ts:
--------------------------------------------------------------------------------
1 | import cornerDotTypes from "../../../constants/cornerDotTypes";
2 | import { CornerDotType, RotateFigureArgsCanvas, BasicFigureDrawArgsCanvas, DrawArgsCanvas } from "../../../types";
3 |
4 | export default class QRCornerDot {
5 | _context: CanvasRenderingContext2D;
6 | _type: CornerDotType;
7 |
8 | constructor({ context, type }: { context: CanvasRenderingContext2D; type: CornerDotType }) {
9 | this._context = context;
10 | this._type = type;
11 | }
12 |
13 | draw(x: number, y: number, size: number, rotation: number): void {
14 | const context = this._context;
15 | const type = this._type;
16 | let drawFunction;
17 |
18 | switch (type) {
19 | case cornerDotTypes.square:
20 | drawFunction = this._drawSquare;
21 | break;
22 | case cornerDotTypes.dot:
23 | default:
24 | drawFunction = this._drawDot;
25 | }
26 |
27 | drawFunction.call(this, { x, y, size, context, rotation });
28 | }
29 |
30 | _rotateFigure({ x, y, size, context, rotation = 0, draw }: RotateFigureArgsCanvas): void {
31 | const cx = x + size / 2;
32 | const cy = y + size / 2;
33 |
34 | context.translate(cx, cy);
35 | rotation && context.rotate(rotation);
36 | draw();
37 | context.closePath();
38 | rotation && context.rotate(-rotation);
39 | context.translate(-cx, -cy);
40 | }
41 |
42 | _basicDot(args: BasicFigureDrawArgsCanvas): void {
43 | const { size, context } = args;
44 |
45 | this._rotateFigure({
46 | ...args,
47 | draw: () => {
48 | context.arc(0, 0, size / 2, 0, Math.PI * 2);
49 | }
50 | });
51 | }
52 |
53 | _basicSquare(args: BasicFigureDrawArgsCanvas): void {
54 | const { size, context } = args;
55 |
56 | this._rotateFigure({
57 | ...args,
58 | draw: () => {
59 | context.rect(-size / 2, -size / 2, size, size);
60 | }
61 | });
62 | }
63 |
64 | _drawDot({ x, y, size, context, rotation }: DrawArgsCanvas): void {
65 | this._basicDot({ x, y, size, context, rotation });
66 | }
67 |
68 | _drawSquare({ x, y, size, context, rotation }: DrawArgsCanvas): void {
69 | this._basicSquare({ x, y, size, context, rotation });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/figures/cornerDot/svg/QRCornerDot.ts:
--------------------------------------------------------------------------------
1 | import cornerDotTypes from "../../../constants/cornerDotTypes";
2 | import { CornerDotType, RotateFigureArgs, BasicFigureDrawArgs, DrawArgs, Window } from "../../../types";
3 |
4 | export default class QRCornerDot {
5 | _element?: SVGElement;
6 | _svg: SVGElement;
7 | _type: CornerDotType;
8 | _window: Window;
9 |
10 | constructor({ svg, type, window }: { svg: SVGElement; type: CornerDotType; window: Window }) {
11 | this._svg = svg;
12 | this._type = type;
13 | this._window = window;
14 | }
15 |
16 | draw(x: number, y: number, size: number, rotation: number): void {
17 | const type = this._type;
18 | let drawFunction;
19 |
20 | switch (type) {
21 | case cornerDotTypes.square:
22 | drawFunction = this._drawSquare;
23 | break;
24 | case cornerDotTypes.dot:
25 | default:
26 | drawFunction = this._drawDot;
27 | }
28 |
29 | drawFunction.call(this, { x, y, size, rotation });
30 | }
31 |
32 | _rotateFigure({ x, y, size, rotation = 0, draw }: RotateFigureArgs): void {
33 | const cx = x + size / 2;
34 | const cy = y + size / 2;
35 |
36 | draw();
37 | this._element?.setAttribute("transform", `rotate(${(180 * rotation) / Math.PI},${cx},${cy})`);
38 | }
39 |
40 | _basicDot(args: BasicFigureDrawArgs): void {
41 | const { size, x, y } = args;
42 |
43 | this._rotateFigure({
44 | ...args,
45 | draw: () => {
46 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "circle");
47 | this._element.setAttribute("cx", String(x + size / 2));
48 | this._element.setAttribute("cy", String(y + size / 2));
49 | this._element.setAttribute("r", String(size / 2));
50 | }
51 | });
52 | }
53 |
54 | _basicSquare(args: BasicFigureDrawArgs): void {
55 | const { size, x, y } = args;
56 |
57 | this._rotateFigure({
58 | ...args,
59 | draw: () => {
60 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "rect");
61 | this._element.setAttribute("x", String(x));
62 | this._element.setAttribute("y", String(y));
63 | this._element.setAttribute("width", String(size));
64 | this._element.setAttribute("height", String(size));
65 | }
66 | });
67 | }
68 |
69 | _drawDot({ x, y, size, rotation }: DrawArgs): void {
70 | this._basicDot({ x, y, size, rotation });
71 | }
72 |
73 | _drawSquare({ x, y, size, rotation }: DrawArgs): void {
74 | this._basicSquare({ x, y, size, rotation });
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/figures/cornerSquare/canvas/QRCornerSquare.ts:
--------------------------------------------------------------------------------
1 | import cornerSquareTypes from "../../../constants/cornerSquareTypes";
2 | import { CornerSquareType, RotateFigureArgsCanvas, BasicFigureDrawArgsCanvas, DrawArgsCanvas } from "../../../types";
3 |
4 | export default class QRCornerSquare {
5 | _context: CanvasRenderingContext2D;
6 | _type: CornerSquareType;
7 |
8 | constructor({ context, type }: { context: CanvasRenderingContext2D; type: CornerSquareType }) {
9 | this._context = context;
10 | this._type = type;
11 | }
12 |
13 | draw(x: number, y: number, size: number, rotation: number): void {
14 | const context = this._context;
15 | const type = this._type;
16 | let drawFunction;
17 |
18 | switch (type) {
19 | case cornerSquareTypes.square:
20 | drawFunction = this._drawSquare;
21 | break;
22 | case cornerSquareTypes.extraRounded:
23 | drawFunction = this._drawExtraRounded;
24 | break;
25 | case cornerSquareTypes.dot:
26 | default:
27 | drawFunction = this._drawDot;
28 | }
29 |
30 | drawFunction.call(this, { x, y, size, context, rotation });
31 | }
32 |
33 | _rotateFigure({ x, y, size, context, rotation = 0, draw }: RotateFigureArgsCanvas): void {
34 | const cx = x + size / 2;
35 | const cy = y + size / 2;
36 |
37 | context.translate(cx, cy);
38 | rotation && context.rotate(rotation);
39 | draw();
40 | context.closePath();
41 | rotation && context.rotate(-rotation);
42 | context.translate(-cx, -cy);
43 | }
44 |
45 | _basicDot(args: BasicFigureDrawArgsCanvas): void {
46 | const { size, context } = args;
47 | const dotSize = size / 7;
48 |
49 | this._rotateFigure({
50 | ...args,
51 | draw: () => {
52 | context.arc(0, 0, size / 2, 0, Math.PI * 2);
53 | context.arc(0, 0, size / 2 - dotSize, 0, Math.PI * 2);
54 | }
55 | });
56 | }
57 |
58 | _basicSquare(args: BasicFigureDrawArgsCanvas): void {
59 | const { size, context } = args;
60 | const dotSize = size / 7;
61 |
62 | this._rotateFigure({
63 | ...args,
64 | draw: () => {
65 | context.rect(-size / 2, -size / 2, size, size);
66 | context.rect(-size / 2 + dotSize, -size / 2 + dotSize, size - 2 * dotSize, size - 2 * dotSize);
67 | }
68 | });
69 | }
70 |
71 | _basicExtraRounded(args: BasicFigureDrawArgsCanvas): void {
72 | const { size, context } = args;
73 | const dotSize = size / 7;
74 |
75 | this._rotateFigure({
76 | ...args,
77 | draw: () => {
78 | context.arc(-dotSize, -dotSize, 2.5 * dotSize, Math.PI, -Math.PI / 2);
79 | context.lineTo(dotSize, -3.5 * dotSize);
80 | context.arc(dotSize, -dotSize, 2.5 * dotSize, -Math.PI / 2, 0);
81 | context.lineTo(3.5 * dotSize, -dotSize);
82 | context.arc(dotSize, dotSize, 2.5 * dotSize, 0, Math.PI / 2);
83 | context.lineTo(-dotSize, 3.5 * dotSize);
84 | context.arc(-dotSize, dotSize, 2.5 * dotSize, Math.PI / 2, Math.PI);
85 | context.lineTo(-3.5 * dotSize, -dotSize);
86 |
87 | context.arc(-dotSize, -dotSize, 1.5 * dotSize, Math.PI, -Math.PI / 2);
88 | context.lineTo(dotSize, -2.5 * dotSize);
89 | context.arc(dotSize, -dotSize, 1.5 * dotSize, -Math.PI / 2, 0);
90 | context.lineTo(2.5 * dotSize, -dotSize);
91 | context.arc(dotSize, dotSize, 1.5 * dotSize, 0, Math.PI / 2);
92 | context.lineTo(-dotSize, 2.5 * dotSize);
93 | context.arc(-dotSize, dotSize, 1.5 * dotSize, Math.PI / 2, Math.PI);
94 | context.lineTo(-2.5 * dotSize, -dotSize);
95 | }
96 | });
97 | }
98 |
99 | _drawDot({ x, y, size, context, rotation }: DrawArgsCanvas): void {
100 | this._basicDot({ x, y, size, context, rotation });
101 | }
102 |
103 | _drawSquare({ x, y, size, context, rotation }: DrawArgsCanvas): void {
104 | this._basicSquare({ x, y, size, context, rotation });
105 | }
106 |
107 | _drawExtraRounded({ x, y, size, context, rotation }: DrawArgsCanvas): void {
108 | this._basicExtraRounded({ x, y, size, context, rotation });
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/figures/cornerSquare/svg/QRCornerSquare.ts:
--------------------------------------------------------------------------------
1 | import cornerSquareTypes from "../../../constants/cornerSquareTypes";
2 | import { CornerSquareType, DrawArgs, BasicFigureDrawArgs, RotateFigureArgs, Window } from "../../../types";
3 |
4 | export default class QRCornerSquare {
5 | _element?: SVGElement;
6 | _svg: SVGElement;
7 | _type: CornerSquareType;
8 | _window: Window;
9 |
10 | constructor({ svg, type, window }: { svg: SVGElement; type: CornerSquareType; window: Window }) {
11 | this._svg = svg;
12 | this._type = type;
13 | this._window = window;
14 | }
15 |
16 | draw(x: number, y: number, size: number, rotation: number): void {
17 | const type = this._type;
18 | let drawFunction;
19 |
20 | switch (type) {
21 | case cornerSquareTypes.square:
22 | drawFunction = this._drawSquare;
23 | break;
24 | case cornerSquareTypes.extraRounded:
25 | drawFunction = this._drawExtraRounded;
26 | break;
27 | case cornerSquareTypes.dot:
28 | default:
29 | drawFunction = this._drawDot;
30 | }
31 |
32 | drawFunction.call(this, { x, y, size, rotation });
33 | }
34 |
35 | _rotateFigure({ x, y, size, rotation = 0, draw }: RotateFigureArgs): void {
36 | const cx = x + size / 2;
37 | const cy = y + size / 2;
38 |
39 | draw();
40 | this._element?.setAttribute("transform", `rotate(${(180 * rotation) / Math.PI},${cx},${cy})`);
41 | }
42 |
43 | _basicDot(args: BasicFigureDrawArgs): void {
44 | const { size, x, y } = args;
45 | const dotSize = size / 7;
46 |
47 | this._rotateFigure({
48 | ...args,
49 | draw: () => {
50 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
51 | this._element.setAttribute("clip-rule", "evenodd");
52 | this._element.setAttribute(
53 | "d",
54 | `M ${x + size / 2} ${y}` + // M cx, y // Move to top of ring
55 | `a ${size / 2} ${size / 2} 0 1 0 0.1 0` + // a outerRadius, outerRadius, 0, 1, 0, 1, 0 // Draw outer arc, but don't close it
56 | `z` + // Z // Close the outer shape
57 | `m 0 ${dotSize}` + // m -1 outerRadius-innerRadius // Move to top point of inner radius
58 | `a ${size / 2 - dotSize} ${size / 2 - dotSize} 0 1 1 -0.1 0` + // a innerRadius, innerRadius, 0, 1, 1, -1, 0 // Draw inner arc, but don't close it
59 | `Z` // Z // Close the inner ring. Actually will still work without, but inner ring will have one unit missing in stroke
60 | );
61 | }
62 | });
63 | }
64 |
65 | _basicSquare(args: BasicFigureDrawArgs): void {
66 | const { size, x, y } = args;
67 | const dotSize = size / 7;
68 |
69 | this._rotateFigure({
70 | ...args,
71 | draw: () => {
72 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
73 | this._element.setAttribute("clip-rule", "evenodd");
74 | this._element.setAttribute(
75 | "d",
76 | `M ${x} ${y}` +
77 | `v ${size}` +
78 | `h ${size}` +
79 | `v ${-size}` +
80 | `z` +
81 | `M ${x + dotSize} ${y + dotSize}` +
82 | `h ${size - 2 * dotSize}` +
83 | `v ${size - 2 * dotSize}` +
84 | `h ${-size + 2 * dotSize}` +
85 | `z`
86 | );
87 | }
88 | });
89 | }
90 |
91 | _basicExtraRounded(args: BasicFigureDrawArgs): void {
92 | const { size, x, y } = args;
93 | const dotSize = size / 7;
94 |
95 | this._rotateFigure({
96 | ...args,
97 | draw: () => {
98 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
99 | this._element.setAttribute("clip-rule", "evenodd");
100 | this._element.setAttribute(
101 | "d",
102 | `M ${x} ${y + 2.5 * dotSize}` +
103 | `v ${2 * dotSize}` +
104 | `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${dotSize * 2.5} ${dotSize * 2.5}` +
105 | `h ${2 * dotSize}` +
106 | `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${dotSize * 2.5} ${-dotSize * 2.5}` +
107 | `v ${-2 * dotSize}` +
108 | `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${-dotSize * 2.5} ${-dotSize * 2.5}` +
109 | `h ${-2 * dotSize}` +
110 | `a ${2.5 * dotSize} ${2.5 * dotSize}, 0, 0, 0, ${-dotSize * 2.5} ${dotSize * 2.5}` +
111 | `M ${x + 2.5 * dotSize} ${y + dotSize}` +
112 | `h ${2 * dotSize}` +
113 | `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${dotSize * 1.5} ${dotSize * 1.5}` +
114 | `v ${2 * dotSize}` +
115 | `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${-dotSize * 1.5} ${dotSize * 1.5}` +
116 | `h ${-2 * dotSize}` +
117 | `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${-dotSize * 1.5} ${-dotSize * 1.5}` +
118 | `v ${-2 * dotSize}` +
119 | `a ${1.5 * dotSize} ${1.5 * dotSize}, 0, 0, 1, ${dotSize * 1.5} ${-dotSize * 1.5}`
120 | );
121 | }
122 | });
123 | }
124 |
125 | _drawDot({ x, y, size, rotation }: DrawArgs): void {
126 | this._basicDot({ x, y, size, rotation });
127 | }
128 |
129 | _drawSquare({ x, y, size, rotation }: DrawArgs): void {
130 | this._basicSquare({ x, y, size, rotation });
131 | }
132 |
133 | _drawExtraRounded({ x, y, size, rotation }: DrawArgs): void {
134 | this._basicExtraRounded({ x, y, size, rotation });
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/figures/dot/canvas/QRDot.test.js:
--------------------------------------------------------------------------------
1 | import QRDot from "./QRDot";
2 | import fs from "fs";
3 | import path from "path";
4 |
5 | describe("Test QRDot class", () => {
6 | const canvasSize = 100;
7 | let canvas, canvasContext;
8 |
9 | beforeAll(() => {
10 | canvas = global.document.createElement("canvas");
11 | canvas.width = canvasSize;
12 | canvas.height = canvasSize;
13 | canvasContext = canvas.getContext("2d");
14 | });
15 |
16 | beforeEach(() => {
17 | canvasContext.fillStyle = "#fff";
18 | canvasContext.fillRect(0, 0, canvasSize, canvasSize);
19 | canvasContext.fillStyle = "#000";
20 | canvasContext.beginPath();
21 | });
22 |
23 | afterEach(() => {
24 | canvasContext.clearRect(0, 0, canvas.width, canvas.height);
25 | });
26 |
27 | it("Should draw simple square dot", () => {
28 | const dotSize = 50;
29 | const imgFile = fs.readFileSync(path.resolve(__dirname, "../../../assets/test/simple_square_dot.png"), "base64");
30 | const dot = new QRDot({ context: canvasContext, type: "square" });
31 | dot.draw(dotSize / 2, dotSize / 2, dotSize, () => false);
32 | canvasContext.fill("evenodd");
33 |
34 | expect(canvas.toDataURL()).toEqual(expect.stringContaining(imgFile));
35 | });
36 |
37 | it("Should draw simple dots", () => {
38 | const dotSize = 40;
39 | const imgFile = fs.readFileSync(path.resolve(__dirname, "../../../assets/test/simple_dots.png"), "base64");
40 | const dot = new QRDot({ context: canvasContext, type: "dots" });
41 | dot.draw(10, 30, dotSize, () => false);
42 | dot.draw(50, 30, dotSize, () => false);
43 | canvasContext.fill("evenodd");
44 |
45 | expect(canvas.toDataURL()).toEqual(expect.stringContaining(imgFile));
46 | });
47 | it("Should draw rounded dots", () => {
48 | const dotSize = 10;
49 | const matrix = [
50 | [1, 0, 1, 1, 0],
51 | [0, 0, 0, 0, 0],
52 | [1, 1, 0, 1, 1],
53 | [0, 1, 0, 1, 1],
54 | [0, 0, 1, 0, 0],
55 | ];
56 | const imgFile = fs.readFileSync(path.resolve(__dirname, "../../../assets/test/rounded_dots.png"), "base64");
57 | const dot = new QRDot({ context: canvasContext, type: "rounded" });
58 |
59 | for (let y = 0; y < matrix.length; y++) {
60 | for (let x = 0; x < matrix[y].length; x++) {
61 | if (!matrix[y][x]) {
62 | continue;
63 | }
64 | dot.draw(25 + x * dotSize, 25 + y * dotSize, dotSize, (xOffset, yOffset) => {
65 | if (matrix[y + yOffset]) {
66 | return !!matrix[y + yOffset][x + xOffset];
67 | } else {
68 | return false;
69 | }
70 | });
71 | }
72 | }
73 | canvasContext.fill("evenodd");
74 |
75 | expect(canvas.toDataURL()).toEqual(expect.stringContaining(imgFile));
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/figures/dot/canvas/QRDot.ts:
--------------------------------------------------------------------------------
1 | import dotTypes from "../../../constants/dotTypes";
2 | import {
3 | DotType,
4 | GetNeighbor,
5 | RotateFigureArgsCanvas,
6 | BasicFigureDrawArgsCanvas,
7 | DrawArgsCanvas
8 | } from "../../../types";
9 |
10 | export default class QRDot {
11 | _context: CanvasRenderingContext2D;
12 | _type: DotType;
13 |
14 | constructor({ context, type }: { context: CanvasRenderingContext2D; type: DotType }) {
15 | this._context = context;
16 | this._type = type;
17 | }
18 |
19 | draw(x: number, y: number, size: number, getNeighbor: GetNeighbor): void {
20 | const context = this._context;
21 | const type = this._type;
22 | let drawFunction;
23 |
24 | switch (type) {
25 | case dotTypes.dots:
26 | drawFunction = this._drawDot;
27 | break;
28 | case dotTypes.classy:
29 | drawFunction = this._drawClassy;
30 | break;
31 | case dotTypes.classyRounded:
32 | drawFunction = this._drawClassyRounded;
33 | break;
34 | case dotTypes.rounded:
35 | drawFunction = this._drawRounded;
36 | break;
37 | case dotTypes.extraRounded:
38 | drawFunction = this._drawExtraRounded;
39 | break;
40 | case dotTypes.square:
41 | default:
42 | drawFunction = this._drawSquare;
43 | }
44 |
45 | drawFunction.call(this, { x, y, size, context, getNeighbor });
46 | }
47 |
48 | _rotateFigure({ x, y, size, context, rotation = 0, draw }: RotateFigureArgsCanvas): void {
49 | const cx = x + size / 2;
50 | const cy = y + size / 2;
51 |
52 | context.translate(cx, cy);
53 | rotation && context.rotate(rotation);
54 | draw();
55 | context.closePath();
56 | rotation && context.rotate(-rotation);
57 | context.translate(-cx, -cy);
58 | }
59 |
60 | _basicDot(args: BasicFigureDrawArgsCanvas): void {
61 | const { size, context } = args;
62 |
63 | this._rotateFigure({
64 | ...args,
65 | draw: () => {
66 | context.arc(0, 0, size / 2, 0, Math.PI * 2);
67 | }
68 | });
69 | }
70 |
71 | _basicSquare(args: BasicFigureDrawArgsCanvas): void {
72 | const { size, context } = args;
73 |
74 | this._rotateFigure({
75 | ...args,
76 | draw: () => {
77 | context.rect(-size / 2, -size / 2, size, size);
78 | }
79 | });
80 | }
81 |
82 | //if rotation === 0 - right side is rounded
83 | _basicSideRounded(args: BasicFigureDrawArgsCanvas): void {
84 | const { size, context } = args;
85 |
86 | this._rotateFigure({
87 | ...args,
88 | draw: () => {
89 | context.arc(0, 0, size / 2, -Math.PI / 2, Math.PI / 2);
90 | context.lineTo(-size / 2, size / 2);
91 | context.lineTo(-size / 2, -size / 2);
92 | context.lineTo(0, -size / 2);
93 | }
94 | });
95 | }
96 |
97 | //if rotation === 0 - top right corner is rounded
98 | _basicCornerRounded(args: BasicFigureDrawArgsCanvas): void {
99 | const { size, context } = args;
100 |
101 | this._rotateFigure({
102 | ...args,
103 | draw: () => {
104 | context.arc(0, 0, size / 2, -Math.PI / 2, 0);
105 | context.lineTo(size / 2, size / 2);
106 | context.lineTo(-size / 2, size / 2);
107 | context.lineTo(-size / 2, -size / 2);
108 | context.lineTo(0, -size / 2);
109 | }
110 | });
111 | }
112 |
113 | //if rotation === 0 - top right corner is rounded
114 | _basicCornerExtraRounded(args: BasicFigureDrawArgsCanvas): void {
115 | const { size, context } = args;
116 |
117 | this._rotateFigure({
118 | ...args,
119 | draw: () => {
120 | context.arc(-size / 2, size / 2, size, -Math.PI / 2, 0);
121 | context.lineTo(-size / 2, size / 2);
122 | context.lineTo(-size / 2, -size / 2);
123 | }
124 | });
125 | }
126 |
127 | _basicCornersRounded(args: BasicFigureDrawArgsCanvas): void {
128 | const { size, context } = args;
129 |
130 | this._rotateFigure({
131 | ...args,
132 | draw: () => {
133 | context.arc(0, 0, size / 2, -Math.PI / 2, 0);
134 | context.lineTo(size / 2, size / 2);
135 | context.lineTo(0, size / 2);
136 | context.arc(0, 0, size / 2, Math.PI / 2, Math.PI);
137 | context.lineTo(-size / 2, -size / 2);
138 | context.lineTo(0, -size / 2);
139 | }
140 | });
141 | }
142 |
143 | _basicCornersExtraRounded(args: BasicFigureDrawArgsCanvas): void {
144 | const { size, context } = args;
145 |
146 | this._rotateFigure({
147 | ...args,
148 | draw: () => {
149 | context.arc(-size / 2, size / 2, size, -Math.PI / 2, 0);
150 | context.arc(size / 2, -size / 2, size, Math.PI / 2, Math.PI);
151 | }
152 | });
153 | }
154 |
155 | _drawDot({ x, y, size, context }: DrawArgsCanvas): void {
156 | this._basicDot({ x, y, size, context, rotation: 0 });
157 | }
158 |
159 | _drawSquare({ x, y, size, context }: DrawArgsCanvas): void {
160 | this._basicSquare({ x, y, size, context, rotation: 0 });
161 | }
162 |
163 | _drawRounded({ x, y, size, context, getNeighbor }: DrawArgsCanvas): void {
164 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
165 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
166 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
167 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
168 |
169 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
170 |
171 | if (neighborsCount === 0) {
172 | this._basicDot({ x, y, size, context, rotation: 0 });
173 | return;
174 | }
175 |
176 | if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) {
177 | this._basicSquare({ x, y, size, context, rotation: 0 });
178 | return;
179 | }
180 |
181 | if (neighborsCount === 2) {
182 | let rotation = 0;
183 |
184 | if (leftNeighbor && topNeighbor) {
185 | rotation = Math.PI / 2;
186 | } else if (topNeighbor && rightNeighbor) {
187 | rotation = Math.PI;
188 | } else if (rightNeighbor && bottomNeighbor) {
189 | rotation = -Math.PI / 2;
190 | }
191 |
192 | this._basicCornerRounded({ x, y, size, context, rotation });
193 | return;
194 | }
195 |
196 | if (neighborsCount === 1) {
197 | let rotation = 0;
198 |
199 | if (topNeighbor) {
200 | rotation = Math.PI / 2;
201 | } else if (rightNeighbor) {
202 | rotation = Math.PI;
203 | } else if (bottomNeighbor) {
204 | rotation = -Math.PI / 2;
205 | }
206 |
207 | this._basicSideRounded({ x, y, size, context, rotation });
208 | return;
209 | }
210 | }
211 |
212 | _drawExtraRounded({ x, y, size, context, getNeighbor }: DrawArgsCanvas): void {
213 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
214 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
215 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
216 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
217 |
218 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
219 |
220 | if (neighborsCount === 0) {
221 | this._basicDot({ x, y, size, context, rotation: 0 });
222 | return;
223 | }
224 |
225 | if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) {
226 | this._basicSquare({ x, y, size, context, rotation: 0 });
227 | return;
228 | }
229 |
230 | if (neighborsCount === 2) {
231 | let rotation = 0;
232 |
233 | if (leftNeighbor && topNeighbor) {
234 | rotation = Math.PI / 2;
235 | } else if (topNeighbor && rightNeighbor) {
236 | rotation = Math.PI;
237 | } else if (rightNeighbor && bottomNeighbor) {
238 | rotation = -Math.PI / 2;
239 | }
240 |
241 | this._basicCornerExtraRounded({ x, y, size, context, rotation });
242 | return;
243 | }
244 |
245 | if (neighborsCount === 1) {
246 | let rotation = 0;
247 |
248 | if (topNeighbor) {
249 | rotation = Math.PI / 2;
250 | } else if (rightNeighbor) {
251 | rotation = Math.PI;
252 | } else if (bottomNeighbor) {
253 | rotation = -Math.PI / 2;
254 | }
255 |
256 | this._basicSideRounded({ x, y, size, context, rotation });
257 | return;
258 | }
259 | }
260 |
261 | _drawClassy({ x, y, size, context, getNeighbor }: DrawArgsCanvas): void {
262 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
263 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
264 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
265 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
266 |
267 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
268 |
269 | if (neighborsCount === 0) {
270 | this._basicCornersRounded({ x, y, size, context, rotation: Math.PI / 2 });
271 | return;
272 | }
273 |
274 | if (!leftNeighbor && !topNeighbor) {
275 | this._basicCornerRounded({ x, y, size, context, rotation: -Math.PI / 2 });
276 | return;
277 | }
278 |
279 | if (!rightNeighbor && !bottomNeighbor) {
280 | this._basicCornerRounded({ x, y, size, context, rotation: Math.PI / 2 });
281 | return;
282 | }
283 |
284 | this._basicSquare({ x, y, size, context, rotation: 0 });
285 | }
286 |
287 | _drawClassyRounded({ x, y, size, context, getNeighbor }: DrawArgsCanvas): void {
288 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
289 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
290 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
291 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
292 |
293 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
294 |
295 | if (neighborsCount === 0) {
296 | this._basicCornersRounded({ x, y, size, context, rotation: Math.PI / 2 });
297 | return;
298 | }
299 |
300 | if (!leftNeighbor && !topNeighbor) {
301 | this._basicCornerExtraRounded({ x, y, size, context, rotation: -Math.PI / 2 });
302 | return;
303 | }
304 |
305 | if (!rightNeighbor && !bottomNeighbor) {
306 | this._basicCornerExtraRounded({ x, y, size, context, rotation: Math.PI / 2 });
307 | return;
308 | }
309 |
310 | this._basicSquare({ x, y, size, context, rotation: 0 });
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/src/figures/dot/svg/QRDot.ts:
--------------------------------------------------------------------------------
1 | import dotTypes from "../../../constants/dotTypes";
2 | import { DotType, GetNeighbor, DrawArgs, BasicFigureDrawArgs, RotateFigureArgs, Window } from "../../../types";
3 |
4 | export default class QRDot {
5 | _element?: SVGElement;
6 | _svg: SVGElement;
7 | _type: DotType;
8 | _window: Window;
9 |
10 | constructor({ svg, type, window }: { svg: SVGElement; type: DotType; window: Window }) {
11 | this._svg = svg;
12 | this._type = type;
13 | this._window = window;
14 | }
15 |
16 | draw(x: number, y: number, size: number, getNeighbor: GetNeighbor): void {
17 | const type = this._type;
18 | let drawFunction;
19 |
20 | switch (type) {
21 | case dotTypes.dots:
22 | drawFunction = this._drawDot;
23 | break;
24 | case dotTypes.classy:
25 | drawFunction = this._drawClassy;
26 | break;
27 | case dotTypes.classyRounded:
28 | drawFunction = this._drawClassyRounded;
29 | break;
30 | case dotTypes.rounded:
31 | drawFunction = this._drawRounded;
32 | break;
33 | case dotTypes.extraRounded:
34 | drawFunction = this._drawExtraRounded;
35 | break;
36 | case dotTypes.square:
37 | default:
38 | drawFunction = this._drawSquare;
39 | }
40 |
41 | drawFunction.call(this, { x, y, size, getNeighbor });
42 | }
43 |
44 | _rotateFigure({ x, y, size, rotation = 0, draw }: RotateFigureArgs): void {
45 | const cx = x + size / 2;
46 | const cy = y + size / 2;
47 |
48 | draw();
49 | this._element?.setAttribute("transform", `rotate(${(180 * rotation) / Math.PI},${cx},${cy})`);
50 | }
51 |
52 | _basicDot(args: BasicFigureDrawArgs): void {
53 | const { size, x, y } = args;
54 |
55 | this._rotateFigure({
56 | ...args,
57 | draw: () => {
58 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "circle");
59 | this._element.setAttribute("cx", String(x + size / 2));
60 | this._element.setAttribute("cy", String(y + size / 2));
61 | this._element.setAttribute("r", String(size / 2));
62 | }
63 | });
64 | }
65 |
66 | _basicSquare(args: BasicFigureDrawArgs): void {
67 | const { size, x, y } = args;
68 |
69 | this._rotateFigure({
70 | ...args,
71 | draw: () => {
72 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "rect");
73 | this._element.setAttribute("x", String(x));
74 | this._element.setAttribute("y", String(y));
75 | this._element.setAttribute("width", String(size));
76 | this._element.setAttribute("height", String(size));
77 | }
78 | });
79 | }
80 |
81 | //if rotation === 0 - right side is rounded
82 | _basicSideRounded(args: BasicFigureDrawArgs): void {
83 | const { size, x, y } = args;
84 |
85 | this._rotateFigure({
86 | ...args,
87 | draw: () => {
88 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
89 | this._element.setAttribute(
90 | "d",
91 | `M ${x} ${y}` + //go to top left position
92 | `v ${size}` + //draw line to left bottom corner
93 | `h ${size / 2}` + //draw line to left bottom corner + half of size right
94 | `a ${size / 2} ${size / 2}, 0, 0, 0, 0 ${-size}` // draw rounded corner
95 | );
96 | }
97 | });
98 | }
99 |
100 | //if rotation === 0 - top right corner is rounded
101 | _basicCornerRounded(args: BasicFigureDrawArgs): void {
102 | const { size, x, y } = args;
103 |
104 | this._rotateFigure({
105 | ...args,
106 | draw: () => {
107 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
108 | this._element.setAttribute(
109 | "d",
110 | `M ${x} ${y}` + //go to top left position
111 | `v ${size}` + //draw line to left bottom corner
112 | `h ${size}` + //draw line to right bottom corner
113 | `v ${-size / 2}` + //draw line to right bottom corner + half of size top
114 | `a ${size / 2} ${size / 2}, 0, 0, 0, ${-size / 2} ${-size / 2}` // draw rounded corner
115 | );
116 | }
117 | });
118 | }
119 |
120 | //if rotation === 0 - top right corner is rounded
121 | _basicCornerExtraRounded(args: BasicFigureDrawArgs): void {
122 | const { size, x, y } = args;
123 |
124 | this._rotateFigure({
125 | ...args,
126 | draw: () => {
127 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
128 | this._element.setAttribute(
129 | "d",
130 | `M ${x} ${y}` + //go to top left position
131 | `v ${size}` + //draw line to left bottom corner
132 | `h ${size}` + //draw line to right bottom corner
133 | `a ${size} ${size}, 0, 0, 0, ${-size} ${-size}` // draw rounded top right corner
134 | );
135 | }
136 | });
137 | }
138 |
139 | //if rotation === 0 - left bottom and right top corners are rounded
140 | _basicCornersRounded(args: BasicFigureDrawArgs): void {
141 | const { size, x, y } = args;
142 |
143 | this._rotateFigure({
144 | ...args,
145 | draw: () => {
146 | this._element = this._window.document.createElementNS("http://www.w3.org/2000/svg", "path");
147 | this._element.setAttribute(
148 | "d",
149 | `M ${x} ${y}` + //go to left top position
150 | `v ${size / 2}` + //draw line to left top corner + half of size bottom
151 | `a ${size / 2} ${size / 2}, 0, 0, 0, ${size / 2} ${size / 2}` + // draw rounded left bottom corner
152 | `h ${size / 2}` + //draw line to right bottom corner
153 | `v ${-size / 2}` + //draw line to right bottom corner + half of size top
154 | `a ${size / 2} ${size / 2}, 0, 0, 0, ${-size / 2} ${-size / 2}` // draw rounded right top corner
155 | );
156 | }
157 | });
158 | }
159 |
160 | _drawDot({ x, y, size }: DrawArgs): void {
161 | this._basicDot({ x, y, size, rotation: 0 });
162 | }
163 |
164 | _drawSquare({ x, y, size }: DrawArgs): void {
165 | this._basicSquare({ x, y, size, rotation: 0 });
166 | }
167 |
168 | _drawRounded({ x, y, size, getNeighbor }: DrawArgs): void {
169 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
170 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
171 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
172 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
173 |
174 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
175 |
176 | if (neighborsCount === 0) {
177 | this._basicDot({ x, y, size, rotation: 0 });
178 | return;
179 | }
180 |
181 | if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) {
182 | this._basicSquare({ x, y, size, rotation: 0 });
183 | return;
184 | }
185 |
186 | if (neighborsCount === 2) {
187 | let rotation = 0;
188 |
189 | if (leftNeighbor && topNeighbor) {
190 | rotation = Math.PI / 2;
191 | } else if (topNeighbor && rightNeighbor) {
192 | rotation = Math.PI;
193 | } else if (rightNeighbor && bottomNeighbor) {
194 | rotation = -Math.PI / 2;
195 | }
196 |
197 | this._basicCornerRounded({ x, y, size, rotation });
198 | return;
199 | }
200 |
201 | if (neighborsCount === 1) {
202 | let rotation = 0;
203 |
204 | if (topNeighbor) {
205 | rotation = Math.PI / 2;
206 | } else if (rightNeighbor) {
207 | rotation = Math.PI;
208 | } else if (bottomNeighbor) {
209 | rotation = -Math.PI / 2;
210 | }
211 |
212 | this._basicSideRounded({ x, y, size, rotation });
213 | return;
214 | }
215 | }
216 |
217 | _drawExtraRounded({ x, y, size, getNeighbor }: DrawArgs): void {
218 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
219 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
220 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
221 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
222 |
223 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
224 |
225 | if (neighborsCount === 0) {
226 | this._basicDot({ x, y, size, rotation: 0 });
227 | return;
228 | }
229 |
230 | if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) {
231 | this._basicSquare({ x, y, size, rotation: 0 });
232 | return;
233 | }
234 |
235 | if (neighborsCount === 2) {
236 | let rotation = 0;
237 |
238 | if (leftNeighbor && topNeighbor) {
239 | rotation = Math.PI / 2;
240 | } else if (topNeighbor && rightNeighbor) {
241 | rotation = Math.PI;
242 | } else if (rightNeighbor && bottomNeighbor) {
243 | rotation = -Math.PI / 2;
244 | }
245 |
246 | this._basicCornerExtraRounded({ x, y, size, rotation });
247 | return;
248 | }
249 |
250 | if (neighborsCount === 1) {
251 | let rotation = 0;
252 |
253 | if (topNeighbor) {
254 | rotation = Math.PI / 2;
255 | } else if (rightNeighbor) {
256 | rotation = Math.PI;
257 | } else if (bottomNeighbor) {
258 | rotation = -Math.PI / 2;
259 | }
260 |
261 | this._basicSideRounded({ x, y, size, rotation });
262 | return;
263 | }
264 | }
265 |
266 | _drawClassy({ x, y, size, getNeighbor }: DrawArgs): void {
267 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
268 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
269 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
270 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
271 |
272 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
273 |
274 | if (neighborsCount === 0) {
275 | this._basicCornersRounded({ x, y, size, rotation: Math.PI / 2 });
276 | return;
277 | }
278 |
279 | if (!leftNeighbor && !topNeighbor) {
280 | this._basicCornerRounded({ x, y, size, rotation: -Math.PI / 2 });
281 | return;
282 | }
283 |
284 | if (!rightNeighbor && !bottomNeighbor) {
285 | this._basicCornerRounded({ x, y, size, rotation: Math.PI / 2 });
286 | return;
287 | }
288 |
289 | this._basicSquare({ x, y, size, rotation: 0 });
290 | }
291 |
292 | _drawClassyRounded({ x, y, size, getNeighbor }: DrawArgs): void {
293 | const leftNeighbor = getNeighbor ? +getNeighbor(-1, 0) : 0;
294 | const rightNeighbor = getNeighbor ? +getNeighbor(1, 0) : 0;
295 | const topNeighbor = getNeighbor ? +getNeighbor(0, -1) : 0;
296 | const bottomNeighbor = getNeighbor ? +getNeighbor(0, 1) : 0;
297 |
298 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor;
299 |
300 | if (neighborsCount === 0) {
301 | this._basicCornersRounded({ x, y, size, rotation: Math.PI / 2 });
302 | return;
303 | }
304 |
305 | if (!leftNeighbor && !topNeighbor) {
306 | this._basicCornerExtraRounded({ x, y, size, rotation: -Math.PI / 2 });
307 | return;
308 | }
309 |
310 | if (!rightNeighbor && !bottomNeighbor) {
311 | this._basicCornerExtraRounded({ x, y, size, rotation: Math.PI / 2 });
312 | return;
313 | }
314 |
315 | this._basicSquare({ x, y, size, rotation: 0 });
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QR Code Styling
6 |
7 |
8 |
9 |
10 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import * as index from "./index";
2 |
3 | describe("Index", () => {
4 | it.each(["dotTypes", "errorCorrectionLevels", "errorCorrectionPercents", "modes", "qrTypes", "default"])(
5 | "The module should export certain submodules",
6 | (moduleName) => {
7 | expect(Object.keys(index)).toContain(moduleName);
8 | }
9 | );
10 | });
11 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import QRCodeStyling from "./core/QRCodeStyling";
2 | import dotTypes from "./constants/dotTypes";
3 | import cornerDotTypes from "./constants/cornerDotTypes";
4 | import cornerSquareTypes from "./constants/cornerSquareTypes";
5 | import errorCorrectionLevels from "./constants/errorCorrectionLevels";
6 | import errorCorrectionPercents from "./constants/errorCorrectionPercents";
7 | import modes from "./constants/modes";
8 | import qrTypes from "./constants/qrTypes";
9 | import drawTypes from "./constants/drawTypes";
10 |
11 | export * from "./types";
12 |
13 | export {
14 | dotTypes,
15 | cornerDotTypes,
16 | cornerSquareTypes,
17 | errorCorrectionLevels,
18 | errorCorrectionPercents,
19 | modes,
20 | qrTypes,
21 | drawTypes
22 | };
23 |
24 | export default QRCodeStyling;
25 |
--------------------------------------------------------------------------------
/src/tools/calculateImageSize.test.js:
--------------------------------------------------------------------------------
1 | import calculateImageSize from "./calculateImageSize";
2 |
3 | describe("Test calculateImageSizeForAxis function", () => {
4 | it("The function should return an correct result for 0 sizes", () => {
5 | expect(
6 | calculateImageSize({
7 | originalHeight: 0,
8 | originalWidth: 0,
9 | maxHiddenDots: 0,
10 | dotSize: 0
11 | })
12 | ).toEqual({
13 | height: 0,
14 | width: 0,
15 | hideYDots: 0,
16 | hideXDots: 0
17 | });
18 | });
19 | it("The function should return an correct result for minus values", () => {
20 | expect(
21 | calculateImageSize({
22 | originalHeight: -1,
23 | originalWidth: 5,
24 | maxHiddenDots: 11,
25 | dotSize: -5
26 | })
27 | ).toEqual({
28 | height: 0,
29 | width: 0,
30 | hideYDots: 0,
31 | hideXDots: 0
32 | });
33 | });
34 | it("The function should return an correct result for small images", () => {
35 | expect(
36 | calculateImageSize({
37 | originalHeight: 20,
38 | originalWidth: 10,
39 | maxHiddenDots: 1,
40 | dotSize: 10
41 | })
42 | ).toEqual({
43 | height: 10,
44 | width: 5,
45 | hideYDots: 1,
46 | hideXDots: 1
47 | });
48 | });
49 | it("The function should return an correct result for small images, if height is smaller than width", () => {
50 | expect(
51 | calculateImageSize({
52 | originalHeight: 10,
53 | originalWidth: 20,
54 | maxHiddenDots: 1,
55 | dotSize: 10
56 | })
57 | ).toEqual({
58 | height: 5,
59 | width: 10,
60 | hideYDots: 1,
61 | hideXDots: 1
62 | });
63 | });
64 | it("The function should return an correct result for large images", () => {
65 | expect(
66 | calculateImageSize({
67 | originalHeight: 1000,
68 | originalWidth: 2020,
69 | maxHiddenDots: 50,
70 | dotSize: 10
71 | })
72 | ).toEqual({
73 | height: 45,
74 | width: 90,
75 | hideYDots: 5,
76 | hideXDots: 9
77 | });
78 | });
79 | it("Use the maxHiddenAxisDots value for x", () => {
80 | expect(
81 | calculateImageSize({
82 | originalHeight: 1000,
83 | originalWidth: 2020,
84 | maxHiddenDots: 50,
85 | dotSize: 10,
86 | maxHiddenAxisDots: 1
87 | })
88 | ).toEqual({
89 | height: 5,
90 | width: 10,
91 | hideYDots: 1,
92 | hideXDots: 1
93 | });
94 | });
95 | it("Use the maxHiddenAxisDots value for y", () => {
96 | expect(
97 | calculateImageSize({
98 | originalHeight: 2020,
99 | originalWidth: 1000,
100 | maxHiddenDots: 50,
101 | dotSize: 10,
102 | maxHiddenAxisDots: 1
103 | })
104 | ).toEqual({
105 | height: 10,
106 | width: 5,
107 | hideYDots: 1,
108 | hideXDots: 1
109 | });
110 | });
111 | it("Use the maxHiddenAxisDots value for y with even value", () => {
112 | expect(
113 | calculateImageSize({
114 | originalHeight: 2020,
115 | originalWidth: 1000,
116 | maxHiddenDots: 50,
117 | dotSize: 10,
118 | maxHiddenAxisDots: 2
119 | })
120 | ).toEqual({
121 | height: 20,
122 | width: 10,
123 | hideYDots: 2,
124 | hideXDots: 1
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/src/tools/calculateImageSize.ts:
--------------------------------------------------------------------------------
1 | interface ImageSizeOptions {
2 | originalHeight: number;
3 | originalWidth: number;
4 | maxHiddenDots: number;
5 | maxHiddenAxisDots?: number;
6 | dotSize: number;
7 | }
8 |
9 | interface ImageSizeResult {
10 | height: number;
11 | width: number;
12 | hideYDots: number;
13 | hideXDots: number;
14 | }
15 |
16 | export default function calculateImageSize({
17 | originalHeight,
18 | originalWidth,
19 | maxHiddenDots,
20 | maxHiddenAxisDots,
21 | dotSize
22 | }: ImageSizeOptions): ImageSizeResult {
23 | const hideDots = { x: 0, y: 0 };
24 | const imageSize = { x: 0, y: 0 };
25 |
26 | if (originalHeight <= 0 || originalWidth <= 0 || maxHiddenDots <= 0 || dotSize <= 0) {
27 | return {
28 | height: 0,
29 | width: 0,
30 | hideYDots: 0,
31 | hideXDots: 0
32 | };
33 | }
34 |
35 | const k = originalHeight / originalWidth;
36 |
37 | //Getting the maximum possible axis hidden dots
38 | hideDots.x = Math.floor(Math.sqrt(maxHiddenDots / k));
39 | //The count of hidden dot's can't be less than 1
40 | if (hideDots.x <= 0) hideDots.x = 1;
41 | //Check the limit of the maximum allowed axis hidden dots
42 | if (maxHiddenAxisDots && maxHiddenAxisDots < hideDots.x) hideDots.x = maxHiddenAxisDots;
43 | //The count of dots should be odd
44 | if (hideDots.x % 2 === 0) hideDots.x--;
45 | imageSize.x = hideDots.x * dotSize;
46 | //Calculate opposite axis hidden dots based on axis value.
47 | //The value will be odd.
48 | //We use ceil to prevent dots covering by the image.
49 | hideDots.y = 1 + 2 * Math.ceil((hideDots.x * k - 1) / 2);
50 | imageSize.y = Math.round(imageSize.x * k);
51 | //If the result dots count is bigger than max - then decrease size and calculate again
52 | if (hideDots.y * hideDots.x > maxHiddenDots || (maxHiddenAxisDots && maxHiddenAxisDots < hideDots.y)) {
53 | if (maxHiddenAxisDots && maxHiddenAxisDots < hideDots.y) {
54 | hideDots.y = maxHiddenAxisDots;
55 | if (hideDots.y % 2 === 0) hideDots.x--;
56 | } else {
57 | hideDots.y -= 2;
58 | }
59 | imageSize.y = hideDots.y * dotSize;
60 | hideDots.x = 1 + 2 * Math.ceil((hideDots.y / k - 1) / 2);
61 | imageSize.x = Math.round(imageSize.y / k);
62 | }
63 |
64 | return {
65 | height: imageSize.y,
66 | width: imageSize.x,
67 | hideYDots: hideDots.y,
68 | hideXDots: hideDots.x
69 | };
70 | }
71 |
--------------------------------------------------------------------------------
/src/tools/downloadURI.ts:
--------------------------------------------------------------------------------
1 | export default function downloadURI(uri: string, name: string): void {
2 | const link = document.createElement("a");
3 | link.download = name;
4 | link.href = uri;
5 | document.body.appendChild(link);
6 | link.click();
7 | document.body.removeChild(link);
8 | }
9 |
--------------------------------------------------------------------------------
/src/tools/getMode.test.js:
--------------------------------------------------------------------------------
1 | import getMode from "./getMode";
2 | import modes from "../constants/modes";
3 |
4 | describe("Test getMode function", () => {
5 | it("Return numeric mode if numbers is passed", () => {
6 | expect(getMode(123)).toBe(modes.numeric);
7 | });
8 | it("Return numeric mode if a string with numbers is passed", () => {
9 | expect(getMode("123")).toBe(modes.numeric);
10 | });
11 | it("Return alphanumeric mode if a string with particular symbols is passed", () => {
12 | expect(getMode("01ABCZ$%*+-./:01ABCZ$%*+-./:")).toBe(modes.alphanumeric);
13 | });
14 | it("Return byte mode if a string with all keyboard symbols is passed", () => {
15 | expect(getMode("01ABCZ./:!@#$%^&*()_+01ABCZ./:!@#$%^&*()_'+|\\")).toBe(modes.byte);
16 | });
17 | it("Return byte mode if a string with Cyrillic symbols is passed", () => {
18 | expect(getMode("абвАБВ")).toBe(modes.byte);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/tools/getMode.ts:
--------------------------------------------------------------------------------
1 | import modes from "../constants/modes";
2 | import { Mode } from "../types";
3 |
4 | export default function getMode(data: string): Mode {
5 | switch (true) {
6 | case /^[0-9]*$/.test(data):
7 | return modes.numeric;
8 | case /^[0-9A-Z $%*+\-./:]*$/.test(data):
9 | return modes.alphanumeric;
10 | default:
11 | return modes.byte;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/tools/merge.test.js:
--------------------------------------------------------------------------------
1 | import mergeDeep from "./merge";
2 |
3 | describe("Test getMode function", () => {
4 | const simpleObject = {
5 | str: "foo"
6 | };
7 |
8 | const objectWithArray = {
9 | arr: [1, 2]
10 | };
11 |
12 | const nestedObject = {
13 | obj: {
14 | foo: "foo"
15 | }
16 | };
17 |
18 | const nestedObjectWithArray = {
19 | obj: {
20 | arr: [1, 2]
21 | }
22 | };
23 |
24 | it("Merge two objects", () => {
25 | expect(mergeDeep(simpleObject, { str: "bar" })).toEqual({ str: "bar" });
26 | });
27 | it("Merge two objects with arrays", () => {
28 | expect(mergeDeep(objectWithArray, { arr: [3, 4] })).toEqual({ arr: [3, 4] });
29 | });
30 | it("Merge two objects with nested objects", () => {
31 | expect(mergeDeep(nestedObject, { obj: { bar: "bar" } })).toEqual({ obj: { foo: "foo", bar: "bar" } });
32 | });
33 | it("Merge three objects with nested objects", () => {
34 | expect(mergeDeep(nestedObjectWithArray, nestedObject, { obj: { arr: [3, 4] } })).toEqual({
35 | obj: {
36 | foo: "foo",
37 | arr: [3, 4]
38 | }
39 | });
40 | });
41 | it("Don't mutate target", () => {
42 | const target = {
43 | str: "foo"
44 | };
45 |
46 | expect(mergeDeep(target, { str: "bar" })).not.toBe(target);
47 | });
48 | it("Skip undefined sources", () => {
49 | expect(mergeDeep(simpleObject, undefined)).toBe(simpleObject);
50 | });
51 | it("Skip undefined sources dfs", () => {
52 | const simpleArray = [1, 2];
53 |
54 | expect(mergeDeep(simpleArray, [3, 4])).toEqual(simpleArray);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/tools/merge.ts:
--------------------------------------------------------------------------------
1 | import { UnknownObject } from "../types";
2 |
3 | const isObject = (obj: Record): boolean => !!obj && typeof obj === "object" && !Array.isArray(obj);
4 |
5 | export default function mergeDeep(target: UnknownObject, ...sources: UnknownObject[]): UnknownObject {
6 | if (!sources.length) return target;
7 | const source = sources.shift();
8 | if (source === undefined || !isObject(target) || !isObject(source)) return target;
9 | target = { ...target };
10 | Object.keys(source).forEach((key: string): void => {
11 | const targetValue = target[key];
12 | const sourceValue = source[key];
13 |
14 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
15 | target[key] = sourceValue;
16 | } else if (isObject(targetValue) && isObject(sourceValue)) {
17 | target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue);
18 | } else {
19 | target[key] = sourceValue;
20 | }
21 | });
22 |
23 | return mergeDeep(target, ...sources);
24 | }
25 |
--------------------------------------------------------------------------------
/src/tools/sanitizeOptions.ts:
--------------------------------------------------------------------------------
1 | import { RequiredOptions } from "../core/QROptions";
2 | import { Gradient } from "../types";
3 |
4 | function sanitizeGradient(gradient: Gradient): Gradient {
5 | const newGradient = { ...gradient };
6 |
7 | if (!newGradient.colorStops || !newGradient.colorStops.length) {
8 | throw "Field 'colorStops' is required in gradient";
9 | }
10 |
11 | if (newGradient.rotation) {
12 | newGradient.rotation = Number(newGradient.rotation);
13 | } else {
14 | newGradient.rotation = 0;
15 | }
16 |
17 | newGradient.colorStops = newGradient.colorStops.map((colorStop: { offset: number; color: string }) => ({
18 | ...colorStop,
19 | offset: Number(colorStop.offset)
20 | }));
21 |
22 | return newGradient;
23 | }
24 |
25 | export default function sanitizeOptions(options: RequiredOptions): RequiredOptions {
26 | const newOptions = { ...options };
27 |
28 | newOptions.width = Number(newOptions.width);
29 | newOptions.height = Number(newOptions.height);
30 | newOptions.margin = Number(newOptions.margin);
31 | newOptions.imageOptions = {
32 | ...newOptions.imageOptions,
33 | hideBackgroundDots: Boolean(newOptions.imageOptions.hideBackgroundDots),
34 | imageSize: Number(newOptions.imageOptions.imageSize),
35 | margin: Number(newOptions.imageOptions.margin)
36 | };
37 |
38 | if (newOptions.margin > Math.min(newOptions.width, newOptions.height)) {
39 | newOptions.margin = Math.min(newOptions.width, newOptions.height);
40 | }
41 |
42 | newOptions.dotsOptions = {
43 | ...newOptions.dotsOptions
44 | };
45 | if (newOptions.dotsOptions.gradient) {
46 | newOptions.dotsOptions.gradient = sanitizeGradient(newOptions.dotsOptions.gradient);
47 | }
48 |
49 | if (newOptions.cornersSquareOptions) {
50 | newOptions.cornersSquareOptions = {
51 | ...newOptions.cornersSquareOptions
52 | };
53 | if (newOptions.cornersSquareOptions.gradient) {
54 | newOptions.cornersSquareOptions.gradient = sanitizeGradient(newOptions.cornersSquareOptions.gradient);
55 | }
56 | }
57 |
58 | if (newOptions.cornersDotOptions) {
59 | newOptions.cornersDotOptions = {
60 | ...newOptions.cornersDotOptions
61 | };
62 | if (newOptions.cornersDotOptions.gradient) {
63 | newOptions.cornersDotOptions.gradient = sanitizeGradient(newOptions.cornersDotOptions.gradient);
64 | }
65 | }
66 |
67 | if (newOptions.backgroundOptions) {
68 | newOptions.backgroundOptions = {
69 | ...newOptions.backgroundOptions
70 | };
71 | if (newOptions.backgroundOptions.gradient) {
72 | newOptions.backgroundOptions.gradient = sanitizeGradient(newOptions.backgroundOptions.gradient);
73 | }
74 | }
75 |
76 | return newOptions;
77 | }
78 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface UnknownObject {
2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
3 | [key: string]: any;
4 | }
5 |
6 | export type DotType = "dots" | "rounded" | "classy" | "classy-rounded" | "square" | "extra-rounded";
7 | export type CornerDotType = "dot" | "square";
8 | export type CornerSquareType = "dot" | "square" | "extra-rounded";
9 | export type Extension = "svg" | "png" | "jpeg" | "webp";
10 | export type GradientType = "radial" | "linear";
11 | export type DrawType = "canvas" | "svg";
12 |
13 | export interface Canvas extends HTMLCanvasElement {
14 | toBuffer?: (type: string) => Buffer;
15 | createCanvas?: (width: number, height: number) => Canvas;
16 | loadImage?: (image: string) => Promise;
17 | }
18 |
19 | export interface Window {
20 | Image: typeof HTMLImageElement;
21 | XMLSerializer: typeof XMLSerializer;
22 | document: Document;
23 | }
24 | declare const window: Window;
25 |
26 | interface JsDomOptions {
27 | resources: string;
28 | }
29 | export class JSDom {
30 | window: Window;
31 | _options: JsDomOptions;
32 | _input: string;
33 |
34 | constructor(input: string, options: JsDomOptions) {
35 | this._options = options;
36 | this._input = input;
37 | this.window = window;
38 | }
39 | }
40 |
41 | export type Gradient = {
42 | type: GradientType;
43 | rotation?: number;
44 | colorStops: {
45 | offset: number;
46 | color: string;
47 | }[];
48 | };
49 |
50 | export interface DotTypes {
51 | [key: string]: DotType;
52 | }
53 |
54 | export interface GradientTypes {
55 | [key: string]: GradientType;
56 | }
57 |
58 | export interface CornerDotTypes {
59 | [key: string]: CornerDotType;
60 | }
61 |
62 | export interface CornerSquareTypes {
63 | [key: string]: CornerSquareType;
64 | }
65 |
66 | export interface DrawTypes {
67 | [key: string]: DrawType;
68 | }
69 |
70 | export type TypeNumber =
71 | | 0
72 | | 1
73 | | 2
74 | | 3
75 | | 4
76 | | 5
77 | | 6
78 | | 7
79 | | 8
80 | | 9
81 | | 10
82 | | 11
83 | | 12
84 | | 13
85 | | 14
86 | | 15
87 | | 16
88 | | 17
89 | | 18
90 | | 19
91 | | 20
92 | | 21
93 | | 22
94 | | 23
95 | | 24
96 | | 25
97 | | 26
98 | | 27
99 | | 28
100 | | 29
101 | | 30
102 | | 31
103 | | 32
104 | | 33
105 | | 34
106 | | 35
107 | | 36
108 | | 37
109 | | 38
110 | | 39
111 | | 40;
112 |
113 | export type ErrorCorrectionLevel = "L" | "M" | "Q" | "H";
114 | export type Mode = "Numeric" | "Alphanumeric" | "Byte" | "Kanji";
115 | export interface QRCode {
116 | addData(data: string, mode?: Mode): void;
117 | make(): void;
118 | getModuleCount(): number;
119 | isDark(row: number, col: number): boolean;
120 | createImgTag(cellSize?: number, margin?: number): string;
121 | createSvgTag(cellSize?: number, margin?: number): string;
122 | createSvgTag(opts?: { cellSize?: number; margin?: number; scalable?: boolean }): string;
123 | createDataURL(cellSize?: number, margin?: number): string;
124 | createTableTag(cellSize?: number, margin?: number): string;
125 | createASCII(cellSize?: number, margin?: number): string;
126 | renderTo2dContext(context: CanvasRenderingContext2D, cellSize?: number): void;
127 | }
128 |
129 | export type Options = {
130 | type?: DrawType;
131 | width?: number;
132 | height?: number;
133 | margin?: number;
134 | data?: string;
135 | image?: string;
136 | nodeCanvas?: Canvas;
137 | jsdom?: typeof JSDom;
138 | qrOptions?: {
139 | typeNumber?: TypeNumber;
140 | mode?: Mode;
141 | errorCorrectionLevel?: ErrorCorrectionLevel;
142 | };
143 | imageOptions?: {
144 | saveAsBlob?: boolean;
145 | hideBackgroundDots?: boolean;
146 | imageSize?: number;
147 | crossOrigin?: string;
148 | margin?: number;
149 | };
150 | dotsOptions?: {
151 | type?: DotType;
152 | color?: string;
153 | gradient?: Gradient;
154 | };
155 | cornersSquareOptions?: {
156 | type?: CornerSquareType;
157 | color?: string;
158 | gradient?: Gradient;
159 | };
160 | cornersDotOptions?: {
161 | type?: CornerDotType;
162 | color?: string;
163 | gradient?: Gradient;
164 | };
165 | backgroundOptions?: {
166 | color?: string;
167 | gradient?: Gradient;
168 | };
169 | };
170 |
171 | export type FilterFunction = (i: number, j: number) => boolean;
172 |
173 | export type DownloadOptions = {
174 | name?: string;
175 | extension?: Extension;
176 | };
177 |
178 | export type DrawArgs = {
179 | x: number;
180 | y: number;
181 | size: number;
182 | rotation?: number;
183 | getNeighbor?: GetNeighbor;
184 | };
185 |
186 | export type BasicFigureDrawArgs = {
187 | x: number;
188 | y: number;
189 | size: number;
190 | rotation?: number;
191 | };
192 |
193 | export type RotateFigureArgs = {
194 | x: number;
195 | y: number;
196 | size: number;
197 | rotation?: number;
198 | draw: () => void;
199 | };
200 |
201 | export type DrawArgsCanvas = DrawArgs & {
202 | context: CanvasRenderingContext2D;
203 | };
204 |
205 | export type BasicFigureDrawArgsCanvas = BasicFigureDrawArgs & {
206 | context: CanvasRenderingContext2D;
207 | };
208 |
209 | export type RotateFigureArgsCanvas = RotateFigureArgs & {
210 | context: CanvasRenderingContext2D;
211 | };
212 |
213 | export type GetNeighbor = (x: number, y: number) => boolean;
214 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./lib/",
4 | "sourceMap": true,
5 | "module": "es6",
6 | "target": "es5",
7 | "allowJs": true,
8 | "moduleResolution": "node",
9 | "declaration": true,
10 | "allowSyntheticDefaultImports": true,
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "noImplicitThis": true,
14 | "alwaysStrict": true,
15 | "strictNullChecks": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "noImplicitReturns": true,
18 | "noUnusedLocals": true,
19 | "esModuleInterop": true
20 | },
21 | "include": ["./src/**/*"]
22 | }
23 |
--------------------------------------------------------------------------------
/webpack.config.build.js:
--------------------------------------------------------------------------------
1 | const commonConfig = require('./webpack.config.common.js');
2 | const config = commonConfig;
3 |
4 | module.exports = (env, argv) => {
5 | config.mode = argv.mode;
6 |
7 | if (argv.mode === "development") {
8 | config.devtool = "inline-source-map";
9 | config.watch = true;
10 | }
11 |
12 | if (argv.mode === "production") {
13 | config.devtool = "source-map";
14 | }
15 |
16 | return config;
17 | };
--------------------------------------------------------------------------------
/webpack.config.common.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
3 | const FileManagerPlugin = require('filemanager-webpack-plugin');
4 |
5 | const rootPath = path.resolve(__dirname, "./");
6 | const srcPath = path.resolve(rootPath, "src");
7 | const libPath = path.resolve(rootPath, "lib");
8 | const tmpPath = path.resolve(rootPath, "tmp");
9 |
10 | const shared = {
11 | entry: srcPath + "/index.ts",
12 | module: {
13 | rules: [
14 | {
15 | test: /\.ts$/,
16 | loader: "ts-loader",
17 | exclude: /node_modules/
18 | },
19 | {
20 | enforce: "pre",
21 | test: /\.ts$/,
22 | loader: "eslint-loader",
23 | exclude: /node_modules/
24 | }
25 | ]
26 | },
27 | plugins: [
28 | new CleanWebpackPlugin(),
29 | new FileManagerPlugin({
30 | events: {
31 | onEnd: {
32 | copy: [
33 | { source: tmpPath + '/**/*', destination: libPath },
34 | ],
35 | delete: [tmpPath],
36 | },
37 | },
38 | }),
39 | ],
40 | resolve: {
41 | extensions: [".ts", ".js"]
42 | }
43 | };
44 |
45 | module.exports = [{
46 | ...shared,
47 | output: {
48 | path: tmpPath,
49 | filename: "qr-code-styling.js",
50 | library: "QRCodeStyling",
51 | libraryTarget: "umd",
52 | libraryExport: "default"
53 | },
54 | }, {
55 | ...shared,
56 | output: {
57 | path: tmpPath,
58 | filename: "qr-code-styling.common.js",
59 | library: "QRCodeStyling",
60 | libraryTarget: "commonjs",
61 | libraryExport: "default"
62 | },
63 | }];
64 |
--------------------------------------------------------------------------------
/webpack.config.dev-server.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const commonConfig = require('./webpack.config.common.js');
4 |
5 | module.exports = merge(commonConfig, {
6 | mode: 'development',
7 | devServer: {
8 | injectClient: false //workaround for bug https://github.com/webpack/webpack-dev-server/issues/2484
9 | },
10 | devtool: "inline-source-map",
11 | plugins: [
12 | new HtmlWebpackPlugin({
13 | template: './src/index.html',
14 | inject: 'head'
15 | })
16 | ]
17 | });
18 |
--------------------------------------------------------------------------------