├── .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 | [![Version](https://img.shields.io/npm/v/qr-code-styling.svg)](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 | --------------------------------------------------------------------------------