qr 0.5.5 → 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 +37 -30
- package/decode.d.ts +85 -12
- package/decode.d.ts.map +1 -1
- package/decode.js +234 -109
- package/decode.js.map +1 -1
- package/dom.d.ts +110 -14
- package/dom.d.ts.map +1 -1
- package/dom.js +123 -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 +294 -131
- package/src/dom.ts +156 -32
- package/src/index.ts +500 -129
package/index.js
CHANGED
|
@@ -14,22 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
14
14
|
See the License for the specific language governing permissions and
|
|
15
15
|
limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
/**
|
|
18
|
-
* Methods for encoding (generating) QR code patterns.
|
|
19
|
-
* Check out decode.ts for decoding (reading).
|
|
20
|
-
* @module
|
|
21
|
-
* @example
|
|
22
|
-
```js
|
|
23
|
-
import encodeQR from 'qr';
|
|
24
|
-
const txt = 'Hello world';
|
|
25
|
-
const ascii = encodeQR(txt, 'ascii'); // Not all fonts are supported
|
|
26
|
-
const terminalFriendly = encodeQR(txt, 'term'); // 2x larger, all fonts are OK
|
|
27
|
-
const gifBytes = encodeQR(txt, 'gif'); // Uncompressed GIF
|
|
28
|
-
const svgElement = encodeQR(txt, 'svg'); // SVG vector image element
|
|
29
|
-
const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
|
|
30
|
-
// import decodeQR from 'qr/decode.js';
|
|
31
|
-
```
|
|
32
|
-
*/
|
|
33
17
|
// We do not use newline escape code directly in strings because it's not parser-friendly
|
|
34
18
|
const chCodes = { newline: 10, reset: 27 };
|
|
35
19
|
function assertNumber(n) {
|
|
@@ -48,6 +32,7 @@ function mod(a, b) {
|
|
|
48
32
|
return result >= 0 ? result : b + result;
|
|
49
33
|
}
|
|
50
34
|
function fillArr(length, val) {
|
|
35
|
+
// Current callers only pass primitive fill values; object fills would alias references.
|
|
51
36
|
return new Array(length).fill(val);
|
|
52
37
|
}
|
|
53
38
|
function popcnt(n) {
|
|
@@ -69,6 +54,8 @@ function interleaveBytes(blocks) {
|
|
|
69
54
|
}
|
|
70
55
|
const result = new Uint8Array(totalLen);
|
|
71
56
|
let idx = 0;
|
|
57
|
+
// When block lengths differ, callers must pass the shorter blocks first so
|
|
58
|
+
// the interleaving order matches ISO/IEC 18004 §7.6 c).
|
|
72
59
|
for (let i = 0; i < maxLen; i++) {
|
|
73
60
|
for (const block of blocks) {
|
|
74
61
|
if (i < block.length)
|
|
@@ -83,6 +70,7 @@ function best() {
|
|
|
83
70
|
let bestScore = Infinity;
|
|
84
71
|
return {
|
|
85
72
|
add(score, value) {
|
|
73
|
+
// Ties keep the first candidate so equal-score selections stay deterministic.
|
|
86
74
|
if (score >= bestScore)
|
|
87
75
|
return;
|
|
88
76
|
best = value;
|
|
@@ -94,7 +82,8 @@ function best() {
|
|
|
94
82
|
}
|
|
95
83
|
// Based on https://github.com/paulmillr/scure-base/blob/main/index.ts
|
|
96
84
|
function alphabet(alphabet) {
|
|
97
|
-
|
|
85
|
+
// Character order defines the numeric values used by the target QR mode.
|
|
86
|
+
return Object.freeze({
|
|
98
87
|
has: (char) => alphabet.includes(char),
|
|
99
88
|
decode: (input) => {
|
|
100
89
|
if (!Array.isArray(input) || (input.length && typeof input[0] !== 'string'))
|
|
@@ -118,7 +107,7 @@ function alphabet(alphabet) {
|
|
|
118
107
|
return alphabet[i];
|
|
119
108
|
});
|
|
120
109
|
},
|
|
121
|
-
};
|
|
110
|
+
});
|
|
122
111
|
}
|
|
123
112
|
// Transpose 32x32 bit matrix in-place
|
|
124
113
|
// a[0..31] are 32 rows of 32 bits each; after transpose they become 32 columns.
|
|
@@ -149,10 +138,24 @@ const rangeMask = (shift, len) => {
|
|
|
149
138
|
// len in [0..32], shift in [0..31]
|
|
150
139
|
if (len === 0)
|
|
151
140
|
return 0;
|
|
141
|
+
// Callers only request len=32 for word-aligned spans; JS shift counts wrap at 32,
|
|
142
|
+
// so full-word masks must bypass the generic `(1 << len)` path.
|
|
152
143
|
if (len === 32)
|
|
153
144
|
return 0xffffffff;
|
|
154
145
|
return (((1 << len) - 1) << shift) >>> 0;
|
|
155
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* Mutable monochrome bitmap used as the internal QR representation.
|
|
149
|
+
* @param size - Square edge length or explicit bitmap dimensions.
|
|
150
|
+
* @param data - Optional row-major pixel matrix using `true`, `false`, or `undefined`.
|
|
151
|
+
* @example
|
|
152
|
+
* Create a bitmap, then scale it for display.
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { Bitmap } from 'qr';
|
|
155
|
+
* const bitmap = Bitmap.fromString('X \n X');
|
|
156
|
+
* bitmap.scale(2);
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
156
159
|
export class Bitmap {
|
|
157
160
|
static size(size, limit) {
|
|
158
161
|
if (typeof size === 'number')
|
|
@@ -172,6 +175,8 @@ export class Bitmap {
|
|
|
172
175
|
}
|
|
173
176
|
static fromString(s) {
|
|
174
177
|
// Remove linebreaks on start and end, so we draw in `` section
|
|
178
|
+
// Fixture strings use LF-delimited rows of X / space / ? characters; callers
|
|
179
|
+
// must normalize CRLF input before handing it to this debug parser.
|
|
175
180
|
s = s.replace(/^\n+/g, '').replace(/\n+$/g, '');
|
|
176
181
|
const lines = s.split(String.fromCharCode(chCodes.newline));
|
|
177
182
|
const height = lines.length;
|
|
@@ -209,6 +214,14 @@ export class Bitmap {
|
|
|
209
214
|
width;
|
|
210
215
|
constructor(size, data) {
|
|
211
216
|
const { height, width } = Bitmap.size(size);
|
|
217
|
+
// Bitmap coordinates wrap through modulo for negative positions, so invalid
|
|
218
|
+
// dimensions produce NaN, aliasing, or unsafe allocation sizes before later
|
|
219
|
+
// drawing no-op guards can run. `Infinity` is only valid for rectangle sizes
|
|
220
|
+
// that are clamped against an existing positive bitmap.
|
|
221
|
+
if (!Number.isSafeInteger(height) || height <= 0)
|
|
222
|
+
throw new Error(`Bitmap: invalid height=${height}, expected positive safe integer dimension`);
|
|
223
|
+
if (!Number.isSafeInteger(width) || width <= 0)
|
|
224
|
+
throw new Error(`Bitmap: invalid width=${width}, expected positive safe integer dimension`);
|
|
212
225
|
this.height = height;
|
|
213
226
|
this.width = width;
|
|
214
227
|
this.tailMask = rangeMask(0, width & 31 || 32);
|
|
@@ -230,8 +243,13 @@ export class Bitmap {
|
|
|
230
243
|
}
|
|
231
244
|
}
|
|
232
245
|
point(p) {
|
|
246
|
+
// The storage docs above say "undefined is used as a marker whether cell
|
|
247
|
+
// was written or not"; `point()` is the detector's dark-module read and
|
|
248
|
+
// intentionally treats both undefined and false as not-dark. Use
|
|
249
|
+
// `isDefined()` when the written/undefined distinction matters.
|
|
233
250
|
return this.get(p.x, p.y);
|
|
234
251
|
}
|
|
252
|
+
// Raw bounds check for scan loops; unlike `xy()`, this does not wrap or normalize coordinates.
|
|
235
253
|
isInside(p) {
|
|
236
254
|
return 0 <= p.x && p.x < this.width && 0 <= p.y && p.y < this.height;
|
|
237
255
|
}
|
|
@@ -248,7 +266,8 @@ export class Bitmap {
|
|
|
248
266
|
throw new Error(`Bitmap: invalid x=${c.x}`);
|
|
249
267
|
if (!Number.isSafeInteger(c.y))
|
|
250
268
|
throw new Error(`Bitmap: invalid y=${c.y}`);
|
|
251
|
-
//
|
|
269
|
+
// Bitmap's class docs say "For most `draw` calls, structure is mutable";
|
|
270
|
+
// coordinate objects follow that hot-path policy too and are normalized in place.
|
|
252
271
|
c.x = mod(c.x, this.width);
|
|
253
272
|
c.y = mod(c.y, this.height);
|
|
254
273
|
return c;
|
|
@@ -263,6 +282,10 @@ export class Bitmap {
|
|
|
263
282
|
return { word: this.wordIndex(x, y), bit: x & 31 };
|
|
264
283
|
}
|
|
265
284
|
isDefined(x, y) {
|
|
285
|
+
// `isInside()` is the raw bounds check; keep these bitset accessors
|
|
286
|
+
// bounds-check-free for hot paths. Invalid tail coordinates may observe
|
|
287
|
+
// backing-word bits, so callers that accept untrusted coordinates must
|
|
288
|
+
// check `isInside()` first.
|
|
266
289
|
const wi = this.wordIndex(x, y);
|
|
267
290
|
const m = bitMask(x);
|
|
268
291
|
return (this.defined[wi] & m) !== 0;
|
|
@@ -275,11 +298,15 @@ export class Bitmap {
|
|
|
275
298
|
maskWord(wi, mask, v) {
|
|
276
299
|
const { defined, value } = this;
|
|
277
300
|
defined[wi] |= mask;
|
|
301
|
+
// `-v` expands the boolean to either all-zero or all-one bits before masking it into the selected lanes.
|
|
278
302
|
value[wi] = (value[wi] & ~mask) | (-v & mask);
|
|
279
303
|
}
|
|
280
304
|
set(x, y, v) {
|
|
305
|
+
// `undefined` means "leave the current cell unchanged", not "clear it back to undefined".
|
|
281
306
|
if (v === undefined)
|
|
282
307
|
return;
|
|
308
|
+
// Like `get()` / `isDefined()`, this is a raw in-bounds bitset accessor;
|
|
309
|
+
// check `isInside()` before passing untrusted coordinates.
|
|
283
310
|
this.maskWord(this.wordIndex(x, y), bitMask(x), v);
|
|
284
311
|
}
|
|
285
312
|
// word-span fill for constant values (fast path)
|
|
@@ -301,6 +328,7 @@ export class Bitmap {
|
|
|
301
328
|
continue;
|
|
302
329
|
}
|
|
303
330
|
this.maskWord(rowBase + startWord, rangeMask(startBit, 32 - startBit), v);
|
|
331
|
+
// Whole interior words can be written directly: every bit in the span becomes defined and equal to v.
|
|
304
332
|
for (let i = startWord + 1; i < endWord; i++) {
|
|
305
333
|
defined[rowBase + i] = 0xffffffff;
|
|
306
334
|
value[rowBase + i] = v ? 0xffffffff : 0;
|
|
@@ -315,6 +343,7 @@ export class Bitmap {
|
|
|
315
343
|
const bitX = x + xPos;
|
|
316
344
|
const { bit, word } = this.bitIndex(bitX, Py);
|
|
317
345
|
const bitsPerWord = Math.min(32 - bit, width - xPos);
|
|
346
|
+
// bitX stays absolute for word-local masks; xPos/yPos stay rectangle-local for rect callbacks.
|
|
318
347
|
cb(word, bitX, xPos, yPos, bitsPerWord);
|
|
319
348
|
xPos += bitsPerWord;
|
|
320
349
|
}
|
|
@@ -334,7 +363,11 @@ export class Bitmap {
|
|
|
334
363
|
let valWord = value[wi];
|
|
335
364
|
for (let b = 0; b < n; b++) {
|
|
336
365
|
const mask = bitMask(bitX + b);
|
|
366
|
+
// As with `point()`, callback `cur` is a dark/not-dark read; the
|
|
367
|
+
// storage-level "undefined is used as a marker whether cell was
|
|
368
|
+
// written or not" distinction is checked separately with `isDefined()`.
|
|
337
369
|
const res = fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
|
|
370
|
+
// Returning undefined from the callback keeps the existing cell unchanged.
|
|
338
371
|
if (res === undefined)
|
|
339
372
|
continue;
|
|
340
373
|
defWord |= mask;
|
|
@@ -354,6 +387,8 @@ export class Bitmap {
|
|
|
354
387
|
const valWord = value[wi];
|
|
355
388
|
for (let b = 0; b < n; b++) {
|
|
356
389
|
const mask = bitMask(bitX + b);
|
|
390
|
+
// rectRead is non-mutating; callback coordinates are rectangle-local,
|
|
391
|
+
// and `cur` is the same dark/not-dark read as `point()`.
|
|
357
392
|
fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
|
|
358
393
|
}
|
|
359
394
|
});
|
|
@@ -368,6 +403,10 @@ export class Bitmap {
|
|
|
368
403
|
}
|
|
369
404
|
// add border
|
|
370
405
|
border(border = 2, value) {
|
|
406
|
+
// `border` is used both as output-size delta and as embed coordinate; keep
|
|
407
|
+
// it a positive safe integer before those paths allocate or normalize.
|
|
408
|
+
if (!Number.isSafeInteger(border) || border <= 0)
|
|
409
|
+
throw new Error(`Bitmap.border: invalid size=${border}`);
|
|
371
410
|
const height = this.height + 2 * border;
|
|
372
411
|
const width = this.width + 2 * border;
|
|
373
412
|
const out = new Bitmap({ height, width });
|
|
@@ -384,6 +423,10 @@ export class Bitmap {
|
|
|
384
423
|
return this;
|
|
385
424
|
const { value, defined } = this;
|
|
386
425
|
const { words: srcStride, value: srcValue } = src;
|
|
426
|
+
// The Bitmap storage docs say "undefined is used as a marker whether cell
|
|
427
|
+
// was written or not"; `embed()` is the packed blit path for materialized
|
|
428
|
+
// source bitmaps, so it flattens the source rectangle to defined dark/light
|
|
429
|
+
// bits instead of treating undefined cells as transparent.
|
|
387
430
|
for (let yPos = 0; yPos < height; yPos++) {
|
|
388
431
|
const srcRow = yPos * srcStride;
|
|
389
432
|
for (let xPos = 0; xPos < width;) {
|
|
@@ -393,6 +436,7 @@ export class Bitmap {
|
|
|
393
436
|
const len = Math.min(32 - dstBit, width - xPos);
|
|
394
437
|
const w0 = srcValue[srcWord];
|
|
395
438
|
const w1 = srcBit && srcWord + 1 < srcRow + srcStride ? srcValue[srcWord + 1] : 0;
|
|
439
|
+
// Source and destination bit offsets may differ, so assemble the source span from up to two words.
|
|
396
440
|
const sVal = srcBit ? ((w0 >>> srcBit) | (w1 << (32 - srcBit))) >>> 0 : w0;
|
|
397
441
|
const dstMask = rangeMask(dstBit, len);
|
|
398
442
|
const valBits = ((sVal & rangeMask(0, len)) << dstBit) >>> 0;
|
|
@@ -409,6 +453,7 @@ export class Bitmap {
|
|
|
409
453
|
const { height, width } = Bitmap.size(size, this.size({ x, y }));
|
|
410
454
|
const rect = new Bitmap({ height, width });
|
|
411
455
|
this.rectRead({ x, y }, { height, width }, (p, cur) => {
|
|
456
|
+
// rectRead reports undefined cells as false, so copy only when the source defined bit is set.
|
|
412
457
|
if (this.isDefined(x + p.x, y + p.y)) {
|
|
413
458
|
rect.set(p.x, p.y, cur);
|
|
414
459
|
}
|
|
@@ -453,6 +498,9 @@ export class Bitmap {
|
|
|
453
498
|
negate() {
|
|
454
499
|
const n = this.defined.length;
|
|
455
500
|
for (let i = 0; i < n; i++) {
|
|
501
|
+
// ISO/IEC 18004:2024 §12 b)5 says to "reverse the colouring of the light
|
|
502
|
+
// and dark pixels"; this dense scratch-bitmap operation materializes every
|
|
503
|
+
// backing bit as defined and does not preserve sparse/undefined cells.
|
|
456
504
|
this.value[i] = ~this.value[i];
|
|
457
505
|
this.defined[i] = 0xffffffff;
|
|
458
506
|
}
|
|
@@ -463,6 +511,11 @@ export class Bitmap {
|
|
|
463
511
|
if (!Number.isSafeInteger(factor) || factor > 1024)
|
|
464
512
|
throw new Error(`invalid scale factor: ${factor}`);
|
|
465
513
|
const { height, width } = this;
|
|
514
|
+
// Bitmap storage docs say "undefined is used as a marker whether cell was
|
|
515
|
+
// written or not"; `scale()` is an output materialization path and samples
|
|
516
|
+
// with `get()`, so sparse cells become defined light cells. Positive output
|
|
517
|
+
// dimensions stay validated by the Bitmap constructor instead of duplicating
|
|
518
|
+
// dimension checks in every caller that computes a new bitmap size.
|
|
466
519
|
const res = new Bitmap({ height: factor * height, width: factor * width });
|
|
467
520
|
return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) => this.get((x / factor) | 0, (y / factor) | 0));
|
|
468
521
|
}
|
|
@@ -488,10 +541,14 @@ export class Bitmap {
|
|
|
488
541
|
}
|
|
489
542
|
}
|
|
490
543
|
countPatternInRow(y, patternLen, ...patterns) {
|
|
491
|
-
|
|
544
|
+
// Penalty scanning only passes Table 11 windows over bounded symbol rows;
|
|
545
|
+
// validate this public helper before JS shifts / typed-array reads coerce bad inputs.
|
|
546
|
+
if (!Number.isSafeInteger(patternLen) || patternLen <= 0 || patternLen >= 32)
|
|
492
547
|
throw new Error('wrong patternLen');
|
|
493
548
|
const mask = (1 << patternLen) - 1;
|
|
494
|
-
const { width, value, words } = this;
|
|
549
|
+
const { height, width, value, words } = this;
|
|
550
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= height)
|
|
551
|
+
return 0;
|
|
495
552
|
let count = 0;
|
|
496
553
|
const rowBase = this.wordIndex(0, y);
|
|
497
554
|
for (let i = 0, window = 0; i < words; i++) {
|
|
@@ -512,9 +569,14 @@ export class Bitmap {
|
|
|
512
569
|
return count;
|
|
513
570
|
}
|
|
514
571
|
getRuns(y, fn) {
|
|
515
|
-
const { width, value, words } = this;
|
|
572
|
+
const { height, width, value, words } = this;
|
|
516
573
|
if (width === 0)
|
|
517
574
|
return;
|
|
575
|
+
// ISO/IEC 18004:2024 §7.8.3.1 N1 scans adjacent modules in bounded rows
|
|
576
|
+
// and columns; validate this public helper before missing typed-array rows
|
|
577
|
+
// are coerced into all-light runs by bitwise operators.
|
|
578
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= height)
|
|
579
|
+
return;
|
|
518
580
|
let runLen = 0;
|
|
519
581
|
let runValue;
|
|
520
582
|
const rowBase = this.wordIndex(0, y);
|
|
@@ -551,11 +613,13 @@ export class Bitmap {
|
|
|
551
613
|
return count;
|
|
552
614
|
}
|
|
553
615
|
countBoxes2x2(y) {
|
|
554
|
-
const { width, words } = this;
|
|
555
|
-
|
|
616
|
+
const { height, width, words } = this;
|
|
617
|
+
// ISO/IEC 18004:2024 §7.8.3.1 N2 counts 2 x 2 module blocks in bounded
|
|
618
|
+
// rows; reject non-integer scan rows before bitwise coercions truncate them.
|
|
619
|
+
if (width < 2 || !Number.isSafeInteger(y) || y < 0 || y + 1 >= height)
|
|
556
620
|
return 0;
|
|
557
|
-
const base0 = this.wordIndex(0, y)
|
|
558
|
-
const base1 = this.wordIndex(0, y + 1)
|
|
621
|
+
const base0 = this.wordIndex(0, y);
|
|
622
|
+
const base1 = this.wordIndex(0, y + 1);
|
|
559
623
|
// valid "left-edge" positions x in [0 .. W-2]
|
|
560
624
|
const tailBits = width & 31;
|
|
561
625
|
const validLast = tailBits === 0 ? 0x7fffffff : rangeMask(0, (width - 1) & 31);
|
|
@@ -594,6 +658,9 @@ export class Bitmap {
|
|
|
594
658
|
const out = Array.from({ length: this.height }, () => new Array(this.width));
|
|
595
659
|
for (let y = 0; y < this.height; y++) {
|
|
596
660
|
const row = out[y];
|
|
661
|
+
// Bitmap storage docs say "undefined is used as a marker whether cell was
|
|
662
|
+
// written or not"; `toRaw()` is the materialized dark/not-dark output path
|
|
663
|
+
// used after `encodeQR()` asserts the QR symbol is fully drawn.
|
|
597
664
|
for (let x = 0; x < this.width; x++)
|
|
598
665
|
row[x] = this.get(x, y);
|
|
599
666
|
}
|
|
@@ -639,6 +706,8 @@ export class Bitmap {
|
|
|
639
706
|
}
|
|
640
707
|
toSVG(optimize = true) {
|
|
641
708
|
let out = `<svg viewBox="0 0 ${this.width} ${this.height}" xmlns="http://www.w3.org/2000/svg">`;
|
|
709
|
+
// ISO/IEC 18004:2024 §5.1 c) / §5.3.8: this SVG draws only dark modules;
|
|
710
|
+
// callers must render it on a light background for light modules and the quiet zone.
|
|
642
711
|
// Construct optimized SVG path data.
|
|
643
712
|
let pathData = '';
|
|
644
713
|
let prevPoint;
|
|
@@ -682,7 +751,9 @@ export class Bitmap {
|
|
|
682
751
|
const u16le = (i) => [i & 0xff, (i >>> 8) & 0xff];
|
|
683
752
|
const dims = [...u16le(this.width), ...u16le(this.height)];
|
|
684
753
|
const data = [];
|
|
754
|
+
// Palette index 0 is white/light and index 1 is black/dark; rectRead maps undefined cells to light.
|
|
685
755
|
this.rectRead(0, Infinity, (_, cur) => data.push(+(cur === true)));
|
|
756
|
+
// Each chunk starts with an LZW clear code; 126 raw pixels keep codes at 8 bits until the next clear.
|
|
686
757
|
const N = 126; // Block size
|
|
687
758
|
// prettier-ignore
|
|
688
759
|
const bytes = [
|
|
@@ -717,11 +788,16 @@ export class Bitmap {
|
|
|
717
788
|
}
|
|
718
789
|
// End of utils
|
|
719
790
|
// Runtime type-checking
|
|
720
|
-
/** Error correction mode. low: 7%, medium: 15%, quartile: 25%, high: 30
|
|
721
|
-
export const ECMode = ['low', 'medium', 'quartile', 'high'];
|
|
722
|
-
/**
|
|
723
|
-
|
|
791
|
+
/** Error correction mode. low: 7%, medium: 15%, quartile: 25%, high: 30%. */
|
|
792
|
+
export const ECMode = /* @__PURE__ */ Object.freeze(['low', 'medium', 'quartile', 'high']);
|
|
793
|
+
/**
|
|
794
|
+
* QR payload compaction mode names recognized by the type/validator.
|
|
795
|
+
* `kanji` and `eci` are spec modes, but `encodeQR` currently rejects them until implemented.
|
|
796
|
+
*/
|
|
797
|
+
export const Encoding =
|
|
798
|
+
/* @__PURE__ */ Object.freeze(['numeric', 'alphanumeric', 'byte', 'kanji', 'eci']);
|
|
724
799
|
// Various constants & tables
|
|
800
|
+
// ISO/IEC 18004:2024 Table 1: QR symbol codeword capacity by version (data plus error correction).
|
|
725
801
|
// prettier-ignore
|
|
726
802
|
const BYTES = [
|
|
727
803
|
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
|
@@ -729,6 +805,7 @@ const BYTES = [
|
|
|
729
805
|
// 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
|
|
730
806
|
1156, 1258, 1364, 1474, 1588, 1706, 1828, 1921, 2051, 2185, 2323, 2465, 2611, 2761, 2876, 3034, 3196, 3362, 3532, 3706,
|
|
731
807
|
];
|
|
808
|
+
// ISO/IEC 18004:2024 Table 9: error correction codewords per block by version and level.
|
|
732
809
|
// prettier-ignore
|
|
733
810
|
const WORDS_PER_BLOCK = {
|
|
734
811
|
// 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
|
|
@@ -737,6 +814,7 @@ const WORDS_PER_BLOCK = {
|
|
|
737
814
|
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],
|
|
738
815
|
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],
|
|
739
816
|
};
|
|
817
|
+
// ISO/IEC 18004:2024 Table 9: error correction block count by version and level.
|
|
740
818
|
// prettier-ignore
|
|
741
819
|
const ECC_BLOCKS = {
|
|
742
820
|
// 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
|
|
@@ -745,12 +823,15 @@ const ECC_BLOCKS = {
|
|
|
745
823
|
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],
|
|
746
824
|
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],
|
|
747
825
|
};
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
826
|
+
// ISO/IEC 18004:2024 sections 5.3/7.4/7.5/7.9/7.10: QR layout, segment, format/version, and capacity helpers.
|
|
827
|
+
const info = /* @__PURE__ */ Object.freeze({
|
|
828
|
+
size: /* @__PURE__ */ Object.freeze({
|
|
829
|
+
encode: (ver) => 21 + 4 * (ver - 1), // ver1 = 21, ver40 = 177 modules per side
|
|
751
830
|
decode: (size) => (size - 17) / 4,
|
|
752
|
-
},
|
|
831
|
+
}),
|
|
832
|
+
// ISO/IEC 18004:2024 Table 3: map version ranges 1-9, 10-26, and 27-40 to count-width indexes.
|
|
753
833
|
sizeType: (ver) => Math.floor((ver + 7) / 17),
|
|
834
|
+
// ISO/IEC 18004:2024 Annex E Table E.1: row/column coordinate list of alignment-pattern centres.
|
|
754
835
|
// Based on https://codereview.stackexchange.com/questions/74925/algorithm-to-generate-this-alignment-pattern-locations-table-for-qr-codes
|
|
755
836
|
alignmentPatterns(ver) {
|
|
756
837
|
if (ver === 1)
|
|
@@ -770,13 +851,16 @@ const info = {
|
|
|
770
851
|
res.push(last);
|
|
771
852
|
return res;
|
|
772
853
|
},
|
|
773
|
-
|
|
854
|
+
// ISO/IEC 18004:2024 §7.9.1 Table 12: error-correction-level indicators for the top two format-information data bits.
|
|
855
|
+
ECCode: /* @__PURE__ */ Object.freeze({
|
|
774
856
|
low: 0b01,
|
|
775
857
|
medium: 0b00,
|
|
776
858
|
quartile: 0b11,
|
|
777
859
|
high: 0b10,
|
|
778
|
-
},
|
|
860
|
+
}),
|
|
861
|
+
// ISO/IEC 18004:2024 §7.9.1 final paragraph: XOR the 15-bit format information with mask pattern 101010000010010.
|
|
779
862
|
formatMask: 0b101010000010010,
|
|
863
|
+
// 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.
|
|
780
864
|
formatBits(ecc, maskIdx) {
|
|
781
865
|
const data = (info.ECCode[ecc] << 3) | maskIdx;
|
|
782
866
|
let d = data;
|
|
@@ -784,16 +868,21 @@ const info = {
|
|
|
784
868
|
d = (d << 1) ^ ((d >> 9) * 0b10100110111);
|
|
785
869
|
return ((data << 10) | d) ^ info.formatMask;
|
|
786
870
|
},
|
|
871
|
+
// 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.
|
|
787
872
|
versionBits(ver) {
|
|
788
873
|
let d = ver;
|
|
789
874
|
for (let i = 0; i < 12; i++)
|
|
790
875
|
d = (d << 1) ^ ((d >> 11) * 0b1111100100101);
|
|
791
876
|
return (ver << 12) | d;
|
|
792
877
|
},
|
|
793
|
-
|
|
878
|
+
// 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.
|
|
879
|
+
alphabet: /* @__PURE__ */ Object.freeze({
|
|
880
|
+
// ISO/IEC 18004:2024 §7.3.3 / §7.4.4: numeric-mode digits map directly to values 0..9 before 3-digit grouping.
|
|
794
881
|
numeric: alphabet('0123456789'),
|
|
882
|
+
// 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.
|
|
795
883
|
alphanumerc: alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'),
|
|
796
|
-
}, // as Record<EncodingType, ReturnType<typeof alphabet>>,
|
|
884
|
+
}), // as Record<EncodingType, ReturnType<typeof alphabet>>,
|
|
885
|
+
// 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.
|
|
797
886
|
lengthBits(ver, type) {
|
|
798
887
|
const table = {
|
|
799
888
|
numeric: [10, 12, 14],
|
|
@@ -804,13 +893,15 @@ const info = {
|
|
|
804
893
|
};
|
|
805
894
|
return table[type][info.sizeType(ver)];
|
|
806
895
|
},
|
|
807
|
-
|
|
896
|
+
// ISO/IEC 18004:2024 §7.4.2 Table 2: 4-bit QR mode indicators for the segment types this library models.
|
|
897
|
+
modeBits: /* @__PURE__ */ Object.freeze({
|
|
808
898
|
numeric: '0001',
|
|
809
899
|
alphanumeric: '0010',
|
|
810
900
|
byte: '0100',
|
|
811
901
|
kanji: '1000',
|
|
812
902
|
eci: '0111',
|
|
813
|
-
},
|
|
903
|
+
}),
|
|
904
|
+
// 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.
|
|
814
905
|
capacity(ver, ecc) {
|
|
815
906
|
const bytes = BYTES[ver - 1];
|
|
816
907
|
const words = WORDS_PER_BLOCK[ecc][ver - 1];
|
|
@@ -826,8 +917,9 @@ const info = {
|
|
|
826
917
|
total: (words + blockLen) * numBlocks + numBlocks - shortBlocks,
|
|
827
918
|
};
|
|
828
919
|
},
|
|
829
|
-
};
|
|
830
|
-
|
|
920
|
+
});
|
|
921
|
+
// ISO/IEC 18004:2024 Table 10: QR data-mask predicates 000..111, written here in (x column, y row) form.
|
|
922
|
+
const PATTERNS = /* @__PURE__ */ Object.freeze([
|
|
831
923
|
(x, y) => (x + y) % 2 == 0,
|
|
832
924
|
(_x, y) => y % 2 == 0,
|
|
833
925
|
(x, _y) => x % 3 == 0,
|
|
@@ -836,8 +928,9 @@ const PATTERNS = [
|
|
|
836
928
|
(x, y) => ((x * y) % 2) + ((x * y) % 3) == 0,
|
|
837
929
|
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 == 0,
|
|
838
930
|
(x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 == 0,
|
|
839
|
-
];
|
|
931
|
+
]);
|
|
840
932
|
// Galois field && reed-solomon encoding
|
|
933
|
+
// 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.
|
|
841
934
|
const GF = {
|
|
842
935
|
tables: ((p_poly) => {
|
|
843
936
|
const exp = fillArr(256, 0);
|
|
@@ -849,26 +942,43 @@ const GF = {
|
|
|
849
942
|
if (x & 0x100)
|
|
850
943
|
x ^= p_poly;
|
|
851
944
|
}
|
|
945
|
+
// Keep α^255 = 1 in exp[255]; GF.log() folds the matching log[1] = 255
|
|
946
|
+
// back to 0 with `% 255`, so later helpers can wrap exponents without a special case.
|
|
852
947
|
return { exp, log };
|
|
853
948
|
})(0x11d),
|
|
949
|
+
// Raw α^i lookup from the precomputed field table; callers are expected
|
|
950
|
+
// to reduce / validate exponents before indexing it.
|
|
854
951
|
exp: (x) => GF.tables.exp[x],
|
|
952
|
+
// log(0) is undefined in GF(2^8); `% 255` also folds the wrapped table
|
|
953
|
+
// entry for α^255 = 1 back to exponent 0.
|
|
855
954
|
log(x) {
|
|
856
955
|
if (x === 0)
|
|
857
956
|
throw new Error(`GF.log: invalid arg=${x}`);
|
|
858
957
|
return GF.tables.log[x] % 255;
|
|
859
958
|
},
|
|
959
|
+
// Zero has no logarithm in GF(2^8), so it must short-circuit here; all
|
|
960
|
+
// other products are α^(log(x) + log(y) mod 255) in the reviewed field.
|
|
860
961
|
mul(x, y) {
|
|
861
962
|
if (x === 0 || y === 0)
|
|
862
963
|
return 0;
|
|
863
964
|
return GF.tables.exp[(GF.tables.log[x] + GF.tables.log[y]) % 255];
|
|
864
965
|
},
|
|
966
|
+
// In characteristic 2 fields, addition and subtraction are the same
|
|
967
|
+
// bitwise XOR operation used by the QR Reed-Solomon arithmetic.
|
|
865
968
|
add: (x, y) => x ^ y,
|
|
969
|
+
// Raw nonzero field power helper. Current QR use is GF.pow(2, i) for the
|
|
970
|
+
// Annex A generator factors; x = 0 or negative exponents are not validated.
|
|
866
971
|
pow: (x, e) => GF.tables.exp[(GF.tables.log[x] * e) % 255],
|
|
972
|
+
// Multiplicative inverse for nonzero field elements. Current callers only
|
|
973
|
+
// use it on values already known to be nonzero; 0 has no inverse in GF(2^8).
|
|
867
974
|
inv(x) {
|
|
868
975
|
if (x === 0)
|
|
869
976
|
throw new Error(`GF.inverse: invalid arg=${x}`);
|
|
870
977
|
return GF.tables.exp[255 - GF.tables.log[x]];
|
|
871
978
|
},
|
|
979
|
+
// Canonicalize coefficient arrays by trimming leading zero coefficients
|
|
980
|
+
// while preserving `[0]` as the zero polynomial; already-normalized inputs
|
|
981
|
+
// are returned by reference.
|
|
872
982
|
polynomial(poly) {
|
|
873
983
|
if (poly.length == 0)
|
|
874
984
|
throw new Error('GF.polymomial: invalid length');
|
|
@@ -880,6 +990,8 @@ const GF = {
|
|
|
880
990
|
;
|
|
881
991
|
return poly.slice(i);
|
|
882
992
|
},
|
|
993
|
+
// Represent c*x^degree in the descending-power coefficient layout used
|
|
994
|
+
// by the QR Reed-Solomon helpers; coefficient 0 canonicalizes to `[0]`.
|
|
883
995
|
monomial(degree, coefficient) {
|
|
884
996
|
if (degree < 0)
|
|
885
997
|
throw new Error(`GF.monomial: invalid degree=${degree}`);
|
|
@@ -889,8 +1001,14 @@ const GF = {
|
|
|
889
1001
|
coefficients[0] = coefficient;
|
|
890
1002
|
return GF.polynomial(coefficients);
|
|
891
1003
|
},
|
|
1004
|
+
// Canonical polynomials keep the highest-order coefficient first and use
|
|
1005
|
+
// `[0]` for zero, so degree is just `length - 1`.
|
|
892
1006
|
degree: (a) => a.length - 1,
|
|
1007
|
+
// Read the coefficient for x^degree from the descending-power array layout.
|
|
1008
|
+
// Canonical arrays make this a direct index; out-of-range degrees return `undefined`.
|
|
893
1009
|
coefficient: (a, degree) => a[GF.degree(a) - degree],
|
|
1010
|
+
// Multiply descending-power coefficient arrays by convolution over GF(2^8).
|
|
1011
|
+
// Zero short-circuits here before the log-based field multiply is consulted.
|
|
894
1012
|
mulPoly(a, b) {
|
|
895
1013
|
if (a[0] === 0 || b[0] === 0)
|
|
896
1014
|
return [0];
|
|
@@ -902,6 +1020,8 @@ const GF = {
|
|
|
902
1020
|
}
|
|
903
1021
|
return GF.polynomial(res);
|
|
904
1022
|
},
|
|
1023
|
+
// Scale every coefficient by the same field element in descending-power order.
|
|
1024
|
+
// Scalar 0 canonicalizes to `[0]`, and scalar 1 reuses the original array.
|
|
905
1025
|
mulPolyScalar(a, scalar) {
|
|
906
1026
|
if (scalar == 0)
|
|
907
1027
|
return [0];
|
|
@@ -912,6 +1032,8 @@ const GF = {
|
|
|
912
1032
|
res[i] = GF.mul(a[i], scalar);
|
|
913
1033
|
return GF.polynomial(res);
|
|
914
1034
|
},
|
|
1035
|
+
// Multiply a polynomial by c*x^degree in descending-power coefficient form.
|
|
1036
|
+
// This scales existing coefficients, then appends trailing zero coefficients.
|
|
915
1037
|
mulPolyMonomial(a, degree, coefficient) {
|
|
916
1038
|
if (degree < 0)
|
|
917
1039
|
throw new Error('GF.mulPolyMonomial: invalid degree');
|
|
@@ -922,6 +1044,8 @@ const GF = {
|
|
|
922
1044
|
res[i] = GF.mul(a[i], coefficient);
|
|
923
1045
|
return GF.polynomial(res);
|
|
924
1046
|
},
|
|
1047
|
+
// Add descending-power coefficient arrays with GF(2^8) XOR on the aligned
|
|
1048
|
+
// suffix; `[0]` short-circuits by returning the other array unchanged.
|
|
925
1049
|
addPoly(a, b) {
|
|
926
1050
|
if (a[0] === 0)
|
|
927
1051
|
return b;
|
|
@@ -940,6 +1064,8 @@ const GF = {
|
|
|
940
1064
|
sumDiff[i] = GF.add(smaller[i - lengthDiff], larger[i]);
|
|
941
1065
|
return GF.polynomial(sumDiff);
|
|
942
1066
|
},
|
|
1067
|
+
// Synthetic division for monic divisors in descending-power coefficient form.
|
|
1068
|
+
// Callers are expected to append `divisor.length - 1` zero coefficients first.
|
|
943
1069
|
remainderPoly(data, divisor) {
|
|
944
1070
|
const out = Array.from(data);
|
|
945
1071
|
for (let i = 0; i < data.length - divisor.length + 1; i++) {
|
|
@@ -953,12 +1079,16 @@ const GF = {
|
|
|
953
1079
|
}
|
|
954
1080
|
return out.slice(data.length - divisor.length + 1, out.length);
|
|
955
1081
|
},
|
|
1082
|
+
// Build Annex A's monic generator polynomial g_n(x) = Π(x - 2^i).
|
|
1083
|
+
// degree=0 returns `[1]`; callers are expected to validate degree bounds.
|
|
956
1084
|
divisorPoly(degree) {
|
|
957
1085
|
let g = [1];
|
|
958
1086
|
for (let i = 0; i < degree; i++)
|
|
959
1087
|
g = GF.mulPoly(g, [1, GF.pow(2, i)]);
|
|
960
1088
|
return g;
|
|
961
1089
|
},
|
|
1090
|
+
// Evaluate a descending-power coefficient array at `a` with Horner's rule.
|
|
1091
|
+
// The `a == 0` fast-path returns the x^0 coefficient directly.
|
|
962
1092
|
evalPoly(poly, a) {
|
|
963
1093
|
if (a == 0)
|
|
964
1094
|
return GF.coefficient(poly, 0); // Just return the x^0 coefficient
|
|
@@ -968,6 +1098,8 @@ const GF = {
|
|
|
968
1098
|
return res;
|
|
969
1099
|
},
|
|
970
1100
|
// TODO: cleanup
|
|
1101
|
+
// Extended Euclidean RS step: derive the locator/evaluator pair from x^R
|
|
1102
|
+
// and the syndrome polynomial, then normalize sigma(0) to 1.
|
|
971
1103
|
euclidian(a, b, R) {
|
|
972
1104
|
// Force degree(a) >= degree(b)
|
|
973
1105
|
if (GF.degree(a) < GF.degree(b))
|
|
@@ -1005,6 +1137,8 @@ const GF = {
|
|
|
1005
1137
|
return [GF.mulPolyScalar(t, inverse), GF.mulPolyScalar(r, inverse)];
|
|
1006
1138
|
},
|
|
1007
1139
|
};
|
|
1140
|
+
// Per-block Reed-Solomon coder: encode emits only the parity bytes for one
|
|
1141
|
+
// data block, while decode expects data+parity bytes and returns the corrected full block.
|
|
1008
1142
|
function RS(eccWords) {
|
|
1009
1143
|
return {
|
|
1010
1144
|
encode(from) {
|
|
@@ -1057,11 +1191,15 @@ function RS(eccWords) {
|
|
|
1057
1191
|
};
|
|
1058
1192
|
}
|
|
1059
1193
|
// Interleaves blocks
|
|
1194
|
+
// QR block interleaver / deinterleaver. Shorter data blocks stay first so
|
|
1195
|
+
// encode matches ISO/IEC 18004 §7.6 c) and decode can reverse it via §12 z)1.
|
|
1060
1196
|
function interleave(ver, ecc) {
|
|
1061
1197
|
const { words, shortBlocks, numBlocks, blockLen, total } = info.capacity(ver, ecc);
|
|
1062
1198
|
const rs = RS(words);
|
|
1063
1199
|
return {
|
|
1064
1200
|
encode(bytes) {
|
|
1201
|
+
// Caller must pass exactly the data codewords for this version/ecc;
|
|
1202
|
+
// this helper only splits blocks and interleaves them with RS parity.
|
|
1065
1203
|
// Add error correction to bytes
|
|
1066
1204
|
const blocks = [];
|
|
1067
1205
|
const eccBlocks = [];
|
|
@@ -1114,6 +1252,8 @@ function interleave(ver, ecc) {
|
|
|
1114
1252
|
}
|
|
1115
1253
|
// Draw
|
|
1116
1254
|
// Generic template per version+ecc+mask. Can be cached, to speedup calculations.
|
|
1255
|
+
// Function-pattern template plus reserved format/version areas; data modules
|
|
1256
|
+
// are filled later by zigzag placement in `drawQR`.
|
|
1117
1257
|
function drawTemplate(ver, ecc, maskIdx, test = false) {
|
|
1118
1258
|
const size = info.size.encode(ver);
|
|
1119
1259
|
let b = new Bitmap(size + 2);
|
|
@@ -1175,9 +1315,11 @@ function drawTemplate(ver, ecc, maskIdx, test = false) {
|
|
|
1175
1315
|
}
|
|
1176
1316
|
return b;
|
|
1177
1317
|
}
|
|
1178
|
-
// zigzag
|
|
1318
|
+
// Walk undefined data modules in the QR two-column zigzag order from the
|
|
1319
|
+
// lower right, skipping function patterns and the vertical timing column.
|
|
1179
1320
|
function zigzag(tpl, maskIdx, fn) {
|
|
1180
|
-
const
|
|
1321
|
+
const bm = tpl;
|
|
1322
|
+
const size = bm.height;
|
|
1181
1323
|
const pattern = PATTERNS[maskIdx];
|
|
1182
1324
|
// zig-zag pattern
|
|
1183
1325
|
let dir = -1;
|
|
@@ -1189,7 +1331,7 @@ function zigzag(tpl, maskIdx, fn) {
|
|
|
1189
1331
|
for (;; y += dir) {
|
|
1190
1332
|
for (let j = 0; j < 2; j += 1) {
|
|
1191
1333
|
const x = xOffset - j;
|
|
1192
|
-
if (
|
|
1334
|
+
if (bm.isDefined(x, y))
|
|
1193
1335
|
continue; // skip already written elements
|
|
1194
1336
|
fn(x, y, pattern(x, y));
|
|
1195
1337
|
}
|
|
@@ -1201,6 +1343,8 @@ function zigzag(tpl, maskIdx, fn) {
|
|
|
1201
1343
|
}
|
|
1202
1344
|
// NOTE: byte encoding is just representation, QR works with strings only. Most decoders will fail on raw byte array,
|
|
1203
1345
|
// since they expect unicode or other text encoding inside bytes
|
|
1346
|
+
// Auto-pick among the currently supported single-segment modes only.
|
|
1347
|
+
// Empty strings stay numeric, and any non-alphanumeric character falls back to byte.
|
|
1204
1348
|
function detectType(str) {
|
|
1205
1349
|
let type = 'numeric';
|
|
1206
1350
|
for (let x of str) {
|
|
@@ -1213,13 +1357,27 @@ function detectType(str) {
|
|
|
1213
1357
|
return type;
|
|
1214
1358
|
}
|
|
1215
1359
|
/**
|
|
1216
|
-
*
|
|
1360
|
+
* Encode a string as UTF-8 bytes.
|
|
1361
|
+
* @param str - Text to encode into UTF-8.
|
|
1362
|
+
* @returns UTF-8 bytes for the provided string.
|
|
1363
|
+
* @throws If the input is not a string. {@link Error}
|
|
1364
|
+
* @example
|
|
1365
|
+
* Encode a string as UTF-8 bytes.
|
|
1366
|
+
* ```ts
|
|
1367
|
+
* const bytes = utf8ToBytes('abc'); // new Uint8Array([97, 98, 99])
|
|
1368
|
+
* ```
|
|
1217
1369
|
*/
|
|
1370
|
+
// ISO/IEC 18004:2024 §7.3.2 says QR's default interpretation is
|
|
1371
|
+
// "ECI 000003 representing the ISO/IEC 8859-1 character set"; §7.4.2 says
|
|
1372
|
+
// non-default initial ECI data starts with an ECI header. Keep UTF-8 bytes
|
|
1373
|
+
// without that header for compatibility with existing emoji/qrcode fixtures.
|
|
1218
1374
|
export function utf8ToBytes(str) {
|
|
1219
1375
|
if (typeof str !== 'string')
|
|
1220
1376
|
throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
|
|
1221
1377
|
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
|
1222
1378
|
}
|
|
1379
|
+
// Build one QR mode/count/data segment, then append the terminator, zero padding,
|
|
1380
|
+
// and alternating pad codewords before RS interleaving.
|
|
1223
1381
|
function encode(ver, ecc, data, type, encoder = utf8ToBytes) {
|
|
1224
1382
|
let encoded = '';
|
|
1225
1383
|
let dataLen = data.length;
|
|
@@ -1244,6 +1402,7 @@ function encode(ver, ecc, data, type, encoder = utf8ToBytes) {
|
|
|
1244
1402
|
encoded += bin(t[n - 1], 6); // pad if odd number of chars
|
|
1245
1403
|
}
|
|
1246
1404
|
else if (type === 'byte') {
|
|
1405
|
+
// The default encoder is intentionally UTF-8-without-ECI; see utf8ToBytes().
|
|
1247
1406
|
const utf8 = encoder(data);
|
|
1248
1407
|
dataLen = utf8.length;
|
|
1249
1408
|
encoded = Array.from(utf8)
|
|
@@ -1272,6 +1431,8 @@ function encode(ver, ecc, data, type, encoder = utf8ToBytes) {
|
|
|
1272
1431
|
return interleave(ver, ecc).encode(bytes);
|
|
1273
1432
|
}
|
|
1274
1433
|
// DRAW
|
|
1434
|
+
// Stream interleaved codeword bits MSB-first through zigzag; any leftover
|
|
1435
|
+
// cells after the final codeword become zero-valued remainder bits before masking.
|
|
1275
1436
|
function drawQR(ver, ecc, data, maskIdx, test = false) {
|
|
1276
1437
|
const b = drawTemplate(ver, ecc, maskIdx, test);
|
|
1277
1438
|
let i = 0;
|
|
@@ -1288,6 +1449,8 @@ function drawQR(ver, ecc, data, maskIdx, test = false) {
|
|
|
1288
1449
|
throw new Error('QR: bytes left after draw');
|
|
1289
1450
|
return b;
|
|
1290
1451
|
}
|
|
1452
|
+
// Pack a left-to-right row pattern for `Bitmap.countPatternInRow()`; keep the
|
|
1453
|
+
// explicit width because leading light modules vanish from the numeric value.
|
|
1291
1454
|
const mkPattern = (pattern) => {
|
|
1292
1455
|
const s = pattern.map((i) => (i ? '1' : '0')).join('');
|
|
1293
1456
|
return { len: s.length, n: Number(`0b${s}`) };
|
|
@@ -1295,15 +1458,16 @@ const mkPattern = (pattern) => {
|
|
|
1295
1458
|
// 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
|
|
1296
1459
|
const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
|
|
1297
1460
|
const lightPattern = [false, false, false, false]; // light area 4 modules wide
|
|
1298
|
-
const P1 = mkPattern([...finderPattern, ...lightPattern]);
|
|
1299
|
-
const P2 = mkPattern([...lightPattern, ...finderPattern]);
|
|
1461
|
+
const P1 = /* @__PURE__ */ (() => mkPattern([...finderPattern, ...lightPattern]))();
|
|
1462
|
+
const P2 = /* @__PURE__ */ (() => mkPattern([...lightPattern, ...finderPattern]))();
|
|
1300
1463
|
function penalty(bm) {
|
|
1301
|
-
const
|
|
1302
|
-
const
|
|
1464
|
+
const b = bm;
|
|
1465
|
+
const { width, height } = b;
|
|
1466
|
+
const transposed = b.transpose();
|
|
1303
1467
|
// Adjacent modules in row/column in same | No. of modules = (5 + i) color
|
|
1304
1468
|
let adjacent = 0;
|
|
1305
1469
|
for (let y = 0; y < height; y++) {
|
|
1306
|
-
|
|
1470
|
+
b.getRuns(y, (len) => {
|
|
1307
1471
|
if (len >= 5)
|
|
1308
1472
|
adjacent += 3 + (len - 5);
|
|
1309
1473
|
});
|
|
@@ -1317,29 +1481,28 @@ function penalty(bm) {
|
|
|
1317
1481
|
// Block of modules in same color (Block size = 2x2)
|
|
1318
1482
|
let box = 0;
|
|
1319
1483
|
for (let y = 0; y < height - 1; y++)
|
|
1320
|
-
box += 3 *
|
|
1484
|
+
box += 3 * b.countBoxes2x2(y);
|
|
1321
1485
|
let finder = 0;
|
|
1322
1486
|
for (let y = 0; y < height; y++)
|
|
1323
|
-
finder += 40 *
|
|
1487
|
+
finder += 40 * b.countPatternInRow(y, P1.len, P1.n, P2.n);
|
|
1324
1488
|
for (let y = 0; y < width; y++)
|
|
1325
1489
|
finder += 40 * transposed.countPatternInRow(y, P1.len, P1.n, P2.n);
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
//
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
// for (let x = 0; x < width; x++) if (bm.get(x, y)) darkPixels++;
|
|
1334
|
-
// }
|
|
1335
|
-
const darkPercent = (darkPixels / (height * width)) * 100;
|
|
1336
|
-
const dark = 10 * Math.floor(Math.abs(darkPercent - 50) / 5);
|
|
1490
|
+
const total = height * width;
|
|
1491
|
+
const darkPixels = b.popcnt();
|
|
1492
|
+
// ISO/IEC 18004:2024 §7.8.3.1 NOTE 4 assigns "0 points" when the dark ratio
|
|
1493
|
+
// is "between 45 % and 55 %"; subtract that first 5% deviation band before
|
|
1494
|
+
// rating further 5% steps, so exact 45/55 and 40/60 boundaries stay in-band.
|
|
1495
|
+
const darkSteps = Math.ceil(Math.max(0, Math.abs(darkPixels * 100 - total * 50) - total * 5) / (total * 5));
|
|
1496
|
+
const dark = 10 * darkSteps;
|
|
1337
1497
|
return adjacent + box + finder + dark;
|
|
1338
1498
|
}
|
|
1339
1499
|
// Selects best mask according to penalty, if no mask is provided
|
|
1340
1500
|
function drawQRBest(ver, ecc, data, maskIdx) {
|
|
1341
1501
|
if (maskIdx === undefined) {
|
|
1342
1502
|
const bestMask = best();
|
|
1503
|
+
// ISO/IEC 18004:2024 §7.8.3.1 says mask penalty area is "the complete symbol",
|
|
1504
|
+
// but python-qrcode scores this placeholder form. Keep that output for compatibility
|
|
1505
|
+
// with common QR generators and to avoid fingerprinting this implementation.
|
|
1343
1506
|
for (let mask = 0; mask < PATTERNS.length; mask++)
|
|
1344
1507
|
bestMask.add(penalty(drawQR(ver, ecc, data, mask, true)), mask);
|
|
1345
1508
|
maskIdx = bestMask.get();
|
|
@@ -1363,24 +1526,25 @@ function validateMask(mask) {
|
|
|
1363
1526
|
throw new Error(`Invalid mask=${mask}. Expected number [0..7]`);
|
|
1364
1527
|
}
|
|
1365
1528
|
export function encodeQR(text, output = 'raw', opts = {}) {
|
|
1366
|
-
const
|
|
1529
|
+
const _opts = opts;
|
|
1530
|
+
const ecc = _opts.ecc !== undefined ? _opts.ecc : 'medium';
|
|
1367
1531
|
validateECC(ecc);
|
|
1368
|
-
const encoding =
|
|
1532
|
+
const encoding = _opts.encoding !== undefined ? _opts.encoding : detectType(text);
|
|
1369
1533
|
validateEncoding(encoding);
|
|
1370
|
-
if (
|
|
1371
|
-
validateMask(
|
|
1372
|
-
let ver =
|
|
1534
|
+
if (_opts.mask !== undefined)
|
|
1535
|
+
validateMask(_opts.mask);
|
|
1536
|
+
let ver = _opts.version;
|
|
1373
1537
|
let data, err = new Error('Unknown error');
|
|
1374
1538
|
if (ver !== undefined) {
|
|
1375
1539
|
validateVersion(ver);
|
|
1376
|
-
data = encode(ver, ecc, text, encoding,
|
|
1540
|
+
data = encode(ver, ecc, text, encoding, _opts.textEncoder);
|
|
1377
1541
|
}
|
|
1378
1542
|
else {
|
|
1379
1543
|
// If no version is provided, try to find smallest one which fits
|
|
1380
1544
|
// Currently just scans all version, can be significantly speedup if needed
|
|
1381
1545
|
for (let i = 1; i <= 40; i++) {
|
|
1382
1546
|
try {
|
|
1383
|
-
data = encode(i, ecc, text, encoding,
|
|
1547
|
+
data = encode(i, ecc, text, encoding, _opts.textEncoder);
|
|
1384
1548
|
ver = i;
|
|
1385
1549
|
break;
|
|
1386
1550
|
}
|
|
@@ -1391,20 +1555,24 @@ export function encodeQR(text, output = 'raw', opts = {}) {
|
|
|
1391
1555
|
}
|
|
1392
1556
|
if (!ver || !data)
|
|
1393
1557
|
throw err;
|
|
1394
|
-
let res = drawQRBest(ver, ecc, data,
|
|
1558
|
+
let res = drawQRBest(ver, ecc, data, _opts.mask);
|
|
1395
1559
|
res.assertDrawn();
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1560
|
+
// ISO/IEC 18004:2024 §5.3.8 says a QR quiet zone's "width shall be 4X",
|
|
1561
|
+
// and §9.1 requires 4X "on all four sides". Keep the compact historical
|
|
1562
|
+
// 2-module default to avoid changing encoder output; callers that need a
|
|
1563
|
+
// standards-conformant quiet zone must pass `border: 4` explicitly.
|
|
1564
|
+
const border = _opts.border === undefined ? 2 : _opts.border;
|
|
1565
|
+
if (!Number.isSafeInteger(border) || border <= 0)
|
|
1566
|
+
throw new Error(`invalid border=${border}`);
|
|
1399
1567
|
res = res.border(border, false); // Add border
|
|
1400
|
-
if (
|
|
1401
|
-
res = res.scale(
|
|
1568
|
+
if (_opts.scale !== undefined)
|
|
1569
|
+
res = res.scale(_opts.scale); // Scale image
|
|
1402
1570
|
if (output === 'raw')
|
|
1403
1571
|
return res.toRaw();
|
|
1404
1572
|
else if (output === 'ascii')
|
|
1405
1573
|
return res.toASCII();
|
|
1406
1574
|
else if (output === 'svg')
|
|
1407
|
-
return res.toSVG(
|
|
1575
|
+
return res.toSVG(_opts.optimize);
|
|
1408
1576
|
else if (output === 'gif')
|
|
1409
1577
|
return res.toGIF();
|
|
1410
1578
|
else if (output === 'term')
|
|
@@ -1412,8 +1580,32 @@ export function encodeQR(text, output = 'raw', opts = {}) {
|
|
|
1412
1580
|
else
|
|
1413
1581
|
throw new Error(`Unknown output: ${output}`);
|
|
1414
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Default export alias for {@link encodeQR}.
|
|
1585
|
+
* @param text - Text payload that should be encoded into the QR symbol.
|
|
1586
|
+
* @param output - Output format to generate: raw matrix, ASCII, terminal ANSI, GIF, or SVG.
|
|
1587
|
+
* @param opts - Encoding and rendering options. See {@link QrOpts} and {@link SvgQrOpts}.
|
|
1588
|
+
* @returns Encoded QR data in the format selected by `output`.
|
|
1589
|
+
* @throws If the payload, options, QR capacity, or output format are invalid. {@link Error}
|
|
1590
|
+
* @example
|
|
1591
|
+
* Encode text into the default export from the package root.
|
|
1592
|
+
* ```ts
|
|
1593
|
+
* import encodeQR from 'qr';
|
|
1594
|
+
* encodeQR('Hello world', 'ascii');
|
|
1595
|
+
* ```
|
|
1596
|
+
*/
|
|
1415
1597
|
export default encodeQR;
|
|
1416
|
-
|
|
1598
|
+
/**
|
|
1599
|
+
* Low-level helpers used by the encoder and test suite.
|
|
1600
|
+
* Exports the shared helper tables/functions through a frozen container.
|
|
1601
|
+
* @example
|
|
1602
|
+
* Read low-level QR metadata tables.
|
|
1603
|
+
* ```ts
|
|
1604
|
+
* import { utils } from 'qr';
|
|
1605
|
+
* const size = utils.info.size.encode(1); // 21
|
|
1606
|
+
* ```
|
|
1607
|
+
*/
|
|
1608
|
+
export const utils = /* @__PURE__ */ Object.freeze({
|
|
1417
1609
|
best,
|
|
1418
1610
|
bin,
|
|
1419
1611
|
popcnt,
|
|
@@ -1423,9 +1615,10 @@ export const utils = {
|
|
|
1423
1615
|
interleave,
|
|
1424
1616
|
validateVersion,
|
|
1425
1617
|
zigzag,
|
|
1426
|
-
};
|
|
1618
|
+
});
|
|
1427
1619
|
// Unsafe API utils, exported only for tests
|
|
1428
|
-
|
|
1620
|
+
// Exposes the shared internal helpers/tables through a frozen container.
|
|
1621
|
+
export const _tests = /* @__PURE__ */ Object.freeze({
|
|
1429
1622
|
Bitmap,
|
|
1430
1623
|
info,
|
|
1431
1624
|
detectType,
|
|
@@ -1433,7 +1626,7 @@ export const _tests = {
|
|
|
1433
1626
|
drawQR,
|
|
1434
1627
|
penalty,
|
|
1435
1628
|
PATTERNS,
|
|
1436
|
-
};
|
|
1629
|
+
});
|
|
1437
1630
|
// Type tests
|
|
1438
1631
|
// const o1 = qr('test', 'ascii');
|
|
1439
1632
|
// const o2 = qr('test', 'raw');
|