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/decode.js
CHANGED
|
@@ -17,18 +17,17 @@ 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
|
import { Bitmap, utils } from "./index.js";
|
|
26
|
-
const { best, bin, drawTemplate, fillArr, info, interleave, validateVersion, zigzag, popcnt } = utils;
|
|
27
22
|
// Constants
|
|
28
23
|
const MAX_BITS_ERROR = 3; // Up to 3 bit errors in version/format
|
|
24
|
+
// Kept at 8: the block-stat fast path reads two u32 words per row and the
|
|
25
|
+
// average uses `sum >>> 6`, so the current binarizer assumes 8x8 = 64 pixels.
|
|
29
26
|
const GRAYSCALE_BLOCK_SIZE = 8;
|
|
30
27
|
const GRAYSCALE_RANGE = 24;
|
|
31
28
|
const PATTERN_VARIANCE = 2;
|
|
29
|
+
// Diagonal finder scans are noisier under blur/perspective than horizontal or
|
|
30
|
+
// vertical runs, so they use a looser ratio tolerance.
|
|
32
31
|
const PATTERN_VARIANCE_DIAGONAL = 1.333;
|
|
33
32
|
const PATTERN_MIN_CONFIRMATIONS = 2;
|
|
34
33
|
const DETECT_MIN_ROW_SKIP = 3;
|
|
@@ -45,6 +44,8 @@ for (let i = 0; i < SUM16.length; i++) {
|
|
|
45
44
|
MAX16[i] = lo > hi ? lo : hi;
|
|
46
45
|
}
|
|
47
46
|
// TODO: move to index, nearby with bitmap and other graph related stuff?
|
|
47
|
+
// Fast truncation for values expected to be non-negative; negatives would wrap
|
|
48
|
+
// through uint32 because this uses `>>> 0`, not `Math.floor()`.
|
|
48
49
|
const int = (n) => n >>> 0;
|
|
49
50
|
// distance ^ 2
|
|
50
51
|
const distance2 = (p1, p2) => {
|
|
@@ -72,10 +73,19 @@ const ctz32 = (v) => {
|
|
|
72
73
|
return 31 - Math.clz32((v & -v) >>> 0);
|
|
73
74
|
};
|
|
74
75
|
function cap(value, min, max) {
|
|
75
|
-
|
|
76
|
+
// ISO/IEC 18004:2024 §12 h) builds the sampling grid from "module centres";
|
|
77
|
+
// detector callers pass `0` as a real image edge when clipping those samples
|
|
78
|
+
// and search windows. `|| value` treats that bound as absent.
|
|
79
|
+
let res = value;
|
|
80
|
+
if (max !== undefined)
|
|
81
|
+
res = Math.min(res, max);
|
|
82
|
+
if (min !== undefined)
|
|
83
|
+
res = Math.max(res, min);
|
|
84
|
+
return res;
|
|
76
85
|
}
|
|
77
86
|
function getBytesPerPixel(img) {
|
|
78
|
-
const
|
|
87
|
+
const image = img;
|
|
88
|
+
const perPixel = image.data.length / (image.width * image.height);
|
|
79
89
|
if (perPixel === 3 || perPixel === 4)
|
|
80
90
|
return perPixel; // RGB or RGBA
|
|
81
91
|
throw new Error(`Unknown image format, bytes per pixel=${perPixel}`);
|
|
@@ -102,9 +112,10 @@ function isBytes(data) {
|
|
|
102
112
|
* keeping false dark regions low enough for downstream finder selection.
|
|
103
113
|
*/
|
|
104
114
|
function toBitmap(img) {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
115
|
+
const image = img;
|
|
116
|
+
const width = image.width;
|
|
117
|
+
const height = image.height;
|
|
118
|
+
const data = image.data;
|
|
108
119
|
const bytesPerPixel = getBytesPerPixel(img);
|
|
109
120
|
const pixLen = height * width;
|
|
110
121
|
const brightness = new Uint8Array(pixLen);
|
|
@@ -471,7 +482,7 @@ const patternsConfirmed = (lst) => lst.filter((i) => i.count >= PATTERN_MIN_CONF
|
|
|
471
482
|
* @returns
|
|
472
483
|
*/
|
|
473
484
|
function pattern(p, size) {
|
|
474
|
-
const _size = size || fillArr(p.length, 1);
|
|
485
|
+
const _size = size || utils.fillArr(p.length, 1);
|
|
475
486
|
if (p.length !== _size.length)
|
|
476
487
|
throw new Error('invalid pattern');
|
|
477
488
|
if (!(p.length & 1))
|
|
@@ -481,7 +492,7 @@ function pattern(p, size) {
|
|
|
481
492
|
length: p.length,
|
|
482
493
|
pattern: p,
|
|
483
494
|
size: _size,
|
|
484
|
-
runs: () => fillArr(p.length, 0),
|
|
495
|
+
runs: () => utils.fillArr(p.length, 0),
|
|
485
496
|
totalSize: sum(_size),
|
|
486
497
|
total: (runs) => runs.reduce((acc, i) => acc + i),
|
|
487
498
|
shift: (runs, n) => {
|
|
@@ -499,7 +510,11 @@ function pattern(p, size) {
|
|
|
499
510
|
return true;
|
|
500
511
|
},
|
|
501
512
|
add(out, x, y, total) {
|
|
502
|
-
|
|
513
|
+
// ISO/IEC 18004:2024 §5.3.3.1 gives finder runs as "1:1:3:1:1";
|
|
514
|
+
// §5.3.6 defines alignment as "5 x 5 dark modules", "3 x 3 light
|
|
515
|
+
// modules", and a central dark module. Use this pattern's own run width
|
|
516
|
+
// so alignment candidates are not divided by finder width 7.
|
|
517
|
+
const moduleSize = total / res.totalSize;
|
|
503
518
|
const cur = { x, y, moduleSize, count: 1 };
|
|
504
519
|
for (let idx = 0; idx < out.length; idx++) {
|
|
505
520
|
const f = out[idx];
|
|
@@ -517,11 +532,12 @@ function pattern(p, size) {
|
|
|
517
532
|
return end;
|
|
518
533
|
},
|
|
519
534
|
check(b, runs, center, incr, maxCount) {
|
|
535
|
+
const bm = b;
|
|
520
536
|
let j = 0;
|
|
521
537
|
let i = pointClone(center);
|
|
522
538
|
const neg = pointNeg(incr);
|
|
523
539
|
const check = (p, step) => {
|
|
524
|
-
for (;
|
|
540
|
+
for (; bm.isInside(i) && !!bm.point(i) === res.pattern[p]; pointIncr(i, step)) {
|
|
525
541
|
runs[p]++;
|
|
526
542
|
j++;
|
|
527
543
|
}
|
|
@@ -544,11 +560,12 @@ function pattern(p, size) {
|
|
|
544
560
|
return j;
|
|
545
561
|
},
|
|
546
562
|
scanLine(b, y, xStart, xEnd, fn) {
|
|
563
|
+
const bm = b;
|
|
547
564
|
const runs = res.runs();
|
|
548
565
|
// Finder scanning also couples to Bitmap internals so it can scan packed
|
|
549
566
|
// 32-bit words directly instead of re-reading one pixel bit at a time.
|
|
550
|
-
const words =
|
|
551
|
-
const vals =
|
|
567
|
+
const words = bm.words;
|
|
568
|
+
const vals = bm.value;
|
|
552
569
|
const row = y * words;
|
|
553
570
|
const pattern = res.pattern;
|
|
554
571
|
// Scan one packed bitmap row by jumping whole equal-bit runs inside 32-bit
|
|
@@ -593,7 +610,7 @@ function pattern(p, size) {
|
|
|
593
610
|
runs[pos] += n;
|
|
594
611
|
x += n - 1;
|
|
595
612
|
// If not last element - continue counting
|
|
596
|
-
if (x !==
|
|
613
|
+
if (x !== bm.width - 1)
|
|
597
614
|
continue;
|
|
598
615
|
// Last element finishes run, set x outside of run
|
|
599
616
|
x++;
|
|
@@ -624,11 +641,12 @@ function pattern(p, size) {
|
|
|
624
641
|
};
|
|
625
642
|
return res;
|
|
626
643
|
}
|
|
627
|
-
// light/dark/light/dark
|
|
628
|
-
const FINDER = pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
|
|
629
|
-
// dark/light
|
|
630
|
-
const ALIGNMENT = pattern([false, true, false]);
|
|
644
|
+
// dark/light/dark/light/dark in 1:1:3:1:1 ratio
|
|
645
|
+
const FINDER = /* @__PURE__ */ pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
|
|
646
|
+
// central light/dark/light runs of an alignment pattern in 1:1:1 ratio
|
|
647
|
+
const ALIGNMENT = /* @__PURE__ */ pattern([false, true, false]);
|
|
631
648
|
function findFinder(b) {
|
|
649
|
+
const bm = b;
|
|
632
650
|
let found = [];
|
|
633
651
|
function checkRuns(runs, v = 2) {
|
|
634
652
|
const total = sum(runs);
|
|
@@ -640,7 +658,7 @@ function findFinder(b) {
|
|
|
640
658
|
// Non-diagonal line (horizontal or vertical)
|
|
641
659
|
function checkLine(center, maxCount, total, incr) {
|
|
642
660
|
const runs = FINDER.runs();
|
|
643
|
-
let i = FINDER.check(
|
|
661
|
+
let i = FINDER.check(bm, runs, center, incr, maxCount);
|
|
644
662
|
if (i === false)
|
|
645
663
|
return false;
|
|
646
664
|
const runsTotal = sum(runs);
|
|
@@ -667,7 +685,7 @@ function findFinder(b) {
|
|
|
667
685
|
x = xx + int(x);
|
|
668
686
|
// Diagonal
|
|
669
687
|
const dRuns = FINDER.runs();
|
|
670
|
-
if (!FINDER.check(
|
|
688
|
+
if (!FINDER.check(bm, dRuns, { x: int(x), y: int(y) }, { x: 1, y: 1 }))
|
|
671
689
|
return false;
|
|
672
690
|
if (!checkRuns(dRuns, PATTERN_VARIANCE_DIAGONAL))
|
|
673
691
|
return false;
|
|
@@ -676,10 +694,10 @@ function findFinder(b) {
|
|
|
676
694
|
}
|
|
677
695
|
let skipped = false;
|
|
678
696
|
// Start with high skip lines count until we find first pattern
|
|
679
|
-
let ySkip = cap(int((3 *
|
|
697
|
+
let ySkip = cap(int((3 * bm.height) / (4 * 97)), DETECT_MIN_ROW_SKIP);
|
|
680
698
|
let done = false;
|
|
681
|
-
for (let y = ySkip - 1; y <
|
|
682
|
-
FINDER.scanLine(
|
|
699
|
+
for (let y = ySkip - 1; y < bm.height && !done; y += ySkip) {
|
|
700
|
+
FINDER.scanLine(bm, y, 0, bm.width, (runs, x) => {
|
|
683
701
|
if (!check(runs, y, x))
|
|
684
702
|
return;
|
|
685
703
|
// Found pattern
|
|
@@ -725,7 +743,7 @@ function findFinder(b) {
|
|
|
725
743
|
if (flen < 3)
|
|
726
744
|
throw new Error(`Finder: len(found) = ${flen}`);
|
|
727
745
|
found.sort((i, j) => i.moduleSize - j.moduleSize);
|
|
728
|
-
const pBest = best();
|
|
746
|
+
const pBest = utils.best();
|
|
729
747
|
// Qubic complexity, but we stop search when we found 3 patterns, so not a problem
|
|
730
748
|
for (let i = 0; i < flen - 2; i++) {
|
|
731
749
|
const fi = found[i];
|
|
@@ -775,20 +793,25 @@ function findFinder(b) {
|
|
|
775
793
|
return { bl, tl, tr };
|
|
776
794
|
}
|
|
777
795
|
function findAlignment(b, est, allowanceFactor) {
|
|
796
|
+
const bm = b;
|
|
778
797
|
const { moduleSize } = est;
|
|
779
798
|
const allowance = int(allowanceFactor * moduleSize);
|
|
780
799
|
const leftX = cap(est.x - allowance, 0);
|
|
781
|
-
const rightX = cap(est.x + allowance, undefined,
|
|
800
|
+
const rightX = cap(est.x + allowance, undefined, bm.width - 1);
|
|
782
801
|
const x = rightX - leftX;
|
|
783
802
|
const topY = cap(est.y - allowance, 0);
|
|
784
|
-
const bottomY = cap(est.y + allowance, undefined,
|
|
803
|
+
const bottomY = cap(est.y + allowance, undefined, bm.height - 1);
|
|
785
804
|
const y = bottomY - topY;
|
|
786
805
|
if (x < moduleSize * 3 || y < moduleSize * 3)
|
|
787
806
|
throw new Error(`x = ${x}, y=${y} moduleSize = ${moduleSize}`);
|
|
788
807
|
const xStart = leftX;
|
|
789
808
|
const yStart = topY;
|
|
790
|
-
|
|
791
|
-
|
|
809
|
+
// ISO/IEC 18004:2024 §12 h)3 scans the alignment pattern's white-square
|
|
810
|
+
// outline from the provisional centre. `rightX` / `bottomY` are inclusive
|
|
811
|
+
// clipped image coordinates; convert them to exclusive scan bounds below so
|
|
812
|
+
// an exact 5x5 search window still includes its final row and column.
|
|
813
|
+
const width = rightX - leftX + 1;
|
|
814
|
+
const height = bottomY - topY + 1;
|
|
792
815
|
const found = [];
|
|
793
816
|
const xEnd = xStart + width;
|
|
794
817
|
const middleY = int(yStart + height / 2);
|
|
@@ -796,14 +819,14 @@ function findAlignment(b, est, allowanceFactor) {
|
|
|
796
819
|
const diff = int((yGen + 1) / 2);
|
|
797
820
|
const y = middleY + (yGen & 1 ? -diff : diff);
|
|
798
821
|
let res;
|
|
799
|
-
ALIGNMENT.scanLine(
|
|
822
|
+
ALIGNMENT.scanLine(bm, y, xStart, xEnd, (runs, x) => {
|
|
800
823
|
if (!ALIGNMENT.checkSize(runs, moduleSize))
|
|
801
824
|
return;
|
|
802
825
|
const total = sum(runs);
|
|
803
826
|
const xx = ALIGNMENT.toCenter(runs, x);
|
|
804
827
|
// Vertical
|
|
805
828
|
const rVert = ALIGNMENT.runs();
|
|
806
|
-
let v = ALIGNMENT.check(
|
|
829
|
+
let v = ALIGNMENT.check(bm, rVert, { x: int(xx), y }, { y: 1, x: 0 }, 2 * runs[1]);
|
|
807
830
|
if (v === false)
|
|
808
831
|
return;
|
|
809
832
|
v += y;
|
|
@@ -826,6 +849,7 @@ function findAlignment(b, est, allowanceFactor) {
|
|
|
826
849
|
throw new Error('Alignment pattern not found');
|
|
827
850
|
}
|
|
828
851
|
function _single(b, from, to) {
|
|
852
|
+
const bm = b;
|
|
829
853
|
// http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
|
|
830
854
|
let steep = false;
|
|
831
855
|
let d = { x: Math.abs(to.x - from.x), y: Math.abs(to.y - from.y) };
|
|
@@ -844,8 +868,10 @@ function _single(b, from, to) {
|
|
|
844
868
|
let real = { x, y };
|
|
845
869
|
if (steep)
|
|
846
870
|
real = pointMirror(real);
|
|
847
|
-
//
|
|
848
|
-
|
|
871
|
+
// Starting from a dark finder center, walk until the ray crosses
|
|
872
|
+
// light -> dark -> light; `BWBRunLength()` mirrors this to recover the
|
|
873
|
+
// full finder width around the center point.
|
|
874
|
+
if ((runPos === 1) === !!bm.point(real)) {
|
|
849
875
|
if (runPos === 2)
|
|
850
876
|
return distance({ x, y }, from);
|
|
851
877
|
runPos++;
|
|
@@ -863,11 +889,12 @@ function _single(b, from, to) {
|
|
|
863
889
|
return NaN;
|
|
864
890
|
}
|
|
865
891
|
function BWBRunLength(b, from, to) {
|
|
866
|
-
|
|
892
|
+
const bm = b;
|
|
893
|
+
let result = _single(bm, from, to);
|
|
867
894
|
let scaleY = 1.0;
|
|
868
895
|
const { x: fx, y: fy } = from;
|
|
869
896
|
let otherToX = fx - (to.x - fx);
|
|
870
|
-
const bw =
|
|
897
|
+
const bw = bm.width;
|
|
871
898
|
if (otherToX < 0) {
|
|
872
899
|
scaleY = fx / (fx - otherToX);
|
|
873
900
|
otherToX = 0;
|
|
@@ -876,9 +903,15 @@ function BWBRunLength(b, from, to) {
|
|
|
876
903
|
scaleY = (bw - 1 - fx) / (otherToX - fx);
|
|
877
904
|
otherToX = bw - 1;
|
|
878
905
|
}
|
|
906
|
+
// ISO/IEC 18004:2024 §12 b) uses finder runs in the 1:1:3:1:1 ratio,
|
|
907
|
+
// and §12 h)1 derives module size from finder pattern width. Clipping the
|
|
908
|
+
// reflected ray before `int()` would avoid `>>> 0` wrapping negative near-edge
|
|
909
|
+
// coordinates to a huge uint32 and clamping to the wrong edge. Policy: keep
|
|
910
|
+
// the existing heuristic because the direct fix degraded decode performance
|
|
911
|
+
// on current vectors (BoofCV sweep 134/485 -> 132/485).
|
|
879
912
|
let otherToY = int(fy - (to.y - fy) * scaleY);
|
|
880
913
|
let scaleX = 1.0;
|
|
881
|
-
const bh =
|
|
914
|
+
const bh = bm.height;
|
|
882
915
|
if (otherToY < 0) {
|
|
883
916
|
scaleX = fy / (fy - otherToY);
|
|
884
917
|
otherToY = 0;
|
|
@@ -888,12 +921,16 @@ function BWBRunLength(b, from, to) {
|
|
|
888
921
|
otherToY = bh - 1;
|
|
889
922
|
}
|
|
890
923
|
otherToX = int(fx + (otherToX - fx) * scaleX);
|
|
891
|
-
result += _single(
|
|
924
|
+
result += _single(bm, from, { x: otherToX, y: otherToY });
|
|
925
|
+
// Both mirrored rays include the center module once, so drop one module
|
|
926
|
+
// after summing them into the full finder-width estimate.
|
|
892
927
|
return result - 1.0;
|
|
893
928
|
}
|
|
894
929
|
function moduleSizeAvg(b, p1, p2) {
|
|
895
930
|
const est1 = BWBRunLength(b, pointInt(p1), pointInt(p2));
|
|
896
931
|
const est2 = BWBRunLength(b, pointInt(p2), pointInt(p1));
|
|
932
|
+
// One ray can fail near image edges, so keep the surviving estimate
|
|
933
|
+
// instead of discarding the finder-width measurement outright.
|
|
897
934
|
if (Number.isNaN(est1))
|
|
898
935
|
return est2 / FINDER.totalSize;
|
|
899
936
|
if (Number.isNaN(est2))
|
|
@@ -901,27 +938,34 @@ function moduleSizeAvg(b, p1, p2) {
|
|
|
901
938
|
return (est1 + est2) / (2 * FINDER.totalSize);
|
|
902
939
|
}
|
|
903
940
|
function detect(b) {
|
|
941
|
+
const bm = b;
|
|
904
942
|
let bl, tl, tr;
|
|
905
943
|
try {
|
|
906
|
-
({ bl, tl, tr } = findFinder(
|
|
944
|
+
({ bl, tl, tr } = findFinder(bm));
|
|
907
945
|
}
|
|
908
946
|
catch (e) {
|
|
909
947
|
try {
|
|
910
|
-
b
|
|
911
|
-
|
|
948
|
+
// ISO/IEC 18004:2024 §12 b)5 says to "reverse the colouring of the
|
|
949
|
+
// light and dark pixels" for reflectance reversal. `detect()` works on
|
|
950
|
+
// the `decodeQR()`-owned scratch bitmap, so keep the retry in-place for
|
|
951
|
+
// performance instead of cloning before this private/test-only helper.
|
|
952
|
+
bm.negate();
|
|
953
|
+
({ bl, tl, tr } = findFinder(bm));
|
|
912
954
|
}
|
|
913
955
|
catch (e) {
|
|
914
|
-
|
|
956
|
+
bm.negate(); // undo negate
|
|
915
957
|
throw e;
|
|
916
958
|
}
|
|
917
959
|
}
|
|
918
|
-
const moduleSize = (moduleSizeAvg(
|
|
960
|
+
const moduleSize = (moduleSizeAvg(bm, tl, tr) + moduleSizeAvg(bm, tl, bl)) / 2;
|
|
919
961
|
if (moduleSize < 1.0)
|
|
920
962
|
throw new Error(`invalid moduleSize = ${moduleSize}`);
|
|
921
963
|
// Estimate size
|
|
922
964
|
const tltr = int(distance(tl, tr) / moduleSize + 0.5);
|
|
923
965
|
const tlbl = int(distance(tl, bl) / moduleSize + 0.5);
|
|
924
966
|
let size = int((tltr + tlbl) / 2 + 7);
|
|
967
|
+
// QR side lengths are 21 + 4 * (version - 1), so normalize the estimate
|
|
968
|
+
// to the nearest size that is 1 modulo 4 before decoding the version.
|
|
925
969
|
const rem = size % 4;
|
|
926
970
|
if (rem === 0)
|
|
927
971
|
size++; // -> 1
|
|
@@ -929,13 +973,13 @@ function detect(b) {
|
|
|
929
973
|
size--; // -> 1
|
|
930
974
|
else if (rem === 3)
|
|
931
975
|
size -= 2;
|
|
932
|
-
const version = info.size.decode(size);
|
|
933
|
-
validateVersion(version);
|
|
976
|
+
const version = utils.info.size.decode(size);
|
|
977
|
+
utils.validateVersion(version);
|
|
934
978
|
let alignmentPattern;
|
|
935
|
-
if (info.alignmentPatterns(version).length > 0) {
|
|
979
|
+
if (utils.info.alignmentPatterns(version).length > 0) {
|
|
936
980
|
// Bottom right estimate
|
|
937
981
|
const br = { x: tr.x - tl.x + bl.x, y: tr.y - tl.y + bl.y };
|
|
938
|
-
const c = 1.0 - 3.0 / (info.size.encode(version) - 7);
|
|
982
|
+
const c = 1.0 - 3.0 / (utils.info.size.encode(version) - 7);
|
|
939
983
|
// Estimated alignment pattern position
|
|
940
984
|
const est = {
|
|
941
985
|
x: int(tl.x + c * (br.x - tl.x)),
|
|
@@ -945,7 +989,7 @@ function detect(b) {
|
|
|
945
989
|
};
|
|
946
990
|
for (let i = 4; i <= 16; i <<= 1) {
|
|
947
991
|
try {
|
|
948
|
-
alignmentPattern = findAlignment(
|
|
992
|
+
alignmentPattern = findAlignment(bm, est, i);
|
|
949
993
|
break;
|
|
950
994
|
}
|
|
951
995
|
catch (e) { }
|
|
@@ -965,13 +1009,15 @@ function detect(b) {
|
|
|
965
1009
|
toBR = { x: size - 3.5, y: size - 3.5 };
|
|
966
1010
|
}
|
|
967
1011
|
const from = [tl, tr, br, bl];
|
|
968
|
-
const bits = transform(
|
|
1012
|
+
const bits = transform(bm, size, from, [toTL, toTR, toBR, toBL]);
|
|
969
1013
|
return { bits: bits, points: from };
|
|
970
1014
|
}
|
|
971
1015
|
// Perspective transform by 4 points
|
|
972
1016
|
function squareToQuadrilateral(p) {
|
|
973
1017
|
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 };
|
|
974
1018
|
if (d3.x === 0.0 && d3.y === 0.0) {
|
|
1019
|
+
// Parallelogram fast path: perspective terms vanish, so the homography
|
|
1020
|
+
// reduces to an affine transform.
|
|
975
1021
|
return [
|
|
976
1022
|
[p[1].x - p[0].x, p[2].x - p[1].x, p[0].x],
|
|
977
1023
|
[p[1].y - p[0].y, p[2].y - p[1].y, p[0].y],
|
|
@@ -993,9 +1039,12 @@ function squareToQuadrilateral(p) {
|
|
|
993
1039
|
}
|
|
994
1040
|
// Transform quadrilateral to square by 4 points
|
|
995
1041
|
function transform(b, size, from, to) {
|
|
1042
|
+
const bm = b;
|
|
996
1043
|
// TODO: check
|
|
997
1044
|
// https://math.stackexchange.com/questions/13404/mapping-irregular-quadrilateral-to-a-rectangle
|
|
998
1045
|
const p = squareToQuadrilateral(to);
|
|
1046
|
+
// Homographies are scale-invariant, so the adjugate is enough here;
|
|
1047
|
+
// there is no need to divide by the determinant when inverting `p`.
|
|
999
1048
|
const qToS = [
|
|
1000
1049
|
[
|
|
1001
1050
|
p[1][1] * p[2][2] - p[2][1] * p[1][2],
|
|
@@ -1016,7 +1065,7 @@ function transform(b, size, from, to) {
|
|
|
1016
1065
|
const sToQ = squareToQuadrilateral(from);
|
|
1017
1066
|
const transform = sToQ.map((i) => i.map((_, qx) => i.reduce((acc, v, j) => acc + v * qToS[j][qx], 0)));
|
|
1018
1067
|
const res = new Bitmap(size);
|
|
1019
|
-
const points = fillArr(2 * size, 0);
|
|
1068
|
+
const points = utils.fillArr(2 * size, 0);
|
|
1020
1069
|
const pointsLength = points.length;
|
|
1021
1070
|
for (let y = 0; y < size; y++) {
|
|
1022
1071
|
const p = transform;
|
|
@@ -1024,13 +1073,15 @@ function transform(b, size, from, to) {
|
|
|
1024
1073
|
const x = i / 2 + 0.5;
|
|
1025
1074
|
const y2 = y + 0.5;
|
|
1026
1075
|
const den = p[2][0] * x + p[2][1] * y2 + p[2][2];
|
|
1027
|
-
|
|
1028
|
-
|
|
1076
|
+
// ISO/IEC 18004:2024 §12 h) maps sampling grid intersections back to
|
|
1077
|
+
// image coordinates before deciding dark/light state. Clip projected
|
|
1078
|
+
// coordinates before `int()` because `>>> 0` wraps negative samples to a
|
|
1079
|
+
// huge uint32 and would clamp them to the far image edge.
|
|
1080
|
+
points[i] = int(cap((p[0][0] * x + p[0][1] * y2 + p[0][2]) / den, 0, bm.width - 1));
|
|
1081
|
+
points[i + 1] = int(cap((p[1][0] * x + p[1][1] * y2 + p[1][2]) / den, 0, bm.height - 1));
|
|
1029
1082
|
}
|
|
1030
1083
|
for (let i = 0; i < pointsLength; i += 2) {
|
|
1031
|
-
|
|
1032
|
-
const py = cap(points[i + 1], 0, b.height - 1);
|
|
1033
|
-
if (b.get(px, py))
|
|
1084
|
+
if (bm.get(points[i], points[i + 1]))
|
|
1034
1085
|
res.set((i / 2) | 0, y, true);
|
|
1035
1086
|
}
|
|
1036
1087
|
}
|
|
@@ -1039,8 +1090,11 @@ function transform(b, size, from, to) {
|
|
|
1039
1090
|
// Same as in drawTemplate, but reading
|
|
1040
1091
|
// TODO: merge in CoderType?
|
|
1041
1092
|
function readInfoBits(b) {
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1093
|
+
const bm = b;
|
|
1094
|
+
// Walk each reserved copy from the highest-numbered module back to module 0
|
|
1095
|
+
// so the shift accumulator rebuilds the canonical bit string from MSB to LSB.
|
|
1096
|
+
const readBit = (x, y, out) => (out << 1) | (bm.get(x, y) ? 1 : 0);
|
|
1097
|
+
const size = bm.height;
|
|
1044
1098
|
// Version information
|
|
1045
1099
|
let version1 = 0;
|
|
1046
1100
|
for (let y = 5; y >= 0; y--)
|
|
@@ -1067,56 +1121,62 @@ function readInfoBits(b) {
|
|
|
1067
1121
|
return { version1, version2, format1, format2 };
|
|
1068
1122
|
}
|
|
1069
1123
|
function parseInfo(b) {
|
|
1124
|
+
const bm = b;
|
|
1070
1125
|
// Population count over xor -> hamming distance
|
|
1071
|
-
const size =
|
|
1072
|
-
const { version1, version2, format1, format2 } = readInfoBits(
|
|
1126
|
+
const size = bm.height;
|
|
1127
|
+
const { version1, version2, format1, format2 } = readInfoBits(bm);
|
|
1073
1128
|
// Guess format
|
|
1074
1129
|
let format;
|
|
1075
|
-
const bestFormat = best();
|
|
1130
|
+
const bestFormat = utils.best();
|
|
1076
1131
|
for (const ecc of ['medium', 'low', 'high', 'quartile']) {
|
|
1077
1132
|
for (let mask = 0; mask < 8; mask++) {
|
|
1078
|
-
const bits = info.formatBits(ecc, mask);
|
|
1133
|
+
const bits = utils.info.formatBits(ecc, mask);
|
|
1079
1134
|
const cur = { ecc, mask: mask };
|
|
1080
1135
|
if (bits === format1 || bits === format2) {
|
|
1081
1136
|
format = cur;
|
|
1082
1137
|
break;
|
|
1083
1138
|
}
|
|
1084
|
-
bestFormat.add(popcnt(format1 ^ bits), cur);
|
|
1139
|
+
bestFormat.add(utils.popcnt(format1 ^ bits), cur);
|
|
1085
1140
|
if (format1 !== format2)
|
|
1086
|
-
bestFormat.add(popcnt(format2 ^ bits), cur);
|
|
1141
|
+
bestFormat.add(utils.popcnt(format2 ^ bits), cur);
|
|
1087
1142
|
}
|
|
1088
1143
|
}
|
|
1089
1144
|
if (format === undefined && bestFormat.score() <= MAX_BITS_ERROR)
|
|
1090
1145
|
format = bestFormat.get();
|
|
1091
1146
|
if (format === undefined)
|
|
1092
1147
|
throw new Error('invalid format pattern');
|
|
1093
|
-
let version = info.size.decode(size); // Guess version based on bitmap size
|
|
1148
|
+
let version = utils.info.size.decode(size); // Guess version based on bitmap size
|
|
1149
|
+
// Versions 1-6 do not carry version-information words, so side length is
|
|
1150
|
+
// the only authoritative version source until version 7 adds those fields.
|
|
1094
1151
|
if (version < 7)
|
|
1095
|
-
validateVersion(version);
|
|
1152
|
+
utils.validateVersion(version);
|
|
1096
1153
|
else {
|
|
1097
1154
|
version = undefined;
|
|
1098
1155
|
// Guess version
|
|
1099
|
-
const bestVer = best();
|
|
1156
|
+
const bestVer = utils.best();
|
|
1100
1157
|
for (let ver = 7; ver <= 40; ver++) {
|
|
1101
|
-
const bits = info.versionBits(ver);
|
|
1158
|
+
const bits = utils.info.versionBits(ver);
|
|
1102
1159
|
if (bits === version1 || bits === version2) {
|
|
1103
1160
|
version = ver;
|
|
1104
1161
|
break;
|
|
1105
1162
|
}
|
|
1106
|
-
bestVer.add(popcnt(version1 ^ bits), ver);
|
|
1163
|
+
bestVer.add(utils.popcnt(version1 ^ bits), ver);
|
|
1107
1164
|
if (version1 !== version2)
|
|
1108
|
-
bestVer.add(popcnt(version2 ^ bits), ver);
|
|
1165
|
+
bestVer.add(utils.popcnt(version2 ^ bits), ver);
|
|
1109
1166
|
}
|
|
1110
1167
|
if (version === undefined && bestVer.score() <= MAX_BITS_ERROR)
|
|
1111
1168
|
version = bestVer.get();
|
|
1112
1169
|
if (version === undefined)
|
|
1113
1170
|
throw new Error('invalid version pattern');
|
|
1114
|
-
if (info.size.encode(version) !== size)
|
|
1171
|
+
if (utils.info.size.encode(version) !== size)
|
|
1115
1172
|
throw new Error('invalid version size');
|
|
1116
1173
|
}
|
|
1117
1174
|
return { version, ...format };
|
|
1118
1175
|
}
|
|
1119
|
-
//
|
|
1176
|
+
// ISO/IEC 18004:2024 §7.4.3.2 says each ECI is a "6-digit assignment
|
|
1177
|
+
// number"; §7.4.3.4 says invoked ECIs apply until "a change of ECI".
|
|
1178
|
+
// Decode through platform TextDecoder only: unsupported labels may throw on
|
|
1179
|
+
// some runtimes, and callers needing them can provide a custom textDecoder.
|
|
1120
1180
|
const eciToEncoding = {
|
|
1121
1181
|
1: 'iso-8859-1',
|
|
1122
1182
|
2: 'ibm437',
|
|
@@ -1146,26 +1206,30 @@ const eciToEncoding = {
|
|
|
1146
1206
|
30: 'euc-kr',
|
|
1147
1207
|
};
|
|
1148
1208
|
function decodeWithEci(bytes, eci = 26) {
|
|
1209
|
+
// ISO/IEC 18004:2024 §7.3.2 says QR's "default interpretation" is
|
|
1210
|
+
// "ECI 000003 representing the ISO/IEC 8859-1 character set". Keep UTF-8
|
|
1211
|
+
// here so this library's UTF-8 byte-mode encoder round-trips without ECI.
|
|
1149
1212
|
const encoding = eciToEncoding[eci];
|
|
1150
1213
|
if (!encoding)
|
|
1151
1214
|
throw new Error(`Unsupported ECI: ${eci}`);
|
|
1152
1215
|
return new TextDecoder(encoding).decode(bytes);
|
|
1153
1216
|
}
|
|
1154
1217
|
function decodeBitmap(b, decoder = decodeWithEci) {
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1218
|
+
const bm = b;
|
|
1219
|
+
const size = bm.height;
|
|
1220
|
+
if (size < 21 || (size & 0b11) !== 1 || size !== bm.width)
|
|
1157
1221
|
throw new Error(`decode: invalid size=${size}`);
|
|
1158
|
-
const { version, mask, ecc } = parseInfo(
|
|
1159
|
-
const tpl = drawTemplate(version, ecc, mask);
|
|
1160
|
-
const { total } = info.capacity(version, ecc);
|
|
1222
|
+
const { version, mask, ecc } = parseInfo(bm);
|
|
1223
|
+
const tpl = utils.drawTemplate(version, ecc, mask);
|
|
1224
|
+
const { total } = utils.info.capacity(version, ecc);
|
|
1161
1225
|
const bytes = new Uint8Array(total);
|
|
1162
1226
|
let pos = 0;
|
|
1163
1227
|
let buf = 0;
|
|
1164
1228
|
let bitPos = 0;
|
|
1165
|
-
zigzag(tpl, mask, (x, y, m) => {
|
|
1229
|
+
utils.zigzag(tpl, mask, (x, y, m) => {
|
|
1166
1230
|
bitPos++;
|
|
1167
1231
|
buf <<= 1;
|
|
1168
|
-
buf |= +(!!
|
|
1232
|
+
buf |= +(!!bm.get(x, y) !== m);
|
|
1169
1233
|
if (bitPos !== 8)
|
|
1170
1234
|
return;
|
|
1171
1235
|
bytes[pos++] = buf;
|
|
@@ -1174,8 +1238,8 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
1174
1238
|
});
|
|
1175
1239
|
if (pos !== total)
|
|
1176
1240
|
throw new Error(`decode: pos=${pos}, total=${total}`);
|
|
1177
|
-
let bits = Array.from(interleave(version, ecc).decode(bytes))
|
|
1178
|
-
.map((i) => bin(i, 8))
|
|
1241
|
+
let bits = Array.from(utils.interleave(version, ecc).decode(bytes))
|
|
1242
|
+
.map((i) => utils.bin(i, 8))
|
|
1179
1243
|
.join('');
|
|
1180
1244
|
// Reverse operation of index.ts/encode working on bits
|
|
1181
1245
|
const readBits = (n) => {
|
|
@@ -1196,6 +1260,9 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
1196
1260
|
'1000': 'kanji',
|
|
1197
1261
|
};
|
|
1198
1262
|
let res = '';
|
|
1263
|
+
// ISO/IEC 18004:2024 §7.3.2 defines no-ECI byte data as ECI 000003
|
|
1264
|
+
// / ISO-8859-1. Keep ECI 26 for compatibility with legacy no-ECI UTF-8
|
|
1265
|
+
// payloads and with encodeQR's UTF-8 byte-mode default.
|
|
1199
1266
|
let eci = 26; // Default to utf-8 for compat with old behavior
|
|
1200
1267
|
while (true) {
|
|
1201
1268
|
if (bits.length < 4)
|
|
@@ -1206,7 +1273,7 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
1206
1273
|
throw new Error(`Unknown modeBits=${modeBits} res="${res}"`);
|
|
1207
1274
|
if (mode === 'terminator')
|
|
1208
1275
|
break;
|
|
1209
|
-
const countBits = info.lengthBits(version, mode);
|
|
1276
|
+
const countBits = utils.info.lengthBits(version, mode);
|
|
1210
1277
|
let count = toNum(readBits(countBits));
|
|
1211
1278
|
if (mode === 'numeric') {
|
|
1212
1279
|
while (count >= 3) {
|
|
@@ -1232,11 +1299,11 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
1232
1299
|
else if (mode === 'alphanumeric') {
|
|
1233
1300
|
while (count >= 2) {
|
|
1234
1301
|
const v = toNum(readBits(11));
|
|
1235
|
-
res += info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
|
|
1302
|
+
res += utils.info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
|
|
1236
1303
|
count -= 2;
|
|
1237
1304
|
}
|
|
1238
1305
|
if (count === 1)
|
|
1239
|
-
res += info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
|
|
1306
|
+
res += utils.info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
|
|
1240
1307
|
}
|
|
1241
1308
|
else if (mode === 'eci') {
|
|
1242
1309
|
const first = toNum(readBits(8));
|
|
@@ -1254,15 +1321,24 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
1254
1321
|
data[i] = toNum(readBits(8));
|
|
1255
1322
|
res += decoder(data, eci);
|
|
1256
1323
|
}
|
|
1324
|
+
else if (mode === 'kanji') {
|
|
1325
|
+
// ISO/IEC 18004:2024 §7.3.6 says Kanji mode uses Shift JIS and
|
|
1326
|
+
// "Each two-byte character value is compacted to a 13-bit binary
|
|
1327
|
+
// codeword." Keep it unsupported until a real interop fixture can verify
|
|
1328
|
+
// the 13-bit-to-Shift-JIS mapping rather than adding untested decoding.
|
|
1329
|
+
throw new Error('Kanji mode is not supported');
|
|
1330
|
+
}
|
|
1257
1331
|
else
|
|
1258
1332
|
throw new Error(`Unknown mode=${mode}`);
|
|
1259
1333
|
}
|
|
1260
1334
|
return res;
|
|
1261
1335
|
}
|
|
1262
|
-
//
|
|
1336
|
+
// Center-crop to a square and return the original-image offset so
|
|
1337
|
+
// `decodeQR()` can map detected points back into caller coordinates.
|
|
1263
1338
|
function cropToSquare(img) {
|
|
1264
|
-
const
|
|
1265
|
-
const
|
|
1339
|
+
const image = img;
|
|
1340
|
+
const data = Array.isArray(image.data) ? new Uint8Array(image.data) : image.data;
|
|
1341
|
+
const { height, width } = image;
|
|
1266
1342
|
const squareSize = Math.min(height, width);
|
|
1267
1343
|
const offset = {
|
|
1268
1344
|
x: Math.floor((width - squareSize) / 2),
|
|
@@ -1278,44 +1354,93 @@ function cropToSquare(img) {
|
|
|
1278
1354
|
}
|
|
1279
1355
|
return { offset, img: { height: squareSize, width: squareSize, data: croppedData } };
|
|
1280
1356
|
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Decode text from a QR image.
|
|
1359
|
+
* @param img - RGB or RGBA image data that contains a QR code.
|
|
1360
|
+
* @param opts - Decoder hooks and image preprocessing options. See {@link DecodeOpts}.
|
|
1361
|
+
* @returns Decoded QR payload as a string.
|
|
1362
|
+
* @throws If the image, decoder options, or QR contents are invalid. {@link Error}
|
|
1363
|
+
* @example
|
|
1364
|
+
* Decode text from a QR image.
|
|
1365
|
+
* ```ts
|
|
1366
|
+
* import encodeQR, { Bitmap } from 'qr';
|
|
1367
|
+
* import decodeQR from 'qr/decode.js';
|
|
1368
|
+
* const bits = encodeQR('Hello world', 'raw', { scale: 4 });
|
|
1369
|
+
* const bm = new Bitmap({ width: bits[0].length, height: bits.length }, bits);
|
|
1370
|
+
* const text = decodeQR(bm.toImage());
|
|
1371
|
+
* ```
|
|
1372
|
+
*/
|
|
1281
1373
|
export function decodeQR(img, opts = {}) {
|
|
1374
|
+
let image = img;
|
|
1375
|
+
const options = opts;
|
|
1282
1376
|
for (const field of ['height', 'width']) {
|
|
1283
|
-
if (!Number.isSafeInteger(
|
|
1284
|
-
throw new Error(`invalid img.${field}=${
|
|
1377
|
+
if (!Number.isSafeInteger(image[field]) || image[field] <= 0)
|
|
1378
|
+
throw new Error(`invalid img.${field}=${image[field]} (${typeof image[field]})`);
|
|
1285
1379
|
}
|
|
1286
|
-
const { data } =
|
|
1380
|
+
const { data } = image;
|
|
1287
1381
|
if (!Array.isArray(data) && !isBytes(data))
|
|
1288
1382
|
throw new Error(`invalid image.data=${data} (${typeof data})`);
|
|
1289
|
-
if (
|
|
1290
|
-
throw new Error(`invalid opts.cropToSquare=${
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1383
|
+
if (options.cropToSquare !== undefined && typeof options.cropToSquare !== 'boolean')
|
|
1384
|
+
throw new Error(`invalid opts.cropToSquare=${options.cropToSquare}`);
|
|
1385
|
+
// Validate callbacks before decoding so payload mode does not decide whether
|
|
1386
|
+
// an invalid public option is accepted.
|
|
1387
|
+
for (const fn of [
|
|
1388
|
+
'textDecoder',
|
|
1389
|
+
'pointsOnDetect',
|
|
1390
|
+
'imageOnBitmap',
|
|
1391
|
+
'imageOnDetect',
|
|
1392
|
+
'imageOnResult',
|
|
1393
|
+
]) {
|
|
1394
|
+
if (options[fn] !== undefined && typeof options[fn] !== 'function')
|
|
1395
|
+
throw new Error(`invalid opts.${fn}=${options[fn]} (${typeof options[fn]})`);
|
|
1294
1396
|
}
|
|
1295
1397
|
let offset = { x: 0, y: 0 };
|
|
1296
|
-
if (
|
|
1297
|
-
({ img, offset } = cropToSquare(
|
|
1298
|
-
const bmp = toBitmap(
|
|
1299
|
-
if (
|
|
1300
|
-
|
|
1398
|
+
if (options.cropToSquare)
|
|
1399
|
+
({ img: image, offset } = cropToSquare(image));
|
|
1400
|
+
const bmp = toBitmap(image);
|
|
1401
|
+
if (options.imageOnBitmap)
|
|
1402
|
+
options.imageOnBitmap(bmp.toImage());
|
|
1301
1403
|
const { bits, points } = detect(bmp);
|
|
1302
|
-
if (
|
|
1404
|
+
if (options.pointsOnDetect) {
|
|
1405
|
+
// Report finder points in the caller's original coordinate space after any center-crop.
|
|
1303
1406
|
const p = points.map((i) => ({ ...i, ...pointAdd(i, offset) }));
|
|
1304
|
-
|
|
1407
|
+
options.pointsOnDetect(p);
|
|
1305
1408
|
}
|
|
1306
|
-
if (
|
|
1307
|
-
|
|
1308
|
-
const res = decodeBitmap(bits,
|
|
1309
|
-
if (
|
|
1310
|
-
|
|
1409
|
+
if (options.imageOnDetect)
|
|
1410
|
+
options.imageOnDetect(bits.toImage());
|
|
1411
|
+
const res = decodeBitmap(bits, options.textDecoder);
|
|
1412
|
+
if (options.imageOnResult)
|
|
1413
|
+
options.imageOnResult(bits.toImage());
|
|
1311
1414
|
return res;
|
|
1312
1415
|
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Default export alias for {@link decodeQR}.
|
|
1418
|
+
* @param img - RGB or RGBA image data that contains a QR code.
|
|
1419
|
+
* @param opts - Decoder hooks and image preprocessing options. See {@link DecodeOpts}.
|
|
1420
|
+
* @returns Decoded QR payload as a string.
|
|
1421
|
+
* @throws If the image, decoder options, or QR contents are invalid. {@link Error}
|
|
1422
|
+
* @example
|
|
1423
|
+
* Decode a rendered QR image via the default decoder export.
|
|
1424
|
+
* ```ts
|
|
1425
|
+
* import encodeQR, { Bitmap } from 'qr';
|
|
1426
|
+
* import decodeQR from 'qr/decode.js';
|
|
1427
|
+
* const bits = encodeQR('Hello world', 'raw', { scale: 4 });
|
|
1428
|
+
* const bm = new Bitmap({ width: bits[0].length, height: bits.length }, bits);
|
|
1429
|
+
* decodeQR(bm.toImage());
|
|
1430
|
+
* ```
|
|
1431
|
+
*/
|
|
1313
1432
|
export default decodeQR;
|
|
1433
|
+
// Additional private helpers for focused regression tests; separate from the
|
|
1434
|
+
// existing `_tests` object so its current keys remain stable.
|
|
1435
|
+
export const _TESTS = /* @__PURE__ */ Object.freeze({
|
|
1436
|
+
findAlignment: findAlignment,
|
|
1437
|
+
transform: transform,
|
|
1438
|
+
});
|
|
1314
1439
|
// Unsafe API utils, exported only for tests
|
|
1315
|
-
export const _tests = {
|
|
1440
|
+
export const _tests = /* @__PURE__ */ Object.freeze({
|
|
1316
1441
|
toBitmap,
|
|
1317
1442
|
decodeBitmap,
|
|
1318
1443
|
findFinder,
|
|
1319
1444
|
detect,
|
|
1320
|
-
};
|
|
1445
|
+
});
|
|
1321
1446
|
//# sourceMappingURL=decode.js.map
|