hcc2d-encoder 0.0.0-stage → 0.9.0-rc.2
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 +19 -0
- package/LICENSE +186 -0
- package/README.md +311 -2
- package/bin/hcc2d-encoder.js +5 -0
- package/package.json +51 -4
- package/src/cli.js +156 -0
- package/src/encoder.js +646 -0
- package/src/index.js +15 -0
package/src/encoder.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
// HCC2D Encoder - pure JavaScript reference implementation
|
|
2
|
+
// Copyright 2010-2026 Marco Querini
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Version 0.9.0-rc.2
|
|
5
|
+
//
|
|
6
|
+
// Intended to conform to HCC2D Code Specification 0.9.0:
|
|
7
|
+
// https://doi.org/10.5281/zenodo.22206528
|
|
8
|
+
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { deflateSync } from "node:zlib";
|
|
11
|
+
|
|
12
|
+
export const PALETTE_QR = Object.freeze([[0, 0, 0], [255, 255, 255]]);
|
|
13
|
+
export const PALETTE_4_MODEL1 = Object.freeze([[0, 0, 0], [220, 0, 0], [0, 200, 220], [255, 255, 255]]);
|
|
14
|
+
export const PALETTE_8_MODEL1 = Object.freeze([
|
|
15
|
+
[0, 0, 0], [200, 0, 0], [0, 130, 0], [0, 60, 180],
|
|
16
|
+
[0, 215, 235], [255, 220, 50], [255, 130, 230], [255, 255, 255],
|
|
17
|
+
]);
|
|
18
|
+
export const PALETTE_4_MODEL2 = Object.freeze([[0, 0, 0], [255, 0, 255], [0, 255, 255], [255, 255, 255]]);
|
|
19
|
+
export const PALETTE_8_MODEL2 = Object.freeze([
|
|
20
|
+
[0, 0, 0], [0, 0, 255], [255, 0, 0], [255, 0, 255],
|
|
21
|
+
[0, 255, 0], [0, 255, 255], [255, 255, 0], [255, 255, 255],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const EC_LEVEL_BITS = Object.freeze({ L: 1, M: 0, Q: 3, H: 2 });
|
|
25
|
+
const EC_LEVEL_INDEX = Object.freeze({ L: 0, M: 1, Q: 2, H: 3 });
|
|
26
|
+
const MODE_BITS_BYTE = 0b0100;
|
|
27
|
+
const VERSION_INFO_POLY = 0x1f25;
|
|
28
|
+
const TYPE_INFO_POLY = 0x537;
|
|
29
|
+
const TYPE_INFO_MASK_PATTERN = 0x5412;
|
|
30
|
+
|
|
31
|
+
// [version, alignment centers, [ec words/block, group1 count/data, group2 count/data] x L/M/Q/H]
|
|
32
|
+
const QR_VERSIONS = [
|
|
33
|
+
[1, [], [[7,1,19,0,0],[10,1,16,0,0],[13,1,13,0,0],[17,1,9,0,0]]],
|
|
34
|
+
[2, [6,18], [[10,1,34,0,0],[16,1,28,0,0],[22,1,22,0,0],[28,1,16,0,0]]],
|
|
35
|
+
[3, [6,22], [[15,1,55,0,0],[26,1,44,0,0],[18,2,17,0,0],[22,2,13,0,0]]],
|
|
36
|
+
[4, [6,26], [[20,1,80,0,0],[18,2,32,0,0],[26,2,24,0,0],[16,4,9,0,0]]],
|
|
37
|
+
[5, [6,30], [[26,1,108,0,0],[24,2,43,0,0],[18,2,15,2,16],[22,2,11,2,12]]],
|
|
38
|
+
[6, [6,34], [[18,2,68,0,0],[16,4,27,0,0],[24,4,19,0,0],[28,4,15,0,0]]],
|
|
39
|
+
[7, [6,22,38], [[20,2,78,0,0],[18,4,31,0,0],[18,2,14,4,15],[26,4,13,1,14]]],
|
|
40
|
+
[8, [6,24,42], [[24,2,97,0,0],[22,2,38,2,39],[22,4,18,2,19],[26,4,14,2,15]]],
|
|
41
|
+
[9, [6,26,46], [[30,2,116,0,0],[22,3,36,2,37],[20,4,16,4,17],[24,4,12,4,13]]],
|
|
42
|
+
[10,[6,28,50], [[18,2,68,2,69],[26,4,43,1,44],[24,6,19,2,20],[28,6,15,2,16]]],
|
|
43
|
+
[11,[6,30,54], [[20,4,81,0,0],[30,1,50,4,51],[28,4,22,4,23],[24,3,12,8,13]]],
|
|
44
|
+
[12,[6,32,58], [[24,2,92,2,93],[22,6,36,2,37],[26,4,20,6,21],[28,7,14,4,15]]],
|
|
45
|
+
[13,[6,34,62], [[26,4,107,0,0],[22,8,37,1,38],[24,8,20,4,21],[22,12,11,4,12]]],
|
|
46
|
+
[14,[6,26,46,66], [[30,3,115,1,116],[24,4,40,5,41],[20,11,16,5,17],[24,11,12,5,13]]],
|
|
47
|
+
[15,[6,26,48,70], [[22,5,87,1,88],[24,5,41,5,42],[30,5,24,7,25],[24,11,12,7,13]]],
|
|
48
|
+
[16,[6,26,50,74], [[24,5,98,1,99],[28,7,45,3,46],[24,15,19,2,20],[30,3,15,13,16]]],
|
|
49
|
+
[17,[6,30,54,78], [[28,1,107,5,108],[28,10,46,1,47],[28,1,22,15,23],[28,2,14,17,15]]],
|
|
50
|
+
[18,[6,30,56,82], [[30,5,120,1,121],[26,9,43,4,44],[28,17,22,1,23],[28,2,14,19,15]]],
|
|
51
|
+
[19,[6,30,58,86], [[28,3,113,4,114],[26,3,44,11,45],[26,17,21,4,22],[26,9,13,16,14]]],
|
|
52
|
+
[20,[6,34,62,90], [[28,3,107,5,108],[26,3,41,13,42],[30,15,24,5,25],[28,15,15,10,16]]],
|
|
53
|
+
[21,[6,28,50,72,94], [[28,4,116,4,117],[26,17,42,0,0],[28,17,22,6,23],[30,19,16,6,17]]],
|
|
54
|
+
[22,[6,26,50,74,98], [[28,2,111,7,112],[28,17,46,0,0],[30,7,24,16,25],[24,34,13,0,0]]],
|
|
55
|
+
[23,[6,30,54,78,102], [[30,4,121,5,122],[28,4,47,14,48],[30,11,24,14,25],[30,16,15,14,16]]],
|
|
56
|
+
[24,[6,28,54,80,106], [[30,6,117,4,118],[28,6,45,14,46],[30,11,24,16,25],[30,30,16,2,17]]],
|
|
57
|
+
[25,[6,32,58,84,110], [[26,8,106,4,107],[28,8,47,13,48],[30,7,24,22,25],[30,22,15,13,16]]],
|
|
58
|
+
[26,[6,30,58,86,114], [[28,10,114,2,115],[28,19,46,4,47],[28,28,22,6,23],[30,33,16,4,17]]],
|
|
59
|
+
[27,[6,34,62,90,118], [[30,8,122,4,123],[28,22,45,3,46],[30,8,23,26,24],[30,12,15,28,16]]],
|
|
60
|
+
[28,[6,26,50,74,98,122], [[30,3,117,10,118],[28,3,45,23,46],[30,4,24,31,25],[30,11,15,31,16]]],
|
|
61
|
+
[29,[6,30,54,78,102,126], [[30,7,116,7,117],[28,21,45,7,46],[30,1,23,37,24],[30,19,15,26,16]]],
|
|
62
|
+
[30,[6,26,52,78,104,130], [[30,5,115,10,116],[28,19,47,10,48],[30,15,24,25,25],[30,23,15,25,16]]],
|
|
63
|
+
[31,[6,30,56,82,108,134], [[30,13,115,3,116],[28,2,46,29,47],[30,42,24,1,25],[30,23,15,28,16]]],
|
|
64
|
+
[32,[6,34,60,86,112,138], [[30,17,115,0,0],[28,10,46,23,47],[30,10,24,35,25],[30,19,15,35,16]]],
|
|
65
|
+
[33,[6,30,58,86,114,142], [[30,17,115,1,116],[28,14,46,21,47],[30,29,24,19,25],[30,11,15,46,16]]],
|
|
66
|
+
[34,[6,34,62,90,118,146], [[30,13,115,6,116],[28,14,46,23,47],[30,44,24,7,25],[30,59,16,1,17]]],
|
|
67
|
+
[35,[6,30,54,78,102,126,150], [[30,12,121,7,122],[28,12,47,26,48],[30,39,24,14,25],[30,22,15,41,16]]],
|
|
68
|
+
[36,[6,24,50,76,102,128,154], [[30,6,121,14,122],[28,6,47,34,48],[30,46,24,10,25],[30,2,15,64,16]]],
|
|
69
|
+
[37,[6,28,54,80,106,132,158], [[30,17,122,4,123],[28,29,46,14,47],[30,49,24,10,25],[30,24,15,46,16]]],
|
|
70
|
+
[38,[6,32,58,84,110,136,162], [[30,4,122,18,123],[28,13,46,32,47],[30,48,24,14,25],[30,42,15,32,16]]],
|
|
71
|
+
[39,[6,26,54,82,110,138,166], [[30,20,117,4,118],[28,40,47,7,48],[30,43,24,22,25],[30,10,15,67,16]]],
|
|
72
|
+
[40,[6,30,58,86,114,142,170], [[30,19,118,6,119],[28,18,47,31,48],[30,34,24,34,25],[30,20,15,61,16]]],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const MODE_SPECS = Object.freeze({
|
|
76
|
+
qr: { planeCount: 1, borderPeriod: 0, defaultPalette: PALETTE_QR, multiplier: 1 },
|
|
77
|
+
hcc2d4: { planeCount: 2, borderPeriod: 4, defaultPalette: PALETTE_4_MODEL1, multiplier: 2 },
|
|
78
|
+
hcc2d8: { planeCount: 3, borderPeriod: 8, defaultPalette: PALETTE_8_MODEL1, multiplier: 3 },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
class ECBlocks {
|
|
82
|
+
constructor(codewordsPerBlock, count1, data1, count2, data2) {
|
|
83
|
+
Object.assign(this, { codewordsPerBlock, count1, data1, count2, data2 });
|
|
84
|
+
}
|
|
85
|
+
get numBlocks() { return this.count1 + this.count2; }
|
|
86
|
+
get totalCodewords() { return this.codewordsPerBlock * this.numBlocks; }
|
|
87
|
+
get totalSymbolCodewords() {
|
|
88
|
+
return this.count1 * (this.data1 + this.codewordsPerBlock)
|
|
89
|
+
+ this.count2 * (this.data2 + this.codewordsPerBlock);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class VersionInfo {
|
|
94
|
+
constructor(number, alignments, ecLevels) { Object.assign(this, { number, alignments, ecLevels }); }
|
|
95
|
+
get dimension() { return 17 + 4 * this.number; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const MODEL2_VERSIONS = QR_VERSIONS.map(([number, alignments, entries]) =>
|
|
99
|
+
new VersionInfo(number, alignments, entries.map((entry) => new ECBlocks(...entry))));
|
|
100
|
+
|
|
101
|
+
class BitBuffer {
|
|
102
|
+
constructor() { this.bits = []; }
|
|
103
|
+
appendBit(value) { this.bits.push(value ? 1 : 0); }
|
|
104
|
+
appendBits(value, count) {
|
|
105
|
+
if (!Number.isInteger(count) || count < 0) throw new RangeError("count must be non-negative");
|
|
106
|
+
for (let shift = count - 1; shift >= 0; shift -= 1) this.bits.push((value >>> shift) & 1);
|
|
107
|
+
}
|
|
108
|
+
toBytes(bitOffset = 0, numBytes = null) {
|
|
109
|
+
const available = this.bits.length - bitOffset;
|
|
110
|
+
if (available < 0) throw new RangeError("bit offset out of range");
|
|
111
|
+
const count = numBytes ?? Math.ceil(available / 8);
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let byteIndex = 0; byteIndex < count; byteIndex += 1) {
|
|
114
|
+
let value = 0;
|
|
115
|
+
for (let bitIndex = 0; bitIndex < 8; bitIndex += 1) {
|
|
116
|
+
const source = bitOffset + byteIndex * 8 + bitIndex;
|
|
117
|
+
value = (value << 1) | (source < this.bits.length ? this.bits[source] : 0);
|
|
118
|
+
}
|
|
119
|
+
out.push(value);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class GF256 {
|
|
126
|
+
constructor(primitive = 0x011d) {
|
|
127
|
+
this.exp = Array(512).fill(0);
|
|
128
|
+
this.log = Array(256).fill(0);
|
|
129
|
+
let x = 1;
|
|
130
|
+
for (let i = 0; i < 255; i += 1) {
|
|
131
|
+
this.exp[i] = x;
|
|
132
|
+
this.log[x] = i;
|
|
133
|
+
x <<= 1;
|
|
134
|
+
if (x & 0x100) x ^= primitive;
|
|
135
|
+
}
|
|
136
|
+
for (let i = 255; i < 512; i += 1) this.exp[i] = this.exp[i - 255];
|
|
137
|
+
}
|
|
138
|
+
multiply(a, b) { return a === 0 || b === 0 ? 0 : this.exp[this.log[a] + this.log[b]]; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const GF = new GF256();
|
|
142
|
+
|
|
143
|
+
function polynomialMultiply(a, b) {
|
|
144
|
+
const out = Array(a.length + b.length - 1).fill(0);
|
|
145
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
146
|
+
for (let j = 0; j < b.length; j += 1) out[i + j] ^= GF.multiply(a[i], b[j]);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function rsGeneratorPolynomial(degree) {
|
|
152
|
+
let result = [1];
|
|
153
|
+
for (let i = 0; i < degree; i += 1) result = polynomialMultiply(result, [1, GF.exp[i]]);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rsComputeRemainder(data, ecWords) {
|
|
158
|
+
const generator = rsGeneratorPolynomial(ecWords);
|
|
159
|
+
let remainder = Array(ecWords).fill(0);
|
|
160
|
+
for (const byte of data) {
|
|
161
|
+
const factor = byte ^ remainder[0];
|
|
162
|
+
remainder = remainder.slice(1).concat(0);
|
|
163
|
+
if (factor !== 0) {
|
|
164
|
+
for (let i = 0; i < ecWords; i += 1) remainder[i] ^= GF.multiply(generator[i + 1], factor);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return remainder;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizePayload(payload) {
|
|
171
|
+
if (payload instanceof Uint8Array) return new Uint8Array(payload);
|
|
172
|
+
if (payload instanceof ArrayBuffer) return new Uint8Array(payload.slice(0));
|
|
173
|
+
throw new TypeError("payload must be a Uint8Array, Buffer, or ArrayBuffer");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getVersion(mode, number) {
|
|
177
|
+
if (!(mode in MODE_SPECS)) throw new RangeError(`unsupported mode: ${mode}`);
|
|
178
|
+
if (!Number.isInteger(number) || number < 1 || number > 40) throw new RangeError("version must be 1-40");
|
|
179
|
+
const base = MODEL2_VERSIONS[number - 1];
|
|
180
|
+
const multiplier = MODE_SPECS[mode].multiplier;
|
|
181
|
+
return new VersionInfo(base.number, base.alignments, base.ecLevels.map((entry) => new ECBlocks(
|
|
182
|
+
entry.codewordsPerBlock, entry.count1 * multiplier, entry.data1, entry.count2 * multiplier, entry.data2,
|
|
183
|
+
)));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildHeaderAndDataBits(payload, countBits = 16) {
|
|
187
|
+
if (![8, 16].includes(countBits)) throw new RangeError("byte-count field must contain 8 or 16 bits");
|
|
188
|
+
if (payload.length >= 2 ** countBits) throw new RangeError(`payload length does not fit in a ${countBits}-bit byte-count field`);
|
|
189
|
+
const bits = new BitBuffer();
|
|
190
|
+
bits.appendBits(MODE_BITS_BYTE, 4);
|
|
191
|
+
bits.appendBits(payload.length, countBits);
|
|
192
|
+
for (const byte of payload) bits.appendBits(byte, 8);
|
|
193
|
+
return bits;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function willFit(mode, ecLevel, payload, version) {
|
|
197
|
+
const countBits = mode === "qr" && version.number <= 9 ? 8 : 16;
|
|
198
|
+
if (payload.length >= 2 ** countBits) return false;
|
|
199
|
+
const bits = buildHeaderAndDataBits(payload, countBits);
|
|
200
|
+
const ec = version.ecLevels[EC_LEVEL_INDEX[ecLevel]];
|
|
201
|
+
return ec.totalSymbolCodewords - ec.totalCodewords >= Math.ceil(bits.bits.length / 8);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function chooseVersion(mode, ecLevel, payload) {
|
|
205
|
+
for (let number = 1; number <= 40; number += 1) {
|
|
206
|
+
const version = getVersion(mode, number);
|
|
207
|
+
if (willFit(mode, ecLevel, payload, version)) return version;
|
|
208
|
+
}
|
|
209
|
+
throw new RangeError("payload does not fit in any supported version");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Wrap file bytes in the HCC2DF container described by specification section 17.
|
|
214
|
+
* @param {string} filename basename stored as UTF-8 metadata
|
|
215
|
+
* @param {Uint8Array|Buffer|ArrayBuffer} content file content
|
|
216
|
+
* @returns {Buffer}
|
|
217
|
+
*/
|
|
218
|
+
export function buildHcc2dfPayload(filename, content) {
|
|
219
|
+
const bytes = normalizePayload(content);
|
|
220
|
+
const basename = String(filename).replaceAll("\\", "/").split("/").at(-1);
|
|
221
|
+
const name = new TextEncoder().encode(basename);
|
|
222
|
+
if (name.length === 0) throw new RangeError("input filename must be non-empty");
|
|
223
|
+
if (name.length > 127) throw new RangeError("input filename must be at most 127 UTF-8 bytes");
|
|
224
|
+
const compressed = deflateSync(bytes);
|
|
225
|
+
const useCompressed = bytes.length >= 128 && compressed.length < bytes.length * 0.9;
|
|
226
|
+
const stored = useCompressed ? compressed : bytes;
|
|
227
|
+
return Buffer.concat([
|
|
228
|
+
Buffer.from("HCC2DF", "ascii"), Buffer.from([1, useCompressed ? 1 : 0, name.length]),
|
|
229
|
+
Buffer.from(name), Buffer.from(stored),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function terminateBits(numDataBytes, bits) {
|
|
234
|
+
const capacity = numDataBytes * 8;
|
|
235
|
+
if (bits.bits.length > capacity) throw new RangeError("data bits exceed symbol capacity");
|
|
236
|
+
for (let i = 0; i < Math.min(4, capacity - bits.bits.length); i += 1) bits.appendBit(false);
|
|
237
|
+
while (bits.bits.length % 8 !== 0) bits.appendBit(false);
|
|
238
|
+
const padding = [0xec, 0x11];
|
|
239
|
+
let index = 0;
|
|
240
|
+
while (bits.bits.length < capacity) { bits.appendBits(padding[index % 2], 8); index += 1; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getBlockLayout(totalBytes, dataBytes, blockCount, blockId) {
|
|
244
|
+
if (blockId >= blockCount) throw new RangeError("block id out of range");
|
|
245
|
+
const group2 = totalBytes % blockCount;
|
|
246
|
+
const group1 = blockCount - group2;
|
|
247
|
+
const total1 = Math.floor(totalBytes / blockCount);
|
|
248
|
+
const data1 = Math.floor(dataBytes / blockCount);
|
|
249
|
+
return blockId < group1 ? [data1, total1 - data1] : [data1 + 1, total1 - data1];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function interleaveWithEcBytes(bits, totalBytes, dataBytes, blockCount) {
|
|
253
|
+
if (Math.floor(bits.bits.length / 8) !== dataBytes) throw new Error("data byte count mismatch");
|
|
254
|
+
const source = bits.toBytes();
|
|
255
|
+
const blocks = [];
|
|
256
|
+
let offset = 0;
|
|
257
|
+
let maxData = 0;
|
|
258
|
+
let maxEc = 0;
|
|
259
|
+
for (let blockId = 0; blockId < blockCount; blockId += 1) {
|
|
260
|
+
const [dataLength, ecLength] = getBlockLayout(totalBytes, dataBytes, blockCount, blockId);
|
|
261
|
+
const data = source.slice(offset, offset + dataLength);
|
|
262
|
+
const ec = rsComputeRemainder(data, ecLength);
|
|
263
|
+
blocks.push([data, ec]);
|
|
264
|
+
offset += dataLength;
|
|
265
|
+
maxData = Math.max(maxData, dataLength);
|
|
266
|
+
maxEc = Math.max(maxEc, ecLength);
|
|
267
|
+
}
|
|
268
|
+
const result = new BitBuffer();
|
|
269
|
+
for (let i = 0; i < maxData; i += 1) for (const [data] of blocks) if (i < data.length) result.appendBits(data[i], 8);
|
|
270
|
+
for (let i = 0; i < maxEc; i += 1) for (const [, ec] of blocks) if (i < ec.length) result.appendBits(ec[i], 8);
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function extractPlaneBits(bits, offset, count) {
|
|
275
|
+
const result = [];
|
|
276
|
+
for (let i = offset; i < bits.length; i += count) result.push(bits[i]);
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function dataMaskBit(mask, x, y) {
|
|
281
|
+
if (mask === 0) return (y + x) % 2 === 0;
|
|
282
|
+
if (mask === 1) return y % 2 === 0;
|
|
283
|
+
if (mask === 2) return x % 3 === 0;
|
|
284
|
+
if (mask === 3) return (y + x) % 3 === 0;
|
|
285
|
+
if (mask === 4) return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0;
|
|
286
|
+
if (mask === 5) return (y * x) % 6 === 0;
|
|
287
|
+
if (mask === 6) return (y * x) % 6 < 3;
|
|
288
|
+
if (mask === 7) return (y + x + ((y * x) % 3)) % 2 === 0;
|
|
289
|
+
throw new RangeError("invalid mask pattern");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const emptyMatrix = (dimension) => Array.from({ length: dimension }, () => Array(dimension).fill(null));
|
|
293
|
+
|
|
294
|
+
function setIfInside(matrix, x, y, value) {
|
|
295
|
+
if (x >= 0 && y >= 0 && x < matrix.length && y < matrix.length) matrix[y][x] = value;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function embedFinder(matrix, x0, y0) {
|
|
299
|
+
for (let y = 0; y < 7; y += 1) for (let x = 0; x < 7; x += 1) {
|
|
300
|
+
matrix[y0 + y][x0 + x] = Math.max(Math.abs(x - 3), Math.abs(y - 3)) !== 2 ? 1 : 0;
|
|
301
|
+
}
|
|
302
|
+
for (let i = -1; i < 8; i += 1) {
|
|
303
|
+
setIfInside(matrix, x0 + i, y0 - 1, 0); setIfInside(matrix, x0 + i, y0 + 7, 0);
|
|
304
|
+
setIfInside(matrix, x0 - 1, y0 + i, 0); setIfInside(matrix, x0 + 7, y0 + i, 0);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function embedFinders(matrix) {
|
|
309
|
+
const dimension = matrix.length;
|
|
310
|
+
embedFinder(matrix, 0, 0); embedFinder(matrix, dimension - 7, 0); embedFinder(matrix, 0, dimension - 7);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function embedAlignment(matrix, x0, y0) {
|
|
314
|
+
for (let y = 0; y < 5; y += 1) for (let x = 0; x < 5; x += 1) {
|
|
315
|
+
matrix[y0 + y][x0 + x] = Math.max(Math.abs(x - 2), Math.abs(y - 2)) !== 1 ? 1 : 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function embedAlignments(version, matrix) {
|
|
320
|
+
if (version.number < 2) return;
|
|
321
|
+
const dimension = matrix.length;
|
|
322
|
+
for (const y of version.alignments) for (const x of version.alignments) {
|
|
323
|
+
if ((x === 6 && y === 6) || (x === 6 && y === dimension - 7) || (x === dimension - 7 && y === 6)) continue;
|
|
324
|
+
embedAlignment(matrix, x - 2, y - 2);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function embedTiming(matrix) {
|
|
329
|
+
for (let i = 8; i < matrix.length - 8; i += 1) {
|
|
330
|
+
const bit = (i + 1) % 2 ? 1 : 0;
|
|
331
|
+
matrix[6][i] = bit; matrix[i][6] = bit;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function bchCode(input, polynomial) {
|
|
336
|
+
const msb = 32 - Math.clz32(polynomial);
|
|
337
|
+
let value = input << (msb - 1);
|
|
338
|
+
while (value !== 0 && 32 - Math.clz32(value) >= msb) value ^= polynomial << (32 - Math.clz32(value) - msb);
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function typeInfoBits(ecLevel, mask) {
|
|
343
|
+
const typeInfo = (EC_LEVEL_BITS[ecLevel] << 3) | mask;
|
|
344
|
+
const value = ((typeInfo << 10) | bchCode(typeInfo, TYPE_INFO_POLY)) ^ TYPE_INFO_MASK_PATTERN;
|
|
345
|
+
return Array.from({ length: 15 }, (_, i) => (value >>> (14 - i)) & 1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function embedTypeInfo(ecLevel, mask, matrix) {
|
|
349
|
+
const coordinates = [[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],[7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8]];
|
|
350
|
+
const bits = typeInfoBits(ecLevel, mask);
|
|
351
|
+
const dimension = matrix.length;
|
|
352
|
+
for (let i = 0; i < bits.length; i += 1) {
|
|
353
|
+
const bit = bits[bits.length - 1 - i];
|
|
354
|
+
const [x, y] = coordinates[i];
|
|
355
|
+
matrix[y][x] = bit;
|
|
356
|
+
if (i < 8) matrix[8][dimension - i - 1] = bit;
|
|
357
|
+
else matrix[dimension - 7 + i - 8][8] = bit;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function embedVersionInfo(version, matrix) {
|
|
362
|
+
if (version.number < 7) return;
|
|
363
|
+
const value = (version.number << 12) | bchCode(version.number, VERSION_INFO_POLY);
|
|
364
|
+
const bits = Array.from({ length: 18 }, (_, i) => (value >>> (17 - i)) & 1);
|
|
365
|
+
let bitIndex = bits.length - 1;
|
|
366
|
+
for (let i = 0; i < 6; i += 1) for (let j = 0; j < 3; j += 1) {
|
|
367
|
+
const bit = bits[bitIndex--];
|
|
368
|
+
matrix[matrix.length - 11 + j][i] = bit;
|
|
369
|
+
matrix[i][matrix.length - 11 + j] = bit;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function embedDataBits(dataBits, mask, matrix) {
|
|
374
|
+
const dimension = matrix.length;
|
|
375
|
+
let bitIndex = 0;
|
|
376
|
+
let direction = -1;
|
|
377
|
+
let x = dimension - 1;
|
|
378
|
+
let y = dimension - 1;
|
|
379
|
+
while (x > 0) {
|
|
380
|
+
if (x === 6) x -= 1;
|
|
381
|
+
while (y >= 0 && y < dimension) {
|
|
382
|
+
for (let xx = x; xx > x - 2; xx -= 1) {
|
|
383
|
+
if (matrix[y][xx] !== null) continue;
|
|
384
|
+
let bit = bitIndex < dataBits.length ? dataBits[bitIndex] : 0;
|
|
385
|
+
bitIndex += 1;
|
|
386
|
+
if (dataMaskBit(mask, xx, y)) bit ^= 1;
|
|
387
|
+
matrix[y][xx] = bit;
|
|
388
|
+
}
|
|
389
|
+
y += direction;
|
|
390
|
+
}
|
|
391
|
+
direction = -direction; y += direction; x -= 2;
|
|
392
|
+
}
|
|
393
|
+
if (bitIndex < dataBits.length) throw new Error(`not all bits consumed: ${bitIndex}/${dataBits.length}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function buildMatrix(dataBits, ecLevel, version, mask) {
|
|
397
|
+
const matrix = emptyMatrix(version.dimension);
|
|
398
|
+
embedFinders(matrix); matrix[matrix.length - 8][8] = 1; embedAlignments(version, matrix); embedTiming(matrix);
|
|
399
|
+
embedTypeInfo(ecLevel, mask, matrix); embedVersionInfo(version, matrix); embedDataBits(dataBits, mask, matrix);
|
|
400
|
+
return matrix.map((row) => row.map((cell) => cell ?? 0));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function functionPattern(version) {
|
|
404
|
+
const matrix = emptyMatrix(version.dimension);
|
|
405
|
+
embedFinders(matrix); matrix[matrix.length - 8][8] = 1; embedAlignments(version, matrix); embedTiming(matrix);
|
|
406
|
+
embedTypeInfo("L", 0, matrix); embedVersionInfo(version, matrix);
|
|
407
|
+
return matrix.map((row) => row.map((cell) => cell !== null));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function penaltyRule1(matrix) {
|
|
411
|
+
let penalty = 0;
|
|
412
|
+
for (const horizontal of [true, false]) for (let i = 0; i < matrix.length; i += 1) {
|
|
413
|
+
let run = 0; let previous = -1;
|
|
414
|
+
for (let j = 0; j < matrix.length; j += 1) {
|
|
415
|
+
const bit = horizontal ? matrix[i][j] : matrix[j][i];
|
|
416
|
+
if (bit === previous) run += 1;
|
|
417
|
+
else { if (run >= 5) penalty += 3 + run - 5; run = 1; previous = bit; }
|
|
418
|
+
}
|
|
419
|
+
if (run >= 5) penalty += 3 + run - 5;
|
|
420
|
+
}
|
|
421
|
+
return penalty;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function penaltyRule2(matrix) {
|
|
425
|
+
let blocks = 0;
|
|
426
|
+
for (let y = 0; y < matrix.length - 1; y += 1) for (let x = 0; x < matrix.length - 1; x += 1) {
|
|
427
|
+
const value = matrix[y][x];
|
|
428
|
+
if (value === matrix[y][x + 1] && value === matrix[y + 1][x] && value === matrix[y + 1][x + 1]) blocks += 1;
|
|
429
|
+
}
|
|
430
|
+
return blocks * 3;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function patternAt(pattern, line, start) {
|
|
434
|
+
if (start < 0 || start + pattern.length > line.length) return false;
|
|
435
|
+
return pattern.every((value, index) => line[start + index] === value);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function allZero(values) { return values.every((value) => value === 0); }
|
|
439
|
+
|
|
440
|
+
function penaltyRule3(matrix) {
|
|
441
|
+
const finder = [1, 0, 1, 1, 1, 0, 1];
|
|
442
|
+
let count = 0;
|
|
443
|
+
for (const line of [...matrix, ...matrix.map((_, x) => matrix.map((row) => row[x]))]) {
|
|
444
|
+
for (let offset = 0; offset < line.length; offset += 1) {
|
|
445
|
+
if (!patternAt(finder, line, offset)) continue;
|
|
446
|
+
const before = line.slice(Math.max(0, offset - 4), offset);
|
|
447
|
+
const after = line.slice(offset + 7, Math.min(line.length, offset + 11));
|
|
448
|
+
// Match the reference implementation's boundary behavior exactly:
|
|
449
|
+
// an empty leading slice does not match, while an empty trailing slice does.
|
|
450
|
+
if ((before.length > 0 && allZero(before)) || allZero(after)) count += 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return count * 40;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function maskPenalty(matrix) {
|
|
457
|
+
const total = matrix.length ** 2;
|
|
458
|
+
const dark = matrix.reduce((sum, row) => sum + row.reduce((a, b) => a + b, 0), 0);
|
|
459
|
+
const rule4 = Math.floor((Math.abs(dark * 2 - total) * 10) / total) * 10;
|
|
460
|
+
return penaltyRule1(matrix) + penaltyRule2(matrix) + penaltyRule3(matrix) + rule4;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function chooseMask(bits, ecLevel, version) {
|
|
464
|
+
let bestMask = 0; let bestPenalty = Infinity;
|
|
465
|
+
for (let mask = 0; mask < 8; mask += 1) {
|
|
466
|
+
const penalty = maskPenalty(buildMatrix(bits, ecLevel, version, mask));
|
|
467
|
+
if (penalty < bestPenalty) { bestPenalty = penalty; bestMask = mask; }
|
|
468
|
+
}
|
|
469
|
+
return bestMask;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function borderColor(row, column, dimension, period) {
|
|
473
|
+
if (row === -1 && column >= 8 && column < dimension - 8) return (column - 8) % period;
|
|
474
|
+
if (row === dimension && column >= 8 && column < dimension) return (column - 8) % period;
|
|
475
|
+
if (column === -1) {
|
|
476
|
+
const start = dimension - 9;
|
|
477
|
+
if (row >= 8 && row <= start) return (start - row) % period;
|
|
478
|
+
}
|
|
479
|
+
if (column === dimension && row >= 8 && row < dimension) return (row - 8) % period;
|
|
480
|
+
return period - 1;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderModules(planes, version, mode) {
|
|
484
|
+
const spec = MODE_SPECS[mode];
|
|
485
|
+
const dimension = version.dimension;
|
|
486
|
+
const white = spec.borderPeriod - 1;
|
|
487
|
+
const result = Array.from({ length: dimension + 2 }, () => Array(dimension + 2).fill(white));
|
|
488
|
+
const functions = functionPattern(version);
|
|
489
|
+
for (let y = -1; y <= dimension; y += 1) for (let x = -1; x <= dimension; x += 1) {
|
|
490
|
+
let color;
|
|
491
|
+
if (x >= 0 && y >= 0 && x < dimension && y < dimension) {
|
|
492
|
+
if (functions[y][x]) color = planes[0][y][x] ? 0 : white;
|
|
493
|
+
else { color = 0; for (let plane = 0; plane < spec.planeCount; plane += 1) color = (color << 1) | planes[plane][y][x]; }
|
|
494
|
+
} else color = borderColor(y, x, dimension, spec.borderPeriod);
|
|
495
|
+
result[y + 1][x + 1] = color;
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function rasterize(modules, scale, quietZone, background) {
|
|
501
|
+
const size = (modules.length + quietZone * 2) * scale;
|
|
502
|
+
const pixels = new Uint8Array(size * size).fill(background);
|
|
503
|
+
for (let my = 0; my < modules.length; my += 1) for (let mx = 0; mx < modules.length; mx += 1) {
|
|
504
|
+
const baseX = (mx + quietZone) * scale;
|
|
505
|
+
const baseY = (my + quietZone) * scale;
|
|
506
|
+
for (let dy = 0; dy < scale; dy += 1) pixels.fill(modules[my][mx], (baseY + dy) * size + baseX, (baseY + dy) * size + baseX + scale);
|
|
507
|
+
}
|
|
508
|
+
return { width: size, height: size, pixels };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const CRC_TABLE = (() => {
|
|
512
|
+
const table = new Uint32Array(256);
|
|
513
|
+
for (let n = 0; n < 256; n += 1) {
|
|
514
|
+
let c = n;
|
|
515
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
516
|
+
table[n] = c >>> 0;
|
|
517
|
+
}
|
|
518
|
+
return table;
|
|
519
|
+
})();
|
|
520
|
+
|
|
521
|
+
function crc32(bytes) {
|
|
522
|
+
let crc = 0xffffffff;
|
|
523
|
+
for (const byte of bytes) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
524
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function u32(value) { const buffer = Buffer.alloc(4); buffer.writeUInt32BE(value >>> 0); return buffer; }
|
|
528
|
+
function pngChunk(type, data) {
|
|
529
|
+
const name = Buffer.from(type, "ascii");
|
|
530
|
+
return Buffer.concat([u32(data.length), name, data, u32(crc32(Buffer.concat([name, data])))]);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Serialize indexed pixels and an RGB palette as a minimal PNG.
|
|
535
|
+
* @param {number} width
|
|
536
|
+
* @param {number} height
|
|
537
|
+
* @param {Uint8Array|number[]} indices
|
|
538
|
+
* @param {number[][]} palette
|
|
539
|
+
* @returns {Buffer}
|
|
540
|
+
*/
|
|
541
|
+
export function buildIndexedPng(width, height, indices, palette) {
|
|
542
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw new RangeError("PNG dimensions must be positive integers");
|
|
543
|
+
if (indices.length !== width * height) throw new RangeError("pixel count does not match PNG dimensions");
|
|
544
|
+
const pixelBytes = indices instanceof Uint8Array ? indices : Uint8Array.from(indices);
|
|
545
|
+
const raw = Buffer.alloc((width + 1) * height);
|
|
546
|
+
for (let y = 0; y < height; y += 1) Buffer.from(pixelBytes.buffer, pixelBytes.byteOffset + y * width, width).copy(raw, y * (width + 1) + 1);
|
|
547
|
+
const ihdr = Buffer.alloc(13);
|
|
548
|
+
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
|
|
549
|
+
ihdr.set([8, 3, 0, 0, 0], 8);
|
|
550
|
+
const plte = Buffer.from(palette.flat());
|
|
551
|
+
return Buffer.concat([
|
|
552
|
+
Buffer.from([137,80,78,71,13,10,26,10]), pngChunk("IHDR", ihdr), pngChunk("PLTE", plte),
|
|
553
|
+
pngChunk("IDAT", deflateSync(raw, { level: 9 })), pngChunk("IEND", Buffer.alloc(0)),
|
|
554
|
+
]);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Fully encoded symbol with indexed raster pixels and PNG serialization. */
|
|
558
|
+
export class EncodedSymbol {
|
|
559
|
+
constructor(values) { Object.assign(this, values); }
|
|
560
|
+
toPng() { return buildIndexedPng(this.width, this.height, this.pixels, this.palette); }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function validateCommon(payload, ecLevel, version, scale, quietZone) {
|
|
564
|
+
if (payload.length === 0) throw new RangeError("payload is empty");
|
|
565
|
+
if (!(ecLevel in EC_LEVEL_INDEX)) throw new RangeError("ec level must be one of L/M/Q/H");
|
|
566
|
+
if (!Number.isInteger(version) || version < 0 || version > 40) throw new RangeError("version must be 0-40");
|
|
567
|
+
if (!Number.isInteger(scale) || scale < 6 || scale > 1000) throw new RangeError("scale must be an integer from 6 to 1000");
|
|
568
|
+
if (!Number.isInteger(quietZone) || quietZone < 0 || quietZone > 1000) throw new RangeError("quiet zone must be an integer from 0 to 1000");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function validatePalette(palette, size) {
|
|
572
|
+
if (!Array.isArray(palette) || palette.length !== size) throw new RangeError(`palette must have exactly ${size} entries`);
|
|
573
|
+
for (const color of palette) if (!Array.isArray(color) || color.length !== 3 || color.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255)) throw new RangeError("palette entries must be RGB byte triples");
|
|
574
|
+
if (palette[0].some((channel) => channel !== 0)) throw new RangeError("palette index 0 must be (0, 0, 0) — spec §5.8");
|
|
575
|
+
if (palette.at(-1).some((channel) => channel !== 255)) throw new RangeError("palette last index must be (255, 255, 255) — spec §5.8");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Encode bytes as HCC2D4 or HCC2D8.
|
|
580
|
+
* @param {Uint8Array|Buffer|ArrayBuffer} payloadInput
|
|
581
|
+
* @param {{mode?: "hcc2d4"|"hcc2d8", ecLevel?: "L"|"M"|"Q"|"H", version?: number, scale?: number, quietZone?: number, palette?: number[][]}} options
|
|
582
|
+
* @returns {EncodedSymbol}
|
|
583
|
+
*/
|
|
584
|
+
export function encodeHcc2d(payloadInput, options = {}) {
|
|
585
|
+
const payload = normalizePayload(payloadInput);
|
|
586
|
+
const { mode = "hcc2d4", ecLevel = "Q", version = 0, scale = 12, quietZone = 4 } = options;
|
|
587
|
+
if (!["hcc2d4", "hcc2d8"].includes(mode)) throw new RangeError("mode must be hcc2d4 or hcc2d8");
|
|
588
|
+
validateCommon(payload, ecLevel, version, scale, quietZone);
|
|
589
|
+
const spec = MODE_SPECS[mode];
|
|
590
|
+
const palette = options.palette ?? spec.defaultPalette;
|
|
591
|
+
validatePalette(palette, 2 ** spec.planeCount);
|
|
592
|
+
const selected = version ? getVersion(mode, version) : chooseVersion(mode, ecLevel, payload);
|
|
593
|
+
if (!willFit(mode, ecLevel, payload, selected)) {
|
|
594
|
+
const ec = selected.ecLevels[EC_LEVEL_INDEX[ecLevel]];
|
|
595
|
+
throw new RangeError(`${mode} payload is ${payload.length} bytes but version ${version} at EC ${ecLevel} fits at most ${Math.max(0, ec.totalSymbolCodewords - ec.totalCodewords - 3)} bytes`);
|
|
596
|
+
}
|
|
597
|
+
const ec = selected.ecLevels[EC_LEVEL_INDEX[ecLevel]];
|
|
598
|
+
const totalBytes = ec.totalSymbolCodewords;
|
|
599
|
+
const dataBytes = totalBytes - ec.totalCodewords;
|
|
600
|
+
const bits = buildHeaderAndDataBits(payload);
|
|
601
|
+
terminateBits(dataBytes, bits);
|
|
602
|
+
const finalBits = interleaveWithEcBytes(bits, totalBytes, dataBytes, ec.numBlocks);
|
|
603
|
+
const proxy = extractPlaneBits(finalBits.bits, 0, spec.planeCount).map((bit) => bit ? 0 : 1);
|
|
604
|
+
const renderVersion = MODEL2_VERSIONS[selected.number - 1];
|
|
605
|
+
const maskPattern = chooseMask(proxy, ecLevel, renderVersion);
|
|
606
|
+
const planes = Array.from({ length: spec.planeCount }, (_, plane) => buildMatrix(extractPlaneBits(finalBits.bits, plane, spec.planeCount), ecLevel, renderVersion, maskPattern));
|
|
607
|
+
const modules = renderModules(planes, renderVersion, mode);
|
|
608
|
+
const image = rasterize(modules, scale, quietZone, palette.length - 1);
|
|
609
|
+
return new EncodedSymbol({ mode, ecLevel, version: selected.number, innerDimension: selected.dimension, fullDimension: modules.length, maskPattern, palette, ...image });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Encode bytes as a standard QR Code using byte mode.
|
|
614
|
+
* @param {Uint8Array|Buffer|ArrayBuffer} payloadInput
|
|
615
|
+
* @param {{ecLevel?: "L"|"M"|"Q"|"H", version?: number, scale?: number, quietZone?: number}} options
|
|
616
|
+
* @returns {EncodedSymbol}
|
|
617
|
+
*/
|
|
618
|
+
export function encodeQr(payloadInput, options = {}) {
|
|
619
|
+
const payload = normalizePayload(payloadInput);
|
|
620
|
+
const { ecLevel = "Q", version = 0, scale = 12, quietZone = 4 } = options;
|
|
621
|
+
validateCommon(payload, ecLevel, version, scale, quietZone);
|
|
622
|
+
const selected = version ? getVersion("qr", version) : chooseVersion("qr", ecLevel, payload);
|
|
623
|
+
if (!willFit("qr", ecLevel, payload, selected)) {
|
|
624
|
+
const ec = selected.ecLevels[EC_LEVEL_INDEX[ecLevel]];
|
|
625
|
+
const countBits = selected.number <= 9 ? 8 : 16;
|
|
626
|
+
const capacity = Math.max(0, Math.floor(((ec.totalSymbolCodewords - ec.totalCodewords) * 8 - 4 - countBits) / 8));
|
|
627
|
+
throw new RangeError(`qr payload is ${payload.length} bytes but version ${version} at EC ${ecLevel} fits at most ${capacity} bytes`);
|
|
628
|
+
}
|
|
629
|
+
const ec = selected.ecLevels[EC_LEVEL_INDEX[ecLevel]];
|
|
630
|
+
const totalBytes = ec.totalSymbolCodewords;
|
|
631
|
+
const dataBytes = totalBytes - ec.totalCodewords;
|
|
632
|
+
const bits = buildHeaderAndDataBits(payload, selected.number <= 9 ? 8 : 16);
|
|
633
|
+
terminateBits(dataBytes, bits);
|
|
634
|
+
const finalBits = interleaveWithEcBytes(bits, totalBytes, dataBytes, ec.numBlocks);
|
|
635
|
+
const maskPattern = chooseMask(finalBits.bits, ecLevel, selected);
|
|
636
|
+
const matrix = buildMatrix(finalBits.bits, ecLevel, selected, maskPattern);
|
|
637
|
+
const modules = matrix.map((row) => row.map((cell) => cell ? 0 : 1));
|
|
638
|
+
const image = rasterize(modules, scale, quietZone, 1);
|
|
639
|
+
return new EncodedSymbol({ mode: "qr", ecLevel, version: selected.number, innerDimension: selected.dimension, fullDimension: selected.dimension, maskPattern, palette: PALETTE_QR, ...image });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Write an EncodedSymbol as an indexed PNG file. */
|
|
643
|
+
export function writePng(path, symbol) {
|
|
644
|
+
if (!(symbol instanceof EncodedSymbol)) throw new TypeError("symbol must be an EncodedSymbol");
|
|
645
|
+
writeFileSync(path, symbol.toPng());
|
|
646
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
EncodedSymbol,
|
|
3
|
+
PALETTE_QR,
|
|
4
|
+
PALETTE_4_MODEL1,
|
|
5
|
+
PALETTE_4_MODEL2,
|
|
6
|
+
PALETTE_8_MODEL1,
|
|
7
|
+
PALETTE_8_MODEL2,
|
|
8
|
+
buildHcc2dfPayload,
|
|
9
|
+
buildIndexedPng,
|
|
10
|
+
encodeHcc2d,
|
|
11
|
+
encodeQr,
|
|
12
|
+
writePng,
|
|
13
|
+
} from "./encoder.js";
|
|
14
|
+
|
|
15
|
+
export const version = "0.9.0-rc.2";
|