├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── index.html ├── outline.md ├── package.json ├── qrcli.mjs ├── qrcode.d.mts └── qrcode.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | /_local 2 | _* 3 | *.svg 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "ALPHABITFIELDS", 4 | "apos", 5 | "BGRs", 6 | "BITFIELDS", 7 | "BITMAPCOREHEADER", 8 | "BITMAPFILEHEADER", 9 | "BITMAPINFOHEADER", 10 | "BITMAPV", 11 | "Chaudhuri", 12 | "CIEXYZTRIPLE", 13 | "danielgjackson", 14 | "Deno", 15 | "fixecl", 16 | "Golay", 17 | "Hocquenghem", 18 | "INFOHEADER", 19 | "larsbrinkhoff", 20 | "LLMMM", 21 | "LLMMMEEEEEEEEEE", 22 | "Nayuki", 23 | "Pels", 24 | "qrcli", 25 | "qrcode", 26 | "qrcodejs", 27 | "sixel", 28 | "svguri", 29 | "xlink" 30 | ] 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Dan Jackson 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QR Code JS 2 | 3 | Javascript QR Code generator. Derived from my C version: [qrcode](https://github.com/danielgjackson/qrcode). 4 | 5 | 6 | ## Demo Site 7 | 8 | Generate your own SVG QR Code: 9 | 10 | * [danielgjackson.github.io/qrcodejs](https://danielgjackson.github.io/qrcodejs) 11 | 12 | 13 | ## QR Codes in you terminal 14 | 15 | If you have [Deno](https://deno.land/) installed, you can generate a QR Code in your terminal: 16 | 17 | ```bash 18 | deno run https://danielgjackson.github.io/qrcodejs/qrcli.mjs 'Hello, World!' 19 | ``` 20 | 21 | ...or write out a QR Code to a file: 22 | 23 | ```bash 24 | deno run --allow-write https://danielgjackson.github.io/qrcodejs/qrcli.mjs --output:svg --file hello.svg 'Hello, World!' 25 | ``` 26 | 27 | 28 | ## Getting started 29 | 30 | ### Example usage 31 | 32 | Install (if using `npm`): 33 | 34 | ```bash 35 | npm i -S https://github.com/danielgjackson/qrcodejs 36 | ``` 37 | 38 | 50 | 51 | Example usage from an ECMAScript module (`.mjs` file): 52 | 53 | ```javascript 54 | import QrCode from 'qrcodejs'; 55 | 56 | const data = 'Hello, World!'; 57 | const matrix = QrCode.generate(data); 58 | const text = QrCode.render('medium', matrix); 59 | console.log(text); 60 | ``` 61 | 62 | ### Example web page usage 63 | 64 | Example usage in a web page: 65 | 66 | ```html 67 | 68 | 76 | ``` 77 | 78 | ### Browser without a server 79 | 80 | If you would like to use this directly as part of a browser-based app over the `file:` protocol (which disallows modules), you can easily convert this to a non-module `.js` file: 81 | 82 | * Download [`qrcode.mjs`](https://raw.githubusercontent.com/danielgjackson/qrcodejs/master/qrcode.mjs) renamed as `qrcode.js`. 83 | * Remove the last line from the file (`export default QrCode`). 84 | * Ensure there is no `type="module"` attribute in your `` tag. 85 | 86 | 87 | ## API 88 | 89 | ### `QrCode.generate(data, options)` 90 | 91 | * `data` - the text to encode in the QR Code. 92 | 93 | * `options` - the configuration object for the QR Code (optional). Options include `errorCorrectionLevel` (0-3), `optimizeEcc` (boolean flag, default `true`, to maximize the error-correction level within the chosen output size), `minVersion`/`maxVersion` (1-40), `maskPattern` (0-7). Hints for the rendering stage are `invert` (boolean flag to invert the code, not as widely supported), and `quiet` (the size, in modules, of the quiet area around the code). 94 | 95 | Returns a *matrix* that can be passed to the `render()` function. 96 | 97 | 98 | ### `QrCode.render(mode, matrix, options)` 99 | 100 | * `mode` - the rendering mode, one of: 101 | 102 | * `large` - Generate block-character text, each module takes 2x1 character cells. 103 | * `medium` - Generate block-character text, fitting 1x2 modules in each character cell. 104 | * `compact` - Generate block-character text, fitting 2x2 modules in each character cell. 105 | * `svg` - Generate the contents for a scalable vector graphics file (`.svg`). 106 | * `bmp` - Generate the contents for a bitmap file (`.bmp`). 107 | * `svg-uri` - Generate a `data:` URI for an SVG file 108 | * `bmp-uri` - Generate a `data:` URI for a BMP file. 109 | 110 | The `-uri` modes can be, for example, directly used as the `src` for an `` tag, or `url()` image in CSS. 111 | 112 | * `matrix` - the matrix to draw, as returned by the `generate()` function. 113 | 114 | * `options` - the configuration object (optional), depends on the chosen rendering `mode`: 115 | 116 | * `svg` / `svg-uri`: `moduleSize` the unit dimensions of each module, `white` (boolean) output the non-set modules (otherwise will be transparent background), `moduleRound` proportion of how rounded the modules are, `finderRound` to hide the standard finder modules and instead output a shape with the specified roundness, `alignmentRound` to hide the standard alignment modules and instead output a shape with the specified roundness. 117 | 118 | * `bmp` / `bmp-uri`: `scale` for the size of a module, `alpha` (boolean) to use a transparent background, `width`/`height` can set a specific image size (rather than scaling the matrix dimensions). 119 | 120 | Returns the text or binary output from the chosen `mode`. 121 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | QR Code JS Demo 8 | 39 | 40 | 41 |

QR Code JS Demo

42 | 43 | 47 | 48 |
49 | 60 | (increases complexity, but more robust to damage or occlusion). 61 |
62 | 63 |
64 | 65 |
66 | 67 |
68 | Text 69 |
70 | - 71 |
72 |
73 | 74 |
75 | Image <img> 76 |
77 | 78 |
79 |
80 | 81 |
82 | Image <svg> 83 |
84 | 85 |
86 |
87 | 88 | 100 | 101 | 340 | 341 | 342 | -------------------------------------------------------------------------------- /outline.md: -------------------------------------------------------------------------------- 1 | # Outline-only QR Code 2 | 3 | For a vector QR code that only uses stroke lines (no fill), replace the `` section of your *.svg* file with: 4 | 5 | ```svg 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcodejs", 3 | "version": "0.0.0", 4 | "description": "Javascript QR Code generator.", 5 | "main": "qrcode.mjs", 6 | "types": "qrcode.d.mts", 7 | "type": "module", 8 | "scripts": { 9 | "start": "node qrcli.mjs", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/danielgjackson/qrcodejs.git" 15 | }, 16 | "keywords": [ 17 | "QR", 18 | "Code", 19 | "javascript" 20 | ], 21 | "author": "Dan Jackson", 22 | "license": "BSD-2-Clause", 23 | "bugs": { 24 | "url": "https://github.com/danielgjackson/qrcodejs/issues" 25 | }, 26 | "homepage": "https://github.com/danielgjackson/qrcodejs#readme" 27 | } 28 | -------------------------------------------------------------------------------- /qrcli.mjs: -------------------------------------------------------------------------------- 1 | import QrCode from './qrcode.mjs'; 2 | import fs from 'node:fs'; 3 | 4 | const programOptions = { 5 | text: 'Hello, world!', 6 | help: false, 7 | output: 'medium', 8 | uppercase: false, 9 | file: null, 10 | } 11 | const qrOptions = {} 12 | const renderOptions = {}; 13 | 14 | let matchParams = true; 15 | const textParts = []; 16 | 17 | // Get args on Deno or Node 18 | const args = globalThis.Deno ? Deno.args : process.argv.slice(2); 19 | 20 | for (let i = 0; i < args.length; i++) { 21 | const arg = args[i]; 22 | if (matchParams && arg.startsWith('-')) { 23 | // Program options 24 | if (arg == '--help') { programOptions.help = true; } 25 | else if (arg.startsWith('--output:')) { programOptions.output = arg.split(':')[1]; } 26 | else if (arg == '--debug-data') { programOptions.output = 'large'; renderOptions.segments = [' ', '██', '▓▓']; } // █▓▒░ 27 | else if (arg == '--uppercase') { programOptions.uppercase = true; } 28 | else if (arg == '--file') { programOptions.file = args[++i]; } 29 | // QR options 30 | else if (arg.startsWith('--ecl:')) { qrOptions.errorCorrectionLevel = QrCode.ErrorCorrectionLevel[arg.split(':')[1].toUpperCase()]; } 31 | else if (arg == '--fixecl') { qrOptions.optimizeEcc = false; } 32 | else if (arg == '--version') { qrOptions.minVersion = qrOptions.maxVersion = parseInt(args[++i]); } 33 | else if (arg == '--mask') { qrOptions.maskPattern = parseInt(args[++i]); } 34 | else if (arg == '--quiet') { qrOptions.quiet = parseInt(args[++i]); } 35 | else if (arg == '--invert') { qrOptions.invert = true; } 36 | // SVG renderer options 37 | else if (arg == '--svg-point') { renderOptions.moduleSize = parseFloat(args[++i]); } 38 | else if (arg == '--svg-round') { renderOptions.moduleRound = parseFloat(args[++i]); } 39 | else if (arg == '--svg-finder-round') { renderOptions.finderRound = parseFloat(args[++i]); } 40 | else if (arg == '--svg-alignment-round') { renderOptions.alignmentRound = parseFloat(args[++i]); } 41 | // Alpha options 42 | else if (arg == '--alpha') { renderOptions.alpha = true; } 43 | else if (arg == '--bmp-alpha') { renderOptions.alpha = true; } 44 | // Scale options 45 | else if (arg == '--scale') { renderOptions.scale = parseFloat(args[++i]); } 46 | else if (arg == '--bmp-scale') { renderOptions.scale = parseFloat(args[++i]); } 47 | // End of options 48 | else if (arg == '--') matchParams = false; 49 | else { 50 | console.error('ERROR: Unknown parameter: ' + arg); 51 | process.exit(1); 52 | } 53 | } else { 54 | //matchParams = false; 55 | textParts.push(arg); 56 | } 57 | } 58 | 59 | if (programOptions.help) { 60 | console.log('USAGE: [--ecl:] [--uppercase] [--invert] [--quiet 4] [--output:] [--file filename] '); 61 | console.log('') 62 | console.log('For --output:svg: [--svg-point 1.0] [--svg-round 0.0] [--svg-finder-round 0.0] [--svg-alignment-round 0.0]'); 63 | 64 | process.exit(1); 65 | } 66 | 67 | if (textParts.length > 0) { 68 | programOptions.text = textParts.join(' '); 69 | } 70 | if (programOptions.uppercase) { 71 | programOptions.text = programOptions.text.toUpperCase(); 72 | } 73 | //console.log('output=' + programOptions.output + ' \"' + programOptions.text + '\"'); 74 | const matrix = QrCode.generate(programOptions.text, qrOptions); 75 | const output = QrCode.render(programOptions.output, matrix, renderOptions); 76 | if (programOptions.file) { 77 | if (typeof output != 'string') { 78 | output = new Uint8Array(output) 79 | } 80 | fs.writeFileSync(programOptions.file, output); 81 | } else { 82 | console.log(output); 83 | } 84 | -------------------------------------------------------------------------------- /qrcode.d.mts: -------------------------------------------------------------------------------- 1 | export interface GenerateOptions { 2 | 3 | /** 0 to 3 */ 4 | errorCorrectionLevel?: number 5 | 6 | /** default true, to maximize the error-correction level within the chosen output size */ 7 | optimizeEcc?: boolean 8 | 9 | /** 1 to 40 */ 10 | minVersion?: number 11 | 12 | /** 1 to 40 */ 13 | maxVersion?: number 14 | 15 | /** 0 to 7 */ 16 | maskPattern?: number 17 | 18 | /** boolean flag to invert the code, not as widely supported */ 19 | invert?: boolean 20 | 21 | /** the size, in modules, of the quiet area around the code */ 22 | quiet?: number 23 | } 24 | 25 | export type RenderMode = 'large' | 'medium' | 'compact' | 'svg' | 'bmp' | 'svg-uri' | 'bmp-uri' | 'sixel' 26 | 27 | export type RenderOptions = SvgRenderOptions | BmpRenderOptions | SixelRenderOptions 28 | 29 | export interface SvgRenderOptions { 30 | /** (svg / svg-uri only) the color of each module (default: 'currentColor') */ 31 | color?: string 32 | 33 | /** (svg / svg-uri only) the unit dimensions of each module */ 34 | moduleSize?: number 35 | 36 | /** (svg / svg-uri only) output the non-set modules (otherwise will be transparent background) */ 37 | white?: boolean 38 | 39 | /** (svg / svg-uri only) proportion of how rounded the modules are */ 40 | moduleRound?: number 41 | 42 | /** (svg / svg-uri only) to hide the standard finder modules and instead output a shape with the specified roundness */ 43 | finderRound?: number 44 | 45 | /** (svg / svg-uri only) to hide the standard alignment modules and instead output a shape with the specified roundness */ 46 | alignmentRound?: number 47 | } 48 | 49 | export interface BmpRenderOptions { 50 | /** (bmp / bmp-uri only) for the size of a module */ 51 | scale?: number 52 | 53 | /** (bmp / bmp-uri only) to use a transparent background */ 54 | alpha?: boolean 55 | 56 | /** (bmp / bmp-uri only) can set a specific image size (rather than scaling the matrix dimensions) */ 57 | width?: boolean 58 | 59 | /** (bmp / bmp-uri only) can set a specific image size (rather than scaling the matrix dimensions) */ 60 | height?: boolean 61 | } 62 | 63 | export interface SixelRenderOptions { 64 | /** (sixel) for the size of a module */ 65 | scale?: number 66 | } 67 | 68 | export class Matrix {} 69 | 70 | export default class QrCode { 71 | static generate(data: string, options?: GenerateOptions): Matrix 72 | 73 | static render(mode: 'large' | 'medium' | 'compact', matrix: Matrix, options?: RenderOptions): string 74 | static render(mode: 'svg' | 'svg-uri', matrix: Matrix, options?: SvgRenderOptions): string 75 | static render(mode: 'bmp', matrix: Matrix, options?: BmpRenderOptions): Uint8Array 76 | static render(mode: 'bmp-uri', matrix: Matrix, options?: BmpRenderOptions): string 77 | static render(mode: 'sixel', matrix: Matrix, options?: SixelRenderOptions): string 78 | } 79 | -------------------------------------------------------------------------------- /qrcode.mjs: -------------------------------------------------------------------------------- 1 | // QR Code Generator 2 | // Dan Jackson, 2020 3 | 4 | 5 | // --- Bit Buffer Writing --- 6 | 7 | class BitBuffer { 8 | constructor(bitCapacity) { 9 | this.bitCapacity = bitCapacity; 10 | const byteLength = (this.bitCapacity + 7) >> 3; 11 | this.buffer = new Uint8Array(byteLength); 12 | this.bitOffset = 0; 13 | } 14 | 15 | append(value, bitCount) { 16 | for (let i = 0; i < bitCount; i++) { 17 | const writeByte = this.buffer[(this.bitOffset) >> 3]; 18 | const writeBit = 7 - (this.bitOffset & 0x07); 19 | const writeMask = 1 << writeBit; 20 | const readMask = 1 << (bitCount - 1 - i); 21 | this.buffer[this.bitOffset >> 3] = (writeByte & ~writeMask) | ((value & readMask) ? writeMask : 0); 22 | this.bitOffset++; 23 | } 24 | } 25 | 26 | position() { 27 | return this.bitOffset; 28 | } 29 | 30 | read(bitPosition) { 31 | const value = (this.buffer[bitPosition >> 3] & (1 << (7 - (bitPosition & 7)))) ? 1 : 0; 32 | return value; 33 | } 34 | } 35 | 36 | 37 | // --- Segment Modes --- 38 | 39 | // Segment Mode 0b0001 - Numeric 40 | // Maximal groups of 3/2/1 digits encoded to 10/7/4-bit binary 41 | class SegmentNumeric { 42 | static MODE = 0x01; 43 | static CHARSET = '0123456789'; 44 | 45 | static canEncode(text) { 46 | return [...text].every(c => SegmentNumeric.CHARSET.includes(c)); 47 | } 48 | 49 | static payloadSize(text) { 50 | const charCount = text.length; 51 | return 10 * Math.floor(charCount / 3) + (charCount % 3 * 4) - Math.floor(charCount % 3 / 2); 52 | } 53 | 54 | static countSize(version) { 55 | return (version < 10) ? 10 : (version < 27) ? 12 : 14; 56 | } 57 | 58 | static totalSize(version, text) { 59 | return Segment.MODE_BITS + SegmentNumeric.countSize(version) + SegmentNumeric.payloadSize(text); 60 | } 61 | 62 | static encode(bitBuffer, version, text) { 63 | const data = [...text].map(c => c.charCodeAt(0) - 0x30); 64 | bitBuffer.append(SegmentNumeric.MODE, Segment.MODE_BITS); 65 | bitBuffer.append(data.length, SegmentNumeric.countSize(version)); 66 | for (let i = 0; i < data.length; ) { 67 | const remain = (data.length - i) > 3 ? 3 : (data.length - i); 68 | let value = data[i]; 69 | let bits = 4; 70 | i++; 71 | // Maximal groups of 3/2/1 digits encoded to 10/7/4-bit binary 72 | if (i < data.length) { value = value * 10 + data[i]; bits += 3; i++; } 73 | if (i < data.length) { value = value * 10 + data[i]; bits += 3; i++; } 74 | bitBuffer.append(value, bits); 75 | } 76 | } 77 | } 78 | 79 | // Segment Mode 0b0010 - Alphanumeric 80 | class SegmentAlphanumeric { 81 | static MODE = 0x02; 82 | static CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; 83 | 84 | static canEncode(text) { 85 | return [...text].every(c => SegmentAlphanumeric.CHARSET.includes(c)); 86 | } 87 | 88 | static payloadSize(text) { 89 | const charCount = text.length; 90 | return 11 * Math.floor(charCount / 2) + 6 * (charCount % 2); 91 | } 92 | 93 | static countSize(version) { 94 | return (version < 10) ? 9 : (version < 27) ? 11 : 13; 95 | } 96 | 97 | static totalSize(version, text) { 98 | return Segment.MODE_BITS + SegmentAlphanumeric.countSize(version) + SegmentAlphanumeric.payloadSize(text); 99 | } 100 | 101 | static encode(bitBuffer, version, text) { 102 | const data = [...text].map(c => SegmentAlphanumeric.CHARSET.indexOf(c)); 103 | bitBuffer.append(SegmentAlphanumeric.MODE, Segment.MODE_BITS); 104 | bitBuffer.append(data.length, SegmentAlphanumeric.countSize(version)); 105 | for (let i = 0; i < data.length; ) { 106 | let value = data[i]; 107 | let bits = 6; 108 | i++; 109 | // Pairs combined(a * 45 + b) encoded as 11-bit; odd remainder encoded as 6-bit. 110 | if (i < data.length) { value = value * 45 + data[i]; bits += 5; i++; } 111 | bitBuffer.append(value, bits); 112 | } 113 | } 114 | } 115 | 116 | // Segment Mode 0b0100 - 8-bit byte 117 | class SegmentEightBit { 118 | static MODE = 0x04; 119 | 120 | static canEncode(text) { 121 | return [...text].every(c => c.charCodeAt(0) >= 0x00 && c.charCodeAt(0) <= 0xff); 122 | } 123 | 124 | static payloadSize(text) { 125 | const charCount = text.length; 126 | return 8 * charCount; 127 | } 128 | 129 | static countSize(version) { 130 | return (version < 10) ? 8 : (version < 27) ? 16 : 16; // 8-bit 131 | } 132 | 133 | static totalSize(version, text) { 134 | return Segment.MODE_BITS + SegmentEightBit.countSize(version) + SegmentEightBit.payloadSize(text); 135 | } 136 | 137 | static encode(bitBuffer, version, text) { 138 | const data = [...text].map(c => c.charCodeAt(0)); 139 | bitBuffer.append(SegmentEightBit.MODE, Segment.MODE_BITS); 140 | bitBuffer.append(data.length, SegmentEightBit.countSize(version)); 141 | for (let i = 0; i < data.length; i++) { 142 | bitBuffer.append(data[i], 8); 143 | } 144 | } 145 | } 146 | 147 | 148 | class Segment { 149 | // In descending order of coding efficiency 150 | static MODES = { 151 | numeric: SegmentNumeric, 152 | alphanumeric: SegmentAlphanumeric, 153 | eightBit: SegmentEightBit, 154 | }; 155 | static MODE_BITS = 4; // 4-bits to indicate mode 156 | static MODE_INDICATOR_TERMINATOR = 0x0; // 0b0000 157 | // ECI Assignment Numbers 158 | //static ECI_UTF8 = 26; // "\000026" UTF8 - ISO/IEC 10646 UTF-8 encoding 159 | 160 | 161 | constructor(text) { 162 | this.text = text; 163 | for (let mode of Object.values(Segment.MODES)) { 164 | if (mode.canEncode(this.text)) { 165 | this.mode = mode; 166 | return; 167 | } 168 | } 169 | throw 'Cannot encode text'; 170 | } 171 | 172 | } 173 | 174 | 175 | // --- Reed-Solomon Error-Correction Code --- 176 | 177 | // These error-correction functions are derived from https://www.nayuki.io/page/qr-code-generator-library Copyright (c) Project Nayuki. (MIT License) 178 | class ReedSolomon { 179 | 180 | // Product modulo GF(2^8/0x011D) 181 | static Multiply(a, b) { // both arguments 8-bit 182 | let value = 0; // 8-bit 183 | for (let i = 7; i >= 0; i--) { 184 | value = ((value << 1) ^ ((value >> 7) * 0x011D)) & 0xff; 185 | value ^= ((b >> i) & 1) * a; 186 | } 187 | return value; 188 | } 189 | 190 | // Reed-Solomon ECC generator polynomial for given degree 191 | static Divisor(degree) { 192 | const result = new Uint8Array(degree); // <= QrCode.ECC_CODEWORDS_MAX 193 | result.fill(0); 194 | result[degree - 1] = 1; 195 | let root = 1; // 8-bit 196 | for (let i = 0; i < degree; i++) { 197 | for (let j = 0; j < degree; j++) { 198 | result[j] = ReedSolomon.Multiply(result[j], root); 199 | if (j + 1 < degree) { 200 | result[j] ^= result[j + 1]; 201 | } 202 | } 203 | root = ReedSolomon.Multiply(root, 0x02) & 0xff; // 8-bit 204 | } 205 | return result; 206 | } 207 | 208 | // Reed-Solomon ECC 209 | static Remainder(data, dataOffset, dataLen, generator, degree, result, resultOffset) { 210 | result.fill(0, resultOffset, resultOffset + degree); 211 | for (let i = 0; i < dataLen; i++) { 212 | let factor = data[dataOffset + i] ^ result[resultOffset + 0]; 213 | // Move (degree-1) bytes from result[resultOffset+1] to result[resultOffset+0]. 214 | result.copyWithin(resultOffset, resultOffset + 1, resultOffset + 1 + degree - 1) 215 | result[resultOffset + degree - 1] = 0; 216 | for (let j = 0; j < degree; j++) { 217 | result[resultOffset + j] ^= ReedSolomon.Multiply(generator[j], factor); 218 | } 219 | } 220 | } 221 | 222 | } 223 | 224 | 225 | // --- 2D Matrix --- 226 | 227 | class Matrix { 228 | 229 | static MODULE_LIGHT = 0; 230 | static MODULE_DARK = 1; 231 | 232 | static FINDER_SIZE = 7; 233 | static TIMING_OFFSET = 6; 234 | static VERSION_SIZE = 3; 235 | static ALIGNMENT_RADIUS = 2; 236 | static QUIET_NONE = 0; 237 | static QUIET_STANDARD = 4; 238 | 239 | static calculateDimension(version) { 240 | return 17 + 4 * version; // V1=21x21; V40=177x177 241 | } 242 | 243 | static calculateMask(maskPattern, j, i) { 244 | switch (maskPattern) 245 | { 246 | case 0: return ((i + j) & 1) == 0; // QRCODE_MASK_000 247 | case 1: return (i & 1) == 0; // QRCODE_MASK_001 248 | case 2: return j % 3 == 0; // QRCODE_MASK_010 249 | case 3: return (i + j) % 3 == 0; // QRCODE_MASK_011 250 | case 4: return (((i >> 1) + ((j / 3)|0)) & 1) == 0; // QRCODE_MASK_100 251 | case 5: return ((i * j) & 1) + ((i * j) % 3) == 0; // QRCODE_MASK_101 252 | case 6: return ((((i * j) & 1) + ((i * j) % 3)) & 1) == 0; // QRCODE_MASK_110 253 | case 7: return ((((i * j) % 3) + ((i + j) & 1)) & 1) == 0; // QRCODE_MASK_111 254 | default: return false; 255 | } 256 | } 257 | 258 | // Returns coordinates to be used in all combinations (unless overlapping finder pattern) as x/y pairs for alignment, <0: end 259 | static alignmentCoordinates(version) { 260 | const count = (version <= 1) ? 0 : Math.floor(version / 7) + 2; 261 | const coords = Array(count); 262 | const step = (version == 32) ? 26 : Math.floor((version * 4 + count * 2 + 1) / (count * 2 - 2)) * 2; // step to previous 263 | let location = version * 4 + 10; // lower alignment marker 264 | for (let i = count - 1; i > 0; i--) { 265 | coords[i] = location; 266 | location -= step; 267 | } 268 | if (count > 0) coords[0] = 6; // first alignment marker is at offset 6 269 | return coords; 270 | } 271 | 272 | constructor(version) { 273 | this.version = version; 274 | this.dimension = Matrix.calculateDimension(this.version); 275 | const capacity = this.dimension * this.dimension; 276 | this.buffer = new Array(capacity); 277 | this.identity = new Array(capacity); 278 | this.quiet = Matrix.QUIET_STANDARD; 279 | this.invert = false; 280 | this.text = null; 281 | } 282 | 283 | setModule(x, y, value, identity) { 284 | if (x < 0 || y < 0 || x >= this.dimension || y >= this.dimension) return; 285 | const index = y * this.dimension + x; 286 | this.buffer[index] = value; 287 | if (typeof identity !== 'undefined') this.identity[index] = identity; 288 | } 289 | 290 | getModule(x, y) { 291 | if (x < 0 || y < 0 || x >= this.dimension || y >= this.dimension) return null; 292 | const index = y * this.dimension + x; 293 | return this.buffer[index]; 294 | } 295 | 296 | identifyModule(x, y) { 297 | if (x < 0 || y < 0 || x >= this.dimension || y >= this.dimension) return undefined; 298 | const index = y * this.dimension + x; 299 | return this.identity[index]; 300 | } 301 | 302 | // Draw finder and separator 303 | drawFinder(ox, oy) { 304 | for (let y = -Math.floor(Matrix.FINDER_SIZE / 2) - 1; y <= Math.floor(Matrix.FINDER_SIZE / 2) + 1; y++) { 305 | for (let x = -Math.floor(Matrix.FINDER_SIZE / 2) - 1; x <= Math.floor(Matrix.FINDER_SIZE / 2) + 1; x++) { 306 | let value = (Math.abs(x) > Math.abs(y) ? Math.abs(x) : Math.abs(y)) & 1 ? Matrix.MODULE_DARK : Matrix.MODULE_LIGHT; 307 | if (x == 0 && y == 0) value = Matrix.MODULE_DARK; 308 | const id = (x == 0 && y == 0) ? 'FI' : 'Fi'; 309 | this.setModule(ox + x, oy + y, value, id); 310 | } 311 | } 312 | } 313 | 314 | drawTiming() { 315 | const id = 'Ti'; 316 | for (let i = Matrix.FINDER_SIZE + 1; i < this.dimension - Matrix.FINDER_SIZE - 1; i++) { 317 | let value = (~i & 1) ? Matrix.MODULE_DARK : Matrix.MODULE_LIGHT; 318 | this.setModule(i, Matrix.TIMING_OFFSET, value, id); 319 | this.setModule(Matrix.TIMING_OFFSET, i, value, id); 320 | } 321 | } 322 | 323 | drawAlignment(ox, oy) { 324 | for (let y = -Matrix.ALIGNMENT_RADIUS; y <= Matrix.ALIGNMENT_RADIUS; y++) { 325 | for (let x = -Matrix.ALIGNMENT_RADIUS; x <= Matrix.ALIGNMENT_RADIUS; x++) { 326 | let value = 1 - ((Math.abs(x) > Math.abs(y) ? Math.abs(x) : Math.abs(y)) & 1) ? Matrix.MODULE_DARK : Matrix.MODULE_LIGHT; 327 | const id = (x == 0 && y == 0) ? 'AL' : 'Al'; 328 | this.setModule(ox + x, oy + y, value, id); 329 | } 330 | } 331 | } 332 | 333 | // Populate the matrix with function patterns: finder, separators, timing, alignment, temporary version & format info 334 | populateFunctionPatterns() { 335 | this.drawFinder(Math.floor(Matrix.FINDER_SIZE / 2), Math.floor(Matrix.FINDER_SIZE / 2)); 336 | this.drawFinder(this.dimension - 1 - Math.floor(Matrix.FINDER_SIZE / 2), Math.floor(Matrix.FINDER_SIZE / 2)); 337 | this.drawFinder(Math.floor(Matrix.FINDER_SIZE / 2), this.dimension - 1 - Math.floor(Matrix.FINDER_SIZE / 2)); 338 | 339 | this.drawTiming(); 340 | 341 | const alignmentCoords = Matrix.alignmentCoordinates(this.version); 342 | 343 | for (let h of alignmentCoords) { 344 | for (let v of alignmentCoords) { 345 | if (h <= Matrix.FINDER_SIZE && v <= Matrix.FINDER_SIZE) continue; // Obscured by top-left finder 346 | if (h >= this.dimension - 1 - Matrix.FINDER_SIZE && v <= Matrix.FINDER_SIZE) continue; // Obscured by top-right finder 347 | if (h <= Matrix.FINDER_SIZE && v >= this.dimension - 1 - Matrix.FINDER_SIZE) continue; // Obscured by bottom-left finder 348 | this.drawAlignment(h, v); 349 | } 350 | } 351 | 352 | // Draw placeholder format/version info (so that masking does not affect these parts) 353 | this.drawFormatInfo(0); 354 | this.drawVersionInfo(0); 355 | } 356 | 357 | // Set the data drawing cursor to the start position (lower-right corner) 358 | cursorReset() { 359 | this.cursorX = this.dimension - 1; 360 | this.cursorY = this.dimension - 1; 361 | } 362 | 363 | // Advance the data drawing cursor to next position 364 | cursorAdvance() { 365 | while (this.cursorX >= 0) { 366 | // Right-hand side of 2-module column? (otherwise, left-hand side) 367 | if ((this.cursorX & 1) ^ (this.cursorX > Matrix.TIMING_OFFSET ? 1 : 0)) { 368 | this.cursorX--; 369 | } else { // Left-hand side 370 | this.cursorX++; 371 | // Upwards? (otherwise, downwards) 372 | if (((this.cursorX - (this.cursorX > Matrix.TIMING_OFFSET ? 1 : 0)) / 2) & 1) { 373 | if (this.cursorY <= 0) this.cursorX -= 2; 374 | else this.cursorY--; 375 | } else { 376 | if (this.cursorY >= this.dimension - 1) this.cursorX -= 2; 377 | else this.cursorY++; 378 | } 379 | } 380 | if (!this.identifyModule(this.cursorX, this.cursorY)) return true; 381 | } 382 | return false; 383 | } 384 | 385 | cursorWrite(buffer, sourceBit, countBits) { 386 | let index = sourceBit; 387 | for (let countWritten = 0; countWritten < countBits; countWritten++) { 388 | let bit = buffer.read(index); 389 | this.setModule(this.cursorX, this.cursorY, bit); 390 | index++; 391 | if (!this.cursorAdvance()) break; 392 | } 393 | return index - sourceBit; 394 | } 395 | 396 | // Draw 15-bit format information (2-bit error-correction level, 3-bit mask, 10-bit BCH error-correction; all masked) 397 | drawFormatInfo(value) { 398 | const id = 'Fo'; 399 | for (let i = 0; i < 15; i++) { 400 | const v = (value >> i) & 1; 401 | 402 | // 15-bits starting LSB clockwise from top-left finder avoiding timing strips 403 | if (i < 6) this.setModule(Matrix.FINDER_SIZE + 1, i, v, id); 404 | else if (i == 6) this.setModule(Matrix.FINDER_SIZE + 1, Matrix.FINDER_SIZE, v, id); 405 | else if (i == 7) this.setModule(Matrix.FINDER_SIZE + 1, Matrix.FINDER_SIZE + 1, v, id); 406 | else if (i == 8) this.setModule(Matrix.FINDER_SIZE, Matrix.FINDER_SIZE + 1, v, id); 407 | else this.setModule(14 - i, Matrix.FINDER_SIZE + 1, v, id); 408 | 409 | // lower 8-bits starting LSB right-to-left underneath top-right finder 410 | if (i < 8) this.setModule(this.dimension - 1 - i, Matrix.FINDER_SIZE + 1, v, id); 411 | // upper 7-bits starting LSB top-to-bottom right of bottom-left finder 412 | else this.setModule(Matrix.FINDER_SIZE + 1, this.dimension - Matrix.FINDER_SIZE - 8 + i, v, id); 413 | } 414 | // dark module 415 | this.setModule(Matrix.FINDER_SIZE + 1, this.dimension - 1 - Matrix.FINDER_SIZE, Matrix.MODULE_DARK, id); 416 | } 417 | 418 | // Draw 18-bit version information (6-bit version number, 12-bit error-correction (18,6) Golay code) 419 | drawVersionInfo(value) { 420 | const id = 'Ve'; 421 | // No version information on V1-V6 422 | if (value === null || this.version < 7) return; 423 | for (let i = 0; i < 18; i++) { 424 | const v = (value >> i) & 1; 425 | const col = Math.floor(i / Matrix.VERSION_SIZE); 426 | const row = i % Matrix.VERSION_SIZE; 427 | this.setModule(col, this.dimension - 1 - Matrix.FINDER_SIZE - Matrix.VERSION_SIZE + row, v, id); 428 | this.setModule(this.dimension - 1 - Matrix.FINDER_SIZE - Matrix.VERSION_SIZE + row, col, v, id); 429 | } 430 | } 431 | 432 | applyMaskPattern(maskPattern) { 433 | for (let y = 0; y < this.dimension; y++) { 434 | for (let x = 0; x < this.dimension; x++) { 435 | const part = this.identifyModule(x, y); 436 | if (!part) { 437 | const mask = Matrix.calculateMask(maskPattern, x, y); 438 | if (mask) { 439 | const module = this.getModule(x, y); 440 | const value = 1 ^ module; 441 | this.setModule(x, y, value); 442 | } 443 | } 444 | } 445 | } 446 | } 447 | 448 | evaluatePenalty() { 449 | // Note: Penalty calculated over entire code (although format information is not yet written) 450 | const scoreN1 = 3; 451 | const scoreN2 = 3; 452 | const scoreN3 = 40; 453 | const scoreN4 = 10; 454 | let totalPenalty = 0; 455 | 456 | // Feature 1: Adjacent identical modules in row/column: (5 + i) count, penalty points: N1 + i 457 | // Feature 3: 1:1:3:1:1 ratio patterns (either polarity) in row/column, penalty points: N3 458 | for (let swapAxis = 0; swapAxis <= 1; swapAxis++) { 459 | let runs = Array(5); 460 | let runsCount = 0; 461 | for (let y = 0; y < this.dimension; y++) { 462 | let lastBit = -1; 463 | let runLength = 0; 464 | for (let x = 0; x < this.dimension; x++) { 465 | let bit = this.getModule(swapAxis ? y : x, swapAxis ? x : y); 466 | // Run extended 467 | if (bit == lastBit) runLength++; 468 | // End of run 469 | if (bit != lastBit || x >= this.dimension - 1) { 470 | // If not start condition 471 | if (lastBit >= 0) { 472 | // Feature 1 473 | if (runLength >= 5) { // or should this be strictly greater-than? 474 | totalPenalty += scoreN1 + (runLength - 5); 475 | } 476 | 477 | // Feature 3 478 | runsCount++; 479 | runs[runsCount % 5] = runLength; 480 | // Once we have a history of 5 lengths, check proportion 481 | if (runsCount >= 5) { 482 | // Proportion: 1 : 1 : 3 : 1 : 1 483 | // Modulo relative index: +3, +4, 0, +1, +2 484 | // Check for proportions 485 | let v = runs[(runsCount + 1) % 5]; 486 | if (runs[runsCount % 5] == 3 * v && v == runs[(runsCount + 2) % 5] && v == runs[(runsCount + 3) % 5] && v == runs[(runsCount + 4) % 5]) { 487 | totalPenalty += scoreN3; 488 | } 489 | } 490 | } 491 | runLength = 1; 492 | lastBit = bit; 493 | } 494 | } 495 | } 496 | } 497 | 498 | // Feature 2: Block of identical modules: m * n size, penalty points: N2 * (m-1) * (n-1) 499 | // (fix from @larsbrinkhoff pull request #3 to danielgjackson/qrcode) 500 | for (let y = 0; y < this.dimension - 1; y++) { 501 | for (let x = 0; x < this.dimension - 1; x++) { 502 | let bits = this.getModule(x, y); 503 | bits += this.getModule(x+1, y); 504 | bits += this.getModule(x, y+1); 505 | bits += this.getModule(x+1, y+1); 506 | if (bits == 0 || bits == 4) totalPenalty += scoreN2; 507 | } 508 | } 509 | 510 | // Feature 4: Dark module percentage: 50 +|- (5*k) to 50 +|- (5*(k+1)), penalty points: N4 * k 511 | { 512 | let darkCount = 0; 513 | for (let y = 0; y < this.dimension; y++) { 514 | for (let x = 0; x < this.dimension; x++) { 515 | let bit = this.getModule(x, y); 516 | if (bit == Matrix.MODULE_DARK) darkCount++; 517 | } 518 | } 519 | // Deviation from 50% 520 | let percentage = (100 * darkCount + (this.dimension * this.dimension / 2)) / (this.dimension * this.dimension); 521 | let deviation = Math.abs(percentage - 50); 522 | let rating = Math.floor(deviation / 5); 523 | let penalty = scoreN4 * rating; 524 | totalPenalty += penalty; 525 | } 526 | 527 | return totalPenalty; 528 | } 529 | 530 | } 531 | 532 | 533 | class QrCode { 534 | 535 | static VERSION_MIN = 1; 536 | static VERSION_MAX = 40; 537 | 538 | // In ascending order of robustness 539 | static ErrorCorrectionLevel = { 540 | L: 0x01, // 0b01 Low (~7%) 541 | M: 0x00, // 0b00 Medium (~15%) 542 | Q: 0x03, // 0b11 Quartile (~25%) 543 | H: 0x02, // 0b10 High (~30%) 544 | }; 545 | 546 | static ECC_CODEWORDS_MAX = 30; 547 | static PAD_CODEWORDS = 0xec11; // Pad codewords 0b11101100=0xec 0b00010001=0x11 548 | 549 | // Calculate the (square) dimension for a version. V1=21x21; V40=177x177. 550 | static dimension(version) { 551 | return 17 + 4 * version; 552 | } 553 | 554 | // Calculate the total number of data modules in a version (raw: data, ecc and remainder bits); does not include finder/alignment/version/timing. 555 | static totalDataModules(version) { 556 | return (((16 * version + 128) * version) + 64 - (version < 2 ? 0 : (25 * (Math.floor(version / 7) + 2) - 10) * (Math.floor(version / 7) + 2) - 55) - (version < 7 ? 0 : 36)); 557 | } 558 | 559 | // Calculate the total number of data bits available in the codewords (cooked: after ecc and remainder) 560 | static dataCapacity(version, errorCorrectionLevel) { 561 | const capacityCodewords = Math.floor(QrCode.totalDataModules(version) / 8); 562 | const eccTotalCodewords = QrCode.eccBlockCodewords(version, errorCorrectionLevel) * QrCode.eccBlockCount(version, errorCorrectionLevel); 563 | const dataCapacityCodewords = capacityCodewords - eccTotalCodewords; 564 | return dataCapacityCodewords * 8; 565 | } 566 | 567 | // Number of error correction blocks 568 | static eccBlockCount(version, errorCorrectionLevel) { 569 | const eccBlockCountLookup = [ 570 | [ 0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49 ], // 0b00 Medium 571 | [ 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25 ], // 0b01 Low 572 | [ 0, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81 ], // 0b10 High 573 | [ 0, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68 ], // 0b11 Quartile 574 | ]; 575 | return eccBlockCountLookup[errorCorrectionLevel][version]; 576 | } 577 | 578 | // Number of error correction codewords in each block 579 | static eccBlockCodewords(version, errorCorrectionLevel) { 580 | const eccBlockCodewordsLookup = [ 581 | [ 0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28 ], // 0b00 Medium 582 | [ 0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 ], // 0b01 Low 583 | [ 0, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 ], // 0b10 High 584 | [ 0, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30 ], // 0b11 Quartile 585 | ]; 586 | return eccBlockCodewordsLookup[errorCorrectionLevel][version]; 587 | } 588 | 589 | // Calculate 18-bit version information (6-bit version number, 12-bit error-correction (18,6) Golay code) 590 | static calculateVersionInfo(version) { 591 | if (version < 7) return null; 592 | // Calculate 12-bit error-correction (18,6) Golay code 593 | let golay = version; 594 | for (let i = 0; i < 12; i++) golay = (golay << 1) ^ ((golay >>> 11) * 0x1f25); 595 | const value = (version << 12) | golay; 596 | return value; 597 | } 598 | 599 | // Calculate 15-bit format information (2-bit error-correction level, 3-bit mask, 10-bit BCH error-correction; all masked) 600 | static calculateFormatInfo(errorCorrectionLevel, maskPattern) { 601 | // TODO: Reframe in terms of QRCODE_SIZE_ECL (2) and QRCODE_SIZE_MASK (3) 602 | 603 | // LLMMM 604 | const value = ((errorCorrectionLevel & 0x03) << 3) | (maskPattern & 0x07); 605 | 606 | // Calculate 10-bit Bose-Chaudhuri-Hocquenghem (15,5) error-correction 607 | let bch = value; 608 | for (let i = 0; i < 10; i++) bch = (bch << 1) ^ ((bch >>> 9) * 0x0537); 609 | 610 | // 0LLMMMEEEEEEEEEE 611 | let format = (value << 10) | (bch & 0x03ff); 612 | const formatMask = 0x5412; // 0b0101010000010010 613 | format ^= formatMask; 614 | 615 | return format; 616 | } 617 | 618 | 619 | // Total number of data bits used (may later require 0-padding to a byte boundary and padding bytes added) 620 | static measureSegments(segments, version) { 621 | let total = 0; 622 | for (let segment of segments) { 623 | total += segment.mode.totalSize(version, segment.text); 624 | } 625 | return total; 626 | } 627 | 628 | static doSegmentsFit(segments, version, errorCorrectionLevel) { 629 | const sizeBits = QrCode.measureSegments(segments, version); 630 | const dataCapacity = QrCode.dataCapacity(version, errorCorrectionLevel); 631 | return sizeBits <= dataCapacity; 632 | } 633 | 634 | static findMinimumVersion(segments, errorCorrectionLevel, minVersion = QrCode.VERSION_MIN, maxVersion = QrCode.VERSION_MAX) { 635 | for (let version = minVersion; version <= maxVersion; version++) { 636 | if (QrCode.doSegmentsFit(segments, version, errorCorrectionLevel)) { 637 | return version; 638 | } 639 | } 640 | throw 'Cannot fit data in any allowed versions'; 641 | } 642 | 643 | static tryToImproveErrorCorrectionLevel(segments, version, currentErrorCorrectionLevel) { 644 | const ranking = Object.values(QrCode.ErrorCorrectionLevel); 645 | for (let i = 1; i < ranking.length; i++) { 646 | if (currentErrorCorrectionLevel == ranking[i - 1]) { 647 | if (QrCode.doSegmentsFit(segments, version, ranking[i])) { 648 | currentErrorCorrectionLevel = ranking[i]; 649 | } 650 | } 651 | } 652 | return currentErrorCorrectionLevel; 653 | } 654 | 655 | // Write segments: header/count/payload 656 | static writeData(scratchBuffer, version, segments) { 657 | // Add segments (mode, count and data) 658 | for (let segment of segments) { 659 | segment.mode.encode(scratchBuffer, version, segment.text); 660 | } 661 | } 662 | 663 | // Finish segments: given the available space, write terminator, rounding bits, and padding codewords 664 | static writePadding(scratchBuffer, version, errorCorrectionLevel) { 665 | 666 | // The total number of data bits available in the codewords (cooked: after ecc and remainder) 667 | const dataCapacity = QrCode.dataCapacity(version, errorCorrectionLevel) 668 | 669 | // Write only in capacity in any available space 670 | let remaining; 671 | 672 | // Add terminator 4-bit (0b0000) 673 | remaining = Math.min(dataCapacity - scratchBuffer.position(), Segment.MODE_BITS); 674 | scratchBuffer.append(Segment.MODE_INDICATOR_TERMINATOR, remaining); // all zeros so won't be misaligned by partial write 675 | 676 | // Remainder bits to round up to a whole byte 677 | remaining = Math.min(dataCapacity - scratchBuffer.position(), (8 - (scratchBuffer.position() & 7)) & 7); 678 | scratchBuffer.append(0x00, remaining); // all zeros so won't be misaligned by partial write 679 | 680 | // Remainder padding codewords 681 | while ((remaining = Math.min(dataCapacity - scratchBuffer.position(), 16)) > 0) { 682 | scratchBuffer.append(QrCode.PAD_CODEWORDS >> (16 - remaining), remaining); // align for partial write 683 | } 684 | 685 | // Check position matches expectation 686 | console.assert(scratchBuffer.position() === dataCapacity, 'Unexpectedly failed to correctly fill the data buffer'); 687 | } 688 | 689 | 690 | // Calculate ECC data at the end of the codewords 691 | // ...and fill the matrix 692 | // TODO: Split this function into two (but depends on a lot of calculated state) 693 | static calculateEccAndFillMatrix(scratchBuffer, version, errorCorrectionLevel, matrix) { 694 | // Number of error correction blocks 695 | const eccBlockCount = QrCode.eccBlockCount(version, errorCorrectionLevel); 696 | 697 | // Number of error correction codewords in each block 698 | const eccCodewords = QrCode.eccBlockCodewords(version, errorCorrectionLevel); 699 | 700 | // The total number of data modules in a version (raw: data, ecc and remainder bits); does not include finder/alignment/version/timing. 701 | const totalCapacity = QrCode.totalDataModules(version); 702 | 703 | // Codeword (byte) position in buffer for ECC data 704 | const eccOffset = Math.floor((totalCapacity - (8 * eccCodewords * eccBlockCount)) / 8); 705 | 706 | console.assert(8 * eccOffset === scratchBuffer.bitOffset, `Expected current bit position ${scratchBuffer.bitOffset} to match ECC offset *8 ${8 * eccOffset}`); 707 | 708 | // Calculate Reed-Solomon divisor 709 | const eccDivisor = ReedSolomon.Divisor(eccCodewords); 710 | 711 | const dataCapacityBytes = eccOffset; 712 | const dataLenShort = Math.floor(dataCapacityBytes / eccBlockCount); 713 | const countShortBlocks = (eccBlockCount - (dataCapacityBytes - (dataLenShort * eccBlockCount))); 714 | const dataLenLong = dataLenShort + (countShortBlocks >= eccBlockCount ? 0 : 1); 715 | for (let block = 0; block < eccBlockCount; block++) { 716 | // Calculate offset and length (earlier consecutive blocks may be short by 1 codeword) 717 | let dataOffset; 718 | if (block < countShortBlocks) { 719 | dataOffset = block * dataLenShort; 720 | } else { 721 | dataOffset = block * dataLenShort + (block - countShortBlocks); 722 | } 723 | let dataLen = dataLenShort + (block < countShortBlocks ? 0 : 1); 724 | // Calculate this block's ECC 725 | let eccDest = eccOffset + (block * eccCodewords); 726 | ReedSolomon.Remainder(scratchBuffer.buffer, dataOffset, dataLen, eccDivisor, eccCodewords, scratchBuffer.buffer, eccDest); 727 | } 728 | 729 | 730 | // Fill the matrix with data 731 | 732 | // Write the codewords interleaved between blocks 733 | matrix.cursorReset(); 734 | let totalWritten = 0; 735 | 736 | // Write data codewords interleaved across ecc blocks -- some early blocks may be short 737 | for (let i = 0; i < dataLenLong; i++) { 738 | for (let block = 0; block < eccBlockCount; block++) { 739 | // Calculate offset and length (earlier consecutive blocks may be short by 1 codeword) 740 | // Skip codewords due to short block 741 | if (i >= dataLenShort && block < countShortBlocks) continue; 742 | const codeword = (block * dataLenShort) + (block > countShortBlocks ? block - countShortBlocks : 0) + i; 743 | const sourceBit = codeword * 8; 744 | const countBits = 8; 745 | totalWritten += matrix.cursorWrite(scratchBuffer, sourceBit, countBits); 746 | } 747 | } 748 | 749 | // Write ECC codewords interleaved across ecc blocks 750 | for (let i = 0; i < eccCodewords; i++) { 751 | for (let block = 0; block < eccBlockCount; block++) { 752 | const sourceBit = 8 * eccOffset + (block * eccCodewords * 8) + (i * 8); 753 | const countBits = 8; 754 | totalWritten += matrix.cursorWrite(scratchBuffer, sourceBit, countBits); 755 | } 756 | } 757 | 758 | // Add any remainder 0 bits (could be 0/3/4/7) 759 | const bit = Matrix.MODULE_LIGHT; 760 | while (totalWritten < totalCapacity) { 761 | matrix.setModule(matrix.cursorX, matrix.cursorY, bit); 762 | totalWritten++; 763 | if (!matrix.cursorAdvance()) break; 764 | } 765 | 766 | } 767 | 768 | 769 | // 770 | static findOptimalMaskPattern(matrix, errorCorrectionLevel) { 771 | let lowestPenalty = -1; 772 | let bestMaskPattern = null; 773 | for (let maskPattern = 0; maskPattern <= 7; maskPattern++) { 774 | // XOR mask pattern 775 | matrix.applyMaskPattern(maskPattern); 776 | 777 | // Write format information before evaluating penalty 778 | // (fix from @larsbrinkhoff pull request #2 to danielgjackson/qrcode) 779 | const formatInfo = QrCode.calculateFormatInfo(errorCorrectionLevel, maskPattern); 780 | matrix.drawFormatInfo(formatInfo); 781 | 782 | // Find penalty score for this mask pattern 783 | const penalty = matrix.evaluatePenalty(); 784 | 785 | // XOR same mask removes it 786 | matrix.applyMaskPattern(maskPattern); 787 | 788 | // See if this is the best so far 789 | if (lowestPenalty < 0 || penalty < lowestPenalty) { 790 | lowestPenalty = penalty; 791 | bestMaskPattern = maskPattern; 792 | } 793 | } 794 | return bestMaskPattern; 795 | } 796 | 797 | 798 | constructor() { 799 | } 800 | 801 | static generate(text, userOptions) { 802 | 803 | // Generation options 804 | const options = Object.assign({ 805 | errorCorrectionLevel: QrCode.ErrorCorrectionLevel.M, 806 | minVersion: QrCode.VERSION_MIN, 807 | maxVersion: QrCode.VERSION_MAX, 808 | optimizeEcc: true, 809 | maskPattern: null, 810 | quiet: Matrix.QUIET_STANDARD, // only information for the renderer 811 | invert: false, // only a flag for the renderer 812 | }, userOptions) 813 | 814 | // Allow either a single text string or an array of text strings likely to encode as different modes 815 | const textArray = Array.isArray(text) ? text : [text]; 816 | 817 | // Create a segment for the text, each with its own best-fit encoding mode 818 | const segments = textArray.map(text => new Segment(text)); 819 | 820 | // Fit the payload to a version (dimension) 821 | let errorCorrectionLevel = options.errorCorrectionLevel; 822 | const version = QrCode.findMinimumVersion(segments, errorCorrectionLevel, options.minVersion, options.maxVersion); 823 | 824 | // Try to find a better error correction level for the given size 825 | if (options.optimizeEcc) { 826 | errorCorrectionLevel = QrCode.tryToImproveErrorCorrectionLevel(segments, version, errorCorrectionLevel); 827 | } 828 | 829 | // 'scratchBuffer' to contain the entire data bitstream for the QR Code 830 | // (payload with headers, terminator, rounding bits, padding modules, ECC, remainder bits) 831 | const totalCapacity = QrCode.totalDataModules(version); // The total number of data modules in a version (raw: data, ecc and remainder bits); does not include finder/alignment/version/timing. 832 | const scratchBuffer = new BitBuffer(totalCapacity); 833 | 834 | // Write segments: header/count/payload 835 | QrCode.writeData(scratchBuffer, version, segments); 836 | 837 | // Finish segments: given the available space, write terminator, rounding bits, and padding codewords 838 | QrCode.writePadding(scratchBuffer, version, errorCorrectionLevel); 839 | 840 | // Create an empty matrix 841 | const matrix = new Matrix(version); 842 | matrix.text = text; 843 | matrix.quiet = options.quiet; 844 | matrix.invert = options.invert; 845 | 846 | // Populate the matrix with function patterns: finder, separators, timing, alignment, temporary version & format info 847 | matrix.populateFunctionPatterns(); 848 | 849 | // Calculate ECC and fill matrix 850 | QrCode.calculateEccAndFillMatrix(scratchBuffer, version, errorCorrectionLevel, matrix); 851 | 852 | // Calculate the optimal mask pattern 853 | let maskPattern = options.maskPattern; 854 | if (maskPattern === null) { 855 | maskPattern = QrCode.findOptimalMaskPattern(matrix, errorCorrectionLevel); 856 | } 857 | 858 | // Apply the chosen mask pattern 859 | matrix.applyMaskPattern(maskPattern); 860 | 861 | // Populate the matrix with version information 862 | const versionInfo = QrCode.calculateVersionInfo(version); 863 | matrix.drawVersionInfo(versionInfo); 864 | 865 | // Fill-in format information 866 | const formatInfo = QrCode.calculateFormatInfo(errorCorrectionLevel, maskPattern); 867 | matrix.drawFormatInfo(formatInfo); 868 | 869 | return matrix; 870 | } 871 | 872 | static render(mode, matrix, renderOptions) { 873 | const renderers = { 874 | 'debug': renderDebug, 875 | 'large': renderTextLarge, 876 | 'medium': renderTextMedium, 877 | 'compact': renderTextCompact, 878 | 'svg': renderSvg, 879 | 'svg-uri': renderSvgUri, 880 | 'bmp': renderBmp, 881 | 'bmp-uri': renderBmpUri, 882 | 'sixel': renderSixel, 883 | 'tgp': renderTerminalGraphicsProtocol, 884 | }; 885 | if (!renderers[mode]) throw new Error('ERROR: Invalid render mode: ' + mode); 886 | return renderers[mode](matrix, renderOptions); 887 | } 888 | 889 | } 890 | 891 | // Generate a bitmap from an array of [R,G,B] or [R,G,B,A] pixels 892 | function BitmapGenerate(data, width, height, alpha = false) { 893 | const bitsPerPixel = alpha ? 32 : 24; 894 | const fileHeaderSize = 14; 895 | const bmpHeaderSizeByVersion = { 896 | BITMAPCOREHEADER: 12, 897 | BITMAPINFOHEADER: 40, 898 | BITMAPV2INFOHEADER: 52, 899 | BITMAPV3INFOHEADER: 56, 900 | BITMAPV4HEADER: 108, 901 | BITMAPV5HEADER: 124, 902 | }; 903 | const version = alpha ? 'BITMAPV4HEADER' : 'BITMAPCOREHEADER'; // V3 provides alpha on Chrome, but V4 required for Firefox 904 | if (!bmpHeaderSizeByVersion.hasOwnProperty(version)) 905 | throw `Unknown BMP header version: ${version}`; 906 | const bmpHeaderSize = bmpHeaderSizeByVersion[version]; 907 | const stride = 4 * Math.floor((width * Math.floor((bitsPerPixel + 7) / 8) + 3) / 4); // Byte width of each line 908 | const biSizeImage = stride * Math.abs(height); // Total number of bytes that will be written 909 | const bfOffBits = fileHeaderSize + bmpHeaderSize; // + paletteSize 910 | const bfSize = bfOffBits + biSizeImage; 911 | const buffer = new ArrayBuffer(bfSize); 912 | const view = new DataView(buffer); 913 | // Write 14-byte BITMAPFILEHEADER 914 | view.setUint8(0, 'B'.charCodeAt(0)); 915 | view.setUint8(1, 'M'.charCodeAt(0)); // @0 WORD bfType 916 | view.setUint32(2, bfSize, true); // @2 DWORD bfSize 917 | view.setUint16(6, 0, true); // @6 WORD bfReserved1 918 | view.setUint16(8, 0, true); // @8 WORD bfReserved2 919 | view.setUint32(10, bfOffBits, true); // @10 DWORD bfOffBits 920 | if (bmpHeaderSize == bmpHeaderSizeByVersion.BITMAPCOREHEADER) { // (14+12=26) BITMAPCOREHEADER 921 | view.setUint32(14, bmpHeaderSize, true); // @14 DWORD biSize 922 | view.setUint16(18, width, true); // @18 WORD biWidth 923 | view.setInt16(20, height, true); // @20 WORD biHeight 924 | view.setUint16(22, 1, true); // @26 WORD biPlanes 925 | view.setUint16(24, bitsPerPixel, true); // @28 WORD biBitCount 926 | } 927 | else if (bmpHeaderSize >= bmpHeaderSizeByVersion.BITMAPINFOHEADER) { // (14+40=54) BITMAPINFOHEADER 928 | view.setUint32(14, bmpHeaderSize, true); // @14 DWORD biSize 929 | view.setUint32(18, width, true); // @18 DWORD biWidth 930 | view.setInt32(22, height, true); // @22 DWORD biHeight 931 | view.setUint16(26, 1, true); // @26 WORD biPlanes 932 | view.setUint16(28, bitsPerPixel, true); // @28 WORD biBitCount 933 | view.setUint32(30, alpha ? 3 : 0, true); // @30 DWORD biCompression (0=BI_RGB, 3=BI_BITFIELDS, 6=BI_ALPHABITFIELDS on Win-CE-5) 934 | view.setUint32(34, biSizeImage, true); // @34 DWORD biSizeImage 935 | view.setUint32(38, 2835, true); // @38 DWORD biXPelsPerMeter 936 | view.setUint32(42, 2835, true); // @42 DWORD biYPelsPerMeter 937 | view.setUint32(46, 0, true); // @46 DWORD biClrUsed 938 | view.setUint32(50, 0, true); // @50 DWORD biClrImportant 939 | } 940 | if (bmpHeaderSize >= bmpHeaderSizeByVersion.BITMAPV2INFOHEADER) { // (14+52=66) BITMAPV2INFOHEADER (+RGB BI_BITFIELDS) 941 | view.setUint32(54, alpha ? 0x00ff0000 : 0x00000000, true); // @54 DWORD bRedMask 942 | view.setUint32(58, alpha ? 0x0000ff00 : 0x00000000, true); // @58 DWORD bGreenMask 943 | view.setUint32(62, alpha ? 0x000000ff : 0x00000000, true); // @62 DWORD bBlueMask 944 | } 945 | if (bmpHeaderSize >= bmpHeaderSizeByVersion.BITMAPV3INFOHEADER) { // (14+56=70) BITMAPV3INFOHEADER (+A BI_BITFIELDS) 946 | view.setUint32(66, alpha ? 0xff000000 : 0x00000000, true); // @66 DWORD bAlphaMask 947 | } 948 | if (bmpHeaderSize >= bmpHeaderSizeByVersion.BITMAPV4HEADER) { // (14+108=122) BITMAPV4HEADER (color space and gamma correction) 949 | const colorSpace = "Win "; // "BGRs"; // @ 70 DWORD bCSType 950 | view.setUint8(70, colorSpace.charCodeAt(0)); 951 | view.setUint8(71, colorSpace.charCodeAt(1)); 952 | view.setUint8(72, colorSpace.charCodeAt(2)); 953 | view.setUint8(73, colorSpace.charCodeAt(3)); 954 | // @74 sizeof(CIEXYZTRIPLE)=36 (can be left empty for "Win ") 955 | view.setUint32(110, 0, true); // @110 DWORD bGammaRed 956 | view.setUint32(114, 0, true); // @114 DWORD bGammaGreen 957 | view.setUint32(118, 0, true); // @118 DWORD bGammaBlue 958 | } 959 | if (bmpHeaderSize >= bmpHeaderSizeByVersion.BITMAPV5HEADER) { // (14+124=138) BITMAPV5HEADER (ICC color profile) 960 | view.setUint32(122, 0x4, true); // @122 DWORD bIntent (0x1=LCS_GM_BUSINESS, 0x2=LCS_GM_GRAPHICS, 0x4=LCS_GM_IMAGES, 0x8=LCS_GM_ABS_COLORIMETRIC) 961 | view.setUint32(126, 0, true); // @126 DWORD bProfileData 962 | view.setUint32(130, 0, true); // @130 DWORD bProfileSize 963 | view.setUint32(134, 0, true); // @134 DWORD bReserved 964 | } 965 | // If there was one, write the palette here (fileHeaderSize + bmpHeaderSize) 966 | // Write pixels 967 | for (let y = 0; y < height; y++) { 968 | let offset = bfOffBits + (height - 1 - y) * stride; 969 | for (let x = 0; x < width; x++) { 970 | const value = data[y * width + x]; 971 | view.setUint8(offset + 0, value[2]); // B 972 | view.setUint8(offset + 1, value[1]); // G 973 | view.setUint8(offset + 2, value[0]); // R 974 | if (alpha) { 975 | view.setUint8(offset + 3, value[3]); // A 976 | offset += 4; 977 | } 978 | else { 979 | offset += 3; 980 | } 981 | } 982 | } 983 | return buffer; 984 | } 985 | 986 | 987 | function renderDebug(matrix, options) { 988 | options = Object.assign({ 989 | segments: [' ', '██'], 990 | sep: '\n', 991 | }, options); 992 | const lines = []; 993 | for (let y = -matrix.quiet; y < matrix.dimension + matrix.quiet; y++) { 994 | const parts = []; 995 | for (let x = -matrix.quiet; x < matrix.dimension + matrix.quiet; x++) { 996 | let part = matrix.identifyModule(x, y); 997 | const bit = matrix.getModule(x, y) ? !matrix.invert : matrix.invert; 998 | const value = bit ? 1 : 0; 999 | if (typeof part == 'undefined' || part === null) part = options.segments[value]; 1000 | parts.push(part); 1001 | } 1002 | lines.push(parts.join('')); 1003 | } 1004 | return lines.join(options.sep); 1005 | } 1006 | 1007 | function renderTextLarge(matrix, options) { 1008 | options = Object.assign({ 1009 | segments: [' ', '██'], 1010 | sep: '\n', 1011 | }, options); 1012 | const lines = []; 1013 | for (let y = -matrix.quiet; y < matrix.dimension + matrix.quiet; y++) { 1014 | const parts = []; 1015 | for (let x = -matrix.quiet; x < matrix.dimension + matrix.quiet; x++) { 1016 | const bit = matrix.getModule(x, y) ? !matrix.invert : matrix.invert; 1017 | const value = bit ? 1 : 0; 1018 | // If an additional segment type is specified, use it to identify data modules differently 1019 | const chars = (options.segments.length >= 3 && bit && !matrix.identifyModule(x, y)) ? options.segments[2] : options.segments[value]; 1020 | parts.push(chars); 1021 | } 1022 | lines.push(parts.join('')); 1023 | } 1024 | return lines.join(options.sep); 1025 | } 1026 | 1027 | function renderTextMedium(matrix, options) { 1028 | options = Object.assign({ 1029 | segments: [' ', '▀', '▄', '█'], 1030 | sep: '\n', 1031 | }, options); 1032 | const lines = []; 1033 | for (let y = -matrix.quiet; y < matrix.dimension + matrix.quiet; y += 2) { 1034 | const parts = []; 1035 | for (let x = -matrix.quiet; x < matrix.dimension + matrix.quiet; x++) { 1036 | const upper = matrix.getModule(x, y) ? !matrix.invert : matrix.invert; 1037 | const lower = (y + 1 < matrix.dimension ? matrix.getModule(x, y + 1) : 0) ? !matrix.invert : matrix.invert; 1038 | const value = (upper ? 0x01 : 0) | (lower ? 0x02 : 0); 1039 | // '▀', '▄', '█' // '\u{0020}' space, '\u{2580}' upper half block, '\u{2584}' lower half block, '\u{2588}' block 1040 | const c = options.segments[value]; 1041 | parts.push(c); 1042 | } 1043 | lines.push(parts.join('')); 1044 | } 1045 | return lines.join(options.sep); 1046 | } 1047 | 1048 | function renderTextCompact(matrix, options) { 1049 | options = Object.assign({ 1050 | segments: [' ', '▘', '▝', '▀', '▖', '▌', '▞', '▛', '▗', '▚', '▐', '▜', '▄', '▙', '▟', '█'], 1051 | sep: '\n', 1052 | }, options); 1053 | const lines = []; 1054 | for (let y = -matrix.quiet; y < matrix.dimension + matrix.quiet; y += 2) { 1055 | const parts = []; 1056 | for (let x = -matrix.quiet; x < matrix.dimension + matrix.quiet; x += 2) { 1057 | let value = 0; 1058 | value |= (matrix.getModule(x, y) ? !matrix.invert : matrix.invert) ? 0x01 : 0x00; 1059 | value |= (((x + 1 < matrix.dimension) ? matrix.getModule(x + 1, y) : 0) ? !matrix.invert : matrix.invert) ? 0x02 : 0x00; 1060 | value |= (((y + 1 < matrix.dimension) ? matrix.getModule(x, y + 1) : 0) ? !matrix.invert : matrix.invert) ? 0x04 : 0x00; 1061 | value |= (((y + 1 < matrix.dimension) && (x + 1 < matrix.dimension) ? matrix.getModule(x + 1, y + 1) : 0) ? !matrix.invert : matrix.invert) ? 0x08 : 0x00; 1062 | let c = options.segments[value]; 1063 | parts.push(c); 1064 | } 1065 | lines.push(parts.join('')); 1066 | } 1067 | return lines.join(options.sep); 1068 | } 1069 | 1070 | 1071 | function renderSixel(matrix, options) { 1072 | const LINE_HEIGHT = 6; 1073 | options = Object.assign({ 1074 | scale: 4, 1075 | }, options); 1076 | const parts = []; 1077 | // Enter sixel mode 1078 | parts.push('\x1BP7;1q'); // 1:1 ratio, 0 pixels remain at current color 1079 | // Set color map 1080 | parts.push('#0;2;0;0;0'); // Background 1081 | parts.push('#1;2;100;100;100'); 1082 | for (let y = -matrix.quiet * options.scale; y < (matrix.dimension + matrix.quiet) * options.scale; y += LINE_HEIGHT) { 1083 | const passes = 2; 1084 | for (let pass = 0; pass < passes; pass++) { 1085 | // Start a pass in a specific color 1086 | parts.push('#' + pass); 1087 | // Line data 1088 | for (let x = -matrix.quiet * options.scale; x < (matrix.dimension + matrix.quiet) * options.scale; x += options.scale) { 1089 | let value = 0; 1090 | for (let yy = 0; yy < LINE_HEIGHT; yy++) { 1091 | const module = (matrix.getModule(Math.floor(x / options.scale), Math.floor((y + yy) / options.scale)) ? !matrix.invert : matrix.invert) ? 0 : 1; 1092 | const bit = (module == pass) ? 1 : 0; 1093 | value |= (bit ? 0x01 : 0x00) << yy; 1094 | } 1095 | const code = (options.scale > 3 ? '!' + options.scale : '') + String.fromCharCode(value + 63).repeat(options.scale <= 3 ? options.scale : 1); 1096 | // Six pixels strip at 'scale' (repeated) width 1097 | parts.push(code); 1098 | } 1099 | // Return to start of the line 1100 | if (pass + 1 < passes) { 1101 | parts.push('$'); 1102 | } 1103 | } 1104 | // Next line 1105 | if (y + LINE_HEIGHT < (matrix.dimension + matrix.quiet) * options.scale) { 1106 | parts.push('-'); 1107 | } 1108 | } 1109 | // Exit sixel mode 1110 | parts.push('\x1B\\'); 1111 | return parts.join(''); 1112 | } 1113 | 1114 | function renderTerminalGraphicsProtocol(matrix, options) { 1115 | options = Object.assign({ 1116 | scale: 4, 1117 | alpha: false, 1118 | }, options); 1119 | const alpha = options.alpha; 1120 | const scale = options.scale; 1121 | const width = matrix.dimension + (2 * matrix.quiet); 1122 | const height = matrix.dimension + (2 * matrix.quiet); 1123 | const MAX_CHUNK_SIZE = 4096; 1124 | 1125 | // Create buffer for scaled image data 1126 | const buffer = new Uint8Array((width * scale) * (height * scale) * (alpha ? 4 : 3)); 1127 | for (let y = 0; y < height * scale; y++) { 1128 | for (let x = 0; x < width * scale; x++) { 1129 | let z = matrix.getModule(Math.floor(x / scale) - matrix.quiet, Math.floor(y / scale) - matrix.quiet); 1130 | if (matrix.invert) z = !z; 1131 | const c = z ? [0, 0, 0, 0] : [255, 255, 255, 255]; 1132 | if (c == null) continue; 1133 | const ofs = (y * (width * scale) + x) * (alpha ? 4 : 3); 1134 | buffer[ofs + 0] = c[0]; // r 1135 | buffer[ofs + 1] = c[1]; // g 1136 | buffer[ofs + 2] = c[2]; // b 1137 | if (alpha) buffer[ofs + 3] = c[3]; // a 1138 | } 1139 | } 1140 | 1141 | // Convert to base64 1142 | let encodedParts = []; 1143 | // Manual code to convert to base64 1144 | const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 1145 | for (let i = 0; i < buffer.byteLength; i += 3) { 1146 | let b1 = buffer[i]; 1147 | let b2 = (i + 1 < buffer.byteLength) ? buffer[i + 1] : 0; 1148 | let b3 = (i + 2 < buffer.byteLength) ? buffer[i + 2] : 0; 1149 | let combined = (b1 << 16) | (b2 << 8) | (b3 << 0); 1150 | encodedParts.push(base64Chars[(combined >> 18) & 0x3F]); 1151 | encodedParts.push(base64Chars[(combined >> 12) & 0x3F]); 1152 | encodedParts.push(base64Chars[(combined >> 6) & 0x3F]); 1153 | encodedParts.push(base64Chars[(combined >> 0) & 0x3F]); 1154 | } 1155 | // Handle padding 1156 | if (buffer.byteLength % 3 === 1) { 1157 | encodedParts[encodedParts.byteLength - 1] = '='; 1158 | encodedParts[encodedParts.byteLength - 2] = '='; 1159 | } else if (buffer.byteLength % 3 === 2) { 1160 | encodedParts[encodedParts.byteLength - 1] = '='; 1161 | } 1162 | const encoded = encodedParts.join(''); 1163 | 1164 | // Chunked output 1165 | const parts = []; 1166 | for (let i = 0; i < encoded.length; i += MAX_CHUNK_SIZE) { 1167 | const chunk = encoded.slice(i, i + MAX_CHUNK_SIZE); 1168 | // action transmit and display (a=T), direct transfer (t=d), uncompressed (o=), 3/4 bytes per pixel (f=24/32 bits per pixel), no responses at all (q=2) 1169 | const initialControls = (i == 0) ? `a=T,f=${alpha ? 32 : 24},s=${width * scale},v=${height * scale},t=d,q=2,` : ''; 1170 | const nonTerminal = (i + MAX_CHUNK_SIZE < encoded.length) ? 1 : 0; 1171 | parts.push(`\x1B_G${initialControls}m=${nonTerminal};${chunk}\x1B\\`); 1172 | } 1173 | 1174 | return parts.join(''); 1175 | } 1176 | 1177 | 1178 | function escape(text) { 1179 | return text.replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/\'/g, "'"); 1180 | } 1181 | 1182 | function renderSvg(matrix, options) { 1183 | options = Object.assign({ 1184 | color: 'currentColor', 1185 | moduleRound: null, // 1.0 looks nice 1186 | finderRound: null, // 0.5 looks nice 1187 | alignmentRound: null, 1188 | white: false, // Output an element for every module, not just black/dark ones but white/light ones too. 1189 | moduleSize: 1, 1190 | }, options); 1191 | 1192 | const vbTopLeft = `${-matrix.quiet - options.moduleSize / 2}`; 1193 | const vbWidthHeight = `${2 * (matrix.quiet + options.moduleSize / 2) + matrix.dimension - 1}`; 1194 | 1195 | const lines = []; 1196 | lines.push(``); 1197 | // viewport-fill=\"white\" 1198 | lines.push(``); 1199 | lines.push(`${escape(matrix.text)}`); 1200 | //lines.push(`${escape(matrix.text)}`); 1201 | lines.push(``); 1202 | 1203 | // module data bit (dark) 1204 | lines.push(``); 1205 | 1206 | // module data bit (light). 1207 | if (options.white) { // Light modules as a ref to a placeholder empty element 1208 | lines.push(``); 1209 | } 1210 | 1211 | // Use one item for the finder marker 1212 | if (options.finderRound != null) { 1213 | // Hide finder module, use finder part 1214 | lines.push(``); 1215 | if (options.white) lines.push(``); 1216 | lines.push(``); 1217 | lines.push(``); 1218 | } else { 1219 | // Use normal module for finder module, hide finder part 1220 | lines.push(``); 1221 | if (options.white) lines.push(``); 1222 | lines.push(``); 1223 | } 1224 | 1225 | // Use one item for the alignment marker 1226 | if (options.alignmentRound != null) { 1227 | // Hide alignment module, use alignment part 1228 | lines.push(``); 1229 | if (options.white) lines.push(``); 1230 | lines.push(``); 1231 | } else { 1232 | // Use normal module for alignment module, hide alignment part 1233 | lines.push(``); 1234 | if (options.white) lines.push(``); 1235 | lines.push(``); 1236 | } 1237 | 1238 | lines.push(``); 1239 | 1240 | for (let y = 0; y < matrix.dimension; y++) { 1241 | for (let x = 0; x < matrix.dimension; x++) { 1242 | const mod = matrix.identifyModule(x, y); 1243 | let bit = matrix.getModule(x, y); 1244 | // IMPORTANT: Inverting the output for SVGs will not be correct if a single finder pattern is used (it would need inverting) 1245 | if (matrix.invert) bit = !bit; 1246 | let type = bit ? 'b' : 'w'; 1247 | 1248 | // Draw finder/alignment as modules (define to nothing if drawing as whole parts) 1249 | if (mod == 'Fi' || mod == 'FI') { type = bit ? 'f' : 'fw'; } 1250 | else if (mod == 'Al' || mod == 'AL') { type = bit ? 'a' : 'aw'; } 1251 | 1252 | if (bit || options.white) { 1253 | lines.push(``); 1254 | } 1255 | } 1256 | } 1257 | 1258 | // Draw finder/alignment as whole parts (define to nothing if drawing as modules) 1259 | for (let y = 0; y < matrix.dimension; y++) { 1260 | for (let x = 0; x < matrix.dimension; x++) { 1261 | const mod = matrix.identifyModule(x, y); 1262 | let type = null; 1263 | if (mod == 'FI') type = 'fc'; 1264 | else if (mod == 'AL') type = 'ac'; 1265 | if (type == null) continue; 1266 | lines.push(``); 1267 | } 1268 | } 1269 | 1270 | lines.push(``); 1271 | 1272 | const svgString = lines.join('\n'); 1273 | return svgString; 1274 | } 1275 | 1276 | function renderSvgUri(matrix, options) { 1277 | return 'data:image/svg+xml,' + encodeURIComponent(renderSvg(matrix, options)); 1278 | } 1279 | 1280 | function renderBmp(matrix, options) { 1281 | options = Object.assign({ 1282 | scale: 8, 1283 | alpha: false, 1284 | width: null, 1285 | height: null, 1286 | }, options); 1287 | const size = matrix.dimension + 2 * matrix.quiet; 1288 | if (options.width === null) options.width = Math.floor(size * options.scale); 1289 | if (options.height === null) options.height = options.width; 1290 | 1291 | const colorData = Array(options.width * options.height).fill(null); 1292 | for (let y = 0; y < options.height; y++) { 1293 | const my = Math.floor(y * size / options.height) - matrix.quiet; 1294 | for (let x = 0; x < options.width; x++) { 1295 | const mx = Math.floor(x * size / options.width) - matrix.quiet; 1296 | let bit = matrix.getModule(mx, my); 1297 | let color; 1298 | if (matrix.invert) { 1299 | color = bit ? [255, 255, 255, 255] : [0, 0, 0, 0]; 1300 | } else { 1301 | color = bit ? [0, 0, 0, 255] : [255, 255, 255, 0]; 1302 | } 1303 | colorData[y * options.width + x] = color; 1304 | } 1305 | } 1306 | 1307 | const bmpData = BitmapGenerate(colorData, options.width, options.height, options.alpha); 1308 | return bmpData; 1309 | } 1310 | 1311 | function renderBmpUri(matrix, options) { 1312 | const bmpData = renderBmp(matrix, options); 1313 | const encoded = btoa(new Uint8Array(bmpData).reduce((data, v) => data + String.fromCharCode(v), '')) 1314 | return 'data:image/bmp;base64,' + encoded; 1315 | } 1316 | 1317 | 1318 | // Comment-out the following line to convert this into a non-module .js file (e.g. for use in a