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/README.md +1 -0
- package/decode.d.ts +2 -1
- package/decode.d.ts.map +1 -1
- package/decode.js +6 -3
- package/decode.js.map +1 -1
- package/dom.d.ts +1 -0
- package/dom.d.ts.map +1 -1
- package/dom.js +4 -1
- package/dom.js.map +1 -1
- package/index.d.ts +3 -2
- package/index.d.ts.map +1 -1
- package/index.js +184 -84
- package/index.js.map +1 -1
- package/package.json +2 -2
- package/src/decode.ts +7 -3
- package/src/dom.ts +5 -1
- package/src/index.ts +213 -73
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 = {
|
|
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(
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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(
|
|
757
|
-
const resECC = interleaveBytes(
|
|
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(
|
|
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 =
|
|
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
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
for (
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
//
|
|
1022
|
-
let
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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) {
|