specqr 1.0.0-rc.1
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/docs/api.md +226 -0
- package/docs/spec-scope.md +75 -0
- package/docs/test-plan.md +119 -0
- package/fixtures/decode-cases.json +116 -0
- package/fixtures/golden-cases.json +5363 -0
- package/package.json +67 -0
- package/src/browser.d.ts +8 -0
- package/src/browser.js +99 -0
- package/src/core/codewords.js +52 -0
- package/src/core/galois-field.js +41 -0
- package/src/core/mask.js +123 -0
- package/src/core/matrix.js +239 -0
- package/src/core/reed-solomon.js +41 -0
- package/src/core/tables.js +112 -0
- package/src/diagnostics.js +208 -0
- package/src/encoding/bit-buffer.js +54 -0
- package/src/encoding/modes.js +666 -0
- package/src/encoding/shift-jis.js +97 -0
- package/src/errors.js +61 -0
- package/src/gs1.js +167 -0
- package/src/index.d.ts +215 -0
- package/src/index.js +255 -0
- package/src/node.d.ts +7 -0
- package/src/node.js +26 -0
- package/src/options.js +117 -0
- package/src/render/base64.js +25 -0
- package/src/render/canvas.js +51 -0
- package/src/render/color.js +68 -0
- package/src/render/png.js +156 -0
- package/src/render/svg.js +38 -0
- package/tools/decode-vision.swift +24 -0
- package/tools/update-golden-fixtures.js +378 -0
- package/tools/verify-decode-optional.js +394 -0
- package/tools/verify-decode.js +84 -0
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specqr",
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
|
+
"description": "A dependency-free QR Code Model 2 generator for JavaScript.",
|
|
5
|
+
"homepage": "https://github.com/SpecQR/SpecQR#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/SpecQR/SpecQR/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/SpecQR/SpecQR.git"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./src/index.js",
|
|
15
|
+
"types": "./src/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./src/index.d.ts",
|
|
19
|
+
"import": "./src/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./browser": {
|
|
22
|
+
"types": "./src/browser.d.ts",
|
|
23
|
+
"import": "./src/browser.js"
|
|
24
|
+
},
|
|
25
|
+
"./node": {
|
|
26
|
+
"types": "./src/node.d.ts",
|
|
27
|
+
"import": "./src/node.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"docs",
|
|
33
|
+
"fixtures",
|
|
34
|
+
"tools",
|
|
35
|
+
"README.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test",
|
|
42
|
+
"fixtures:golden": "node tools/update-golden-fixtures.js",
|
|
43
|
+
"verify:decode": "node tools/verify-decode.js",
|
|
44
|
+
"verify:decode:optional": "node tools/verify-decode-optional.js",
|
|
45
|
+
"verify:decode:jsqr": "node tools/verify-decode-optional.js --only-jsqr --require-jsqr",
|
|
46
|
+
"verify:decode:independent": "npm run verify:decode:jsqr"
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"qr",
|
|
50
|
+
"qrcode",
|
|
51
|
+
"barcode",
|
|
52
|
+
"svg",
|
|
53
|
+
"png",
|
|
54
|
+
"kanji",
|
|
55
|
+
"shift-jis",
|
|
56
|
+
"gs1",
|
|
57
|
+
"eci",
|
|
58
|
+
"model2"
|
|
59
|
+
],
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"jsqr": "^1.4.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/browser.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { QRCodeOptions, QRInput, QRSegmentInput } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export function toBlob(input: QRInput, options?: QRCodeOptions): Blob;
|
|
4
|
+
export function toBlobFromSegments(segments: QRSegmentInput[], options?: QRCodeOptions): Blob;
|
|
5
|
+
export function toObjectURL(input: QRInput, options?: QRCodeOptions): string;
|
|
6
|
+
export function toObjectURLFromSegments(segments: QRSegmentInput[], options?: QRCodeOptions): string;
|
|
7
|
+
export function toImageData(input: QRInput, options?: QRCodeOptions): ImageData;
|
|
8
|
+
export function toImageDataFromSegments(segments: QRSegmentInput[], options?: QRCodeOptions): ImageData;
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { generate, generateSegments } from "./index.js";
|
|
2
|
+
import { InvalidInputError } from "./errors.js";
|
|
3
|
+
import { normalizeOptions } from "./options.js";
|
|
4
|
+
import { parseRgbaColor } from "./render/color.js";
|
|
5
|
+
|
|
6
|
+
export function toBlob(input, options = {}) {
|
|
7
|
+
assertBlobSupport();
|
|
8
|
+
const png = generate(input, {
|
|
9
|
+
...options,
|
|
10
|
+
output: "png",
|
|
11
|
+
diagnostics: false
|
|
12
|
+
});
|
|
13
|
+
return new Blob([png], { type: "image/png" });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toBlobFromSegments(segments, options = {}) {
|
|
17
|
+
assertBlobSupport();
|
|
18
|
+
const png = generateSegments(segments, {
|
|
19
|
+
...options,
|
|
20
|
+
output: "png",
|
|
21
|
+
diagnostics: false
|
|
22
|
+
});
|
|
23
|
+
return new Blob([png], { type: "image/png" });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toObjectURL(input, options = {}) {
|
|
27
|
+
assertObjectUrlSupport();
|
|
28
|
+
return URL.createObjectURL(toBlob(input, options));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toObjectURLFromSegments(segments, options = {}) {
|
|
32
|
+
assertObjectUrlSupport();
|
|
33
|
+
return URL.createObjectURL(toBlobFromSegments(segments, options));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toImageData(input, options = {}) {
|
|
37
|
+
const normalized = normalizeOptions({
|
|
38
|
+
...options,
|
|
39
|
+
output: "matrix",
|
|
40
|
+
diagnostics: false
|
|
41
|
+
});
|
|
42
|
+
const matrix = generate(input, normalized);
|
|
43
|
+
return matrixToImageData(matrix, normalized);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toImageDataFromSegments(segments, options = {}) {
|
|
47
|
+
const normalized = normalizeOptions({
|
|
48
|
+
...options,
|
|
49
|
+
output: "matrix",
|
|
50
|
+
diagnostics: false
|
|
51
|
+
});
|
|
52
|
+
const matrix = generateSegments(segments, normalized);
|
|
53
|
+
return matrixToImageData(matrix, normalized);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matrixToImageData(matrix, options) {
|
|
57
|
+
if (typeof ImageData !== "function") {
|
|
58
|
+
throw new InvalidInputError("ImageData is not available in this environment");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const size = matrix.length;
|
|
62
|
+
const dimension = (size + options.margin * 2) * options.scale;
|
|
63
|
+
const foreground = parseRgbaColor(options.foreground, "foreground", true);
|
|
64
|
+
const background = parseRgbaColor(options.background, "background", true);
|
|
65
|
+
const data = new Uint8ClampedArray(dimension * dimension * 4);
|
|
66
|
+
|
|
67
|
+
for (let y = 0; y < dimension; y += 1) {
|
|
68
|
+
const moduleY = Math.floor(y / options.scale) - options.margin;
|
|
69
|
+
for (let x = 0; x < dimension; x += 1) {
|
|
70
|
+
const moduleX = Math.floor(x / options.scale) - options.margin;
|
|
71
|
+
const dark =
|
|
72
|
+
moduleX >= 0 &&
|
|
73
|
+
moduleY >= 0 &&
|
|
74
|
+
moduleX < size &&
|
|
75
|
+
moduleY < size &&
|
|
76
|
+
matrix[moduleY][moduleX];
|
|
77
|
+
const color = dark ? foreground : background;
|
|
78
|
+
const offset = (y * dimension + x) * 4;
|
|
79
|
+
data[offset] = color[0];
|
|
80
|
+
data[offset + 1] = color[1];
|
|
81
|
+
data[offset + 2] = color[2];
|
|
82
|
+
data[offset + 3] = color[3];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new ImageData(data, dimension, dimension);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertBlobSupport() {
|
|
90
|
+
if (typeof Blob !== "function") {
|
|
91
|
+
throw new InvalidInputError("Blob is not available in this environment");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertObjectUrlSupport() {
|
|
96
|
+
if (typeof URL !== "function" || typeof URL.createObjectURL !== "function") {
|
|
97
|
+
throw new InvalidInputError("URL.createObjectURL is not available in this environment");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getErrorCorrectionBlockInfo } from "./tables.js";
|
|
2
|
+
import { computeRemainder, createGeneratorPolynomial } from "./reed-solomon.js";
|
|
3
|
+
|
|
4
|
+
export function interleaveCodewords(dataCodewords, version, errorCorrectionLevel) {
|
|
5
|
+
const info = getErrorCorrectionBlockInfo(version, errorCorrectionLevel);
|
|
6
|
+
if (dataCodewords.length !== info.dataCodewords) {
|
|
7
|
+
throw new Error(`Expected ${info.dataCodewords} data codewords; got ${dataCodewords.length}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const generator = createGeneratorPolynomial(info.eccPerBlock);
|
|
11
|
+
const numShortBlocks = info.blocks - (info.rawCodewords % info.blocks);
|
|
12
|
+
const shortBlockLength = Math.floor(info.rawCodewords / info.blocks);
|
|
13
|
+
const shortDataLength = shortBlockLength - info.eccPerBlock;
|
|
14
|
+
|
|
15
|
+
const blocks = [];
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (let i = 0; i < info.blocks; i += 1) {
|
|
18
|
+
const dataLength = shortDataLength + (i < numShortBlocks ? 0 : 1);
|
|
19
|
+
const data = dataCodewords.slice(offset, offset + dataLength);
|
|
20
|
+
offset += dataLength;
|
|
21
|
+
|
|
22
|
+
const ecc = computeRemainder(data, generator);
|
|
23
|
+
const interleavable = data.slice();
|
|
24
|
+
if (i < numShortBlocks) {
|
|
25
|
+
interleavable.push(null);
|
|
26
|
+
}
|
|
27
|
+
interleavable.push(...ecc);
|
|
28
|
+
blocks.push({ data, ecc, interleavable });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = [];
|
|
32
|
+
for (let i = 0; i < shortBlockLength + 1; i += 1) {
|
|
33
|
+
for (const block of blocks) {
|
|
34
|
+
const value = block.interleavable[i];
|
|
35
|
+
if (value !== undefined && value !== null) {
|
|
36
|
+
result.push(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (result.length !== info.rawCodewords) {
|
|
42
|
+
throw new Error(`Expected ${info.rawCodewords} interleaved codewords; got ${result.length}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
codewords: result,
|
|
47
|
+
blocks,
|
|
48
|
+
dataCodewords: info.dataCodewords,
|
|
49
|
+
errorCorrectionCodewords: info.rawCodewords - info.dataCodewords,
|
|
50
|
+
totalCodewords: info.rawCodewords
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const REDUCTION_POLYNOMIAL = 0x11D;
|
|
2
|
+
|
|
3
|
+
export function multiply(a, b) {
|
|
4
|
+
assertByte(a);
|
|
5
|
+
assertByte(b);
|
|
6
|
+
|
|
7
|
+
let result = 0;
|
|
8
|
+
for (let i = 0; i < 8; i += 1) {
|
|
9
|
+
if ((b & 1) !== 0) {
|
|
10
|
+
result ^= a;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const carry = (a & 0x80) !== 0;
|
|
14
|
+
a = (a << 1) & 0xFF;
|
|
15
|
+
if (carry) {
|
|
16
|
+
a ^= REDUCTION_POLYNOMIAL & 0xFF;
|
|
17
|
+
}
|
|
18
|
+
b >>>= 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function pow(base, exponent) {
|
|
25
|
+
assertByte(base);
|
|
26
|
+
if (!Number.isInteger(exponent) || exponent < 0) {
|
|
27
|
+
throw new RangeError(`Exponent must be a non-negative integer; got ${exponent}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let result = 1;
|
|
31
|
+
for (let i = 0; i < exponent; i += 1) {
|
|
32
|
+
result = multiply(result, base);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertByte(value) {
|
|
38
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
|
|
39
|
+
throw new RangeError(`Expected byte value; got ${value}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/core/mask.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export function maskCondition(maskPattern, x, y) {
|
|
2
|
+
switch (maskPattern) {
|
|
3
|
+
case 0:
|
|
4
|
+
return (x + y) % 2 === 0;
|
|
5
|
+
case 1:
|
|
6
|
+
return y % 2 === 0;
|
|
7
|
+
case 2:
|
|
8
|
+
return x % 3 === 0;
|
|
9
|
+
case 3:
|
|
10
|
+
return (x + y) % 3 === 0;
|
|
11
|
+
case 4:
|
|
12
|
+
return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0;
|
|
13
|
+
case 5:
|
|
14
|
+
return ((x * y) % 2) + ((x * y) % 3) === 0;
|
|
15
|
+
case 6:
|
|
16
|
+
return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
|
|
17
|
+
case 7:
|
|
18
|
+
return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
|
|
19
|
+
default:
|
|
20
|
+
throw new RangeError(`Mask pattern must be from 0 to 7; got ${maskPattern}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getPenaltyScore(matrix) {
|
|
25
|
+
const size = matrix.length;
|
|
26
|
+
let penalty = 0;
|
|
27
|
+
|
|
28
|
+
penalty += getRunPenalty(matrix, true);
|
|
29
|
+
penalty += getRunPenalty(matrix, false);
|
|
30
|
+
penalty += getBlockPenalty(matrix);
|
|
31
|
+
penalty += getFinderLikePenalty(matrix, true);
|
|
32
|
+
penalty += getFinderLikePenalty(matrix, false);
|
|
33
|
+
penalty += getBalancePenalty(matrix);
|
|
34
|
+
|
|
35
|
+
return penalty;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getRunPenalty(matrix, horizontal) {
|
|
39
|
+
const size = matrix.length;
|
|
40
|
+
let penalty = 0;
|
|
41
|
+
|
|
42
|
+
for (let a = 0; a < size; a += 1) {
|
|
43
|
+
let runColor = false;
|
|
44
|
+
let runLength = 0;
|
|
45
|
+
|
|
46
|
+
for (let b = 0; b < size; b += 1) {
|
|
47
|
+
const value = horizontal ? matrix[a][b] : matrix[b][a];
|
|
48
|
+
if (b === 0 || value !== runColor) {
|
|
49
|
+
if (runLength >= 5) {
|
|
50
|
+
penalty += runLength - 2;
|
|
51
|
+
}
|
|
52
|
+
runColor = value;
|
|
53
|
+
runLength = 1;
|
|
54
|
+
} else {
|
|
55
|
+
runLength += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (runLength >= 5) {
|
|
60
|
+
penalty += runLength - 2;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return penalty;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getBlockPenalty(matrix) {
|
|
68
|
+
const size = matrix.length;
|
|
69
|
+
let penalty = 0;
|
|
70
|
+
|
|
71
|
+
for (let y = 0; y < size - 1; y += 1) {
|
|
72
|
+
for (let x = 0; x < size - 1; x += 1) {
|
|
73
|
+
const color = matrix[y][x];
|
|
74
|
+
if (color === matrix[y][x + 1] && color === matrix[y + 1][x] && color === matrix[y + 1][x + 1]) {
|
|
75
|
+
penalty += 3;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return penalty;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getFinderLikePenalty(matrix, horizontal) {
|
|
84
|
+
const size = matrix.length;
|
|
85
|
+
const patternA = [true, false, true, true, true, false, true, false, false, false, false];
|
|
86
|
+
const patternB = [false, false, false, false, true, false, true, true, true, false, true];
|
|
87
|
+
let penalty = 0;
|
|
88
|
+
|
|
89
|
+
for (let a = 0; a < size; a += 1) {
|
|
90
|
+
for (let b = 0; b <= size - 11; b += 1) {
|
|
91
|
+
let matchA = true;
|
|
92
|
+
let matchB = true;
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < 11; i += 1) {
|
|
95
|
+
const value = horizontal ? matrix[a][b + i] : matrix[b + i][a];
|
|
96
|
+
matchA &&= value === patternA[i];
|
|
97
|
+
matchB &&= value === patternB[i];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (matchA || matchB) {
|
|
101
|
+
penalty += 40;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return penalty;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getBalancePenalty(matrix) {
|
|
110
|
+
const size = matrix.length;
|
|
111
|
+
let dark = 0;
|
|
112
|
+
|
|
113
|
+
for (const row of matrix) {
|
|
114
|
+
for (const value of row) {
|
|
115
|
+
if (value) {
|
|
116
|
+
dark += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const total = size * size;
|
|
122
|
+
return Math.floor(Math.abs(dark * 20 - total * 10) / total) * 10;
|
|
123
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { ERROR_CORRECTION_LEVELS, getAlignmentPatternPositions, getSize } from "./tables.js";
|
|
2
|
+
import { getPenaltyScore, maskCondition } from "./mask.js";
|
|
3
|
+
|
|
4
|
+
class QRMatrix {
|
|
5
|
+
constructor(size) {
|
|
6
|
+
this.size = size;
|
|
7
|
+
this.modules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
8
|
+
this.functionModules = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
clone() {
|
|
12
|
+
const copy = new QRMatrix(this.size);
|
|
13
|
+
copy.modules = this.modules.map((row) => row.slice());
|
|
14
|
+
copy.functionModules = this.functionModules.map((row) => row.slice());
|
|
15
|
+
return copy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isInside(x, y) {
|
|
19
|
+
return x >= 0 && y >= 0 && x < this.size && y < this.size;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isFunctionAt(x, y) {
|
|
23
|
+
return this.functionModules[y][x];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setFunction(x, y, dark) {
|
|
27
|
+
if (!this.isInside(x, y)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.modules[y][x] = Boolean(dark);
|
|
31
|
+
this.functionModules[y][x] = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setData(x, y, dark) {
|
|
35
|
+
if (this.functionModules[y][x]) {
|
|
36
|
+
throw new Error(`Cannot write data over function module at ${x},${y}`);
|
|
37
|
+
}
|
|
38
|
+
this.modules[y][x] = Boolean(dark);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
applyMask(maskPattern) {
|
|
42
|
+
for (let y = 0; y < this.size; y += 1) {
|
|
43
|
+
for (let x = 0; x < this.size; x += 1) {
|
|
44
|
+
if (!this.functionModules[y][x] && maskCondition(maskPattern, x, y)) {
|
|
45
|
+
this.modules[y][x] = !this.modules[y][x];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toBooleans() {
|
|
52
|
+
return this.modules.map((row) => row.slice());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildMatrix(codewords, version, errorCorrectionLevel, maskPatternOption = "auto") {
|
|
57
|
+
const base = new QRMatrix(getSize(version));
|
|
58
|
+
drawFunctionPatterns(base, version, errorCorrectionLevel);
|
|
59
|
+
drawCodewords(base, codewords);
|
|
60
|
+
|
|
61
|
+
const candidateMasks = maskPatternOption === "auto"
|
|
62
|
+
? [0, 1, 2, 3, 4, 5, 6, 7]
|
|
63
|
+
: [maskPatternOption];
|
|
64
|
+
|
|
65
|
+
let best = null;
|
|
66
|
+
const maskPenalties = [];
|
|
67
|
+
for (const maskPattern of candidateMasks) {
|
|
68
|
+
const candidate = base.clone();
|
|
69
|
+
candidate.applyMask(maskPattern);
|
|
70
|
+
drawFormatBits(candidate, errorCorrectionLevel, maskPattern);
|
|
71
|
+
|
|
72
|
+
const matrix = candidate.toBooleans();
|
|
73
|
+
const penalty = getPenaltyScore(matrix);
|
|
74
|
+
maskPenalties.push({ maskPattern, penalty });
|
|
75
|
+
if (!best || penalty < best.penalty) {
|
|
76
|
+
best = { matrix, maskPattern, penalty };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { ...best, maskPenalties };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function drawFunctionPatterns(matrix, version, errorCorrectionLevel) {
|
|
84
|
+
const size = matrix.size;
|
|
85
|
+
|
|
86
|
+
drawFinderPattern(matrix, 0, 0);
|
|
87
|
+
drawFinderPattern(matrix, size - 7, 0);
|
|
88
|
+
drawFinderPattern(matrix, 0, size - 7);
|
|
89
|
+
drawTimingPatterns(matrix);
|
|
90
|
+
drawAlignmentPatterns(matrix, version);
|
|
91
|
+
drawFormatBits(matrix, errorCorrectionLevel, 0);
|
|
92
|
+
drawDarkModule(matrix);
|
|
93
|
+
drawVersionBits(matrix, version);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function drawFinderPattern(matrix, left, top) {
|
|
97
|
+
for (let dy = -1; dy <= 7; dy += 1) {
|
|
98
|
+
for (let dx = -1; dx <= 7; dx += 1) {
|
|
99
|
+
const x = left + dx;
|
|
100
|
+
const y = top + dy;
|
|
101
|
+
if (!matrix.isInside(x, y)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const inPattern = dx >= 0 && dx <= 6 && dy >= 0 && dy <= 6;
|
|
106
|
+
const dark = inPattern && (
|
|
107
|
+
dx === 0 || dx === 6 || dy === 0 || dy === 6 ||
|
|
108
|
+
(dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4)
|
|
109
|
+
);
|
|
110
|
+
matrix.setFunction(x, y, dark);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function drawTimingPatterns(matrix) {
|
|
116
|
+
const size = matrix.size;
|
|
117
|
+
for (let i = 8; i < size - 8; i += 1) {
|
|
118
|
+
const dark = i % 2 === 0;
|
|
119
|
+
matrix.setFunction(i, 6, dark);
|
|
120
|
+
matrix.setFunction(6, i, dark);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function drawAlignmentPatterns(matrix, version) {
|
|
125
|
+
const positions = getAlignmentPatternPositions(version);
|
|
126
|
+
const last = positions.length - 1;
|
|
127
|
+
|
|
128
|
+
for (let yIndex = 0; yIndex < positions.length; yIndex += 1) {
|
|
129
|
+
for (let xIndex = 0; xIndex < positions.length; xIndex += 1) {
|
|
130
|
+
const overlapsFinder =
|
|
131
|
+
(xIndex === 0 && yIndex === 0) ||
|
|
132
|
+
(xIndex === last && yIndex === 0) ||
|
|
133
|
+
(xIndex === 0 && yIndex === last);
|
|
134
|
+
|
|
135
|
+
if (!overlapsFinder) {
|
|
136
|
+
drawAlignmentPattern(matrix, positions[xIndex], positions[yIndex]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function drawAlignmentPattern(matrix, centerX, centerY) {
|
|
143
|
+
for (let dy = -2; dy <= 2; dy += 1) {
|
|
144
|
+
for (let dx = -2; dx <= 2; dx += 1) {
|
|
145
|
+
const distance = Math.max(Math.abs(dx), Math.abs(dy));
|
|
146
|
+
matrix.setFunction(centerX + dx, centerY + dy, distance !== 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function drawFormatBits(matrix, errorCorrectionLevel, maskPattern) {
|
|
152
|
+
const ecl = ERROR_CORRECTION_LEVELS[errorCorrectionLevel];
|
|
153
|
+
let data = (ecl.formatBits << 3) | maskPattern;
|
|
154
|
+
let remainder = data;
|
|
155
|
+
for (let i = 0; i < 10; i += 1) {
|
|
156
|
+
remainder = (remainder << 1) ^ (((remainder >>> 9) & 1) * 0x537);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const bits = ((data << 10) | remainder) ^ 0x5412;
|
|
160
|
+
const size = matrix.size;
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i <= 5; i += 1) {
|
|
163
|
+
matrix.setFunction(8, i, getBit(bits, i));
|
|
164
|
+
}
|
|
165
|
+
matrix.setFunction(8, 7, getBit(bits, 6));
|
|
166
|
+
matrix.setFunction(8, 8, getBit(bits, 7));
|
|
167
|
+
matrix.setFunction(7, 8, getBit(bits, 8));
|
|
168
|
+
for (let i = 9; i < 15; i += 1) {
|
|
169
|
+
matrix.setFunction(14 - i, 8, getBit(bits, i));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < 8; i += 1) {
|
|
173
|
+
matrix.setFunction(size - 1 - i, 8, getBit(bits, i));
|
|
174
|
+
}
|
|
175
|
+
for (let i = 8; i < 15; i += 1) {
|
|
176
|
+
matrix.setFunction(8, size - 15 + i, getBit(bits, i));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function drawDarkModule(matrix) {
|
|
181
|
+
matrix.setFunction(8, matrix.size - 8, true);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function drawVersionBits(matrix, version) {
|
|
185
|
+
if (version < 7) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let remainder = version;
|
|
190
|
+
for (let i = 0; i < 12; i += 1) {
|
|
191
|
+
remainder = (remainder << 1) ^ (((remainder >>> 11) & 1) * 0x1F25);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const bits = (version << 12) | remainder;
|
|
195
|
+
const size = matrix.size;
|
|
196
|
+
for (let i = 0; i < 18; i += 1) {
|
|
197
|
+
const bit = getBit(bits, i);
|
|
198
|
+
const a = size - 11 + (i % 3);
|
|
199
|
+
const b = Math.floor(i / 3);
|
|
200
|
+
matrix.setFunction(a, b, bit);
|
|
201
|
+
matrix.setFunction(b, a, bit);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function drawCodewords(matrix, codewords) {
|
|
206
|
+
const size = matrix.size;
|
|
207
|
+
let bitIndex = 0;
|
|
208
|
+
|
|
209
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
210
|
+
if (right === 6) {
|
|
211
|
+
right = 5;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (let vert = 0; vert < size; vert += 1) {
|
|
215
|
+
const upward = ((right + 1) & 2) === 0;
|
|
216
|
+
const y = upward ? size - 1 - vert : vert;
|
|
217
|
+
|
|
218
|
+
for (let j = 0; j < 2; j += 1) {
|
|
219
|
+
const x = right - j;
|
|
220
|
+
if (!matrix.isFunctionAt(x, y)) {
|
|
221
|
+
const dark = bitIndex < codewords.length * 8
|
|
222
|
+
? getCodewordBit(codewords, bitIndex)
|
|
223
|
+
: false;
|
|
224
|
+
matrix.setData(x, y, dark);
|
|
225
|
+
bitIndex += 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getCodewordBit(codewords, bitIndex) {
|
|
233
|
+
const value = codewords[Math.floor(bitIndex / 8)];
|
|
234
|
+
return ((value >>> (7 - (bitIndex % 8))) & 1) !== 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getBit(value, index) {
|
|
238
|
+
return ((value >>> index) & 1) !== 0;
|
|
239
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { multiply, pow } from "./galois-field.js";
|
|
2
|
+
|
|
3
|
+
export function createGeneratorPolynomial(degree) {
|
|
4
|
+
if (!Number.isInteger(degree) || degree < 1) {
|
|
5
|
+
throw new RangeError(`Generator degree must be a positive integer; got ${degree}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let result = [1];
|
|
9
|
+
for (let i = 0; i < degree; i += 1) {
|
|
10
|
+
const factor = [1, pow(2, i)];
|
|
11
|
+
result = multiplyPolynomials(result, factor);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function computeRemainder(data, generator) {
|
|
18
|
+
const degree = generator.length - 1;
|
|
19
|
+
const result = new Array(degree).fill(0);
|
|
20
|
+
|
|
21
|
+
for (const value of data) {
|
|
22
|
+
const factor = value ^ result.shift();
|
|
23
|
+
result.push(0);
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < degree; i += 1) {
|
|
26
|
+
result[i] ^= multiply(generator[i + 1], factor);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function multiplyPolynomials(left, right) {
|
|
34
|
+
const result = new Array(left.length + right.length - 1).fill(0);
|
|
35
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
36
|
+
for (let j = 0; j < right.length; j += 1) {
|
|
37
|
+
result[i + j] ^= multiply(left[i], right[j]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|