├── CNAME ├── examples ├── images │ ├── sample1.png │ ├── sample2.png │ └── sample3.png ├── sample-node.js ├── sample-web.html ├── sample-web-svg.html └── editor.html ├── tests └── library.test.ts ├── src ├── index.ts ├── index-browser.ts ├── canvas │ ├── canvas-node.ts │ ├── canvas-browser.ts │ ├── canvas-wrapper.ts │ └── canvas-svg.ts └── qr-builder.ts ├── .github ├── pull_request_template.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── publish-npm.yml │ ├── build-docs.yml │ ├── test-package.yml │ └── count-loc.yml ├── tsconfig.json ├── LICENSE ├── rollup.config.ts ├── package.json ├── .gitignore ├── README.md └── jest.config.js /CNAME: -------------------------------------------------------------------------------- 1 | qart.rofl.wtf -------------------------------------------------------------------------------- /examples/images/sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowmoose/Q-Art-Codes/HEAD/examples/images/sample1.png -------------------------------------------------------------------------------- /examples/images/sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowmoose/Q-Art-Codes/HEAD/examples/images/sample2.png -------------------------------------------------------------------------------- /examples/images/sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowmoose/Q-Art-Codes/HEAD/examples/images/sample3.png -------------------------------------------------------------------------------- /tests/library.test.ts: -------------------------------------------------------------------------------- 1 | import makeQR from "../"; 2 | 3 | describe("Base tests", () => { 4 | it("should create Library", () => { 5 | expect(makeQR).not.toBeUndefined() 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {make as makeQR, setLoaderClass} from './qr-builder'; 2 | import NodeLoader from './canvas/canvas-node'; 3 | 4 | setLoaderClass(NodeLoader); 5 | 6 | 7 | export default makeQR; 8 | 9 | -------------------------------------------------------------------------------- /src/index-browser.ts: -------------------------------------------------------------------------------- 1 | import {make as makeQR, setLoaderClass} from './qr-builder'; 2 | import BrowserLoader from './canvas/canvas-browser'; 3 | 4 | setLoaderClass(BrowserLoader); 5 | 6 | 7 | export default makeQR; 8 | 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Thanks for your contribution! 2 | 3 | Before submitting this PR, please make sure: 4 | 5 | - [ ] Your code builds clean without any errors or warnings, if applicable 6 | - [ ] You have added unit tests, if applicable 7 | -------------------------------------------------------------------------------- /examples/sample-node.js: -------------------------------------------------------------------------------- 1 | const qr = require('../'); 2 | const fs = require('fs'); 3 | 4 | const img = __dirname + '/images/img.png'; 5 | const outPath = __dirname + '/generated-QR.png'; 6 | 7 | console.log(img); 8 | 9 | qr('https://pathofexile.gamepedia.com/Perquil%27s_Toe', img).then(async res => { 10 | console.log('Result:', res); 11 | await res.toStream(fs.createWriteStream(outPath)); 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es5", 5 | "module": "es2015", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "declarationDir": "dist", 15 | "outDir": "dist/lib", 16 | "typeRoots": ["node_modules/@types"], 17 | "resolveJsonModule": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: ShadowMoose 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: TheShadowMoose 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe: 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like: 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered: 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context: 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/canvas/canvas-node.ts: -------------------------------------------------------------------------------- 1 | import CanvasLoader, {CanvasIsh} from "./canvas-wrapper"; 2 | import * as fs from "fs"; 3 | // @ts-ignore 4 | import * as PImage from 'pureimage'; 5 | 6 | /** 7 | * Node Canvas implementation. 8 | */ 9 | export default class NodeLoader extends CanvasLoader { 10 | async loadImage(image: string): Promise { 11 | return PImage.decodePNGFromStream(fs.createReadStream(image)) 12 | } 13 | 14 | makeCanvas(width: number, height: number): CanvasIsh { 15 | return PImage.make(width, height); 16 | } 17 | 18 | async toBlob(_mimeType: string, _quality: number): Promise { 19 | throw Error('Cannot encode canvas to Blob inside Node! Use "toStream" instead.') 20 | } 21 | 22 | toStream(stream: any): void { 23 | return PImage.encodePNGToStream(this.canvas, stream); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/sample-web.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample QR 6 | 7 | 8 | 9 | 10 | 11 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish NPM 2 | on: 3 | release: 4 | types: [released] 5 | 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | registry-url: https://registry.npmjs.org 19 | 20 | - name: Install dependencies 21 | run: yarn install --frozen-lockfile 22 | 23 | - name: Version 24 | run: yarn version --new-version "${GITHUB_REF:10}" --no-git-tag-version 25 | 26 | - name: Build pre-release test 27 | run: yarn build 28 | 29 | - name: Test 30 | run: yarn test --ci 31 | 32 | - name: Publish 33 | run: yarn publish --access public 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master, ] 4 | 5 | name: Build Docs 6 | 7 | jobs: 8 | build-docs: 9 | runs-on: ubuntu-latest 10 | name: Build and test 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v1 14 | 15 | - name: Setup node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '12' 19 | 20 | - name: Install packages 21 | uses: bahmutov/npm-install@v1 22 | 23 | - name: Build Docs 24 | run: | 25 | yarn build-docs 26 | mv ./examples ./docs/ 27 | 28 | - name: Deploy to Web 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./docs/ 33 | publish_branch: gh-pages 34 | #user_name: "build-bot" 35 | #user_email: "github_bot@github.com" 36 | allow_empty_commit: false 37 | #force_orphan: true 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug: 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce: 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior: 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots: 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ### Desktop (please complete the following information): 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | ### Smartphone (please complete the following information): 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | ## Additional context: 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Test Package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x, 13.x, 14.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | 17 | - name: Setup Node.js (${{ matrix.node-version }}) 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | registry-url: https://registry.npmjs.org 22 | 23 | - name: Install dependencies 24 | run: yarn install --frozen-lockfile 25 | 26 | - name: Build 27 | run: yarn build 28 | 29 | - name: Test 30 | run: yarn test --ci 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v1 34 | with: 35 | # token: ${{ secrets.CODECOV_TOKEN }} # Not needed for public repos. 36 | directory: ./coverage/ 37 | flags: unittests 38 | env_vars: ${{ matrix.node-version }} 39 | name: codecov-umbrella 40 | fail_ci_if_error: false 41 | verbose: false 42 | -------------------------------------------------------------------------------- /.github/workflows/count-loc.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | name: Count Lines of Code 5 | 6 | jobs: 7 | count_LoC: 8 | runs-on: ubuntu-latest 9 | name: Count Lines of Code 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | 14 | - name: Count Lines of Code 15 | uses: shadowmoose/GHA-LoC-Badge@1.0.0 16 | id: badge 17 | with: 18 | debug: true 19 | directory: ./ 20 | badge: ./output/loc-badge.svg 21 | ignore: '*.lock' 22 | 23 | - name: Print the output 24 | run: | 25 | echo "Scanned: ${{ steps.badge.outputs.counted_files }}"; 26 | echo "Line Count: ${{ steps.badge.outputs.total_lines }}"; 27 | 28 | - name: Deploy to image-data branch 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | publish_dir: ./output 32 | publish_branch: image-data 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | user_name: 'github-actions[bot]' 35 | user_email: 'github-actions[bot]@users.noreply.github.com' 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mike 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 | -------------------------------------------------------------------------------- /examples/sample-web-svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample SVG QR 6 | 7 | 8 | 9 | 10 | 11 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/canvas/canvas-browser.ts: -------------------------------------------------------------------------------- 1 | import CanvasLoader, {CanvasIsh} from "./canvas-wrapper"; 2 | 3 | export default class BrowserLoader extends CanvasLoader { 4 | async loadImage(image: string): Promise { 5 | return new Promise((res, rej) => { 6 | const img = new Image(); 7 | img.crossOrigin = "Anonymous"; 8 | img.onload = () => { 9 | res(img); 10 | }; 11 | img.onerror = rej; 12 | img.src = image; 13 | }); 14 | } 15 | 16 | makeCanvas(width: number, height: number): CanvasIsh { 17 | let ret; 18 | if (typeof OffscreenCanvas !== 'undefined') { 19 | ret = new OffscreenCanvas(width, height); 20 | } else { 21 | const canv = document.createElement('canvas'); 22 | canv.width = width; 23 | canv.height = height; 24 | ret = canv; 25 | } 26 | 27 | return ret; 28 | } 29 | 30 | async toBlob(mimeType: string, quality: number): Promise { 31 | let blob: any; 32 | if (this.canvas.convertToBlob) { 33 | blob = await this.canvas.convertToBlob({type: mimeType, quality}); 34 | } else if (this.canvas.toBlob){ 35 | blob = await new Promise(res => { 36 | // @ts-ignore 37 | canvas.toBlob(res, mimeType, quality); 38 | }) 39 | } 40 | 41 | return blob; 42 | } 43 | 44 | toStream(_stream: any): void { 45 | throw Error('Cannot encode canvas to stream inside the browser! Use "toBlob" instead.') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import json from "rollup-plugin-json"; 2 | import typescript from "rollup-plugin-typescript2"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | import resolve from "rollup-plugin-node-resolve"; 5 | import uglify from "@lopatnov/rollup-plugin-uglify"; 6 | import nodePolyfills from 'rollup-plugin-node-polyfills'; 7 | 8 | import pkg from './package.json'; 9 | 10 | export default [ 11 | { 12 | input: `src/${pkg.buildEntryPoint}.ts`, 13 | output: [ 14 | { // NodeJS (or generic) Package format, for standard import: 15 | file: pkg.main, 16 | format: "umd", 17 | name: pkg.umdName, 18 | sourcemap: true 19 | }, 20 | { // Newer, cleaner export (ESM) for bundlers that support ES6. 21 | file: pkg.module, 22 | format: "es", 23 | sourcemap: true 24 | } 25 | ], 26 | external: [ 27 | ...Object.keys(pkg.devDependencies || {}), 28 | ...Object.keys(pkg.peerDependencies || {}), 29 | ...Object.keys(pkg.dependencies || {}), 30 | 'fs' 31 | ], 32 | plugins: [ 33 | json(), 34 | typescript({ 35 | typescript: require("typescript") 36 | }), 37 | resolve({ preferBuiltins: true, browser: false }), 38 | commonjs(), 39 | ] 40 | }, 41 | { 42 | input: `src/${pkg.buildEntryPoint}-browser.ts`, 43 | output: { // Build minified version for the Browser, including all polyfills for Node-specific libraries: 44 | file: `dist/${pkg.buildEntryPoint}-browser.min.js`, 45 | name: pkg.umdName, 46 | format: "umd", 47 | sourcemap: true 48 | }, 49 | external: [ 50 | ...Object.keys(pkg.devDependencies || {}), 51 | ...Object.keys(pkg.peerDependencies || {}) 52 | ], 53 | plugins: [ 54 | json(), 55 | typescript({ 56 | typescript: require("typescript") 57 | }), 58 | resolve({ preferBuiltins: true, browser: true }), 59 | commonjs(), 60 | nodePolyfills(), 61 | uglify() 62 | ] 63 | } 64 | ]; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qart-codes", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "author": "ShadowMoose", 6 | "description": "Create beautiful QR codes with custom images, colors, and sizes.", 7 | "homepage": "https://github.com/shadowmoose/Q-Art-Codes", 8 | "keywords": [ 9 | "qr", 10 | "code" 11 | ], 12 | "umdName": "makeQR", 13 | "buildEntryPoint": "index", 14 | "main": "dist/index.js", 15 | "module": "dist/index.es.js", 16 | "types": "dist/index.d.ts", 17 | "browser": "dist/index-browser.min.js", 18 | "files": [ 19 | "dist" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/shadowmoose/Q-Art-Codes.git" 24 | }, 25 | "bugs": "https://github.com/shadowmoose/Q-Art-Codes/issues", 26 | "scripts": { 27 | "build": "rollup -c rollup.config.ts", 28 | "watch": "rollup -c rollup.config.ts --watch", 29 | "test": "jest", 30 | "build-docs": "typedoc src --out docs --excludeNotExported --mode modules", 31 | "prepublishOnly": "yarn build" 32 | }, 33 | "devDependencies": { 34 | "@lopatnov/rollup-plugin-uglify": "^2.1.0", 35 | "@types/jest": "^26.0.9", 36 | "@types/node": "^14.0.26", 37 | "@types/qrcode": "^1.3.5", 38 | "@types/rollup-plugin-json": "^3.0.2", 39 | "@types/typescript": "^2.0.0", 40 | "jest": "^26.2.2", 41 | "jest-config": "^26.2.2", 42 | "rollup": "^2.23.1", 43 | "rollup-plugin-commonjs": "^10.1.0", 44 | "rollup-plugin-json": "^4.0.0", 45 | "rollup-plugin-node-polyfills": "^0.2.1", 46 | "rollup-plugin-node-resolve": "^5.2.0", 47 | "rollup-plugin-sourcemaps": "^0.6.2", 48 | "rollup-plugin-typescript2": "^0.27.2", 49 | "terser": "^5.0.0", 50 | "ts-jest": "^26.1.4", 51 | "typedoc": "^0.19.2", 52 | "typescript": "^3.9.7" 53 | }, 54 | "peerDependencies": {}, 55 | "dependencies": { 56 | "pureimage": "^0.2.5", 57 | "qrcode": "^1.4.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | dist/ 4 | docs/ 5 | coverage/ 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | -------------------------------------------------------------------------------- /src/canvas/canvas-wrapper.ts: -------------------------------------------------------------------------------- 1 | import {Stream} from "stream"; 2 | 3 | 4 | /** 5 | * A helper class, which wraps all encoding/decoding of images in Node or the Browser. 6 | */ 7 | export default abstract class CanvasLoader { 8 | protected canvas: CanvasIsh; 9 | constructor(width: number, height: number) { 10 | this.canvas = this.makeCanvas(width, height); 11 | } 12 | 13 | /** 14 | * Build a "Canvas" representation internally. The exact implementation depends on the environment. 15 | * @param width 16 | * @param height 17 | * @hidden 18 | */ 19 | protected abstract makeCanvas (width: number, height: number): CanvasIsh; 20 | 21 | /** 22 | * Export the generated QR code as a Blob. 23 | * 24 | * Note that this only works within a Browser, and will raise an Error otherwise. 25 | * @param mimeType The type of image file to export. eg: "image/png" 26 | * @param quality A float representing the desired image quality (0.95 is a good baseline). 27 | * @see {@link toStream} For running in Node. 28 | */ 29 | abstract toBlob (mimeType: string, quality: number): Promise; 30 | 31 | /** 32 | * Export the generated QR code to an open Stream. 33 | * 34 | * Note that this only works within NodeJS, and will raise an Error otherwise. 35 | * @param stream A Stream, already opened by Node. This can lead to a file, an outgoing HTTP stream, etc. 36 | * @see {@link toStream} For running in Node. 37 | */ 38 | abstract toStream (stream: Stream): void; 39 | 40 | /** 41 | * Load an image, using logic inherent to the environment. 42 | * @param image 43 | * @hidden 44 | */ 45 | abstract loadImage (image: string): Promise; 46 | 47 | toSVG(): string { throw Error('toSVG() is not implemented for this QR Code Type.')} 48 | } 49 | 50 | interface blobCB {(): Blob} 51 | 52 | export interface CanvasIsh { 53 | getContext(arg0: string): CanvasGraphics|null; 54 | width: number; 55 | height: number; 56 | convertToBlob?(opts: any): Promise; 57 | toBlob?(callback: blobCB, ...opts: any): void; 58 | } 59 | 60 | 61 | export interface CanvasGraphics { 62 | fillStyle: any; 63 | fillRect: (x: number, y: number, width: number, height: number) => void; 64 | drawImage: (img: any, x: number, y: number, width: number, height: number, dBoxW: number, dBoxH: number, dWidth: number, dHeight: number) => void; 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/canvas/canvas-svg.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polyfill class that replaces several basic Canvas methods with custom SVG rendering. 3 | */ 4 | import CanvasLoader, {CanvasGraphics, CanvasIsh} from "./canvas-wrapper"; 5 | 6 | export default class CanvasSVG extends CanvasLoader { 7 | constructor(width: number, height: number) { 8 | super(width, height); 9 | } 10 | 11 | makeCanvas(width: number, height: number): CanvasIsh { 12 | return new SVGCanvas(width, height); 13 | } 14 | 15 | loadImage(image: string): Promise { 16 | // @ts-ignore 17 | return this.canvas.loadImage(image); 18 | } 19 | 20 | toBlob(mimeType: string, quality: number): Promise { 21 | throw Error('Blob conversion is not implemented for SVG QR Codes.') 22 | } 23 | 24 | toStream(stream: any): void { 25 | throw Error('toStream is not implemented for SVG QR Codes.') 26 | } 27 | 28 | toSVG() { 29 | // @ts-ignore 30 | return this.canvas.toSVG(); 31 | } 32 | } 33 | 34 | export class SVGCanvas implements CanvasIsh, CanvasGraphics{ 35 | private col: string; 36 | private readonly shapeCache: Record = {}; 37 | private readonly defs: Record = {}; 38 | private readonly shapes: any[] = []; 39 | readonly width: number; 40 | readonly height: number; 41 | 42 | constructor(width: number, height: number) { 43 | this.col = 'white'; 44 | this.width = width; 45 | this.height = height; 46 | } 47 | 48 | getContext() { 49 | return this; 50 | } 51 | 52 | set fillStyle(value: string) { 53 | this.col = value; 54 | } 55 | 56 | fillRect(x: number, y: number, width: number, height: number) { 57 | const key = `${width}-${height}-${this.col}`; 58 | const newID = `s${Object.values(this.defs).length}`; 59 | let rect = this.shapeCache[key]; 60 | if (!rect){ 61 | this.defs[newID] = ``; 62 | this.shapeCache[key] = newID; 63 | rect = newID; 64 | } 65 | this.shapes.push({ rect, x, y }); 66 | } 67 | 68 | /** 69 | * Mocks a dummy image object so the existing render commands work. 70 | * 71 | * @param img 72 | * @return {{img: string, width: number, height: number}} 73 | */ 74 | loadImage(img: string) { 75 | if (typeof img !== 'string') { 76 | throw Error('Invalid image input - SVG Mode only supports pure text background images!'); 77 | } 78 | 79 | return { 80 | width: 1, 81 | height: 1, 82 | img: `${img}`.replace(/<[?\s]*(xml)/gm, (m)=>m.replace('xml', 'misc')) 83 | } 84 | } 85 | 86 | drawImage(imageObj: any, xx: number, yy: number, ow: number, oh: number, dx: number, dy: number, dw: number, dh: number) { 87 | const txt = `${imageObj.img}`; 88 | this.shapes.push({txt}); 89 | } 90 | 91 | toSVG() { 92 | let out = `\n`; 93 | out += '\n' + Object.values(this.defs).join('\n') + '\n'; 94 | out += this.shapes.map(s => s.txt || ``).join('\n'); 95 | return out + "" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Q-Art-Codes [![](https://data.jsdelivr.com/v1/package/npm/qart-codes/badge)](https://www.jsdelivr.com/package/npm/qart-codes) [![npm](https://img.shields.io/npm/v/qart-codes?style=flat-square)](https://www.npmjs.com/package/qart-codes) ![](https://raw.githubusercontent.com/shadowmoose/Q-Art-Codes/image-data/loc-badge.svg) 2 | My own "fancy" browser-and-server-side QR Code generator. 3 | 4 | It generates QR Codes with image backgrounds, supporting any size, data, or color combinations you want. 5 | 6 | This is built in pure JavaScript, so it should run anywhere. A bundle has also been provided for browser use. 7 | 8 | [![](./examples/images/sample1.png) 9 | ![](./examples/images/sample2.png) 10 | ![](./examples/images/sample3.png)](https://shadowmoose.github.io/Q-Art-Codes/examples/editor.html) 11 | 12 | Edit your own image and check out the options [in-browser here](https://shadowmoose.github.io/Q-Art-Codes/examples/editor.html). 13 | 14 | ## Installation: 15 | Simply run `npm i qart-codes` 16 | 17 | ## Use guide: 18 | 19 | ### NodeJS: 20 | ```js 21 | const qr = require('qart-codes'); 22 | const fs = require('fs'); 23 | 24 | qr( 25 | `{"lastName":"ever","firstName":"greatest","employeeID":1337,"online":true}`, // Data to encode - string or binary array. 26 | "C:\\CompanyLogo.png" // The background image to use. 27 | ).then(async res => { 28 | await res.toStream(fs.createWriteStream(outPath)); // Write to a file, or choose another output stream. 29 | }) 30 | ``` 31 | 32 | ### Browser: 33 | ```html 34 | 35 | 36 | 53 | ``` 54 | 55 | 56 | ## Full Generator Options: 57 | This config is the same in the browser and server. 58 | Here are all the options, with sample values: 59 | ```js 60 | const opts = { 61 | qrOpts: { 62 | version: 2, // You may pin the QR version used here, no lower than 2. 63 | errorCorrectionLevel: 'H' // See https://www.npmjs.com/package/qrcode#error-correction-level 64 | }, 65 | size: { 66 | boxSize: 6, // The base size, in px, that each square should take in the grid. 67 | scale: 0.35 // Scale the "small" boxes down to this ratio. 68 | }, 69 | colors: { 70 | dark: 'black', // The color to use for the dark squares. 71 | light: 'white', // The color to use for the light squares. 72 | overlay: 'rgba(0,0,0,0.7)' // If set, cover the background image in a color - this can be used to increase readability. 73 | }, 74 | useSVG: false // See "Encoding SVGs". 75 | } 76 | ``` 77 | 78 | ## Platform Differences: 79 | For the most part, the code runs the same in the browser and in Node. 80 | However, due to limitations in each environment, the result object behaves differently with binary image data. 81 | 82 | + In the Browser, the canvas `toBlob('image/png', 0.95)` accepts an image mimetype, and a quality level. 83 | + It returns a Blob, which can be used to create images in the DOM. 84 | 85 | + In Node, the canvas `toStream(stream)` accepts an output stream, which it writes binary PNG image data to. 86 | + There is no return value for this call, as it is assumed Node will be sending or saving this image. 87 | 88 | 89 | ## Encoding SVGs: 90 | SVG output is tentatively supported in the Browser & Node as of version 2.0.0. 91 | 92 | When `useSVG` is enabled, the output `toSVG()` will always return raw SVG text, no matter the platform running the code. 93 | Additionally, the given background image MUST be a pre-loaded string, containing the background SVG text. 94 | 95 | You may have to do some manual formatting of your input background SVG, if you want things to look a specific way. 96 | The encoder tries to fix some of the more common issues, but it is not able to fix everything automatically. 97 | -------------------------------------------------------------------------------- /src/qr-builder.ts: -------------------------------------------------------------------------------- 1 | import * as QRCode from 'qrcode'; 2 | import {QRCodeOptions} from "qrcode"; 3 | import CanvasLoader from "./canvas/canvas-wrapper"; 4 | import SVGLoader from './canvas/canvas-svg'; 5 | let LoaderClass: CanvasLoader|null = null; // TODO: SVG Loader support. 6 | 7 | /** 8 | * @hidden 9 | * @param newLoader 10 | */ 11 | export const setLoaderClass = (newLoader: any) => { 12 | LoaderClass = newLoader; 13 | } 14 | 15 | export interface QRStyleOpts { 16 | /** 17 | * The Version/error level to use. See [node-qrcode](https://www.npmjs.com/package/qrcode#error-correction-level). 18 | */ 19 | qrOpts?: QRCodeOptions; 20 | /** 21 | * The individual colors used in the QR code. 22 | * All colors can be specified in rgba(), or any other HTML string. 23 | * 24 | * If `overlay` is set to a non-empty string, covers the image with this color to increase contrast. Use rgba(). 25 | */ 26 | colors?: { 27 | dark: string, 28 | light: string, 29 | overlay: string 30 | }; 31 | /** 32 | * Adjust the scaling of the image. 33 | */ 34 | size?: { 35 | /** 36 | * The size per-square in pixels. 37 | */ 38 | boxSize: number, 39 | /** 40 | * The scale to resize data squares. 41 | */ 42 | scale: number 43 | }; 44 | useSVG?: boolean; 45 | } 46 | 47 | /** 48 | * Generates a QR code with an image background. 49 | * 50 | * @param data The data to encode within the image. 51 | * @param backgroundPath A path to the background PNG to use. In browser, should be a URL. 52 | * @param props Customize the QR code using these properties. 53 | * 54 | * @see {@link https://www.npmjs.com/package/qrcode#manual-mode QRCode data structure} 55 | */ 56 | export const make = async(data: string|QRCode.QRCodeSegment[], backgroundPath: string, props?: QRStyleOpts): Promise => { 57 | if (!LoaderClass) throw Error("Loader was not set."); 58 | let { qrOpts, colors, size, useSVG} = props || {}; 59 | qrOpts = qrOpts || {}; 60 | colors = Object.assign({ 61 | dark: 'rgba(0,0,0, 1.0)', 62 | light: 'rgba(255,255,255, 0.75)', 63 | overlay: '' 64 | }, colors||{}); 65 | size = size || {boxSize: 6, scale: 0.35}; 66 | // noinspection JSCheckFunctionSignatures 67 | const qrCode = QRCode.create(data, qrOpts); 68 | if (qrCode.version === 1) { 69 | // V2 makes the image more robust by adding bottom right square, so require v2 minimum. 70 | return await make(data, backgroundPath,{...props, qrOpts: {...qrOpts, version: 2}}); 71 | } 72 | 73 | // Calc size information: 74 | const boxSize = size.boxSize || 6; 75 | const boxScale = size.scale || 0.35; 76 | const bits = new Uint8Array(qrCode.modules.data); 77 | const qrWidth = qrCode.modules.size; 78 | const reserved = new Uint8Array(qrCode.modules.reservedBit); 79 | const w = (qrWidth+2)*boxSize, h = (Math.ceil(bits.length / qrWidth)+2) * boxSize 80 | 81 | // Create working canvas: 82 | // @ts-ignore 83 | const loader = useSVG ? new SVGLoader(w, h) : new LoaderClass(w, h); 84 | // Init styles and images: 85 | const background = await loader.loadImage(backgroundPath); 86 | const img = await loader.canvas; 87 | const ctx = img.getContext('2d'); 88 | 89 | ctx.fillStyle = colors.light; 90 | ctx.fillRect(0, 0, img.width, img.height); 91 | 92 | ctx.drawImage(background, 93 | 0, 0, background.width, background.height, // source dimensions 94 | boxSize, boxSize, img.width - boxSize*2, img.height - boxSize*2 // destination dimensions 95 | ); 96 | 97 | if (colors.overlay) { 98 | ctx.fillStyle = colors.overlay; 99 | ctx.fillRect(0, 0, img.width, img.height); 100 | } 101 | 102 | let x=0, y=0; 103 | bits.forEach( (b, idx) => { 104 | if (idx && 0 === (idx % qrWidth)){ 105 | y +=1; 106 | x = 0; 107 | } 108 | const important = reserved[idx]; 109 | let ox = (x+1)*boxSize, oy = (y+1)*boxSize, square = boxSize; 110 | 111 | if (!important) { 112 | square = Math.ceil(boxSize * boxScale); 113 | ox += Math.ceil((boxSize - square)/2); 114 | oy += Math.floor((boxSize - square)/2); 115 | } 116 | 117 | // @ts-ignore 118 | ctx.fillStyle = b ? colors.dark : colors.light; 119 | ctx.fillRect(ox,oy, square, square); 120 | x += 1; 121 | }) 122 | 123 | return loader; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /examples/editor.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | Custom QR Code Editor 11 | 12 | 13 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 140 | 141 |
142 |
143 | preview
144 | Check out the Project! 145 |
146 |
147 | 148 |
149 | Dark Color:

Dark

150 | Light Color:

Dark

151 | Overlay:

Dark

152 |
153 |
154 | 155 |
156 | 157 |
158 |
159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "C:\\Users\\User\\AppData\\Local\\Temp\\jest", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | coveragePathIgnorePatterns: [ 31 | "\\\\node_modules\\\\" 32 | ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // 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. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: null, 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | roots: [ 119 | "tests" 120 | ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: "node", 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "\\\\node_modules\\\\" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | transform: { 171 | "^.+\\.tsx?$": "ts-jest" 172 | }, 173 | 174 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 175 | // transformIgnorePatterns: [ 176 | // "\\\\node_modules\\\\" 177 | // ], 178 | 179 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 180 | // unmockedModulePathPatterns: undefined, 181 | 182 | // Indicates whether each individual test should be reported during the run 183 | // verbose: null, 184 | 185 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 186 | // watchPathIgnorePatterns: [], 187 | 188 | // Whether to use watchman for file crawling 189 | // watchman: true, 190 | }; 191 | --------------------------------------------------------------------------------