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