qr 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,1261 @@
1
+ /*!
2
+ Copyright (c) 2023 Paul Miller (paulmillr.com)
3
+ The library paulmillr-qr is dual-licensed under the Apache 2.0 OR MIT license.
4
+ You can select a license of your choice.
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+
18
+ /**
19
+ * Methods for encoding (generating) QR code patterns.
20
+ * Check out decode.ts for decoding (reading).
21
+ * @module
22
+ * @example
23
+ ```js
24
+ import encodeQR from 'qr';
25
+ const txt = 'Hello world';
26
+ const ascii = encodeQR(txt, 'ascii'); // Not all fonts are supported
27
+ const terminalFriendly = encodeQR(txt, 'term'); // 2x larger, all fonts are OK
28
+ const gifBytes = encodeQR(txt, 'gif'); // Uncompressed GIF
29
+ const svgElement = encodeQR(txt, 'svg'); // SVG vector image element
30
+ const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
31
+ // import decodeQR from 'qr/decode.js';
32
+ ```
33
+ */
34
+
35
+ // We do not use newline escape code directly in strings because it's not parser-friendly
36
+ const chCodes = { newline: 10, reset: 27 };
37
+
38
+ export interface Coder<F, T> {
39
+ encode(from: F): T;
40
+ decode(to: T): F;
41
+ }
42
+
43
+ function assertNumber(n: number) {
44
+ if (!Number.isSafeInteger(n)) throw new Error(`integer expected: ${n}`);
45
+ }
46
+
47
+ function validateVersion(ver: Version): void {
48
+ if (!Number.isSafeInteger(ver) || ver < 1 || ver > 40)
49
+ throw new Error(`Invalid version=${ver}. Expected number [1..40]`);
50
+ }
51
+
52
+ function bin(dec: number, pad: number): string {
53
+ return dec.toString(2).padStart(pad, '0');
54
+ }
55
+
56
+ function mod(a: number, b: number): number {
57
+ const result = a % b;
58
+ return result >= 0 ? result : b + result;
59
+ }
60
+
61
+ function fillArr<T>(length: number, val: T): T[] {
62
+ return new Array(length).fill(val);
63
+ }
64
+
65
+ /**
66
+ * Interleaves byte blocks.
67
+ * @param blocks [[1, 2, 3], [4, 5, 6]]
68
+ * @returns [1, 4, 2, 5, 3, 6]
69
+ */
70
+ function interleaveBytes(...blocks: Uint8Array[]): Uint8Array {
71
+ let len = 0;
72
+ for (const b of blocks) len = Math.max(len, b.length);
73
+ const res = [];
74
+ for (let i = 0; i < len; i++) {
75
+ for (const b of blocks) {
76
+ if (i >= b.length) continue; // outside of block, skip
77
+ res.push(b[i]);
78
+ }
79
+ }
80
+ return new Uint8Array(res);
81
+ }
82
+
83
+ function includesAt<T>(lst: T[], pattern: T[], index: number): boolean {
84
+ if (index < 0 || index + pattern.length > lst.length) return false;
85
+ for (let i = 0; i < pattern.length; i++) if (pattern[i] !== lst[index + i]) return false;
86
+ return true;
87
+ }
88
+ // Optimize for minimal score/penalty
89
+ function best<T>(): {
90
+ add(score: number, value: T): void;
91
+ get: () => T | undefined;
92
+ score: () => number;
93
+ } {
94
+ let best: T | undefined;
95
+ let bestScore = Infinity;
96
+ return {
97
+ add(score: number, value: T): void {
98
+ if (score >= bestScore) return;
99
+ best = value;
100
+ bestScore = score;
101
+ },
102
+ get: (): T | undefined => best,
103
+ score: (): number => bestScore,
104
+ };
105
+ }
106
+
107
+ // Based on https://github.com/paulmillr/scure-base/blob/main/index.ts
108
+ function alphabet(
109
+ alphabet: string
110
+ ): Coder<number[], string[]> & { has: (char: string) => boolean } {
111
+ return {
112
+ has: (char: string) => alphabet.includes(char),
113
+ decode: (input: string[]) => {
114
+ if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string'))
115
+ throw new Error('alphabet.decode input should be array of strings');
116
+ return input.map((letter) => {
117
+ if (typeof letter !== 'string')
118
+ throw new Error(`alphabet.decode: not string element=${letter}`);
119
+ const index = alphabet.indexOf(letter);
120
+ if (index === -1) throw new Error(`Unknown letter: "${letter}". Allowed: ${alphabet}`);
121
+ return index;
122
+ });
123
+ },
124
+ encode: (digits: number[]) => {
125
+ if (!Array.isArray(digits) || (digits.length && typeof digits[0] !== 'number'))
126
+ throw new Error('alphabet.encode input should be an array of numbers');
127
+ return digits.map((i) => {
128
+ assertNumber(i);
129
+ if (i < 0 || i >= alphabet.length)
130
+ throw new Error(`Digit index outside alphabet: ${i} (alphabet: ${alphabet.length})`);
131
+ return alphabet[i];
132
+ });
133
+ },
134
+ };
135
+ }
136
+
137
+ /*
138
+ Basic bitmap structure for two colors (black & white) small images.
139
+ - undefined is used as a marker whether cell was written or not
140
+ - Internal array structure:
141
+ boolean?[y][x], where Y is row and X is column (similar to cartesian system):
142
+ ____X
143
+ |
144
+ Y |
145
+ - For most `draw` calls, structure is mutable. Reason for this:
146
+ it would be wasteful to copy full nested array structure on a single cell change
147
+ - Nested structure is easy to work with, but it can be flattened for performance
148
+ - There can be memory-efficient way to store bitmap (numbers), however, most operations
149
+ will work on a single bit anyway. It will only reduce storage without
150
+ significant performance impact, but will increase code complexity
151
+ */
152
+ export type Point = { x: number; y: number };
153
+ export type Size = { height: number; width: number };
154
+ export type Image = Size & { data: Uint8Array | Uint8ClampedArray | number[] };
155
+ type DrawValue = boolean | undefined; // undefined=not written, true=foreground, false=background
156
+ // value or fn returning value based on coords
157
+ type DrawFn = DrawValue | ((c: Point, curr: DrawValue) => DrawValue);
158
+ type ReadFn = (c: Point, curr: DrawValue) => void;
159
+ export class Bitmap {
160
+ private static size(size: Size | number, limit?: Size) {
161
+ if (typeof size === 'number') size = { height: size, width: size };
162
+ if (!Number.isSafeInteger(size.height) && size.height !== Infinity)
163
+ throw new Error(`Bitmap: invalid height=${size.height} (${typeof size.height})`);
164
+ if (!Number.isSafeInteger(size.width) && size.width !== Infinity)
165
+ throw new Error(`Bitmap: invalid width=${size.width} (${typeof size.width})`);
166
+ if (limit !== undefined) {
167
+ // Clamp length, so it won't overflow, also allows to use Infinity, so we draw until end
168
+ size = {
169
+ width: Math.min(size.width, limit.width),
170
+ height: Math.min(size.height, limit.height),
171
+ };
172
+ }
173
+ return size;
174
+ }
175
+ static fromString(s: string): Bitmap {
176
+ // Remove linebreaks on start and end, so we draw in `` section
177
+ s = s.replace(/^\n+/g, '').replace(/\n+$/g, '');
178
+ const lines = s.split(String.fromCharCode(chCodes.newline));
179
+ const height = lines.length;
180
+ const data = new Array(height);
181
+ let width: number | undefined;
182
+ for (const line of lines) {
183
+ const row = line.split('').map((i) => {
184
+ if (i === 'X') return true;
185
+ if (i === ' ') return false;
186
+ if (i === '?') return undefined;
187
+ throw new Error(`Bitmap.fromString: unknown symbol=${i}`);
188
+ });
189
+ if (width && row.length !== width)
190
+ throw new Error(`Bitmap.fromString different row sizes: width=${width} cur=${row.length}`);
191
+ width = row.length;
192
+ data.push(row);
193
+ }
194
+ if (!width) width = 0;
195
+ return new Bitmap({ height, width }, data);
196
+ }
197
+
198
+ data: DrawValue[][];
199
+ height: number;
200
+ width: number;
201
+ constructor(size: Size | number, data?: DrawValue[][]) {
202
+ const { height, width } = Bitmap.size(size);
203
+ this.data = data || Array.from({ length: height }, () => fillArr(width, undefined));
204
+ this.height = height;
205
+ this.width = width;
206
+ }
207
+ point(p: Point): DrawValue {
208
+ return this.data[p.y][p.x];
209
+ }
210
+ isInside(p: Point): boolean {
211
+ return 0 <= p.x && p.x < this.width && 0 <= p.y && p.y < this.height;
212
+ }
213
+ size(offset?: Point | number): {
214
+ height: number;
215
+ width: number;
216
+ } {
217
+ if (!offset) return { height: this.height, width: this.width };
218
+ const { x, y } = this.xy(offset);
219
+ return { height: this.height - y, width: this.width - x };
220
+ }
221
+ private xy(c: Point | number) {
222
+ if (typeof c === 'number') c = { x: c, y: c };
223
+ if (!Number.isSafeInteger(c.x)) throw new Error(`Bitmap: invalid x=${c.x}`);
224
+ if (!Number.isSafeInteger(c.y)) throw new Error(`Bitmap: invalid y=${c.y}`);
225
+ // Do modulo, so we can use negative positions
226
+ c.x = mod(c.x, this.width);
227
+ c.y = mod(c.y, this.height);
228
+ return c;
229
+ }
230
+ // Basically every operation can be represented as rect
231
+ rect(c: Point | number, size: Size | number, value: DrawFn): this {
232
+ const { x, y } = this.xy(c);
233
+ const { height, width } = Bitmap.size(size, this.size({ x, y }));
234
+ for (let yPos = 0; yPos < height; yPos++) {
235
+ for (let xPos = 0; xPos < width; xPos++) {
236
+ // NOTE: we use give function relative coordinates inside box
237
+ this.data[y + yPos][x + xPos] =
238
+ typeof value === 'function'
239
+ ? value({ x: xPos, y: yPos }, this.data[y + yPos][x + xPos])
240
+ : value;
241
+ }
242
+ }
243
+ return this;
244
+ }
245
+ // returns rectangular part of bitmap
246
+ rectRead(c: Point | number, size: Size | number, fn: ReadFn): this {
247
+ return this.rect(c, size, (c, cur) => {
248
+ fn(c, cur);
249
+ return cur;
250
+ });
251
+ }
252
+ // Horizontal & vertical lines
253
+ hLine(c: Point | number, len: number, value: DrawFn): this {
254
+ return this.rect(c, { width: len, height: 1 }, value);
255
+ }
256
+ vLine(c: Point | number, len: number, value: DrawFn): this {
257
+ return this.rect(c, { width: 1, height: len }, value);
258
+ }
259
+ // add border
260
+ border(border = 2, value: DrawValue): Bitmap {
261
+ const height = this.height + 2 * border;
262
+ const width = this.width + 2 * border;
263
+ const v = fillArr(border, value);
264
+ const h: DrawValue[][] = Array.from({ length: border }, () => fillArr(width, value));
265
+ return new Bitmap({ height, width }, [...h, ...this.data.map((i) => [...v, ...i, ...v]), ...h]);
266
+ }
267
+ // Embed another bitmap on coordinates
268
+ embed(c: Point | number, bm: Bitmap): this {
269
+ return this.rect(c, bm.size(), ({ x, y }) => bm.data[y][x]);
270
+ }
271
+ // returns rectangular part of bitmap
272
+ rectSlice(c: Point | number, size: Size | number = this.size()): Bitmap {
273
+ const rect = new Bitmap(Bitmap.size(size, this.size(this.xy(c))));
274
+ this.rect(c, size, ({ x, y }, cur) => (rect.data[y][x] = cur));
275
+ return rect;
276
+ }
277
+ // Change shape, replace rows with columns (data[y][x] -> data[x][y])
278
+ inverse(): Bitmap {
279
+ const { height, width } = this;
280
+ const res = new Bitmap({ height: width, width: height });
281
+ return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) => this.data[x][y]);
282
+ }
283
+ // Each pixel size is multiplied by factor
284
+ scale(factor: number): Bitmap {
285
+ if (!Number.isSafeInteger(factor) || factor > 1024)
286
+ throw new Error(`invalid scale factor: ${factor}`);
287
+ const { height, width } = this;
288
+ const res = new Bitmap({ height: factor * height, width: factor * width });
289
+ return res.rect(
290
+ { x: 0, y: 0 },
291
+ Infinity,
292
+ ({ x, y }) => this.data[Math.floor(y / factor)][Math.floor(x / factor)]
293
+ );
294
+ }
295
+ clone(): Bitmap {
296
+ const res = new Bitmap(this.size());
297
+ return res.rect({ x: 0, y: 0 }, this.size(), ({ x, y }) => this.data[y][x]);
298
+ }
299
+ // Ensure that there is no undefined values left
300
+ assertDrawn(): void {
301
+ this.rectRead(0, Infinity, (_, cur) => {
302
+ if (typeof cur !== 'boolean') throw new Error(`Invalid color type=${typeof cur}`);
303
+ });
304
+ }
305
+ // Simple string representation for debugging
306
+ toString(): string {
307
+ return this.data
308
+ .map((i) => i.map((j) => (j === undefined ? '?' : j ? 'X' : ' ')).join(''))
309
+ .join(String.fromCharCode(chCodes.newline));
310
+ }
311
+ toASCII(): string {
312
+ const { height, width, data } = this;
313
+ let out = '';
314
+ // Terminal character height is x2 of character width, so we process two rows of bitmap
315
+ // to produce one row of ASCII
316
+ for (let y = 0; y < height; y += 2) {
317
+ for (let x = 0; x < width; x++) {
318
+ const first = data[y][x];
319
+ const second = y + 1 >= height ? true : data[y + 1][x]; // if last row outside bitmap, make it black
320
+ if (!first && !second)
321
+ out += '█'; // both rows white (empty)
322
+ else if (!first && second)
323
+ out += '▀'; // top row white
324
+ else if (first && !second)
325
+ out += '▄'; // down row white
326
+ else if (first && second) out += ' '; // both rows black
327
+ }
328
+ out += String.fromCharCode(chCodes.newline);
329
+ }
330
+ return out;
331
+ }
332
+ toTerm(): string {
333
+ const cc = String.fromCharCode(chCodes.reset);
334
+ const reset = cc + '[0m';
335
+ const whiteBG = cc + '[1;47m ' + reset;
336
+ const darkBG = cc + `[40m ` + reset;
337
+ return this.data
338
+ .map((i) => i.map((j) => (j ? darkBG : whiteBG)).join(''))
339
+ .join(String.fromCharCode(chCodes.newline));
340
+ }
341
+ toSVG(optimize = true): string {
342
+ let out = `<svg viewBox="0 0 ${this.width} ${this.height}" xmlns="http://www.w3.org/2000/svg">`;
343
+ // Construct optimized SVG path data.
344
+ let pathData = '';
345
+ let prevPoint: Point | undefined;
346
+ this.rectRead(0, Infinity, (point, val) => {
347
+ if (!val) return;
348
+ const { x, y } = point;
349
+
350
+ if (!optimize) {
351
+ out += `<rect x="${x}" y="${y}" width="1" height="1" />`;
352
+ return;
353
+ }
354
+
355
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/d#path_commands
356
+
357
+ // Determine the shortest way to represent the initial cursor movement.
358
+ // M - Move cursor (without drawing) to absolute coordinate pair.
359
+ let m = `M${x} ${y}`;
360
+ // Only allow using the relative cursor move command if previous points
361
+ // were drawn.
362
+ if (prevPoint) {
363
+ // m - Move cursor (without drawing) to relative coordinate pair.
364
+ const relM = `m${x - prevPoint.x} ${y - prevPoint.y}`;
365
+ if (relM.length <= m.length) m = relM;
366
+ }
367
+
368
+ // Determine the shortest way to represent the cell's bottom line draw.
369
+ // H - Draw line from cursor position to absolute x coordinate.
370
+ // h - Draw line from cursor position to relative x coordinate.
371
+ const bH = x < 10 ? `H${x}` : 'h-1';
372
+
373
+ // v - Draw line from cursor position to relative y coordinate.
374
+ // Z - Close path (draws line from cursor position to M coordinate).
375
+ pathData += `${m}h1v1${bH}Z`;
376
+ prevPoint = point;
377
+ });
378
+ if (optimize) out += `<path d="${pathData}"/>`;
379
+ out += `</svg>`;
380
+ return out;
381
+ }
382
+ toGIF(): Uint8Array {
383
+ // NOTE: Small, but inefficient implementation.
384
+ // Uses 1 byte per pixel.
385
+ const u16le = (i: number) => [i & 0xff, (i >>> 8) & 0xff];
386
+ const dims = [...u16le(this.width), ...u16le(this.height)];
387
+ const data: number[] = [];
388
+ this.rectRead(0, Infinity, (_, cur) => data.push(+(cur === true)));
389
+ const N = 126; // Block size
390
+ // prettier-ignore
391
+ const bytes = [
392
+ 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, ...dims, 0xf6, 0x00, 0x00, 0xff, 0xff, 0xff,
393
+ ...fillArr(3 * 127, 0x00), 0x2c, 0x00, 0x00, 0x00, 0x00, ...dims, 0x00, 0x07
394
+ ];
395
+ const fullChunks = Math.floor(data.length / N);
396
+ // Full blocks
397
+ for (let i = 0; i < fullChunks; i++)
398
+ bytes.push(N + 1, 0x80, ...data.slice(N * i, N * (i + 1)).map((i) => +i));
399
+ // Remaining bytes
400
+ bytes.push((data.length % N) + 1, 0x80, ...data.slice(fullChunks * N).map((i) => +i));
401
+ bytes.push(0x01, 0x81, 0x00, 0x3b);
402
+ return new Uint8Array(bytes);
403
+ }
404
+ toImage(isRGB = false): Image {
405
+ const { height, width } = this.size();
406
+ const data = new Uint8Array(height * width * (isRGB ? 3 : 4));
407
+ let i = 0;
408
+ for (let y = 0; y < height; y++) {
409
+ for (let x = 0; x < width; x++) {
410
+ const value = !!this.data[y][x] ? 0 : 255;
411
+ data[i++] = value;
412
+ data[i++] = value;
413
+ data[i++] = value;
414
+ if (!isRGB) data[i++] = 255; // alpha channel
415
+ }
416
+ }
417
+ return { height, width, data };
418
+ }
419
+ }
420
+ // End of utils
421
+
422
+ // Runtime type-checking
423
+ /** Error correction mode. low: 7%, medium: 15%, quartile: 25%, high: 30% */
424
+ export const ECMode = ['low', 'medium', 'quartile', 'high'] as const;
425
+ /** Error correction mode. */
426
+ export type ErrorCorrection = (typeof ECMode)[number];
427
+ /** QR Code version. */
428
+ export type Version = number; // 1..40
429
+ /** QR Code mask type */
430
+ export type Mask = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7) & keyof typeof PATTERNS; // 0..7
431
+ /** QR Code encoding */
432
+ export const Encoding = ['numeric', 'alphanumeric', 'byte', 'kanji', 'eci'] as const;
433
+ /** QR Code encoding type */
434
+ export type EncodingType = (typeof Encoding)[number];
435
+
436
+ // Various constants & tables
437
+ // prettier-ignore
438
+ const BYTES = [
439
+ // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
440
+ 26, 44, 70, 100, 134, 172, 196, 242, 292, 346, 404, 466, 532, 581, 655, 733, 815, 901, 991, 1085,
441
+ // 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
442
+ 1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706,
443
+ ];
444
+ // prettier-ignore
445
+ const WORDS_PER_BLOCK = {
446
+ // Version 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
447
+ low: [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],
448
+ medium: [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],
449
+ quartile: [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],
450
+ high: [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],
451
+ };
452
+ // prettier-ignore
453
+ const ECC_BLOCKS = {
454
+ // Version 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
455
+ low: [ 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],
456
+ medium: [ 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],
457
+ quartile: [ 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],
458
+ high: [ 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],
459
+ };
460
+
461
+ const info = {
462
+ size: {
463
+ encode: (ver: Version) => 21 + 4 * (ver - 1), // ver1 = 21, ver40=177 blocks
464
+ decode: (size: number) => (size - 17) / 4,
465
+ } as Coder<Version, number>,
466
+ sizeType: (ver: Version) => Math.floor((ver + 7) / 17),
467
+ // Based on https://codereview.stackexchange.com/questions/74925/algorithm-to-generate-this-alignment-pattern-locations-table-for-qr-codes
468
+ alignmentPatterns(ver: Version) {
469
+ if (ver === 1) return [];
470
+ const first = 6;
471
+ const last = info.size.encode(ver) - first - 1;
472
+ const distance = last - first;
473
+ const count = Math.ceil(distance / 28);
474
+ let interval = Math.floor(distance / count);
475
+ if (interval % 2) interval += 1;
476
+ else if ((distance % count) * 2 >= count) interval += 2;
477
+ const res = [first];
478
+ for (let m = 1; m < count; m++) res.push(last - (count - m) * interval);
479
+ res.push(last);
480
+ return res;
481
+ },
482
+ ECCode: {
483
+ low: 0b01,
484
+ medium: 0b00,
485
+ quartile: 0b11,
486
+ high: 0b10,
487
+ } as Record<ErrorCorrection, number>,
488
+ formatMask: 0b101010000010010,
489
+ formatBits(ecc: ErrorCorrection, maskIdx: Mask) {
490
+ const data = (info.ECCode[ecc] << 3) | maskIdx;
491
+ let d = data;
492
+ for (let i = 0; i < 10; i++) d = (d << 1) ^ ((d >> 9) * 0b10100110111);
493
+ return ((data << 10) | d) ^ info.formatMask;
494
+ },
495
+ versionBits(ver: Version) {
496
+ let d = ver;
497
+ for (let i = 0; i < 12; i++) d = (d << 1) ^ ((d >> 11) * 0b1111100100101);
498
+ return (ver << 12) | d;
499
+ },
500
+ alphabet: {
501
+ numeric: alphabet('0123456789'),
502
+ alphanumerc: alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'),
503
+ }, // as Record<EncodingType, ReturnType<typeof alphabet>>,
504
+ lengthBits(ver: Version, type: EncodingType) {
505
+ const table: Record<EncodingType, [number, number, number]> = {
506
+ numeric: [10, 12, 14],
507
+ alphanumeric: [9, 11, 13],
508
+ byte: [8, 16, 16],
509
+ kanji: [8, 10, 12],
510
+ eci: [0, 0, 0],
511
+ };
512
+ return table[type][info.sizeType(ver)];
513
+ },
514
+ modeBits: {
515
+ numeric: '0001',
516
+ alphanumeric: '0010',
517
+ byte: '0100',
518
+ kanji: '1000',
519
+ eci: '0111',
520
+ },
521
+ capacity(ver: Version, ecc: ErrorCorrection) {
522
+ const bytes = BYTES[ver - 1];
523
+ const words = WORDS_PER_BLOCK[ecc][ver - 1];
524
+ const numBlocks = ECC_BLOCKS[ecc][ver - 1];
525
+ const blockLen = Math.floor(bytes / numBlocks) - words;
526
+ const shortBlocks = numBlocks - (bytes % numBlocks);
527
+ return {
528
+ words,
529
+ numBlocks,
530
+ shortBlocks,
531
+ blockLen,
532
+ capacity: (bytes - words * numBlocks) * 8,
533
+ total: (words + blockLen) * numBlocks + numBlocks - shortBlocks,
534
+ };
535
+ },
536
+ };
537
+
538
+ const PATTERNS: readonly ((x: number, y: number) => boolean)[] = [
539
+ (x, y) => (x + y) % 2 == 0,
540
+ (_x, y) => y % 2 == 0,
541
+ (x, _y) => x % 3 == 0,
542
+ (x, y) => (x + y) % 3 == 0,
543
+ (x, y) => (Math.floor(y / 2) + Math.floor(x / 3)) % 2 == 0,
544
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) == 0,
545
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 == 0,
546
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 == 0,
547
+ ] as const;
548
+
549
+ // Galois field && reed-solomon encoding
550
+ const GF = {
551
+ tables: ((p_poly) => {
552
+ const exp = fillArr(256, 0);
553
+ const log = fillArr(256, 0);
554
+ for (let i = 0, x = 1; i < 256; i++) {
555
+ exp[i] = x;
556
+ log[x] = i;
557
+ x <<= 1;
558
+ if (x & 0x100) x ^= p_poly;
559
+ }
560
+ return { exp, log };
561
+ })(0x11d),
562
+ exp: (x: number) => GF.tables.exp[x],
563
+ log(x: number) {
564
+ if (x === 0) throw new Error(`GF.log: invalid arg=${x}`);
565
+ return GF.tables.log[x] % 255;
566
+ },
567
+ mul(x: number, y: number) {
568
+ if (x === 0 || y === 0) return 0;
569
+ return GF.tables.exp[(GF.tables.log[x] + GF.tables.log[y]) % 255];
570
+ },
571
+ add: (x: number, y: number) => x ^ y,
572
+ pow: (x: number, e: number) => GF.tables.exp[(GF.tables.log[x] * e) % 255],
573
+ inv(x: number) {
574
+ if (x === 0) throw new Error(`GF.inverse: invalid arg=${x}`);
575
+ return GF.tables.exp[255 - GF.tables.log[x]];
576
+ },
577
+ polynomial(poly: number[]) {
578
+ if (poly.length == 0) throw new Error('GF.polymomial: invalid length');
579
+ if (poly[0] !== 0) return poly;
580
+ // Strip leading zeros
581
+ let i = 0;
582
+ for (; i < poly.length - 1 && poly[i] == 0; i++);
583
+ return poly.slice(i);
584
+ },
585
+ monomial(degree: number, coefficient: number) {
586
+ if (degree < 0) throw new Error(`GF.monomial: invalid degree=${degree}`);
587
+ if (coefficient == 0) return [0];
588
+ let coefficients = fillArr(degree + 1, 0);
589
+ coefficients[0] = coefficient;
590
+ return GF.polynomial(coefficients);
591
+ },
592
+ degree: (a: number[]) => a.length - 1,
593
+ coefficient: (a: any, degree: number) => a[GF.degree(a) - degree],
594
+ mulPoly(a: number[], b: number[]) {
595
+ if (a[0] === 0 || b[0] === 0) return [0];
596
+ const res = fillArr(a.length + b.length - 1, 0);
597
+ for (let i = 0; i < a.length; i++) {
598
+ for (let j = 0; j < b.length; j++) {
599
+ res[i + j] = GF.add(res[i + j], GF.mul(a[i], b[j]));
600
+ }
601
+ }
602
+ return GF.polynomial(res);
603
+ },
604
+ mulPolyScalar(a: number[], scalar: number) {
605
+ if (scalar == 0) return [0];
606
+ if (scalar == 1) return a;
607
+ const res = fillArr(a.length, 0);
608
+ for (let i = 0; i < a.length; i++) res[i] = GF.mul(a[i], scalar);
609
+ return GF.polynomial(res);
610
+ },
611
+ mulPolyMonomial(a: number[], degree: number, coefficient: number) {
612
+ if (degree < 0) throw new Error('GF.mulPolyMonomial: invalid degree');
613
+ if (coefficient == 0) return [0];
614
+ const res = fillArr(a.length + degree, 0);
615
+ for (let i = 0; i < a.length; i++) res[i] = GF.mul(a[i], coefficient);
616
+ return GF.polynomial(res);
617
+ },
618
+ addPoly(a: number[], b: number[]) {
619
+ if (a[0] === 0) return b;
620
+ if (b[0] === 0) return a;
621
+ let smaller = a;
622
+ let larger = b;
623
+ if (smaller.length > larger.length) [smaller, larger] = [larger, smaller];
624
+ let sumDiff = fillArr(larger.length, 0);
625
+ let lengthDiff = larger.length - smaller.length;
626
+ let s = larger.slice(0, lengthDiff);
627
+ for (let i = 0; i < s.length; i++) sumDiff[i] = s[i];
628
+ for (let i = lengthDiff; i < larger.length; i++)
629
+ sumDiff[i] = GF.add(smaller[i - lengthDiff], larger[i]);
630
+ return GF.polynomial(sumDiff);
631
+ },
632
+ remainderPoly(data: number[], divisor: number[]) {
633
+ const out = Array.from(data);
634
+ for (let i = 0; i < data.length - divisor.length + 1; i++) {
635
+ const elm = out[i];
636
+ if (elm === 0) continue;
637
+ for (let j = 1; j < divisor.length; j++) {
638
+ if (divisor[j] !== 0) out[i + j] = GF.add(out[i + j], GF.mul(divisor[j], elm));
639
+ }
640
+ }
641
+ return out.slice(data.length - divisor.length + 1, out.length);
642
+ },
643
+ divisorPoly(degree: number) {
644
+ let g = [1];
645
+ for (let i = 0; i < degree; i++) g = GF.mulPoly(g, [1, GF.pow(2, i)]);
646
+ return g;
647
+ },
648
+ evalPoly(poly: any, a: number) {
649
+ if (a == 0) return GF.coefficient(poly, 0); // Just return the x^0 coefficient
650
+ let res = poly[0];
651
+ for (let i = 1; i < poly.length; i++) res = GF.add(GF.mul(a, res), poly[i]);
652
+ return res;
653
+ },
654
+ // TODO: cleanup
655
+ euclidian(a: number[], b: number[], R: number) {
656
+ // Force degree(a) >= degree(b)
657
+ if (GF.degree(a) < GF.degree(b)) [a, b] = [b, a];
658
+ let rLast = a;
659
+ let r = b;
660
+ let tLast = [0];
661
+ let t = [1];
662
+ // while degree of Ri ≥ t/2
663
+ while (2 * GF.degree(r) >= R) {
664
+ let rLastLast = rLast;
665
+ let tLastLast = tLast;
666
+ rLast = r;
667
+ tLast = t;
668
+ if (rLast[0] === 0) throw new Error('rLast[0] === 0');
669
+ r = rLastLast;
670
+
671
+ let q = [0];
672
+ const dltInverse = GF.inv(rLast[0]);
673
+ while (GF.degree(r) >= GF.degree(rLast) && r[0] !== 0) {
674
+ const degreeDiff = GF.degree(r) - GF.degree(rLast);
675
+ const scale = GF.mul(r[0], dltInverse);
676
+ q = GF.addPoly(q, GF.monomial(degreeDiff, scale));
677
+ r = GF.addPoly(r, GF.mulPolyMonomial(rLast, degreeDiff, scale));
678
+ }
679
+ q = GF.mulPoly(q, tLast);
680
+ t = GF.addPoly(q, tLastLast);
681
+ if (GF.degree(r) >= GF.degree(rLast))
682
+ throw new Error(`Division failed r: ${r}, rLast: ${rLast}`);
683
+ }
684
+ const sigmaTildeAtZero = GF.coefficient(t, 0);
685
+ if (sigmaTildeAtZero == 0) throw new Error('sigmaTilde(0) was zero');
686
+ const inverse = GF.inv(sigmaTildeAtZero);
687
+ return [GF.mulPolyScalar(t, inverse), GF.mulPolyScalar(r, inverse)];
688
+ },
689
+ };
690
+
691
+ function RS(eccWords: number): Coder<Uint8Array, Uint8Array> {
692
+ return {
693
+ encode(from: Uint8Array) {
694
+ const d = GF.divisorPoly(eccWords);
695
+ const pol = Array.from(from);
696
+ pol.push(...d.slice(0, -1).fill(0));
697
+ return Uint8Array.from(GF.remainderPoly(pol, d));
698
+ },
699
+ decode(to: Uint8Array) {
700
+ const res = to.slice();
701
+ const poly = GF.polynomial(Array.from(to));
702
+ // Find errors
703
+ let syndrome = fillArr(eccWords, 0);
704
+ let hasError = false;
705
+ for (let i = 0; i < eccWords; i++) {
706
+ const evl = GF.evalPoly(poly, GF.exp(i));
707
+ syndrome[syndrome.length - 1 - i] = evl;
708
+ if (evl !== 0) hasError = true;
709
+ }
710
+ if (!hasError) return res;
711
+ syndrome = GF.polynomial(syndrome);
712
+ const monomial = GF.monomial(eccWords, 1);
713
+ const [errorLocator, errorEvaluator] = GF.euclidian(monomial, syndrome, eccWords);
714
+ // Error locations
715
+ const locations = fillArr(GF.degree(errorLocator), 0);
716
+ let e = 0;
717
+ for (let i = 1; i < 256 && e < locations.length; i++) {
718
+ if (GF.evalPoly(errorLocator, i) === 0) locations[e++] = GF.inv(i);
719
+ }
720
+ if (e !== locations.length) throw new Error('RS.decode: invalid errors number');
721
+ for (let i = 0; i < locations.length; i++) {
722
+ const pos = res.length - 1 - GF.log(locations[i]);
723
+ if (pos < 0) throw new Error('RS.decode: invalid error location');
724
+ const xiInverse = GF.inv(locations[i]);
725
+ let denominator = 1;
726
+ for (let j = 0; j < locations.length; j++) {
727
+ if (i === j) continue;
728
+ denominator = GF.mul(denominator, GF.add(1, GF.mul(locations[j], xiInverse)));
729
+ }
730
+ res[pos] = GF.add(
731
+ res[pos],
732
+ GF.mul(GF.evalPoly(errorEvaluator, xiInverse), GF.inv(denominator))
733
+ );
734
+ }
735
+ return res;
736
+ },
737
+ };
738
+ }
739
+
740
+ // Interleaves blocks
741
+ function interleave(ver: Version, ecc: ErrorCorrection): Coder<Uint8Array, Uint8Array> {
742
+ const { words, shortBlocks, numBlocks, blockLen, total } = info.capacity(ver, ecc);
743
+ const rs = RS(words);
744
+ return {
745
+ encode(bytes: Uint8Array) {
746
+ // Add error correction to bytes
747
+ const blocks: Uint8Array[] = [];
748
+ const eccBlocks: Uint8Array[] = [];
749
+ for (let i = 0; i < numBlocks; i++) {
750
+ const isShort = i < shortBlocks;
751
+ const len = blockLen + (isShort ? 0 : 1);
752
+ blocks.push(bytes.subarray(0, len));
753
+ eccBlocks.push(rs.encode(bytes.subarray(0, len)));
754
+ bytes = bytes.subarray(len);
755
+ }
756
+ const resBlocks = interleaveBytes(...blocks);
757
+ const resECC = interleaveBytes(...eccBlocks);
758
+ const res = new Uint8Array(resBlocks.length + resECC.length);
759
+ res.set(resBlocks);
760
+ res.set(resECC, resBlocks.length);
761
+ return res;
762
+ },
763
+ decode(data: Uint8Array) {
764
+ if (data.length !== total)
765
+ throw new Error(`interleave.decode: len(data)=${data.length}, total=${total}`);
766
+ const blocks = [];
767
+ for (let i = 0; i < numBlocks; i++) {
768
+ const isShort = i < shortBlocks;
769
+ blocks.push(new Uint8Array(words + blockLen + (isShort ? 0 : 1)));
770
+ }
771
+ // Short blocks
772
+ let pos = 0;
773
+ for (let i = 0; i < blockLen; i++) {
774
+ for (let j = 0; j < numBlocks; j++) blocks[j][i] = data[pos++];
775
+ }
776
+ // Long blocks
777
+ for (let j = shortBlocks; j < numBlocks; j++) blocks[j][blockLen] = data[pos++];
778
+ // ECC
779
+ for (let i = blockLen; i < blockLen + words; i++) {
780
+ for (let j = 0; j < numBlocks; j++) {
781
+ const isShort = j < shortBlocks;
782
+ blocks[j][i + (isShort ? 0 : 1)] = data[pos++];
783
+ }
784
+ }
785
+ // Decode
786
+ // Error-correct and copy data blocks together into a stream of bytes
787
+ const res: number[] = [];
788
+ for (const block of blocks) res.push(...Array.from(rs.decode(block)).slice(0, -words));
789
+ return Uint8Array.from(res);
790
+ },
791
+ };
792
+ }
793
+
794
+ // Draw
795
+ // Generic template per version+ecc+mask. Can be cached, to speedup calculations.
796
+ function drawTemplate(
797
+ ver: Version,
798
+ ecc: ErrorCorrection,
799
+ maskIdx: Mask,
800
+ test: boolean = false
801
+ ): Bitmap {
802
+ const size = info.size.encode(ver);
803
+ let b = new Bitmap(size + 2);
804
+ // Finder patterns
805
+ // We draw full pattern and later slice, since before addition of borders finder is truncated by one pixel on sides
806
+ const finder = new Bitmap(3).rect(0, 3, true).border(1, false).border(1, true).border(1, false);
807
+ b = b
808
+ .embed(0, finder) // top left
809
+ .embed({ x: -finder.width, y: 0 }, finder) // top right
810
+ .embed({ x: 0, y: -finder.height }, finder); // bottom left
811
+ b = b.rectSlice(1, size);
812
+ // Alignment patterns
813
+ const align = new Bitmap(1).rect(0, 1, true).border(1, false).border(1, true);
814
+ const alignPos = info.alignmentPatterns(ver);
815
+ for (const y of alignPos) {
816
+ for (const x of alignPos) {
817
+ if (b.data[y][x] !== undefined) continue;
818
+ b.embed({ x: x - 2, y: y - 2 }, align); // center of pattern should be at position
819
+ }
820
+ }
821
+ // Timing patterns
822
+ b = b
823
+ .hLine({ x: 0, y: 6 }, Infinity, ({ x }, cur) => (cur === undefined ? x % 2 == 0 : cur))
824
+ .vLine({ x: 6, y: 0 }, Infinity, ({ y }, cur) => (cur === undefined ? y % 2 == 0 : cur));
825
+ // Format information
826
+ {
827
+ const bits = info.formatBits(ecc, maskIdx);
828
+ const getBit = (i: number) => !test && ((bits >> i) & 1) == 1;
829
+ // vertical
830
+ for (let i = 0; i < 6; i++) b.data[i][8] = getBit(i); // right of top-left finder
831
+ // TODO: re-write as lines, like:
832
+ // b.vLine({ x: 8, y: 0 }, 6, ({ x, y }) => getBit(y));
833
+ for (let i = 6; i < 8; i++) b.data[i + 1][8] = getBit(i); // after timing pattern
834
+ for (let i = 8; i < 15; i++) b.data[size - 15 + i][8] = getBit(i); // right of bottom-left finder
835
+ // horizontal
836
+ for (let i = 0; i < 8; i++) b.data[8][size - i - 1] = getBit(i); // under top-right finder
837
+ for (let i = 8; i < 9; i++) b.data[8][15 - i - 1 + 1] = getBit(i); // VVV, after timing
838
+ for (let i = 9; i < 15; i++) b.data[8][15 - i - 1] = getBit(i); // under top-left finder
839
+ b.data[size - 8][8] = !test; // bottom-left finder, right
840
+ }
841
+ // Version information
842
+ if (ver >= 7) {
843
+ const bits = info.versionBits(ver);
844
+ for (let i = 0; i < 18; i += 1) {
845
+ const bit = !test && ((bits >> i) & 1) == 1;
846
+ const x = Math.floor(i / 3);
847
+ const y = (i % 3) + size - 8 - 3;
848
+ // two copies
849
+ b.data[x][y] = bit;
850
+ b.data[y][x] = bit;
851
+ }
852
+ }
853
+ return b;
854
+ }
855
+ // zigzag: bottom->top && top->bottom
856
+ function zigzag(
857
+ tpl: Bitmap,
858
+ maskIdx: Mask,
859
+ fn: (x: number, y: number, mask: boolean) => void
860
+ ): void {
861
+ const size = tpl.height;
862
+ const pattern = PATTERNS[maskIdx];
863
+ // zig-zag pattern
864
+ let dir = -1;
865
+ let y = size - 1;
866
+ // two columns at time
867
+ for (let xOffset = size - 1; xOffset > 0; xOffset -= 2) {
868
+ if (xOffset == 6) xOffset = 5; // skip vertical timing pattern
869
+ for (; ; y += dir) {
870
+ for (let j = 0; j < 2; j += 1) {
871
+ const x = xOffset - j;
872
+ if (tpl.data[y][x] !== undefined) continue; // skip already written elements
873
+ fn(x, y, pattern(x, y));
874
+ }
875
+ if (y + dir < 0 || y + dir >= size) break;
876
+ }
877
+ dir = -dir; // change direction
878
+ }
879
+ }
880
+
881
+ // NOTE: byte encoding is just representation, QR works with strings only. Most decoders will fail on raw byte array,
882
+ // since they expect unicode or other text encoding inside bytes
883
+ function detectType(str: string): EncodingType {
884
+ let type: EncodingType = 'numeric';
885
+ for (let x of str) {
886
+ if (info.alphabet.numeric.has(x)) continue;
887
+ type = 'alphanumeric';
888
+ if (!info.alphabet.alphanumerc.has(x)) return 'byte';
889
+ }
890
+ return type;
891
+ }
892
+
893
+ // Global symbols in both browsers and Node.js since v11
894
+ // See https://github.com/microsoft/TypeScript/issues/31535
895
+ declare const TextEncoder: any;
896
+ /**
897
+ * @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99])
898
+ */
899
+ export function utf8ToBytes(str: string): Uint8Array {
900
+ if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
901
+ return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
902
+ }
903
+
904
+ function encode(ver: Version, ecc: ErrorCorrection, data: string, type: EncodingType): Uint8Array {
905
+ let encoded = '';
906
+ let dataLen = data.length;
907
+ if (type === 'numeric') {
908
+ const t = info.alphabet.numeric.decode(data.split(''));
909
+ const n = t.length;
910
+ for (let i = 0; i < n - 2; i += 3) encoded += bin(t[i] * 100 + t[i + 1] * 10 + t[i + 2], 10);
911
+ if (n % 3 === 1) {
912
+ encoded += bin(t[n - 1], 4);
913
+ } else if (n % 3 === 2) {
914
+ encoded += bin(t[n - 2] * 10 + t[n - 1], 7);
915
+ }
916
+ } else if (type === 'alphanumeric') {
917
+ const t = info.alphabet.alphanumerc.decode(data.split(''));
918
+ const n = t.length;
919
+ for (let i = 0; i < n - 1; i += 2) encoded += bin(t[i] * 45 + t[i + 1], 11);
920
+ if (n % 2 == 1) encoded += bin(t[n - 1], 6); // pad if odd number of chars
921
+ } else if (type === 'byte') {
922
+ const utf8 = utf8ToBytes(data);
923
+ dataLen = utf8.length;
924
+ encoded = Array.from(utf8)
925
+ .map((i) => bin(i, 8))
926
+ .join('');
927
+ } else {
928
+ throw new Error('encode: unsupported type');
929
+ }
930
+ const { capacity } = info.capacity(ver, ecc);
931
+ const len = bin(dataLen, info.lengthBits(ver, type));
932
+ let bits = info.modeBits[type] + len + encoded;
933
+ if (bits.length > capacity) throw new Error('Capacity overflow');
934
+ // Terminator
935
+ bits += '0'.repeat(Math.min(4, Math.max(0, capacity - bits.length)));
936
+ // Pad bits string untill full byte
937
+ if (bits.length % 8) bits += '0'.repeat(8 - (bits.length % 8));
938
+ // Add padding until capacity is full
939
+ const padding = '1110110000010001';
940
+ for (let idx = 0; bits.length !== capacity; idx++) bits += padding[idx % padding.length];
941
+ // Convert a bitstring to array of bytes
942
+ const bytes = Uint8Array.from(bits.match(/(.{8})/g)!.map((i) => Number(`0b${i}`)));
943
+ return interleave(ver, ecc).encode(bytes);
944
+ }
945
+
946
+ // DRAW
947
+
948
+ function drawQR(
949
+ ver: Version,
950
+ ecc: ErrorCorrection,
951
+ data: Uint8Array,
952
+ maskIdx: Mask,
953
+ test: boolean = false
954
+ ): Bitmap {
955
+ const b = drawTemplate(ver, ecc, maskIdx, test);
956
+ let i = 0;
957
+ const need = 8 * data.length;
958
+ zigzag(b, maskIdx, (x, y, mask) => {
959
+ let value = false;
960
+ if (i < need) {
961
+ value = ((data[i >>> 3] >> ((7 - i) & 7)) & 1) !== 0;
962
+ i++;
963
+ }
964
+ b.data[y][x] = value !== mask; // !== as xor
965
+ });
966
+ if (i !== need) throw new Error('QR: bytes left after draw');
967
+ return b;
968
+ }
969
+
970
+ function penalty(bm: Bitmap): number {
971
+ const inverse = bm.inverse();
972
+ // Adjacent modules in row/column in same | No. of modules = (5 + i) color
973
+ const sameColor = (row: DrawValue[]) => {
974
+ let res = 0;
975
+ for (let i = 0, same = 1, last = undefined; i < row.length; i++) {
976
+ if (last === row[i]) {
977
+ same++;
978
+ if (i !== row.length - 1) continue; // handle last element
979
+ }
980
+ if (same >= 5) res += 3 + (same - 5);
981
+ last = row[i];
982
+ same = 1;
983
+ }
984
+ return res;
985
+ };
986
+ let adjacent = 0;
987
+ bm.data.forEach((row) => (adjacent += sameColor(row)));
988
+ inverse.data.forEach((column) => (adjacent += sameColor(column)));
989
+ // Block of modules in same color (Block size = 2x2)
990
+ let box = 0;
991
+ let b = bm.data;
992
+ const lastW = bm.width - 1;
993
+ const lastH = bm.height - 1;
994
+ for (let x = 0; x < lastW; x++) {
995
+ for (let y = 0; y < lastH; y++) {
996
+ const x1 = x + 1;
997
+ const y1 = y + 1;
998
+ if (b[x][y] === b[x1][y] && b[x1][y] === b[x][y1] && b[x1][y] === b[x1][y1]) {
999
+ box += 3;
1000
+ }
1001
+ }
1002
+ }
1003
+ // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
1004
+ const finderPattern = (row: DrawValue[]) => {
1005
+ const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
1006
+ const lightPattern = [false, false, false, false]; // light area 4 modules wide
1007
+ const p1 = [...finderPattern, ...lightPattern];
1008
+ const p2 = [...lightPattern, ...finderPattern];
1009
+ let res = 0;
1010
+ for (let i = 0; i < row.length; i++) {
1011
+ if (includesAt(row, p1, i)) res += 40;
1012
+ if (includesAt(row, p2, i)) res += 40;
1013
+ }
1014
+ return res;
1015
+ };
1016
+ let finder = 0;
1017
+ for (const row of bm.data) finder += finderPattern(row);
1018
+ for (const column of inverse.data) finder += finderPattern(column);
1019
+ // Proportion of dark modules in entire symbol
1020
+ // Add 10 points to a deviation of 5% increment or decrement in the proportion
1021
+ // ratio of dark module from the referential 50%
1022
+ let darkPixels = 0;
1023
+ bm.rectRead(0, Infinity, (_c, val) => (darkPixels += val ? 1 : 0));
1024
+ const darkPercent = (darkPixels / (bm.height * bm.width)) * 100;
1025
+ const dark = 10 * Math.floor(Math.abs(darkPercent - 50) / 5);
1026
+ return adjacent + box + finder + dark;
1027
+ }
1028
+ // Selects best mask according to penalty, if no mask is provided
1029
+ function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskIdx?: Mask) {
1030
+ if (maskIdx === undefined) {
1031
+ const bestMask = best<Mask>();
1032
+ for (let mask = 0; mask < PATTERNS.length; mask++)
1033
+ bestMask.add(penalty(drawQR(ver, ecc, data, mask as Mask, true)), mask as Mask);
1034
+ maskIdx = bestMask.get();
1035
+ }
1036
+ if (maskIdx === undefined) throw new Error('Cannot find mask'); // Should never happen
1037
+ return drawQR(ver, ecc, data, maskIdx);
1038
+ }
1039
+
1040
+ /** QR Code generation options. */
1041
+ export type QrOpts = {
1042
+ ecc?: ErrorCorrection | undefined;
1043
+ encoding?: EncodingType | undefined;
1044
+ version?: Version | undefined;
1045
+ mask?: number | undefined;
1046
+ border?: number | undefined;
1047
+ scale?: number | undefined;
1048
+ };
1049
+ export type SvgQrOpts = {
1050
+ /**
1051
+ * Controls how cells are generated within the SVG.
1052
+ *
1053
+ * If `true`:
1054
+ * - Cells are drawn using a single `path` element.
1055
+ * - Pro: significantly reduces the size of the QR code (>70% smaller than
1056
+ * unoptimized).
1057
+ * - Con: less flexible with visually customizing cell shapes.
1058
+ *
1059
+ * If `false`:
1060
+ * - Each cell is drawn with its own `rect` element.
1061
+ * - Pro: allows more flexibility with visually customizing cells shapes.
1062
+ * - Con: significantly increases the QR code size (>230% larger than
1063
+ * optimized).
1064
+ *
1065
+ * @default true
1066
+ */
1067
+ optimize?: boolean | undefined;
1068
+ };
1069
+ function validateECC(ec: ErrorCorrection) {
1070
+ if (!ECMode.includes(ec))
1071
+ throw new Error(`Invalid error correction mode=${ec}. Expected: ${ECMode}`);
1072
+ }
1073
+ function validateEncoding(enc: EncodingType) {
1074
+ if (!Encoding.includes(enc))
1075
+ throw new Error(`Encoding: invalid mode=${enc}. Expected: ${Encoding}`);
1076
+ if (enc === 'kanji' || enc === 'eci')
1077
+ throw new Error(`Encoding: ${enc} is not supported (yet?).`);
1078
+ }
1079
+ function validateMask(mask: Mask) {
1080
+ if (![0, 1, 2, 3, 4, 5, 6, 7].includes(mask) || !PATTERNS[mask])
1081
+ throw new Error(`Invalid mask=${mask}. Expected number [0..7]`);
1082
+ }
1083
+ export type Output = 'raw' | 'ascii' | 'term' | 'gif' | 'svg';
1084
+
1085
+ /**
1086
+ * Encodes (creates / generates) QR code.
1087
+ * @param text text that would be encoded
1088
+ * @param output output type: raw, ascii, svg, gif, or term
1089
+ * @param opts
1090
+ * @example
1091
+ ```js
1092
+ const txt = 'Hello world';
1093
+ const ascii = encodeQR(txt, 'ascii'); // Not all fonts are supported
1094
+ const terminalFriendly = encodeQR(txt, 'term'); // 2x larger, all fonts are OK
1095
+ const gifBytes = encodeQR(txt, 'gif'); // Uncompressed GIF
1096
+ const svgElement = encodeQR(txt, 'svg'); // SVG vector image element
1097
+ const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
1098
+ ```
1099
+ */
1100
+ export function encodeQR(text: string, output: 'raw', opts?: QrOpts): boolean[][];
1101
+ export function encodeQR(text: string, output: 'ascii' | 'term', opts?: QrOpts): string;
1102
+ export function encodeQR(text: string, output: 'svg', opts?: QrOpts & SvgQrOpts): string;
1103
+ export function encodeQR(text: string, output: 'gif', opts?: QrOpts): Uint8Array;
1104
+ export function encodeQR(text: string, output: Output = 'raw', opts: QrOpts & SvgQrOpts = {}) {
1105
+ const ecc = opts.ecc !== undefined ? opts.ecc : 'medium';
1106
+ validateECC(ecc);
1107
+ const encoding = opts.encoding !== undefined ? opts.encoding : detectType(text);
1108
+ validateEncoding(encoding);
1109
+ if (opts.mask !== undefined) validateMask(opts.mask as Mask);
1110
+ let ver = opts.version;
1111
+ let data,
1112
+ err = new Error('Unknown error');
1113
+ if (ver !== undefined) {
1114
+ validateVersion(ver);
1115
+ data = encode(ver, ecc, text, encoding);
1116
+ } else {
1117
+ // If no version is provided, try to find smallest one which fits
1118
+ // Currently just scans all version, can be significantly speedup if needed
1119
+ for (let i = 1; i <= 40; i++) {
1120
+ try {
1121
+ data = encode(i, ecc, text, encoding);
1122
+ ver = i;
1123
+ break;
1124
+ } catch (e) {
1125
+ err = e as Error;
1126
+ }
1127
+ }
1128
+ }
1129
+ if (!ver || !data) throw err;
1130
+ let res = drawQRBest(ver, ecc, data, opts.mask as Mask);
1131
+ res.assertDrawn();
1132
+ const border = opts.border === undefined ? 2 : opts.border;
1133
+ if (!Number.isSafeInteger(border)) throw new Error(`invalid border type=${typeof border}`);
1134
+ res = res.border(border, false); // Add border
1135
+ if (opts.scale !== undefined) res = res.scale(opts.scale); // Scale image
1136
+ if (output === 'raw') return res.data;
1137
+ else if (output === 'ascii') return res.toASCII();
1138
+ else if (output === 'svg') return res.toSVG(opts.optimize);
1139
+ else if (output === 'gif') return res.toGIF();
1140
+ else if (output === 'term') return res.toTerm();
1141
+ else throw new Error(`Unknown output: ${output}`);
1142
+ }
1143
+
1144
+ export default encodeQR;
1145
+
1146
+ export const utils: {
1147
+ best: typeof best;
1148
+ bin: typeof bin;
1149
+ drawTemplate: typeof drawTemplate;
1150
+ fillArr: typeof fillArr;
1151
+ info: {
1152
+ size: Coder<Version, number>;
1153
+ sizeType: (ver: Version) => number;
1154
+ // Based on https://codereview.stackexchange.com/questions/74925/algorithm-to-generate-this-alignment-pattern-locations-table-for-qr-codes
1155
+ alignmentPatterns(ver: Version): number[];
1156
+ ECCode: Record<ErrorCorrection, number>;
1157
+ formatMask: number;
1158
+ formatBits(ecc: ErrorCorrection, maskIdx: Mask): number;
1159
+ versionBits(ver: Version): number;
1160
+ alphabet: {
1161
+ numeric: Coder<number[], string[]> & {
1162
+ has: (char: string) => boolean;
1163
+ };
1164
+ alphanumerc: Coder<number[], string[]> & {
1165
+ has: (char: string) => boolean;
1166
+ };
1167
+ };
1168
+ lengthBits(ver: Version, type: EncodingType): number;
1169
+ modeBits: {
1170
+ numeric: string;
1171
+ alphanumeric: string;
1172
+ byte: string;
1173
+ kanji: string;
1174
+ eci: string;
1175
+ };
1176
+ capacity(
1177
+ ver: Version,
1178
+ ecc: ErrorCorrection
1179
+ ): {
1180
+ words: number;
1181
+ numBlocks: number;
1182
+ shortBlocks: number;
1183
+ blockLen: number;
1184
+ capacity: number;
1185
+ total: number;
1186
+ };
1187
+ };
1188
+ interleave: typeof interleave;
1189
+ validateVersion: typeof validateVersion;
1190
+ zigzag: typeof zigzag;
1191
+ } = {
1192
+ best,
1193
+ bin,
1194
+ drawTemplate,
1195
+ fillArr,
1196
+ info,
1197
+ interleave,
1198
+ validateVersion,
1199
+ zigzag,
1200
+ };
1201
+
1202
+ // Unsafe API utils, exported only for tests
1203
+ export const _tests: {
1204
+ Bitmap: typeof Bitmap;
1205
+ info: {
1206
+ size: Coder<Version, number>;
1207
+ sizeType: (ver: Version) => number;
1208
+ // Based on https://codereview.stackexchange.com/questions/74925/algorithm-to-generate-this-alignment-pattern-locations-table-for-qr-codes
1209
+ alignmentPatterns(ver: Version): number[];
1210
+ ECCode: Record<ErrorCorrection, number>;
1211
+ formatMask: number;
1212
+ formatBits(ecc: ErrorCorrection, maskIdx: Mask): number;
1213
+ versionBits(ver: Version): number;
1214
+ alphabet: {
1215
+ numeric: Coder<number[], string[]> & {
1216
+ has: (char: string) => boolean;
1217
+ };
1218
+ alphanumerc: Coder<number[], string[]> & {
1219
+ has: (char: string) => boolean;
1220
+ };
1221
+ };
1222
+ lengthBits(ver: Version, type: EncodingType): number;
1223
+ modeBits: {
1224
+ numeric: string;
1225
+ alphanumeric: string;
1226
+ byte: string;
1227
+ kanji: string;
1228
+ eci: string;
1229
+ };
1230
+ capacity(
1231
+ ver: Version,
1232
+ ecc: ErrorCorrection
1233
+ ): {
1234
+ words: number;
1235
+ numBlocks: number;
1236
+ shortBlocks: number;
1237
+ blockLen: number;
1238
+ capacity: number;
1239
+ total: number;
1240
+ };
1241
+ };
1242
+ detectType: typeof detectType;
1243
+ encode: typeof encode;
1244
+ drawQR: typeof drawQR;
1245
+ penalty: typeof penalty;
1246
+ PATTERNS: readonly ((x: number, y: number) => boolean)[];
1247
+ } = {
1248
+ Bitmap,
1249
+ info,
1250
+ detectType,
1251
+ encode,
1252
+ drawQR,
1253
+ penalty,
1254
+ PATTERNS,
1255
+ };
1256
+ // Type tests
1257
+ // const o1 = qr('test', 'ascii');
1258
+ // const o2 = qr('test', 'raw');
1259
+ // const o3 = qr('test', 'gif');
1260
+ // const o4 = qr('test', 'svg');
1261
+ // const o5 = qr('test', 'term');