@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.
package/dist/locate.js ADDED
@@ -0,0 +1,489 @@
1
+ const dark = (image, x, y) => image.data[y * image.width + x] === 1;
2
+ function distance(a, b) {
3
+ return Math.hypot(a.x - b.x, a.y - b.y);
4
+ }
5
+ /** Runs in 1:1:3:1:1 proportion, each within half a module (the centre within 1.5). */
6
+ function finderRatio(counts, tolerance = 0.5) {
7
+ let total = 0;
8
+ for (const c of counts) {
9
+ if (c === 0)
10
+ return false;
11
+ total += c;
12
+ }
13
+ if (total < 7)
14
+ return false;
15
+ const module = total / 7;
16
+ const slack = module * tolerance;
17
+ return (Math.abs(module - counts[0]) < slack &&
18
+ Math.abs(module - counts[1]) < slack &&
19
+ Math.abs(3 * module - counts[2]) < 3 * slack &&
20
+ Math.abs(module - counts[3]) < slack &&
21
+ Math.abs(module - counts[4]) < slack);
22
+ }
23
+ const centreFromEnd = (counts, end) => end - counts[4] - counts[3] - counts[2] / 2;
24
+ /**
25
+ * Counts the five runs through (x, y) along direction (dx, dy), starting in
26
+ * the dark centre, and returns the centre along that line, or NaN.
27
+ */
28
+ function crossCheck(image, x, y, dx, dy, maxCount, originalTotal, tolerance = 0.5) {
29
+ const { width, height } = image;
30
+ const inside = (px, py) => px >= 0 && py >= 0 && px < width && py < height;
31
+ const counts = [0, 0, 0, 0, 0];
32
+ let px = x;
33
+ let py = y;
34
+ while (inside(px, py) && dark(image, px, py)) {
35
+ counts[2]++;
36
+ px -= dx;
37
+ py -= dy;
38
+ }
39
+ while (inside(px, py) && !dark(image, px, py) && counts[1] <= maxCount) {
40
+ counts[1]++;
41
+ px -= dx;
42
+ py -= dy;
43
+ }
44
+ if (!inside(px, py) || counts[1] > maxCount)
45
+ return NaN;
46
+ while (inside(px, py) && dark(image, px, py) && counts[0] <= maxCount) {
47
+ counts[0]++;
48
+ px -= dx;
49
+ py -= dy;
50
+ }
51
+ if (counts[0] > maxCount)
52
+ return NaN;
53
+ px = x + dx;
54
+ py = y + dy;
55
+ while (inside(px, py) && dark(image, px, py)) {
56
+ counts[2]++;
57
+ px += dx;
58
+ py += dy;
59
+ }
60
+ while (inside(px, py) && !dark(image, px, py) && counts[3] < maxCount) {
61
+ counts[3]++;
62
+ px += dx;
63
+ py += dy;
64
+ }
65
+ if (!inside(px, py) || counts[3] >= maxCount)
66
+ return NaN;
67
+ while (inside(px, py) && dark(image, px, py) && counts[4] < maxCount) {
68
+ counts[4]++;
69
+ px += dx;
70
+ py += dy;
71
+ }
72
+ if (counts[4] >= maxCount)
73
+ return NaN;
74
+ const total = counts.reduce((a, b) => a + b, 0);
75
+ // A diagonal crosses the pattern at a longer stretch, so only straight lines compare totals.
76
+ if (!(dx && dy) && 5 * Math.abs(total - originalTotal) >= 2 * originalTotal)
77
+ return NaN;
78
+ if (!finderRatio(counts, tolerance))
79
+ return NaN;
80
+ // Position along the line, measured in steps from the start point.
81
+ const end = dx !== 0 ? px - x : py - y;
82
+ return centreFromEnd(counts, end * (dx + dy > 0 ? 1 : -1));
83
+ }
84
+ /**
85
+ * Finds finder pattern candidates, most often confirmed first. `tolerance`
86
+ * is how far a run may stray from its share, in modules; a blurred light
87
+ * ring needs more than the default half module.
88
+ */
89
+ export function findFinders(image, tolerance = 0.5) {
90
+ const { width, height } = image;
91
+ const found = [];
92
+ const consider = (counts, row, end) => {
93
+ const total = counts.reduce((a, b) => a + b, 0);
94
+ let cx = centreFromEnd(counts, end);
95
+ const maxCount = counts[2];
96
+ const dy = crossCheck(image, Math.floor(cx), row, 0, 1, maxCount, total, tolerance);
97
+ if (Number.isNaN(dy))
98
+ return false;
99
+ const cy = row + dy;
100
+ const dx = crossCheck(image, Math.floor(cx), Math.floor(cy), 1, 0, maxCount, total, tolerance);
101
+ if (Number.isNaN(dx))
102
+ return false;
103
+ cx = Math.floor(cx) + dx;
104
+ const diagonal = crossCheck(image, Math.floor(cx), Math.floor(cy), 1, 1, maxCount * 2, total, tolerance + 0.25);
105
+ if (Number.isNaN(diagonal))
106
+ return false;
107
+ const moduleSize = total / 7;
108
+ for (const f of found) {
109
+ if (Math.abs(cy - f.y) <= moduleSize &&
110
+ Math.abs(cx - f.x) <= moduleSize &&
111
+ (Math.abs(moduleSize - f.moduleSize) <= 1 || Math.abs(moduleSize - f.moduleSize) <= f.moduleSize * 0.25)) {
112
+ const n = f.count + 1;
113
+ f.x = (f.x * f.count + cx) / n;
114
+ f.y = (f.y * f.count + cy) / n;
115
+ f.moduleSize = (f.moduleSize * f.count + moduleSize) / n;
116
+ f.count = n;
117
+ return true;
118
+ }
119
+ }
120
+ found.push({ x: cx, y: cy, moduleSize, count: 1 });
121
+ return true;
122
+ };
123
+ const skip = Math.max(1, Math.min(3, Math.floor(height / 200)));
124
+ for (let y = skip - 1; y < height; y += skip) {
125
+ const counts = [0, 0, 0, 0, 0];
126
+ let state = 0;
127
+ const row = y * width;
128
+ for (let x = 0; x < width; x++) {
129
+ if (image.data[row + x] === 1) {
130
+ if (state & 1)
131
+ state++;
132
+ counts[state]++;
133
+ }
134
+ else if (state & 1) {
135
+ counts[state]++;
136
+ }
137
+ else if (state === 4) {
138
+ if (finderRatio(counts, tolerance) && consider(counts, y, x)) {
139
+ counts.fill(0);
140
+ state = 0;
141
+ }
142
+ else {
143
+ counts.copyWithin(0, 2);
144
+ counts[3] = 1;
145
+ counts[4] = 0;
146
+ state = 3;
147
+ }
148
+ }
149
+ else {
150
+ state++;
151
+ counts[state]++;
152
+ }
153
+ }
154
+ if (state === 4 && finderRatio(counts, tolerance))
155
+ consider(counts, y, width);
156
+ }
157
+ return found.sort((a, b) => b.count - a.count);
158
+ }
159
+ /**
160
+ * Every plausible top-left, top-right, bottom-left triple, best first: the
161
+ * three should form a right-angled isosceles triangle of like-sized finders.
162
+ */
163
+ export function finderTriples(finders) {
164
+ const pool = finders.slice(0, 12);
165
+ const triples = [];
166
+ for (let i = 0; i < pool.length; i++) {
167
+ for (let j = i + 1; j < pool.length; j++) {
168
+ for (let k = j + 1; k < pool.length; k++) {
169
+ const a = pool[i];
170
+ const b = pool[j];
171
+ const c = pool[k];
172
+ const sizes = [a.moduleSize, b.moduleSize, c.moduleSize];
173
+ if (Math.max(...sizes) > 2 * Math.min(...sizes))
174
+ continue;
175
+ const ab = distance(a, b) ** 2;
176
+ const bc = distance(b, c) ** 2;
177
+ const ca = distance(c, a) ** 2;
178
+ // The corner opposite the longest side is the top-left.
179
+ let topLeft = c;
180
+ let p = a;
181
+ let q = b;
182
+ let hyp = ab;
183
+ let s1 = bc;
184
+ let s2 = ca;
185
+ if (bc >= ab && bc >= ca)
186
+ [topLeft, p, q, hyp, s1, s2] = [a, b, c, bc, ab, ca];
187
+ else if (ca >= ab)
188
+ [topLeft, p, q, hyp, s1, s2] = [b, c, a, ca, ab, bc];
189
+ const side = Math.sqrt((s1 + s2) / 2);
190
+ if (side < 10 * Math.min(...sizes))
191
+ continue;
192
+ const score = (Math.abs(hyp - s1 - s2) + Math.abs(s1 - s2)) / hyp;
193
+ if (score > 0.5)
194
+ continue;
195
+ // In picture coordinates (y down) top-right x bottom-left is positive.
196
+ const cross = (p.x - topLeft.x) * (q.y - topLeft.y) - (p.y - topLeft.y) * (q.x - topLeft.x);
197
+ if (cross < 0)
198
+ [p, q] = [q, p];
199
+ triples.push({ score: score - 0.01 * Math.min(a.count + b.count + c.count, 15), triple: [topLeft, p, q] });
200
+ }
201
+ }
202
+ }
203
+ return triples.sort((x, y) => x.score - y.score).map((t) => t.triple);
204
+ }
205
+ /** Module size along a line from one finder centre to another, by its black-white-black runs. */
206
+ function runModuleSize(image, from, to) {
207
+ const one = blackWhiteBlack(image, from, to);
208
+ const other = blackWhiteBlack(image, to, from);
209
+ if (Number.isNaN(one))
210
+ return other / 7;
211
+ if (Number.isNaN(other))
212
+ return one / 7;
213
+ return (one + other) / 14;
214
+ }
215
+ /** Length of the dark-light-dark run from a finder centre outwards, through and beyond the finder edge. */
216
+ function blackWhiteBlack(image, from, to) {
217
+ const run = (sx, sy, tx, ty) => {
218
+ const steep = Math.abs(ty - sy) > Math.abs(tx - sx);
219
+ if (steep)
220
+ [sx, sy, tx, ty] = [sy, sx, ty, tx];
221
+ const dx = Math.abs(tx - sx);
222
+ const dy = Math.abs(ty - sy);
223
+ let error = -dx / 2;
224
+ const xstep = sx < tx ? 1 : -1;
225
+ const ystep = sy < ty ? 1 : -1;
226
+ let state = 0;
227
+ let y = sy;
228
+ for (let x = sx; x !== tx + xstep; x += xstep) {
229
+ const rx = steep ? y : x;
230
+ const ry = steep ? x : y;
231
+ if (rx < 0 || ry < 0 || rx >= image.width || ry >= image.height)
232
+ break;
233
+ if ((state === 1) === dark(image, rx, ry)) {
234
+ if (state === 2)
235
+ return Math.hypot(x - sx, y - sy);
236
+ state++;
237
+ }
238
+ error += dy;
239
+ if (error > 0) {
240
+ if (y === ty)
241
+ break;
242
+ y += ystep;
243
+ error -= dx;
244
+ }
245
+ }
246
+ return state === 2 ? Math.hypot(tx + xstep - sx, ty - sy) : NaN;
247
+ };
248
+ const sx = Math.floor(from.x);
249
+ const sy = Math.floor(from.y);
250
+ const forward = run(sx, sy, Math.floor(to.x), Math.floor(to.y));
251
+ // The same distance the other way, clipped to the picture.
252
+ let ox = sx - (Math.floor(to.x) - sx);
253
+ let oy = sy - (Math.floor(to.y) - sy);
254
+ let scale = 1;
255
+ if (ox < 0)
256
+ scale = Math.min(scale, sx / (sx - ox));
257
+ else if (ox >= image.width)
258
+ scale = Math.min(scale, (image.width - 1 - sx) / (ox - sx));
259
+ if (oy < 0)
260
+ scale = Math.min(scale, sy / (sy - oy));
261
+ else if (oy >= image.height)
262
+ scale = Math.min(scale, (image.height - 1 - sy) / (oy - sy));
263
+ ox = Math.floor(sx + (ox - sx) * scale);
264
+ oy = Math.floor(sy + (oy - sy) * scale);
265
+ const backward = run(sx, sy, ox, oy);
266
+ if (Number.isNaN(forward) || Number.isNaN(backward))
267
+ return NaN;
268
+ return forward + backward - 1;
269
+ }
270
+ /**
271
+ * Searches near `estimate` for alignment pattern centres. Candidates seen on
272
+ * two or more rows come first, then nearest the estimate.
273
+ */
274
+ function findAlignments(image, estimate, moduleSize, reach) {
275
+ const radius = Math.ceil(reach * moduleSize);
276
+ const left = Math.max(0, Math.floor(estimate.x - radius));
277
+ const right = Math.min(image.width - 1, Math.ceil(estimate.x + radius));
278
+ const top = Math.max(0, Math.floor(estimate.y - radius));
279
+ const bottom = Math.min(image.height - 1, Math.ceil(estimate.y + radius));
280
+ if (right - left < moduleSize * 3 || bottom - top < moduleSize * 3)
281
+ return [];
282
+ const fits = (n) => Math.abs(n - moduleSize) < moduleSize * 0.6;
283
+ const vertical = (x, y) => {
284
+ // Light, dark, light runs through (x, y) going up and down from the dark centre.
285
+ let up = y;
286
+ while (up >= top && dark(image, x, up))
287
+ up--;
288
+ let down = y;
289
+ while (down <= bottom && dark(image, x, down))
290
+ down++;
291
+ const middle = down - up - 1;
292
+ if (!fits(middle))
293
+ return NaN;
294
+ let above = 0;
295
+ for (let yy = up; yy >= top && !dark(image, x, yy); yy--)
296
+ above++;
297
+ let below = 0;
298
+ for (let yy = down; yy <= bottom && !dark(image, x, yy); yy++)
299
+ below++;
300
+ if (!fits(above) || !fits(below))
301
+ return NaN;
302
+ return (up + 1 + down) / 2;
303
+ };
304
+ const found = [];
305
+ for (let y = top; y <= bottom; y++) {
306
+ const runs = [];
307
+ let x = left;
308
+ while (x <= right && dark(image, x, y))
309
+ x++; // skip a dark run cut by the window
310
+ let start = x;
311
+ let current = false;
312
+ for (; x <= right + 1; x++) {
313
+ const d = x <= right && dark(image, x, y);
314
+ if (x <= right && d === current)
315
+ continue;
316
+ runs.push(x - start);
317
+ start = x;
318
+ current = d;
319
+ // runs alternate light, dark, light ... ; check the last three when they end in light
320
+ if (runs.length >= 3 && runs.length % 2 === 1) {
321
+ const [l1, d1, l2] = runs.slice(-3);
322
+ if (fits(l1) && fits(d1) && fits(l2)) {
323
+ const cx = x - l2 - d1 / 2;
324
+ const cy = vertical(Math.floor(cx), y);
325
+ if (!Number.isNaN(cy)) {
326
+ const same = found.find((f) => Math.abs(f.x - cx) <= moduleSize && Math.abs(f.y - cy) <= moduleSize);
327
+ if (same) {
328
+ same.x = (same.x * same.count + cx) / (same.count + 1);
329
+ same.y = (same.y * same.count + cy) / (same.count + 1);
330
+ same.count++;
331
+ }
332
+ else {
333
+ found.push({ x: cx, y: cy, count: 1 });
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ // A candidate seen on one row only counts as three modules further away.
341
+ const score = (f) => distance(f, estimate) / moduleSize + (f.count >= 2 ? 0 : 3);
342
+ return found.sort((p, q) => score(p) - score(q)).map(({ x, y }) => ({ x, y }));
343
+ }
344
+ /** A projective map from the unit square's corners to four picture points. */
345
+ function squareToQuad(p0, p1, p2, p3) {
346
+ const dx3 = p0.x - p1.x + p2.x - p3.x;
347
+ const dy3 = p0.y - p1.y + p2.y - p3.y;
348
+ if (dx3 === 0 && dy3 === 0) {
349
+ return [p1.x - p0.x, p2.x - p1.x, p0.x, p1.y - p0.y, p2.y - p1.y, p0.y, 0, 0, 1];
350
+ }
351
+ const dx1 = p1.x - p2.x;
352
+ const dx2 = p3.x - p2.x;
353
+ const dy1 = p1.y - p2.y;
354
+ const dy2 = p3.y - p2.y;
355
+ const den = dx1 * dy2 - dx2 * dy1;
356
+ const a13 = (dx3 * dy2 - dx2 * dy3) / den;
357
+ const a23 = (dx1 * dy3 - dx3 * dy1) / den;
358
+ return [
359
+ p1.x - p0.x + a13 * p1.x, p3.x - p0.x + a23 * p3.x, p0.x,
360
+ p1.y - p0.y + a13 * p1.y, p3.y - p0.y + a23 * p3.y, p0.y,
361
+ a13, a23, 1,
362
+ ];
363
+ }
364
+ function adjoint(m) {
365
+ const [a, b, c, d, e, f, g, h, i] = m;
366
+ return [e * i - f * h, c * h - b * i, b * f - c * e, f * g - d * i, a * i - c * g, c * d - a * f, d * h - e * g, b * g - a * h, a * e - b * d];
367
+ }
368
+ function multiply(m, n) {
369
+ const out = [];
370
+ for (let r = 0; r < 3; r++) {
371
+ for (let c = 0; c < 3; c++)
372
+ out.push(m[r * 3] * n[c] + m[r * 3 + 1] * n[3 + c] + m[r * 3 + 2] * n[6 + c]);
373
+ }
374
+ return out;
375
+ }
376
+ /** The map taking four module-space points to four picture points. */
377
+ function quadToQuad(from, to) {
378
+ const toPicture = squareToQuad(to[0], to[1], to[2], to[3]);
379
+ const fromModules = adjoint(squareToQuad(from[0], from[1], from[2], from[3]));
380
+ // Column-vector convention: picture = toPicture · fromModules⁻¹ · module.
381
+ const m = multiply(toPicture, fromModules);
382
+ return (x, y) => {
383
+ const w = m[6] * x + m[7] * y + m[8];
384
+ return { x: (m[0] * x + m[1] * y + m[2]) / w, y: (m[3] * x + m[4] * y + m[5]) / w };
385
+ };
386
+ }
387
+ /**
388
+ * Out of 25, how many modules around `centre` match an alignment pattern
389
+ * (dark centre, light ring, dark ring), stepping by `u` across and `v` down.
390
+ * Perspective changes the local module size, so three scales are tried.
391
+ */
392
+ function alignmentScore(image, centre, u, v) {
393
+ let best = 0;
394
+ for (const scale of [0.85, 1, 1.15]) {
395
+ let score = 0;
396
+ for (let j = -2; j <= 2; j++) {
397
+ for (let i = -2; i <= 2; i++) {
398
+ const x = Math.floor(centre.x + (i * u.x + j * v.x) * scale);
399
+ const y = Math.floor(centre.y + (i * u.y + j * v.y) * scale);
400
+ const want = Math.max(Math.abs(i), Math.abs(j)) !== 1;
401
+ const got = x >= 0 && y >= 0 && x < image.width && y < image.height && dark(image, x, y);
402
+ if (got === want)
403
+ score++;
404
+ }
405
+ }
406
+ best = Math.max(best, score);
407
+ }
408
+ return best;
409
+ }
410
+ /**
411
+ * Turns three finders into sampling maps: one through each likely alignment
412
+ * pattern (when the version has one), then one through the parallelogram's
413
+ * fourth corner. `sizeHint` overrides the estimated module count.
414
+ */
415
+ export function locate(image, triple, sizeHint) {
416
+ const [topLeft, topRight, bottomLeft] = triple;
417
+ const moduleSize = (runModuleSize(image, topLeft, topRight) + runModuleSize(image, topLeft, bottomLeft)) / 2;
418
+ if (!(moduleSize >= 1))
419
+ return [];
420
+ let size = sizeHint;
421
+ if (size === undefined) {
422
+ size = Math.round((distance(topLeft, topRight) + distance(topLeft, bottomLeft)) / 2 / moduleSize) + 7;
423
+ switch (size & 3) {
424
+ case 0:
425
+ size++;
426
+ break;
427
+ case 2:
428
+ size--;
429
+ break;
430
+ case 3:
431
+ size -= 2;
432
+ break;
433
+ }
434
+ }
435
+ if (size < 21 || size > 177)
436
+ return [];
437
+ const corner = { x: topRight.x - topLeft.x + bottomLeft.x, y: topRight.y - topLeft.y + bottomLeft.y };
438
+ let alignments = [];
439
+ if (size > 21) {
440
+ // Two guesses at the fourth corner: the parallelogram's, and one that
441
+ // follows perspective, where modules grow towards the nearer edge.
442
+ const tl = topLeft.moduleSize;
443
+ const down = bottomLeft.moduleSize / tl;
444
+ const right = topRight.moduleSize / tl;
445
+ const scaled = {
446
+ x: (bottomLeft.x + (topRight.x - topLeft.x) * down + topRight.x + (bottomLeft.x - topLeft.x) * right) / 2,
447
+ y: (bottomLeft.y + (topRight.y - topLeft.y) * down + topRight.y + (bottomLeft.y - topLeft.y) * right) / 2,
448
+ };
449
+ const correction = 1 - 3 / (size - 7);
450
+ const estimates = [corner, scaled].map((c) => ({
451
+ x: topLeft.x + correction * (c.x - topLeft.x),
452
+ y: topLeft.y + correction * (c.y - topLeft.y),
453
+ }));
454
+ // A row or column crosses a turned module on the slant: at 45 degrees a
455
+ // module's run is the diagonal, root 2 times its side.
456
+ const across = distance(topLeft, topRight);
457
+ const slant = Math.max(Math.abs(topRight.x - topLeft.x), Math.abs(topRight.y - topLeft.y)) / across;
458
+ const runSize = moduleSize / slant;
459
+ // Rank candidates by how well the 5x5 pattern around them matches, read
460
+ // along the code's own axes, then by nearness to a guess.
461
+ const steps = size - 7;
462
+ const u = { x: (topRight.x - topLeft.x) / steps, y: (topRight.y - topLeft.y) / steps };
463
+ const v = { x: (bottomLeft.x - topLeft.x) / steps, y: (bottomLeft.y - topLeft.y) / steps };
464
+ const near = (p) => Math.min(...estimates.map((e) => distance(p, e)));
465
+ const scored = [];
466
+ for (const reach of [4, 8, 16]) {
467
+ for (const estimate of estimates) {
468
+ for (const p of findAlignments(image, estimate, runSize, reach)) {
469
+ if (scored.some((a) => distance(a.p, p) <= moduleSize / 2))
470
+ continue;
471
+ scored.push({ p, score: alignmentScore(image, p, u, v), away: near(p) });
472
+ }
473
+ }
474
+ if (scored.some((c) => c.score >= 23))
475
+ break;
476
+ }
477
+ alignments = scored
478
+ .filter((c) => c.score >= 20)
479
+ .sort((a, b) => b.score - a.score || a.away - b.away)
480
+ .slice(0, 4)
481
+ .map((c) => c.p);
482
+ }
483
+ const far = size - 3.5;
484
+ const map = (alignment) => {
485
+ const project = quadToQuad([{ x: 3.5, y: 3.5 }, { x: far, y: 3.5 }, alignment ? { x: size - 6.5, y: size - 6.5 } : { x: far, y: far }, { x: 3.5, y: far }], [topLeft, topRight, alignment ?? corner, bottomLeft]);
486
+ return { topLeft, topRight, bottomLeft, alignment, size, project: (x, y) => project(x + 0.5, y + 0.5) };
487
+ };
488
+ return [...alignments.map(map), map(undefined)];
489
+ }
package/dist/rs.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Corrects `block` (data codewords then `ecCount` check codewords) in place.
3
+ * Returns the number of codewords corrected, or -1 when the block has more
4
+ * errors than the code can correct.
5
+ */
6
+ export declare function correct(block: Uint8Array, ecCount: number): number;
package/dist/rs.js ADDED
@@ -0,0 +1,99 @@
1
+ import { EXP, gfMul, LOG } from './encode.js';
2
+ // Reed-Solomon error correction for QR codes: GF(256) with 0x11D, generator
3
+ // roots α^0 .. α^(ecCount - 1). Polynomials here are lowest power first,
4
+ // except `block`, which is a received codeword block, highest power first.
5
+ function gfDiv(a, b) {
6
+ if (b === 0)
7
+ throw new RangeError('Division by zero in GF(256)');
8
+ return a === 0 ? 0 : EXP[(LOG[a] + 255 - LOG[b]) % 255];
9
+ }
10
+ function evaluate(poly, x) {
11
+ let y = 0;
12
+ for (let i = poly.length - 1; i >= 0; i--)
13
+ y = gfMul(y, x) ^ poly[i];
14
+ return y;
15
+ }
16
+ /**
17
+ * Corrects `block` (data codewords then `ecCount` check codewords) in place.
18
+ * Returns the number of codewords corrected, or -1 when the block has more
19
+ * errors than the code can correct.
20
+ */
21
+ export function correct(block, ecCount) {
22
+ const n = block.length;
23
+ const syndromes = [];
24
+ let clean = true;
25
+ for (let j = 0; j < ecCount; j++) {
26
+ let s = 0;
27
+ const x = EXP[j];
28
+ for (let i = 0; i < n; i++)
29
+ s = gfMul(s, x) ^ block[i];
30
+ syndromes.push(s);
31
+ if (s !== 0)
32
+ clean = false;
33
+ }
34
+ if (clean)
35
+ return 0;
36
+ // Berlekamp-Massey: the error locator Λ.
37
+ let locator = [1];
38
+ let previous = [1];
39
+ let errors = 0;
40
+ let shift = 1;
41
+ let lastDiscrepancy = 1;
42
+ for (let k = 0; k < ecCount; k++) {
43
+ let d = syndromes[k];
44
+ for (let i = 1; i <= errors; i++)
45
+ d ^= gfMul(locator[i] ?? 0, syndromes[k - i]);
46
+ if (d === 0) {
47
+ shift++;
48
+ continue;
49
+ }
50
+ const factor = gfDiv(d, lastDiscrepancy);
51
+ const next = locator.slice();
52
+ for (let i = 0; i < previous.length; i++) {
53
+ const at = i + shift;
54
+ while (next.length <= at)
55
+ next.push(0);
56
+ next[at] ^= gfMul(factor, previous[i]);
57
+ }
58
+ if (2 * errors <= k) {
59
+ previous = locator;
60
+ errors = k + 1 - errors;
61
+ lastDiscrepancy = d;
62
+ shift = 1;
63
+ }
64
+ else {
65
+ shift++;
66
+ }
67
+ locator = next;
68
+ }
69
+ while (locator.length > 1 && locator[locator.length - 1] === 0)
70
+ locator.pop();
71
+ if (errors * 2 > ecCount || locator.length - 1 !== errors)
72
+ return -1;
73
+ // Chien search: a root at α^-p marks an error at power p, index n - 1 - p.
74
+ const powers = [];
75
+ for (let p = 0; p < n; p++) {
76
+ if (evaluate(locator, EXP[(255 - p) % 255]) === 0)
77
+ powers.push(p);
78
+ }
79
+ if (powers.length !== errors)
80
+ return -1;
81
+ // Forney: Ω = S·Λ mod x^ecCount; e = X·Ω(X⁻¹) / Λ'(X⁻¹) for first root α^0.
82
+ const omega = new Array(ecCount).fill(0);
83
+ for (let i = 0; i < ecCount; i++) {
84
+ for (let j = 0; j < locator.length && i + j < ecCount; j++)
85
+ omega[i + j] ^= gfMul(syndromes[i], locator[j]);
86
+ }
87
+ const derivative = [];
88
+ for (let i = 1; i < locator.length; i++)
89
+ derivative.push(i % 2 === 1 ? locator[i] : 0);
90
+ for (const p of powers) {
91
+ const x = EXP[p % 255];
92
+ const xInverse = EXP[(255 - p) % 255];
93
+ const denominator = evaluate(derivative, xInverse);
94
+ if (denominator === 0)
95
+ return -1;
96
+ block[n - 1 - p] ^= gfMul(x, gfDiv(evaluate(omega, xInverse), denominator));
97
+ }
98
+ return errors;
99
+ }
package/dist/svg.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type QrCode } from './encode.js';
2
+ export interface SvgOptions {
3
+ /** Quiet zone in modules on every side. Default 4, the standard's minimum. */
4
+ margin?: number;
5
+ /** Colour of dark modules. Default `'#000'`. */
6
+ dark?: string;
7
+ /** Background colour, or `null` for none. Default `'#fff'`. */
8
+ light?: string | null;
9
+ /** Accessible name, written as the SVG `<title>`. */
10
+ title?: string;
11
+ }
12
+ /**
13
+ * Renders a code as a standalone SVG document, one module per user unit and
14
+ * one path for all dark modules. Size it with CSS or `width`/`height`; the
15
+ * viewBox keeps it square.
16
+ */
17
+ export declare function toSvg(qr: QrCode, options?: SvgOptions): string;
package/dist/svg.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Renders a code as a standalone SVG document, one module per user unit and
3
+ * one path for all dark modules. Size it with CSS or `width`/`height`; the
4
+ * viewBox keeps it square.
5
+ */
6
+ export function toSvg(qr, options = {}) {
7
+ const margin = options.margin ?? 4;
8
+ if (!Number.isInteger(margin) || margin < 0)
9
+ throw new RangeError('Margin must be a whole number of modules');
10
+ const dark = escape(options.dark ?? '#000');
11
+ const light = options.light === undefined ? '#fff' : options.light;
12
+ const side = qr.size + margin * 2;
13
+ let path = '';
14
+ for (let y = 0; y < qr.size; y++) {
15
+ let x = 0;
16
+ while (x < qr.size) {
17
+ if (qr.modules[y * qr.size + x] !== 1) {
18
+ x++;
19
+ continue;
20
+ }
21
+ const start = x;
22
+ while (x < qr.size && qr.modules[y * qr.size + x] === 1)
23
+ x++;
24
+ path += `M${start + margin} ${y + margin}h${x - start}v1h${start - x}z`;
25
+ }
26
+ }
27
+ const title = options.title === undefined ? '' : `<title>${escape(options.title)}</title>`;
28
+ const background = light === null ? '' : `<rect width="${side}" height="${side}" fill="${escape(light)}"/>`;
29
+ const role = options.title === undefined ? '' : ' role="img"';
30
+ return (`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${side} ${side}" shape-rendering="crispEdges"${role}>` +
31
+ `${title}${background}<path fill="${dark}" d="${path}"/></svg>`);
32
+ }
33
+ function escape(text) {
34
+ return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
35
+ }
@@ -0,0 +1,25 @@
1
+ import { type ErrorCorrection } from './encode.js';
2
+ /** What a symbol says, once read. */
3
+ export interface Payload {
4
+ /** The decoded text. */
5
+ readonly text: string;
6
+ /** The payload as bytes: byte-mode data as stored, numeric and alphanumeric as ASCII. */
7
+ readonly bytes: Uint8Array;
8
+ readonly version: number;
9
+ readonly ecc: ErrorCorrection;
10
+ readonly mask: number;
11
+ /** ECI assignment numbers in the order they appeared; empty if none. */
12
+ readonly eci: readonly number[];
13
+ /** Codewords Reed-Solomon had to correct. */
14
+ readonly corrected: number;
15
+ }
16
+ /** Reads modules through a callback: true for dark, x across, y down. */
17
+ export type ModuleReader = (x: number, y: number) => boolean;
18
+ /** The version a symbol of `size` modules declares, when its version blocks can be read. */
19
+ export declare function readVersion(read: ModuleReader, size: number): number | undefined;
20
+ /**
21
+ * Decodes a symbol from its modules. `size` must be 17 + 4v; for version 7
22
+ * and up the version blocks decide, and a size that disagrees fails.
23
+ * Returns undefined when the symbol cannot be read.
24
+ */
25
+ export declare function readSymbol(read: ModuleReader, size: number): Payload | undefined;