qr 0.5.2 → 0.5.3

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/src/dom.ts CHANGED
@@ -62,6 +62,7 @@ export type QRCanvasOpts = {
62
62
  overlaySideColor: string;
63
63
  overlayTimeout: number; // how must time from last detect until hide overlay stuff
64
64
  cropToSquare: boolean; // crop image to square
65
+ textDecoder?: (bytes: Uint8Array) => string;
65
66
  };
66
67
 
67
68
  export type QRCanvasElements = {
@@ -182,7 +183,10 @@ export class QRCanvas {
182
183
  const { context } = this.main;
183
184
  context.drawImage(image, 0, 0, width, height);
184
185
  const data = context.getImageData(0, 0, width, height);
185
- const options: DecodeOpts = { cropToSquare: this.opts.cropToSquare };
186
+ const options: DecodeOpts = {
187
+ cropToSquare: this.opts.cropToSquare,
188
+ textDecoder: this.opts.textDecoder,
189
+ };
186
190
  if (this.bitmap) options.imageOnBitmap = (img) => this.drawBitmap(img);
187
191
  if (this.overlay) options.pointsOnDetect = (points) => this.drawOverlay(points);
188
192
  if (this.resultQR) options.imageOnResult = (img) => this.drawResultQr(img);
package/src/index.ts CHANGED
@@ -32,6 +32,13 @@ const array = encodeQR(txt, 'raw'); // 2d array for canvas or other libs
32
32
  ```
33
33
  */
34
34
 
35
+ const R1_RUN_LENGTH_THRESHOLD = 5;
36
+ const R2_BLOCK_PENALTY = 3;
37
+ const R3_FINDER_PATTERN_LENGTH = 11;
38
+ const R3_FINDER_PENALTY = 40;
39
+ const R4_BALANCE_STEP_PERCENT = 5;
40
+ const R4_BALANCE_STEP_POINTS = 10;
41
+
35
42
  // We do not use newline escape code directly in strings because it's not parser-friendly
36
43
  const chCodes = { newline: 10, reset: 27 };
37
44
 
@@ -67,24 +74,25 @@ function fillArr<T>(length: number, val: T): T[] {
67
74
  * @param blocks [[1, 2, 3], [4, 5, 6]]
68
75
  * @returns [1, 4, 2, 5, 3, 6]
69
76
  */
70
- function interleaveBytes(...blocks: Uint8Array[]): Uint8Array {
71
- let len = 0;
72
- for (const b of blocks) len = Math.max(len, b.length);
73
- const res = [];
74
- for (let i = 0; i < len; i++) {
75
- for (const b of blocks) {
76
- if (i >= b.length) continue; // outside of block, skip
77
- res.push(b[i]);
77
+ function interleaveBytes(blocks: Uint8Array[]): Uint8Array {
78
+ let maxLen = 0;
79
+ let totalLen = 0;
80
+ for (const block of blocks) {
81
+ maxLen = Math.max(maxLen, block.length);
82
+ totalLen += block.length;
83
+ }
84
+
85
+ const result = new Uint8Array(totalLen);
86
+ let idx = 0;
87
+ for (let i = 0; i < maxLen; i++) {
88
+ for (const block of blocks) {
89
+ if (i < block.length) result[idx++] = block[i];
78
90
  }
79
91
  }
80
- return new Uint8Array(res);
81
- }
82
92
 
83
- function includesAt<T>(lst: T[], pattern: T[], index: number): boolean {
84
- if (index < 0 || index + pattern.length > lst.length) return false;
85
- for (let i = 0; i < pattern.length; i++) if (pattern[i] !== lst[index + i]) return false;
86
- return true;
93
+ return result;
87
94
  }
95
+
88
96
  // Optimize for minimal score/penalty
89
97
  function best<T>(): {
90
98
  add(score: number, value: T): void;
@@ -753,8 +761,8 @@ function interleave(ver: Version, ecc: ErrorCorrection): Coder<Uint8Array, Uint8
753
761
  eccBlocks.push(rs.encode(bytes.subarray(0, len)));
754
762
  bytes = bytes.subarray(len);
755
763
  }
756
- const resBlocks = interleaveBytes(...blocks);
757
- const resECC = interleaveBytes(...eccBlocks);
764
+ const resBlocks = interleaveBytes(blocks);
765
+ const resECC = interleaveBytes(eccBlocks);
758
766
  const res = new Uint8Array(resBlocks.length + resECC.length);
759
767
  res.set(resBlocks);
760
768
  res.set(resECC, resBlocks.length);
@@ -901,7 +909,13 @@ export function utf8ToBytes(str: string): Uint8Array {
901
909
  return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
902
910
  }
903
911
 
904
- function encode(ver: Version, ecc: ErrorCorrection, data: string, type: EncodingType): Uint8Array {
912
+ function encode(
913
+ ver: Version,
914
+ ecc: ErrorCorrection,
915
+ data: string,
916
+ type: EncodingType,
917
+ encoder: (value: string) => Uint8Array = utf8ToBytes
918
+ ): Uint8Array {
905
919
  let encoded = '';
906
920
  let dataLen = data.length;
907
921
  if (type === 'numeric') {
@@ -919,7 +933,7 @@ function encode(ver: Version, ecc: ErrorCorrection, data: string, type: Encoding
919
933
  for (let i = 0; i < n - 1; i += 2) encoded += bin(t[i] * 45 + t[i + 1], 11);
920
934
  if (n % 2 == 1) encoded += bin(t[n - 1], 6); // pad if odd number of chars
921
935
  } else if (type === 'byte') {
922
- const utf8 = utf8ToBytes(data);
936
+ const utf8 = encoder(data);
923
937
  dataLen = utf8.length;
924
938
  encoded = Array.from(utf8)
925
939
  .map((i) => bin(i, 8))
@@ -967,64 +981,189 @@ function drawQR(
967
981
  return b;
968
982
  }
969
983
 
970
- function penalty(bm: Bitmap): number {
971
- const inverse = bm.inverse();
972
- // Adjacent modules in row/column in same | No. of modules = (5 + i) color
973
- const sameColor = (row: DrawValue[]) => {
974
- let res = 0;
975
- for (let i = 0, same = 1, last = undefined; i < row.length; i++) {
976
- if (last === row[i]) {
977
- same++;
978
- if (i !== row.length - 1) continue; // handle last element
979
- }
980
- if (same >= 5) res += 3 + (same - 5);
981
- last = row[i];
982
- same = 1;
984
+ function calculateRowRunPenalty(rowBits: readonly boolean[]): number {
985
+ const moduleCount = rowBits.length;
986
+ if (moduleCount <= 1) return 0;
987
+
988
+ let penalty = 0;
989
+ let runLength = 1;
990
+ let previousColor = rowBits[0];
991
+
992
+ for (let i = 1; i < moduleCount; i++) {
993
+ const currentColor = rowBits[i];
994
+ if (currentColor === previousColor) {
995
+ runLength++;
996
+ } else {
997
+ if (runLength >= R1_RUN_LENGTH_THRESHOLD) penalty += runLength - 2;
998
+ runLength = 1;
999
+ previousColor = currentColor;
983
1000
  }
984
- return res;
985
- };
986
- let adjacent = 0;
987
- bm.data.forEach((row) => (adjacent += sameColor(row)));
988
- inverse.data.forEach((column) => (adjacent += sameColor(column)));
989
- // Block of modules in same color (Block size = 2x2)
990
- let box = 0;
991
- let b = bm.data;
992
- const lastW = bm.width - 1;
993
- const lastH = bm.height - 1;
994
- for (let x = 0; x < lastW; x++) {
995
- for (let y = 0; y < lastH; y++) {
996
- const x1 = x + 1;
997
- const y1 = y + 1;
998
- if (b[x][y] === b[x1][y] && b[x1][y] === b[x][y1] && b[x1][y] === b[x1][y1]) {
999
- box += 3;
1000
- }
1001
+ }
1002
+
1003
+ if (runLength >= R1_RUN_LENGTH_THRESHOLD) penalty += runLength - 2;
1004
+
1005
+ return penalty;
1006
+ }
1007
+
1008
+ function calculateColumnRunPenalty(
1009
+ bitmap: readonly boolean[][],
1010
+ columnIndex: number,
1011
+ columnHeight: number
1012
+ ): number {
1013
+ if (columnHeight <= 1) return 0;
1014
+
1015
+ let penalty = 0;
1016
+ let runLength = 1;
1017
+ let previousColor = bitmap[0][columnIndex];
1018
+
1019
+ for (let y = 1; y < columnHeight; y++) {
1020
+ const currentColor = bitmap[y][columnIndex];
1021
+ if (currentColor === previousColor) {
1022
+ runLength++;
1023
+ } else {
1024
+ if (runLength >= R1_RUN_LENGTH_THRESHOLD) penalty += runLength - 2;
1025
+ runLength = 1;
1026
+ previousColor = currentColor;
1001
1027
  }
1002
1028
  }
1003
- // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
1004
- const finderPattern = (row: DrawValue[]) => {
1005
- const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
1006
- const lightPattern = [false, false, false, false]; // light area 4 modules wide
1007
- const p1 = [...finderPattern, ...lightPattern];
1008
- const p2 = [...lightPattern, ...finderPattern];
1009
- let res = 0;
1010
- for (let i = 0; i < row.length; i++) {
1011
- if (includesAt(row, p1, i)) res += 40;
1012
- if (includesAt(row, p2, i)) res += 40;
1029
+
1030
+ if (runLength >= R1_RUN_LENGTH_THRESHOLD) penalty += runLength - 2;
1031
+
1032
+ return penalty;
1033
+ }
1034
+
1035
+ function calculateRowFinderPenalty(rowBits: readonly boolean[]): number {
1036
+ const rowLength = rowBits.length;
1037
+ if (rowLength < R3_FINDER_PATTERN_LENGTH) return 0;
1038
+
1039
+ let penalty = 0;
1040
+ const lastStart = rowLength - R3_FINDER_PATTERN_LENGTH;
1041
+
1042
+ for (let i = 0; i <= lastStart; i++) {
1043
+ // Case A: L L L L + 1 0 1 1 1 0 1
1044
+ const light4ThenFinder7 =
1045
+ !rowBits[i] &&
1046
+ !rowBits[i + 1] &&
1047
+ !rowBits[i + 2] &&
1048
+ !rowBits[i + 3] &&
1049
+ rowBits[i + 4] &&
1050
+ !rowBits[i + 5] &&
1051
+ rowBits[i + 6] &&
1052
+ rowBits[i + 7] &&
1053
+ rowBits[i + 8] &&
1054
+ !rowBits[i + 9] &&
1055
+ rowBits[i + 10];
1056
+
1057
+ // Case B: 1 0 1 1 1 0 1 + L L L L
1058
+ const finder7ThenLight4 =
1059
+ rowBits[i] &&
1060
+ !rowBits[i + 1] &&
1061
+ rowBits[i + 2] &&
1062
+ rowBits[i + 3] &&
1063
+ rowBits[i + 4] &&
1064
+ !rowBits[i + 5] &&
1065
+ rowBits[i + 6] &&
1066
+ !rowBits[i + 7] &&
1067
+ !rowBits[i + 8] &&
1068
+ !rowBits[i + 9] &&
1069
+ !rowBits[i + 10];
1070
+
1071
+ if (light4ThenFinder7 || finder7ThenLight4) penalty += R3_FINDER_PENALTY;
1072
+ }
1073
+ return penalty;
1074
+ }
1075
+
1076
+ function calculateColumnFinderPenalty(
1077
+ matrix: readonly boolean[][],
1078
+ rowIndex: number,
1079
+ width: number
1080
+ ): number {
1081
+ if (width < R3_FINDER_PATTERN_LENGTH) return 0;
1082
+
1083
+ let penalty = 0;
1084
+ const y = rowIndex;
1085
+ const lastStart = width - R3_FINDER_PATTERN_LENGTH;
1086
+
1087
+ for (let x = 0; x <= lastStart; x++) {
1088
+ // Case A: L L L L + 1 0 1 1 1 0 1
1089
+ const light4ThenFinder7 =
1090
+ !matrix[x][y] &&
1091
+ !matrix[x + 1][y] &&
1092
+ !matrix[x + 2][y] &&
1093
+ !matrix[x + 3][y] &&
1094
+ matrix[x + 4][y] &&
1095
+ !matrix[x + 5][y] &&
1096
+ matrix[x + 6][y] &&
1097
+ matrix[x + 7][y] &&
1098
+ matrix[x + 8][y] &&
1099
+ !matrix[x + 9][y] &&
1100
+ matrix[x + 10][y];
1101
+
1102
+ // Case B: 1 0 1 1 1 0 1 + L L L L
1103
+ const finder7ThenLight4 =
1104
+ matrix[x][y] &&
1105
+ !matrix[x + 1][y] &&
1106
+ matrix[x + 2][y] &&
1107
+ matrix[x + 3][y] &&
1108
+ matrix[x + 4][y] &&
1109
+ !matrix[x + 5][y] &&
1110
+ matrix[x + 6][y] &&
1111
+ !matrix[x + 7][y] &&
1112
+ !matrix[x + 8][y] &&
1113
+ !matrix[x + 9][y] &&
1114
+ !matrix[x + 10][y];
1115
+
1116
+ if (light4ThenFinder7 || finder7ThenLight4) penalty += R3_FINDER_PENALTY;
1117
+ }
1118
+ return penalty;
1119
+ }
1120
+
1121
+ function penalty(bitmap: Bitmap): number {
1122
+ const matrix = bitmap.data as boolean[][];
1123
+ const width = bitmap.width | 0;
1124
+ const height = bitmap.height | 0;
1125
+
1126
+ if (width === 0 || height === 0) return 0;
1127
+
1128
+ // Rule 1: same-color runs
1129
+ let runPenalty = 0;
1130
+ for (let x = 0; x < width; x++) runPenalty += calculateRowRunPenalty(matrix[x]);
1131
+ for (let y = 0; y < height; y++) runPenalty += calculateColumnRunPenalty(matrix, y, width);
1132
+
1133
+ // Rule 2: 2×2 blocks of the same color
1134
+ let blockPenalty = 0;
1135
+ const lastCol = width - 1;
1136
+ const lastRow = height - 1;
1137
+ for (let x = 0; x < lastCol; x++) {
1138
+ const col = matrix[x];
1139
+ const nextCol = matrix[x + 1];
1140
+ for (let y = 0; y < lastRow; y++) {
1141
+ const cell = col[y];
1142
+ if (cell === nextCol[y] && cell === col[y + 1] && cell === nextCol[y + 1]) {
1143
+ blockPenalty += R2_BLOCK_PENALTY;
1144
+ }
1013
1145
  }
1014
- return res;
1015
- };
1016
- let finder = 0;
1017
- for (const row of bm.data) finder += finderPattern(row);
1018
- for (const column of inverse.data) finder += finderPattern(column);
1019
- // Proportion of dark modules in entire symbol
1020
- // Add 10 points to a deviation of 5% increment or decrement in the proportion
1021
- // ratio of dark module from the referential 50%
1022
- let darkPixels = 0;
1023
- bm.rectRead(0, Infinity, (_c, val) => (darkPixels += val ? 1 : 0));
1024
- const darkPercent = (darkPixels / (bm.height * bm.width)) * 100;
1025
- const dark = 10 * Math.floor(Math.abs(darkPercent - 50) / 5);
1026
- return adjacent + box + finder + dark;
1146
+ }
1147
+
1148
+ // Rule 3: finder-like 1:1:3:1:1 with 4-light padding
1149
+ let finderPenalty = 0;
1150
+ for (let x = 0; x < width; x++) finderPenalty += calculateRowFinderPenalty(matrix[x]);
1151
+ for (let y = 0; y < height; y++) finderPenalty += calculateColumnFinderPenalty(matrix, y, width);
1152
+
1153
+ // Rule 4: dark-module balance vs 50%
1154
+ let darkCount = 0;
1155
+ for (let x = 0; x < width; x++) {
1156
+ const col = matrix[x];
1157
+ for (let y = 0; y < height; y++) if (col[y]) darkCount++;
1158
+ }
1159
+ const moduleCount = width * height;
1160
+ const darkPercent = (darkCount * 100) / moduleCount;
1161
+ const deviation = Math.abs(darkPercent - 50);
1162
+ const balancePenalty = R4_BALANCE_STEP_POINTS * Math.floor(deviation / R4_BALANCE_STEP_PERCENT);
1163
+
1164
+ return runPenalty + blockPenalty + finderPenalty + balancePenalty;
1027
1165
  }
1166
+
1028
1167
  // Selects best mask according to penalty, if no mask is provided
1029
1168
  function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskIdx?: Mask) {
1030
1169
  if (maskIdx === undefined) {
@@ -1041,6 +1180,7 @@ function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskId
1041
1180
  export type QrOpts = {
1042
1181
  ecc?: ErrorCorrection | undefined;
1043
1182
  encoding?: EncodingType | undefined;
1183
+ textEncoder?: (text: string) => Uint8Array;
1044
1184
  version?: Version | undefined;
1045
1185
  mask?: number | undefined;
1046
1186
  border?: number | undefined;
@@ -1112,13 +1252,13 @@ export function encodeQR(text: string, output: Output = 'raw', opts: QrOpts & Sv
1112
1252
  err = new Error('Unknown error');
1113
1253
  if (ver !== undefined) {
1114
1254
  validateVersion(ver);
1115
- data = encode(ver, ecc, text, encoding);
1255
+ data = encode(ver, ecc, text, encoding, opts.textEncoder);
1116
1256
  } else {
1117
1257
  // If no version is provided, try to find smallest one which fits
1118
1258
  // Currently just scans all version, can be significantly speedup if needed
1119
1259
  for (let i = 1; i <= 40; i++) {
1120
1260
  try {
1121
- data = encode(i, ecc, text, encoding);
1261
+ data = encode(i, ecc, text, encoding, opts.textEncoder);
1122
1262
  ver = i;
1123
1263
  break;
1124
1264
  } catch (e) {