qr 0.5.4 → 0.6.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/README.md +43 -36
- package/decode.d.ts +100 -13
- package/decode.d.ts.map +1 -1
- package/decode.js +623 -153
- package/decode.js.map +1 -1
- package/dom.d.ts +110 -13
- package/dom.d.ts.map +1 -1
- package/dom.js +138 -18
- package/dom.js.map +1 -1
- package/index.d.ts +175 -35
- package/index.d.ts.map +1 -1
- package/index.js +276 -83
- package/index.js.map +1 -1
- package/package.json +8 -4
- package/src/decode.ts +648 -177
- package/src/dom.ts +169 -31
- package/src/index.ts +500 -129
package/src/index.ts
CHANGED
|
@@ -32,11 +32,128 @@ const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
|
|
|
32
32
|
```
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Bytes API type helpers for old + new TypeScript.
|
|
37
|
+
*
|
|
38
|
+
* TS 5.6 has `Uint8Array`, while TS 5.9+ made it generic `Uint8Array<ArrayBuffer>`.
|
|
39
|
+
* We can't use specific return type, because TS 5.6 will error.
|
|
40
|
+
* We can't use generic return type, because most TS 5.9 software will expect specific type.
|
|
41
|
+
*
|
|
42
|
+
* Maps typed-array input leaves to broad forms.
|
|
43
|
+
* These are compatibility adapters, not ownership guarantees.
|
|
44
|
+
*
|
|
45
|
+
* - `TArg` keeps byte inputs broad.
|
|
46
|
+
* - `TRet` marks byte outputs for TS 5.6 and TS 5.9+ compatibility.
|
|
47
|
+
*/
|
|
48
|
+
export type TypedArg<T> = T extends BigInt64Array
|
|
49
|
+
? BigInt64Array
|
|
50
|
+
: T extends BigUint64Array
|
|
51
|
+
? BigUint64Array
|
|
52
|
+
: T extends Float32Array
|
|
53
|
+
? Float32Array
|
|
54
|
+
: T extends Float64Array
|
|
55
|
+
? Float64Array
|
|
56
|
+
: T extends Int16Array
|
|
57
|
+
? Int16Array
|
|
58
|
+
: T extends Int32Array
|
|
59
|
+
? Int32Array
|
|
60
|
+
: T extends Int8Array
|
|
61
|
+
? Int8Array
|
|
62
|
+
: T extends Uint16Array
|
|
63
|
+
? Uint16Array
|
|
64
|
+
: T extends Uint32Array
|
|
65
|
+
? Uint32Array
|
|
66
|
+
: T extends Uint8ClampedArray
|
|
67
|
+
? Uint8ClampedArray
|
|
68
|
+
: T extends Uint8Array
|
|
69
|
+
? Uint8Array
|
|
70
|
+
: never;
|
|
71
|
+
/** Maps typed-array output leaves to narrow TS-compatible forms. */
|
|
72
|
+
export type TypedRet<T> = T extends BigInt64Array
|
|
73
|
+
? ReturnType<typeof BigInt64Array.of>
|
|
74
|
+
: T extends BigUint64Array
|
|
75
|
+
? ReturnType<typeof BigUint64Array.of>
|
|
76
|
+
: T extends Float32Array
|
|
77
|
+
? ReturnType<typeof Float32Array.of>
|
|
78
|
+
: T extends Float64Array
|
|
79
|
+
? ReturnType<typeof Float64Array.of>
|
|
80
|
+
: T extends Int16Array
|
|
81
|
+
? ReturnType<typeof Int16Array.of>
|
|
82
|
+
: T extends Int32Array
|
|
83
|
+
? ReturnType<typeof Int32Array.of>
|
|
84
|
+
: T extends Int8Array
|
|
85
|
+
? ReturnType<typeof Int8Array.of>
|
|
86
|
+
: T extends Uint16Array
|
|
87
|
+
? ReturnType<typeof Uint16Array.of>
|
|
88
|
+
: T extends Uint32Array
|
|
89
|
+
? ReturnType<typeof Uint32Array.of>
|
|
90
|
+
: T extends Uint8ClampedArray
|
|
91
|
+
? ReturnType<typeof Uint8ClampedArray.of>
|
|
92
|
+
: T extends Uint8Array
|
|
93
|
+
? ReturnType<typeof Uint8Array.of>
|
|
94
|
+
: never;
|
|
95
|
+
/** Recursively adapts byte-carrying API input types. See {@link TypedArg}. */
|
|
96
|
+
export type TArg<T> =
|
|
97
|
+
| T
|
|
98
|
+
| ([TypedArg<T>] extends [never]
|
|
99
|
+
? T extends (...args: infer A) => infer R
|
|
100
|
+
? ((...args: { [K in keyof A]: TRet<A[K]> }) => TArg<R>) & {
|
|
101
|
+
[K in keyof T]: T[K] extends (...args: any) => any ? T[K] : TArg<T[K]>;
|
|
102
|
+
}
|
|
103
|
+
: T extends [infer A, ...infer R]
|
|
104
|
+
? [TArg<A>, ...{ [K in keyof R]: TArg<R[K]> }]
|
|
105
|
+
: T extends readonly [infer A, ...infer R]
|
|
106
|
+
? readonly [TArg<A>, ...{ [K in keyof R]: TArg<R[K]> }]
|
|
107
|
+
: T extends (infer A)[]
|
|
108
|
+
? TArg<A>[]
|
|
109
|
+
: T extends readonly (infer A)[]
|
|
110
|
+
? readonly TArg<A>[]
|
|
111
|
+
: T extends Promise<infer A>
|
|
112
|
+
? Promise<TArg<A>>
|
|
113
|
+
: T extends object
|
|
114
|
+
? { [K in keyof T]: TArg<T[K]> }
|
|
115
|
+
: T
|
|
116
|
+
: TypedArg<T>);
|
|
117
|
+
/** Recursively adapts byte-carrying API output types. See {@link TypedArg}. */
|
|
118
|
+
export type TRet<T> = T extends unknown
|
|
119
|
+
? T &
|
|
120
|
+
([TypedRet<T>] extends [never]
|
|
121
|
+
? T extends (...args: infer A) => infer R
|
|
122
|
+
? ((...args: { [K in keyof A]: TArg<A[K]> }) => TRet<R>) & {
|
|
123
|
+
[K in keyof T]: T[K] extends (...args: any) => any ? T[K] : TRet<T[K]>;
|
|
124
|
+
}
|
|
125
|
+
: T extends [infer A, ...infer R]
|
|
126
|
+
? [TRet<A>, ...{ [K in keyof R]: TRet<R[K]> }]
|
|
127
|
+
: T extends readonly [infer A, ...infer R]
|
|
128
|
+
? readonly [TRet<A>, ...{ [K in keyof R]: TRet<R[K]> }]
|
|
129
|
+
: T extends (infer A)[]
|
|
130
|
+
? TRet<A>[]
|
|
131
|
+
: T extends readonly (infer A)[]
|
|
132
|
+
? readonly TRet<A>[]
|
|
133
|
+
: T extends Promise<infer A>
|
|
134
|
+
? Promise<TRet<A>>
|
|
135
|
+
: T extends object
|
|
136
|
+
? { [K in keyof T]: TRet<T[K]> }
|
|
137
|
+
: T
|
|
138
|
+
: TypedRet<T>)
|
|
139
|
+
: never;
|
|
140
|
+
|
|
35
141
|
// We do not use newline escape code directly in strings because it's not parser-friendly
|
|
36
142
|
const chCodes = { newline: 10, reset: 27 };
|
|
37
143
|
|
|
144
|
+
/** Bidirectional codec interface. */
|
|
38
145
|
export interface Coder<F, T> {
|
|
146
|
+
/**
|
|
147
|
+
* Encodes a source value into the target representation.
|
|
148
|
+
* @param from - Source value to encode.
|
|
149
|
+
* @returns Encoded representation.
|
|
150
|
+
*/
|
|
39
151
|
encode(from: F): T;
|
|
152
|
+
/**
|
|
153
|
+
* Decodes a target value back into the source representation.
|
|
154
|
+
* @param to - Encoded representation to decode.
|
|
155
|
+
* @returns Decoded source value.
|
|
156
|
+
*/
|
|
40
157
|
decode(to: T): F;
|
|
41
158
|
}
|
|
42
159
|
|
|
@@ -59,6 +176,7 @@ function mod(a: number, b: number): number {
|
|
|
59
176
|
}
|
|
60
177
|
|
|
61
178
|
function fillArr<T>(length: number, val: T): T[] {
|
|
179
|
+
// Current callers only pass primitive fill values; object fills would alias references.
|
|
62
180
|
return new Array(length).fill(val);
|
|
63
181
|
}
|
|
64
182
|
|
|
@@ -73,7 +191,7 @@ function popcnt(n: number): number {
|
|
|
73
191
|
* @param blocks [[1, 2, 3], [4, 5, 6]]
|
|
74
192
|
* @returns [1, 4, 2, 5, 3, 6]
|
|
75
193
|
*/
|
|
76
|
-
function interleaveBytes(blocks: Uint8Array[]): Uint8Array {
|
|
194
|
+
function interleaveBytes(blocks: TArg<Uint8Array[]>): TRet<Uint8Array> {
|
|
77
195
|
let maxLen = 0;
|
|
78
196
|
let totalLen = 0;
|
|
79
197
|
for (const block of blocks) {
|
|
@@ -83,13 +201,15 @@ function interleaveBytes(blocks: Uint8Array[]): Uint8Array {
|
|
|
83
201
|
|
|
84
202
|
const result = new Uint8Array(totalLen);
|
|
85
203
|
let idx = 0;
|
|
204
|
+
// When block lengths differ, callers must pass the shorter blocks first so
|
|
205
|
+
// the interleaving order matches ISO/IEC 18004 §7.6 c).
|
|
86
206
|
for (let i = 0; i < maxLen; i++) {
|
|
87
207
|
for (const block of blocks) {
|
|
88
208
|
if (i < block.length) result[idx++] = block[i];
|
|
89
209
|
}
|
|
90
210
|
}
|
|
91
211
|
|
|
92
|
-
return result
|
|
212
|
+
return result as TRet<Uint8Array>;
|
|
93
213
|
}
|
|
94
214
|
|
|
95
215
|
// Optimize for minimal score/penalty
|
|
@@ -102,6 +222,7 @@ function best<T>(): {
|
|
|
102
222
|
let bestScore = Infinity;
|
|
103
223
|
return {
|
|
104
224
|
add(score: number, value: T): void {
|
|
225
|
+
// Ties keep the first candidate so equal-score selections stay deterministic.
|
|
105
226
|
if (score >= bestScore) return;
|
|
106
227
|
best = value;
|
|
107
228
|
bestScore = score;
|
|
@@ -115,7 +236,8 @@ function best<T>(): {
|
|
|
115
236
|
function alphabet(
|
|
116
237
|
alphabet: string
|
|
117
238
|
): Coder<number[], string[]> & { has: (char: string) => boolean } {
|
|
118
|
-
|
|
239
|
+
// Character order defines the numeric values used by the target QR mode.
|
|
240
|
+
return Object.freeze({
|
|
119
241
|
has: (char: string) => alphabet.includes(char),
|
|
120
242
|
decode: (input: string[]) => {
|
|
121
243
|
if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string'))
|
|
@@ -138,12 +260,12 @@ function alphabet(
|
|
|
138
260
|
return alphabet[i];
|
|
139
261
|
});
|
|
140
262
|
},
|
|
141
|
-
};
|
|
263
|
+
});
|
|
142
264
|
}
|
|
143
265
|
|
|
144
266
|
// Transpose 32x32 bit matrix in-place
|
|
145
267
|
// a[0..31] are 32 rows of 32 bits each; after transpose they become 32 columns.
|
|
146
|
-
function transpose32(a: Uint32Array) {
|
|
268
|
+
function transpose32(a: TArg<Uint32Array>) {
|
|
147
269
|
if (a.length !== 32) throw new Error('expects 32 element matrix');
|
|
148
270
|
const masks = [0x55555555, 0x33333333, 0x0f0f0f0f, 0x00ff00ff, 0x0000ffff] as const;
|
|
149
271
|
// Hello again, FFT
|
|
@@ -168,6 +290,8 @@ const bitMask = (x: number): number => (1 << (x & 31)) >>> 0;
|
|
|
168
290
|
const rangeMask = (shift: number, len: number): number => {
|
|
169
291
|
// len in [0..32], shift in [0..31]
|
|
170
292
|
if (len === 0) return 0;
|
|
293
|
+
// Callers only request len=32 for word-aligned spans; JS shift counts wrap at 32,
|
|
294
|
+
// so full-word masks must bypass the generic `(1 << len)` path.
|
|
171
295
|
if (len === 32) return 0xffffffff;
|
|
172
296
|
return (((1 << len) - 1) << shift) >>> 0;
|
|
173
297
|
};
|
|
@@ -186,13 +310,41 @@ Basic bitmap structure for two colors (black & white) small images.
|
|
|
186
310
|
will work on a single bit anyway. It will only reduce storage without
|
|
187
311
|
significant performance impact, but will increase code complexity
|
|
188
312
|
*/
|
|
189
|
-
|
|
190
|
-
export type
|
|
191
|
-
|
|
313
|
+
/** Two-dimensional point. */
|
|
314
|
+
export type Point = {
|
|
315
|
+
/** Horizontal coordinate. */
|
|
316
|
+
x: number;
|
|
317
|
+
/** Vertical coordinate. */
|
|
318
|
+
y: number;
|
|
319
|
+
};
|
|
320
|
+
/** Width and height pair. */
|
|
321
|
+
export type Size = {
|
|
322
|
+
/** Pixel height. */
|
|
323
|
+
height: number;
|
|
324
|
+
/** Pixel width. */
|
|
325
|
+
width: number;
|
|
326
|
+
};
|
|
327
|
+
/** Raster image used by the encoder and decoder. */
|
|
328
|
+
export type Image = Size & {
|
|
329
|
+
/** Row-major RGB or RGBA pixel data. */
|
|
330
|
+
data: Uint8Array | Uint8ClampedArray | number[];
|
|
331
|
+
};
|
|
192
332
|
type DrawValue = boolean | undefined; // undefined=not written, true=foreground, false=background
|
|
193
333
|
// value or fn returning value based on coords
|
|
194
334
|
type DrawFn = DrawValue | ((c: Point, curr: DrawValue) => DrawValue);
|
|
195
335
|
type ReadFn = (c: Point, curr: DrawValue) => void;
|
|
336
|
+
/**
|
|
337
|
+
* Mutable monochrome bitmap used as the internal QR representation.
|
|
338
|
+
* @param size - Square edge length or explicit bitmap dimensions.
|
|
339
|
+
* @param data - Optional row-major pixel matrix using `true`, `false`, or `undefined`.
|
|
340
|
+
* @example
|
|
341
|
+
* Create a bitmap, then scale it for display.
|
|
342
|
+
* ```ts
|
|
343
|
+
* import { Bitmap } from 'qr';
|
|
344
|
+
* const bitmap = Bitmap.fromString('X \n X');
|
|
345
|
+
* bitmap.scale(2);
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
196
348
|
export class Bitmap {
|
|
197
349
|
private static size(size: Size | number, limit?: Size) {
|
|
198
350
|
if (typeof size === 'number') size = { height: size, width: size };
|
|
@@ -211,6 +363,8 @@ export class Bitmap {
|
|
|
211
363
|
}
|
|
212
364
|
static fromString(s: string): Bitmap {
|
|
213
365
|
// Remove linebreaks on start and end, so we draw in `` section
|
|
366
|
+
// Fixture strings use LF-delimited rows of X / space / ? characters; callers
|
|
367
|
+
// must normalize CRLF input before handing it to this debug parser.
|
|
214
368
|
s = s.replace(/^\n+/g, '').replace(/\n+$/g, '');
|
|
215
369
|
const lines = s.split(String.fromCharCode(chCodes.newline));
|
|
216
370
|
const height = lines.length;
|
|
@@ -244,6 +398,14 @@ export class Bitmap {
|
|
|
244
398
|
width: number;
|
|
245
399
|
constructor(size: Size | number, data?: DrawValue[][]) {
|
|
246
400
|
const { height, width } = Bitmap.size(size);
|
|
401
|
+
// Bitmap coordinates wrap through modulo for negative positions, so invalid
|
|
402
|
+
// dimensions produce NaN, aliasing, or unsafe allocation sizes before later
|
|
403
|
+
// drawing no-op guards can run. `Infinity` is only valid for rectangle sizes
|
|
404
|
+
// that are clamped against an existing positive bitmap.
|
|
405
|
+
if (!Number.isSafeInteger(height) || height <= 0)
|
|
406
|
+
throw new Error(`Bitmap: invalid height=${height}, expected positive safe integer dimension`);
|
|
407
|
+
if (!Number.isSafeInteger(width) || width <= 0)
|
|
408
|
+
throw new Error(`Bitmap: invalid width=${width}, expected positive safe integer dimension`);
|
|
247
409
|
this.height = height;
|
|
248
410
|
this.width = width;
|
|
249
411
|
this.tailMask = rangeMask(0, width & 31 || 32);
|
|
@@ -264,8 +426,13 @@ export class Bitmap {
|
|
|
264
426
|
}
|
|
265
427
|
}
|
|
266
428
|
point(p: Point): DrawValue {
|
|
429
|
+
// The storage docs above say "undefined is used as a marker whether cell
|
|
430
|
+
// was written or not"; `point()` is the detector's dark-module read and
|
|
431
|
+
// intentionally treats both undefined and false as not-dark. Use
|
|
432
|
+
// `isDefined()` when the written/undefined distinction matters.
|
|
267
433
|
return this.get(p.x, p.y);
|
|
268
434
|
}
|
|
435
|
+
// Raw bounds check for scan loops; unlike `xy()`, this does not wrap or normalize coordinates.
|
|
269
436
|
isInside(p: Point): boolean {
|
|
270
437
|
return 0 <= p.x && p.x < this.width && 0 <= p.y && p.y < this.height;
|
|
271
438
|
}
|
|
@@ -278,7 +445,8 @@ export class Bitmap {
|
|
|
278
445
|
if (typeof c === 'number') c = { x: c, y: c };
|
|
279
446
|
if (!Number.isSafeInteger(c.x)) throw new Error(`Bitmap: invalid x=${c.x}`);
|
|
280
447
|
if (!Number.isSafeInteger(c.y)) throw new Error(`Bitmap: invalid y=${c.y}`);
|
|
281
|
-
//
|
|
448
|
+
// Bitmap's class docs say "For most `draw` calls, structure is mutable";
|
|
449
|
+
// coordinate objects follow that hot-path policy too and are normalized in place.
|
|
282
450
|
c.x = mod(c.x, this.width);
|
|
283
451
|
c.y = mod(c.y, this.height);
|
|
284
452
|
return c;
|
|
@@ -293,6 +461,10 @@ export class Bitmap {
|
|
|
293
461
|
return { word: this.wordIndex(x, y), bit: x & 31 };
|
|
294
462
|
}
|
|
295
463
|
isDefined(x: number, y: number): boolean {
|
|
464
|
+
// `isInside()` is the raw bounds check; keep these bitset accessors
|
|
465
|
+
// bounds-check-free for hot paths. Invalid tail coordinates may observe
|
|
466
|
+
// backing-word bits, so callers that accept untrusted coordinates must
|
|
467
|
+
// check `isInside()` first.
|
|
296
468
|
const wi = this.wordIndex(x, y);
|
|
297
469
|
const m = bitMask(x);
|
|
298
470
|
return (this.defined[wi] & m) !== 0;
|
|
@@ -305,10 +477,14 @@ export class Bitmap {
|
|
|
305
477
|
private maskWord(wi: number, mask: number, v: boolean): void {
|
|
306
478
|
const { defined, value } = this;
|
|
307
479
|
defined[wi] |= mask;
|
|
480
|
+
// `-v` expands the boolean to either all-zero or all-one bits before masking it into the selected lanes.
|
|
308
481
|
value[wi] = (value[wi] & ~mask) | (-v & mask);
|
|
309
482
|
}
|
|
310
483
|
set(x: number, y: number, v: DrawValue): void {
|
|
484
|
+
// `undefined` means "leave the current cell unchanged", not "clear it back to undefined".
|
|
311
485
|
if (v === undefined) return;
|
|
486
|
+
// Like `get()` / `isDefined()`, this is a raw in-bounds bitset accessor;
|
|
487
|
+
// check `isInside()` before passing untrusted coordinates.
|
|
312
488
|
this.maskWord(this.wordIndex(x, y), bitMask(x), v);
|
|
313
489
|
}
|
|
314
490
|
// word-span fill for constant values (fast path)
|
|
@@ -328,6 +504,7 @@ export class Bitmap {
|
|
|
328
504
|
continue;
|
|
329
505
|
}
|
|
330
506
|
this.maskWord(rowBase + startWord, rangeMask(startBit, 32 - startBit), v);
|
|
507
|
+
// Whole interior words can be written directly: every bit in the span becomes defined and equal to v.
|
|
331
508
|
for (let i = startWord + 1; i < endWord; i++) {
|
|
332
509
|
defined[rowBase + i] = 0xffffffff;
|
|
333
510
|
value[rowBase + i] = v ? 0xffffffff : 0;
|
|
@@ -348,6 +525,7 @@ export class Bitmap {
|
|
|
348
525
|
const bitX = x + xPos;
|
|
349
526
|
const { bit, word } = this.bitIndex(bitX, Py);
|
|
350
527
|
const bitsPerWord = Math.min(32 - bit, width - xPos);
|
|
528
|
+
// bitX stays absolute for word-local masks; xPos/yPos stay rectangle-local for rect callbacks.
|
|
351
529
|
cb(word, bitX, xPos, yPos, bitsPerWord);
|
|
352
530
|
xPos += bitsPerWord;
|
|
353
531
|
}
|
|
@@ -367,7 +545,11 @@ export class Bitmap {
|
|
|
367
545
|
let valWord = value[wi];
|
|
368
546
|
for (let b = 0; b < n; b++) {
|
|
369
547
|
const mask = bitMask(bitX + b);
|
|
548
|
+
// As with `point()`, callback `cur` is a dark/not-dark read; the
|
|
549
|
+
// storage-level "undefined is used as a marker whether cell was
|
|
550
|
+
// written or not" distinction is checked separately with `isDefined()`.
|
|
370
551
|
const res = fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
|
|
552
|
+
// Returning undefined from the callback keeps the existing cell unchanged.
|
|
371
553
|
if (res === undefined) continue;
|
|
372
554
|
defWord |= mask;
|
|
373
555
|
valWord = (valWord & ~mask) | (-res & mask);
|
|
@@ -386,6 +568,8 @@ export class Bitmap {
|
|
|
386
568
|
const valWord = value[wi];
|
|
387
569
|
for (let b = 0; b < n; b++) {
|
|
388
570
|
const mask = bitMask(bitX + b);
|
|
571
|
+
// rectRead is non-mutating; callback coordinates are rectangle-local,
|
|
572
|
+
// and `cur` is the same dark/not-dark read as `point()`.
|
|
389
573
|
fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
|
|
390
574
|
}
|
|
391
575
|
});
|
|
@@ -400,6 +584,10 @@ export class Bitmap {
|
|
|
400
584
|
}
|
|
401
585
|
// add border
|
|
402
586
|
border(border = 2, value: DrawValue): Bitmap {
|
|
587
|
+
// `border` is used both as output-size delta and as embed coordinate; keep
|
|
588
|
+
// it a positive safe integer before those paths allocate or normalize.
|
|
589
|
+
if (!Number.isSafeInteger(border) || border <= 0)
|
|
590
|
+
throw new Error(`Bitmap.border: invalid size=${border}`);
|
|
403
591
|
const height = this.height + 2 * border;
|
|
404
592
|
const width = this.width + 2 * border;
|
|
405
593
|
const out = new Bitmap({ height, width });
|
|
@@ -415,6 +603,10 @@ export class Bitmap {
|
|
|
415
603
|
if (width <= 0 || height <= 0) return this;
|
|
416
604
|
const { value, defined } = this;
|
|
417
605
|
const { words: srcStride, value: srcValue } = src;
|
|
606
|
+
// The Bitmap storage docs say "undefined is used as a marker whether cell
|
|
607
|
+
// was written or not"; `embed()` is the packed blit path for materialized
|
|
608
|
+
// source bitmaps, so it flattens the source rectangle to defined dark/light
|
|
609
|
+
// bits instead of treating undefined cells as transparent.
|
|
418
610
|
for (let yPos = 0; yPos < height; yPos++) {
|
|
419
611
|
const srcRow = yPos * srcStride;
|
|
420
612
|
for (let xPos = 0; xPos < width; ) {
|
|
@@ -424,6 +616,7 @@ export class Bitmap {
|
|
|
424
616
|
const len = Math.min(32 - dstBit, width - xPos);
|
|
425
617
|
const w0 = srcValue[srcWord];
|
|
426
618
|
const w1 = srcBit && srcWord + 1 < srcRow + srcStride ? srcValue[srcWord + 1] : 0;
|
|
619
|
+
// Source and destination bit offsets may differ, so assemble the source span from up to two words.
|
|
427
620
|
const sVal = srcBit ? ((w0 >>> srcBit) | (w1 << (32 - srcBit))) >>> 0 : w0;
|
|
428
621
|
const dstMask = rangeMask(dstBit, len);
|
|
429
622
|
const valBits = ((sVal & rangeMask(0, len)) << dstBit) >>> 0;
|
|
@@ -440,6 +633,7 @@ export class Bitmap {
|
|
|
440
633
|
const { height, width } = Bitmap.size(size, this.size({ x, y }));
|
|
441
634
|
const rect = new Bitmap({ height, width });
|
|
442
635
|
this.rectRead({ x, y }, { height, width }, (p, cur) => {
|
|
636
|
+
// rectRead reports undefined cells as false, so copy only when the source defined bit is set.
|
|
443
637
|
if (this.isDefined(x + p.x, y + p.y)) {
|
|
444
638
|
rect.set(p.x, p.y, cur);
|
|
445
639
|
}
|
|
@@ -483,6 +677,9 @@ export class Bitmap {
|
|
|
483
677
|
negate(): Bitmap {
|
|
484
678
|
const n = this.defined.length;
|
|
485
679
|
for (let i = 0; i < n; i++) {
|
|
680
|
+
// ISO/IEC 18004:2024 §12 b)5 says to "reverse the colouring of the light
|
|
681
|
+
// and dark pixels"; this dense scratch-bitmap operation materializes every
|
|
682
|
+
// backing bit as defined and does not preserve sparse/undefined cells.
|
|
486
683
|
this.value[i] = ~this.value[i];
|
|
487
684
|
this.defined[i] = 0xffffffff;
|
|
488
685
|
}
|
|
@@ -493,6 +690,11 @@ export class Bitmap {
|
|
|
493
690
|
if (!Number.isSafeInteger(factor) || factor > 1024)
|
|
494
691
|
throw new Error(`invalid scale factor: ${factor}`);
|
|
495
692
|
const { height, width } = this;
|
|
693
|
+
// Bitmap storage docs say "undefined is used as a marker whether cell was
|
|
694
|
+
// written or not"; `scale()` is an output materialization path and samples
|
|
695
|
+
// with `get()`, so sparse cells become defined light cells. Positive output
|
|
696
|
+
// dimensions stay validated by the Bitmap constructor instead of duplicating
|
|
697
|
+
// dimension checks in every caller that computes a new bitmap size.
|
|
496
698
|
const res = new Bitmap({ height: factor * height, width: factor * width });
|
|
497
699
|
return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) =>
|
|
498
700
|
this.get((x / factor) | 0, (y / factor) | 0)
|
|
@@ -518,9 +720,13 @@ export class Bitmap {
|
|
|
518
720
|
}
|
|
519
721
|
}
|
|
520
722
|
countPatternInRow(y: number, patternLen: number, ...patterns: number[]): number {
|
|
521
|
-
|
|
723
|
+
// Penalty scanning only passes Table 11 windows over bounded symbol rows;
|
|
724
|
+
// validate this public helper before JS shifts / typed-array reads coerce bad inputs.
|
|
725
|
+
if (!Number.isSafeInteger(patternLen) || patternLen <= 0 || patternLen >= 32)
|
|
726
|
+
throw new Error('wrong patternLen');
|
|
522
727
|
const mask = (1 << patternLen) - 1;
|
|
523
|
-
const { width, value, words } = this;
|
|
728
|
+
const { height, width, value, words } = this;
|
|
729
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= height) return 0;
|
|
524
730
|
let count = 0;
|
|
525
731
|
const rowBase = this.wordIndex(0, y);
|
|
526
732
|
for (let i = 0, window = 0; i < words; i++) {
|
|
@@ -539,8 +745,12 @@ export class Bitmap {
|
|
|
539
745
|
return count;
|
|
540
746
|
}
|
|
541
747
|
getRuns(y: number, fn: (len: number, value: boolean) => void): void {
|
|
542
|
-
const { width, value, words } = this;
|
|
748
|
+
const { height, width, value, words } = this;
|
|
543
749
|
if (width === 0) return;
|
|
750
|
+
// ISO/IEC 18004:2024 §7.8.3.1 N1 scans adjacent modules in bounded rows
|
|
751
|
+
// and columns; validate this public helper before missing typed-array rows
|
|
752
|
+
// are coerced into all-light runs by bitwise operators.
|
|
753
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= height) return;
|
|
544
754
|
let runLen = 0;
|
|
545
755
|
let runValue: boolean | undefined;
|
|
546
756
|
const rowBase = this.wordIndex(0, y);
|
|
@@ -572,10 +782,12 @@ export class Bitmap {
|
|
|
572
782
|
return count;
|
|
573
783
|
}
|
|
574
784
|
countBoxes2x2(y: number): number {
|
|
575
|
-
const { width, words } = this;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
785
|
+
const { height, width, words } = this;
|
|
786
|
+
// ISO/IEC 18004:2024 §7.8.3.1 N2 counts 2 x 2 module blocks in bounded
|
|
787
|
+
// rows; reject non-integer scan rows before bitwise coercions truncate them.
|
|
788
|
+
if (width < 2 || !Number.isSafeInteger(y) || y < 0 || y + 1 >= height) return 0;
|
|
789
|
+
const base0 = this.wordIndex(0, y);
|
|
790
|
+
const base1 = this.wordIndex(0, y + 1);
|
|
579
791
|
// valid "left-edge" positions x in [0 .. W-2]
|
|
580
792
|
const tailBits = width & 31;
|
|
581
793
|
const validLast = tailBits === 0 ? 0x7fffffff : rangeMask(0, (width - 1) & 31);
|
|
@@ -613,6 +825,9 @@ export class Bitmap {
|
|
|
613
825
|
const out: DrawValue[][] = Array.from({ length: this.height }, () => new Array(this.width));
|
|
614
826
|
for (let y = 0; y < this.height; y++) {
|
|
615
827
|
const row = out[y];
|
|
828
|
+
// Bitmap storage docs say "undefined is used as a marker whether cell was
|
|
829
|
+
// written or not"; `toRaw()` is the materialized dark/not-dark output path
|
|
830
|
+
// used after `encodeQR()` asserts the QR symbol is fully drawn.
|
|
616
831
|
for (let x = 0; x < this.width; x++) row[x] = this.get(x, y);
|
|
617
832
|
}
|
|
618
833
|
return out;
|
|
@@ -656,6 +871,8 @@ export class Bitmap {
|
|
|
656
871
|
}
|
|
657
872
|
toSVG(optimize = true): string {
|
|
658
873
|
let out = `<svg viewBox="0 0 ${this.width} ${this.height}" xmlns="http://www.w3.org/2000/svg">`;
|
|
874
|
+
// ISO/IEC 18004:2024 §5.1 c) / §5.3.8: this SVG draws only dark modules;
|
|
875
|
+
// callers must render it on a light background for light modules and the quiet zone.
|
|
659
876
|
// Construct optimized SVG path data.
|
|
660
877
|
let pathData = '';
|
|
661
878
|
let prevPoint: Point | undefined;
|
|
@@ -701,7 +918,9 @@ export class Bitmap {
|
|
|
701
918
|
const u16le = (i: number) => [i & 0xff, (i >>> 8) & 0xff];
|
|
702
919
|
const dims = [...u16le(this.width), ...u16le(this.height)];
|
|
703
920
|
const data: number[] = [];
|
|
921
|
+
// Palette index 0 is white/light and index 1 is black/dark; rectRead maps undefined cells to light.
|
|
704
922
|
this.rectRead(0, Infinity, (_, cur) => data.push(+(cur === true)));
|
|
923
|
+
// Each chunk starts with an LZW clear code; 126 raw pixels keep codes at 8 bits until the next clear.
|
|
705
924
|
const N = 126; // Block size
|
|
706
925
|
// prettier-ignore
|
|
707
926
|
const bytes = [
|
|
@@ -736,20 +955,27 @@ export class Bitmap {
|
|
|
736
955
|
// End of utils
|
|
737
956
|
|
|
738
957
|
// Runtime type-checking
|
|
739
|
-
/** Error correction mode. low: 7%, medium: 15%, quartile: 25%, high: 30
|
|
740
|
-
export const ECMode
|
|
741
|
-
|
|
958
|
+
/** Error correction mode. low: 7%, medium: 15%, quartile: 25%, high: 30%. */
|
|
959
|
+
export const ECMode: readonly ['low', 'medium', 'quartile', 'high'] = /* @__PURE__ */ Object.freeze(
|
|
960
|
+
['low', 'medium', 'quartile', 'high']
|
|
961
|
+
);
|
|
962
|
+
/** Error correction mode name. */
|
|
742
963
|
export type ErrorCorrection = (typeof ECMode)[number];
|
|
743
|
-
/** QR Code version. */
|
|
964
|
+
/** QR Code version in the `[1..40]` range. */
|
|
744
965
|
export type Version = number; // 1..40
|
|
745
|
-
/** QR Code mask
|
|
966
|
+
/** QR Code mask index. */
|
|
746
967
|
export type Mask = (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7) & keyof typeof PATTERNS; // 0..7
|
|
747
|
-
/**
|
|
748
|
-
|
|
749
|
-
|
|
968
|
+
/**
|
|
969
|
+
* QR payload compaction mode names recognized by the type/validator.
|
|
970
|
+
* `kanji` and `eci` are spec modes, but `encodeQR` currently rejects them until implemented.
|
|
971
|
+
*/
|
|
972
|
+
export const Encoding: readonly ['numeric', 'alphanumeric', 'byte', 'kanji', 'eci'] =
|
|
973
|
+
/* @__PURE__ */ Object.freeze(['numeric', 'alphanumeric', 'byte', 'kanji', 'eci']);
|
|
974
|
+
/** QR payload encoding name. */
|
|
750
975
|
export type EncodingType = (typeof Encoding)[number];
|
|
751
976
|
|
|
752
977
|
// Various constants & tables
|
|
978
|
+
// ISO/IEC 18004:2024 Table 1: QR symbol codeword capacity by version (data plus error correction).
|
|
753
979
|
// prettier-ignore
|
|
754
980
|
const BYTES = [
|
|
755
981
|
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
|
@@ -757,6 +983,7 @@ const BYTES = [
|
|
|
757
983
|
// 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
|
|
758
984
|
1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706,
|
|
759
985
|
];
|
|
986
|
+
// ISO/IEC 18004:2024 Table 9: error correction codewords per block by version and level.
|
|
760
987
|
// prettier-ignore
|
|
761
988
|
const WORDS_PER_BLOCK = {
|
|
762
989
|
// 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
|
|
@@ -765,6 +992,7 @@ const WORDS_PER_BLOCK = {
|
|
|
765
992
|
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],
|
|
766
993
|
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],
|
|
767
994
|
};
|
|
995
|
+
// ISO/IEC 18004:2024 Table 9: error correction block count by version and level.
|
|
768
996
|
// prettier-ignore
|
|
769
997
|
const ECC_BLOCKS = {
|
|
770
998
|
// 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
|
|
@@ -774,12 +1002,15 @@ const ECC_BLOCKS = {
|
|
|
774
1002
|
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],
|
|
775
1003
|
};
|
|
776
1004
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1005
|
+
// ISO/IEC 18004:2024 sections 5.3/7.4/7.5/7.9/7.10: QR layout, segment, format/version, and capacity helpers.
|
|
1006
|
+
const info = /* @__PURE__ */ Object.freeze({
|
|
1007
|
+
size: /* @__PURE__ */ Object.freeze({
|
|
1008
|
+
encode: (ver: Version) => 21 + 4 * (ver - 1), // ver1 = 21, ver40 = 177 modules per side
|
|
780
1009
|
decode: (size: number) => (size - 17) / 4,
|
|
781
|
-
} as Coder<Version, number
|
|
1010
|
+
} as Coder<Version, number>),
|
|
1011
|
+
// ISO/IEC 18004:2024 Table 3: map version ranges 1-9, 10-26, and 27-40 to count-width indexes.
|
|
782
1012
|
sizeType: (ver: Version) => Math.floor((ver + 7) / 17),
|
|
1013
|
+
// ISO/IEC 18004:2024 Annex E Table E.1: row/column coordinate list of alignment-pattern centres.
|
|
783
1014
|
// Based on https://codereview.stackexchange.com/questions/74925/algorithm-to-generate-this-alignment-pattern-locations-table-for-qr-codes
|
|
784
1015
|
alignmentPatterns(ver: Version) {
|
|
785
1016
|
if (ver === 1) return [];
|
|
@@ -795,28 +1026,36 @@ const info = {
|
|
|
795
1026
|
res.push(last);
|
|
796
1027
|
return res;
|
|
797
1028
|
},
|
|
798
|
-
|
|
1029
|
+
// ISO/IEC 18004:2024 §7.9.1 Table 12: error-correction-level indicators for the top two format-information data bits.
|
|
1030
|
+
ECCode: /* @__PURE__ */ Object.freeze({
|
|
799
1031
|
low: 0b01,
|
|
800
1032
|
medium: 0b00,
|
|
801
1033
|
quartile: 0b11,
|
|
802
1034
|
high: 0b10,
|
|
803
|
-
} as Record<ErrorCorrection, number
|
|
1035
|
+
} as Record<ErrorCorrection, number>),
|
|
1036
|
+
// ISO/IEC 18004:2024 §7.9.1 final paragraph: XOR the 15-bit format information with mask pattern 101010000010010.
|
|
804
1037
|
formatMask: 0b101010000010010,
|
|
1038
|
+
// ISO/IEC 18004:2024 §7.9.1 / Annex C.2: append the 10-bit BCH remainder for the 5 data bits, then apply the fixed QR format mask.
|
|
805
1039
|
formatBits(ecc: ErrorCorrection, maskIdx: Mask) {
|
|
806
1040
|
const data = (info.ECCode[ecc] << 3) | maskIdx;
|
|
807
1041
|
let d = data;
|
|
808
1042
|
for (let i = 0; i < 10; i++) d = (d << 1) ^ ((d >> 9) * 0b10100110111);
|
|
809
1043
|
return ((data << 10) | d) ^ info.formatMask;
|
|
810
1044
|
},
|
|
1045
|
+
// ISO/IEC 18004:2024 §7.10 / Annex D.2: append the 12-bit Golay remainder to the 6-bit version word; version information is not masked.
|
|
811
1046
|
versionBits(ver: Version) {
|
|
812
1047
|
let d = ver;
|
|
813
1048
|
for (let i = 0; i < 12; i++) d = (d << 1) ^ ((d >> 11) * 0b1111100100101);
|
|
814
1049
|
return (ver << 12) | d;
|
|
815
1050
|
},
|
|
816
|
-
|
|
1051
|
+
// ISO/IEC 18004:2024 §7.3.3 / §7.3.4 / §7.4.5 Table 5: character-set membership and value codecs for numeric and alphanumeric QR modes.
|
|
1052
|
+
alphabet: /* @__PURE__ */ Object.freeze({
|
|
1053
|
+
// ISO/IEC 18004:2024 §7.3.3 / §7.4.4: numeric-mode digits map directly to values 0..9 before 3-digit grouping.
|
|
817
1054
|
numeric: alphabet('0123456789'),
|
|
1055
|
+
// ISO/IEC 18004:2024 §7.3.4 / §7.4.5 Table 5: 45-character alphanumeric-mode value order used for 11-bit pair packing. Keep the legacy `alphanumerc` key name in sync with existing callers.
|
|
818
1056
|
alphanumerc: alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'),
|
|
819
|
-
}, // as Record<EncodingType, ReturnType<typeof alphabet>>,
|
|
1057
|
+
}), // as Record<EncodingType, ReturnType<typeof alphabet>>,
|
|
1058
|
+
// ISO/IEC 18004:2024 Table 3 gives QR character-count widths for data modes; ECI headers instead carry only the mode indicator plus the designator from §7.4.3.
|
|
820
1059
|
lengthBits(ver: Version, type: EncodingType) {
|
|
821
1060
|
const table: Record<EncodingType, [number, number, number]> = {
|
|
822
1061
|
numeric: [10, 12, 14],
|
|
@@ -827,13 +1066,15 @@ const info = {
|
|
|
827
1066
|
};
|
|
828
1067
|
return table[type][info.sizeType(ver)];
|
|
829
1068
|
},
|
|
830
|
-
|
|
1069
|
+
// ISO/IEC 18004:2024 §7.4.2 Table 2: 4-bit QR mode indicators for the segment types this library models.
|
|
1070
|
+
modeBits: /* @__PURE__ */ Object.freeze({
|
|
831
1071
|
numeric: '0001',
|
|
832
1072
|
alphanumeric: '0010',
|
|
833
1073
|
byte: '0100',
|
|
834
1074
|
kanji: '1000',
|
|
835
1075
|
eci: '0111',
|
|
836
|
-
},
|
|
1076
|
+
}),
|
|
1077
|
+
// ISO/IEC 18004:2024 Table 1 / §7.5.1 Table 9: derive total data bits and short/long RS block layout from total codewords, ECC words per block, and block counts.
|
|
837
1078
|
capacity(ver: Version, ecc: ErrorCorrection) {
|
|
838
1079
|
const bytes = BYTES[ver - 1];
|
|
839
1080
|
const words = WORDS_PER_BLOCK[ecc][ver - 1];
|
|
@@ -849,20 +1090,22 @@ const info = {
|
|
|
849
1090
|
total: (words + blockLen) * numBlocks + numBlocks - shortBlocks,
|
|
850
1091
|
};
|
|
851
1092
|
},
|
|
852
|
-
};
|
|
1093
|
+
});
|
|
853
1094
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
(
|
|
857
|
-
(
|
|
858
|
-
(x,
|
|
859
|
-
(x, y) => (
|
|
860
|
-
(x, y) => ((
|
|
861
|
-
(x, y) => ((
|
|
862
|
-
(x, y) => (((x
|
|
863
|
-
|
|
1095
|
+
// ISO/IEC 18004:2024 Table 10: QR data-mask predicates 000..111, written here in (x column, y row) form.
|
|
1096
|
+
const PATTERNS: readonly ((x: number, y: number) => boolean)[] = /* @__PURE__ */ Object.freeze([
|
|
1097
|
+
(x: number, y: number) => (x + y) % 2 == 0,
|
|
1098
|
+
(_x: number, y: number) => y % 2 == 0,
|
|
1099
|
+
(x: number, _y: number) => x % 3 == 0,
|
|
1100
|
+
(x: number, y: number) => (x + y) % 3 == 0,
|
|
1101
|
+
(x: number, y: number) => (Math.floor(y / 2) + Math.floor(x / 3)) % 2 == 0,
|
|
1102
|
+
(x: number, y: number) => ((x * y) % 2) + ((x * y) % 3) == 0,
|
|
1103
|
+
(x: number, y: number) => (((x * y) % 2) + ((x * y) % 3)) % 2 == 0,
|
|
1104
|
+
(x: number, y: number) => (((x + y) % 2) + ((x * y) % 3)) % 2 == 0,
|
|
1105
|
+
] as const);
|
|
864
1106
|
|
|
865
1107
|
// Galois field && reed-solomon encoding
|
|
1108
|
+
// ISO/IEC 18004:2024 §7.5.2 / Annex A / Annex B: GF(2^8) field and polynomial helpers shared by QR Reed-Solomon parity generation and decoding.
|
|
866
1109
|
const GF = {
|
|
867
1110
|
tables: ((p_poly) => {
|
|
868
1111
|
const exp = fillArr(256, 0);
|
|
@@ -873,23 +1116,40 @@ const GF = {
|
|
|
873
1116
|
x <<= 1;
|
|
874
1117
|
if (x & 0x100) x ^= p_poly;
|
|
875
1118
|
}
|
|
1119
|
+
// Keep α^255 = 1 in exp[255]; GF.log() folds the matching log[1] = 255
|
|
1120
|
+
// back to 0 with `% 255`, so later helpers can wrap exponents without a special case.
|
|
876
1121
|
return { exp, log };
|
|
877
1122
|
})(0x11d),
|
|
1123
|
+
// Raw α^i lookup from the precomputed field table; callers are expected
|
|
1124
|
+
// to reduce / validate exponents before indexing it.
|
|
878
1125
|
exp: (x: number) => GF.tables.exp[x],
|
|
1126
|
+
// log(0) is undefined in GF(2^8); `% 255` also folds the wrapped table
|
|
1127
|
+
// entry for α^255 = 1 back to exponent 0.
|
|
879
1128
|
log(x: number) {
|
|
880
1129
|
if (x === 0) throw new Error(`GF.log: invalid arg=${x}`);
|
|
881
1130
|
return GF.tables.log[x] % 255;
|
|
882
1131
|
},
|
|
1132
|
+
// Zero has no logarithm in GF(2^8), so it must short-circuit here; all
|
|
1133
|
+
// other products are α^(log(x) + log(y) mod 255) in the reviewed field.
|
|
883
1134
|
mul(x: number, y: number) {
|
|
884
1135
|
if (x === 0 || y === 0) return 0;
|
|
885
1136
|
return GF.tables.exp[(GF.tables.log[x] + GF.tables.log[y]) % 255];
|
|
886
1137
|
},
|
|
1138
|
+
// In characteristic 2 fields, addition and subtraction are the same
|
|
1139
|
+
// bitwise XOR operation used by the QR Reed-Solomon arithmetic.
|
|
887
1140
|
add: (x: number, y: number) => x ^ y,
|
|
1141
|
+
// Raw nonzero field power helper. Current QR use is GF.pow(2, i) for the
|
|
1142
|
+
// Annex A generator factors; x = 0 or negative exponents are not validated.
|
|
888
1143
|
pow: (x: number, e: number) => GF.tables.exp[(GF.tables.log[x] * e) % 255],
|
|
1144
|
+
// Multiplicative inverse for nonzero field elements. Current callers only
|
|
1145
|
+
// use it on values already known to be nonzero; 0 has no inverse in GF(2^8).
|
|
889
1146
|
inv(x: number) {
|
|
890
1147
|
if (x === 0) throw new Error(`GF.inverse: invalid arg=${x}`);
|
|
891
1148
|
return GF.tables.exp[255 - GF.tables.log[x]];
|
|
892
1149
|
},
|
|
1150
|
+
// Canonicalize coefficient arrays by trimming leading zero coefficients
|
|
1151
|
+
// while preserving `[0]` as the zero polynomial; already-normalized inputs
|
|
1152
|
+
// are returned by reference.
|
|
893
1153
|
polynomial(poly: number[]) {
|
|
894
1154
|
if (poly.length == 0) throw new Error('GF.polymomial: invalid length');
|
|
895
1155
|
if (poly[0] !== 0) return poly;
|
|
@@ -898,6 +1158,8 @@ const GF = {
|
|
|
898
1158
|
for (; i < poly.length - 1 && poly[i] == 0; i++);
|
|
899
1159
|
return poly.slice(i);
|
|
900
1160
|
},
|
|
1161
|
+
// Represent c*x^degree in the descending-power coefficient layout used
|
|
1162
|
+
// by the QR Reed-Solomon helpers; coefficient 0 canonicalizes to `[0]`.
|
|
901
1163
|
monomial(degree: number, coefficient: number) {
|
|
902
1164
|
if (degree < 0) throw new Error(`GF.monomial: invalid degree=${degree}`);
|
|
903
1165
|
if (coefficient == 0) return [0];
|
|
@@ -905,8 +1167,14 @@ const GF = {
|
|
|
905
1167
|
coefficients[0] = coefficient;
|
|
906
1168
|
return GF.polynomial(coefficients);
|
|
907
1169
|
},
|
|
1170
|
+
// Canonical polynomials keep the highest-order coefficient first and use
|
|
1171
|
+
// `[0]` for zero, so degree is just `length - 1`.
|
|
908
1172
|
degree: (a: number[]) => a.length - 1,
|
|
1173
|
+
// Read the coefficient for x^degree from the descending-power array layout.
|
|
1174
|
+
// Canonical arrays make this a direct index; out-of-range degrees return `undefined`.
|
|
909
1175
|
coefficient: (a: any, degree: number) => a[GF.degree(a) - degree],
|
|
1176
|
+
// Multiply descending-power coefficient arrays by convolution over GF(2^8).
|
|
1177
|
+
// Zero short-circuits here before the log-based field multiply is consulted.
|
|
910
1178
|
mulPoly(a: number[], b: number[]) {
|
|
911
1179
|
if (a[0] === 0 || b[0] === 0) return [0];
|
|
912
1180
|
const res = fillArr(a.length + b.length - 1, 0);
|
|
@@ -917,6 +1185,8 @@ const GF = {
|
|
|
917
1185
|
}
|
|
918
1186
|
return GF.polynomial(res);
|
|
919
1187
|
},
|
|
1188
|
+
// Scale every coefficient by the same field element in descending-power order.
|
|
1189
|
+
// Scalar 0 canonicalizes to `[0]`, and scalar 1 reuses the original array.
|
|
920
1190
|
mulPolyScalar(a: number[], scalar: number) {
|
|
921
1191
|
if (scalar == 0) return [0];
|
|
922
1192
|
if (scalar == 1) return a;
|
|
@@ -924,6 +1194,8 @@ const GF = {
|
|
|
924
1194
|
for (let i = 0; i < a.length; i++) res[i] = GF.mul(a[i], scalar);
|
|
925
1195
|
return GF.polynomial(res);
|
|
926
1196
|
},
|
|
1197
|
+
// Multiply a polynomial by c*x^degree in descending-power coefficient form.
|
|
1198
|
+
// This scales existing coefficients, then appends trailing zero coefficients.
|
|
927
1199
|
mulPolyMonomial(a: number[], degree: number, coefficient: number) {
|
|
928
1200
|
if (degree < 0) throw new Error('GF.mulPolyMonomial: invalid degree');
|
|
929
1201
|
if (coefficient == 0) return [0];
|
|
@@ -931,6 +1203,8 @@ const GF = {
|
|
|
931
1203
|
for (let i = 0; i < a.length; i++) res[i] = GF.mul(a[i], coefficient);
|
|
932
1204
|
return GF.polynomial(res);
|
|
933
1205
|
},
|
|
1206
|
+
// Add descending-power coefficient arrays with GF(2^8) XOR on the aligned
|
|
1207
|
+
// suffix; `[0]` short-circuits by returning the other array unchanged.
|
|
934
1208
|
addPoly(a: number[], b: number[]) {
|
|
935
1209
|
if (a[0] === 0) return b;
|
|
936
1210
|
if (b[0] === 0) return a;
|
|
@@ -945,6 +1219,8 @@ const GF = {
|
|
|
945
1219
|
sumDiff[i] = GF.add(smaller[i - lengthDiff], larger[i]);
|
|
946
1220
|
return GF.polynomial(sumDiff);
|
|
947
1221
|
},
|
|
1222
|
+
// Synthetic division for monic divisors in descending-power coefficient form.
|
|
1223
|
+
// Callers are expected to append `divisor.length - 1` zero coefficients first.
|
|
948
1224
|
remainderPoly(data: number[], divisor: number[]) {
|
|
949
1225
|
const out = Array.from(data);
|
|
950
1226
|
for (let i = 0; i < data.length - divisor.length + 1; i++) {
|
|
@@ -956,11 +1232,15 @@ const GF = {
|
|
|
956
1232
|
}
|
|
957
1233
|
return out.slice(data.length - divisor.length + 1, out.length);
|
|
958
1234
|
},
|
|
1235
|
+
// Build Annex A's monic generator polynomial g_n(x) = Π(x - 2^i).
|
|
1236
|
+
// degree=0 returns `[1]`; callers are expected to validate degree bounds.
|
|
959
1237
|
divisorPoly(degree: number) {
|
|
960
1238
|
let g = [1];
|
|
961
1239
|
for (let i = 0; i < degree; i++) g = GF.mulPoly(g, [1, GF.pow(2, i)]);
|
|
962
1240
|
return g;
|
|
963
1241
|
},
|
|
1242
|
+
// Evaluate a descending-power coefficient array at `a` with Horner's rule.
|
|
1243
|
+
// The `a == 0` fast-path returns the x^0 coefficient directly.
|
|
964
1244
|
evalPoly(poly: any, a: number) {
|
|
965
1245
|
if (a == 0) return GF.coefficient(poly, 0); // Just return the x^0 coefficient
|
|
966
1246
|
let res = poly[0];
|
|
@@ -968,6 +1248,8 @@ const GF = {
|
|
|
968
1248
|
return res;
|
|
969
1249
|
},
|
|
970
1250
|
// TODO: cleanup
|
|
1251
|
+
// Extended Euclidean RS step: derive the locator/evaluator pair from x^R
|
|
1252
|
+
// and the syndrome polynomial, then normalize sigma(0) to 1.
|
|
971
1253
|
euclidian(a: number[], b: number[], R: number) {
|
|
972
1254
|
// Force degree(a) >= degree(b)
|
|
973
1255
|
if (GF.degree(a) < GF.degree(b)) [a, b] = [b, a];
|
|
@@ -1004,15 +1286,17 @@ const GF = {
|
|
|
1004
1286
|
},
|
|
1005
1287
|
};
|
|
1006
1288
|
|
|
1007
|
-
|
|
1289
|
+
// Per-block Reed-Solomon coder: encode emits only the parity bytes for one
|
|
1290
|
+
// data block, while decode expects data+parity bytes and returns the corrected full block.
|
|
1291
|
+
function RS(eccWords: number): TRet<Coder<Uint8Array, Uint8Array>> {
|
|
1008
1292
|
return {
|
|
1009
|
-
encode(from: Uint8Array) {
|
|
1293
|
+
encode(from: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
1010
1294
|
const d = GF.divisorPoly(eccWords);
|
|
1011
1295
|
const pol = Array.from(from);
|
|
1012
1296
|
pol.push(...d.slice(0, -1).fill(0));
|
|
1013
|
-
return Uint8Array.from(GF.remainderPoly(pol, d))
|
|
1297
|
+
return Uint8Array.from(GF.remainderPoly(pol, d)) as TRet<Uint8Array>;
|
|
1014
1298
|
},
|
|
1015
|
-
decode(to: Uint8Array) {
|
|
1299
|
+
decode(to: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
1016
1300
|
const res = to.slice();
|
|
1017
1301
|
const poly = GF.polynomial(Array.from(to));
|
|
1018
1302
|
// Find errors
|
|
@@ -1048,17 +1332,21 @@ function RS(eccWords: number): Coder<Uint8Array, Uint8Array> {
|
|
|
1048
1332
|
GF.mul(GF.evalPoly(errorEvaluator, xiInverse), GF.inv(denominator))
|
|
1049
1333
|
);
|
|
1050
1334
|
}
|
|
1051
|
-
return res
|
|
1335
|
+
return res as TRet<Uint8Array>;
|
|
1052
1336
|
},
|
|
1053
|
-
}
|
|
1337
|
+
} as TRet<Coder<Uint8Array, Uint8Array>>;
|
|
1054
1338
|
}
|
|
1055
1339
|
|
|
1056
1340
|
// Interleaves blocks
|
|
1057
|
-
|
|
1341
|
+
// QR block interleaver / deinterleaver. Shorter data blocks stay first so
|
|
1342
|
+
// encode matches ISO/IEC 18004 §7.6 c) and decode can reverse it via §12 z)1.
|
|
1343
|
+
function interleave(ver: Version, ecc: ErrorCorrection): TRet<Coder<Uint8Array, Uint8Array>> {
|
|
1058
1344
|
const { words, shortBlocks, numBlocks, blockLen, total } = info.capacity(ver, ecc);
|
|
1059
1345
|
const rs = RS(words);
|
|
1060
1346
|
return {
|
|
1061
|
-
encode(bytes: Uint8Array) {
|
|
1347
|
+
encode(bytes: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
1348
|
+
// Caller must pass exactly the data codewords for this version/ecc;
|
|
1349
|
+
// this helper only splits blocks and interleaves them with RS parity.
|
|
1062
1350
|
// Add error correction to bytes
|
|
1063
1351
|
const blocks: Uint8Array[] = [];
|
|
1064
1352
|
const eccBlocks: Uint8Array[] = [];
|
|
@@ -1074,9 +1362,9 @@ function interleave(ver: Version, ecc: ErrorCorrection): Coder<Uint8Array, Uint8
|
|
|
1074
1362
|
const res = new Uint8Array(resBlocks.length + resECC.length);
|
|
1075
1363
|
res.set(resBlocks);
|
|
1076
1364
|
res.set(resECC, resBlocks.length);
|
|
1077
|
-
return res
|
|
1365
|
+
return res as TRet<Uint8Array>;
|
|
1078
1366
|
},
|
|
1079
|
-
decode(data: Uint8Array) {
|
|
1367
|
+
decode(data: TArg<Uint8Array>): TRet<Uint8Array> {
|
|
1080
1368
|
if (data.length !== total)
|
|
1081
1369
|
throw new Error(`interleave.decode: len(data)=${data.length}, total=${total}`);
|
|
1082
1370
|
const blocks = [];
|
|
@@ -1102,19 +1390,21 @@ function interleave(ver: Version, ecc: ErrorCorrection): Coder<Uint8Array, Uint8
|
|
|
1102
1390
|
// Error-correct and copy data blocks together into a stream of bytes
|
|
1103
1391
|
const res: number[] = [];
|
|
1104
1392
|
for (const block of blocks) res.push(...Array.from(rs.decode(block)).slice(0, -words));
|
|
1105
|
-
return Uint8Array.from(res)
|
|
1393
|
+
return Uint8Array.from(res) as TRet<Uint8Array>;
|
|
1106
1394
|
},
|
|
1107
|
-
}
|
|
1395
|
+
} as TRet<Coder<Uint8Array, Uint8Array>>;
|
|
1108
1396
|
}
|
|
1109
1397
|
|
|
1110
1398
|
// Draw
|
|
1111
1399
|
// Generic template per version+ecc+mask. Can be cached, to speedup calculations.
|
|
1400
|
+
// Function-pattern template plus reserved format/version areas; data modules
|
|
1401
|
+
// are filled later by zigzag placement in `drawQR`.
|
|
1112
1402
|
function drawTemplate(
|
|
1113
1403
|
ver: Version,
|
|
1114
1404
|
ecc: ErrorCorrection,
|
|
1115
1405
|
maskIdx: Mask,
|
|
1116
1406
|
test: boolean = false
|
|
1117
|
-
): Bitmap {
|
|
1407
|
+
): TRet<Bitmap> {
|
|
1118
1408
|
const size = info.size.encode(ver);
|
|
1119
1409
|
let b = new Bitmap(size + 2);
|
|
1120
1410
|
// Finder patterns
|
|
@@ -1166,15 +1456,17 @@ function drawTemplate(
|
|
|
1166
1456
|
b.set(x, y, bit);
|
|
1167
1457
|
}
|
|
1168
1458
|
}
|
|
1169
|
-
return b
|
|
1459
|
+
return b as TRet<Bitmap>;
|
|
1170
1460
|
}
|
|
1171
|
-
// zigzag
|
|
1461
|
+
// Walk undefined data modules in the QR two-column zigzag order from the
|
|
1462
|
+
// lower right, skipping function patterns and the vertical timing column.
|
|
1172
1463
|
function zigzag(
|
|
1173
|
-
tpl: Bitmap
|
|
1464
|
+
tpl: TArg<Bitmap>,
|
|
1174
1465
|
maskIdx: Mask,
|
|
1175
1466
|
fn: (x: number, y: number, mask: boolean) => void
|
|
1176
1467
|
): void {
|
|
1177
|
-
const
|
|
1468
|
+
const bm = tpl as Bitmap;
|
|
1469
|
+
const size = bm.height;
|
|
1178
1470
|
const pattern = PATTERNS[maskIdx];
|
|
1179
1471
|
// zig-zag pattern
|
|
1180
1472
|
let dir = -1;
|
|
@@ -1185,7 +1477,7 @@ function zigzag(
|
|
|
1185
1477
|
for (; ; y += dir) {
|
|
1186
1478
|
for (let j = 0; j < 2; j += 1) {
|
|
1187
1479
|
const x = xOffset - j;
|
|
1188
|
-
if (
|
|
1480
|
+
if (bm.isDefined(x, y)) continue; // skip already written elements
|
|
1189
1481
|
fn(x, y, pattern(x, y));
|
|
1190
1482
|
}
|
|
1191
1483
|
if (y + dir < 0 || y + dir >= size) break;
|
|
@@ -1196,6 +1488,8 @@ function zigzag(
|
|
|
1196
1488
|
|
|
1197
1489
|
// NOTE: byte encoding is just representation, QR works with strings only. Most decoders will fail on raw byte array,
|
|
1198
1490
|
// since they expect unicode or other text encoding inside bytes
|
|
1491
|
+
// Auto-pick among the currently supported single-segment modes only.
|
|
1492
|
+
// Empty strings stay numeric, and any non-alphanumeric character falls back to byte.
|
|
1199
1493
|
function detectType(str: string): EncodingType {
|
|
1200
1494
|
let type: EncodingType = 'numeric';
|
|
1201
1495
|
for (let x of str) {
|
|
@@ -1210,20 +1504,34 @@ function detectType(str: string): EncodingType {
|
|
|
1210
1504
|
// See https://github.com/microsoft/TypeScript/issues/31535
|
|
1211
1505
|
declare const TextEncoder: any;
|
|
1212
1506
|
/**
|
|
1213
|
-
*
|
|
1507
|
+
* Encode a string as UTF-8 bytes.
|
|
1508
|
+
* @param str - Text to encode into UTF-8.
|
|
1509
|
+
* @returns UTF-8 bytes for the provided string.
|
|
1510
|
+
* @throws If the input is not a string. {@link Error}
|
|
1511
|
+
* @example
|
|
1512
|
+
* Encode a string as UTF-8 bytes.
|
|
1513
|
+
* ```ts
|
|
1514
|
+
* const bytes = utf8ToBytes('abc'); // new Uint8Array([97, 98, 99])
|
|
1515
|
+
* ```
|
|
1214
1516
|
*/
|
|
1215
|
-
|
|
1517
|
+
// ISO/IEC 18004:2024 §7.3.2 says QR's default interpretation is
|
|
1518
|
+
// "ECI 000003 representing the ISO/IEC 8859-1 character set"; §7.4.2 says
|
|
1519
|
+
// non-default initial ECI data starts with an ECI header. Keep UTF-8 bytes
|
|
1520
|
+
// without that header for compatibility with existing emoji/qrcode fixtures.
|
|
1521
|
+
export function utf8ToBytes(str: string): TRet<Uint8Array> {
|
|
1216
1522
|
if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
|
|
1217
|
-
return new Uint8Array(new TextEncoder().encode(str))
|
|
1523
|
+
return new Uint8Array(new TextEncoder().encode(str)) as TRet<Uint8Array>; // https://bugzil.la/1681809
|
|
1218
1524
|
}
|
|
1219
1525
|
|
|
1526
|
+
// Build one QR mode/count/data segment, then append the terminator, zero padding,
|
|
1527
|
+
// and alternating pad codewords before RS interleaving.
|
|
1220
1528
|
function encode(
|
|
1221
1529
|
ver: Version,
|
|
1222
1530
|
ecc: ErrorCorrection,
|
|
1223
1531
|
data: string,
|
|
1224
1532
|
type: EncodingType,
|
|
1225
|
-
encoder: (value: string) => Uint8Array = utf8ToBytes
|
|
1226
|
-
): Uint8Array {
|
|
1533
|
+
encoder: TArg<(value: string) => Uint8Array> = utf8ToBytes
|
|
1534
|
+
): TRet<Uint8Array> {
|
|
1227
1535
|
let encoded = '';
|
|
1228
1536
|
let dataLen = data.length;
|
|
1229
1537
|
if (type === 'numeric') {
|
|
@@ -1241,6 +1549,7 @@ function encode(
|
|
|
1241
1549
|
for (let i = 0; i < n - 1; i += 2) encoded += bin(t[i] * 45 + t[i + 1], 11);
|
|
1242
1550
|
if (n % 2 == 1) encoded += bin(t[n - 1], 6); // pad if odd number of chars
|
|
1243
1551
|
} else if (type === 'byte') {
|
|
1552
|
+
// The default encoder is intentionally UTF-8-without-ECI; see utf8ToBytes().
|
|
1244
1553
|
const utf8 = encoder(data);
|
|
1245
1554
|
dataLen = utf8.length;
|
|
1246
1555
|
encoded = Array.from(utf8)
|
|
@@ -1262,19 +1571,20 @@ function encode(
|
|
|
1262
1571
|
for (let idx = 0; bits.length !== capacity; idx++) bits += padding[idx % padding.length];
|
|
1263
1572
|
// Convert a bitstring to array of bytes
|
|
1264
1573
|
const bytes = Uint8Array.from(bits.match(/(.{8})/g)!.map((i) => Number(`0b${i}`)));
|
|
1265
|
-
return interleave(ver, ecc).encode(bytes)
|
|
1574
|
+
return interleave(ver, ecc).encode(bytes) as TRet<Uint8Array>;
|
|
1266
1575
|
}
|
|
1267
1576
|
|
|
1268
1577
|
// DRAW
|
|
1269
|
-
|
|
1578
|
+
// Stream interleaved codeword bits MSB-first through zigzag; any leftover
|
|
1579
|
+
// cells after the final codeword become zero-valued remainder bits before masking.
|
|
1270
1580
|
function drawQR(
|
|
1271
1581
|
ver: Version,
|
|
1272
1582
|
ecc: ErrorCorrection,
|
|
1273
|
-
data: Uint8Array
|
|
1583
|
+
data: TArg<Uint8Array>,
|
|
1274
1584
|
maskIdx: Mask,
|
|
1275
1585
|
test: boolean = false
|
|
1276
|
-
): Bitmap {
|
|
1277
|
-
const b = drawTemplate(ver, ecc, maskIdx, test);
|
|
1586
|
+
): TRet<Bitmap> {
|
|
1587
|
+
const b = drawTemplate(ver, ecc, maskIdx, test) as Bitmap;
|
|
1278
1588
|
let i = 0;
|
|
1279
1589
|
const need = 8 * data.length;
|
|
1280
1590
|
zigzag(b, maskIdx, (x, y, mask) => {
|
|
@@ -1286,9 +1596,11 @@ function drawQR(
|
|
|
1286
1596
|
b.set(x, y, value !== mask); // !== as xor
|
|
1287
1597
|
});
|
|
1288
1598
|
if (i !== need) throw new Error('QR: bytes left after draw');
|
|
1289
|
-
return b
|
|
1599
|
+
return b as TRet<Bitmap>;
|
|
1290
1600
|
}
|
|
1291
1601
|
|
|
1602
|
+
// Pack a left-to-right row pattern for `Bitmap.countPatternInRow()`; keep the
|
|
1603
|
+
// explicit width because leading light modules vanish from the numeric value.
|
|
1292
1604
|
const mkPattern = (pattern: boolean[]) => {
|
|
1293
1605
|
const s = pattern.map((i) => (i ? '1' : '0')).join('');
|
|
1294
1606
|
return { len: s.length, n: Number(`0b${s}`) };
|
|
@@ -1296,16 +1608,17 @@ const mkPattern = (pattern: boolean[]) => {
|
|
|
1296
1608
|
// 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
|
|
1297
1609
|
const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
|
|
1298
1610
|
const lightPattern = [false, false, false, false]; // light area 4 modules wide
|
|
1299
|
-
const P1 = mkPattern([...finderPattern, ...lightPattern]);
|
|
1300
|
-
const P2 = mkPattern([...lightPattern, ...finderPattern]);
|
|
1611
|
+
const P1 = /* @__PURE__ */ (() => mkPattern([...finderPattern, ...lightPattern]))();
|
|
1612
|
+
const P2 = /* @__PURE__ */ (() => mkPattern([...lightPattern, ...finderPattern]))();
|
|
1301
1613
|
|
|
1302
|
-
function penalty(bm: Bitmap): number {
|
|
1303
|
-
const
|
|
1304
|
-
const
|
|
1614
|
+
function penalty(bm: TArg<Bitmap>): number {
|
|
1615
|
+
const b = bm as Bitmap;
|
|
1616
|
+
const { width, height } = b;
|
|
1617
|
+
const transposed = b.transpose();
|
|
1305
1618
|
// Adjacent modules in row/column in same | No. of modules = (5 + i) color
|
|
1306
1619
|
let adjacent = 0;
|
|
1307
1620
|
for (let y = 0; y < height; y++) {
|
|
1308
|
-
|
|
1621
|
+
b.getRuns(y, (len) => {
|
|
1309
1622
|
if (len >= 5) adjacent += 3 + (len - 5);
|
|
1310
1623
|
});
|
|
1311
1624
|
}
|
|
@@ -1316,31 +1629,37 @@ function penalty(bm: Bitmap): number {
|
|
|
1316
1629
|
}
|
|
1317
1630
|
// Block of modules in same color (Block size = 2x2)
|
|
1318
1631
|
let box = 0;
|
|
1319
|
-
for (let y = 0; y < height - 1; y++) box += 3 *
|
|
1632
|
+
for (let y = 0; y < height - 1; y++) box += 3 * b.countBoxes2x2(y);
|
|
1320
1633
|
|
|
1321
1634
|
let finder = 0;
|
|
1322
|
-
for (let y = 0; y < height; y++) finder += 40 *
|
|
1635
|
+
for (let y = 0; y < height; y++) finder += 40 * b.countPatternInRow(y, P1.len, P1.n, P2.n);
|
|
1323
1636
|
for (let y = 0; y < width; y++)
|
|
1324
1637
|
finder += 40 * transposed.countPatternInRow(y, P1.len, P1.n, P2.n);
|
|
1325
1638
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
//
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const darkPercent = (darkPixels / (height * width)) * 100;
|
|
1336
|
-
const dark = 10 * Math.floor(Math.abs(darkPercent - 50) / 5);
|
|
1639
|
+
const total = height * width;
|
|
1640
|
+
const darkPixels = b.popcnt();
|
|
1641
|
+
// ISO/IEC 18004:2024 §7.8.3.1 NOTE 4 assigns "0 points" when the dark ratio
|
|
1642
|
+
// is "between 45 % and 55 %"; subtract that first 5% deviation band before
|
|
1643
|
+
// rating further 5% steps, so exact 45/55 and 40/60 boundaries stay in-band.
|
|
1644
|
+
const darkSteps = Math.ceil(
|
|
1645
|
+
Math.max(0, Math.abs(darkPixels * 100 - total * 50) - total * 5) / (total * 5)
|
|
1646
|
+
);
|
|
1647
|
+
const dark = 10 * darkSteps;
|
|
1337
1648
|
return adjacent + box + finder + dark;
|
|
1338
1649
|
}
|
|
1339
1650
|
|
|
1340
1651
|
// Selects best mask according to penalty, if no mask is provided
|
|
1341
|
-
function drawQRBest(
|
|
1652
|
+
function drawQRBest(
|
|
1653
|
+
ver: Version,
|
|
1654
|
+
ecc: ErrorCorrection,
|
|
1655
|
+
data: TArg<Uint8Array>,
|
|
1656
|
+
maskIdx?: Mask
|
|
1657
|
+
): TRet<Bitmap> {
|
|
1342
1658
|
if (maskIdx === undefined) {
|
|
1343
1659
|
const bestMask = best<Mask>();
|
|
1660
|
+
// ISO/IEC 18004:2024 §7.8.3.1 says mask penalty area is "the complete symbol",
|
|
1661
|
+
// but python-qrcode scores this placeholder form. Keep that output for compatibility
|
|
1662
|
+
// with common QR generators and to avoid fingerprinting this implementation.
|
|
1344
1663
|
for (let mask = 0; mask < PATTERNS.length; mask++)
|
|
1345
1664
|
bestMask.add(penalty(drawQR(ver, ecc, data, mask as Mask, true)), mask as Mask);
|
|
1346
1665
|
maskIdx = bestMask.get();
|
|
@@ -1351,31 +1670,45 @@ function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskId
|
|
|
1351
1670
|
|
|
1352
1671
|
/** QR Code generation options. */
|
|
1353
1672
|
export type QrOpts = {
|
|
1673
|
+
/** Error-correction level to encode into the symbol. */
|
|
1354
1674
|
ecc?: ErrorCorrection | undefined;
|
|
1675
|
+
/** Explicit payload encoding, otherwise detected from the input text. */
|
|
1355
1676
|
encoding?: EncodingType | undefined;
|
|
1356
|
-
|
|
1677
|
+
/**
|
|
1678
|
+
* Custom text encoder used for `byte` payloads.
|
|
1679
|
+
*
|
|
1680
|
+
* Receives the text payload and returns the encoded byte sequence.
|
|
1681
|
+
* @param text - Text payload to encode.
|
|
1682
|
+
* @returns Encoded byte sequence for the payload.
|
|
1683
|
+
*/
|
|
1684
|
+
textEncoder?: TArg<(text: string) => Uint8Array>;
|
|
1685
|
+
/** Explicit QR version to use instead of auto-fitting. */
|
|
1357
1686
|
version?: Version | undefined;
|
|
1687
|
+
/** Explicit mask pattern to apply instead of choosing the best one. */
|
|
1358
1688
|
mask?: number | undefined;
|
|
1689
|
+
/** Quiet-zone border width in modules. */
|
|
1359
1690
|
border?: number | undefined;
|
|
1691
|
+
/** Output scale multiplier for raster formats. */
|
|
1360
1692
|
scale?: number | undefined;
|
|
1361
1693
|
};
|
|
1694
|
+
/** SVG-specific QR output options. */
|
|
1362
1695
|
export type SvgQrOpts = {
|
|
1363
1696
|
/**
|
|
1364
1697
|
* Controls how cells are generated within the SVG.
|
|
1365
1698
|
*
|
|
1366
1699
|
* If `true`:
|
|
1367
1700
|
* - Cells are drawn using a single `path` element.
|
|
1368
|
-
* - Pro: significantly reduces the size of the QR code (
|
|
1701
|
+
* - Pro: significantly reduces the size of the QR code (`70%` smaller than
|
|
1369
1702
|
* unoptimized).
|
|
1370
1703
|
* - Con: less flexible with visually customizing cell shapes.
|
|
1371
1704
|
*
|
|
1372
1705
|
* If `false`:
|
|
1373
1706
|
* - Each cell is drawn with its own `rect` element.
|
|
1374
1707
|
* - Pro: allows more flexibility with visually customizing cells shapes.
|
|
1375
|
-
* - Con: significantly increases the QR code size (
|
|
1708
|
+
* - Con: significantly increases the QR code size (`230%` larger than
|
|
1376
1709
|
* optimized).
|
|
1377
1710
|
*
|
|
1378
|
-
*
|
|
1711
|
+
* Default is `true`.
|
|
1379
1712
|
*/
|
|
1380
1713
|
optimize?: boolean | undefined;
|
|
1381
1714
|
};
|
|
@@ -1393,45 +1726,54 @@ function validateMask(mask: Mask) {
|
|
|
1393
1726
|
if (![0, 1, 2, 3, 4, 5, 6, 7].includes(mask) || !PATTERNS[mask])
|
|
1394
1727
|
throw new Error(`Invalid mask=${mask}. Expected number [0..7]`);
|
|
1395
1728
|
}
|
|
1729
|
+
/** Supported encoder outputs. */
|
|
1396
1730
|
export type Output = 'raw' | 'ascii' | 'term' | 'gif' | 'svg';
|
|
1397
1731
|
|
|
1398
1732
|
/**
|
|
1399
1733
|
* Encodes (creates / generates) QR code.
|
|
1400
|
-
* @param text
|
|
1401
|
-
* @param output
|
|
1402
|
-
* @param opts
|
|
1734
|
+
* @param text - Text payload that should be encoded into the QR symbol.
|
|
1735
|
+
* @param output - Output format to generate: raw matrix, ASCII, terminal ANSI, GIF, or SVG.
|
|
1736
|
+
* @param opts - Encoding and rendering options. See {@link QrOpts} and {@link SvgQrOpts}.
|
|
1737
|
+
* @returns Encoded QR data in the format selected by `output`.
|
|
1738
|
+
* @throws If the payload, options, QR capacity, or output format are invalid. {@link Error}
|
|
1403
1739
|
* @example
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
const
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1740
|
+
* Encode one text payload into several QR output formats.
|
|
1741
|
+
* ```ts
|
|
1742
|
+
* const txt = 'Hello world';
|
|
1743
|
+
* const ascii = encodeQR(txt, 'ascii'); // Not all fonts are supported
|
|
1744
|
+
* const terminalFriendly = encodeQR(txt, 'term'); // 2x larger, all fonts are OK
|
|
1745
|
+
* const gifBytes = encodeQR(txt, 'gif'); // Uncompressed GIF
|
|
1746
|
+
* const svgElement = encodeQR(txt, 'svg'); // SVG vector image element
|
|
1747
|
+
* const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
|
|
1748
|
+
* ```
|
|
1412
1749
|
*/
|
|
1413
|
-
export function encodeQR(text: string, output: 'raw', opts?: QrOpts): boolean[][];
|
|
1414
|
-
export function encodeQR(text: string, output: 'ascii' | 'term', opts?: QrOpts): string;
|
|
1415
|
-
export function encodeQR(text: string, output: 'svg', opts?: QrOpts & SvgQrOpts): string;
|
|
1416
|
-
export function encodeQR(text: string, output: 'gif', opts?: QrOpts): Uint8Array
|
|
1417
|
-
export function encodeQR(
|
|
1418
|
-
|
|
1750
|
+
export function encodeQR(text: string, output: 'raw', opts?: TArg<QrOpts>): boolean[][];
|
|
1751
|
+
export function encodeQR(text: string, output: 'ascii' | 'term', opts?: TArg<QrOpts>): string;
|
|
1752
|
+
export function encodeQR(text: string, output: 'svg', opts?: TArg<QrOpts & SvgQrOpts>): string;
|
|
1753
|
+
export function encodeQR(text: string, output: 'gif', opts?: TArg<QrOpts>): TRet<Uint8Array>;
|
|
1754
|
+
export function encodeQR(
|
|
1755
|
+
text: string,
|
|
1756
|
+
output: Output = 'raw',
|
|
1757
|
+
opts: TArg<QrOpts & SvgQrOpts> = {}
|
|
1758
|
+
) {
|
|
1759
|
+
const _opts = opts as QrOpts & SvgQrOpts;
|
|
1760
|
+
const ecc = _opts.ecc !== undefined ? _opts.ecc : 'medium';
|
|
1419
1761
|
validateECC(ecc);
|
|
1420
|
-
const encoding =
|
|
1762
|
+
const encoding = _opts.encoding !== undefined ? _opts.encoding : detectType(text);
|
|
1421
1763
|
validateEncoding(encoding);
|
|
1422
|
-
if (
|
|
1423
|
-
let ver =
|
|
1764
|
+
if (_opts.mask !== undefined) validateMask(_opts.mask as Mask);
|
|
1765
|
+
let ver = _opts.version;
|
|
1424
1766
|
let data,
|
|
1425
1767
|
err = new Error('Unknown error');
|
|
1426
1768
|
if (ver !== undefined) {
|
|
1427
1769
|
validateVersion(ver);
|
|
1428
|
-
data = encode(ver, ecc, text, encoding,
|
|
1770
|
+
data = encode(ver, ecc, text, encoding, _opts.textEncoder);
|
|
1429
1771
|
} else {
|
|
1430
1772
|
// If no version is provided, try to find smallest one which fits
|
|
1431
1773
|
// Currently just scans all version, can be significantly speedup if needed
|
|
1432
1774
|
for (let i = 1; i <= 40; i++) {
|
|
1433
1775
|
try {
|
|
1434
|
-
data = encode(i, ecc, text, encoding,
|
|
1776
|
+
data = encode(i, ecc, text, encoding, _opts.textEncoder);
|
|
1435
1777
|
ver = i;
|
|
1436
1778
|
break;
|
|
1437
1779
|
} catch (e) {
|
|
@@ -1440,22 +1782,50 @@ export function encodeQR(text: string, output: Output = 'raw', opts: QrOpts & Sv
|
|
|
1440
1782
|
}
|
|
1441
1783
|
}
|
|
1442
1784
|
if (!ver || !data) throw err;
|
|
1443
|
-
let res = drawQRBest(ver, ecc, data,
|
|
1785
|
+
let res = drawQRBest(ver, ecc, data, _opts.mask as Mask) as Bitmap;
|
|
1444
1786
|
res.assertDrawn();
|
|
1445
|
-
|
|
1446
|
-
|
|
1787
|
+
// ISO/IEC 18004:2024 §5.3.8 says a QR quiet zone's "width shall be 4X",
|
|
1788
|
+
// and §9.1 requires 4X "on all four sides". Keep the compact historical
|
|
1789
|
+
// 2-module default to avoid changing encoder output; callers that need a
|
|
1790
|
+
// standards-conformant quiet zone must pass `border: 4` explicitly.
|
|
1791
|
+
const border = _opts.border === undefined ? 2 : _opts.border;
|
|
1792
|
+
if (!Number.isSafeInteger(border) || border <= 0) throw new Error(`invalid border=${border}`);
|
|
1447
1793
|
res = res.border(border, false); // Add border
|
|
1448
|
-
if (
|
|
1794
|
+
if (_opts.scale !== undefined) res = res.scale(_opts.scale); // Scale image
|
|
1449
1795
|
if (output === 'raw') return res.toRaw();
|
|
1450
1796
|
else if (output === 'ascii') return res.toASCII();
|
|
1451
|
-
else if (output === 'svg') return res.toSVG(
|
|
1797
|
+
else if (output === 'svg') return res.toSVG(_opts.optimize);
|
|
1452
1798
|
else if (output === 'gif') return res.toGIF();
|
|
1453
1799
|
else if (output === 'term') return res.toTerm();
|
|
1454
1800
|
else throw new Error(`Unknown output: ${output}`);
|
|
1455
1801
|
}
|
|
1456
1802
|
|
|
1803
|
+
/**
|
|
1804
|
+
* Default export alias for {@link encodeQR}.
|
|
1805
|
+
* @param text - Text payload that should be encoded into the QR symbol.
|
|
1806
|
+
* @param output - Output format to generate: raw matrix, ASCII, terminal ANSI, GIF, or SVG.
|
|
1807
|
+
* @param opts - Encoding and rendering options. See {@link QrOpts} and {@link SvgQrOpts}.
|
|
1808
|
+
* @returns Encoded QR data in the format selected by `output`.
|
|
1809
|
+
* @throws If the payload, options, QR capacity, or output format are invalid. {@link Error}
|
|
1810
|
+
* @example
|
|
1811
|
+
* Encode text into the default export from the package root.
|
|
1812
|
+
* ```ts
|
|
1813
|
+
* import encodeQR from 'qr';
|
|
1814
|
+
* encodeQR('Hello world', 'ascii');
|
|
1815
|
+
* ```
|
|
1816
|
+
*/
|
|
1457
1817
|
export default encodeQR;
|
|
1458
1818
|
|
|
1819
|
+
/**
|
|
1820
|
+
* Low-level helpers used by the encoder and test suite.
|
|
1821
|
+
* Exports the shared helper tables/functions through a frozen container.
|
|
1822
|
+
* @example
|
|
1823
|
+
* Read low-level QR metadata tables.
|
|
1824
|
+
* ```ts
|
|
1825
|
+
* import { utils } from 'qr';
|
|
1826
|
+
* const size = utils.info.size.encode(1); // 21
|
|
1827
|
+
* ```
|
|
1828
|
+
*/
|
|
1459
1829
|
export const utils: {
|
|
1460
1830
|
best: typeof best;
|
|
1461
1831
|
bin: typeof bin;
|
|
@@ -1502,7 +1872,7 @@ export const utils: {
|
|
|
1502
1872
|
interleave: typeof interleave;
|
|
1503
1873
|
validateVersion: typeof validateVersion;
|
|
1504
1874
|
zigzag: typeof zigzag;
|
|
1505
|
-
} = {
|
|
1875
|
+
} = /* @__PURE__ */ Object.freeze({
|
|
1506
1876
|
best,
|
|
1507
1877
|
bin,
|
|
1508
1878
|
popcnt,
|
|
@@ -1512,9 +1882,10 @@ export const utils: {
|
|
|
1512
1882
|
interleave,
|
|
1513
1883
|
validateVersion,
|
|
1514
1884
|
zigzag,
|
|
1515
|
-
};
|
|
1885
|
+
});
|
|
1516
1886
|
|
|
1517
1887
|
// Unsafe API utils, exported only for tests
|
|
1888
|
+
// Exposes the shared internal helpers/tables through a frozen container.
|
|
1518
1889
|
export const _tests: {
|
|
1519
1890
|
Bitmap: typeof Bitmap;
|
|
1520
1891
|
info: {
|
|
@@ -1559,7 +1930,7 @@ export const _tests: {
|
|
|
1559
1930
|
drawQR: typeof drawQR;
|
|
1560
1931
|
penalty: typeof penalty;
|
|
1561
1932
|
PATTERNS: readonly ((x: number, y: number) => boolean)[];
|
|
1562
|
-
} = {
|
|
1933
|
+
} = /* @__PURE__ */ Object.freeze({
|
|
1563
1934
|
Bitmap,
|
|
1564
1935
|
info,
|
|
1565
1936
|
detectType,
|
|
@@ -1567,7 +1938,7 @@ export const _tests: {
|
|
|
1567
1938
|
drawQR,
|
|
1568
1939
|
penalty,
|
|
1569
1940
|
PATTERNS,
|
|
1570
|
-
};
|
|
1941
|
+
});
|
|
1571
1942
|
// Type tests
|
|
1572
1943
|
// const o1 = qr('test', 'ascii');
|
|
1573
1944
|
// const o2 = qr('test', 'raw');
|