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/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
- return Math.max(Math.min(value, max || value), min || value);
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 perPixel = img.data.length / (img.width * img.height);
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 width = img.width;
106
- const height = img.height;
107
- const data = img.data;
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
- const moduleSize = total / FINDER.totalSize;
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 (; b.isInside(i) && !!b.point(i) === res.pattern[p]; pointIncr(i, step)) {
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 = b.words;
551
- const vals = b.value;
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 !== b.width - 1)
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/light in 1:1:3:1:1 ratio
628
- const FINDER = pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
629
- // dark/light/dark in 1:1:1 ratio
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(b, runs, center, incr, maxCount);
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(b, dRuns, { x: int(x), y: int(y) }, { x: 1, y: 1 }))
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 * b.height) / (4 * 97)), DETECT_MIN_ROW_SKIP);
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 < b.height && !done; y += ySkip) {
682
- FINDER.scanLine(b, y, 0, b.width, (runs, x) => {
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, b.width - 1);
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, b.height - 1);
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
- const width = rightX - leftX;
791
- const height = bottomY - topY;
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(b, y, xStart, xEnd, (runs, x) => {
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(b, rVert, { x: int(xx), y }, { y: 1, x: 0 }, 2 * runs[1]);
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
- // Same as alignment pattern ([true, false, true])
848
- if ((runPos === 1) === !!b.point(real)) {
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
- let result = _single(b, from, to);
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 = b.width;
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 = b.height;
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(b, from, { x: otherToX, y: otherToY });
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(b));
944
+ ({ bl, tl, tr } = findFinder(bm));
907
945
  }
908
946
  catch (e) {
909
947
  try {
910
- b.negate();
911
- ({ bl, tl, tr } = findFinder(b));
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
- b.negate(); // undo negate
956
+ bm.negate(); // undo negate
915
957
  throw e;
916
958
  }
917
959
  }
918
- const moduleSize = (moduleSizeAvg(b, tl, tr) + moduleSizeAvg(b, tl, bl)) / 2;
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(b, est, i);
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(b, size, from, [toTL, toTR, toBR, toBL]);
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
- points[i] = int((p[0][0] * x + p[0][1] * y2 + p[0][2]) / den);
1028
- points[i + 1] = int((p[1][0] * x + p[1][1] * y2 + p[1][2]) / den);
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
- const px = cap(points[i], 0, b.width - 1);
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 readBit = (x, y, out) => (out << 1) | (b.get(x, y) ? 1 : 0);
1043
- const size = b.height;
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 = b.height;
1072
- const { version1, version2, format1, format2 } = readInfoBits(b);
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
- // Common encodings, please open issue if something popular missing
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 size = b.height;
1156
- if (size < 21 || (size & 0b11) !== 1 || size !== b.width)
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(b);
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 |= +(!!b.get(x, y) !== m);
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
- // Creates square from rectangle
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 data = Array.isArray(img.data) ? new Uint8Array(img.data) : img.data;
1265
- const { height, width } = img;
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(img[field]) || img[field] <= 0)
1284
- throw new Error(`invalid img.${field}=${img[field]} (${typeof 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 } = img;
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 (opts.cropToSquare !== undefined && typeof opts.cropToSquare !== 'boolean')
1290
- throw new Error(`invalid opts.cropToSquare=${opts.cropToSquare}`);
1291
- for (const fn of ['pointsOnDetect', 'imageOnBitmap', 'imageOnDetect', 'imageOnResult']) {
1292
- if (opts[fn] !== undefined && typeof opts[fn] !== 'function')
1293
- throw new Error(`invalid opts.${fn}=${opts[fn]} (${typeof opts[fn]})`);
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 (opts.cropToSquare)
1297
- ({ img, offset } = cropToSquare(img));
1298
- const bmp = toBitmap(img);
1299
- if (opts.imageOnBitmap)
1300
- opts.imageOnBitmap(bmp.toImage());
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 (opts.pointsOnDetect) {
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
- opts.pointsOnDetect(p);
1407
+ options.pointsOnDetect(p);
1305
1408
  }
1306
- if (opts.imageOnDetect)
1307
- opts.imageOnDetect(bits.toImage());
1308
- const res = decodeBitmap(bits, opts.textDecoder);
1309
- if (opts.imageOnResult)
1310
- opts.imageOnResult(bits.toImage());
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