qr 0.2.4 → 0.4.2

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/esm/decode.js ADDED
@@ -0,0 +1,926 @@
1
+ /*!
2
+ Copyright (c) 2023 Paul Miller (paulmillr.com)
3
+ The library paulmillr-qr is dual-licensed under the Apache 2.0 OR MIT license.
4
+ You can select a license of your choice.
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+ /**
18
+ * Methods for decoding (reading) QR code patterns.
19
+ * @module
20
+ * @example
21
+ ```js
22
+
23
+ ```
24
+ */
25
+ import { Bitmap, utils } from "./index.js";
26
+ const { best, bin, drawTemplate, fillArr, info, interleave, validateVersion, zigzag } = utils;
27
+ // Constants
28
+ const MAX_BITS_ERROR = 3; // Up to 3 bit errors in version/format
29
+ const GRAYSCALE_BLOCK_SIZE = 8;
30
+ const GRAYSCALE_RANGE = 24;
31
+ const PATTERN_VARIANCE = 2;
32
+ const PATTERN_VARIANCE_DIAGONAL = 1.333;
33
+ const PATTERN_MIN_CONFIRMATIONS = 2;
34
+ const DETECT_MIN_ROW_SKIP = 3;
35
+ // TODO: move to index, nearby with bitmap and other graph related stuff?
36
+ const int = (n) => n >>> 0;
37
+ // distance ^ 2
38
+ const distance2 = (p1, p2) => {
39
+ const x = p1.x - p2.x;
40
+ const y = p1.y - p2.y;
41
+ return x * x + y * y;
42
+ };
43
+ const distance = (p1, p2) => Math.sqrt(distance2(p1, p2));
44
+ const sum = (lst) => lst.reduce((acc, i) => acc + i);
45
+ const pointIncr = (p, incr) => {
46
+ p.x += incr.x;
47
+ p.y += incr.y;
48
+ };
49
+ const pointNeg = (p) => ({ x: -p.x, y: -p.y });
50
+ const pointMirror = (p) => ({ x: p.y, y: p.x });
51
+ const pointClone = (p) => ({ x: p.x, y: p.y });
52
+ const pointInt = (p) => ({ x: int(p.x), y: int(p.y) });
53
+ const pointAdd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
54
+ function cap(value, min, max) {
55
+ return Math.max(Math.min(value, max || value), min || value);
56
+ }
57
+ const getBytesPerPixel = (img) => {
58
+ const perPixel = img.data.length / (img.width * img.height);
59
+ if (perPixel === 3 || perPixel === 4)
60
+ return perPixel; // RGB or RGBA
61
+ throw new Error(`Unknown image format, bytes per pixel=${perPixel}`);
62
+ };
63
+ /**
64
+ * Convert to grayscale. The function is the most expensive part of decoding:
65
+ * it takes up to 90% of time. TODO: check gamma correction / sqr.
66
+ */
67
+ function toBitmap(img) {
68
+ const bytesPerPixel = getBytesPerPixel(img);
69
+ const brightness = new Uint8Array(img.height * img.width);
70
+ for (let i = 0, j = 0, d = img.data; i < d.length; i += bytesPerPixel) {
71
+ const r = d[i];
72
+ const g = d[i + 1];
73
+ const b = d[i + 2];
74
+ brightness[j++] = int((r + 2 * g + b) / 4) & 0xff;
75
+ }
76
+ // Convert to bitmap
77
+ const block = GRAYSCALE_BLOCK_SIZE;
78
+ if (img.width < block * 5 || img.height < block * 5)
79
+ throw new Error('image too small');
80
+ const bWidth = Math.ceil(img.width / block);
81
+ const bHeight = Math.ceil(img.height / block);
82
+ const maxY = img.height - block;
83
+ const maxX = img.width - block;
84
+ const blocks = new Uint8Array(bWidth * bHeight);
85
+ for (let y = 0; y < bHeight; y++) {
86
+ const yPos = cap(y * block, 0, maxY);
87
+ for (let x = 0; x < bWidth; x++) {
88
+ const xPos = cap(x * block, 0, maxX);
89
+ let sum = 0;
90
+ let min = 0xff;
91
+ let max = 0;
92
+ for (let yy = 0, pos = yPos * img.width + xPos; yy < block; yy = yy + 1, pos = pos + img.width) {
93
+ for (let xx = 0; xx < block; xx++) {
94
+ const pixel = brightness[pos + xx];
95
+ sum += pixel;
96
+ min = Math.min(min, pixel);
97
+ max = Math.max(max, pixel);
98
+ }
99
+ }
100
+ // Average brightness of block
101
+ let average = Math.floor(sum / block ** 2);
102
+ if (max - min <= GRAYSCALE_RANGE) {
103
+ average = min / 2;
104
+ if (y > 0 && x > 0) {
105
+ const idx = (x, y) => y * bWidth + x;
106
+ const prev = (blocks[idx(x, y - 1)] + 2 * blocks[idx(x - 1, y)] + blocks[idx(x - 1, y - 1)]) / 4;
107
+ if (min < prev)
108
+ average = prev;
109
+ }
110
+ }
111
+ blocks[bWidth * y + x] = int(average);
112
+ }
113
+ }
114
+ const matrix = new Bitmap({ width: img.width, height: img.height });
115
+ for (let y = 0; y < bHeight; y++) {
116
+ const yPos = cap(y * block, 0, maxY);
117
+ const top = cap(y, 2, bHeight - 3);
118
+ for (let x = 0; x < bWidth; x++) {
119
+ const xPos = cap(x * block, 0, maxX);
120
+ const left = cap(x, 2, bWidth - 3);
121
+ // 5x5 blocks average
122
+ let sum = 0;
123
+ for (let yy = -2; yy <= 2; yy++) {
124
+ const y2 = bWidth * (top + yy) + left;
125
+ for (let xx = -2; xx <= 2; xx++)
126
+ sum += blocks[y2 + xx];
127
+ }
128
+ const average = sum / 25;
129
+ for (let y = 0, pos = yPos * img.width + xPos; y < block; y += 1, pos += img.width) {
130
+ for (let x = 0; x < block; x++) {
131
+ if (brightness[pos + x] <= average)
132
+ matrix.data[yPos + y][xPos + x] = true;
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return matrix;
138
+ }
139
+ function patternEquals(p, p2) {
140
+ if (Math.abs(p2.y - p.y) <= p2.moduleSize && Math.abs(p2.x - p.x) <= p2.moduleSize) {
141
+ const diff = Math.abs(p2.moduleSize - p.moduleSize);
142
+ return diff <= 1.0 || diff <= p.moduleSize;
143
+ }
144
+ return false;
145
+ }
146
+ function patternMerge(a, b) {
147
+ const count = a.count + b.count;
148
+ return {
149
+ x: (a.count * a.x + b.count * b.x) / count,
150
+ y: (a.count * a.y + b.count * b.y) / count,
151
+ moduleSize: (a.count * a.moduleSize + b.count * b.moduleSize) / count,
152
+ count,
153
+ };
154
+ }
155
+ const patternsConfirmed = (lst) => lst.filter((i) => i.count >= PATTERN_MIN_CONFIRMATIONS);
156
+ /**
157
+ * Since pattern means runs of identical color (dark or white), we cannot
158
+ * have pattern like [true, true], because it will be hard to separate same color runs.
159
+ * @param p boolean pattern
160
+ * @param size size of run relative to others
161
+ * @returns
162
+ */
163
+ function pattern(p, size) {
164
+ const _size = size || fillArr(p.length, 1);
165
+ if (p.length !== _size.length)
166
+ throw new Error('invalid pattern');
167
+ if (!(p.length & 1))
168
+ throw new Error('invalid pattern, length should be odd');
169
+ const res = {
170
+ center: Math.ceil(p.length / 2) - 1,
171
+ length: p.length,
172
+ pattern: p,
173
+ size: _size,
174
+ runs: () => fillArr(p.length, 0),
175
+ totalSize: sum(_size),
176
+ total: (runs) => runs.reduce((acc, i) => acc + i),
177
+ shift: (runs, n) => {
178
+ for (let i = 0; i < runs.length - n; i++)
179
+ runs[i] = runs[i + 2];
180
+ for (let i = runs.length - n; i < runs.length; i++)
181
+ runs[i] = 0;
182
+ },
183
+ checkSize(runs, moduleSize, v = PATTERN_VARIANCE) {
184
+ const variance = moduleSize / v;
185
+ for (let i = 0; i < runs.length; i++) {
186
+ if (Math.abs(_size[i] * moduleSize - runs[i]) >= _size[i] * variance)
187
+ return false;
188
+ }
189
+ return true;
190
+ },
191
+ add(out, x, y, total) {
192
+ const moduleSize = total / FINDER.totalSize;
193
+ const cur = { x, y, moduleSize, count: 1 };
194
+ for (let idx = 0; idx < out.length; idx++) {
195
+ const f = out[idx];
196
+ if (!patternEquals(f, cur))
197
+ continue;
198
+ return (out[idx] = patternMerge(f, cur));
199
+ }
200
+ out.push(cur);
201
+ return;
202
+ },
203
+ toCenter(runs, end) {
204
+ for (let i = p.length - 1; i > res.center; i--)
205
+ end -= runs[i];
206
+ end -= runs[res.center] / 2;
207
+ return end;
208
+ },
209
+ check(b, runs, center, incr, maxCount) {
210
+ let j = 0;
211
+ let i = pointClone(center);
212
+ const neg = pointNeg(incr);
213
+ const check = (p, step) => {
214
+ for (; b.isInside(i) && !!b.point(i) === res.pattern[p]; pointIncr(i, step)) {
215
+ runs[p]++;
216
+ j++;
217
+ }
218
+ if (runs[p] === 0)
219
+ return true;
220
+ const center = p === res.center;
221
+ if (maxCount && !center && runs[p] > res.size[p] * maxCount)
222
+ return true;
223
+ return false;
224
+ };
225
+ for (let p = res.center; p >= 0; p--)
226
+ if (check(p, neg))
227
+ return false;
228
+ i = pointClone(center);
229
+ pointIncr(i, incr);
230
+ j = 1;
231
+ for (let p = res.center; p < res.length; p++)
232
+ if (check(p, incr))
233
+ return false;
234
+ return j;
235
+ },
236
+ scanLine(b, y, xStart, xEnd, fn) {
237
+ const runs = res.runs();
238
+ let pos = 0;
239
+ let x = xStart;
240
+ // If we start in middle of an image, skip first pattern run,
241
+ // since we don't know run length of pixels from left side
242
+ if (xStart)
243
+ while (x < xEnd && !!b.data[y][x] === res.pattern[0])
244
+ x++;
245
+ for (; x < xEnd; x++) {
246
+ // Same run, continue counting
247
+ if (!!b.data[y][x] === res.pattern[pos]) {
248
+ runs[pos]++;
249
+ // If not last element - continue counting
250
+ if (x !== b.width - 1)
251
+ continue;
252
+ // Last element finishes run, set x outside of run
253
+ x++;
254
+ }
255
+ // Not last run: count new one
256
+ if (pos !== res.length - 1) {
257
+ runs[++pos]++;
258
+ continue;
259
+ }
260
+ const found = fn(runs, x);
261
+ if (found) {
262
+ // We found pattern, reset runs counting
263
+ pos = 0;
264
+ runs.fill(0);
265
+ }
266
+ else if (found === false) {
267
+ // Stop scanning
268
+ break;
269
+ }
270
+ else {
271
+ // Not found: shift runs by two (so pattern will continue)
272
+ res.shift(runs, 2);
273
+ pos = res.length - 2;
274
+ runs[pos]++;
275
+ }
276
+ }
277
+ },
278
+ };
279
+ return res;
280
+ }
281
+ // light/dark/light/dark/light in 1:1:3:1:1 ratio
282
+ const FINDER = pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
283
+ // dark/light/dark in 1:1:1 ratio
284
+ const ALIGNMENT = pattern([false, true, false]);
285
+ function findFinder(b) {
286
+ let found = [];
287
+ function checkRuns(runs, v = 2) {
288
+ const total = sum(runs);
289
+ if (total < FINDER.totalSize)
290
+ return false;
291
+ const moduleSize = total / FINDER.totalSize;
292
+ return FINDER.checkSize(runs, moduleSize, v);
293
+ }
294
+ // Non-diagonal line (horizontal or vertical)
295
+ function checkLine(center, maxCount, total, incr) {
296
+ const runs = FINDER.runs();
297
+ let i = FINDER.check(b, runs, center, incr, maxCount);
298
+ if (i === false)
299
+ return false;
300
+ const runsTotal = sum(runs);
301
+ if (5 * Math.abs(runsTotal - total) >= 2 * total)
302
+ return false;
303
+ if (checkRuns(runs))
304
+ return FINDER.toCenter(runs, i);
305
+ return false;
306
+ }
307
+ function check(runs, i, j) {
308
+ if (!checkRuns(runs))
309
+ return false;
310
+ const total = sum(runs);
311
+ let x = FINDER.toCenter(runs, j);
312
+ // Vertical
313
+ let y = checkLine({ x: int(x), y: i }, runs[2], total, { y: 1, x: 0 });
314
+ if (y === false)
315
+ return false;
316
+ y += i;
317
+ // Horizontal
318
+ let xx = checkLine({ x: int(x), y: int(y) }, runs[2], total, { y: 0, x: 1 });
319
+ if (xx === false)
320
+ return false;
321
+ x = xx + int(x);
322
+ // Diagonal
323
+ const dRuns = FINDER.runs();
324
+ if (!FINDER.check(b, dRuns, { x: int(x), y: int(y) }, { x: 1, y: 1 }))
325
+ return false;
326
+ if (!checkRuns(dRuns, PATTERN_VARIANCE_DIAGONAL))
327
+ return false;
328
+ FINDER.add(found, x, y, total);
329
+ return true;
330
+ }
331
+ let skipped = false;
332
+ // Start with high skip lines count until we find first pattern
333
+ let ySkip = cap(int((3 * b.height) / (4 * 97)), DETECT_MIN_ROW_SKIP);
334
+ let done = false;
335
+ for (let y = ySkip - 1; y < b.height && !done; y += ySkip) {
336
+ FINDER.scanLine(b, y, 0, b.width, (runs, x) => {
337
+ if (!check(runs, y, x))
338
+ return;
339
+ // Found pattern
340
+ // Reduce row skip, since we found pattern and qr code is nearby
341
+ ySkip = 2;
342
+ if (skipped) {
343
+ // Already skipped, so we have at least 2 patterns, lets check if third is ok
344
+ let count = 0;
345
+ let total = 0;
346
+ for (const p of found) {
347
+ if (p.count < PATTERN_MIN_CONFIRMATIONS)
348
+ continue;
349
+ count++;
350
+ total += p.moduleSize;
351
+ }
352
+ if (count < 3)
353
+ return;
354
+ const average = total / found.length;
355
+ let deviation = 0.0;
356
+ for (const p of found)
357
+ deviation += Math.abs(p.moduleSize - average);
358
+ if (deviation <= 0.05 * total) {
359
+ done = true;
360
+ return false;
361
+ }
362
+ }
363
+ else if (found.length > 1) {
364
+ // We found two top patterns, lets skip to approximate location of third pattern
365
+ const q = patternsConfirmed(found);
366
+ if (q.length < 2)
367
+ return true;
368
+ skipped = true;
369
+ const d = int((Math.abs(q[0].x - q[1].x) - Math.abs(q[0].y - q[1].y)) / 2);
370
+ if (d <= runs[2] + ySkip)
371
+ return true;
372
+ y += d - runs[2] - ySkip;
373
+ return false;
374
+ }
375
+ return;
376
+ });
377
+ }
378
+ const flen = found.length;
379
+ if (flen < 3)
380
+ throw new Error(`Finder: len(found) = ${flen}`);
381
+ found.sort((i, j) => i.moduleSize - j.moduleSize);
382
+ const pBest = best();
383
+ // Qubic complexity, but we stop search when we found 3 patterns, so not a problem
384
+ for (let i = 0; i < flen - 2; i++) {
385
+ const fi = found[i];
386
+ for (let j = i + 1; j < flen - 1; j++) {
387
+ const fj = found[j];
388
+ const square0 = distance2(fi, fj);
389
+ for (let k = j + 1; k < flen; k++) {
390
+ const fk = found[k];
391
+ if (fk.moduleSize > fi.moduleSize * 1.4)
392
+ continue;
393
+ const arr = [square0, distance2(fj, fk), distance2(fi, fk)].sort((a, b) => a - b);
394
+ const a = arr[0];
395
+ const b = arr[1];
396
+ const c = arr[2];
397
+ pBest.add(Math.abs(c - 2 * b) + Math.abs(c - 2 * a), [fi, fj, fk]);
398
+ }
399
+ }
400
+ }
401
+ const p = pBest.get();
402
+ if (!p)
403
+ throw new Error('cannot find finder');
404
+ const p0 = p[0];
405
+ const p1 = p[1];
406
+ const p2 = p[2];
407
+ const d01 = distance(p0, p1);
408
+ const d12 = distance(p1, p2);
409
+ const d02 = distance(p0, p2);
410
+ let tl = p2;
411
+ let bl = p0;
412
+ let tr = p1;
413
+ if (d12 >= d01 && d12 >= d02) {
414
+ tl = p0;
415
+ bl = p1;
416
+ tr = p2;
417
+ }
418
+ else if (d02 >= d12 && d02 >= d01) {
419
+ tl = p1;
420
+ bl = p0;
421
+ tr = p2;
422
+ }
423
+ // If cross product is negative -> flip points
424
+ if ((tr.x - tl.x) * (bl.y - tl.y) - (tr.y - tl.y) * (bl.x - tl.x) < 0.0) {
425
+ let _bl = bl;
426
+ bl = tr;
427
+ tr = _bl;
428
+ }
429
+ return { bl, tl, tr };
430
+ }
431
+ function findAlignment(b, est, allowanceFactor) {
432
+ const { moduleSize } = est;
433
+ const allowance = int(allowanceFactor * moduleSize);
434
+ const leftX = cap(est.x - allowance, 0);
435
+ const rightX = cap(est.x + allowance, undefined, b.width - 1);
436
+ const x = rightX - leftX;
437
+ const topY = cap(est.y - allowance, 0);
438
+ const bottomY = cap(est.y + allowance, undefined, b.height - 1);
439
+ const y = bottomY - topY;
440
+ if (x < moduleSize * 3 || y < moduleSize * 3)
441
+ throw new Error(`x = ${x}, y=${y} moduleSize = ${moduleSize}`);
442
+ const xStart = leftX;
443
+ const yStart = topY;
444
+ const width = rightX - leftX;
445
+ const height = bottomY - topY;
446
+ const found = [];
447
+ const xEnd = xStart + width;
448
+ const middleY = int(yStart + height / 2);
449
+ for (let yGen = 0; yGen < height; yGen++) {
450
+ const diff = int((yGen + 1) / 2);
451
+ const y = middleY + (yGen & 1 ? -diff : diff);
452
+ let res;
453
+ ALIGNMENT.scanLine(b, y, xStart, xEnd, (runs, x) => {
454
+ if (!ALIGNMENT.checkSize(runs, moduleSize))
455
+ return;
456
+ const total = sum(runs);
457
+ const xx = ALIGNMENT.toCenter(runs, x);
458
+ // Vertical
459
+ const rVert = ALIGNMENT.runs();
460
+ let v = ALIGNMENT.check(b, rVert, { x: int(xx), y }, { y: 1, x: 0 }, 2 * runs[1]);
461
+ if (v === false)
462
+ return;
463
+ v += y;
464
+ const vTotal = sum(rVert);
465
+ if (5 * Math.abs(vTotal - total) >= 2 * total)
466
+ return;
467
+ if (!ALIGNMENT.checkSize(rVert, moduleSize))
468
+ return;
469
+ const yy = ALIGNMENT.toCenter(rVert, v);
470
+ res = ALIGNMENT.add(found, xx, yy, total);
471
+ if (res)
472
+ return false;
473
+ return;
474
+ });
475
+ if (res)
476
+ return res;
477
+ }
478
+ if (found.length > 0)
479
+ return found[0];
480
+ throw new Error('Alignment pattern not found');
481
+ }
482
+ function _single(b, from, to) {
483
+ // http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
484
+ let steep = false;
485
+ let d = { x: Math.abs(to.x - from.x), y: Math.abs(to.y - from.y) };
486
+ if (d.y > d.x) {
487
+ steep = true;
488
+ from = pointMirror(from);
489
+ to = pointMirror(to);
490
+ d = pointMirror(d);
491
+ }
492
+ let error = -d.x / 2;
493
+ let step = { x: from.x >= to.x ? -1 : 1, y: from.y >= to.y ? -1 : 1 };
494
+ let runPos = 0;
495
+ let xLimit = to.x + step.x;
496
+ // TODO: re-use pattern scanLine here?
497
+ for (let x = from.x, y = from.y; x !== xLimit; x += step.x) {
498
+ let real = { x, y };
499
+ if (steep)
500
+ real = pointMirror(real);
501
+ // Same as alignment pattern ([true, false, true])
502
+ if ((runPos === 1) === !!b.point(real)) {
503
+ if (runPos === 2)
504
+ return distance({ x, y }, from);
505
+ runPos++;
506
+ }
507
+ error += d.y;
508
+ if (error <= 0)
509
+ continue;
510
+ if (y === to.y)
511
+ break;
512
+ y += step.y;
513
+ error -= d.x;
514
+ }
515
+ if (runPos === 2)
516
+ return distance({ x: to.x + step.x, y: to.y }, from);
517
+ return NaN;
518
+ }
519
+ function BWBRunLength(b, from, to) {
520
+ let result = _single(b, from, to);
521
+ let scaleY = 1.0;
522
+ const { x: fx, y: fy } = from;
523
+ let otherToX = fx - (to.x - fx);
524
+ const bw = b.width;
525
+ if (otherToX < 0) {
526
+ scaleY = fx / (fx - otherToX);
527
+ otherToX = 0;
528
+ }
529
+ else if (otherToX >= bw) {
530
+ scaleY = (bw - 1 - fx) / (otherToX - fx);
531
+ otherToX = bw - 1;
532
+ }
533
+ let otherToY = int(fy - (to.y - fy) * scaleY);
534
+ let scaleX = 1.0;
535
+ const bh = b.height;
536
+ if (otherToY < 0) {
537
+ scaleX = fy / (fy - otherToY);
538
+ otherToY = 0;
539
+ }
540
+ else if (otherToY >= bh) {
541
+ scaleX = (bh - 1 - fy) / (otherToY - fy);
542
+ otherToY = bh - 1;
543
+ }
544
+ otherToX = int(fx + (otherToX - fx) * scaleX);
545
+ result += _single(b, from, { x: otherToX, y: otherToY });
546
+ return result - 1.0;
547
+ }
548
+ function moduleSizeAvg(b, p1, p2) {
549
+ const est1 = BWBRunLength(b, pointInt(p1), pointInt(p2));
550
+ const est2 = BWBRunLength(b, pointInt(p2), pointInt(p1));
551
+ if (Number.isNaN(est1))
552
+ return est2 / FINDER.totalSize;
553
+ if (Number.isNaN(est2))
554
+ return est1 / FINDER.totalSize;
555
+ return (est1 + est2) / (2 * FINDER.totalSize);
556
+ }
557
+ function detect(b) {
558
+ const { bl, tl, tr } = findFinder(b);
559
+ const moduleSize = (moduleSizeAvg(b, tl, tr) + moduleSizeAvg(b, tl, bl)) / 2;
560
+ if (moduleSize < 1.0)
561
+ throw new Error(`invalid moduleSize = ${moduleSize}`);
562
+ // Estimate size
563
+ const tltr = int(distance(tl, tr) / moduleSize + 0.5);
564
+ const tlbl = int(distance(tl, bl) / moduleSize + 0.5);
565
+ let size = int((tltr + tlbl) / 2 + 7);
566
+ const rem = size % 4;
567
+ if (rem === 0)
568
+ size++; // -> 1
569
+ else if (rem === 2)
570
+ size--; // -> 1
571
+ else if (rem === 3)
572
+ size -= 2;
573
+ const version = info.size.decode(size);
574
+ validateVersion(version);
575
+ let alignmentPattern;
576
+ if (info.alignmentPatterns(version).length > 0) {
577
+ // Bottom right estimate
578
+ const br = { x: tr.x - tl.x + bl.x, y: tr.y - tl.y + bl.y };
579
+ const c = 1.0 - 3.0 / (info.size.encode(version) - 7);
580
+ // Estimated alignment pattern position
581
+ const est = {
582
+ x: int(tl.x + c * (br.x - tl.x)),
583
+ y: int(tl.y + c * (br.y - tl.y)),
584
+ moduleSize,
585
+ count: 1,
586
+ };
587
+ for (let i = 4; i <= 16; i <<= 1) {
588
+ try {
589
+ alignmentPattern = findAlignment(b, est, i);
590
+ break;
591
+ }
592
+ catch (e) { }
593
+ }
594
+ }
595
+ const toTL = { x: 3.5, y: 3.5 };
596
+ const toTR = { x: size - 3.5, y: 3.5 };
597
+ const toBL = { x: 3.5, y: size - 3.5 };
598
+ let br;
599
+ let toBR;
600
+ if (alignmentPattern) {
601
+ br = alignmentPattern;
602
+ toBR = { x: size - 6.5, y: size - 6.5 };
603
+ }
604
+ else {
605
+ br = { x: tr.x - tl.x + bl.x, y: tr.y - tl.y + bl.y };
606
+ toBR = { x: size - 3.5, y: size - 3.5 };
607
+ }
608
+ const from = [tl, tr, br, bl];
609
+ const bits = transform(b, size, from, [toTL, toTR, toBR, toBL]);
610
+ return { bits: bits, points: from };
611
+ }
612
+ // Perspective transform by 4 points
613
+ function squareToQuadrilateral(p) {
614
+ const d3 = { x: p[0].x - p[1].x + p[2].x - p[3].x, y: p[0].y - p[1].y + p[2].y - p[3].y };
615
+ if (d3.x === 0.0 && d3.y === 0.0) {
616
+ return [
617
+ [p[1].x - p[0].x, p[2].x - p[1].x, p[0].x],
618
+ [p[1].y - p[0].y, p[2].y - p[1].y, p[0].y],
619
+ [0.0, 0.0, 1.0],
620
+ ];
621
+ }
622
+ else {
623
+ const d1 = { x: p[1].x - p[2].x, y: p[1].y - p[2].y };
624
+ const d2 = { x: p[3].x - p[2].x, y: p[3].y - p[2].y };
625
+ const den = d1.x * d2.y - d2.x * d1.y;
626
+ const p13 = (d3.x * d2.y - d2.x * d3.y) / den;
627
+ const p23 = (d1.x * d3.y - d3.x * d1.y) / den;
628
+ return [
629
+ [p[1].x - p[0].x + p13 * p[1].x, p[3].x - p[0].x + p23 * p[3].x, p[0].x],
630
+ [p[1].y - p[0].y + p13 * p[1].y, p[3].y - p[0].y + p23 * p[3].y, p[0].y],
631
+ [p13, p23, 1.0],
632
+ ];
633
+ }
634
+ }
635
+ // Transform quadrilateral to square by 4 points
636
+ function transform(b, size, from, to) {
637
+ // TODO: check
638
+ // https://math.stackexchange.com/questions/13404/mapping-irregular-quadrilateral-to-a-rectangle
639
+ const p = squareToQuadrilateral(to);
640
+ const qToS = [
641
+ [
642
+ p[1][1] * p[2][2] - p[2][1] * p[1][2],
643
+ p[2][1] * p[0][2] - p[0][1] * p[2][2],
644
+ p[0][1] * p[1][2] - p[1][1] * p[0][2],
645
+ ],
646
+ [
647
+ p[2][0] * p[1][2] - p[1][0] * p[2][2],
648
+ p[0][0] * p[2][2] - p[2][0] * p[0][2],
649
+ p[1][0] * p[0][2] - p[0][0] * p[1][2],
650
+ ],
651
+ [
652
+ p[1][0] * p[2][1] - p[2][0] * p[1][1],
653
+ p[2][0] * p[0][1] - p[0][0] * p[2][1],
654
+ p[0][0] * p[1][1] - p[1][0] * p[0][1],
655
+ ],
656
+ ];
657
+ const sToQ = squareToQuadrilateral(from);
658
+ const transform = sToQ.map((i) => i.map((_, qx) => i.reduce((acc, v, j) => acc + v * qToS[j][qx], 0)));
659
+ const res = new Bitmap(size);
660
+ const points = fillArr(2 * size, 0);
661
+ const pointsLength = points.length;
662
+ for (let y = 0; y < size; y++) {
663
+ const p = transform;
664
+ for (let i = 0; i < pointsLength - 1; i += 2) {
665
+ const x = i / 2 + 0.5;
666
+ const y2 = y + 0.5;
667
+ const den = p[2][0] * x + p[2][1] * y2 + p[2][2];
668
+ points[i] = int((p[0][0] * x + p[0][1] * y2 + p[0][2]) / den);
669
+ points[i + 1] = int((p[1][0] * x + p[1][1] * y2 + p[1][2]) / den);
670
+ }
671
+ for (let i = 0; i < pointsLength; i += 2) {
672
+ const px = cap(points[i], 0, b.width - 1);
673
+ const py = cap(points[i + 1], 0, b.height - 1);
674
+ if (b.data[py][px])
675
+ res.data[y][i / 2] = true;
676
+ }
677
+ }
678
+ return res;
679
+ }
680
+ // Same as in drawTemplate, but reading
681
+ // TODO: merge in CoderType?
682
+ function readInfoBits(b) {
683
+ const readBit = (x, y, out) => (out << 1) | (b.data[y][x] ? 1 : 0);
684
+ const size = b.height;
685
+ // Version information
686
+ let version1 = 0;
687
+ for (let y = 5; y >= 0; y--)
688
+ for (let x = size - 9; x >= size - 11; x--)
689
+ version1 = readBit(x, y, version1);
690
+ let version2 = 0;
691
+ for (let x = 5; x >= 0; x--)
692
+ for (let y = size - 9; y >= size - 11; y--)
693
+ version2 = readBit(x, y, version2);
694
+ // Format information
695
+ let format1 = 0;
696
+ for (let x = 0; x < 6; x++)
697
+ format1 = readBit(x, 8, format1);
698
+ format1 = readBit(7, 8, format1);
699
+ format1 = readBit(8, 8, format1);
700
+ format1 = readBit(8, 7, format1);
701
+ for (let y = 5; y >= 0; y--)
702
+ format1 = readBit(8, y, format1);
703
+ let format2 = 0;
704
+ for (let y = size - 1; y >= size - 7; y--)
705
+ format2 = readBit(8, y, format2);
706
+ for (let x = size - 8; x < size; x++)
707
+ format2 = readBit(x, 8, format2);
708
+ return { version1, version2, format1, format2 };
709
+ }
710
+ function parseInfo(b) {
711
+ // Population count over xor -> hamming distance
712
+ const popcnt = (a) => {
713
+ let cnt = 0;
714
+ while (a) {
715
+ if (a & 1)
716
+ cnt++;
717
+ a >>= 1;
718
+ }
719
+ return cnt;
720
+ };
721
+ const size = b.height;
722
+ const { version1, version2, format1, format2 } = readInfoBits(b);
723
+ // Guess format
724
+ let format;
725
+ const bestFormat = best();
726
+ for (const ecc of ['medium', 'low', 'high', 'quartile']) {
727
+ for (let mask = 0; mask < 8; mask++) {
728
+ const bits = info.formatBits(ecc, mask);
729
+ const cur = { ecc, mask: mask };
730
+ if (bits === format1 || bits === format2) {
731
+ format = cur;
732
+ break;
733
+ }
734
+ bestFormat.add(popcnt(format1 ^ bits), cur);
735
+ if (format1 !== format2)
736
+ bestFormat.add(popcnt(format2 ^ bits), cur);
737
+ }
738
+ }
739
+ if (format === undefined && bestFormat.score() <= MAX_BITS_ERROR)
740
+ format = bestFormat.get();
741
+ if (format === undefined)
742
+ throw new Error('invalid format pattern');
743
+ let version = info.size.decode(size); // Guess version based on bitmap size
744
+ if (version < 7)
745
+ validateVersion(version);
746
+ else {
747
+ version = undefined;
748
+ // Guess version
749
+ const bestVer = best();
750
+ for (let ver = 7; ver <= 40; ver++) {
751
+ const bits = info.versionBits(ver);
752
+ if (bits === version1 || bits === version2) {
753
+ version = ver;
754
+ break;
755
+ }
756
+ bestVer.add(popcnt(version1 ^ bits), ver);
757
+ if (version1 !== version2)
758
+ bestVer.add(popcnt(version2 ^ bits), ver);
759
+ }
760
+ if (version === undefined && bestVer.score() <= MAX_BITS_ERROR)
761
+ version = bestVer.get();
762
+ if (version === undefined)
763
+ throw new Error('invalid version pattern');
764
+ if (info.size.encode(version) !== size)
765
+ throw new Error('invalid version size');
766
+ }
767
+ return { version, ...format };
768
+ }
769
+ function decodeBitmap(b) {
770
+ const size = b.height;
771
+ if (size < 21 || (size & 0b11) !== 1 || size !== b.width)
772
+ throw new Error(`decode: invalid size=${size}`);
773
+ const { version, mask, ecc } = parseInfo(b);
774
+ const tpl = drawTemplate(version, ecc, mask);
775
+ const { total } = info.capacity(version, ecc);
776
+ const bytes = new Uint8Array(total);
777
+ let pos = 0;
778
+ let buf = 0;
779
+ let bitPos = 0;
780
+ zigzag(tpl, mask, (x, y, m) => {
781
+ bitPos++;
782
+ buf <<= 1;
783
+ buf |= +(!!b.data[y][x] !== m);
784
+ if (bitPos !== 8)
785
+ return;
786
+ bytes[pos++] = buf;
787
+ bitPos = 0;
788
+ buf = 0;
789
+ });
790
+ if (pos !== total)
791
+ throw new Error(`decode: pos=${pos}, total=${total}`);
792
+ let bits = Array.from(interleave(version, ecc).decode(bytes))
793
+ .map((i) => bin(i, 8))
794
+ .join('');
795
+ // Reverse operation of index.ts/encode working on bits
796
+ const readBits = (n) => {
797
+ if (n > bits.length)
798
+ throw new Error('Not enough bits');
799
+ const val = bits.slice(0, n);
800
+ bits = bits.slice(n);
801
+ return val;
802
+ };
803
+ const toNum = (n) => Number(`0b${n}`);
804
+ // reverse of common.info.modebits
805
+ const modes = {
806
+ '0000': 'terminator',
807
+ '0001': 'numeric',
808
+ '0010': 'alphanumeric',
809
+ '0100': 'byte',
810
+ '0111': 'eci',
811
+ '1000': 'kanji',
812
+ };
813
+ let res = '';
814
+ while (true) {
815
+ if (bits.length < 4)
816
+ break;
817
+ const modeBits = readBits(4);
818
+ const mode = modes[modeBits];
819
+ if (mode === undefined)
820
+ throw new Error(`Unknown modeBits=${modeBits} res="${res}"`);
821
+ if (mode === 'terminator')
822
+ break;
823
+ const countBits = info.lengthBits(version, mode);
824
+ let count = toNum(readBits(countBits));
825
+ if (mode === 'numeric') {
826
+ while (count >= 3) {
827
+ const v = toNum(readBits(10));
828
+ if (v >= 1000)
829
+ throw new Error(`numberic(3) = ${v}`);
830
+ res += v.toString().padStart(3, '0');
831
+ count -= 3;
832
+ }
833
+ if (count === 2) {
834
+ const v = toNum(readBits(7));
835
+ if (v >= 100)
836
+ throw new Error(`numeric(2) = ${v}`);
837
+ res += v.toString().padStart(2, '0');
838
+ }
839
+ else if (count === 1) {
840
+ const v = toNum(readBits(4));
841
+ if (v >= 10)
842
+ throw new Error(`Numeric(1) = ${v}`);
843
+ res += v.toString();
844
+ }
845
+ }
846
+ else if (mode === 'alphanumeric') {
847
+ while (count >= 2) {
848
+ const v = toNum(readBits(11));
849
+ res += info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
850
+ count -= 2;
851
+ }
852
+ if (count === 1)
853
+ res += info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
854
+ }
855
+ else if (mode === 'byte') {
856
+ let utf8 = [];
857
+ for (let i = 0; i < count; i++)
858
+ utf8.push(Number(`0b${readBits(8)}`));
859
+ res += new TextDecoder().decode(new Uint8Array(utf8));
860
+ }
861
+ else
862
+ throw new Error(`Unknown mode=${mode}`);
863
+ }
864
+ return res;
865
+ }
866
+ // Creates square from rectangle
867
+ function cropToSquare(img) {
868
+ const data = Array.isArray(img.data) ? new Uint8Array(img.data) : img.data;
869
+ const { height, width } = img;
870
+ const squareSize = Math.min(height, width);
871
+ const offset = {
872
+ x: Math.floor((width - squareSize) / 2),
873
+ y: Math.floor((height - squareSize) / 2),
874
+ };
875
+ const bytesPerPixel = getBytesPerPixel(img);
876
+ const croppedData = new Uint8Array(squareSize * squareSize * bytesPerPixel);
877
+ for (let y = 0; y < squareSize; y++) {
878
+ const srcPos = ((y + offset.y) * width + offset.x) * bytesPerPixel;
879
+ const dstPos = y * squareSize * bytesPerPixel;
880
+ const length = squareSize * bytesPerPixel;
881
+ croppedData.set(data.subarray(srcPos, srcPos + length), dstPos);
882
+ }
883
+ return { offset, img: { height: squareSize, width: squareSize, data: croppedData } };
884
+ }
885
+ export function decodeQR(img, opts = {}) {
886
+ for (const field of ['height', 'width']) {
887
+ if (!Number.isSafeInteger(img[field]) || img[field] <= 0)
888
+ throw new Error(`invalid img.${field}=${img[field]} (${typeof img[field]})`);
889
+ }
890
+ if (!Array.isArray(img.data) &&
891
+ !(img.data instanceof Uint8Array) &&
892
+ !(img.data instanceof Uint8ClampedArray))
893
+ throw new Error(`invalid image.data=${img.data} (${typeof img.data})`);
894
+ if (opts.cropToSquare !== undefined && typeof opts.cropToSquare !== 'boolean')
895
+ throw new Error(`invalid opts.cropToSquare=${opts.cropToSquare}`);
896
+ for (const fn of ['pointsOnDetect', 'imageOnBitmap', 'imageOnDetect', 'imageOnResult']) {
897
+ if (opts[fn] !== undefined && typeof opts[fn] !== 'function')
898
+ throw new Error(`invalid opts.${fn}=${opts[fn]} (${typeof opts[fn]})`);
899
+ }
900
+ let offset = { x: 0, y: 0 };
901
+ if (opts.cropToSquare)
902
+ ({ img, offset } = cropToSquare(img));
903
+ const bmp = toBitmap(img);
904
+ if (opts.imageOnBitmap)
905
+ opts.imageOnBitmap(bmp.toImage());
906
+ const { bits, points } = detect(bmp);
907
+ if (opts.pointsOnDetect) {
908
+ const p = points.map((i) => ({ ...i, ...pointAdd(i, offset) }));
909
+ opts.pointsOnDetect(p);
910
+ }
911
+ if (opts.imageOnDetect)
912
+ opts.imageOnDetect(bits.toImage());
913
+ const res = decodeBitmap(bits);
914
+ if (opts.imageOnResult)
915
+ opts.imageOnResult(bits.toImage());
916
+ return res;
917
+ }
918
+ export default decodeQR;
919
+ // Unsafe API utils, exported only for tests
920
+ export const _tests = {
921
+ toBitmap,
922
+ decodeBitmap,
923
+ findFinder,
924
+ detect,
925
+ };
926
+ //# sourceMappingURL=decode.js.map