@naturalcycles/js-lib 15.78.0 → 15.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/qr/qr.ts ADDED
@@ -0,0 +1,1087 @@
1
+ // Vendored & modernized from https://github.com/kazuhikoarase/qrcode-generator
2
+ // Original: QR Code Generator for JavaScript, Copyright (c) 2009 Kazuhiko Arase, MIT license.
3
+ // The word 'QR Code' is a registered trademark of DENSO WAVE INCORPORATED.
4
+ //
5
+ // Reason for vendoring: the upstream package is unmaintained, ships as legacy closure-based
6
+ // JS, and carries a lot of dead weight (GIF/LZW encoder, base64 streams, HTML <table> renderer,
7
+ // Kanji/SJIS conversion tables). This is a compact, typed, tree-shakeable rewrite that keeps the
8
+ // proven core (Galois-field math, Reed-Solomon ECC, masking, data encoding) and drops everything
9
+ // that modern browsers/Node make trivial:
10
+ // - Byte mode now encodes UTF-8 via TextEncoder (the original mangled non-ASCII via `c & 0xff`).
11
+ // - Output is SVG (string or data URL), ASCII (terminal), or a 2d canvas context - no GIF/LZW.
12
+ //
13
+ // The generated module matrix is byte-for-byte identical to the upstream library (verified by
14
+ // differential tests against qrcode-generator@2.0.4), so any scanner-validated output stays valid.
15
+
16
+ // oxlint-disable no-bitwise, prefer-math-trunc -- QR encoding (Galois field, BCH, bit packing) is
17
+ // inherently bitwise; `(1 << 0)` terms mirror the spec's generator polynomials
18
+
19
+ /// <reference lib="dom" preserve="true" />
20
+
21
+ /**
22
+ * Error correction level, ordered by recovery capacity (and overhead):
23
+ * L ~7%, M ~15%, Q ~25%, H ~30%.
24
+ */
25
+ export type QrErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'
26
+
27
+ /**
28
+ * Encoding mode for the payload.
29
+ * - `numeric`: digits only (most compact)
30
+ * - `alphanumeric`: 0-9 A-Z and ` $%*+-./:` (uppercase only)
31
+ * - `byte`: any string, encoded as UTF-8
32
+ *
33
+ * When omitted, the smallest applicable mode is auto-detected.
34
+ */
35
+ export type QrMode = 'numeric' | 'alphanumeric' | 'byte'
36
+
37
+ export interface QrCodeOptions {
38
+ /**
39
+ * Error correction level. Default: `M`.
40
+ */
41
+ ecl?: QrErrorCorrectionLevel
42
+ /**
43
+ * QR version 1..40 (matrix grows by 4 modules per step: v1=21x21, v40=177x177).
44
+ * Default: `0` = auto-select the smallest version that fits the payload.
45
+ */
46
+ typeNumber?: number
47
+ /**
48
+ * Force an encoding mode. Default: auto-detect (`numeric` < `alphanumeric` < `byte`).
49
+ * Ignored when the content is a `Uint8Array` (always `byte`).
50
+ */
51
+ mode?: QrMode
52
+ }
53
+
54
+ export interface QrSvgOptions {
55
+ /** Pixels per module (the rendered `width`/`height` = `(size + border*2) * scale`). Default `4`. */
56
+ scale?: number
57
+ /** Quiet-zone width in modules. The spec recommends `4`. Default `4`. */
58
+ border?: number
59
+ /** Color of dark modules. Default `#000000`. */
60
+ dark?: string
61
+ /** Color of light modules / background. Default `#ffffff`. */
62
+ light?: string
63
+ }
64
+
65
+ export interface QrAsciiOptions {
66
+ /** Quiet-zone width in modules. Default `2`. */
67
+ border?: number
68
+ /** Swap dark/light glyphs (useful on dark terminal backgrounds). Default `false`. */
69
+ invert?: boolean
70
+ }
71
+
72
+ export interface QrCanvasOptions {
73
+ /** Pixels per module. Default `4`. */
74
+ scale?: number
75
+ /** Quiet-zone width in modules. Default `4`. */
76
+ border?: number
77
+ /** Color of dark modules. Default `#000000`. */
78
+ dark?: string
79
+ /** Color of light modules. Default `#ffffff`. */
80
+ light?: string
81
+ }
82
+
83
+ /**
84
+ * Create a QR code from a string or raw bytes.
85
+ *
86
+ * @example
87
+ * createQrCode('https://example.com').toDataUrl()
88
+ * createQrCode('HELLO', { ecl: 'H' }).toSvg({ scale: 8 })
89
+ */
90
+ export function createQrCode(content: string | Uint8Array, opt: QrCodeOptions = {}): QrCode {
91
+ const ecl = opt.ecl ?? 'M'
92
+ const segment = makeSegment(content, opt.mode)
93
+ const { size, modules } = generate(segment, opt.typeNumber ?? 0, ecl)
94
+ return new QrCode(size, modules, ecl)
95
+ }
96
+
97
+ /**
98
+ * An immutable QR code: a square matrix of dark/light modules, plus renderers.
99
+ */
100
+ export class QrCode {
101
+ constructor(
102
+ /** Width/height of the matrix in modules (`typeNumber * 4 + 17`). */
103
+ readonly size: number,
104
+ /** `modules[row][col]` - `true` = dark. */
105
+ readonly modules: readonly boolean[][],
106
+ /** Error correction level used. */
107
+ readonly ecl: QrErrorCorrectionLevel,
108
+ ) {}
109
+
110
+ /** Whether the module at `[row, col]` is dark. */
111
+ isDark(row: number, col: number): boolean {
112
+ return this.modules[row]![col] === true
113
+ }
114
+
115
+ /**
116
+ * Render as a standalone SVG document string.
117
+ */
118
+ toSvg(opt: QrSvgOptions = {}): string {
119
+ const { scale = 4, border = 4, dark = '#000000', light = '#ffffff' } = opt
120
+ const dim = this.size + border * 2
121
+ const px = dim * scale
122
+
123
+ let path = ''
124
+ for (let row = 0; row < this.size; row++) {
125
+ for (let col = 0; col < this.size; col++) {
126
+ if (this.modules[row]![col]) {
127
+ // a 1x1 module rect, in module-unit coordinates (the viewBox scales it up)
128
+ path += `M${col + border},${row + border}h1v1h-1z`
129
+ }
130
+ }
131
+ }
132
+
133
+ return (
134
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${px}" height="${px}" ` +
135
+ `viewBox="0 0 ${dim} ${dim}" shape-rendering="crispEdges">` +
136
+ `<rect width="${dim}" height="${dim}" fill="${light}"/>` +
137
+ `<path d="${path}" fill="${dark}"/>` +
138
+ `</svg>`
139
+ )
140
+ }
141
+
142
+ /**
143
+ * Render as an `data:image/svg+xml` URL, ready for `<img src>` or CSS `background`.
144
+ */
145
+ toDataUrl(opt: QrSvgOptions = {}): string {
146
+ return `data:image/svg+xml,${encodeURIComponent(this.toSvg(opt))}`
147
+ }
148
+
149
+ /**
150
+ * Render as ASCII art using block characters - handy for terminals and logs.
151
+ * Each module is 2 chars wide so the output keeps a square aspect ratio.
152
+ */
153
+ toAscii(opt: QrAsciiOptions = {}): string {
154
+ const { border = 2, invert = false } = opt
155
+ const darkCell = invert ? ' ' : '██'
156
+ const lightCell = invert ? '██' : ' '
157
+ const lines: string[] = []
158
+
159
+ for (let row = -border; row < this.size + border; row++) {
160
+ let line = ''
161
+ for (let col = -border; col < this.size + border; col++) {
162
+ const dark =
163
+ row >= 0 && row < this.size && col >= 0 && col < this.size && this.modules[row]![col]
164
+ line += dark ? darkCell : lightCell
165
+ }
166
+ lines.push(line)
167
+ }
168
+ return lines.join('\n')
169
+ }
170
+
171
+ /** Same as {@link toAscii} with defaults. */
172
+ toString(): string {
173
+ return this.toAscii()
174
+ }
175
+
176
+ /**
177
+ * Paint the QR code onto a 2d canvas context (browser).
178
+ */
179
+ renderToCanvas(ctx: CanvasRenderingContext2D, opt: QrCanvasOptions = {}): void {
180
+ const { scale = 4, border = 4, dark = '#000000', light = '#ffffff' } = opt
181
+ const px = (this.size + border * 2) * scale
182
+ ctx.fillStyle = light
183
+ ctx.fillRect(0, 0, px, px)
184
+ ctx.fillStyle = dark
185
+ for (let row = 0; row < this.size; row++) {
186
+ for (let col = 0; col < this.size; col++) {
187
+ if (this.modules[row]![col]) {
188
+ ctx.fillRect((col + border) * scale, (row + border) * scale, scale, scale)
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // --- internals -------------------------------------------------------------
196
+
197
+ const MODE_NUMERIC = 1 << 0
198
+ const MODE_ALPHANUMERIC = 1 << 1
199
+ const MODE_BYTE = 1 << 2
200
+
201
+ const PAD0 = 0xec
202
+ const PAD1 = 0x11
203
+
204
+ /** ECC level as encoded in the format-info bits. */
205
+ const ECL_BITS: Record<QrErrorCorrectionLevel, number> = { L: 1, M: 0, Q: 3, H: 2 }
206
+ /** ECC level as a row offset into the RS block table (ordered L, M, Q, H per version). */
207
+ const ECL_OFFSET: Record<QrErrorCorrectionLevel, number> = { L: 0, M: 1, Q: 2, H: 3 }
208
+
209
+ const NUMERIC_RE = /^\d+$/
210
+ const ALPHANUMERIC_RE = /^[\dA-Z $%*+./:-]+$/
211
+ const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
212
+
213
+ const textEncoder = /* @__PURE__ */ new TextEncoder()
214
+
215
+ /**
216
+ * A single encoded data segment (one mode + payload). The current API always produces exactly
217
+ * one segment, but the matrix builder is written against a list to mirror the spec.
218
+ */
219
+ interface QrSegment {
220
+ mode: number
221
+ /** Character count as written into the length field (digits/chars for text, bytes for byte mode). */
222
+ length: number
223
+ write: (bb: BitBuffer) => void
224
+ }
225
+
226
+ function makeSegment(content: string | Uint8Array, mode: QrMode | undefined): QrSegment {
227
+ if (typeof content !== 'string') return byteSegment(content)
228
+
229
+ const resolved = mode ?? detectMode(content)
230
+ if (resolved === 'numeric') return numericSegment(content)
231
+ if (resolved === 'alphanumeric') return alphanumericSegment(content)
232
+ return byteSegment(textEncoder.encode(content))
233
+ }
234
+
235
+ function detectMode(s: string): QrMode {
236
+ if (NUMERIC_RE.test(s)) return 'numeric'
237
+ if (ALPHANUMERIC_RE.test(s)) return 'alphanumeric'
238
+ return 'byte'
239
+ }
240
+
241
+ function numericSegment(data: string): QrSegment {
242
+ return {
243
+ mode: MODE_NUMERIC,
244
+ length: data.length,
245
+ write(bb) {
246
+ let i = 0
247
+ // 3 digits -> 10 bits
248
+ for (; i + 2 < data.length; i += 3) {
249
+ bb.put(Number(data.slice(i, i + 3)), 10)
250
+ }
251
+ // tail: 2 digits -> 7 bits, 1 digit -> 4 bits
252
+ if (data.length - i === 2) {
253
+ bb.put(Number(data.slice(i, i + 2)), 7)
254
+ } else if (data.length - i === 1) {
255
+ bb.put(Number(data.slice(i, i + 1)), 4)
256
+ }
257
+ },
258
+ }
259
+ }
260
+
261
+ function alphanumericSegment(data: string): QrSegment {
262
+ return {
263
+ mode: MODE_ALPHANUMERIC,
264
+ length: data.length,
265
+ write(bb) {
266
+ let i = 0
267
+ // 2 chars -> 11 bits (c0 * 45 + c1)
268
+ for (; i + 1 < data.length; i += 2) {
269
+ bb.put(alphanumericCode(data[i]!) * 45 + alphanumericCode(data[i + 1]!), 11)
270
+ }
271
+ // tail: 1 char -> 6 bits
272
+ if (i < data.length) {
273
+ bb.put(alphanumericCode(data[i]!), 6)
274
+ }
275
+ },
276
+ }
277
+ }
278
+
279
+ function alphanumericCode(c: string): number {
280
+ const code = ALPHANUMERIC_CHARS.indexOf(c)
281
+ if (code === -1) throw new Error(`qr: illegal alphanumeric char: ${c}`)
282
+ return code
283
+ }
284
+
285
+ function byteSegment(bytes: Uint8Array): QrSegment {
286
+ return {
287
+ mode: MODE_BYTE,
288
+ length: bytes.length,
289
+ write(bb) {
290
+ for (const b of bytes) bb.put(b, 8)
291
+ },
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Build the final module matrix: resolve version, encode data, then pick the mask pattern with the
297
+ * lowest penalty score (matching the reference scoring, incl. tie-break to the lowest mask index).
298
+ */
299
+ function generate(
300
+ segment: QrSegment,
301
+ typeNumber: number,
302
+ ecl: QrErrorCorrectionLevel,
303
+ ): { size: number; modules: boolean[][] } {
304
+ const resolvedType = typeNumber >= 1 ? typeNumber : bestTypeNumber(segment, ecl)
305
+ const data = createData(resolvedType, ecl, segment)
306
+
307
+ let bestMask = 0
308
+ let minLostPoint = Number.POSITIVE_INFINITY
309
+ for (let mask = 0; mask < 8; mask++) {
310
+ // scoring uses "test" modules (format/version bits blanked), as the original does
311
+ const testModules = renderModules(resolvedType, ecl, data, mask, true)
312
+ const lostPoint = getLostPoint(testModules)
313
+ if (lostPoint < minLostPoint) {
314
+ minLostPoint = lostPoint
315
+ bestMask = mask
316
+ }
317
+ }
318
+
319
+ const modules = renderModules(resolvedType, ecl, data, bestMask, false)
320
+ return { size: modules.length, modules }
321
+ }
322
+
323
+ /** Smallest version (1..40) whose data capacity fits the segment at the given ECC level. */
324
+ function bestTypeNumber(segment: QrSegment, ecl: QrErrorCorrectionLevel): number {
325
+ for (let type = 1; type <= 40; type++) {
326
+ const bb = new BitBuffer()
327
+ writeSegment(bb, segment, type)
328
+ let totalDataCount = 0
329
+ for (const block of getRsBlocks(type, ecl)) totalDataCount += block.dataCount
330
+ if (bb.lengthInBits <= totalDataCount * 8) return type
331
+ }
332
+ throw new Error('qr: data too long for a single QR code')
333
+ }
334
+
335
+ function renderModules(
336
+ typeNumber: number,
337
+ ecl: QrErrorCorrectionLevel,
338
+ data: number[],
339
+ maskPattern: number,
340
+ test: boolean,
341
+ ): boolean[][] {
342
+ const size = typeNumber * 4 + 17
343
+ // `undefined` = "not yet filled" (function patterns must not be overwritten by data/timing)
344
+ const modules: (boolean | undefined)[][] = Array.from({ length: size }, () =>
345
+ new Array<boolean | undefined>(size).fill(undefined),
346
+ )
347
+
348
+ setupPositionProbePattern(modules, 0, 0)
349
+ setupPositionProbePattern(modules, size - 7, 0)
350
+ setupPositionProbePattern(modules, 0, size - 7)
351
+ setupPositionAdjustPattern(modules, typeNumber)
352
+ setupTimingPattern(modules)
353
+ setupTypeInfo(modules, test, maskPattern, ecl)
354
+ if (typeNumber >= 7) {
355
+ setupTypeNumber(modules, test, typeNumber)
356
+ }
357
+ mapData(modules, data, maskPattern)
358
+
359
+ // every cell is filled by now; coerce the sentinel away
360
+ return modules as boolean[][]
361
+ }
362
+
363
+ function setupPositionProbePattern(
364
+ modules: (boolean | undefined)[][],
365
+ row: number,
366
+ col: number,
367
+ ): void {
368
+ const size = modules.length
369
+ for (let r = -1; r <= 7; r++) {
370
+ if (row + r <= -1 || size <= row + r) continue
371
+ for (let c = -1; c <= 7; c++) {
372
+ if (col + c <= -1 || size <= col + c) continue
373
+ modules[row + r]![col + c] =
374
+ (r >= 0 && r <= 6 && (c === 0 || c === 6)) ||
375
+ (c >= 0 && c <= 6 && (r === 0 || r === 6)) ||
376
+ (r >= 2 && r <= 4 && c >= 2 && c <= 4)
377
+ }
378
+ }
379
+ }
380
+
381
+ function setupPositionAdjustPattern(modules: (boolean | undefined)[][], typeNumber: number): void {
382
+ const pos = PATTERN_POSITION_TABLE[typeNumber - 1]!
383
+ for (const row of pos) {
384
+ for (const col of pos) {
385
+ if (modules[row]![col] !== undefined) continue
386
+ for (let r = -2; r <= 2; r++) {
387
+ for (let c = -2; c <= 2; c++) {
388
+ modules[row + r]![col + c] =
389
+ r === -2 || r === 2 || c === -2 || c === 2 || (r === 0 && c === 0)
390
+ }
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ function setupTimingPattern(modules: (boolean | undefined)[][]): void {
397
+ const size = modules.length
398
+ for (let r = 8; r < size - 8; r++) {
399
+ if (modules[r]![6] === undefined) modules[r]![6] = r % 2 === 0
400
+ }
401
+ for (let c = 8; c < size - 8; c++) {
402
+ if (modules[6]![c] === undefined) modules[6]![c] = c % 2 === 0
403
+ }
404
+ }
405
+
406
+ function setupTypeInfo(
407
+ modules: (boolean | undefined)[][],
408
+ test: boolean,
409
+ maskPattern: number,
410
+ ecl: QrErrorCorrectionLevel,
411
+ ): void {
412
+ const size = modules.length
413
+ const data = (ECL_BITS[ecl] << 3) | maskPattern
414
+ const bits = getBchTypeInfo(data)
415
+
416
+ for (let i = 0; i < 15; i++) {
417
+ const mod = !test && ((bits >> i) & 1) === 1
418
+
419
+ // vertical
420
+ if (i < 6) {
421
+ modules[i]![8] = mod
422
+ } else if (i < 8) {
423
+ modules[i + 1]![8] = mod
424
+ } else {
425
+ modules[size - 15 + i]![8] = mod
426
+ }
427
+
428
+ // horizontal
429
+ if (i < 8) {
430
+ modules[8]![size - i - 1] = mod
431
+ } else if (i < 9) {
432
+ modules[8]![15 - i - 1 + 1] = mod
433
+ } else {
434
+ modules[8]![15 - i - 1] = mod
435
+ }
436
+ }
437
+
438
+ // fixed dark module
439
+ modules[size - 8]![8] = !test
440
+ }
441
+
442
+ function setupTypeNumber(
443
+ modules: (boolean | undefined)[][],
444
+ test: boolean,
445
+ typeNumber: number,
446
+ ): void {
447
+ const size = modules.length
448
+ const bits = getBchTypeNumber(typeNumber)
449
+ for (let i = 0; i < 18; i++) {
450
+ const mod = !test && ((bits >> i) & 1) === 1
451
+ modules[Math.floor(i / 3)]![(i % 3) + size - 8 - 3] = mod
452
+ modules[(i % 3) + size - 8 - 3]![Math.floor(i / 3)] = mod
453
+ }
454
+ }
455
+
456
+ function mapData(modules: (boolean | undefined)[][], data: number[], maskPattern: number): void {
457
+ const size = modules.length
458
+ let inc = -1
459
+ let row = size - 1
460
+ let bitIndex = 7
461
+ let byteIndex = 0
462
+ const maskFunc = MASK_FUNCTIONS[maskPattern]!
463
+
464
+ for (let col = size - 1; col > 0; col -= 2) {
465
+ if (col === 6) col -= 1
466
+ for (;;) {
467
+ for (let c = 0; c < 2; c++) {
468
+ if (modules[row]![col - c] === undefined) {
469
+ let dark = false
470
+ if (byteIndex < data.length) {
471
+ dark = ((data[byteIndex]! >>> bitIndex) & 1) === 1
472
+ }
473
+ if (maskFunc(row, col - c)) dark = !dark
474
+ modules[row]![col - c] = dark
475
+ bitIndex--
476
+ if (bitIndex === -1) {
477
+ byteIndex++
478
+ bitIndex = 7
479
+ }
480
+ }
481
+ }
482
+ row += inc
483
+ if (row < 0 || size <= row) {
484
+ row -= inc
485
+ inc = -inc
486
+ break
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ /** Mask penalty score - lower is better. Sum of the 4 penalty rules from the QR spec. */
493
+ function getLostPoint(modules: boolean[][]): number {
494
+ return (
495
+ lostPointAdjacent(modules) +
496
+ lostPointBlocks(modules) +
497
+ lostPointFinderLike(modules) +
498
+ lostPointDarkRatio(modules)
499
+ )
500
+ }
501
+
502
+ /** LEVEL 1: runs of same-colored modules in a 3x3 neighbourhood. */
503
+ function lostPointAdjacent(modules: boolean[][]): number {
504
+ const size = modules.length
505
+ let lostPoint = 0
506
+ for (let row = 0; row < size; row++) {
507
+ for (let col = 0; col < size; col++) {
508
+ let sameCount = 0
509
+ const dark = modules[row]![col]
510
+ for (let r = -1; r <= 1; r++) {
511
+ if (row + r < 0 || size <= row + r) continue
512
+ for (let c = -1; c <= 1; c++) {
513
+ if (col + c < 0 || size <= col + c) continue
514
+ if (r === 0 && c === 0) continue
515
+ if (dark === modules[row + r]![col + c]) sameCount++
516
+ }
517
+ }
518
+ if (sameCount > 5) lostPoint += 3 + sameCount - 5
519
+ }
520
+ }
521
+ return lostPoint
522
+ }
523
+
524
+ /** LEVEL 2: 2x2 blocks of one color. */
525
+ function lostPointBlocks(modules: boolean[][]): number {
526
+ const size = modules.length
527
+ let lostPoint = 0
528
+ for (let row = 0; row < size - 1; row++) {
529
+ for (let col = 0; col < size - 1; col++) {
530
+ let count = 0
531
+ if (modules[row]![col]) count++
532
+ if (modules[row + 1]![col]) count++
533
+ if (modules[row]![col + 1]) count++
534
+ if (modules[row + 1]![col + 1]) count++
535
+ if (count === 0 || count === 4) lostPoint += 3
536
+ }
537
+ }
538
+ return lostPoint
539
+ }
540
+
541
+ /** LEVEL 3: finder-like 1:1:3:1:1 patterns, horizontal and vertical. */
542
+ function lostPointFinderLike(modules: boolean[][]): number {
543
+ const size = modules.length
544
+ let lostPoint = 0
545
+ const isDark = (r: number, c: number): boolean => modules[r]![c] === true
546
+ for (let row = 0; row < size; row++) {
547
+ for (let col = 0; col < size - 6; col++) {
548
+ if (
549
+ isDark(row, col) &&
550
+ !isDark(row, col + 1) &&
551
+ isDark(row, col + 2) &&
552
+ isDark(row, col + 3) &&
553
+ isDark(row, col + 4) &&
554
+ !isDark(row, col + 5) &&
555
+ isDark(row, col + 6)
556
+ ) {
557
+ lostPoint += 40
558
+ }
559
+ }
560
+ }
561
+ for (let col = 0; col < size; col++) {
562
+ for (let row = 0; row < size - 6; row++) {
563
+ if (
564
+ isDark(row, col) &&
565
+ !isDark(row + 1, col) &&
566
+ isDark(row + 2, col) &&
567
+ isDark(row + 3, col) &&
568
+ isDark(row + 4, col) &&
569
+ !isDark(row + 5, col) &&
570
+ isDark(row + 6, col)
571
+ ) {
572
+ lostPoint += 40
573
+ }
574
+ }
575
+ }
576
+ return lostPoint
577
+ }
578
+
579
+ /** LEVEL 4: deviation of the dark-module ratio from 50%. */
580
+ function lostPointDarkRatio(modules: boolean[][]): number {
581
+ const size = modules.length
582
+ let darkCount = 0
583
+ for (let col = 0; col < size; col++) {
584
+ for (let row = 0; row < size; row++) {
585
+ if (modules[row]![col]) darkCount++
586
+ }
587
+ }
588
+ const ratio = Math.abs((100 * darkCount) / size / size - 50) / 5
589
+ return ratio * 10
590
+ }
591
+
592
+ // --- data encoding (ECC) ---------------------------------------------------
593
+
594
+ function createData(typeNumber: number, ecl: QrErrorCorrectionLevel, segment: QrSegment): number[] {
595
+ const rsBlocks = getRsBlocks(typeNumber, ecl)
596
+ const bb = new BitBuffer()
597
+ writeSegment(bb, segment, typeNumber)
598
+
599
+ let totalDataCount = 0
600
+ for (const block of rsBlocks) totalDataCount += block.dataCount
601
+
602
+ if (bb.lengthInBits > totalDataCount * 8) {
603
+ throw new Error(`qr: code length overflow (${bb.lengthInBits} > ${totalDataCount * 8})`)
604
+ }
605
+
606
+ // terminator
607
+ if (bb.lengthInBits + 4 <= totalDataCount * 8) bb.put(0, 4)
608
+ // pad to byte boundary
609
+ while (bb.lengthInBits % 8 !== 0) bb.putBit(false)
610
+ // pad with alternating PAD0/PAD1 to capacity
611
+ for (;;) {
612
+ if (bb.lengthInBits >= totalDataCount * 8) break
613
+ bb.put(PAD0, 8)
614
+ if (bb.lengthInBits >= totalDataCount * 8) break
615
+ bb.put(PAD1, 8)
616
+ }
617
+
618
+ return createBytes(bb, rsBlocks)
619
+ }
620
+
621
+ function writeSegment(bb: BitBuffer, segment: QrSegment, typeNumber: number): void {
622
+ bb.put(segment.mode, 4)
623
+ bb.put(segment.length, getLengthInBits(segment.mode, typeNumber))
624
+ segment.write(bb)
625
+ }
626
+
627
+ /** Interleave data & error-correction codewords across the RS blocks. */
628
+ function createBytes(buffer: BitBuffer, rsBlocks: RsBlock[]): number[] {
629
+ let offset = 0
630
+ let maxDcCount = 0
631
+ let maxEcCount = 0
632
+ const dcdata: number[][] = new Array(rsBlocks.length)
633
+ const ecdata: number[][] = new Array(rsBlocks.length)
634
+ const src = buffer.buffer
635
+
636
+ for (let r = 0; r < rsBlocks.length; r++) {
637
+ const dcCount = rsBlocks[r]!.dataCount
638
+ const ecCount = rsBlocks[r]!.totalCount - dcCount
639
+ maxDcCount = Math.max(maxDcCount, dcCount)
640
+ maxEcCount = Math.max(maxEcCount, ecCount)
641
+
642
+ const dc = new Array<number>(dcCount)
643
+ for (let i = 0; i < dcCount; i++) dc[i] = 0xff & src[i + offset]!
644
+ dcdata[r] = dc
645
+ offset += dcCount
646
+
647
+ const rsPoly = getErrorCorrectPolynomial(ecCount)
648
+ const modPoly = polyMod(newPolynomial(dc, rsPoly.length - 1), rsPoly)
649
+ const ec = new Array<number>(rsPoly.length - 1)
650
+ for (let i = 0; i < ec.length; i++) {
651
+ const modIndex = i + modPoly.length - ec.length
652
+ ec[i] = modIndex >= 0 ? modPoly[modIndex]! : 0
653
+ }
654
+ ecdata[r] = ec
655
+ }
656
+
657
+ let totalCodeCount = 0
658
+ for (const block of rsBlocks) totalCodeCount += block.totalCount
659
+
660
+ const data = new Array<number>(totalCodeCount)
661
+ let index = 0
662
+ for (let i = 0; i < maxDcCount; i++) {
663
+ for (let r = 0; r < rsBlocks.length; r++) {
664
+ if (i < dcdata[r]!.length) data[index++] = dcdata[r]![i]!
665
+ }
666
+ }
667
+ for (let i = 0; i < maxEcCount; i++) {
668
+ for (let r = 0; r < rsBlocks.length; r++) {
669
+ if (i < ecdata[r]!.length) data[index++] = ecdata[r]![i]!
670
+ }
671
+ }
672
+ return data
673
+ }
674
+
675
+ /** Bits in the character-count field, by mode, for version ranges [1-9], [10-26], [27-40]. */
676
+ const LENGTH_BITS: Record<number, [number, number, number]> = {
677
+ [MODE_NUMERIC]: [10, 12, 14],
678
+ [MODE_ALPHANUMERIC]: [9, 11, 13],
679
+ [MODE_BYTE]: [8, 16, 16],
680
+ }
681
+
682
+ /** Number of bits in the character-count field, per mode & version range. */
683
+ function getLengthInBits(mode: number, type: number): number {
684
+ if (type < 1 || type > 40) throw new Error(`qr: bad type number: ${type}`)
685
+ const bits = LENGTH_BITS[mode]
686
+ if (!bits) throw new Error(`qr: bad mode: ${mode}`)
687
+ const range = type < 10 ? 0 : type < 27 ? 1 : 2
688
+ return bits[range]
689
+ }
690
+
691
+ // --- Reed-Solomon polynomials over GF(256) ---------------------------------
692
+
693
+ /** Drop leading zeros, then right-pad with `shift` zeros. */
694
+ function newPolynomial(num: number[], shift: number): number[] {
695
+ let offset = 0
696
+ while (offset < num.length && num[offset] === 0) offset++
697
+ const poly = new Array<number>(num.length - offset + shift).fill(0)
698
+ for (let i = 0; i < num.length - offset; i++) poly[i] = num[i + offset]!
699
+ return poly
700
+ }
701
+
702
+ function polyMultiply(a: number[], b: number[]): number[] {
703
+ const num = new Array<number>(a.length + b.length - 1).fill(0)
704
+ for (let i = 0; i < a.length; i++) {
705
+ for (let j = 0; j < b.length; j++) {
706
+ num[i + j]! ^= gexp(glog(a[i]!) + glog(b[j]!))
707
+ }
708
+ }
709
+ return newPolynomial(num, 0)
710
+ }
711
+
712
+ function polyMod(a: number[], b: number[]): number[] {
713
+ if (a.length - b.length < 0) return a
714
+ const ratio = glog(a[0]!) - glog(b[0]!)
715
+ const num = a.slice()
716
+ for (let i = 0; i < b.length; i++) {
717
+ num[i]! ^= gexp(glog(b[i]!) + ratio)
718
+ }
719
+ // recurse until degree drops below the divisor
720
+ return polyMod(newPolynomial(num, 0), b)
721
+ }
722
+
723
+ function getErrorCorrectPolynomial(ecLength: number): number[] {
724
+ let poly = [1]
725
+ for (let i = 0; i < ecLength; i++) {
726
+ poly = polyMultiply(poly, [1, gexp(i)])
727
+ }
728
+ return poly
729
+ }
730
+
731
+ // --- BCH codes (format & version info) -------------------------------------
732
+
733
+ const G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)
734
+ const G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0)
735
+ const G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
736
+
737
+ function getBchDigit(data: number): number {
738
+ let digit = 0
739
+ while (data !== 0) {
740
+ digit++
741
+ data >>>= 1
742
+ }
743
+ return digit
744
+ }
745
+
746
+ function getBchTypeInfo(data: number): number {
747
+ let d = data << 10
748
+ while (getBchDigit(d) - getBchDigit(G15) >= 0) {
749
+ d ^= G15 << (getBchDigit(d) - getBchDigit(G15))
750
+ }
751
+ return ((data << 10) | d) ^ G15_MASK
752
+ }
753
+
754
+ function getBchTypeNumber(data: number): number {
755
+ let d = data << 12
756
+ while (getBchDigit(d) - getBchDigit(G18) >= 0) {
757
+ d ^= G18 << (getBchDigit(d) - getBchDigit(G18))
758
+ }
759
+ return (data << 12) | d
760
+ }
761
+
762
+ // --- Galois field GF(256) math ---------------------------------------------
763
+
764
+ const EXP_TABLE = new Uint8Array(256)
765
+ const LOG_TABLE = new Uint8Array(256)
766
+ for (let i = 0; i < 8; i++) EXP_TABLE[i] = 1 << i
767
+ for (let i = 8; i < 256; i++) {
768
+ EXP_TABLE[i] = EXP_TABLE[i - 4]! ^ EXP_TABLE[i - 5]! ^ EXP_TABLE[i - 6]! ^ EXP_TABLE[i - 8]!
769
+ }
770
+ for (let i = 0; i < 255; i++) LOG_TABLE[EXP_TABLE[i]!] = i
771
+
772
+ function glog(n: number): number {
773
+ if (n < 1) throw new Error(`qr: glog(${n})`)
774
+ return LOG_TABLE[n]!
775
+ }
776
+
777
+ function gexp(n: number): number {
778
+ while (n < 0) n += 255
779
+ while (n >= 256) n -= 255
780
+ return EXP_TABLE[n]!
781
+ }
782
+
783
+ // --- bit buffer ------------------------------------------------------------
784
+
785
+ class BitBuffer {
786
+ readonly buffer: number[] = []
787
+ lengthInBits = 0
788
+
789
+ put(num: number, length: number): void {
790
+ for (let i = 0; i < length; i++) {
791
+ this.putBit(((num >>> (length - i - 1)) & 1) === 1)
792
+ }
793
+ }
794
+
795
+ putBit(bit: boolean): void {
796
+ const bufIndex = Math.floor(this.lengthInBits / 8)
797
+ if (this.buffer.length <= bufIndex) this.buffer.push(0)
798
+ if (bit) this.buffer[bufIndex]! |= 0x80 >>> (this.lengthInBits % 8)
799
+ this.lengthInBits++
800
+ }
801
+ }
802
+
803
+ // --- RS block & alignment-pattern tables -----------------------------------
804
+
805
+ interface RsBlock {
806
+ totalCount: number
807
+ dataCount: number
808
+ }
809
+
810
+ function getRsBlocks(typeNumber: number, ecl: QrErrorCorrectionLevel): RsBlock[] {
811
+ const rsBlock = RS_BLOCK_TABLE[(typeNumber - 1) * 4 + ECL_OFFSET[ecl]]
812
+ if (!rsBlock) {
813
+ throw new Error(`qr: bad rs block @ typeNumber:${typeNumber}/ecl:${ecl}`)
814
+ }
815
+ const blocks: RsBlock[] = []
816
+ // each row is a flat list of [count, totalCount, dataCount] triples
817
+ for (let i = 0; i < rsBlock.length; i += 3) {
818
+ const count = rsBlock[i]!
819
+ const totalCount = rsBlock[i + 1]!
820
+ const dataCount = rsBlock[i + 2]!
821
+ for (let j = 0; j < count; j++) blocks.push({ totalCount, dataCount })
822
+ }
823
+ return blocks
824
+ }
825
+
826
+ /** Alignment-pattern centre coordinates, indexed by `typeNumber - 1`. */
827
+ const PATTERN_POSITION_TABLE: number[][] = [
828
+ [],
829
+ [6, 18],
830
+ [6, 22],
831
+ [6, 26],
832
+ [6, 30],
833
+ [6, 34],
834
+ [6, 22, 38],
835
+ [6, 24, 42],
836
+ [6, 26, 46],
837
+ [6, 28, 50],
838
+ [6, 30, 54],
839
+ [6, 32, 58],
840
+ [6, 34, 62],
841
+ [6, 26, 46, 66],
842
+ [6, 26, 48, 70],
843
+ [6, 26, 50, 74],
844
+ [6, 30, 54, 78],
845
+ [6, 30, 56, 82],
846
+ [6, 30, 58, 86],
847
+ [6, 34, 62, 90],
848
+ [6, 28, 50, 72, 94],
849
+ [6, 26, 50, 74, 98],
850
+ [6, 30, 54, 78, 102],
851
+ [6, 28, 54, 80, 106],
852
+ [6, 32, 58, 84, 110],
853
+ [6, 30, 58, 86, 114],
854
+ [6, 34, 62, 90, 118],
855
+ [6, 26, 50, 74, 98, 122],
856
+ [6, 30, 54, 78, 102, 126],
857
+ [6, 26, 52, 78, 104, 130],
858
+ [6, 30, 56, 82, 108, 134],
859
+ [6, 34, 60, 86, 112, 138],
860
+ [6, 30, 58, 86, 114, 142],
861
+ [6, 34, 62, 90, 118, 146],
862
+ [6, 30, 54, 78, 102, 126, 150],
863
+ [6, 24, 50, 76, 102, 128, 154],
864
+ [6, 28, 54, 80, 106, 132, 158],
865
+ [6, 32, 58, 84, 110, 136, 162],
866
+ [6, 26, 54, 82, 110, 138, 166],
867
+ [6, 30, 58, 86, 114, 142, 170],
868
+ ]
869
+
870
+ /** The 8 data-masking functions, indexed by mask pattern (0..7). */
871
+ const MASK_FUNCTIONS: ((i: number, j: number) => boolean)[] = [
872
+ (i, j) => (i + j) % 2 === 0,
873
+ (i, _j) => i % 2 === 0,
874
+ (_i, j) => j % 3 === 0,
875
+ (i, j) => (i + j) % 3 === 0,
876
+ (i, j) => (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0,
877
+ (i, j) => ((i * j) % 2) + ((i * j) % 3) === 0,
878
+ (i, j) => (((i * j) % 2) + ((i * j) % 3)) % 2 === 0,
879
+ (i, j) => (((i * j) % 3) + ((i + j) % 2)) % 2 === 0,
880
+ ]
881
+
882
+ /**
883
+ * RS block layout, 4 rows per version (L, M, Q, H), each a flat list of
884
+ * `[count, totalCount, dataCount]` triples. Verbatim from the QR spec.
885
+ */
886
+ const RS_BLOCK_TABLE: number[][] = [
887
+ // 1
888
+ [1, 26, 19],
889
+ [1, 26, 16],
890
+ [1, 26, 13],
891
+ [1, 26, 9],
892
+ // 2
893
+ [1, 44, 34],
894
+ [1, 44, 28],
895
+ [1, 44, 22],
896
+ [1, 44, 16],
897
+ // 3
898
+ [1, 70, 55],
899
+ [1, 70, 44],
900
+ [2, 35, 17],
901
+ [2, 35, 13],
902
+ // 4
903
+ [1, 100, 80],
904
+ [2, 50, 32],
905
+ [2, 50, 24],
906
+ [4, 25, 9],
907
+ // 5
908
+ [1, 134, 108],
909
+ [2, 67, 43],
910
+ [2, 33, 15, 2, 34, 16],
911
+ [2, 33, 11, 2, 34, 12],
912
+ // 6
913
+ [2, 86, 68],
914
+ [4, 43, 27],
915
+ [4, 43, 19],
916
+ [4, 43, 15],
917
+ // 7
918
+ [2, 98, 78],
919
+ [4, 49, 31],
920
+ [2, 32, 14, 4, 33, 15],
921
+ [4, 39, 13, 1, 40, 14],
922
+ // 8
923
+ [2, 121, 97],
924
+ [2, 60, 38, 2, 61, 39],
925
+ [4, 40, 18, 2, 41, 19],
926
+ [4, 40, 14, 2, 41, 15],
927
+ // 9
928
+ [2, 146, 116],
929
+ [3, 58, 36, 2, 59, 37],
930
+ [4, 36, 16, 4, 37, 17],
931
+ [4, 36, 12, 4, 37, 13],
932
+ // 10
933
+ [2, 86, 68, 2, 87, 69],
934
+ [4, 69, 43, 1, 70, 44],
935
+ [6, 43, 19, 2, 44, 20],
936
+ [6, 43, 15, 2, 44, 16],
937
+ // 11
938
+ [4, 101, 81],
939
+ [1, 80, 50, 4, 81, 51],
940
+ [4, 50, 22, 4, 51, 23],
941
+ [3, 36, 12, 8, 37, 13],
942
+ // 12
943
+ [2, 116, 92, 2, 117, 93],
944
+ [6, 58, 36, 2, 59, 37],
945
+ [4, 46, 20, 6, 47, 21],
946
+ [7, 42, 14, 4, 43, 15],
947
+ // 13
948
+ [4, 133, 107],
949
+ [8, 59, 37, 1, 60, 38],
950
+ [8, 44, 20, 4, 45, 21],
951
+ [12, 33, 11, 4, 34, 12],
952
+ // 14
953
+ [3, 145, 115, 1, 146, 116],
954
+ [4, 64, 40, 5, 65, 41],
955
+ [11, 36, 16, 5, 37, 17],
956
+ [11, 36, 12, 5, 37, 13],
957
+ // 15
958
+ [5, 109, 87, 1, 110, 88],
959
+ [5, 65, 41, 5, 66, 42],
960
+ [5, 54, 24, 7, 55, 25],
961
+ [11, 36, 12, 7, 37, 13],
962
+ // 16
963
+ [5, 122, 98, 1, 123, 99],
964
+ [7, 73, 45, 3, 74, 46],
965
+ [15, 43, 19, 2, 44, 20],
966
+ [3, 45, 15, 13, 46, 16],
967
+ // 17
968
+ [1, 135, 107, 5, 136, 108],
969
+ [10, 74, 46, 1, 75, 47],
970
+ [1, 50, 22, 15, 51, 23],
971
+ [2, 42, 14, 17, 43, 15],
972
+ // 18
973
+ [5, 150, 120, 1, 151, 121],
974
+ [9, 69, 43, 4, 70, 44],
975
+ [17, 50, 22, 1, 51, 23],
976
+ [2, 42, 14, 19, 43, 15],
977
+ // 19
978
+ [3, 141, 113, 4, 142, 114],
979
+ [3, 70, 44, 11, 71, 45],
980
+ [17, 47, 21, 4, 48, 22],
981
+ [9, 39, 13, 16, 40, 14],
982
+ // 20
983
+ [3, 135, 107, 5, 136, 108],
984
+ [3, 67, 41, 13, 68, 42],
985
+ [15, 54, 24, 5, 55, 25],
986
+ [15, 43, 15, 10, 44, 16],
987
+ // 21
988
+ [4, 144, 116, 4, 145, 117],
989
+ [17, 68, 42],
990
+ [17, 50, 22, 6, 51, 23],
991
+ [19, 46, 16, 6, 47, 17],
992
+ // 22
993
+ [2, 139, 111, 7, 140, 112],
994
+ [17, 74, 46],
995
+ [7, 54, 24, 16, 55, 25],
996
+ [34, 37, 13],
997
+ // 23
998
+ [4, 151, 121, 5, 152, 122],
999
+ [4, 75, 47, 14, 76, 48],
1000
+ [11, 54, 24, 14, 55, 25],
1001
+ [16, 45, 15, 14, 46, 16],
1002
+ // 24
1003
+ [6, 147, 117, 4, 148, 118],
1004
+ [6, 73, 45, 14, 74, 46],
1005
+ [11, 54, 24, 16, 55, 25],
1006
+ [30, 46, 16, 2, 47, 17],
1007
+ // 25
1008
+ [8, 132, 106, 4, 133, 107],
1009
+ [8, 75, 47, 13, 76, 48],
1010
+ [7, 54, 24, 22, 55, 25],
1011
+ [22, 45, 15, 13, 46, 16],
1012
+ // 26
1013
+ [10, 142, 114, 2, 143, 115],
1014
+ [19, 74, 46, 4, 75, 47],
1015
+ [28, 50, 22, 6, 51, 23],
1016
+ [33, 46, 16, 4, 47, 17],
1017
+ // 27
1018
+ [8, 152, 122, 4, 153, 123],
1019
+ [22, 73, 45, 3, 74, 46],
1020
+ [8, 53, 23, 26, 54, 24],
1021
+ [12, 45, 15, 28, 46, 16],
1022
+ // 28
1023
+ [3, 147, 117, 10, 148, 118],
1024
+ [3, 73, 45, 23, 74, 46],
1025
+ [4, 54, 24, 31, 55, 25],
1026
+ [11, 45, 15, 31, 46, 16],
1027
+ // 29
1028
+ [7, 146, 116, 7, 147, 117],
1029
+ [21, 73, 45, 7, 74, 46],
1030
+ [1, 53, 23, 37, 54, 24],
1031
+ [19, 45, 15, 26, 46, 16],
1032
+ // 30
1033
+ [5, 145, 115, 10, 146, 116],
1034
+ [19, 75, 47, 10, 76, 48],
1035
+ [15, 54, 24, 25, 55, 25],
1036
+ [23, 45, 15, 25, 46, 16],
1037
+ // 31
1038
+ [13, 145, 115, 3, 146, 116],
1039
+ [2, 74, 46, 29, 75, 47],
1040
+ [42, 54, 24, 1, 55, 25],
1041
+ [23, 45, 15, 28, 46, 16],
1042
+ // 32
1043
+ [17, 145, 115],
1044
+ [10, 74, 46, 23, 75, 47],
1045
+ [10, 54, 24, 35, 55, 25],
1046
+ [19, 45, 15, 35, 46, 16],
1047
+ // 33
1048
+ [17, 145, 115, 1, 146, 116],
1049
+ [14, 74, 46, 21, 75, 47],
1050
+ [29, 54, 24, 19, 55, 25],
1051
+ [11, 45, 15, 46, 46, 16],
1052
+ // 34
1053
+ [13, 145, 115, 6, 146, 116],
1054
+ [14, 74, 46, 23, 75, 47],
1055
+ [44, 54, 24, 7, 55, 25],
1056
+ [59, 46, 16, 1, 47, 17],
1057
+ // 35
1058
+ [12, 151, 121, 7, 152, 122],
1059
+ [12, 75, 47, 26, 76, 48],
1060
+ [39, 54, 24, 14, 55, 25],
1061
+ [22, 45, 15, 41, 46, 16],
1062
+ // 36
1063
+ [6, 151, 121, 14, 152, 122],
1064
+ [6, 75, 47, 34, 76, 48],
1065
+ [46, 54, 24, 10, 55, 25],
1066
+ [2, 45, 15, 64, 46, 16],
1067
+ // 37
1068
+ [17, 152, 122, 4, 153, 123],
1069
+ [29, 74, 46, 14, 75, 47],
1070
+ [49, 54, 24, 10, 55, 25],
1071
+ [24, 45, 15, 46, 46, 16],
1072
+ // 38
1073
+ [4, 152, 122, 18, 153, 123],
1074
+ [13, 74, 46, 32, 75, 47],
1075
+ [48, 54, 24, 14, 55, 25],
1076
+ [42, 45, 15, 32, 46, 16],
1077
+ // 39
1078
+ [20, 147, 117, 4, 148, 118],
1079
+ [40, 75, 47, 7, 76, 48],
1080
+ [43, 54, 24, 22, 55, 25],
1081
+ [10, 45, 15, 67, 46, 16],
1082
+ // 40
1083
+ [19, 148, 118, 6, 149, 119],
1084
+ [18, 75, 47, 31, 76, 48],
1085
+ [34, 54, 24, 34, 55, 25],
1086
+ [20, 45, 15, 61, 46, 16],
1087
+ ]