├── .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 |
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 |
89 |
Simpler codes are easier to scan:
90 |
91 |
Keep the input as short as possible.
92 |
If using a URL with a common top-level-domain (such as .com), some readers work without an initial http:// or https://, but probably best not to rely on this.
93 |
All-numeric text is the most compact, but all upper-case alphanumeric (with some symbols allowed) is more compat than mixed-case.
94 |
Do not use upper case for the URL past the domain name without first checking the server responds correctly.
95 |
96 |
97 |
Include a written alternative for the URL.
98 |
For WiFi QR codes, use the format: WIFI:S:my_ssid;T:WPA;P:my_passphrase;;
99 |
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(``);
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