@forgesworn/qr 0.0.0-seed.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.
@@ -0,0 +1,76 @@
1
+ /** Error correction level: about 7, 15, 25 or 30 per cent of codewords recoverable. */
2
+ export type ErrorCorrection = 'L' | 'M' | 'Q' | 'H';
3
+ /** Data mode of the single segment a code carries. */
4
+ export type Mode = 'numeric' | 'alphanumeric' | 'byte';
5
+ export interface EncodeOptions {
6
+ /** Error correction level. Default `'M'`. */
7
+ ecc?: ErrorCorrection;
8
+ /** Exact version, 1-40. Default: the smallest version, from `minVersion`, that fits. */
9
+ version?: number;
10
+ /** Smallest version to consider when choosing one. Default 1. */
11
+ minVersion?: number;
12
+ /** Mask pattern, 0-7. Default: the pattern with the lowest penalty score. */
13
+ mask?: number;
14
+ /**
15
+ * Data mode. Default for text: numeric if every character is a digit,
16
+ * alphanumeric if every character is in that set, otherwise byte.
17
+ * Bytes (`Uint8Array`) are always byte mode.
18
+ */
19
+ mode?: Mode;
20
+ /**
21
+ * In byte mode, prefix an ECI designator for UTF-8 (assignment 26) so a
22
+ * reader need not guess the encoding. Default false: most readers assume
23
+ * UTF-8 for byte mode, and the designator costs 12 bits.
24
+ */
25
+ eci?: boolean;
26
+ }
27
+ export interface QrCode {
28
+ readonly version: number;
29
+ readonly ecc: ErrorCorrection;
30
+ readonly mask: number;
31
+ readonly mode: Mode;
32
+ /** Modules per side, 17 + 4 * version. Excludes the quiet zone. */
33
+ readonly size: number;
34
+ /** Row-major modules, `size * size` entries, 1 for dark. */
35
+ readonly modules: Uint8Array;
36
+ }
37
+ /** Thrown when the data does not fit the requested or any version. */
38
+ export declare class QrCapacityError extends RangeError {
39
+ readonly name = "QrCapacityError";
40
+ }
41
+ /** True if the module at column `x`, row `y` is dark. */
42
+ export declare function isDark(qr: QrCode, x: number, y: number): boolean;
43
+ export declare const ALPHANUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
44
+ export declare const EC_ORDER: readonly ErrorCorrection[];
45
+ export declare const COUNT_BITS: Record<Mode, readonly [number, number, number]>;
46
+ /** Encodes text (as UTF-8 in byte mode) or bytes as one QR code. */
47
+ export declare function encode(data: string | Uint8Array, options?: EncodeOptions): QrCode;
48
+ export declare function countBand(version: number): 0 | 1 | 2;
49
+ /** UTF-8 bytes of `text`, with lone surrogates as U+FFFD (as TextEncoder does). */
50
+ export declare function utf8(text: string): Uint8Array;
51
+ export declare function blocksFor(version: number, ecc: ErrorCorrection): readonly [number, number, number, number];
52
+ export declare const EXP: Uint8Array<ArrayBuffer>;
53
+ export declare const LOG: Uint8Array<ArrayBuffer>;
54
+ export declare function gfMul(a: number, b: number): number;
55
+ /** Format information coordinates (x, y) for bits 0 to 14, beside the top-left finder. */
56
+ export declare const FORMAT_COORDS: readonly (readonly [number, number])[];
57
+ export declare class Grid {
58
+ readonly size: number;
59
+ private readonly base;
60
+ private readonly reserved;
61
+ private readonly version;
62
+ constructor(version: number);
63
+ /** True for finder, separator, timing, alignment, format and version modules. */
64
+ isReserved(x: number, y: number): boolean;
65
+ private fn;
66
+ private finder;
67
+ /** Zigzag placement of the final codeword stream; remainder bits are light. */
68
+ placeData(stream: Uint8Array): void;
69
+ /** A copy with the data masked and the format and version information drawn. */
70
+ render(ecc: ErrorCorrection, mask: number): Uint8Array;
71
+ }
72
+ export declare function maskBit(mask: number, x: number, y: number): boolean;
73
+ export declare function formatBits(ecc: ErrorCorrection, mask: number): number;
74
+ export declare function versionBits(version: number): number;
75
+ /** Mask penalty per ISO/IEC 18004 section 7.8.3, scoring the finished symbol. */
76
+ export declare function penalty(modules: Uint8Array, size: number): number;
package/dist/encode.js ADDED
@@ -0,0 +1,495 @@
1
+ import { ALIGNMENT_CENTRES, EC_BLOCKS } from './tables.js';
2
+ /** Thrown when the data does not fit the requested or any version. */
3
+ export class QrCapacityError extends RangeError {
4
+ name = 'QrCapacityError';
5
+ }
6
+ /** True if the module at column `x`, row `y` is dark. */
7
+ export function isDark(qr, x, y) {
8
+ return qr.modules[y * qr.size + x] === 1;
9
+ }
10
+ export const ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
11
+ export const EC_ORDER = ['L', 'M', 'Q', 'H'];
12
+ const EC_FORMAT_BITS = { L: 1, M: 0, Q: 3, H: 2 };
13
+ const MODE_BITS = { numeric: 1, alphanumeric: 2, byte: 4 };
14
+ export const COUNT_BITS = {
15
+ numeric: [10, 12, 14],
16
+ alphanumeric: [9, 11, 13],
17
+ byte: [8, 16, 16],
18
+ };
19
+ const ECI_MODE = 7;
20
+ const ECI_UTF8 = 26;
21
+ /** Encodes text (as UTF-8 in byte mode) or bytes as one QR code. */
22
+ export function encode(data, options = {}) {
23
+ const ecc = options.ecc ?? 'M';
24
+ if (!EC_ORDER.includes(ecc))
25
+ throw new RangeError(`Unknown error correction level: ${String(ecc)}`);
26
+ const mask = options.mask;
27
+ if (mask !== undefined && !(Number.isInteger(mask) && mask >= 0 && mask <= 7)) {
28
+ throw new RangeError('Mask must be an integer from 0 to 7');
29
+ }
30
+ const fixedVersion = options.version;
31
+ if (fixedVersion !== undefined && !validVersion(fixedVersion)) {
32
+ throw new RangeError('Version must be an integer from 1 to 40');
33
+ }
34
+ const minVersion = options.minVersion ?? 1;
35
+ if (!validVersion(minVersion))
36
+ throw new RangeError('minVersion must be an integer from 1 to 40');
37
+ const mode = chooseMode(data, options.mode);
38
+ const payload = segmentBits(data, mode);
39
+ const eci = mode === 'byte' && options.eci === true;
40
+ const version = fixedVersion ?? chooseVersion(mode, payload, eci, ecc, minVersion);
41
+ const capacity = dataCodewords(version, ecc) * 8;
42
+ const countBits = COUNT_BITS[mode][countBand(version)];
43
+ if (payload.count >= 1 << countBits || headerLength(countBits, eci) + payload.bits.length > capacity) {
44
+ throw new QrCapacityError(`Data does not fit version ${version} at level ${ecc}`);
45
+ }
46
+ const bits = [];
47
+ if (eci) {
48
+ push(bits, ECI_MODE, 4);
49
+ push(bits, ECI_UTF8, 8);
50
+ }
51
+ push(bits, MODE_BITS[mode], 4);
52
+ push(bits, payload.count, countBits);
53
+ for (const bit of payload.bits)
54
+ bits.push(bit);
55
+ for (let i = 0; i < 4 && bits.length < capacity; i++)
56
+ bits.push(0);
57
+ while (bits.length % 8 !== 0)
58
+ bits.push(0);
59
+ const codewords = new Uint8Array(capacity / 8);
60
+ for (let i = 0; i < bits.length; i += 8) {
61
+ let byte = 0;
62
+ for (let j = 0; j < 8; j++)
63
+ byte = (byte << 1) | bits[i + j];
64
+ codewords[i / 8] = byte;
65
+ }
66
+ for (let i = bits.length / 8, pad = 0xec; i < codewords.length; i++, pad ^= 0xec ^ 0x11)
67
+ codewords[i] = pad;
68
+ const stream = interleave(codewords, version, ecc);
69
+ const grid = new Grid(version);
70
+ grid.placeData(stream);
71
+ let chosen = mask ?? 0;
72
+ let modules;
73
+ if (mask === undefined) {
74
+ let best = Infinity;
75
+ let bestModules = new Uint8Array(0);
76
+ for (let m = 0; m < 8; m++) {
77
+ const candidate = grid.render(ecc, m);
78
+ const score = penalty(candidate, grid.size);
79
+ if (score < best) {
80
+ best = score;
81
+ chosen = m;
82
+ bestModules = candidate;
83
+ }
84
+ }
85
+ modules = bestModules;
86
+ }
87
+ else {
88
+ modules = grid.render(ecc, chosen);
89
+ }
90
+ return { version, ecc, mask: chosen, mode, size: grid.size, modules };
91
+ }
92
+ function validVersion(v) {
93
+ return Number.isInteger(v) && v >= 1 && v <= 40;
94
+ }
95
+ export function countBand(version) {
96
+ return version <= 9 ? 0 : version <= 26 ? 1 : 2;
97
+ }
98
+ function headerLength(countBits, eci) {
99
+ return (eci ? 12 : 0) + 4 + countBits;
100
+ }
101
+ function chooseMode(data, requested) {
102
+ if (typeof data !== 'string') {
103
+ if (requested !== undefined && requested !== 'byte')
104
+ throw new RangeError('Bytes can only be encoded in byte mode');
105
+ return 'byte';
106
+ }
107
+ if (requested === 'numeric') {
108
+ if (!/^[0-9]*$/.test(data))
109
+ throw new RangeError('Numeric mode takes digits only');
110
+ return requested;
111
+ }
112
+ if (requested === 'alphanumeric') {
113
+ for (const c of data)
114
+ if (!ALPHANUMERIC.includes(c))
115
+ throw new RangeError(`Not in the alphanumeric set: ${c}`);
116
+ return requested;
117
+ }
118
+ if (requested === 'byte')
119
+ return requested;
120
+ if (requested !== undefined)
121
+ throw new RangeError(`Unknown mode: ${String(requested)}`);
122
+ let numeric = true;
123
+ for (const c of data) {
124
+ if (c >= '0' && c <= '9')
125
+ continue;
126
+ if (!ALPHANUMERIC.includes(c))
127
+ return 'byte';
128
+ numeric = false;
129
+ }
130
+ return data.length > 0 && numeric ? 'numeric' : data.length > 0 ? 'alphanumeric' : 'byte';
131
+ }
132
+ function segmentBits(data, mode) {
133
+ const bits = [];
134
+ if (mode === 'byte') {
135
+ const bytes = typeof data === 'string' ? utf8(data) : data;
136
+ for (const b of bytes)
137
+ push(bits, b, 8);
138
+ return { count: bytes.length, bits };
139
+ }
140
+ const text = data;
141
+ if (mode === 'numeric') {
142
+ for (let i = 0; i < text.length; i += 3) {
143
+ const group = text.slice(i, i + 3);
144
+ push(bits, Number(group), group.length * 3 + 1);
145
+ }
146
+ }
147
+ else {
148
+ for (let i = 0; i < text.length; i += 2) {
149
+ const a = ALPHANUMERIC.indexOf(text[i]);
150
+ if (i + 1 < text.length)
151
+ push(bits, a * 45 + ALPHANUMERIC.indexOf(text[i + 1]), 11);
152
+ else
153
+ push(bits, a, 6);
154
+ }
155
+ }
156
+ return { count: text.length, bits };
157
+ }
158
+ /** UTF-8 bytes of `text`, with lone surrogates as U+FFFD (as TextEncoder does). */
159
+ export function utf8(text) {
160
+ const out = [];
161
+ for (let i = 0; i < text.length; i++) {
162
+ let c = text.charCodeAt(i);
163
+ if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) {
164
+ const d = text.charCodeAt(i + 1);
165
+ if (d >= 0xdc00 && d <= 0xdfff) {
166
+ c = 0x10000 + ((c - 0xd800) << 10) + (d - 0xdc00);
167
+ i++;
168
+ }
169
+ }
170
+ if (c >= 0xd800 && c <= 0xdfff)
171
+ c = 0xfffd;
172
+ if (c < 0x80)
173
+ out.push(c);
174
+ else if (c < 0x800)
175
+ out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
176
+ else if (c < 0x10000)
177
+ out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
178
+ else
179
+ out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
180
+ }
181
+ return Uint8Array.from(out);
182
+ }
183
+ function chooseVersion(mode, payload, eci, ecc, minVersion) {
184
+ for (let v = minVersion; v <= 40; v++) {
185
+ const countBits = COUNT_BITS[mode][countBand(v)];
186
+ if (payload.count >= 1 << countBits)
187
+ continue;
188
+ if (headerLength(countBits, eci) + payload.bits.length <= dataCodewords(v, ecc) * 8)
189
+ return v;
190
+ }
191
+ throw new QrCapacityError(`Data does not fit any version at level ${ecc}`);
192
+ }
193
+ export function blocksFor(version, ecc) {
194
+ return EC_BLOCKS[version - 1][EC_ORDER.indexOf(ecc)];
195
+ }
196
+ function dataCodewords(version, ecc) {
197
+ const [, g1, d1, g2] = blocksFor(version, ecc);
198
+ return g1 * d1 + g2 * (d1 + 1);
199
+ }
200
+ function push(bits, value, length) {
201
+ for (let i = length - 1; i >= 0; i--)
202
+ bits.push((value >>> i) & 1);
203
+ }
204
+ // Reed-Solomon over GF(256), primitive polynomial x^8 + x^4 + x^3 + x^2 + 1.
205
+ export const EXP = new Uint8Array(512);
206
+ export const LOG = new Uint8Array(256);
207
+ for (let i = 0, x = 1; i < 255; i++) {
208
+ EXP[i] = x;
209
+ LOG[x] = i;
210
+ x <<= 1;
211
+ if (x & 0x100)
212
+ x ^= 0x11d;
213
+ }
214
+ for (let i = 255; i < 512; i++)
215
+ EXP[i] = EXP[i - 255];
216
+ export function gfMul(a, b) {
217
+ return a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]];
218
+ }
219
+ const generators = new Map();
220
+ /** Monic generator polynomial of the given degree, highest coefficient first. */
221
+ function generator(degree) {
222
+ const cached = generators.get(degree);
223
+ if (cached)
224
+ return cached;
225
+ let poly = new Uint8Array([1]);
226
+ for (let i = 0; i < degree; i++) {
227
+ const next = new Uint8Array(poly.length + 1);
228
+ for (let j = 0; j < poly.length; j++) {
229
+ next[j] ^= poly[j];
230
+ next[j + 1] ^= gfMul(poly[j], EXP[i]);
231
+ }
232
+ poly = next;
233
+ }
234
+ generators.set(degree, poly);
235
+ return poly;
236
+ }
237
+ function remainder(data, degree) {
238
+ const gen = generator(degree);
239
+ const out = new Uint8Array(degree);
240
+ for (const byte of data) {
241
+ const factor = byte ^ out[0];
242
+ out.copyWithin(0, 1);
243
+ out[degree - 1] = 0;
244
+ for (let i = 0; i < degree; i++)
245
+ out[i] ^= gfMul(gen[i + 1], factor);
246
+ }
247
+ return out;
248
+ }
249
+ function interleave(codewords, version, ecc) {
250
+ const [ecPerBlock, g1, d1, g2] = blocksFor(version, ecc);
251
+ const data = [];
252
+ const checks = [];
253
+ for (let b = 0, offset = 0; b < g1 + g2; b++) {
254
+ const length = b < g1 ? d1 : d1 + 1;
255
+ const block = codewords.subarray(offset, offset + length);
256
+ offset += length;
257
+ data.push(block);
258
+ checks.push(remainder(block, ecPerBlock));
259
+ }
260
+ const out = new Uint8Array(codewords.length + (g1 + g2) * ecPerBlock);
261
+ let k = 0;
262
+ for (let i = 0; i <= d1; i++)
263
+ for (const block of data)
264
+ if (i < block.length)
265
+ out[k++] = block[i];
266
+ for (let i = 0; i < ecPerBlock; i++)
267
+ for (const block of checks)
268
+ out[k++] = block[i];
269
+ return out;
270
+ }
271
+ /** Format information coordinates (x, y) for bits 0 to 14, beside the top-left finder. */
272
+ export const FORMAT_COORDS = [
273
+ [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8],
274
+ [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8],
275
+ ];
276
+ export class Grid {
277
+ size;
278
+ base;
279
+ reserved;
280
+ version;
281
+ constructor(version) {
282
+ this.version = version;
283
+ this.size = 17 + 4 * version;
284
+ this.base = new Uint8Array(this.size * this.size);
285
+ this.reserved = new Uint8Array(this.size * this.size);
286
+ const s = this.size;
287
+ this.finder(0, 0);
288
+ this.finder(s - 7, 0);
289
+ this.finder(0, s - 7);
290
+ this.fn(8, s - 8, 1);
291
+ const centres = ALIGNMENT_CENTRES[version - 1];
292
+ for (const cy of centres) {
293
+ for (const cx of centres) {
294
+ if (this.reserved[cy * s + cx])
295
+ continue;
296
+ for (let dy = -2; dy <= 2; dy++) {
297
+ for (let dx = -2; dx <= 2; dx++) {
298
+ this.fn(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1 ? 1 : 0);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ for (let i = 8; i < s - 8; i++) {
304
+ if (!this.reserved[6 * s + i])
305
+ this.fn(i, 6, i % 2 === 0 ? 1 : 0);
306
+ if (!this.reserved[i * s + 6])
307
+ this.fn(6, i, i % 2 === 0 ? 1 : 0);
308
+ }
309
+ for (let i = 0; i < 15; i++) {
310
+ const [x, y] = FORMAT_COORDS[i];
311
+ this.fn(x, y, 0);
312
+ if (i < 8)
313
+ this.fn(s - 1 - i, 8, 0);
314
+ else
315
+ this.fn(8, s - 7 + (i - 8), 0);
316
+ }
317
+ if (version >= 7) {
318
+ for (let i = 0; i < 6; i++) {
319
+ for (let j = 0; j < 3; j++) {
320
+ this.fn(i, s - 11 + j, 0);
321
+ this.fn(s - 11 + j, i, 0);
322
+ }
323
+ }
324
+ }
325
+ }
326
+ /** True for finder, separator, timing, alignment, format and version modules. */
327
+ isReserved(x, y) {
328
+ return this.reserved[y * this.size + x] === 1;
329
+ }
330
+ fn(x, y, dark) {
331
+ this.base[y * this.size + x] = dark;
332
+ this.reserved[y * this.size + x] = 1;
333
+ }
334
+ finder(left, top) {
335
+ for (let dy = -1; dy <= 7; dy++) {
336
+ for (let dx = -1; dx <= 7; dx++) {
337
+ const x = left + dx;
338
+ const y = top + dy;
339
+ if (x < 0 || y < 0 || x >= this.size || y >= this.size)
340
+ continue;
341
+ const ring = Math.max(Math.abs(dx - 3), Math.abs(dy - 3));
342
+ this.fn(x, y, ring !== 2 && ring !== 4 ? 1 : 0);
343
+ }
344
+ }
345
+ }
346
+ /** Zigzag placement of the final codeword stream; remainder bits are light. */
347
+ placeData(stream) {
348
+ const s = this.size;
349
+ const total = stream.length * 8;
350
+ let bit = 0;
351
+ let upward = true;
352
+ for (let right = s - 1; right >= 1; right -= 2) {
353
+ if (right === 6)
354
+ right = 5;
355
+ for (let step = 0; step < s; step++) {
356
+ const y = upward ? s - 1 - step : step;
357
+ for (let i = 0; i < 2; i++) {
358
+ const x = right - i;
359
+ if (this.reserved[y * s + x])
360
+ continue;
361
+ if (bit < total)
362
+ this.base[y * s + x] = (stream[bit >>> 3] >>> (7 - (bit & 7))) & 1;
363
+ bit++;
364
+ }
365
+ }
366
+ upward = !upward;
367
+ }
368
+ }
369
+ /** A copy with the data masked and the format and version information drawn. */
370
+ render(ecc, mask) {
371
+ const s = this.size;
372
+ const out = this.base.slice();
373
+ for (let y = 0; y < s; y++) {
374
+ for (let x = 0; x < s; x++) {
375
+ if (!this.reserved[y * s + x] && maskBit(mask, x, y))
376
+ out[y * s + x] ^= 1;
377
+ }
378
+ }
379
+ const format = formatBits(ecc, mask);
380
+ for (let i = 0; i < 15; i++) {
381
+ const bit = (format >>> i) & 1;
382
+ const [x, y] = FORMAT_COORDS[i];
383
+ out[y * s + x] = bit;
384
+ if (i < 8)
385
+ out[8 * s + (s - 1 - i)] = bit;
386
+ else
387
+ out[(s - 7 + (i - 8)) * s + 8] = bit;
388
+ }
389
+ if (this.version >= 7) {
390
+ const info = versionBits(this.version);
391
+ for (let k = 0; k < 18; k++) {
392
+ const bit = (info >>> k) & 1;
393
+ const i = Math.floor(k / 3);
394
+ const j = k % 3;
395
+ out[(s - 11 + j) * s + i] = bit;
396
+ out[i * s + (s - 11 + j)] = bit;
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+ }
402
+ export function maskBit(mask, x, y) {
403
+ switch (mask) {
404
+ case 0: return (x + y) % 2 === 0;
405
+ case 1: return y % 2 === 0;
406
+ case 2: return x % 3 === 0;
407
+ case 3: return (x + y) % 3 === 0;
408
+ case 4: return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0;
409
+ case 5: return ((x * y) % 2) + ((x * y) % 3) === 0;
410
+ case 6: return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
411
+ default: return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
412
+ }
413
+ }
414
+ export function formatBits(ecc, mask) {
415
+ const data = (EC_FORMAT_BITS[ecc] << 3) | mask;
416
+ let rem = data;
417
+ for (let i = 0; i < 10; i++)
418
+ rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
419
+ return ((data << 10) | rem) ^ 0x5412;
420
+ }
421
+ export function versionBits(version) {
422
+ let rem = version;
423
+ for (let i = 0; i < 12; i++)
424
+ rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
425
+ return (version << 12) | rem;
426
+ }
427
+ /** Mask penalty per ISO/IEC 18004 section 7.8.3, scoring the finished symbol. */
428
+ export function penalty(modules, size) {
429
+ let score = 0;
430
+ let dark = 0;
431
+ for (let y = 0; y < size; y++) {
432
+ let rowRun = 0;
433
+ let colRun = 0;
434
+ for (let x = 0; x < size; x++) {
435
+ const here = modules[y * size + x];
436
+ dark += here;
437
+ if (x > 0 && here === modules[y * size + x - 1])
438
+ rowRun++;
439
+ else {
440
+ if (rowRun >= 5)
441
+ score += rowRun - 2;
442
+ rowRun = 1;
443
+ }
444
+ const down = modules[x * size + y];
445
+ if (x > 0 && down === modules[(x - 1) * size + y])
446
+ colRun++;
447
+ else {
448
+ if (colRun >= 5)
449
+ score += colRun - 2;
450
+ colRun = 1;
451
+ }
452
+ if (x < size - 1 && y < size - 1) {
453
+ if (here === modules[y * size + x + 1] &&
454
+ here === modules[(y + 1) * size + x] &&
455
+ here === modules[(y + 1) * size + x + 1])
456
+ score += 3;
457
+ }
458
+ if (x + 6 < size && finderLike(modules, size, x, y, 1, 0))
459
+ score += 40;
460
+ if (x + 6 < size && finderLike(modules, size, y, x, 0, 1))
461
+ score += 40;
462
+ }
463
+ if (rowRun >= 5)
464
+ score += rowRun - 2;
465
+ if (colRun >= 5)
466
+ score += colRun - 2;
467
+ }
468
+ const total = size * size;
469
+ score += Math.floor((Math.abs(dark * 2 - total) * 10) / total) * 10;
470
+ return score;
471
+ }
472
+ const FINDER_RUN = [1, 0, 1, 1, 1, 0, 1];
473
+ /** 1:1:3:1:1 starting at (x, y) along (dx, dy), with four light modules on one side. */
474
+ function finderLike(m, size, x, y, dx, dy) {
475
+ for (let i = 0; i < 7; i++)
476
+ if (m[(y + i * dy) * size + x + i * dx] !== FINDER_RUN[i])
477
+ return false;
478
+ return lightRun(m, size, x, y, dx, dy, -4) || lightRun(m, size, x, y, dx, dy, 7);
479
+ }
480
+ /**
481
+ * Four modules from offset `from` along the line are light. A run that would
482
+ * leave the symbol does not count: the quiet zone is not scored. ZXing scores
483
+ * it the same way, which keeps mask choice identical to that oracle.
484
+ */
485
+ function lightRun(m, size, x, y, dx, dy, from) {
486
+ for (let i = from; i < from + 4; i++) {
487
+ const px = x + i * dx;
488
+ const py = y + i * dy;
489
+ if (px < 0 || py < 0 || px >= size || py >= size)
490
+ return false;
491
+ if (m[py * size + px] === 1)
492
+ return false;
493
+ }
494
+ return true;
495
+ }
@@ -0,0 +1,12 @@
1
+ export { encode, isDark, QrCapacityError } from './encode.js';
2
+ export type { EncodeOptions, ErrorCorrection, Mode, QrCode } from './encode.js';
3
+ export { toSvg } from './svg.js';
4
+ export type { SvgOptions } from './svg.js';
5
+ export { drawCanvas } from './canvas.js';
6
+ export type { Canvas2dLike, CanvasLike, CanvasOptions } from './canvas.js';
7
+ export { toTerminal } from './terminal.js';
8
+ export type { TerminalOptions } from './terminal.js';
9
+ export { decode, decodeModules } from './decode.js';
10
+ export type { DecodeOptions, DecodeResult, ImageLike } from './decode.js';
11
+ export type { Payload } from './symbol.js';
12
+ export type { Point } from './locate.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { encode, isDark, QrCapacityError } from './encode.js';
2
+ export { toSvg } from './svg.js';
3
+ export { drawCanvas } from './canvas.js';
4
+ export { toTerminal } from './terminal.js';
5
+ export { decode, decodeModules } from './decode.js';
@@ -0,0 +1,37 @@
1
+ import { type BitImage } from './binarise.js';
2
+ export interface Point {
3
+ readonly x: number;
4
+ readonly y: number;
5
+ }
6
+ interface Finder extends Point {
7
+ readonly moduleSize: number;
8
+ readonly count: number;
9
+ }
10
+ /** Where a symbol sits in the picture, and how to sample it. */
11
+ export interface Location {
12
+ readonly topLeft: Point;
13
+ readonly topRight: Point;
14
+ readonly bottomLeft: Point;
15
+ readonly alignment: Point | undefined;
16
+ readonly size: number;
17
+ /** Picture coordinates of the centre of module (x, y). */
18
+ readonly project: (x: number, y: number) => Point;
19
+ }
20
+ /**
21
+ * Finds finder pattern candidates, most often confirmed first. `tolerance`
22
+ * is how far a run may stray from its share, in modules; a blurred light
23
+ * ring needs more than the default half module.
24
+ */
25
+ export declare function findFinders(image: BitImage, tolerance?: number): Finder[];
26
+ /**
27
+ * Every plausible top-left, top-right, bottom-left triple, best first: the
28
+ * three should form a right-angled isosceles triangle of like-sized finders.
29
+ */
30
+ export declare function finderTriples(finders: readonly Finder[]): [Finder, Finder, Finder][];
31
+ /**
32
+ * Turns three finders into sampling maps: one through each likely alignment
33
+ * pattern (when the version has one), then one through the parallelogram's
34
+ * fourth corner. `sizeHint` overrides the estimated module count.
35
+ */
36
+ export declare function locate(image: BitImage, triple: readonly [Finder, Finder, Finder], sizeHint?: number): Location[];
37
+ export {};