├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── src ├── app.ts ├── assets │ ├── .DS_Store │ ├── facebook_example_new.png │ ├── instagram_example.png │ ├── qr_code_example.png │ ├── telegram_example_new.png │ └── test │ │ ├── image_from_readme.png │ │ ├── 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.ts │ ├── errorCorrectionLevels.ts │ ├── errorCorrectionPercents.ts │ ├── gradientTypes.ts │ ├── modes.ts │ └── qrTypes.ts ├── core │ ├── QRCanvas.ts │ ├── QRCodeStyling.ts │ ├── QRCornerDot.ts │ ├── QRCornerSquare.ts │ ├── QRDot.ts │ └── QROptions.ts ├── index.d.ts ├── index.js ├── tools │ ├── calculateImageSize.ts │ ├── downloadURI.ts │ ├── getMode.ts │ ├── merge.ts │ └── sanitizeOptions.ts ├── types │ └── index.ts └── vue3-qr-code-styling.vue ├── tsconfig.json ├── webpack.config.build.js └── webpack.config.common.js /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "none", 4 | singleQuote: false, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | { 2 | "language": "node_js", 3 | "os": ["linux"], 4 | "dist": "xenial", 5 | "node_js": ["node", "13", "12", "11", "10", "8", "6"], 6 | "script": ["node lib/index.js"] 7 | } 8 | -------------------------------------------------------------------------------- /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 | [![Known Vulnerabilities](https://snyk.io/test/github/diadal/vue3-qr-code-styling/badge.svg)](https://snyk.io/test/github/diadal/vue3-qr-code-styling) 2 | 3 | # Vue3 QR Code Styling 4 | 5 | [![Version](https://img.shields.io/npm/v/vue3-qr-code-styling.svg)](https://www.npmjs.org/package/vue3-qr-code-styling) 6 | 7 | JavaScript library for generating QR codes with a logo and styling. 8 | 9 | this clone copy of https://qr-code-styling.com 10 | 11 | If you have issues / suggestions / notes / questions, please open an issue or contact me. Let's create a cool library together. 12 | 13 | ### Examples 14 | 15 |

16 | 17 | 18 | 19 |

20 | 21 | ### Installation 22 | 23 | ``` 24 | npm install @diadal/vue3-qr-code-styling 25 | or 26 | yarn add @diadal/vue3-qr-code-styling 27 | ``` 28 | 29 | ### Usage 30 | 31 | ```HTML 32 | 66 | 67 | 87 | 88 | 92 | ``` 93 | 94 | ### API Documentation 95 | 96 | #### VQRCodeStyling instance 97 | 98 | `new VQRCodeStyling(options) => VQRCodeStyling` 99 | 100 | | Param | Type | Description | 101 | | ------- | ------ | ----------- | 102 | | options | object | Init object | 103 | 104 | `options` structure 105 | 106 | | Property | Type | Default Value | Description | 107 | | ----------------------- | ------ | ------------- | ----------------------------------------------------- | 108 | | width | number | `300` | Size of canvas | 109 | | height | number | `300` | Size of canvas | 110 | | download | boolean| false | To endable download button | 111 | | myclass | string | '' | Image DIV class | 112 | | imgclass | string | '' | Image class | 113 | | downloadButton | string | '' | download button class | 114 | | downloadOptions | object | | download option name and extension | 115 | | data | string | | The date will be encoded to the QR code | 116 | | image | string | | The image will be copied to the center of the QR code | 117 | | margin | number | `0` | Margin around canvas | 118 | | qrOptions | object | | Options will be passed to `qrcode-generator` lib | 119 | | imageOptions | object | | Specific image options, details see below | 120 | | dotsOptions | object | | Dots styling options | 121 | | cornersSquareOptions | object | | Square in the corners styling options | 122 | | cornersDotOptionsHelper | object | | Dots in the corners styling options | 123 | | backgroundOptions | object | | QR background styling options | 124 | 125 | `options.qrOptions` structure 126 | 127 | | Property | Type | Default Value | 128 | | -------------------- | -------------------------------------------------- | ------------- | 129 | | typeNumber | number (`0 - 40`) | `0` | 130 | | mode | string (`'Numeric' 'Alphanumeric' 'Byte' 'Kanji'`) | 131 | | errorCorrectionLevel | string (`'L' 'M' 'Q' 'H'`) | `'Q'` | 132 | 133 | `options.imageOptions` structure 134 | 135 | | Property | Type | Default Value | Description | 136 | | ------------------ | --------------------------------------- | ------------- | ------------------------------------------------------------------------------ | 137 | | hideBackgroundDots | boolean | `true` | Hide all dots covered by the image | 138 | | imageSize | number | `0.4` | Coefficient of the image size. Not recommended to use ove 0.5. Lower is better | 139 | | margin | number | `0` | Margin of the image in px | 140 | | crossOrigin | string(`'anonymous' 'use-credentials'`) | | Set "anonymous" if you want to download QR code from other origins. | 141 | 142 | `options.dotsOptions` structure 143 | 144 | | Property | Type | Default Value | Description | 145 | | -------- | ------------------------------------------------------------------------------ | ------------- | ------------------- | 146 | | color | string | `'#000'` | Color of QR dots | 147 | | gradient | object | | Gradient of QR dots | 148 | | type | string (`'rounded' 'dots' 'classy' 'classy-rounded' 'square' 'extra-rounded'`) | `'square'` | Style of QR dots | 149 | 150 | `options.backgroundOptions` structure 151 | 152 | | Property | Type | Default Value | 153 | | -------- | ------ | ------------- | 154 | | color | string | `'#fff'` | 155 | | gradient | object | 156 | 157 | `options.cornersSquareOptions` structure 158 | 159 | | Property | Type | Default Value | Description | 160 | | -------- | ----------------------------------------- | ------------- | -------------------------- | 161 | | color | string | | Color of Corners Square | 162 | | gradient | object | | Gradient of Corners Square | 163 | | type | string (`'dot' 'square' 'extra-rounded'`) | | Style of Corners Square | 164 | 165 | `options.cornersDotOptions` structure 166 | 167 | | Property | Type | Default Value | Description | 168 | | -------- | ------------------------- | ------------- | ----------------------- | 169 | | color | string | | Color of Corners Dot | 170 | | gradient | object | | Gradient of Corners Dot | 171 | | type | string (`'dot' 'square'`) | | Style of Corners Dot | 172 | 173 | Gradient structure 174 | 175 | `options.dotsOptions.gradient` 176 | 177 | `options.backgroundOptions.gradient` 178 | 179 | `options.cornersSquareOptions.gradient` 180 | 181 | `options.cornersDotOptions.gradient` 182 | 183 | | Property | Type | Default Value | Description | 184 | | ---------- | ---------------------------- | ------------- | -------------------------------------------------------------------------------------- | 185 | | type | string (`'linear' 'radial'`) | "linear" | Type of gradient spread | 186 | | rotation | number | 0 | Rotation of gradient in radians (Math.PI === 180 degrees) | 187 | | colorStops | array of objects | | Gradient colors. Example `[{ offset: 0, color: 'blue' }, { offset: 1, color: 'red' }]` | 188 | 189 | Gradient colorStops structure 190 | 191 | `options.dotsOptions.gradient.colorStops[]` 192 | 193 | `options.backgroundOptions.gradient.colorStops[]` 194 | 195 | `options.cornersSquareOptions.gradient.colorStops[]` 196 | 197 | `options.cornersDotOptions.gradient.colorStops[]` 198 | 199 | | Property | Type | Default Value | Description | 200 | | -------- | ---------------- | ------------- | ----------------------------------- | 201 | | offset | number (`0 - 1`) | | Position of color in gradient range | 202 | | color | string | | Color of stop in gradient range | 203 | 204 | #### VQRCodeStyling methods 205 | 206 | `VQRCodeStyling.append(container) => void` 207 | 208 | | Param | Type | Description | 209 | | --------- | ----------- | -------------------------------------------------------- | 210 | | container | DOM element | This container will be used for appending of the QR code | 211 | 212 | `VQRCodeStyling.update(options) => void` 213 | 214 | | Param | Type | Description | 215 | | ------- | ------ | -------------------------------------- | 216 | | options | object | The same options as for initialization | 217 | 218 | `VQRCodeStyling.download(downloadOptions) => void` 219 | 220 | | Param | Type | Description | 221 | | --------------- | ------ | ------------------------------------------------------ | 222 | | downloadOptions | object | Options with extension and name of file (not required) | 223 | 224 | `downloadOptions` structure 225 | 226 | | Property | Type | Default Value | Description | 227 | | --------- | ------------------------------ | ------------- | --------------------------- | 228 | | name | string | `'qr'` | Name of the downloaded file | 229 | | extension | string (`'png' 'jpeg' 'webp'`) | `'png'` | File extension | 230 | 231 | if any issue [check](https://github.com/diadal/vue3-qr-code-styling/issues) 232 | 233 | _also you can buy me a coffee @ [Patreon](https://www.patreon.com/diadal)_ 234 | 235 | ### License 236 | 237 | [MIT License](https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/master/LICENSE). Copyright (c) 2021 Diadal Nig 238 | -------------------------------------------------------------------------------- /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": "@diadal/vue3-qr-code-styling", 3 | "version": "1.4.6", 4 | "description": "Add a style and an image to your QR code Vue3", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "files": [ 8 | "src" 9 | ], 10 | "dependencies": { 11 | "@vue/compiler-sfc": "^3.0.6", 12 | "qrcode-generator": "^1.4.3", 13 | "vue": "^3.0.6", 14 | "vue-loader": "^16.1.2" 15 | }, 16 | "devDependencies": { 17 | "@typescript-eslint/eslint-plugin": "^4.13.0", 18 | "@typescript-eslint/parser": "^4.13.0", 19 | "canvas": "^2.6.1", 20 | "clean-webpack-plugin": "^3.0.0", 21 | "eslint": "^7.17.0", 22 | "eslint-config-prettier": "^7.1.0", 23 | "eslint-loader": "^4.0.2", 24 | "eslint-plugin-jest": "^24.1.3", 25 | "eslint-plugin-prettier": "^3.3.1", 26 | "html-webpack-plugin": "^4.5.1", 27 | "jest": "^26.6.3", 28 | "jest-environment-jsdom-fifteen": "^1.0.0", 29 | "prettier": "^2.2.1", 30 | "ts-jest": "^26.4.4", 31 | "ts-loader": "^8.0.17", 32 | "typescript": "^4.2.2", 33 | "webpack": "^5.24.2", 34 | "webpack-cli": "^4.5.0", 35 | "webpack-dev-server": "^3.11.2", 36 | "webpack-merge": "^5.7.3" 37 | }, 38 | "scripts": { 39 | "build": "webpack --mode=production --config webpack.config.build.js && copyfiles -f ./src/*.vue lib", 40 | "build:dev": "webpack --mode=development --config webpack.config.build.js", 41 | "test": "jest" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/diadal/vue3-qr-code-styling.git" 46 | }, 47 | "keywords": [ 48 | "qr", 49 | "qrcode", 50 | "qr-code", 51 | "js", 52 | "qrjs", 53 | "qrstyling", 54 | "styling", 55 | "qrbranding", 56 | "branding", 57 | "qrimage", 58 | "image", 59 | "qrlogo", 60 | "logo", 61 | "design" 62 | ], 63 | "author": "Diadal ", 64 | "private": false, 65 | "license": "MIT", 66 | "bugs": { 67 | "url": "https://github.com/diadal/vue3-qr-code-styling/issues" 68 | }, 69 | "homepage": "https://github.com/diadal/vue3-qr-code-styling" 70 | } 71 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import VQRCodeStyling 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 | 10 | export { dotTypes, cornerDotTypes, cornerSquareTypes, errorCorrectionLevels, errorCorrectionPercents, modes, qrTypes } 11 | export default VQRCodeStyling 12 | -------------------------------------------------------------------------------- /src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/.DS_Store -------------------------------------------------------------------------------- /src/assets/facebook_example_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/facebook_example_new.png -------------------------------------------------------------------------------- /src/assets/instagram_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/instagram_example.png -------------------------------------------------------------------------------- /src/assets/qr_code_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/qr_code_example.png -------------------------------------------------------------------------------- /src/assets/telegram_example_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/telegram_example_new.png -------------------------------------------------------------------------------- /src/assets/test/image_from_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/image_from_readme.png -------------------------------------------------------------------------------- /src/assets/test/rounded_dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/rounded_dots.png -------------------------------------------------------------------------------- /src/assets/test/simple_dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/simple_dots.png -------------------------------------------------------------------------------- /src/assets/test/simple_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/simple_qr.png -------------------------------------------------------------------------------- /src/assets/test/simple_qr_with_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/simple_qr_with_image.png -------------------------------------------------------------------------------- /src/assets/test/simple_qr_with_image_margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/simple_qr_with_image_margin.png -------------------------------------------------------------------------------- /src/assets/test/simple_qr_with_margin_canvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/src/assets/test/simple_qr_with_margin_canvas.png -------------------------------------------------------------------------------- /src/assets/test/simple_square_dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diadal/vue3-qr-code-styling/e6d53ebac1006be50a2779dce26e205aa84df340/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.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/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.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.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.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.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-throw-literal */ 2 | import calculateImageSize from '../tools/calculateImageSize' 3 | import errorCorrectionPercents from '../constants/errorCorrectionPercents' 4 | import QRDot from './QRDot' 5 | import QRCornerSquare from './QRCornerSquare' 6 | import QRCornerDot from './QRCornerDot' 7 | import { RequiredOptions, Gradient } from './QROptions' 8 | import gradientTypes from '../constants/gradientTypes' 9 | import { QRCode } from '../types' 10 | 11 | type FilterFunction = (i: number, j: number) => boolean; 12 | 13 | const squareMask = [ 14 | [1, 1, 1, 1, 1, 1, 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, 0, 0, 0, 0, 0, 1], 20 | [1, 1, 1, 1, 1, 1, 1] 21 | ] 22 | 23 | const dotMask = [ 24 | [0, 0, 0, 0, 0, 0, 0], 25 | [0, 0, 0, 0, 0, 0, 0], 26 | [0, 0, 1, 1, 1, 0, 0], 27 | [0, 0, 1, 1, 1, 0, 0], 28 | [0, 0, 1, 1, 1, 0, 0], 29 | [0, 0, 0, 0, 0, 0, 0], 30 | [0, 0, 0, 0, 0, 0, 0] 31 | ] 32 | 33 | export default class QRCanvas { 34 | _canvas: HTMLCanvasElement; 35 | _options: RequiredOptions; 36 | _qr?: QRCode; 37 | _image?: HTMLImageElement; 38 | 39 | // TODO don't pass all options to this class 40 | constructor (options: RequiredOptions) { 41 | this._canvas = document.createElement('canvas') 42 | this._canvas.width = options.width 43 | this._canvas.height = options.height 44 | this._options = options 45 | } 46 | 47 | get context (): CanvasRenderingContext2D | null { 48 | return this._canvas.getContext('2d') 49 | } 50 | 51 | get width (): number { 52 | return this._canvas.width 53 | } 54 | 55 | get height (): number { 56 | return this._canvas.height 57 | } 58 | 59 | getCanvas (): HTMLCanvasElement { 60 | return this._canvas 61 | } 62 | 63 | clear (): void { 64 | const canvasContext = this.context 65 | 66 | if (canvasContext) { 67 | canvasContext.clearRect(0, 0, this._canvas.width, this._canvas.height) 68 | } 69 | } 70 | 71 | async drawQR (qr: QRCode): Promise { 72 | const count = qr.getModuleCount() 73 | const minSize = Math.min(this._options.width, this._options.height) - this._options.margin * 2 74 | const dotSize = Math.floor(minSize / count) 75 | let drawImageSize = { 76 | hideXDots: 0, 77 | hideYDots: 0, 78 | width: 0, 79 | height: 0 80 | } 81 | 82 | this._qr = qr 83 | 84 | if (this._options.image) { 85 | await this.loadImage() 86 | if (!this._image) return 87 | const { imageOptions, qrOptions } = this._options 88 | const coverLevel = imageOptions.imageSize * errorCorrectionPercents[qrOptions.errorCorrectionLevel] 89 | const maxHiddenDots = Math.floor(coverLevel * count * count) 90 | 91 | drawImageSize = calculateImageSize({ 92 | originalWidth: this._image.width, 93 | originalHeight: this._image.height, 94 | maxHiddenDots, 95 | maxHiddenAxisDots: count - 14, 96 | dotSize 97 | }) 98 | } 99 | 100 | this.clear() 101 | this.drawBackground() 102 | this.drawDots((i: number, j: number): boolean => { 103 | if (this._options.imageOptions.hideBackgroundDots) { 104 | if ( 105 | i >= (count - drawImageSize.hideXDots) / 2 && 106 | i < (count + drawImageSize.hideXDots) / 2 && 107 | j >= (count - drawImageSize.hideYDots) / 2 && 108 | j < (count + drawImageSize.hideYDots) / 2 109 | ) { 110 | return false 111 | } 112 | } 113 | 114 | if (squareMask[i]?.[j] || squareMask[i - count + 7]?.[j] || squareMask[i]?.[j - count + 7]) { 115 | return false 116 | } 117 | 118 | if (dotMask[i]?.[j] || dotMask[i - count + 7]?.[j] || dotMask[i]?.[j - count + 7]) { 119 | return false 120 | } 121 | 122 | return true 123 | }) 124 | this.drawCorners() 125 | 126 | if (this._options.image) { 127 | this.drawImage({ width: drawImageSize.width, height: drawImageSize.height, count, dotSize }) 128 | } 129 | } 130 | 131 | drawBackground (): void { 132 | const canvasContext = this.context 133 | const options = this._options 134 | 135 | if (canvasContext) { 136 | if (options.backgroundOptions.gradient) { 137 | const gradientOptions = options.backgroundOptions.gradient 138 | const gradient = this._createGradient({ 139 | context: canvasContext, 140 | options: gradientOptions, 141 | additionalRotation: 0, 142 | x: 0, 143 | y: 0, 144 | size: this._canvas.width > this._canvas.height ? this._canvas.width : this._canvas.height 145 | }) 146 | 147 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { 148 | gradient.addColorStop(offset, color) 149 | }) 150 | 151 | canvasContext.fillStyle = gradient 152 | } else if (options.backgroundOptions.color) { 153 | canvasContext.fillStyle = options.backgroundOptions.color 154 | } 155 | canvasContext.fillRect(0, 0, this._canvas.width, this._canvas.height) 156 | } 157 | } 158 | 159 | drawDots (filter?: FilterFunction): void { 160 | if (!this._qr) { 161 | throw 'QR code is not defined' 162 | } 163 | 164 | const canvasContext = this.context 165 | 166 | if (!canvasContext) { 167 | throw 'QR code is not defined' 168 | } 169 | 170 | const options = this._options 171 | const count = this._qr.getModuleCount() 172 | 173 | if (count > options.width || count > options.height) { 174 | throw 'The canvas is too small.' 175 | } 176 | 177 | const minSize = Math.min(options.width, options.height) - options.margin * 2 178 | const dotSize = Math.floor(minSize / count) 179 | const xBeginning = Math.floor((options.width - count * dotSize) / 2) 180 | const yBeginning = Math.floor((options.height - count * dotSize) / 2) 181 | const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type }) 182 | 183 | canvasContext.beginPath() 184 | 185 | for (let i = 0; i < count; i++) { 186 | for (let j = 0; j < count; j++) { 187 | if (filter && !filter(i, j)) { 188 | continue 189 | } 190 | if (!this._qr.isDark(i, j)) { 191 | continue 192 | } 193 | dot.draw( 194 | xBeginning + i * dotSize, 195 | yBeginning + j * dotSize, 196 | dotSize, 197 | (xOffset: number, yOffset: number): boolean => { 198 | if (i + xOffset < 0 || j + yOffset < 0 || i + xOffset >= count || j + yOffset >= count) return false 199 | if (filter && !filter(i + xOffset, j + yOffset)) return false 200 | return !!this._qr && this._qr.isDark(i + xOffset, j + yOffset) 201 | } 202 | ) 203 | } 204 | } 205 | 206 | if (options.dotsOptions.gradient) { 207 | const gradientOptions = options.dotsOptions.gradient 208 | const gradient = this._createGradient({ 209 | context: canvasContext, 210 | options: gradientOptions, 211 | additionalRotation: 0, 212 | x: xBeginning, 213 | y: yBeginning, 214 | size: count * dotSize 215 | }) 216 | 217 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { 218 | gradient.addColorStop(offset, color) 219 | }) 220 | 221 | canvasContext.fillStyle = canvasContext.strokeStyle = gradient 222 | } else if (options.dotsOptions.color) { 223 | canvasContext.fillStyle = canvasContext.strokeStyle = options.dotsOptions.color 224 | } 225 | 226 | canvasContext.fill('evenodd') 227 | } 228 | 229 | drawCorners (filter?: FilterFunction): void { 230 | if (!this._qr) { 231 | throw 'QR code is not defined' 232 | } 233 | 234 | const canvasContext = this.context 235 | 236 | if (!canvasContext) { 237 | throw 'QR code is not defined' 238 | } 239 | 240 | const options = this._options 241 | 242 | const count = this._qr.getModuleCount() 243 | const minSize = Math.min(options.width, options.height) - options.margin * 2 244 | const dotSize = Math.floor(minSize / count) 245 | const cornersSquareSize = dotSize * 7 246 | const cornersDotSize = dotSize * 3 247 | const xBeginning = Math.floor((options.width - count * dotSize) / 2) 248 | const yBeginning = Math.floor((options.height - count * dotSize) / 2); 249 | 250 | [ 251 | [0, 0, 0], 252 | [1, 0, Math.PI / 2], 253 | [0, 1, -Math.PI / 2] 254 | ].forEach(([column, row, rotation]) => { 255 | if (filter && !filter(column, row)) { 256 | return 257 | } 258 | 259 | const x = xBeginning + column * dotSize * (count - 7) 260 | const y = yBeginning + row * dotSize * (count - 7) 261 | 262 | if (options.cornersSquareOptions?.type) { 263 | const cornersSquare = new QRCornerSquare({ context: canvasContext, type: options.cornersSquareOptions?.type }) 264 | 265 | canvasContext.beginPath() 266 | cornersSquare.draw(x, y, cornersSquareSize, rotation) 267 | } else { 268 | const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type }) 269 | 270 | canvasContext.beginPath() 271 | 272 | for (let i = 0; i < squareMask.length; i++) { 273 | for (let j = 0; j < squareMask[i].length; j++) { 274 | if (!squareMask[i]?.[j]) { 275 | continue 276 | } 277 | 278 | dot.draw( 279 | x + i * dotSize, 280 | y + j * dotSize, 281 | dotSize, 282 | (xOffset: number, yOffset: number): boolean => !!squareMask[i + xOffset]?.[j + yOffset] 283 | ) 284 | } 285 | } 286 | } 287 | 288 | if (options.cornersSquareOptions?.gradient) { 289 | const gradientOptions = options.cornersSquareOptions.gradient 290 | const gradient = this._createGradient({ 291 | context: canvasContext, 292 | options: gradientOptions, 293 | additionalRotation: rotation, 294 | x, 295 | y, 296 | size: cornersSquareSize 297 | }) 298 | 299 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { 300 | gradient.addColorStop(offset, color) 301 | }) 302 | 303 | canvasContext.fillStyle = canvasContext.strokeStyle = gradient 304 | } else if (options.cornersSquareOptions?.color) { 305 | canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersSquareOptions.color 306 | } 307 | 308 | canvasContext.fill('evenodd') 309 | 310 | if (options.cornersDotOptions?.type) { 311 | const cornersDot = new QRCornerDot({ context: canvasContext, type: options.cornersDotOptions?.type }) 312 | 313 | canvasContext.beginPath() 314 | cornersDot.draw(x + dotSize * 2, y + dotSize * 2, cornersDotSize, rotation) 315 | } else { 316 | const dot = new QRDot({ context: canvasContext, type: options.dotsOptions.type }) 317 | 318 | canvasContext.beginPath() 319 | 320 | for (let i = 0; i < dotMask.length; i++) { 321 | for (let j = 0; j < dotMask[i].length; j++) { 322 | if (!dotMask[i]?.[j]) { 323 | continue 324 | } 325 | 326 | dot.draw( 327 | x + i * dotSize, 328 | y + j * dotSize, 329 | dotSize, 330 | (xOffset: number, yOffset: number): boolean => !!dotMask[i + xOffset]?.[j + yOffset] 331 | ) 332 | } 333 | } 334 | } 335 | 336 | if (options.cornersDotOptions?.gradient) { 337 | const gradientOptions = options.cornersDotOptions.gradient 338 | const gradient = this._createGradient({ 339 | context: canvasContext, 340 | options: gradientOptions, 341 | additionalRotation: rotation, 342 | x: x + dotSize * 2, 343 | y: y + dotSize * 2, 344 | size: cornersDotSize 345 | }) 346 | 347 | gradientOptions.colorStops.forEach(({ offset, color }: { offset: number; color: string }) => { 348 | gradient.addColorStop(offset, color) 349 | }) 350 | 351 | canvasContext.fillStyle = canvasContext.strokeStyle = gradient 352 | } else if (options.cornersDotOptions?.color) { 353 | canvasContext.fillStyle = canvasContext.strokeStyle = options.cornersDotOptions.color 354 | } 355 | 356 | canvasContext.fill('evenodd') 357 | }) 358 | } 359 | 360 | loadImage (): Promise { 361 | return new Promise((resolve, reject) => { 362 | const options = this._options 363 | const image = new Image() 364 | 365 | if (!options.image) { 366 | return reject('Image is not defined') 367 | } 368 | 369 | if (typeof options.imageOptions.crossOrigin === 'string') { 370 | image.crossOrigin = options.imageOptions.crossOrigin 371 | } 372 | 373 | this._image = image 374 | image.onload = (): void => { 375 | resolve() 376 | } 377 | image.src = options.image 378 | }) 379 | } 380 | 381 | drawImage ({ 382 | width, 383 | height, 384 | count, 385 | dotSize 386 | }: { 387 | width: number; 388 | height: number; 389 | count: number; 390 | dotSize: number; 391 | }): void { 392 | const canvasContext = this.context 393 | 394 | if (!canvasContext) { 395 | throw 'canvasContext is not defined' 396 | } 397 | 398 | if (!this._image) { 399 | throw 'image is not defined' 400 | } 401 | 402 | const options = this._options 403 | const xBeginning = Math.floor((options.width - count * dotSize) / 2) 404 | const yBeginning = Math.floor((options.height - count * dotSize) / 2) 405 | const dx = xBeginning + options.imageOptions.margin + (count * dotSize - width) / 2 406 | const dy = yBeginning + options.imageOptions.margin + (count * dotSize - height) / 2 407 | const dw = width - options.imageOptions.margin * 2 408 | const dh = height - options.imageOptions.margin * 2 409 | 410 | canvasContext.drawImage(this._image, dx, dy, dw < 0 ? 0 : dw, dh < 0 ? 0 : dh) 411 | } 412 | 413 | _createGradient ({ 414 | context, 415 | options, 416 | additionalRotation, 417 | x, 418 | y, 419 | size 420 | }: { 421 | context: CanvasRenderingContext2D; 422 | options: Gradient; 423 | additionalRotation: number; 424 | x: number; 425 | y: number; 426 | size: number; 427 | }): CanvasGradient { 428 | let gradient 429 | 430 | if (options.type === gradientTypes.radial) { 431 | gradient = context.createRadialGradient(x + size / 2, y + size / 2, 0, x + size / 2, y + size / 2, size / 2) 432 | } else { 433 | const rotation = ((options.rotation || 0) + additionalRotation) % (2 * Math.PI) 434 | const positiveRotation = (rotation + 2 * Math.PI) % (2 * Math.PI) 435 | let x0 = x + size / 2 436 | let y0 = y + size / 2 437 | let x1 = x + size / 2 438 | let y1 = y + size / 2 439 | 440 | if ( 441 | (positiveRotation >= 0 && positiveRotation <= 0.25 * Math.PI) || 442 | (positiveRotation > 1.75 * Math.PI && positiveRotation <= 2 * Math.PI) 443 | ) { 444 | x0 = x0 - size / 2 445 | y0 = y0 - (size / 2) * Math.tan(rotation) 446 | x1 = x1 + size / 2 447 | y1 = y1 + (size / 2) * Math.tan(rotation) 448 | } else if (positiveRotation > 0.25 * Math.PI && positiveRotation <= 0.75 * Math.PI) { 449 | y0 = y0 - size / 2 450 | x0 = x0 - size / 2 / Math.tan(rotation) 451 | y1 = y1 + size / 2 452 | x1 = x1 + size / 2 / Math.tan(rotation) 453 | } else if (positiveRotation > 0.75 * Math.PI && positiveRotation <= 1.25 * Math.PI) { 454 | x0 = x0 + size / 2 455 | y0 = y0 + (size / 2) * Math.tan(rotation) 456 | x1 = x1 - size / 2 457 | y1 = y1 - (size / 2) * Math.tan(rotation) 458 | } else if (positiveRotation > 1.25 * Math.PI && positiveRotation <= 1.75 * Math.PI) { 459 | y0 = y0 + size / 2 460 | x0 = x0 + size / 2 / Math.tan(rotation) 461 | y1 = y1 - size / 2 462 | x1 = x1 - size / 2 / Math.tan(rotation) 463 | } 464 | 465 | gradient = context.createLinearGradient(Math.round(x0), Math.round(y0), Math.round(x1), Math.round(y1)) 466 | } 467 | 468 | return gradient 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /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 defaultOptions, { Options, RequiredOptions } from './QROptions' 6 | import sanitizeOptions from '../tools/sanitizeOptions' 7 | import { Extension, QRCode } from '../types' 8 | import qrcode from 'qrcode-generator' 9 | 10 | type DownloadOptions = { 11 | name?: string; 12 | extension?: Extension; 13 | }; 14 | 15 | export default class QRCodeStyling { 16 | _options: RequiredOptions; 17 | _container?: HTMLElement; 18 | _canvas?: QRCanvas; 19 | _qr?: QRCode; 20 | _drawingPromise?: 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 | update (options?: Partial): void { 34 | QRCodeStyling._clearContainer(this._container) 35 | this._options = options ? sanitizeOptions(mergeDeep(this._options, options) as RequiredOptions) : this._options 36 | 37 | if (!this._options.data) { 38 | return 39 | } 40 | 41 | this._qr = qrcode(this._options.qrOptions.typeNumber, this._options.qrOptions.errorCorrectionLevel) 42 | this._qr.addData(this._options.data, this._options.qrOptions.mode || getMode(this._options.data)) 43 | this._qr.make() 44 | this._canvas = new QRCanvas(this._options) 45 | this._drawingPromise = this._canvas.drawQR(this._qr) 46 | this.append(this._container) 47 | } 48 | 49 | append (container?: HTMLElement): void { 50 | if (!container) { 51 | return 52 | } 53 | 54 | if (typeof container.appendChild !== 'function') { 55 | // eslint-disable-next-line no-throw-literal 56 | throw 'Container should be a single DOM node' 57 | } 58 | 59 | if (this._canvas) { 60 | container.appendChild(this._canvas.getCanvas()) 61 | } 62 | 63 | this._container = container 64 | } 65 | 66 | async getImageUrl (extension: string): Promise { 67 | if (!this._drawingPromise) return '' 68 | 69 | const getImageUrl = await this._drawingPromise 70 | if (getImageUrl === undefined) { 71 | if (!this._canvas) return '' 72 | const data = this._canvas.getCanvas().toDataURL(`image/${extension}`) 73 | return data 74 | } 75 | return '' 76 | } 77 | 78 | download (downloadOptions?: Partial): void { 79 | if (!this._drawingPromise) return 80 | 81 | void this._drawingPromise.then(() => { 82 | if (!this._canvas) return 83 | const opt = downloadOptions 84 | const extension = opt.extension || 'png' 85 | const name = opt.name || 'qr' 86 | const data = this._canvas.getCanvas().toDataURL(`image/${extension}`) 87 | downloadURI(data, `${name}.${extension}`) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/core/QRCornerDot.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import cornerDotTypes from '../constants/cornerDotTypes' 3 | import { CornerDotType } from '../types' 4 | 5 | type DrawArgs = { 6 | x: number; 7 | y: number; 8 | size: number; 9 | context: CanvasRenderingContext2D; 10 | rotation: number; 11 | }; 12 | 13 | type BasicFigureDrawArgs = { 14 | x: number; 15 | y: number; 16 | size: number; 17 | context: CanvasRenderingContext2D; 18 | rotation: number; 19 | }; 20 | 21 | type RotateFigureArgs = { 22 | x: number; 23 | y: number; 24 | size: number; 25 | context: CanvasRenderingContext2D; 26 | rotation: number; 27 | draw: () => void; 28 | }; 29 | 30 | export default class QRCornerDot { 31 | _context: CanvasRenderingContext2D; 32 | _type: CornerDotType; 33 | 34 | constructor ({ context, type }: { context: CanvasRenderingContext2D; type: CornerDotType }) { 35 | this._context = context 36 | this._type = type 37 | } 38 | 39 | draw (x: number, y: number, size: number, rotation: number): void { 40 | const context = this._context 41 | const type = this._type 42 | let drawFunction 43 | 44 | switch (type) { 45 | case cornerDotTypes.square: 46 | drawFunction = this._drawSquare 47 | break 48 | case cornerDotTypes.dot: 49 | default: 50 | drawFunction = this._drawDot 51 | } 52 | 53 | drawFunction.call(this, { x, y, size, context, rotation }) 54 | } 55 | 56 | _rotateFigure ({ x, y, size, context, rotation, draw }: RotateFigureArgs): void { 57 | const cx = x + size / 2 58 | const cy = y + size / 2 59 | 60 | context.translate(cx, cy) 61 | rotation && context.rotate(rotation) 62 | draw() 63 | context.closePath() 64 | rotation && context.rotate(-rotation) 65 | context.translate(-cx, -cy) 66 | } 67 | 68 | _basicDot (args: BasicFigureDrawArgs): void { 69 | const { size, context } = args 70 | 71 | this._rotateFigure({ 72 | ...args, 73 | draw: () => { 74 | context.arc(0, 0, size / 2, 0, Math.PI * 2) 75 | } 76 | }) 77 | } 78 | 79 | _basicSquare (args: BasicFigureDrawArgs): void { 80 | const { size, context } = args 81 | 82 | this._rotateFigure({ 83 | ...args, 84 | draw: () => { 85 | context.rect(-size / 2, -size / 2, size, size) 86 | } 87 | }) 88 | } 89 | 90 | _drawDot ({ x, y, size, context, rotation }: DrawArgs): void { 91 | this._basicDot({ x, y, size, context, rotation }) 92 | } 93 | 94 | _drawSquare ({ x, y, size, context, rotation }: DrawArgs): void { 95 | this._basicSquare({ x, y, size, context, rotation }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/core/QRCornerSquare.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import cornerSquareTypes from '../constants/cornerSquareTypes' 3 | import { CornerSquareType } from '../types' 4 | 5 | type DrawArgs = { 6 | x: number; 7 | y: number; 8 | size: number; 9 | context: CanvasRenderingContext2D; 10 | rotation: number; 11 | }; 12 | 13 | type BasicFigureDrawArgs = { 14 | x: number; 15 | y: number; 16 | size: number; 17 | context: CanvasRenderingContext2D; 18 | rotation: number; 19 | }; 20 | 21 | type RotateFigureArgs = { 22 | x: number; 23 | y: number; 24 | size: number; 25 | context: CanvasRenderingContext2D; 26 | rotation: number; 27 | draw: () => void; 28 | }; 29 | 30 | export default class QRCornerSquare { 31 | _context: CanvasRenderingContext2D; 32 | _type: CornerSquareType; 33 | 34 | constructor ({ context, type }: { context: CanvasRenderingContext2D; type: CornerSquareType }) { 35 | this._context = context 36 | this._type = type 37 | } 38 | 39 | draw (x: number, y: number, size: number, rotation: number): void { 40 | const context = this._context 41 | const type = this._type 42 | let drawFunction 43 | 44 | switch (type) { 45 | case cornerSquareTypes.square: 46 | drawFunction = this._drawSquare 47 | break 48 | case cornerSquareTypes.extraRounded: 49 | drawFunction = this._drawExtraRounded 50 | break 51 | case cornerSquareTypes.dot: 52 | default: 53 | drawFunction = this._drawDot 54 | } 55 | 56 | drawFunction.call(this, { x, y, size, context, rotation }) 57 | } 58 | 59 | _rotateFigure ({ x, y, size, context, rotation, draw }: RotateFigureArgs): void { 60 | const cx = x + size / 2 61 | const cy = y + size / 2 62 | 63 | context.translate(cx, cy) 64 | rotation && context.rotate(rotation) 65 | draw() 66 | context.closePath() 67 | rotation && context.rotate(-rotation) 68 | context.translate(-cx, -cy) 69 | } 70 | 71 | _basicDot (args: BasicFigureDrawArgs): void { 72 | const { size, context } = args 73 | const dotSize = size / 7 74 | 75 | this._rotateFigure({ 76 | ...args, 77 | draw: () => { 78 | context.arc(0, 0, size / 2, 0, Math.PI * 2) 79 | context.arc(0, 0, size / 2 - dotSize, 0, Math.PI * 2) 80 | } 81 | }) 82 | } 83 | 84 | _basicSquare (args: BasicFigureDrawArgs): void { 85 | const { size, context } = args 86 | const dotSize = size / 7 87 | 88 | this._rotateFigure({ 89 | ...args, 90 | draw: () => { 91 | context.rect(-size / 2, -size / 2, size, size) 92 | context.rect(-size / 2 + dotSize, -size / 2 + dotSize, size - 2 * dotSize, size - 2 * dotSize) 93 | } 94 | }) 95 | } 96 | 97 | _basicExtraRounded (args: BasicFigureDrawArgs): void { 98 | const { size, context } = args 99 | const dotSize = size / 7 100 | 101 | this._rotateFigure({ 102 | ...args, 103 | draw: () => { 104 | context.arc(-dotSize, -dotSize, 2.5 * dotSize, Math.PI, -Math.PI / 2) 105 | context.lineTo(dotSize, -3.5 * dotSize) 106 | context.arc(dotSize, -dotSize, 2.5 * dotSize, -Math.PI / 2, 0) 107 | context.lineTo(3.5 * dotSize, -dotSize) 108 | context.arc(dotSize, dotSize, 2.5 * dotSize, 0, Math.PI / 2) 109 | context.lineTo(-dotSize, 3.5 * dotSize) 110 | context.arc(-dotSize, dotSize, 2.5 * dotSize, Math.PI / 2, Math.PI) 111 | context.lineTo(-3.5 * dotSize, -dotSize) 112 | 113 | context.arc(-dotSize, -dotSize, 1.5 * dotSize, Math.PI, -Math.PI / 2) 114 | context.lineTo(dotSize, -2.5 * dotSize) 115 | context.arc(dotSize, -dotSize, 1.5 * dotSize, -Math.PI / 2, 0) 116 | context.lineTo(2.5 * dotSize, -dotSize) 117 | context.arc(dotSize, dotSize, 1.5 * dotSize, 0, Math.PI / 2) 118 | context.lineTo(-dotSize, 2.5 * dotSize) 119 | context.arc(-dotSize, dotSize, 1.5 * dotSize, Math.PI / 2, Math.PI) 120 | context.lineTo(-2.5 * dotSize, -dotSize) 121 | } 122 | }) 123 | } 124 | 125 | _drawDot ({ x, y, size, context, rotation }: DrawArgs): void { 126 | this._basicDot({ x, y, size, context, rotation }) 127 | } 128 | 129 | _drawSquare ({ x, y, size, context, rotation }: DrawArgs): void { 130 | this._basicSquare({ x, y, size, context, rotation }) 131 | } 132 | 133 | _drawExtraRounded ({ x, y, size, context, rotation }: DrawArgs): void { 134 | this._basicExtraRounded({ x, y, size, context, rotation }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/core/QRDot.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/unbound-method */ 2 | import dotTypes from '../constants/dotTypes' 3 | import { DotType } from '../types' 4 | 5 | type GetNeighbor = (x: number, y: number) => boolean; 6 | type DrawArgs = { 7 | x: number; 8 | y: number; 9 | size: number; 10 | context: CanvasRenderingContext2D; 11 | getNeighbor: GetNeighbor; 12 | }; 13 | 14 | type BasicFigureDrawArgs = { 15 | x: number; 16 | y: number; 17 | size: number; 18 | context: CanvasRenderingContext2D; 19 | rotation: number; 20 | }; 21 | 22 | type RotateFigureArgs = { 23 | x: number; 24 | y: number; 25 | size: number; 26 | context: CanvasRenderingContext2D; 27 | rotation: number; 28 | draw: () => void; 29 | }; 30 | 31 | export default class QRDot { 32 | _context: CanvasRenderingContext2D; 33 | _type: DotType; 34 | 35 | constructor ({ context, type }: { context: CanvasRenderingContext2D; type: DotType }) { 36 | this._context = context 37 | this._type = type 38 | } 39 | 40 | draw (x: number, y: number, size: number, getNeighbor: GetNeighbor): void { 41 | const context = this._context 42 | const type = this._type 43 | let drawFunction 44 | 45 | switch (type) { 46 | case dotTypes.dots: 47 | drawFunction = this._drawDot 48 | break 49 | case dotTypes.classy: 50 | drawFunction = this._drawClassy 51 | break 52 | case dotTypes.classyRounded: 53 | drawFunction = this._drawClassyRounded 54 | break 55 | case dotTypes.rounded: 56 | drawFunction = this._drawRounded 57 | break 58 | case dotTypes.extraRounded: 59 | drawFunction = this._drawExtraRounded 60 | break 61 | case dotTypes.square: 62 | default: 63 | drawFunction = this._drawSquare 64 | } 65 | 66 | drawFunction.call(this, { x, y, size, context, getNeighbor }) 67 | } 68 | 69 | _rotateFigure ({ x, y, size, context, rotation, draw }: RotateFigureArgs): void { 70 | const cx = x + size / 2 71 | const cy = y + size / 2 72 | 73 | context.translate(cx, cy) 74 | rotation && context.rotate(rotation) 75 | draw() 76 | context.closePath() 77 | rotation && context.rotate(-rotation) 78 | context.translate(-cx, -cy) 79 | } 80 | 81 | _basicDot (args: BasicFigureDrawArgs): void { 82 | const { size, context } = args 83 | 84 | this._rotateFigure({ 85 | ...args, 86 | draw: () => { 87 | context.arc(0, 0, size / 2, 0, Math.PI * 2) 88 | } 89 | }) 90 | } 91 | 92 | _basicSquare (args: BasicFigureDrawArgs): void { 93 | const { size, context } = args 94 | 95 | this._rotateFigure({ 96 | ...args, 97 | draw: () => { 98 | context.rect(-size / 2, -size / 2, size, size) 99 | } 100 | }) 101 | } 102 | 103 | // if rotation === 0 - right side is rounded 104 | _basicSideRounded (args: BasicFigureDrawArgs): void { 105 | const { size, context } = args 106 | 107 | this._rotateFigure({ 108 | ...args, 109 | draw: () => { 110 | context.arc(0, 0, size / 2, -Math.PI / 2, Math.PI / 2) 111 | context.lineTo(-size / 2, size / 2) 112 | context.lineTo(-size / 2, -size / 2) 113 | context.lineTo(0, -size / 2) 114 | } 115 | }) 116 | } 117 | 118 | // if rotation === 0 - top right corner is rounded 119 | _basicCornerRounded (args: BasicFigureDrawArgs): void { 120 | const { size, context } = args 121 | 122 | this._rotateFigure({ 123 | ...args, 124 | draw: () => { 125 | context.arc(0, 0, size / 2, -Math.PI / 2, 0) 126 | context.lineTo(size / 2, size / 2) 127 | context.lineTo(-size / 2, size / 2) 128 | context.lineTo(-size / 2, -size / 2) 129 | context.lineTo(0, -size / 2) 130 | } 131 | }) 132 | } 133 | 134 | // if rotation === 0 - top right corner is rounded 135 | _basicCornerExtraRounded (args: BasicFigureDrawArgs): void { 136 | const { size, context } = args 137 | 138 | this._rotateFigure({ 139 | ...args, 140 | draw: () => { 141 | context.arc(-size / 2, size / 2, size, -Math.PI / 2, 0) 142 | context.lineTo(-size / 2, size / 2) 143 | context.lineTo(-size / 2, -size / 2) 144 | } 145 | }) 146 | } 147 | 148 | _basicCornersRounded (args: BasicFigureDrawArgs): void { 149 | const { size, context } = args 150 | 151 | this._rotateFigure({ 152 | ...args, 153 | draw: () => { 154 | context.arc(0, 0, size / 2, -Math.PI / 2, 0) 155 | context.lineTo(size / 2, size / 2) 156 | context.lineTo(0, size / 2) 157 | context.arc(0, 0, size / 2, Math.PI / 2, Math.PI) 158 | context.lineTo(-size / 2, -size / 2) 159 | context.lineTo(0, -size / 2) 160 | } 161 | }) 162 | } 163 | 164 | _basicCornersExtraRounded (args: BasicFigureDrawArgs): void { 165 | const { size, context } = args 166 | 167 | this._rotateFigure({ 168 | ...args, 169 | draw: () => { 170 | context.arc(-size / 2, size / 2, size, -Math.PI / 2, 0) 171 | context.arc(size / 2, -size / 2, size, Math.PI / 2, Math.PI) 172 | } 173 | }) 174 | } 175 | 176 | _drawDot ({ x, y, size, context }: DrawArgs): void { 177 | this._basicDot({ x, y, size, context, rotation: 0 }) 178 | } 179 | 180 | _drawSquare ({ x, y, size, context }: DrawArgs): void { 181 | this._basicSquare({ x, y, size, context, rotation: 0 }) 182 | } 183 | 184 | _drawRounded ({ x, y, size, context, getNeighbor }: DrawArgs): void { 185 | const leftNeighbor = +getNeighbor(-1, 0) 186 | const rightNeighbor = +getNeighbor(1, 0) 187 | const topNeighbor = +getNeighbor(0, -1) 188 | const bottomNeighbor = +getNeighbor(0, 1) 189 | 190 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor 191 | 192 | if (neighborsCount === 0) { 193 | this._basicDot({ x, y, size, context, rotation: 0 }) 194 | return 195 | } 196 | 197 | if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) { 198 | this._basicSquare({ x, y, size, context, rotation: 0 }) 199 | return 200 | } 201 | 202 | if (neighborsCount === 2) { 203 | let rotation = 0 204 | 205 | if (leftNeighbor && topNeighbor) { 206 | rotation = Math.PI / 2 207 | } else if (topNeighbor && rightNeighbor) { 208 | rotation = Math.PI 209 | } else if (rightNeighbor && bottomNeighbor) { 210 | rotation = -Math.PI / 2 211 | } 212 | 213 | this._basicCornerRounded({ x, y, size, context, rotation }) 214 | return 215 | } 216 | 217 | if (neighborsCount === 1) { 218 | let rotation = 0 219 | 220 | if (topNeighbor) { 221 | rotation = Math.PI / 2 222 | } else if (rightNeighbor) { 223 | rotation = Math.PI 224 | } else if (bottomNeighbor) { 225 | rotation = -Math.PI / 2 226 | } 227 | 228 | this._basicSideRounded({ x, y, size, context, rotation }) 229 | } 230 | } 231 | 232 | _drawExtraRounded ({ x, y, size, context, getNeighbor }: DrawArgs): void { 233 | const leftNeighbor = +getNeighbor(-1, 0) 234 | const rightNeighbor = +getNeighbor(1, 0) 235 | const topNeighbor = +getNeighbor(0, -1) 236 | const bottomNeighbor = +getNeighbor(0, 1) 237 | 238 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor 239 | 240 | if (neighborsCount === 0) { 241 | this._basicDot({ x, y, size, context, rotation: 0 }) 242 | return 243 | } 244 | 245 | if (neighborsCount > 2 || (leftNeighbor && rightNeighbor) || (topNeighbor && bottomNeighbor)) { 246 | this._basicSquare({ x, y, size, context, rotation: 0 }) 247 | return 248 | } 249 | 250 | if (neighborsCount === 2) { 251 | let rotation = 0 252 | 253 | if (leftNeighbor && topNeighbor) { 254 | rotation = Math.PI / 2 255 | } else if (topNeighbor && rightNeighbor) { 256 | rotation = Math.PI 257 | } else if (rightNeighbor && bottomNeighbor) { 258 | rotation = -Math.PI / 2 259 | } 260 | 261 | this._basicCornerExtraRounded({ x, y, size, context, rotation }) 262 | return 263 | } 264 | 265 | if (neighborsCount === 1) { 266 | let rotation = 0 267 | 268 | if (topNeighbor) { 269 | rotation = Math.PI / 2 270 | } else if (rightNeighbor) { 271 | rotation = Math.PI 272 | } else if (bottomNeighbor) { 273 | rotation = -Math.PI / 2 274 | } 275 | 276 | this._basicSideRounded({ x, y, size, context, rotation }) 277 | } 278 | } 279 | 280 | _drawClassy ({ x, y, size, context, getNeighbor }: DrawArgs): void { 281 | const leftNeighbor = +getNeighbor(-1, 0) 282 | const rightNeighbor = +getNeighbor(1, 0) 283 | const topNeighbor = +getNeighbor(0, -1) 284 | const bottomNeighbor = +getNeighbor(0, 1) 285 | 286 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor 287 | 288 | if (neighborsCount === 0) { 289 | this._basicCornersRounded({ x, y, size, context, rotation: Math.PI / 2 }) 290 | return 291 | } 292 | 293 | if (!leftNeighbor && !topNeighbor) { 294 | this._basicCornerRounded({ x, y, size, context, rotation: -Math.PI / 2 }) 295 | return 296 | } 297 | 298 | if (!rightNeighbor && !bottomNeighbor) { 299 | this._basicCornerRounded({ x, y, size, context, rotation: Math.PI / 2 }) 300 | return 301 | } 302 | 303 | this._basicSquare({ x, y, size, context, rotation: 0 }) 304 | } 305 | 306 | _drawClassyRounded ({ x, y, size, context, getNeighbor }: DrawArgs): void { 307 | const leftNeighbor = +getNeighbor(-1, 0) 308 | const rightNeighbor = +getNeighbor(1, 0) 309 | const topNeighbor = +getNeighbor(0, -1) 310 | const bottomNeighbor = +getNeighbor(0, 1) 311 | 312 | const neighborsCount = leftNeighbor + rightNeighbor + topNeighbor + bottomNeighbor 313 | 314 | if (neighborsCount === 0) { 315 | this._basicCornersRounded({ x, y, size, context, rotation: Math.PI / 2 }) 316 | return 317 | } 318 | 319 | if (!leftNeighbor && !topNeighbor) { 320 | this._basicCornerExtraRounded({ x, y, size, context, rotation: -Math.PI / 2 }) 321 | return 322 | } 323 | 324 | if (!rightNeighbor && !bottomNeighbor) { 325 | this._basicCornerExtraRounded({ x, y, size, context, rotation: Math.PI / 2 }) 326 | return 327 | } 328 | 329 | this._basicSquare({ x, y, size, context, rotation: 0 }) 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/core/QROptions.ts: -------------------------------------------------------------------------------- 1 | import qrTypes from '../constants/qrTypes' 2 | import errorCorrectionLevels from '../constants/errorCorrectionLevels' 3 | import { 4 | DotType, 5 | GradientType, 6 | CornerSquareType, 7 | CornerDotType, 8 | TypeNumber, 9 | ErrorCorrectionLevel, 10 | Mode 11 | } from '../types' 12 | 13 | export type Gradient = { 14 | type: GradientType; 15 | rotation?: number; 16 | colorStops: { 17 | offset: number; 18 | color: string; 19 | }[]; 20 | }; 21 | 22 | export type Options = { 23 | width?: number; 24 | height?: number; 25 | data?: string; 26 | image?: string; 27 | qrOptions?: { 28 | typeNumber?: TypeNumber; 29 | mode?: Mode; 30 | errorCorrectionLevel?: ErrorCorrectionLevel; 31 | }; 32 | imageOptions?: { 33 | hideBackgroundDots?: boolean; 34 | imageSize?: number; 35 | crossOrigin?: string; 36 | margin?: number; 37 | }; 38 | dotsOptions?: { 39 | type?: DotType; 40 | color?: string; 41 | gradient?: Gradient; 42 | }; 43 | cornersSquareOptions?: { 44 | type?: CornerSquareType; 45 | color?: string; 46 | gradient?: Gradient; 47 | }; 48 | cornersDotOptions?: { 49 | type?: CornerDotType; 50 | color?: string; 51 | gradient?: Gradient; 52 | }; 53 | backgroundOptions?: { 54 | color?: string; 55 | gradient?: Gradient; 56 | }; 57 | }; 58 | 59 | export interface RequiredOptions extends Options { 60 | width: number; 61 | height: number; 62 | margin: number; 63 | data: string; 64 | qrOptions: { 65 | typeNumber: TypeNumber; 66 | mode?: Mode; 67 | errorCorrectionLevel: ErrorCorrectionLevel; 68 | }; 69 | imageOptions: { 70 | hideBackgroundDots: boolean; 71 | imageSize: number; 72 | crossOrigin?: string; 73 | margin: number; 74 | }; 75 | dotsOptions: { 76 | type: DotType; 77 | color: string; 78 | gradient?: Gradient; 79 | }; 80 | backgroundOptions: { 81 | color: string; 82 | gradient?: Gradient; 83 | }; 84 | } 85 | 86 | const defaultOptions: RequiredOptions = { 87 | width: 300, 88 | height: 300, 89 | data: '', 90 | margin: 0, 91 | qrOptions: { 92 | typeNumber: qrTypes[0], 93 | mode: undefined, 94 | errorCorrectionLevel: errorCorrectionLevels.Q 95 | }, 96 | imageOptions: { 97 | hideBackgroundDots: true, 98 | imageSize: 0.4, 99 | crossOrigin: undefined, 100 | margin: 0 101 | }, 102 | dotsOptions: { 103 | type: 'square', 104 | color: '#000' 105 | }, 106 | backgroundOptions: { 107 | color: '#fff' 108 | } 109 | } 110 | 111 | export default defaultOptions 112 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import VueQr3 from './vue3-qr-code-styling.vue' 2 | 3 | export default VueQr3 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VueQr3 from "./vue3-qr-code-styling.vue"; 2 | 3 | export default VueQr3; 4 | -------------------------------------------------------------------------------- /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.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.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 | import { UnknownObject } from '../types' 3 | 4 | const isObject = (obj: Record): boolean => !!obj && typeof obj === 'object' && !Array.isArray(obj) 5 | 6 | export default function mergeDeep (target: UnknownObject, ...sources: UnknownObject[]): UnknownObject { 7 | if (!sources.length) return target 8 | const source = sources.shift() 9 | if (source === undefined || !isObject(target) || !isObject(source)) return target 10 | target = { ...target } 11 | Object.keys(source).forEach((key: string): void => { 12 | const targetValue = target[key] 13 | const sourceValue = source[key] 14 | 15 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { 16 | target[key] = sourceValue 17 | } else if (isObject(targetValue) && isObject(sourceValue)) { 18 | target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue) 19 | } else { 20 | target[key] = sourceValue 21 | } 22 | }) 23 | 24 | return mergeDeep(target, ...sources) 25 | } 26 | -------------------------------------------------------------------------------- /src/tools/sanitizeOptions.ts: -------------------------------------------------------------------------------- 1 | import { Gradient, RequiredOptions } from '../core/QROptions' 2 | 3 | function sanitizeGradient (gradient: Gradient): Gradient { 4 | const newGradient = { ...gradient } 5 | 6 | if (!newGradient.colorStops || !newGradient.colorStops.length) { 7 | // eslint-disable-next-line no-throw-literal 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 = string; 10 | export type GradientType = 'radial' | 'linear'; 11 | 12 | export interface DotTypes { 13 | [key: string]: DotType; 14 | } 15 | 16 | export interface GradientTypes { 17 | [key: string]: GradientType; 18 | } 19 | 20 | export interface CornerDotTypes { 21 | [key: string]: CornerDotType; 22 | } 23 | 24 | export interface CornerSquareTypes { 25 | [key: string]: CornerSquareType; 26 | } 27 | 28 | export type TypeNumber = 29 | | 0 30 | | 1 31 | | 2 32 | | 3 33 | | 4 34 | | 5 35 | | 6 36 | | 7 37 | | 8 38 | | 9 39 | | 10 40 | | 11 41 | | 12 42 | | 13 43 | | 14 44 | | 15 45 | | 16 46 | | 17 47 | | 18 48 | | 19 49 | | 20 50 | | 21 51 | | 22 52 | | 23 53 | | 24 54 | | 25 55 | | 26 56 | | 27 57 | | 28 58 | | 29 59 | | 30 60 | | 31 61 | | 32 62 | | 33 63 | | 34 64 | | 35 65 | | 36 66 | | 37 67 | | 38 68 | | 39 69 | | 40; 70 | 71 | export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'; 72 | export type Mode = 'Numeric' | 'Alphanumeric' | 'Byte' | 'Kanji'; 73 | export interface QRCode { 74 | addData(data: string, mode?: Mode): void; 75 | make(): void; 76 | getModuleCount(): number; 77 | isDark(row: number, col: number): boolean; 78 | createImgTag(cellSize?: number, margin?: number): string; 79 | createSvgTag(cellSize?: number, margin?: number): string; 80 | createSvgTag(opts?: { cellSize?: number; margin?: number; scalable?: boolean }): string; 81 | createDataURL(cellSize?: number, margin?: number): string; 82 | createTableTag(cellSize?: number, margin?: number): string; 83 | createASCII(cellSize?: number, margin?: number): string; 84 | renderTo2dContext(context: CanvasRenderingContext2D, cellSize?: number): void; 85 | } 86 | -------------------------------------------------------------------------------- /src/vue3-qr-code-styling.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 148 | -------------------------------------------------------------------------------- /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 { VueLoaderPlugin } = require('vue-loader') 4 | 5 | 6 | const rootPath = path.resolve(__dirname, "./"); 7 | const srcPath = path.resolve(rootPath, "src"); 8 | const libPath = path.resolve(rootPath, "lib"); 9 | 10 | module.exports = { 11 | entry: srcPath + "/index.ts", 12 | output: { 13 | path: libPath, 14 | filename: "index.js", 15 | library: "VQRCodeStyling", 16 | libraryTarget: "umd", 17 | libraryExport: "default" 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.vue$/, 23 | use: 'vue-loader', 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.ts$/, 28 | loader: 'ts-loader', 29 | exclude: /node_modules/, 30 | options: { 31 | appendTsSuffixTo: [/\.vue$/], 32 | } 33 | }, 34 | ] 35 | }, 36 | plugins: [new VueLoaderPlugin(), new CleanWebpackPlugin()], 37 | resolve: { 38 | extensions: ['.ts', '.js', '.vue'], 39 | alias: { 40 | 'vue': '@vue/runtime-dom' 41 | } 42 | }, 43 | }; 44 | --------------------------------------------------------------------------------