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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { bytesToBase64 } from "./base64.js";
|
|
2
|
+
import { parseRgbaColor } from "./color.js";
|
|
3
|
+
|
|
4
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
|
5
|
+
const CRC_TABLE = createCrcTable();
|
|
6
|
+
|
|
7
|
+
export function renderPng(matrix, options) {
|
|
8
|
+
const size = matrix.length;
|
|
9
|
+
const margin = options.margin;
|
|
10
|
+
const scale = options.scale;
|
|
11
|
+
const width = (size + margin * 2) * scale;
|
|
12
|
+
const height = width;
|
|
13
|
+
const foreground = parseRgbaColor(options.foreground, "foreground", true);
|
|
14
|
+
const background = parseRgbaColor(options.background, "background", true);
|
|
15
|
+
const raw = rasterize(matrix, width, height, margin, scale, foreground, background);
|
|
16
|
+
const compressed = createZlibStoredStream(raw);
|
|
17
|
+
|
|
18
|
+
return concatBytes([
|
|
19
|
+
Uint8Array.from(PNG_SIGNATURE),
|
|
20
|
+
createChunk("IHDR", createIhdr(width, height)),
|
|
21
|
+
createChunk("IDAT", compressed),
|
|
22
|
+
createChunk("IEND", new Uint8Array(0))
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderPngDataUrl(matrix, options) {
|
|
27
|
+
return `data:image/png;base64,${bytesToBase64(renderPng(matrix, options))}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rasterize(matrix, width, height, margin, scale, foreground, background) {
|
|
31
|
+
const rowBytes = width * 4 + 1;
|
|
32
|
+
const raw = new Uint8Array(rowBytes * height);
|
|
33
|
+
const size = matrix.length;
|
|
34
|
+
|
|
35
|
+
for (let y = 0; y < height; y += 1) {
|
|
36
|
+
const rowStart = y * rowBytes;
|
|
37
|
+
raw[rowStart] = 0;
|
|
38
|
+
const moduleY = Math.floor(y / scale) - margin;
|
|
39
|
+
|
|
40
|
+
for (let x = 0; x < width; x += 1) {
|
|
41
|
+
const moduleX = Math.floor(x / scale) - margin;
|
|
42
|
+
const isDark =
|
|
43
|
+
moduleX >= 0 &&
|
|
44
|
+
moduleY >= 0 &&
|
|
45
|
+
moduleX < size &&
|
|
46
|
+
moduleY < size &&
|
|
47
|
+
matrix[moduleY][moduleX];
|
|
48
|
+
const color = isDark ? foreground : background;
|
|
49
|
+
const offset = rowStart + 1 + x * 4;
|
|
50
|
+
raw[offset] = color[0];
|
|
51
|
+
raw[offset + 1] = color[1];
|
|
52
|
+
raw[offset + 2] = color[2];
|
|
53
|
+
raw[offset + 3] = color[3];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createIhdr(width, height) {
|
|
61
|
+
const data = new Uint8Array(13);
|
|
62
|
+
writeUint32(data, 0, width);
|
|
63
|
+
writeUint32(data, 4, height);
|
|
64
|
+
data[8] = 8;
|
|
65
|
+
data[9] = 6;
|
|
66
|
+
data[10] = 0;
|
|
67
|
+
data[11] = 0;
|
|
68
|
+
data[12] = 0;
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createChunk(type, data) {
|
|
73
|
+
const typeBytes = asciiBytes(type);
|
|
74
|
+
const chunk = new Uint8Array(12 + data.length);
|
|
75
|
+
writeUint32(chunk, 0, data.length);
|
|
76
|
+
chunk.set(typeBytes, 4);
|
|
77
|
+
chunk.set(data, 8);
|
|
78
|
+
writeUint32(chunk, 8 + data.length, crc32(concatBytes([typeBytes, data])));
|
|
79
|
+
return chunk;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createZlibStoredStream(data) {
|
|
83
|
+
const chunks = [];
|
|
84
|
+
chunks.push(Uint8Array.from([0x78, 0x01]));
|
|
85
|
+
|
|
86
|
+
for (let offset = 0; offset < data.length; offset += 0xFFFF) {
|
|
87
|
+
const length = Math.min(0xFFFF, data.length - offset);
|
|
88
|
+
const isFinal = offset + length >= data.length;
|
|
89
|
+
const block = new Uint8Array(5 + length);
|
|
90
|
+
block[0] = isFinal ? 0x01 : 0x00;
|
|
91
|
+
block[1] = length & 0xFF;
|
|
92
|
+
block[2] = (length >>> 8) & 0xFF;
|
|
93
|
+
const inverse = length ^ 0xFFFF;
|
|
94
|
+
block[3] = inverse & 0xFF;
|
|
95
|
+
block[4] = (inverse >>> 8) & 0xFF;
|
|
96
|
+
block.set(data.subarray(offset, offset + length), 5);
|
|
97
|
+
chunks.push(block);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const checksum = new Uint8Array(4);
|
|
101
|
+
writeUint32(checksum, 0, adler32(data));
|
|
102
|
+
chunks.push(checksum);
|
|
103
|
+
return concatBytes(chunks);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function asciiBytes(text) {
|
|
107
|
+
return Uint8Array.from(Array.from(text, (character) => character.charCodeAt(0)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function concatBytes(parts) {
|
|
111
|
+
const length = parts.reduce((total, part) => total + part.length, 0);
|
|
112
|
+
const result = new Uint8Array(length);
|
|
113
|
+
let offset = 0;
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
result.set(part, offset);
|
|
116
|
+
offset += part.length;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeUint32(target, offset, value) {
|
|
122
|
+
target[offset] = (value >>> 24) & 0xFF;
|
|
123
|
+
target[offset + 1] = (value >>> 16) & 0xFF;
|
|
124
|
+
target[offset + 2] = (value >>> 8) & 0xFF;
|
|
125
|
+
target[offset + 3] = value & 0xFF;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function crc32(bytes) {
|
|
129
|
+
let crc = 0xFFFFFFFF;
|
|
130
|
+
for (const byte of bytes) {
|
|
131
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
|
|
132
|
+
}
|
|
133
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createCrcTable() {
|
|
137
|
+
const table = new Uint32Array(256);
|
|
138
|
+
for (let i = 0; i < 256; i += 1) {
|
|
139
|
+
let value = i;
|
|
140
|
+
for (let j = 0; j < 8; j += 1) {
|
|
141
|
+
value = (value & 1) !== 0 ? 0xEDB88320 ^ (value >>> 1) : value >>> 1;
|
|
142
|
+
}
|
|
143
|
+
table[i] = value >>> 0;
|
|
144
|
+
}
|
|
145
|
+
return table;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function adler32(bytes) {
|
|
149
|
+
let a = 1;
|
|
150
|
+
let b = 0;
|
|
151
|
+
for (const byte of bytes) {
|
|
152
|
+
a = (a + byte) % 65521;
|
|
153
|
+
b = (b + a) % 65521;
|
|
154
|
+
}
|
|
155
|
+
return ((b << 16) | a) >>> 0;
|
|
156
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function renderSvg(matrix, options) {
|
|
2
|
+
const size = matrix.length;
|
|
3
|
+
const margin = options.margin;
|
|
4
|
+
const scale = options.scale;
|
|
5
|
+
const dimension = (size + margin * 2) * scale;
|
|
6
|
+
const foreground = escapeAttribute(options.foreground);
|
|
7
|
+
const background = escapeAttribute(options.background);
|
|
8
|
+
|
|
9
|
+
const path = [];
|
|
10
|
+
for (let y = 0; y < size; y += 1) {
|
|
11
|
+
for (let x = 0; x < size; x += 1) {
|
|
12
|
+
if (matrix[y][x]) {
|
|
13
|
+
const px = (x + margin) * scale;
|
|
14
|
+
const py = (y + margin) * scale;
|
|
15
|
+
path.push(`M${px},${py}h${scale}v${scale}h-${scale}z`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [
|
|
21
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${dimension}" height="${dimension}" viewBox="0 0 ${dimension} ${dimension}" role="img">`,
|
|
22
|
+
`<rect width="100%" height="100%" fill="${background}"/>`,
|
|
23
|
+
`<path fill="${foreground}" d="${path.join("")}"/>`,
|
|
24
|
+
"</svg>"
|
|
25
|
+
].join("");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function renderSvgDataUrl(matrix, options) {
|
|
29
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(renderSvg(matrix, options))}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function escapeAttribute(value) {
|
|
33
|
+
return String(value)
|
|
34
|
+
.replaceAll("&", "&")
|
|
35
|
+
.replaceAll('"', """)
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">");
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Vision
|
|
3
|
+
|
|
4
|
+
guard CommandLine.arguments.count == 2 else {
|
|
5
|
+
FileHandle.standardError.write(Data("Usage: swift tools/decode-vision.swift <image>\n".utf8))
|
|
6
|
+
exit(2)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let url = URL(fileURLWithPath: CommandLine.arguments[1])
|
|
10
|
+
let request = VNDetectBarcodesRequest()
|
|
11
|
+
request.symbologies = [.qr]
|
|
12
|
+
|
|
13
|
+
let handler = VNImageRequestHandler(url: url, options: [:])
|
|
14
|
+
try handler.perform([request])
|
|
15
|
+
|
|
16
|
+
let payloads = (request.results ?? [])
|
|
17
|
+
.compactMap { $0 as? VNBarcodeObservation }
|
|
18
|
+
.compactMap(\.payloadStringValue)
|
|
19
|
+
|
|
20
|
+
for payload in payloads {
|
|
21
|
+
print(payload)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exit(payloads.isEmpty ? 1 : 0)
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { interleaveCodewords } from "../src/core/codewords.js";
|
|
6
|
+
import { encodeSegments, normalizeManualSegments, createSegments, prependEciSegment, prependFnc1Segment } from "../src/encoding/modes.js";
|
|
7
|
+
import { generate, generateSegments } from "../src/index.js";
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const outputPath = path.join(root, "fixtures", "golden-cases.json");
|
|
11
|
+
|
|
12
|
+
const CASES = [
|
|
13
|
+
{
|
|
14
|
+
id: "numeric-v1-l-mask0",
|
|
15
|
+
description: "Numeric mode, fixed version 1-L, mask 0.",
|
|
16
|
+
coverage: ["numeric", "format"],
|
|
17
|
+
input: "01234567",
|
|
18
|
+
options: { version: 1, errorCorrectionLevel: "L", maskPattern: 0, mode: "numeric" }
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "alphanumeric-v1-m-mask1",
|
|
22
|
+
description: "Alphanumeric mode, fixed version 1-M, mask 1.",
|
|
23
|
+
coverage: ["alphanumeric", "format"],
|
|
24
|
+
input: "HELLO WORLD",
|
|
25
|
+
options: { version: 1, errorCorrectionLevel: "M", maskPattern: 1, mode: "alphanumeric" }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "byte-url-v2-q-mask2",
|
|
29
|
+
description: "Byte mode URL, fixed version 2-Q, mask 2.",
|
|
30
|
+
coverage: ["byte", "format", "alignment"],
|
|
31
|
+
input: "https://example.com",
|
|
32
|
+
options: { version: 2, errorCorrectionLevel: "Q", maskPattern: 2, mode: "byte" }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "utf8-byte-v2-m-mask3",
|
|
36
|
+
description: "UTF-8 byte mode Japanese text, fixed version 2-M, mask 3.",
|
|
37
|
+
coverage: ["utf8", "byte", "alignment"],
|
|
38
|
+
input: "こんにちは",
|
|
39
|
+
options: { version: 2, errorCorrectionLevel: "M", maskPattern: 3, mode: "byte" }
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "kanji-v1-m-mask4",
|
|
43
|
+
description: "QR Kanji mode, fixed version 1-M, mask 4.",
|
|
44
|
+
coverage: ["kanji", "format"],
|
|
45
|
+
input: "漢字",
|
|
46
|
+
options: { version: 1, errorCorrectionLevel: "M", maskPattern: 4, mode: "kanji" }
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "manual-mixed-v3-q-mask5",
|
|
50
|
+
description: "Manual mixed segments with alphanumeric, numeric, byte, and Kanji data.",
|
|
51
|
+
coverage: ["manual-segments", "alphanumeric", "numeric", "byte", "kanji", "alignment"],
|
|
52
|
+
segments: [
|
|
53
|
+
{ mode: "alphanumeric", text: "HELLO " },
|
|
54
|
+
{ mode: "numeric", text: "1234567890" },
|
|
55
|
+
{ mode: "byte", text: "-web-" },
|
|
56
|
+
{ mode: "kanji", text: "漢字" }
|
|
57
|
+
],
|
|
58
|
+
options: { version: 3, errorCorrectionLevel: "Q", maskPattern: 5 }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "eci-utf8-v2-q-mask6",
|
|
62
|
+
description: "UTF-8 ECI metadata with byte-mode Japanese text.",
|
|
63
|
+
coverage: ["eci", "utf8", "byte", "alignment"],
|
|
64
|
+
input: "こんにちは",
|
|
65
|
+
options: { version: 2, errorCorrectionLevel: "Q", maskPattern: 6, mode: "byte", eci: true }
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "eci-auto-mixed-v1-q-exact-mask3",
|
|
69
|
+
description: "ECI-prefixed auto mixed segments that exactly fill version 1-Q data capacity.",
|
|
70
|
+
coverage: ["eci", "eci-mixed", "capacity-edge", "auto-segments", "numeric", "byte"],
|
|
71
|
+
input: `a${"1".repeat(9)}bb`,
|
|
72
|
+
options: { version: 1, errorCorrectionLevel: "Q", maskPattern: 3, eci: true }
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "gs1-fnc1-v1-h-exact-mask4",
|
|
76
|
+
description: "FNC1 first position GS1 numeric payload that exactly fills version 1-H data capacity.",
|
|
77
|
+
coverage: ["gs1", "fnc1", "capacity-edge", "numeric"],
|
|
78
|
+
input: "0104912345678904",
|
|
79
|
+
options: { version: 1, errorCorrectionLevel: "H", maskPattern: 4, mode: "numeric", gs1: true }
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "binary-v1-q-mask7",
|
|
83
|
+
description: "Binary byte payload including 0x00 and 0xff.",
|
|
84
|
+
coverage: ["binary", "byte", "format"],
|
|
85
|
+
inputBytes: [0x00, 0x41, 0xff, 0x42, 0x00],
|
|
86
|
+
options: { version: 1, errorCorrectionLevel: "Q", maskPattern: 7, mode: "byte" }
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "version7-v7-h-mask0",
|
|
90
|
+
description: "Version 7 symbol to exercise version information modules.",
|
|
91
|
+
coverage: ["version-info", "format", "alignment"],
|
|
92
|
+
input: "Version 7 conformance",
|
|
93
|
+
options: { version: 7, errorCorrectionLevel: "H", maskPattern: 0, mode: "byte" }
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "boundary-v10-h-numeric-exact-mask1",
|
|
97
|
+
description: "Version 10 numeric payload that exactly fills 10-H data capacity.",
|
|
98
|
+
coverage: ["version-boundary-10", "capacity-edge", "numeric", "version-info"],
|
|
99
|
+
input: "1".repeat(288),
|
|
100
|
+
options: { version: 10, errorCorrectionLevel: "H", maskPattern: 1, mode: "numeric" }
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "boundary-v27-q-kanji-exact-mask2",
|
|
104
|
+
description: "Version 27 Kanji payload that exactly fills 27-Q data capacity.",
|
|
105
|
+
coverage: ["version-boundary-27", "capacity-edge", "kanji", "version-info", "alignment"],
|
|
106
|
+
input: "漢".repeat(496),
|
|
107
|
+
options: { version: 27, errorCorrectionLevel: "Q", maskPattern: 2, mode: "kanji" }
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const fixtures = CASES.map((testCase) => {
|
|
112
|
+
const result = generateCase(testCase);
|
|
113
|
+
const diagnostics = pickDiagnostics(result.diagnostics);
|
|
114
|
+
const codewords = getCodewords(testCase, diagnostics.version, diagnostics.errorCorrectionLevel);
|
|
115
|
+
const matrixRows = matrixToRows(result.matrix);
|
|
116
|
+
const formatBits = computeFormatBits(diagnostics.errorCorrectionLevel, diagnostics.maskPattern);
|
|
117
|
+
const versionBits = diagnostics.version >= 7 ? computeVersionBits(diagnostics.version) : null;
|
|
118
|
+
const functionModuleCount = countFunctionModules(diagnostics.version);
|
|
119
|
+
const dataModuleCount = diagnostics.size * diagnostics.size - functionModuleCount;
|
|
120
|
+
const remainderBits = dataModuleCount - diagnostics.totalCodewords * 8;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: testCase.id,
|
|
124
|
+
description: testCase.description,
|
|
125
|
+
coverage: testCase.coverage,
|
|
126
|
+
input: testCase.input,
|
|
127
|
+
inputBytes: testCase.inputBytes,
|
|
128
|
+
segments: testCase.segments,
|
|
129
|
+
options: testCase.options,
|
|
130
|
+
expected: {
|
|
131
|
+
diagnostics,
|
|
132
|
+
segments: result.diagnostics.segments,
|
|
133
|
+
dataCodewords: codewords.dataCodewords,
|
|
134
|
+
interleavedCodewords: codewords.interleavedCodewords,
|
|
135
|
+
matrixRows,
|
|
136
|
+
matrixSha256: hashRows(matrixRows),
|
|
137
|
+
darkModules: countDarkModules(result.matrix),
|
|
138
|
+
formatBits: {
|
|
139
|
+
value: formatBits,
|
|
140
|
+
bits: toBinary(formatBits, 15)
|
|
141
|
+
},
|
|
142
|
+
versionBits: versionBits === null
|
|
143
|
+
? null
|
|
144
|
+
: {
|
|
145
|
+
value: versionBits,
|
|
146
|
+
bits: toBinary(versionBits, 18)
|
|
147
|
+
},
|
|
148
|
+
functionModuleCount,
|
|
149
|
+
dataModuleCount,
|
|
150
|
+
remainderBits
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
156
|
+
writeFileSync(outputPath, `${JSON.stringify(fixtures, null, 2)}\n`);
|
|
157
|
+
console.log(`Wrote ${fixtures.length} golden fixtures to ${path.relative(root, outputPath)}`);
|
|
158
|
+
|
|
159
|
+
function generateCase(testCase) {
|
|
160
|
+
const options = {
|
|
161
|
+
...testCase.options,
|
|
162
|
+
output: "matrix",
|
|
163
|
+
diagnostics: true
|
|
164
|
+
};
|
|
165
|
+
if (testCase.segments) {
|
|
166
|
+
return generateSegments(testCase.segments, options);
|
|
167
|
+
}
|
|
168
|
+
return generate(getInput(testCase), options);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getCodewords(testCase, version, errorCorrectionLevel) {
|
|
172
|
+
const segments = getSegments(testCase, version);
|
|
173
|
+
const dataCodewords = encodeSegments(segments, version, errorCorrectionLevel);
|
|
174
|
+
const interleaved = interleaveCodewords(dataCodewords, version, errorCorrectionLevel);
|
|
175
|
+
return {
|
|
176
|
+
dataCodewords,
|
|
177
|
+
interleavedCodewords: interleaved.codewords
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getSegments(testCase, version) {
|
|
182
|
+
const eciAssignmentNumber = normalizeEciOption(testCase.options.eci ?? false);
|
|
183
|
+
if (testCase.segments) {
|
|
184
|
+
return prependFnc1Segment(
|
|
185
|
+
prependEciSegment(normalizeManualSegments(testCase.segments), eciAssignmentNumber),
|
|
186
|
+
testCase.options.gs1 ?? false
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return prependFnc1Segment(
|
|
190
|
+
createSegments(
|
|
191
|
+
getInput(testCase),
|
|
192
|
+
testCase.options.mode ?? "auto",
|
|
193
|
+
version,
|
|
194
|
+
testCase.options.optimizeSegments ?? true,
|
|
195
|
+
eciAssignmentNumber
|
|
196
|
+
),
|
|
197
|
+
testCase.options.gs1 ?? false
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeEciOption(value) {
|
|
202
|
+
return value === true ? 26 : value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getInput(testCase) {
|
|
206
|
+
return testCase.inputBytes ? Uint8Array.from(testCase.inputBytes) : testCase.input;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function pickDiagnostics(diagnostics) {
|
|
210
|
+
return {
|
|
211
|
+
version: diagnostics.version,
|
|
212
|
+
size: diagnostics.size,
|
|
213
|
+
errorCorrectionLevel: diagnostics.errorCorrectionLevel,
|
|
214
|
+
requestedErrorCorrectionLevel: diagnostics.requestedErrorCorrectionLevel,
|
|
215
|
+
boostedErrorCorrection: diagnostics.boostedErrorCorrection,
|
|
216
|
+
versionSelection: diagnostics.versionSelection,
|
|
217
|
+
maskPattern: diagnostics.maskPattern,
|
|
218
|
+
maskPenalty: diagnostics.maskPenalty,
|
|
219
|
+
maskPenalties: diagnostics.maskPenalties,
|
|
220
|
+
mode: diagnostics.mode,
|
|
221
|
+
eciAssignmentNumber: diagnostics.eciAssignmentNumber,
|
|
222
|
+
fnc1: diagnostics.fnc1,
|
|
223
|
+
gs1: diagnostics.gs1,
|
|
224
|
+
dataBitLength: diagnostics.dataBitLength,
|
|
225
|
+
capacityBits: diagnostics.capacityBits,
|
|
226
|
+
remainingBits: diagnostics.remainingBits,
|
|
227
|
+
inputBytes: diagnostics.inputBytes,
|
|
228
|
+
dataCodewords: diagnostics.dataCodewords,
|
|
229
|
+
errorCorrectionCodewords: diagnostics.errorCorrectionCodewords,
|
|
230
|
+
totalCodewords: diagnostics.totalCodewords
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function matrixToRows(matrix) {
|
|
235
|
+
return matrix.map((row) => row.map((module) => module ? "1" : "0").join(""));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function hashRows(rows) {
|
|
239
|
+
return createHash("sha256").update(rows.join("\n")).digest("hex");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function countDarkModules(matrix) {
|
|
243
|
+
return matrix.reduce((total, row) => total + row.filter(Boolean).length, 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function computeFormatBits(errorCorrectionLevel, maskPattern) {
|
|
247
|
+
const formatBitsByLevel = { L: 0b01, M: 0b00, Q: 0b11, H: 0b10 };
|
|
248
|
+
const data = (formatBitsByLevel[errorCorrectionLevel] << 3) | maskPattern;
|
|
249
|
+
let remainder = data;
|
|
250
|
+
for (let i = 0; i < 10; i += 1) {
|
|
251
|
+
remainder = (remainder << 1) ^ (((remainder >>> 9) & 1) * 0x537);
|
|
252
|
+
}
|
|
253
|
+
return ((data << 10) | remainder) ^ 0x5412;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function computeVersionBits(version) {
|
|
257
|
+
let remainder = version;
|
|
258
|
+
for (let i = 0; i < 12; i += 1) {
|
|
259
|
+
remainder = (remainder << 1) ^ (((remainder >>> 11) & 1) * 0x1f25);
|
|
260
|
+
}
|
|
261
|
+
return (version << 12) | remainder;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function countFunctionModules(version) {
|
|
265
|
+
const mask = createFunctionModuleMask(version);
|
|
266
|
+
return mask.reduce((total, row) => total + row.filter(Boolean).length, 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function createFunctionModuleMask(version) {
|
|
270
|
+
const size = version * 4 + 17;
|
|
271
|
+
const mask = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
272
|
+
drawFinder(mask, 0, 0);
|
|
273
|
+
drawFinder(mask, size - 7, 0);
|
|
274
|
+
drawFinder(mask, 0, size - 7);
|
|
275
|
+
drawTiming(mask);
|
|
276
|
+
drawAlignment(mask, version);
|
|
277
|
+
drawFormat(mask);
|
|
278
|
+
set(mask, 8, size - 8);
|
|
279
|
+
drawVersion(mask, version);
|
|
280
|
+
return mask;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function drawFinder(mask, left, top) {
|
|
284
|
+
for (let dy = -1; dy <= 7; dy += 1) {
|
|
285
|
+
for (let dx = -1; dx <= 7; dx += 1) {
|
|
286
|
+
set(mask, left + dx, top + dy);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function drawTiming(mask) {
|
|
292
|
+
const size = mask.length;
|
|
293
|
+
for (let i = 8; i < size - 8; i += 1) {
|
|
294
|
+
set(mask, i, 6);
|
|
295
|
+
set(mask, 6, i);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function drawAlignment(mask, version) {
|
|
300
|
+
const positions = alignmentPositions(version);
|
|
301
|
+
const last = positions.length - 1;
|
|
302
|
+
for (let yIndex = 0; yIndex < positions.length; yIndex += 1) {
|
|
303
|
+
for (let xIndex = 0; xIndex < positions.length; xIndex += 1) {
|
|
304
|
+
const overlapsFinder =
|
|
305
|
+
(xIndex === 0 && yIndex === 0) ||
|
|
306
|
+
(xIndex === last && yIndex === 0) ||
|
|
307
|
+
(xIndex === 0 && yIndex === last);
|
|
308
|
+
if (!overlapsFinder) {
|
|
309
|
+
for (let dy = -2; dy <= 2; dy += 1) {
|
|
310
|
+
for (let dx = -2; dx <= 2; dx += 1) {
|
|
311
|
+
set(mask, positions[xIndex] + dx, positions[yIndex] + dy);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function drawFormat(mask) {
|
|
320
|
+
const size = mask.length;
|
|
321
|
+
for (let i = 0; i <= 5; i += 1) {
|
|
322
|
+
set(mask, 8, i);
|
|
323
|
+
}
|
|
324
|
+
set(mask, 8, 7);
|
|
325
|
+
set(mask, 8, 8);
|
|
326
|
+
set(mask, 7, 8);
|
|
327
|
+
for (let i = 9; i < 15; i += 1) {
|
|
328
|
+
set(mask, 14 - i, 8);
|
|
329
|
+
}
|
|
330
|
+
for (let i = 0; i < 8; i += 1) {
|
|
331
|
+
set(mask, size - 1 - i, 8);
|
|
332
|
+
}
|
|
333
|
+
for (let i = 8; i < 15; i += 1) {
|
|
334
|
+
set(mask, 8, size - 15 + i);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function drawVersion(mask, version) {
|
|
339
|
+
if (version < 7) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const size = mask.length;
|
|
343
|
+
for (let i = 0; i < 18; i += 1) {
|
|
344
|
+
const a = size - 11 + (i % 3);
|
|
345
|
+
const b = Math.floor(i / 3);
|
|
346
|
+
set(mask, a, b);
|
|
347
|
+
set(mask, b, a);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function alignmentPositions(version) {
|
|
352
|
+
switch (version) {
|
|
353
|
+
case 1:
|
|
354
|
+
return [];
|
|
355
|
+
case 2:
|
|
356
|
+
return [6, 18];
|
|
357
|
+
case 3:
|
|
358
|
+
return [6, 22];
|
|
359
|
+
case 7:
|
|
360
|
+
return [6, 22, 38];
|
|
361
|
+
case 10:
|
|
362
|
+
return [6, 28, 50];
|
|
363
|
+
case 27:
|
|
364
|
+
return [6, 34, 62, 90, 118];
|
|
365
|
+
default:
|
|
366
|
+
throw new Error(`Golden fixture helper does not define alignment positions for version ${version}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function set(mask, x, y) {
|
|
371
|
+
if (y >= 0 && y < mask.length && x >= 0 && x < mask.length) {
|
|
372
|
+
mask[y][x] = true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function toBinary(value, width) {
|
|
377
|
+
return value.toString(2).padStart(width, "0");
|
|
378
|
+
}
|