qr 0.5.5 → 0.6.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/README.md +37 -30
- package/decode.d.ts +85 -12
- package/decode.d.ts.map +1 -1
- package/decode.js +234 -109
- package/decode.js.map +1 -1
- package/dom.d.ts +110 -14
- package/dom.d.ts.map +1 -1
- package/dom.js +123 -18
- package/dom.js.map +1 -1
- package/index.d.ts +175 -35
- package/index.d.ts.map +1 -1
- package/index.js +276 -83
- package/index.js.map +1 -1
- package/package.json +8 -4
- package/src/decode.ts +294 -131
- package/src/dom.ts +156 -32
- package/src/index.ts +500 -129
package/src/decode.ts
CHANGED
|
@@ -17,22 +17,20 @@ limitations under the License.
|
|
|
17
17
|
/**
|
|
18
18
|
* Methods for decoding (reading) QR code patterns.
|
|
19
19
|
* @module
|
|
20
|
-
* @example
|
|
21
|
-
```js
|
|
22
|
-
|
|
23
|
-
```
|
|
24
20
|
*/
|
|
25
21
|
|
|
26
|
-
import type { EncodingType, ErrorCorrection, Image, Mask, Point } from './index.ts';
|
|
22
|
+
import type { EncodingType, ErrorCorrection, Image, Mask, Point, TArg, TRet } from './index.ts';
|
|
27
23
|
import { Bitmap, utils } from './index.ts';
|
|
28
|
-
const { best, bin, drawTemplate, fillArr, info, interleave, validateVersion, zigzag, popcnt } =
|
|
29
|
-
utils;
|
|
30
24
|
|
|
31
25
|
// Constants
|
|
32
26
|
const MAX_BITS_ERROR = 3; // Up to 3 bit errors in version/format
|
|
27
|
+
// Kept at 8: the block-stat fast path reads two u32 words per row and the
|
|
28
|
+
// average uses `sum >>> 6`, so the current binarizer assumes 8x8 = 64 pixels.
|
|
33
29
|
const GRAYSCALE_BLOCK_SIZE = 8;
|
|
34
30
|
const GRAYSCALE_RANGE = 24;
|
|
35
31
|
const PATTERN_VARIANCE = 2;
|
|
32
|
+
// Diagonal finder scans are noisier under blur/perspective than horizontal or
|
|
33
|
+
// vertical runs, so they use a looser ratio tolerance.
|
|
36
34
|
const PATTERN_VARIANCE_DIAGONAL = 1.333;
|
|
37
35
|
const PATTERN_MIN_CONFIRMATIONS = 2;
|
|
38
36
|
const DETECT_MIN_ROW_SKIP = 3;
|
|
@@ -50,9 +48,12 @@ for (let i = 0; i < SUM16.length; i++) {
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
// TODO: move to index, nearby with bitmap and other graph related stuff?
|
|
51
|
+
// Fast truncation for values expected to be non-negative; negatives would wrap
|
|
52
|
+
// through uint32 because this uses `>>> 0`, not `Math.floor()`.
|
|
53
53
|
const int = (n: number) => n >>> 0;
|
|
54
54
|
|
|
55
55
|
type Point4 = [Point, Point, Point, Point];
|
|
56
|
+
/** Finder and alignment points returned by the detector. */
|
|
56
57
|
export type FinderPoints = [Pattern, Pattern, Point, Pattern];
|
|
57
58
|
// distance ^ 2
|
|
58
59
|
const distance2 = (p1: Point, p2: Point) => {
|
|
@@ -78,11 +79,18 @@ const ctz32 = (v: number) => {
|
|
|
78
79
|
if (v === 0) return 32;
|
|
79
80
|
return 31 - Math.clz32((v & -v) >>> 0);
|
|
80
81
|
};
|
|
81
|
-
function cap(value: number, min?: number, max?: number) {
|
|
82
|
-
|
|
82
|
+
function cap(value: number, min?: number, max?: number): number {
|
|
83
|
+
// ISO/IEC 18004:2024 §12 h) builds the sampling grid from "module centres";
|
|
84
|
+
// detector callers pass `0` as a real image edge when clipping those samples
|
|
85
|
+
// and search windows. `|| value` treats that bound as absent.
|
|
86
|
+
let res = value;
|
|
87
|
+
if (max !== undefined) res = Math.min(res, max);
|
|
88
|
+
if (min !== undefined) res = Math.max(res, min);
|
|
89
|
+
return res;
|
|
83
90
|
}
|
|
84
|
-
function getBytesPerPixel(img: Image): number {
|
|
85
|
-
const
|
|
91
|
+
function getBytesPerPixel(img: TArg<Image>): number {
|
|
92
|
+
const image = img as Image;
|
|
93
|
+
const perPixel = image.data.length / (image.width * image.height);
|
|
86
94
|
if (perPixel === 3 || perPixel === 4) return perPixel; // RGB or RGBA
|
|
87
95
|
throw new Error(`Unknown image format, bytes per pixel=${perPixel}`);
|
|
88
96
|
}
|
|
@@ -107,10 +115,11 @@ function isBytes(data: unknown): data is Uint8Array {
|
|
|
107
115
|
* bitmap where finder patterns survive perspective / blur / highlights while
|
|
108
116
|
* keeping false dark regions low enough for downstream finder selection.
|
|
109
117
|
*/
|
|
110
|
-
function toBitmap(img: Image): Bitmap {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
118
|
+
function toBitmap(img: TArg<Image>): TRet<Bitmap> {
|
|
119
|
+
const image = img as Image;
|
|
120
|
+
const width = image.width;
|
|
121
|
+
const height = image.height;
|
|
122
|
+
const data = image.data;
|
|
114
123
|
const bytesPerPixel = getBytesPerPixel(img);
|
|
115
124
|
const pixLen = height * width;
|
|
116
125
|
const brightness = new Uint8Array(pixLen);
|
|
@@ -421,7 +430,7 @@ function toBitmap(img: Image): Bitmap {
|
|
|
421
430
|
}
|
|
422
431
|
}
|
|
423
432
|
}
|
|
424
|
-
return matrix
|
|
433
|
+
return matrix as TRet<Bitmap>;
|
|
425
434
|
}
|
|
426
435
|
|
|
427
436
|
// Various utilities for pattern
|
|
@@ -456,7 +465,7 @@ type Runs = number[];
|
|
|
456
465
|
* @returns
|
|
457
466
|
*/
|
|
458
467
|
function pattern(p: boolean[], size?: number[]) {
|
|
459
|
-
const _size = size || fillArr(p.length, 1);
|
|
468
|
+
const _size = size || utils.fillArr(p.length, 1);
|
|
460
469
|
if (p.length !== _size.length) throw new Error('invalid pattern');
|
|
461
470
|
if (!(p.length & 1)) throw new Error('invalid pattern, length should be odd');
|
|
462
471
|
const res = {
|
|
@@ -464,7 +473,7 @@ function pattern(p: boolean[], size?: number[]) {
|
|
|
464
473
|
length: p.length,
|
|
465
474
|
pattern: p,
|
|
466
475
|
size: _size,
|
|
467
|
-
runs: () => fillArr(p.length, 0),
|
|
476
|
+
runs: () => utils.fillArr(p.length, 0),
|
|
468
477
|
totalSize: sum(_size),
|
|
469
478
|
total: (runs: Runs) => runs.reduce((acc, i) => acc + i),
|
|
470
479
|
shift: (runs: Runs, n: number) => {
|
|
@@ -479,7 +488,11 @@ function pattern(p: boolean[], size?: number[]) {
|
|
|
479
488
|
return true;
|
|
480
489
|
},
|
|
481
490
|
add(out: Pattern[], x: number, y: number, total: number) {
|
|
482
|
-
|
|
491
|
+
// ISO/IEC 18004:2024 §5.3.3.1 gives finder runs as "1:1:3:1:1";
|
|
492
|
+
// §5.3.6 defines alignment as "5 x 5 dark modules", "3 x 3 light
|
|
493
|
+
// modules", and a central dark module. Use this pattern's own run width
|
|
494
|
+
// so alignment candidates are not divided by finder width 7.
|
|
495
|
+
const moduleSize = total / res.totalSize;
|
|
483
496
|
const cur = { x, y, moduleSize, count: 1 };
|
|
484
497
|
for (let idx = 0; idx < out.length; idx++) {
|
|
485
498
|
const f = out[idx];
|
|
@@ -494,12 +507,13 @@ function pattern(p: boolean[], size?: number[]) {
|
|
|
494
507
|
end -= runs[res.center] / 2;
|
|
495
508
|
return end;
|
|
496
509
|
},
|
|
497
|
-
check(b: Bitmap
|
|
510
|
+
check(b: TArg<Bitmap>, runs: Runs, center: Point, incr: Point, maxCount?: number) {
|
|
511
|
+
const bm = b as Bitmap;
|
|
498
512
|
let j = 0;
|
|
499
513
|
let i = pointClone(center);
|
|
500
514
|
const neg = pointNeg(incr);
|
|
501
515
|
const check = (p: number, step: Point) => {
|
|
502
|
-
for (;
|
|
516
|
+
for (; bm.isInside(i) && !!bm.point(i) === res.pattern[p]; pointIncr(i, step)) {
|
|
503
517
|
runs[p]++;
|
|
504
518
|
j++;
|
|
505
519
|
}
|
|
@@ -516,17 +530,18 @@ function pattern(p: boolean[], size?: number[]) {
|
|
|
516
530
|
return j;
|
|
517
531
|
},
|
|
518
532
|
scanLine(
|
|
519
|
-
b: Bitmap
|
|
533
|
+
b: TArg<Bitmap>,
|
|
520
534
|
y: number,
|
|
521
535
|
xStart: number,
|
|
522
536
|
xEnd: number,
|
|
523
537
|
fn: (runs: Runs, x: number) => boolean | void
|
|
524
538
|
) {
|
|
539
|
+
const bm = b as Bitmap;
|
|
525
540
|
const runs = res.runs();
|
|
526
541
|
// Finder scanning also couples to Bitmap internals so it can scan packed
|
|
527
542
|
// 32-bit words directly instead of re-reading one pixel bit at a time.
|
|
528
|
-
const words = (
|
|
529
|
-
const vals = (
|
|
543
|
+
const words = (bm as unknown as { words: number }).words;
|
|
544
|
+
const vals = (bm as unknown as { value: Uint32Array }).value;
|
|
530
545
|
const row = y * words;
|
|
531
546
|
const pattern = res.pattern;
|
|
532
547
|
// Scan one packed bitmap row by jumping whole equal-bit runs inside 32-bit
|
|
@@ -566,7 +581,7 @@ function pattern(p: boolean[], size?: number[]) {
|
|
|
566
581
|
runs[pos] += n;
|
|
567
582
|
x += n - 1;
|
|
568
583
|
// If not last element - continue counting
|
|
569
|
-
if (x !==
|
|
584
|
+
if (x !== bm.width - 1) continue;
|
|
570
585
|
// Last element finishes run, set x outside of run
|
|
571
586
|
x++;
|
|
572
587
|
}
|
|
@@ -594,16 +609,17 @@ function pattern(p: boolean[], size?: number[]) {
|
|
|
594
609
|
};
|
|
595
610
|
return res;
|
|
596
611
|
}
|
|
597
|
-
// light/dark/light/dark
|
|
598
|
-
const FINDER = pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
|
|
599
|
-
// dark/light
|
|
600
|
-
const ALIGNMENT = pattern([false, true, false]);
|
|
612
|
+
// dark/light/dark/light/dark in 1:1:3:1:1 ratio
|
|
613
|
+
const FINDER = /* @__PURE__ */ pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
|
|
614
|
+
// central light/dark/light runs of an alignment pattern in 1:1:1 ratio
|
|
615
|
+
const ALIGNMENT = /* @__PURE__ */ pattern([false, true, false]);
|
|
601
616
|
|
|
602
|
-
function findFinder(b: Bitmap): {
|
|
617
|
+
function findFinder(b: TArg<Bitmap>): {
|
|
603
618
|
bl: Pattern;
|
|
604
619
|
tl: Pattern;
|
|
605
620
|
tr: Pattern;
|
|
606
621
|
} {
|
|
622
|
+
const bm = b as Bitmap;
|
|
607
623
|
let found: Pattern[] = [];
|
|
608
624
|
function checkRuns(runs: Runs, v = 2) {
|
|
609
625
|
const total = sum(runs);
|
|
@@ -614,7 +630,7 @@ function findFinder(b: Bitmap): {
|
|
|
614
630
|
// Non-diagonal line (horizontal or vertical)
|
|
615
631
|
function checkLine(center: Point, maxCount: number, total: number, incr: Point) {
|
|
616
632
|
const runs = FINDER.runs();
|
|
617
|
-
let i = FINDER.check(
|
|
633
|
+
let i = FINDER.check(bm, runs, center, incr, maxCount);
|
|
618
634
|
if (i === false) return false;
|
|
619
635
|
const runsTotal = sum(runs);
|
|
620
636
|
if (5 * Math.abs(runsTotal - total) >= 2 * total) return false;
|
|
@@ -636,17 +652,17 @@ function findFinder(b: Bitmap): {
|
|
|
636
652
|
x = xx + int(x);
|
|
637
653
|
// Diagonal
|
|
638
654
|
const dRuns = FINDER.runs();
|
|
639
|
-
if (!FINDER.check(
|
|
655
|
+
if (!FINDER.check(bm, dRuns, { x: int(x), y: int(y) }, { x: 1, y: 1 })) return false;
|
|
640
656
|
if (!checkRuns(dRuns, PATTERN_VARIANCE_DIAGONAL)) return false;
|
|
641
657
|
FINDER.add(found, x, y, total);
|
|
642
658
|
return true;
|
|
643
659
|
}
|
|
644
660
|
let skipped = false;
|
|
645
661
|
// Start with high skip lines count until we find first pattern
|
|
646
|
-
let ySkip = cap(int((3 *
|
|
662
|
+
let ySkip = cap(int((3 * bm.height) / (4 * 97)), DETECT_MIN_ROW_SKIP);
|
|
647
663
|
let done = false;
|
|
648
|
-
for (let y = ySkip - 1; y <
|
|
649
|
-
FINDER.scanLine(
|
|
664
|
+
for (let y = ySkip - 1; y < bm.height && !done; y += ySkip) {
|
|
665
|
+
FINDER.scanLine(bm, y, 0, bm.width, (runs, x) => {
|
|
650
666
|
if (!check(runs, y, x)) return;
|
|
651
667
|
// Found pattern
|
|
652
668
|
// Reduce row skip, since we found pattern and qr code is nearby
|
|
@@ -684,7 +700,7 @@ function findFinder(b: Bitmap): {
|
|
|
684
700
|
const flen = found.length;
|
|
685
701
|
if (flen < 3) throw new Error(`Finder: len(found) = ${flen}`);
|
|
686
702
|
found.sort((i, j) => i.moduleSize - j.moduleSize);
|
|
687
|
-
const pBest = best<[Pattern, Pattern, Pattern]>();
|
|
703
|
+
const pBest = utils.best<[Pattern, Pattern, Pattern]>();
|
|
688
704
|
// Qubic complexity, but we stop search when we found 3 patterns, so not a problem
|
|
689
705
|
for (let i = 0; i < flen - 2; i++) {
|
|
690
706
|
const fi = found[i];
|
|
@@ -731,21 +747,26 @@ function findFinder(b: Bitmap): {
|
|
|
731
747
|
return { bl, tl, tr };
|
|
732
748
|
}
|
|
733
749
|
|
|
734
|
-
function findAlignment(b: Bitmap
|
|
750
|
+
function findAlignment(b: TArg<Bitmap>, est: Pattern, allowanceFactor: number): Pattern {
|
|
751
|
+
const bm = b as Bitmap;
|
|
735
752
|
const { moduleSize } = est;
|
|
736
753
|
const allowance = int(allowanceFactor * moduleSize);
|
|
737
754
|
const leftX = cap(est.x - allowance, 0);
|
|
738
|
-
const rightX = cap(est.x + allowance, undefined,
|
|
755
|
+
const rightX = cap(est.x + allowance, undefined, bm.width - 1);
|
|
739
756
|
const x = rightX - leftX;
|
|
740
757
|
const topY = cap(est.y - allowance, 0);
|
|
741
|
-
const bottomY = cap(est.y + allowance, undefined,
|
|
758
|
+
const bottomY = cap(est.y + allowance, undefined, bm.height - 1);
|
|
742
759
|
const y = bottomY - topY;
|
|
743
760
|
if (x < moduleSize * 3 || y < moduleSize * 3)
|
|
744
761
|
throw new Error(`x = ${x}, y=${y} moduleSize = ${moduleSize}`);
|
|
745
762
|
const xStart = leftX;
|
|
746
763
|
const yStart = topY;
|
|
747
|
-
|
|
748
|
-
|
|
764
|
+
// ISO/IEC 18004:2024 §12 h)3 scans the alignment pattern's white-square
|
|
765
|
+
// outline from the provisional centre. `rightX` / `bottomY` are inclusive
|
|
766
|
+
// clipped image coordinates; convert them to exclusive scan bounds below so
|
|
767
|
+
// an exact 5x5 search window still includes its final row and column.
|
|
768
|
+
const width = rightX - leftX + 1;
|
|
769
|
+
const height = bottomY - topY + 1;
|
|
749
770
|
const found: Pattern[] = [];
|
|
750
771
|
const xEnd = xStart + width;
|
|
751
772
|
const middleY = int(yStart + height / 2);
|
|
@@ -753,13 +774,13 @@ function findAlignment(b: Bitmap, est: Pattern, allowanceFactor: number) {
|
|
|
753
774
|
const diff = int((yGen + 1) / 2);
|
|
754
775
|
const y = middleY + (yGen & 1 ? -diff : diff);
|
|
755
776
|
let res;
|
|
756
|
-
ALIGNMENT.scanLine(
|
|
777
|
+
ALIGNMENT.scanLine(bm, y, xStart, xEnd, (runs, x) => {
|
|
757
778
|
if (!ALIGNMENT.checkSize(runs, moduleSize)) return;
|
|
758
779
|
const total = sum(runs);
|
|
759
780
|
const xx = ALIGNMENT.toCenter(runs, x);
|
|
760
781
|
// Vertical
|
|
761
782
|
const rVert = ALIGNMENT.runs();
|
|
762
|
-
let v = ALIGNMENT.check(
|
|
783
|
+
let v = ALIGNMENT.check(bm, rVert, { x: int(xx), y }, { y: 1, x: 0 }, 2 * runs[1]);
|
|
763
784
|
if (v === false) return;
|
|
764
785
|
v += y;
|
|
765
786
|
const vTotal = sum(rVert);
|
|
@@ -776,7 +797,8 @@ function findAlignment(b: Bitmap, est: Pattern, allowanceFactor: number) {
|
|
|
776
797
|
throw new Error('Alignment pattern not found');
|
|
777
798
|
}
|
|
778
799
|
|
|
779
|
-
function _single(b: Bitmap
|
|
800
|
+
function _single(b: TArg<Bitmap>, from: Point, to: Point) {
|
|
801
|
+
const bm = b as Bitmap;
|
|
780
802
|
// http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
|
|
781
803
|
let steep = false;
|
|
782
804
|
let d = { x: Math.abs(to.x - from.x), y: Math.abs(to.y - from.y) };
|
|
@@ -794,8 +816,10 @@ function _single(b: Bitmap, from: Point, to: Point) {
|
|
|
794
816
|
for (let x = from.x, y = from.y; x !== xLimit; x += step.x) {
|
|
795
817
|
let real = { x, y };
|
|
796
818
|
if (steep) real = pointMirror(real);
|
|
797
|
-
//
|
|
798
|
-
|
|
819
|
+
// Starting from a dark finder center, walk until the ray crosses
|
|
820
|
+
// light -> dark -> light; `BWBRunLength()` mirrors this to recover the
|
|
821
|
+
// full finder width around the center point.
|
|
822
|
+
if ((runPos === 1) === !!bm.point(real)) {
|
|
799
823
|
if (runPos === 2) return distance({ x, y }, from);
|
|
800
824
|
runPos++;
|
|
801
825
|
}
|
|
@@ -809,12 +833,13 @@ function _single(b: Bitmap, from: Point, to: Point) {
|
|
|
809
833
|
return NaN;
|
|
810
834
|
}
|
|
811
835
|
|
|
812
|
-
function BWBRunLength(b: Bitmap
|
|
813
|
-
|
|
836
|
+
function BWBRunLength(b: TArg<Bitmap>, from: Point, to: Point) {
|
|
837
|
+
const bm = b as Bitmap;
|
|
838
|
+
let result = _single(bm, from, to);
|
|
814
839
|
let scaleY = 1.0;
|
|
815
840
|
const { x: fx, y: fy } = from;
|
|
816
841
|
let otherToX = fx - (to.x - fx);
|
|
817
|
-
const bw =
|
|
842
|
+
const bw = bm.width;
|
|
818
843
|
if (otherToX < 0) {
|
|
819
844
|
scaleY = fx / (fx - otherToX);
|
|
820
845
|
otherToX = 0;
|
|
@@ -822,9 +847,15 @@ function BWBRunLength(b: Bitmap, from: Point, to: Point) {
|
|
|
822
847
|
scaleY = (bw - 1 - fx) / (otherToX - fx);
|
|
823
848
|
otherToX = bw - 1;
|
|
824
849
|
}
|
|
850
|
+
// ISO/IEC 18004:2024 §12 b) uses finder runs in the 1:1:3:1:1 ratio,
|
|
851
|
+
// and §12 h)1 derives module size from finder pattern width. Clipping the
|
|
852
|
+
// reflected ray before `int()` would avoid `>>> 0` wrapping negative near-edge
|
|
853
|
+
// coordinates to a huge uint32 and clamping to the wrong edge. Policy: keep
|
|
854
|
+
// the existing heuristic because the direct fix degraded decode performance
|
|
855
|
+
// on current vectors (BoofCV sweep 134/485 -> 132/485).
|
|
825
856
|
let otherToY = int(fy - (to.y - fy) * scaleY);
|
|
826
857
|
let scaleX = 1.0;
|
|
827
|
-
const bh =
|
|
858
|
+
const bh = bm.height;
|
|
828
859
|
if (otherToY < 0) {
|
|
829
860
|
scaleX = fy / (fy - otherToY);
|
|
830
861
|
otherToY = 0;
|
|
@@ -833,53 +864,64 @@ function BWBRunLength(b: Bitmap, from: Point, to: Point) {
|
|
|
833
864
|
otherToY = bh - 1;
|
|
834
865
|
}
|
|
835
866
|
otherToX = int(fx + (otherToX - fx) * scaleX);
|
|
836
|
-
result += _single(
|
|
867
|
+
result += _single(bm, from, { x: otherToX, y: otherToY });
|
|
868
|
+
// Both mirrored rays include the center module once, so drop one module
|
|
869
|
+
// after summing them into the full finder-width estimate.
|
|
837
870
|
return result - 1.0;
|
|
838
871
|
}
|
|
839
872
|
|
|
840
|
-
function moduleSizeAvg(b: Bitmap
|
|
873
|
+
function moduleSizeAvg(b: TArg<Bitmap>, p1: Point, p2: Point) {
|
|
841
874
|
const est1 = BWBRunLength(b, pointInt(p1), pointInt(p2));
|
|
842
875
|
const est2 = BWBRunLength(b, pointInt(p2), pointInt(p1));
|
|
876
|
+
// One ray can fail near image edges, so keep the surviving estimate
|
|
877
|
+
// instead of discarding the finder-width measurement outright.
|
|
843
878
|
if (Number.isNaN(est1)) return est2 / FINDER.totalSize;
|
|
844
879
|
if (Number.isNaN(est2)) return est1 / FINDER.totalSize;
|
|
845
880
|
return (est1 + est2) / (2 * FINDER.totalSize);
|
|
846
881
|
}
|
|
847
882
|
|
|
848
|
-
function detect(b: Bitmap): {
|
|
883
|
+
function detect(b: TArg<Bitmap>): TRet<{
|
|
849
884
|
bits: Bitmap;
|
|
850
885
|
points: FinderPoints;
|
|
851
|
-
} {
|
|
886
|
+
}> {
|
|
887
|
+
const bm = b as Bitmap;
|
|
852
888
|
let bl, tl, tr;
|
|
853
889
|
try {
|
|
854
|
-
({ bl, tl, tr } = findFinder(
|
|
890
|
+
({ bl, tl, tr } = findFinder(bm));
|
|
855
891
|
} catch (e) {
|
|
856
892
|
try {
|
|
857
|
-
b
|
|
858
|
-
|
|
893
|
+
// ISO/IEC 18004:2024 §12 b)5 says to "reverse the colouring of the
|
|
894
|
+
// light and dark pixels" for reflectance reversal. `detect()` works on
|
|
895
|
+
// the `decodeQR()`-owned scratch bitmap, so keep the retry in-place for
|
|
896
|
+
// performance instead of cloning before this private/test-only helper.
|
|
897
|
+
bm.negate();
|
|
898
|
+
({ bl, tl, tr } = findFinder(bm));
|
|
859
899
|
} catch (e) {
|
|
860
|
-
|
|
900
|
+
bm.negate(); // undo negate
|
|
861
901
|
throw e;
|
|
862
902
|
}
|
|
863
903
|
}
|
|
864
|
-
const moduleSize = (moduleSizeAvg(
|
|
904
|
+
const moduleSize = (moduleSizeAvg(bm, tl, tr) + moduleSizeAvg(bm, tl, bl)) / 2;
|
|
865
905
|
if (moduleSize < 1.0) throw new Error(`invalid moduleSize = ${moduleSize}`);
|
|
866
906
|
// Estimate size
|
|
867
907
|
const tltr = int(distance(tl, tr) / moduleSize + 0.5);
|
|
868
908
|
const tlbl = int(distance(tl, bl) / moduleSize + 0.5);
|
|
869
909
|
let size = int((tltr + tlbl) / 2 + 7);
|
|
910
|
+
// QR side lengths are 21 + 4 * (version - 1), so normalize the estimate
|
|
911
|
+
// to the nearest size that is 1 modulo 4 before decoding the version.
|
|
870
912
|
const rem = size % 4;
|
|
871
913
|
if (rem === 0)
|
|
872
914
|
size++; // -> 1
|
|
873
915
|
else if (rem === 2)
|
|
874
916
|
size--; // -> 1
|
|
875
917
|
else if (rem === 3) size -= 2;
|
|
876
|
-
const version = info.size.decode(size);
|
|
877
|
-
validateVersion(version);
|
|
918
|
+
const version = utils.info.size.decode(size);
|
|
919
|
+
utils.validateVersion(version);
|
|
878
920
|
let alignmentPattern;
|
|
879
|
-
if (info.alignmentPatterns(version).length > 0) {
|
|
921
|
+
if (utils.info.alignmentPatterns(version).length > 0) {
|
|
880
922
|
// Bottom right estimate
|
|
881
923
|
const br = { x: tr.x - tl.x + bl.x, y: tr.y - tl.y + bl.y };
|
|
882
|
-
const c = 1.0 - 3.0 / (info.size.encode(version) - 7);
|
|
924
|
+
const c = 1.0 - 3.0 / (utils.info.size.encode(version) - 7);
|
|
883
925
|
// Estimated alignment pattern position
|
|
884
926
|
const est = {
|
|
885
927
|
x: int(tl.x + c * (br.x - tl.x)),
|
|
@@ -889,7 +931,7 @@ function detect(b: Bitmap): {
|
|
|
889
931
|
};
|
|
890
932
|
for (let i = 4; i <= 16; i <<= 1) {
|
|
891
933
|
try {
|
|
892
|
-
alignmentPattern = findAlignment(
|
|
934
|
+
alignmentPattern = findAlignment(bm, est, i);
|
|
893
935
|
break;
|
|
894
936
|
} catch (e) {}
|
|
895
937
|
}
|
|
@@ -907,14 +949,16 @@ function detect(b: Bitmap): {
|
|
|
907
949
|
toBR = { x: size - 3.5, y: size - 3.5 };
|
|
908
950
|
}
|
|
909
951
|
const from: FinderPoints = [tl, tr, br, bl];
|
|
910
|
-
const bits = transform(
|
|
911
|
-
return { bits: bits, points: from };
|
|
952
|
+
const bits = transform(bm, size, from, [toTL, toTR, toBR, toBL]);
|
|
953
|
+
return { bits: bits as Bitmap, points: from } as TRet<{ bits: Bitmap; points: FinderPoints }>;
|
|
912
954
|
}
|
|
913
955
|
|
|
914
956
|
// Perspective transform by 4 points
|
|
915
957
|
function squareToQuadrilateral(p: Point4) {
|
|
916
958
|
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 };
|
|
917
959
|
if (d3.x === 0.0 && d3.y === 0.0) {
|
|
960
|
+
// Parallelogram fast path: perspective terms vanish, so the homography
|
|
961
|
+
// reduces to an affine transform.
|
|
918
962
|
return [
|
|
919
963
|
[p[1].x - p[0].x, p[2].x - p[1].x, p[0].x],
|
|
920
964
|
[p[1].y - p[0].y, p[2].y - p[1].y, p[0].y],
|
|
@@ -935,10 +979,13 @@ function squareToQuadrilateral(p: Point4) {
|
|
|
935
979
|
}
|
|
936
980
|
|
|
937
981
|
// Transform quadrilateral to square by 4 points
|
|
938
|
-
function transform(b: Bitmap
|
|
982
|
+
function transform(b: TArg<Bitmap>, size: number, from: Point4, to: Point4): TRet<Bitmap> {
|
|
983
|
+
const bm = b as Bitmap;
|
|
939
984
|
// TODO: check
|
|
940
985
|
// https://math.stackexchange.com/questions/13404/mapping-irregular-quadrilateral-to-a-rectangle
|
|
941
986
|
const p = squareToQuadrilateral(to);
|
|
987
|
+
// Homographies are scale-invariant, so the adjugate is enough here;
|
|
988
|
+
// there is no need to divide by the determinant when inverting `p`.
|
|
942
989
|
const qToS = [
|
|
943
990
|
[
|
|
944
991
|
p[1][1] * p[2][2] - p[2][1] * p[1][2],
|
|
@@ -962,7 +1009,7 @@ function transform(b: Bitmap, size: number, from: Point4, to: Point4): Bitmap {
|
|
|
962
1009
|
);
|
|
963
1010
|
|
|
964
1011
|
const res = new Bitmap(size);
|
|
965
|
-
const points = fillArr(2 * size, 0);
|
|
1012
|
+
const points = utils.fillArr(2 * size, 0);
|
|
966
1013
|
const pointsLength = points.length;
|
|
967
1014
|
for (let y = 0; y < size; y++) {
|
|
968
1015
|
const p = transform;
|
|
@@ -970,23 +1017,28 @@ function transform(b: Bitmap, size: number, from: Point4, to: Point4): Bitmap {
|
|
|
970
1017
|
const x = i / 2 + 0.5;
|
|
971
1018
|
const y2 = y + 0.5;
|
|
972
1019
|
const den = p[2][0] * x + p[2][1] * y2 + p[2][2];
|
|
973
|
-
|
|
974
|
-
|
|
1020
|
+
// ISO/IEC 18004:2024 §12 h) maps sampling grid intersections back to
|
|
1021
|
+
// image coordinates before deciding dark/light state. Clip projected
|
|
1022
|
+
// coordinates before `int()` because `>>> 0` wraps negative samples to a
|
|
1023
|
+
// huge uint32 and would clamp them to the far image edge.
|
|
1024
|
+
points[i] = int(cap((p[0][0] * x + p[0][1] * y2 + p[0][2]) / den, 0, bm.width - 1));
|
|
1025
|
+
points[i + 1] = int(cap((p[1][0] * x + p[1][1] * y2 + p[1][2]) / den, 0, bm.height - 1));
|
|
975
1026
|
}
|
|
976
1027
|
for (let i = 0; i < pointsLength; i += 2) {
|
|
977
|
-
|
|
978
|
-
const py = cap(points[i + 1], 0, b.height - 1);
|
|
979
|
-
if (b.get(px, py)) res.set((i / 2) | 0, y, true);
|
|
1028
|
+
if (bm.get(points[i], points[i + 1])) res.set((i / 2) | 0, y, true);
|
|
980
1029
|
}
|
|
981
1030
|
}
|
|
982
|
-
return res
|
|
1031
|
+
return res as TRet<Bitmap>;
|
|
983
1032
|
}
|
|
984
1033
|
|
|
985
1034
|
// Same as in drawTemplate, but reading
|
|
986
1035
|
// TODO: merge in CoderType?
|
|
987
|
-
function readInfoBits(b: Bitmap) {
|
|
988
|
-
const
|
|
989
|
-
|
|
1036
|
+
function readInfoBits(b: TArg<Bitmap>) {
|
|
1037
|
+
const bm = b as Bitmap;
|
|
1038
|
+
// Walk each reserved copy from the highest-numbered module back to module 0
|
|
1039
|
+
// so the shift accumulator rebuilds the canonical bit string from MSB to LSB.
|
|
1040
|
+
const readBit = (x: number, y: number, out: number) => (out << 1) | (bm.get(x, y) ? 1 : 0);
|
|
1041
|
+
const size = bm.height;
|
|
990
1042
|
// Version information
|
|
991
1043
|
let version1 = 0;
|
|
992
1044
|
for (let y = 5; y >= 0; y--)
|
|
@@ -1007,45 +1059,48 @@ function readInfoBits(b: Bitmap) {
|
|
|
1007
1059
|
return { version1, version2, format1, format2 };
|
|
1008
1060
|
}
|
|
1009
1061
|
|
|
1010
|
-
function parseInfo(b: Bitmap) {
|
|
1062
|
+
function parseInfo(b: TArg<Bitmap>) {
|
|
1063
|
+
const bm = b as Bitmap;
|
|
1011
1064
|
// Population count over xor -> hamming distance
|
|
1012
|
-
const size =
|
|
1013
|
-
const { version1, version2, format1, format2 } = readInfoBits(
|
|
1065
|
+
const size = bm.height;
|
|
1066
|
+
const { version1, version2, format1, format2 } = readInfoBits(bm);
|
|
1014
1067
|
// Guess format
|
|
1015
1068
|
let format;
|
|
1016
|
-
const bestFormat = best<{ ecc: ErrorCorrection; mask: Mask }>();
|
|
1069
|
+
const bestFormat = utils.best<{ ecc: ErrorCorrection; mask: Mask }>();
|
|
1017
1070
|
for (const ecc of ['medium', 'low', 'high', 'quartile'] as const) {
|
|
1018
1071
|
for (let mask: Mask = 0; mask < 8; mask++) {
|
|
1019
|
-
const bits = info.formatBits(ecc, mask as Mask);
|
|
1072
|
+
const bits = utils.info.formatBits(ecc, mask as Mask);
|
|
1020
1073
|
const cur = { ecc, mask: mask as Mask };
|
|
1021
1074
|
if (bits === format1 || bits === format2) {
|
|
1022
1075
|
format = cur;
|
|
1023
1076
|
break;
|
|
1024
1077
|
}
|
|
1025
|
-
bestFormat.add(popcnt(format1 ^ bits), cur);
|
|
1026
|
-
if (format1 !== format2) bestFormat.add(popcnt(format2 ^ bits), cur);
|
|
1078
|
+
bestFormat.add(utils.popcnt(format1 ^ bits), cur);
|
|
1079
|
+
if (format1 !== format2) bestFormat.add(utils.popcnt(format2 ^ bits), cur);
|
|
1027
1080
|
}
|
|
1028
1081
|
}
|
|
1029
1082
|
if (format === undefined && bestFormat.score() <= MAX_BITS_ERROR) format = bestFormat.get();
|
|
1030
1083
|
if (format === undefined) throw new Error('invalid format pattern');
|
|
1031
|
-
let version: number | undefined = info.size.decode(size); // Guess version based on bitmap size
|
|
1032
|
-
|
|
1084
|
+
let version: number | undefined = utils.info.size.decode(size); // Guess version based on bitmap size
|
|
1085
|
+
// Versions 1-6 do not carry version-information words, so side length is
|
|
1086
|
+
// the only authoritative version source until version 7 adds those fields.
|
|
1087
|
+
if (version < 7) utils.validateVersion(version);
|
|
1033
1088
|
else {
|
|
1034
1089
|
version = undefined;
|
|
1035
1090
|
// Guess version
|
|
1036
|
-
const bestVer = best<number>();
|
|
1091
|
+
const bestVer = utils.best<number>();
|
|
1037
1092
|
for (let ver = 7; ver <= 40; ver++) {
|
|
1038
|
-
const bits = info.versionBits(ver);
|
|
1093
|
+
const bits = utils.info.versionBits(ver);
|
|
1039
1094
|
if (bits === version1 || bits === version2) {
|
|
1040
1095
|
version = ver;
|
|
1041
1096
|
break;
|
|
1042
1097
|
}
|
|
1043
|
-
bestVer.add(popcnt(version1 ^ bits), ver);
|
|
1044
|
-
if (version1 !== version2) bestVer.add(popcnt(version2 ^ bits), ver);
|
|
1098
|
+
bestVer.add(utils.popcnt(version1 ^ bits), ver);
|
|
1099
|
+
if (version1 !== version2) bestVer.add(utils.popcnt(version2 ^ bits), ver);
|
|
1045
1100
|
}
|
|
1046
1101
|
if (version === undefined && bestVer.score() <= MAX_BITS_ERROR) version = bestVer.get();
|
|
1047
1102
|
if (version === undefined) throw new Error('invalid version pattern');
|
|
1048
|
-
if (info.size.encode(version) !== size) throw new Error('invalid version size');
|
|
1103
|
+
if (utils.info.size.encode(version) !== size) throw new Error('invalid version size');
|
|
1049
1104
|
}
|
|
1050
1105
|
return { version, ...format };
|
|
1051
1106
|
}
|
|
@@ -1054,7 +1109,10 @@ function parseInfo(b: Bitmap) {
|
|
|
1054
1109
|
// See https://github.com/microsoft/TypeScript/issues/31535
|
|
1055
1110
|
declare const TextDecoder: any;
|
|
1056
1111
|
|
|
1057
|
-
//
|
|
1112
|
+
// ISO/IEC 18004:2024 §7.4.3.2 says each ECI is a "6-digit assignment
|
|
1113
|
+
// number"; §7.4.3.4 says invoked ECIs apply until "a change of ECI".
|
|
1114
|
+
// Decode through platform TextDecoder only: unsupported labels may throw on
|
|
1115
|
+
// some runtimes, and callers needing them can provide a custom textDecoder.
|
|
1058
1116
|
const eciToEncoding: Record<number, string> = {
|
|
1059
1117
|
1: 'iso-8859-1',
|
|
1060
1118
|
2: 'ibm437',
|
|
@@ -1084,38 +1142,42 @@ const eciToEncoding: Record<number, string> = {
|
|
|
1084
1142
|
30: 'euc-kr',
|
|
1085
1143
|
};
|
|
1086
1144
|
|
|
1087
|
-
function decodeWithEci(bytes: Uint8Array
|
|
1145
|
+
function decodeWithEci(bytes: TArg<Uint8Array>, eci: number = 26): string {
|
|
1146
|
+
// ISO/IEC 18004:2024 §7.3.2 says QR's "default interpretation" is
|
|
1147
|
+
// "ECI 000003 representing the ISO/IEC 8859-1 character set". Keep UTF-8
|
|
1148
|
+
// here so this library's UTF-8 byte-mode encoder round-trips without ECI.
|
|
1088
1149
|
const encoding = eciToEncoding[eci];
|
|
1089
1150
|
if (!encoding) throw new Error(`Unsupported ECI: ${eci}`);
|
|
1090
1151
|
return new TextDecoder(encoding).decode(bytes);
|
|
1091
1152
|
}
|
|
1092
1153
|
|
|
1093
1154
|
function decodeBitmap(
|
|
1094
|
-
b: Bitmap
|
|
1095
|
-
decoder: (bytes: Uint8Array, eci: number) => string = decodeWithEci
|
|
1155
|
+
b: TArg<Bitmap>,
|
|
1156
|
+
decoder: TArg<(bytes: Uint8Array, eci: number) => string> = decodeWithEci
|
|
1096
1157
|
): string {
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1158
|
+
const bm = b as Bitmap;
|
|
1159
|
+
const size = bm.height;
|
|
1160
|
+
if (size < 21 || (size & 0b11) !== 1 || size !== bm.width)
|
|
1099
1161
|
throw new Error(`decode: invalid size=${size}`);
|
|
1100
|
-
const { version, mask, ecc } = parseInfo(
|
|
1101
|
-
const tpl = drawTemplate(version, ecc, mask);
|
|
1102
|
-
const { total } = info.capacity(version, ecc);
|
|
1162
|
+
const { version, mask, ecc } = parseInfo(bm);
|
|
1163
|
+
const tpl = utils.drawTemplate(version, ecc, mask);
|
|
1164
|
+
const { total } = utils.info.capacity(version, ecc);
|
|
1103
1165
|
const bytes = new Uint8Array(total);
|
|
1104
1166
|
let pos = 0;
|
|
1105
1167
|
let buf = 0;
|
|
1106
1168
|
let bitPos = 0;
|
|
1107
|
-
zigzag(tpl, mask, (x, y, m) => {
|
|
1169
|
+
utils.zigzag(tpl, mask, (x, y, m) => {
|
|
1108
1170
|
bitPos++;
|
|
1109
1171
|
buf <<= 1;
|
|
1110
|
-
buf |= +(!!
|
|
1172
|
+
buf |= +(!!bm.get(x, y) !== m);
|
|
1111
1173
|
if (bitPos !== 8) return;
|
|
1112
1174
|
bytes[pos++] = buf;
|
|
1113
1175
|
bitPos = 0;
|
|
1114
1176
|
buf = 0;
|
|
1115
1177
|
});
|
|
1116
1178
|
if (pos !== total) throw new Error(`decode: pos=${pos}, total=${total}`);
|
|
1117
|
-
let bits = Array.from(interleave(version, ecc).decode(bytes))
|
|
1118
|
-
.map((i) => bin(i, 8))
|
|
1179
|
+
let bits = Array.from(utils.interleave(version, ecc).decode(bytes))
|
|
1180
|
+
.map((i) => utils.bin(i, 8))
|
|
1119
1181
|
.join('');
|
|
1120
1182
|
// Reverse operation of index.ts/encode working on bits
|
|
1121
1183
|
const readBits = (n: number) => {
|
|
@@ -1135,6 +1197,9 @@ function decodeBitmap(
|
|
|
1135
1197
|
'1000': 'kanji',
|
|
1136
1198
|
};
|
|
1137
1199
|
let res = '';
|
|
1200
|
+
// ISO/IEC 18004:2024 §7.3.2 defines no-ECI byte data as ECI 000003
|
|
1201
|
+
// / ISO-8859-1. Keep ECI 26 for compatibility with legacy no-ECI UTF-8
|
|
1202
|
+
// payloads and with encodeQR's UTF-8 byte-mode default.
|
|
1138
1203
|
let eci: number = 26; // Default to utf-8 for compat with old behavior
|
|
1139
1204
|
while (true) {
|
|
1140
1205
|
if (bits.length < 4) break;
|
|
@@ -1142,7 +1207,7 @@ function decodeBitmap(
|
|
|
1142
1207
|
const mode = modes[modeBits];
|
|
1143
1208
|
if (mode === undefined) throw new Error(`Unknown modeBits=${modeBits} res="${res}"`);
|
|
1144
1209
|
if (mode === 'terminator') break;
|
|
1145
|
-
const countBits = info.lengthBits(version, mode);
|
|
1210
|
+
const countBits = utils.info.lengthBits(version, mode);
|
|
1146
1211
|
let count = toNum(readBits(countBits));
|
|
1147
1212
|
if (mode === 'numeric') {
|
|
1148
1213
|
while (count >= 3) {
|
|
@@ -1163,10 +1228,10 @@ function decodeBitmap(
|
|
|
1163
1228
|
} else if (mode === 'alphanumeric') {
|
|
1164
1229
|
while (count >= 2) {
|
|
1165
1230
|
const v = toNum(readBits(11));
|
|
1166
|
-
res += info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
|
|
1231
|
+
res += utils.info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
|
|
1167
1232
|
count -= 2;
|
|
1168
1233
|
}
|
|
1169
|
-
if (count === 1) res += info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
|
|
1234
|
+
if (count === 1) res += utils.info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
|
|
1170
1235
|
} else if (mode === 'eci') {
|
|
1171
1236
|
const first = toNum(readBits(8));
|
|
1172
1237
|
if ((first & 0x80) === 0) eci = first;
|
|
@@ -1177,24 +1242,69 @@ function decodeBitmap(
|
|
|
1177
1242
|
const data = new Uint8Array(count);
|
|
1178
1243
|
for (let i = 0; i < count; i++) data[i] = toNum(readBits(8));
|
|
1179
1244
|
res += decoder(data, eci);
|
|
1245
|
+
} else if (mode === 'kanji') {
|
|
1246
|
+
// ISO/IEC 18004:2024 §7.3.6 says Kanji mode uses Shift JIS and
|
|
1247
|
+
// "Each two-byte character value is compacted to a 13-bit binary
|
|
1248
|
+
// codeword." Keep it unsupported until a real interop fixture can verify
|
|
1249
|
+
// the 13-bit-to-Shift-JIS mapping rather than adding untested decoding.
|
|
1250
|
+
throw new Error('Kanji mode is not supported');
|
|
1180
1251
|
} else throw new Error(`Unknown mode=${mode}`);
|
|
1181
1252
|
}
|
|
1182
1253
|
return res;
|
|
1183
1254
|
}
|
|
1184
1255
|
|
|
1256
|
+
/** QR decoding hooks and image preprocessing options. */
|
|
1185
1257
|
export type DecodeOpts = {
|
|
1258
|
+
/** Crop rectangular inputs to a centered square before decoding. */
|
|
1186
1259
|
cropToSquare?: boolean;
|
|
1187
|
-
|
|
1260
|
+
/**
|
|
1261
|
+
* Custom byte-to-text decoder used for byte segments.
|
|
1262
|
+
*
|
|
1263
|
+
* Receives the byte segment and, when needed, the active ECI designator.
|
|
1264
|
+
* ISO/IEC 18004:2024 §7.4.3.4 keeps ECIs active "until the end of
|
|
1265
|
+
* the encoded data or a change of ECI", so callers need the active
|
|
1266
|
+
* designator to apply their own charset policy.
|
|
1267
|
+
* @param bytes - Byte segment payload to decode.
|
|
1268
|
+
* @param eci - Active ECI designator for the byte segment.
|
|
1269
|
+
* @returns Decoded text for the byte segment.
|
|
1270
|
+
*/
|
|
1271
|
+
textDecoder?: TArg<(bytes: Uint8Array, eci?: number) => string>;
|
|
1272
|
+
/**
|
|
1273
|
+
* Callback invoked with finder/alignment points after detection succeeds.
|
|
1274
|
+
*
|
|
1275
|
+
* Receives finder and alignment points returned by the detector.
|
|
1276
|
+
* @param points - Finder and alignment points returned by the detector.
|
|
1277
|
+
*/
|
|
1188
1278
|
pointsOnDetect?: (points: FinderPoints) => void;
|
|
1279
|
+
/**
|
|
1280
|
+
* Callback invoked with the grayscale bitmap that the detector sees.
|
|
1281
|
+
*
|
|
1282
|
+
* Receives the grayscale image generated during bitmap conversion.
|
|
1283
|
+
* @param img - Grayscale image generated during bitmap conversion.
|
|
1284
|
+
*/
|
|
1189
1285
|
imageOnBitmap?: (img: Image) => void;
|
|
1286
|
+
/**
|
|
1287
|
+
* Callback invoked with the perspective-corrected QR image.
|
|
1288
|
+
*
|
|
1289
|
+
* Receives the perspective-corrected QR image.
|
|
1290
|
+
* @param img - Perspective-corrected QR image.
|
|
1291
|
+
*/
|
|
1190
1292
|
imageOnDetect?: (img: Image) => void;
|
|
1293
|
+
/**
|
|
1294
|
+
* Callback invoked with the final decoded QR image.
|
|
1295
|
+
*
|
|
1296
|
+
* Receives the final QR image used to decode the payload.
|
|
1297
|
+
* @param img - Final QR image used to decode the payload.
|
|
1298
|
+
*/
|
|
1191
1299
|
imageOnResult?: (img: Image) => void;
|
|
1192
1300
|
};
|
|
1193
1301
|
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const
|
|
1302
|
+
// Center-crop to a square and return the original-image offset so
|
|
1303
|
+
// `decodeQR()` can map detected points back into caller coordinates.
|
|
1304
|
+
function cropToSquare(img: TArg<Image>) {
|
|
1305
|
+
const image = img as Image;
|
|
1306
|
+
const data = Array.isArray(image.data) ? new Uint8Array(image.data) : image.data;
|
|
1307
|
+
const { height, width } = image;
|
|
1198
1308
|
const squareSize = Math.min(height, width);
|
|
1199
1309
|
const offset = {
|
|
1200
1310
|
x: Math.floor((width - squareSize) / 2),
|
|
@@ -1211,46 +1321,99 @@ function cropToSquare(img: Image) {
|
|
|
1211
1321
|
return { offset, img: { height: squareSize, width: squareSize, data: croppedData } };
|
|
1212
1322
|
}
|
|
1213
1323
|
|
|
1214
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* Decode text from a QR image.
|
|
1326
|
+
* @param img - RGB or RGBA image data that contains a QR code.
|
|
1327
|
+
* @param opts - Decoder hooks and image preprocessing options. See {@link DecodeOpts}.
|
|
1328
|
+
* @returns Decoded QR payload as a string.
|
|
1329
|
+
* @throws If the image, decoder options, or QR contents are invalid. {@link Error}
|
|
1330
|
+
* @example
|
|
1331
|
+
* Decode text from a QR image.
|
|
1332
|
+
* ```ts
|
|
1333
|
+
* import encodeQR, { Bitmap } from 'qr';
|
|
1334
|
+
* import decodeQR from 'qr/decode.js';
|
|
1335
|
+
* const bits = encodeQR('Hello world', 'raw', { scale: 4 });
|
|
1336
|
+
* const bm = new Bitmap({ width: bits[0].length, height: bits.length }, bits);
|
|
1337
|
+
* const text = decodeQR(bm.toImage());
|
|
1338
|
+
* ```
|
|
1339
|
+
*/
|
|
1340
|
+
export function decodeQR(img: TArg<Image>, opts: TArg<DecodeOpts> = {}): string {
|
|
1341
|
+
let image = img as Image;
|
|
1342
|
+
const options = opts as DecodeOpts;
|
|
1215
1343
|
for (const field of ['height', 'width'] as const) {
|
|
1216
|
-
if (!Number.isSafeInteger(
|
|
1217
|
-
throw new Error(`invalid img.${field}=${
|
|
1344
|
+
if (!Number.isSafeInteger(image[field]) || image[field] <= 0)
|
|
1345
|
+
throw new Error(`invalid img.${field}=${image[field]} (${typeof image[field]})`);
|
|
1218
1346
|
}
|
|
1219
|
-
const { data } =
|
|
1347
|
+
const { data } = image;
|
|
1220
1348
|
if (!Array.isArray(data) && !isBytes(data))
|
|
1221
1349
|
throw new Error(`invalid image.data=${data} (${typeof data})`);
|
|
1222
|
-
if (
|
|
1223
|
-
throw new Error(`invalid opts.cropToSquare=${
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1350
|
+
if (options.cropToSquare !== undefined && typeof options.cropToSquare !== 'boolean')
|
|
1351
|
+
throw new Error(`invalid opts.cropToSquare=${options.cropToSquare}`);
|
|
1352
|
+
// Validate callbacks before decoding so payload mode does not decide whether
|
|
1353
|
+
// an invalid public option is accepted.
|
|
1354
|
+
for (const fn of [
|
|
1355
|
+
'textDecoder',
|
|
1356
|
+
'pointsOnDetect',
|
|
1357
|
+
'imageOnBitmap',
|
|
1358
|
+
'imageOnDetect',
|
|
1359
|
+
'imageOnResult',
|
|
1360
|
+
] as const) {
|
|
1361
|
+
if (options[fn] !== undefined && typeof options[fn] !== 'function')
|
|
1362
|
+
throw new Error(`invalid opts.${fn}=${options[fn]} (${typeof options[fn]})`);
|
|
1227
1363
|
}
|
|
1228
1364
|
let offset = { x: 0, y: 0 };
|
|
1229
|
-
if (
|
|
1230
|
-
const bmp = toBitmap(
|
|
1231
|
-
if (
|
|
1365
|
+
if (options.cropToSquare) ({ img: image, offset } = cropToSquare(image));
|
|
1366
|
+
const bmp = toBitmap(image) as Bitmap;
|
|
1367
|
+
if (options.imageOnBitmap) options.imageOnBitmap(bmp.toImage());
|
|
1232
1368
|
const { bits, points } = detect(bmp);
|
|
1233
|
-
if (
|
|
1369
|
+
if (options.pointsOnDetect) {
|
|
1370
|
+
// Report finder points in the caller's original coordinate space after any center-crop.
|
|
1234
1371
|
const p = points.map((i) => ({ ...i, ...pointAdd(i, offset) })) as FinderPoints;
|
|
1235
|
-
|
|
1372
|
+
options.pointsOnDetect(p);
|
|
1236
1373
|
}
|
|
1237
|
-
if (
|
|
1238
|
-
const res = decodeBitmap(bits,
|
|
1239
|
-
if (
|
|
1374
|
+
if (options.imageOnDetect) options.imageOnDetect(bits.toImage());
|
|
1375
|
+
const res = decodeBitmap(bits, options.textDecoder);
|
|
1376
|
+
if (options.imageOnResult) options.imageOnResult(bits.toImage());
|
|
1240
1377
|
return res;
|
|
1241
1378
|
}
|
|
1242
1379
|
|
|
1380
|
+
/**
|
|
1381
|
+
* Default export alias for {@link decodeQR}.
|
|
1382
|
+
* @param img - RGB or RGBA image data that contains a QR code.
|
|
1383
|
+
* @param opts - Decoder hooks and image preprocessing options. See {@link DecodeOpts}.
|
|
1384
|
+
* @returns Decoded QR payload as a string.
|
|
1385
|
+
* @throws If the image, decoder options, or QR contents are invalid. {@link Error}
|
|
1386
|
+
* @example
|
|
1387
|
+
* Decode a rendered QR image via the default decoder export.
|
|
1388
|
+
* ```ts
|
|
1389
|
+
* import encodeQR, { Bitmap } from 'qr';
|
|
1390
|
+
* import decodeQR from 'qr/decode.js';
|
|
1391
|
+
* const bits = encodeQR('Hello world', 'raw', { scale: 4 });
|
|
1392
|
+
* const bm = new Bitmap({ width: bits[0].length, height: bits.length }, bits);
|
|
1393
|
+
* decodeQR(bm.toImage());
|
|
1394
|
+
* ```
|
|
1395
|
+
*/
|
|
1243
1396
|
export default decodeQR;
|
|
1244
1397
|
|
|
1398
|
+
// Additional private helpers for focused regression tests; separate from the
|
|
1399
|
+
// existing `_tests` object so its current keys remain stable.
|
|
1400
|
+
export const _TESTS: {
|
|
1401
|
+
findAlignment: typeof findAlignment;
|
|
1402
|
+
transform: typeof transform;
|
|
1403
|
+
} = /* @__PURE__ */ Object.freeze({
|
|
1404
|
+
findAlignment: findAlignment,
|
|
1405
|
+
transform: transform,
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1245
1408
|
// Unsafe API utils, exported only for tests
|
|
1246
1409
|
export const _tests: {
|
|
1247
1410
|
toBitmap: typeof toBitmap;
|
|
1248
1411
|
decodeBitmap: typeof decodeBitmap;
|
|
1249
1412
|
findFinder: typeof findFinder;
|
|
1250
1413
|
detect: typeof detect;
|
|
1251
|
-
} = {
|
|
1414
|
+
} = /* @__PURE__ */ Object.freeze({
|
|
1252
1415
|
toBitmap,
|
|
1253
1416
|
decodeBitmap,
|
|
1254
1417
|
findFinder,
|
|
1255
1418
|
detect,
|
|
1256
|
-
};
|
|
1419
|
+
});
|