quadqr-js 1.3.0 → 1.5.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/FORMAT.md +13 -11
- package/README.md +84 -48
- package/SPECIFICATION.md +34 -19
- package/bin/quadqr.js +4 -2
- package/dist/camera-scanner-worker.js +793 -0
- package/dist/esm/benchmark.js +37 -6
- package/dist/esm/brotli.js +2210 -0
- package/dist/esm/camera-scanner-worker.js +793 -0
- package/dist/esm/deflate.js +381 -0
- package/dist/esm/geometry.js +25 -10
- package/dist/esm/quadqr.js +1904 -191
- package/dist/esm/security.js +19 -0
- package/dist/esm/vision.js +1118 -175
- package/dist/esm/wasm.js +85 -12
- package/dist/quadqr.js +5728 -408
- package/dist/quadqr.min.js +5621 -430
- package/dist/wasm/quadqr-core.wasm +0 -0
- package/docs/API.md +54 -15
- package/docs/BROWSER_CDN.md +10 -6
- package/docs/CLI.md +167 -156
- package/docs/COMPRESSION.md +170 -0
- package/docs/GETTING_STARTED.md +12 -2
- package/docs/HIGH_DENSITY_MODE.md +100 -100
- package/docs/NODE.md +159 -137
- package/docs/README.md +62 -60
- package/docs/RELIABILITY_LAB.md +64 -0
- package/docs/TRIANGLE16.md +98 -108
- package/docs/WASM.md +30 -4
- package/package.json +12 -11
- package/types/benchmark.d.ts +0 -8
- package/types/index.d.ts +0 -403
- package/types/node.d.ts +0 -11
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small synchronous RFC 1951 raw-DEFLATE codec used by QuadQR Compression 3.0.
|
|
3
|
+
*
|
|
4
|
+
* The compressor intentionally emits fixed-Huffman blocks only. This keeps the
|
|
5
|
+
* implementation compact, deterministic, dependency-free, and usable in both
|
|
6
|
+
* browsers and Node.js while still allowing DEFLATE's 32 KiB window and
|
|
7
|
+
* 258-byte matches to compress repetitive payloads far better than the legacy
|
|
8
|
+
* QuadQR LZSS stream.
|
|
9
|
+
*
|
|
10
|
+
* The decoder accepts stored and fixed-Huffman blocks produced by this module.
|
|
11
|
+
* Dynamic-Huffman input is intentionally rejected because QuadQR's envelope
|
|
12
|
+
* only needs to decode streams created by its own compressor.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function assert(condition, message) {
|
|
16
|
+
if (!condition) throw new Error(message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function asBytes(input) {
|
|
20
|
+
if (input instanceof Uint8Array) return input;
|
|
21
|
+
if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
22
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
23
|
+
return new Uint8Array(input);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DEFLATE_LEVEL_MIN = 1;
|
|
27
|
+
export const DEFLATE_LEVEL_MAX = 9;
|
|
28
|
+
export const DEFAULT_DEFLATE_LEVEL = 6;
|
|
29
|
+
|
|
30
|
+
function normalizeDeflateLevel(value = DEFAULT_DEFLATE_LEVEL) {
|
|
31
|
+
const level = Number(value);
|
|
32
|
+
assert(Number.isInteger(level), `DEFLATE level must be an integer ${DEFLATE_LEVEL_MIN}..${DEFLATE_LEVEL_MAX}.`);
|
|
33
|
+
assert(
|
|
34
|
+
level >= DEFLATE_LEVEL_MIN && level <= DEFLATE_LEVEL_MAX,
|
|
35
|
+
`DEFLATE level must be ${DEFLATE_LEVEL_MIN}..${DEFLATE_LEVEL_MAX}.`
|
|
36
|
+
);
|
|
37
|
+
return level;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deflateTuning(level) {
|
|
41
|
+
// Higher levels spend progressively more CPU walking candidate chains and
|
|
42
|
+
// performing lazy-match lookahead. The RFC 1951 stream stays compatible
|
|
43
|
+
// regardless of level, so the decoder never needs this value.
|
|
44
|
+
const candidateDepth = [0, 8, 16, 28, 48, 72, 96, 144, 224, 320][level];
|
|
45
|
+
const lazyDepth = [0, 0, 0, 8, 12, 18, 24, 40, 64, 96][level];
|
|
46
|
+
const lazyGain = level <= 3 ? 3 : level <= 6 ? 2 : 1;
|
|
47
|
+
return { candidateDepth, lazyDepth, lazyGain };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function reverseBits(value, width) {
|
|
51
|
+
let out = 0;
|
|
52
|
+
for (let i = 0; i < width; i++) {
|
|
53
|
+
out = (out << 1) | ((value >>> i) & 1);
|
|
54
|
+
}
|
|
55
|
+
return out >>> 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class BitWriter {
|
|
59
|
+
constructor() {
|
|
60
|
+
this.bytes = [];
|
|
61
|
+
this.bitBuffer = 0;
|
|
62
|
+
this.bitCount = 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
writeBits(value, count) {
|
|
66
|
+
let v = value >>> 0;
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
this.bitBuffer |= ((v >>> i) & 1) << this.bitCount;
|
|
69
|
+
this.bitCount++;
|
|
70
|
+
if (this.bitCount === 8) {
|
|
71
|
+
this.bytes.push(this.bitBuffer & 0xff);
|
|
72
|
+
this.bitBuffer = 0;
|
|
73
|
+
this.bitCount = 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
finish() {
|
|
79
|
+
if (this.bitCount) this.bytes.push(this.bitBuffer & 0xff);
|
|
80
|
+
return Uint8Array.from(this.bytes);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class BitReader {
|
|
85
|
+
constructor(bytes) {
|
|
86
|
+
this.bytes = asBytes(bytes);
|
|
87
|
+
this.bytePos = 0;
|
|
88
|
+
this.bitPos = 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
readBits(count) {
|
|
92
|
+
let value = 0;
|
|
93
|
+
for (let i = 0; i < count; i++) {
|
|
94
|
+
assert(this.bytePos < this.bytes.length, "DEFLATE stream is truncated.");
|
|
95
|
+
const bit = (this.bytes[this.bytePos] >>> this.bitPos) & 1;
|
|
96
|
+
value |= bit << i;
|
|
97
|
+
this.bitPos++;
|
|
98
|
+
if (this.bitPos === 8) {
|
|
99
|
+
this.bitPos = 0;
|
|
100
|
+
this.bytePos++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return value >>> 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
alignByte() {
|
|
107
|
+
if (this.bitPos) {
|
|
108
|
+
this.bitPos = 0;
|
|
109
|
+
this.bytePos++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fixedLiteralCode(symbol) {
|
|
115
|
+
if (symbol <= 143) return { bits: reverseBits(0x30 + symbol, 8), width: 8 };
|
|
116
|
+
if (symbol <= 255) return { bits: reverseBits(0x190 + (symbol - 144), 9), width: 9 };
|
|
117
|
+
if (symbol <= 279) return { bits: reverseBits(symbol - 256, 7), width: 7 };
|
|
118
|
+
assert(symbol <= 287, "Invalid fixed-Huffman literal/length symbol.");
|
|
119
|
+
return { bits: reverseBits(0xc0 + (symbol - 280), 8), width: 8 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const LENGTH_BASE = new Uint16Array([
|
|
123
|
+
3, 4, 5, 6, 7, 8, 9, 10,
|
|
124
|
+
11, 13, 15, 17,
|
|
125
|
+
19, 23, 27, 31,
|
|
126
|
+
35, 43, 51, 59,
|
|
127
|
+
67, 83, 99, 115,
|
|
128
|
+
131, 163, 195, 227,
|
|
129
|
+
258
|
|
130
|
+
]);
|
|
131
|
+
const LENGTH_EXTRA = new Uint8Array([
|
|
132
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
133
|
+
1, 1, 1, 1,
|
|
134
|
+
2, 2, 2, 2,
|
|
135
|
+
3, 3, 3, 3,
|
|
136
|
+
4, 4, 4, 4,
|
|
137
|
+
5, 5, 5, 5,
|
|
138
|
+
0
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const DIST_BASE = new Uint16Array([
|
|
142
|
+
1, 2, 3, 4,
|
|
143
|
+
5, 7,
|
|
144
|
+
9, 13,
|
|
145
|
+
17, 25,
|
|
146
|
+
33, 49,
|
|
147
|
+
65, 97,
|
|
148
|
+
129, 193,
|
|
149
|
+
257, 385,
|
|
150
|
+
513, 769,
|
|
151
|
+
1025, 1537,
|
|
152
|
+
2049, 3073,
|
|
153
|
+
4097, 6145,
|
|
154
|
+
8193, 12289,
|
|
155
|
+
16385, 24577
|
|
156
|
+
]);
|
|
157
|
+
const DIST_EXTRA = new Uint8Array([
|
|
158
|
+
0, 0, 0, 0,
|
|
159
|
+
1, 1,
|
|
160
|
+
2, 2,
|
|
161
|
+
3, 3,
|
|
162
|
+
4, 4,
|
|
163
|
+
5, 5,
|
|
164
|
+
6, 6,
|
|
165
|
+
7, 7,
|
|
166
|
+
8, 8,
|
|
167
|
+
9, 9,
|
|
168
|
+
10, 10,
|
|
169
|
+
11, 11,
|
|
170
|
+
12, 12,
|
|
171
|
+
13, 13
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
function lengthSymbol(length) {
|
|
175
|
+
assert(length >= 3 && length <= 258, "DEFLATE match length must be 3..258.");
|
|
176
|
+
if (length === 258) return { symbol: 285, extraBits: 0, extraValue: 0 };
|
|
177
|
+
for (let i = 0; i < 28; i++) {
|
|
178
|
+
const base = LENGTH_BASE[i];
|
|
179
|
+
const extraBits = LENGTH_EXTRA[i];
|
|
180
|
+
const max = base + ((1 << extraBits) - 1);
|
|
181
|
+
if (length <= max) return { symbol: 257 + i, extraBits, extraValue: length - base };
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Unable to encode DEFLATE match length.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function distanceSymbol(distance) {
|
|
187
|
+
assert(distance >= 1 && distance <= 32768, "DEFLATE match distance must be 1..32768.");
|
|
188
|
+
for (let i = 0; i < DIST_BASE.length; i++) {
|
|
189
|
+
const base = DIST_BASE[i];
|
|
190
|
+
const extraBits = DIST_EXTRA[i];
|
|
191
|
+
const max = base + ((1 << extraBits) - 1);
|
|
192
|
+
if (distance <= max) return { symbol: i, extraBits, extraValue: distance - base };
|
|
193
|
+
}
|
|
194
|
+
throw new Error("Unable to encode DEFLATE match distance.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function hash3(bytes, i) {
|
|
198
|
+
if (i + 2 >= bytes.length) return -1;
|
|
199
|
+
return (((bytes[i] * 251 + bytes[i + 1]) * 251 + bytes[i + 2]) >>> 0) & 0xffff;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emitLiteral(writer, value) {
|
|
203
|
+
const code = fixedLiteralCode(value);
|
|
204
|
+
writer.writeBits(code.bits, code.width);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function emitMatch(writer, length, distance) {
|
|
208
|
+
const len = lengthSymbol(length);
|
|
209
|
+
const lenCode = fixedLiteralCode(len.symbol);
|
|
210
|
+
writer.writeBits(lenCode.bits, lenCode.width);
|
|
211
|
+
if (len.extraBits) writer.writeBits(len.extraValue, len.extraBits);
|
|
212
|
+
|
|
213
|
+
const dist = distanceSymbol(distance);
|
|
214
|
+
writer.writeBits(reverseBits(dist.symbol, 5), 5);
|
|
215
|
+
if (dist.extraBits) writer.writeBits(dist.extraValue, dist.extraBits);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Compress bytes as a raw RFC 1951 DEFLATE stream using one final
|
|
220
|
+
* fixed-Huffman block. The function is synchronous and runtime-neutral.
|
|
221
|
+
*/
|
|
222
|
+
export function compressDeflatePayload(input, options = {}) {
|
|
223
|
+
const bytes = asBytes(input);
|
|
224
|
+
const level = normalizeDeflateLevel(
|
|
225
|
+
typeof options === "number" ? options : (options.level ?? options.compressionLevel ?? DEFAULT_DEFLATE_LEVEL)
|
|
226
|
+
);
|
|
227
|
+
const { candidateDepth, lazyDepth, lazyGain } = deflateTuning(level);
|
|
228
|
+
const writer = new BitWriter();
|
|
229
|
+
|
|
230
|
+
// BFINAL=1, BTYPE=01 (fixed Huffman). Bits are written LSB-first.
|
|
231
|
+
writer.writeBits(1, 1);
|
|
232
|
+
writer.writeBits(1, 2);
|
|
233
|
+
|
|
234
|
+
const recent = new Map();
|
|
235
|
+
const WINDOW = 32768;
|
|
236
|
+
const MAX_MATCH = 258;
|
|
237
|
+
const HISTORY_LIMIT = Math.max(candidateDepth, lazyDepth, 8);
|
|
238
|
+
|
|
239
|
+
const remember = (position) => {
|
|
240
|
+
const hash = hash3(bytes, position);
|
|
241
|
+
if (hash < 0) return;
|
|
242
|
+
let list = recent.get(hash);
|
|
243
|
+
if (!list) recent.set(hash, list = []);
|
|
244
|
+
list.push(position);
|
|
245
|
+
while (list.length > HISTORY_LIMIT) list.shift();
|
|
246
|
+
const minimum = position - WINDOW;
|
|
247
|
+
while (list.length && list[0] < minimum) list.shift();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const findBest = (position, maxCandidates) => {
|
|
251
|
+
let bestLength = 0;
|
|
252
|
+
let bestDistance = 0;
|
|
253
|
+
const hash = hash3(bytes, position);
|
|
254
|
+
const candidates = hash >= 0 ? (recent.get(hash) ?? []) : [];
|
|
255
|
+
for (let ci = candidates.length - 1, checked = 0; ci >= 0 && checked < maxCandidates; ci--, checked++) {
|
|
256
|
+
const candidate = candidates[ci];
|
|
257
|
+
const distance = position - candidate;
|
|
258
|
+
if (distance <= 0 || distance > WINDOW) continue;
|
|
259
|
+
let length = 0;
|
|
260
|
+
const limit = Math.min(MAX_MATCH, bytes.length - position);
|
|
261
|
+
while (length < limit && bytes[candidate + (length % distance)] === bytes[position + length]) length++;
|
|
262
|
+
if (length >= 3 && length > bestLength) {
|
|
263
|
+
bestLength = length;
|
|
264
|
+
bestDistance = distance;
|
|
265
|
+
if (length === limit) break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { length: bestLength, distance: bestDistance };
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
let pos = 0;
|
|
272
|
+
while (pos < bytes.length) {
|
|
273
|
+
let best = findBest(pos, candidateDepth);
|
|
274
|
+
|
|
275
|
+
// Levels 3+ may defer a match when the next byte starts a meaningfully
|
|
276
|
+
// longer one. Stronger levels search deeper and accept a smaller gain.
|
|
277
|
+
if (best.length >= 3 && lazyDepth > 0 && pos + 1 < bytes.length) {
|
|
278
|
+
const next = findBest(pos + 1, lazyDepth);
|
|
279
|
+
if (next.length > best.length + lazyGain) best = { length: 0, distance: 0 };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (best.length >= 3) {
|
|
283
|
+
emitMatch(writer, best.length, best.distance);
|
|
284
|
+
for (let i = 0; i < best.length; i++) remember(pos + i);
|
|
285
|
+
pos += best.length;
|
|
286
|
+
} else {
|
|
287
|
+
emitLiteral(writer, bytes[pos]);
|
|
288
|
+
remember(pos);
|
|
289
|
+
pos++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// End-of-block symbol.
|
|
294
|
+
const end = fixedLiteralCode(256);
|
|
295
|
+
writer.writeBits(end.bits, end.width);
|
|
296
|
+
return writer.finish();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const FIXED_DECODE = (() => {
|
|
300
|
+
const byLength = new Map();
|
|
301
|
+
for (let symbol = 0; symbol <= 287; symbol++) {
|
|
302
|
+
const { bits, width } = fixedLiteralCode(symbol);
|
|
303
|
+
let map = byLength.get(width);
|
|
304
|
+
if (!map) byLength.set(width, map = new Map());
|
|
305
|
+
map.set(bits, symbol);
|
|
306
|
+
}
|
|
307
|
+
return byLength;
|
|
308
|
+
})();
|
|
309
|
+
|
|
310
|
+
function readFixedSymbol(reader) {
|
|
311
|
+
let code = 0;
|
|
312
|
+
for (let width = 1; width <= 9; width++) {
|
|
313
|
+
code |= reader.readBits(1) << (width - 1);
|
|
314
|
+
const symbol = FIXED_DECODE.get(width)?.get(code);
|
|
315
|
+
if (symbol != null) return symbol;
|
|
316
|
+
}
|
|
317
|
+
throw new Error("Invalid fixed-Huffman code in DEFLATE stream.");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readStoredBlock(reader, out) {
|
|
321
|
+
reader.alignByte();
|
|
322
|
+
assert(reader.bytePos + 4 <= reader.bytes.length, "Stored DEFLATE block is truncated.");
|
|
323
|
+
const len = reader.bytes[reader.bytePos] | (reader.bytes[reader.bytePos + 1] << 8);
|
|
324
|
+
const nlen = reader.bytes[reader.bytePos + 2] | (reader.bytes[reader.bytePos + 3] << 8);
|
|
325
|
+
reader.bytePos += 4;
|
|
326
|
+
assert(((len ^ 0xffff) & 0xffff) === nlen, "Stored DEFLATE block length checksum is invalid.");
|
|
327
|
+
assert(reader.bytePos + len <= reader.bytes.length, "Stored DEFLATE block payload is truncated.");
|
|
328
|
+
for (let i = 0; i < len; i++) out.push(reader.bytes[reader.bytePos++]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Decode raw DEFLATE streams produced by compressDeflatePayload(). */
|
|
332
|
+
export function decompressDeflatePayload(input, expectedLength = null) {
|
|
333
|
+
const reader = new BitReader(input);
|
|
334
|
+
const out = [];
|
|
335
|
+
let finalBlock = false;
|
|
336
|
+
|
|
337
|
+
while (!finalBlock) {
|
|
338
|
+
finalBlock = reader.readBits(1) === 1;
|
|
339
|
+
const blockType = reader.readBits(2);
|
|
340
|
+
if (blockType === 0) {
|
|
341
|
+
readStoredBlock(reader, out);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
assert(blockType === 1, "QuadQR DEFLATE decoder supports stored and fixed-Huffman blocks only.");
|
|
345
|
+
|
|
346
|
+
while (true) {
|
|
347
|
+
const symbol = readFixedSymbol(reader);
|
|
348
|
+
if (symbol < 256) {
|
|
349
|
+
out.push(symbol);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (symbol === 256) break;
|
|
353
|
+
assert(symbol >= 257 && symbol <= 285, "Invalid DEFLATE length symbol.");
|
|
354
|
+
|
|
355
|
+
const lengthIndex = symbol - 257;
|
|
356
|
+
const lengthBase = LENGTH_BASE[lengthIndex];
|
|
357
|
+
const lengthExtra = LENGTH_EXTRA[lengthIndex];
|
|
358
|
+
const length = lengthBase + (lengthExtra ? reader.readBits(lengthExtra) : 0);
|
|
359
|
+
|
|
360
|
+
const reversedDistanceCode = reader.readBits(5);
|
|
361
|
+
let distanceCode = -1;
|
|
362
|
+
for (let code = 0; code < 30; code++) {
|
|
363
|
+
if (reverseBits(code, 5) === reversedDistanceCode) {
|
|
364
|
+
distanceCode = code;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
assert(distanceCode >= 0, "Invalid DEFLATE distance symbol.");
|
|
369
|
+
const distanceBase = DIST_BASE[distanceCode];
|
|
370
|
+
const distanceExtra = DIST_EXTRA[distanceCode];
|
|
371
|
+
const distance = distanceBase + (distanceExtra ? reader.readBits(distanceExtra) : 0);
|
|
372
|
+
assert(distance > 0 && distance <= out.length, "DEFLATE stream contains an invalid back-reference.");
|
|
373
|
+
|
|
374
|
+
for (let i = 0; i < length; i++) out.push(out[out.length - distance]);
|
|
375
|
+
if (expectedLength != null) assert(out.length <= expectedLength, "DEFLATE payload expands beyond the expected length.");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (expectedLength != null) assert(out.length === expectedLength, "DEFLATE payload length mismatch.");
|
|
380
|
+
return Uint8Array.from(out);
|
|
381
|
+
}
|
package/dist/esm/geometry.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Versions 2..40 use the same alignment-pattern center schedule as standard
|
|
5
5
|
* QR Code matrices. QuadQR remains its own symbology, but reuses that proven
|
|
6
|
-
* spatial distribution for alignment references. QuadQR uses
|
|
7
|
-
*
|
|
6
|
+
* spatial distribution for alignment references. QuadQR format v6 uses the same
|
|
7
|
+
* proven 5x5 nested alignment marker shape at every scheduled alignment center.
|
|
8
|
+
* Legacy format v5 used one 5x5 primary marker plus compact 3x3 secondaries.
|
|
8
9
|
* Version 1 keeps the legacy QuadQR bottom-right bootstrap marker because standard QR v1 has no
|
|
9
10
|
* alignment pattern and the QuadQR camera scanner needs a fourth projective
|
|
10
11
|
* reference point.
|
|
@@ -13,6 +14,14 @@
|
|
|
13
14
|
export const MIN_GEOMETRY_VERSION = 1;
|
|
14
15
|
export const MAX_GEOMETRY_VERSION = 40;
|
|
15
16
|
|
|
17
|
+
export const ALIGNMENT_PROFILE_STANDARD_5 = "standard5";
|
|
18
|
+
export const ALIGNMENT_PROFILE_LEGACY_3 = "compact3";
|
|
19
|
+
|
|
20
|
+
export function normalizeAlignmentProfile(profile = ALIGNMENT_PROFILE_STANDARD_5) {
|
|
21
|
+
if (profile === ALIGNMENT_PROFILE_STANDARD_5 || profile === ALIGNMENT_PROFILE_LEGACY_3) return profile;
|
|
22
|
+
throw new Error(`Unsupported alignment profile ${profile}.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
// Index by version. Entry 0 is unused. Values are module indices of alignment
|
|
17
26
|
// pattern centers, matching the standard QR Code version layout for v2..v40.
|
|
18
27
|
export const ALIGNMENT_PATTERN_AXES = Object.freeze([
|
|
@@ -79,9 +88,11 @@ export function versionFromSize(size) {
|
|
|
79
88
|
: null;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
export function alignmentPatternCentersForVersion(version) {
|
|
91
|
+
export function alignmentPatternCentersForVersion(version, options = {}) {
|
|
83
92
|
assertVersion(version);
|
|
84
93
|
const size = sizeForVersion(version);
|
|
94
|
+
const requestedProfile = typeof options === "string" ? options : options.profile;
|
|
95
|
+
const profile = normalizeAlignmentProfile(requestedProfile ?? ALIGNMENT_PROFILE_STANDARD_5);
|
|
85
96
|
|
|
86
97
|
if (version === 1) {
|
|
87
98
|
const center = size - 4;
|
|
@@ -91,7 +102,8 @@ export function alignmentPatternCentersForVersion(version) {
|
|
|
91
102
|
size: 5,
|
|
92
103
|
primary: true,
|
|
93
104
|
bootstrap: true,
|
|
94
|
-
separator: true
|
|
105
|
+
separator: true,
|
|
106
|
+
profile
|
|
95
107
|
}];
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -113,10 +125,13 @@ export function alignmentPatternCentersForVersion(version) {
|
|
|
113
125
|
centers.push({
|
|
114
126
|
row,
|
|
115
127
|
col,
|
|
116
|
-
|
|
128
|
+
// Format v6 uses a real QR-style 5x5 black/white/black alignment eye
|
|
129
|
+
// everywhere. Legacy v5 kept compact 3x3 secondaries to save cells.
|
|
130
|
+
size: profile === ALIGNMENT_PROFILE_LEGACY_3 && !primary ? 3 : 5,
|
|
117
131
|
primary,
|
|
118
132
|
bootstrap: false,
|
|
119
|
-
separator: false
|
|
133
|
+
separator: false,
|
|
134
|
+
profile
|
|
120
135
|
});
|
|
121
136
|
}
|
|
122
137
|
}
|
|
@@ -124,8 +139,8 @@ export function alignmentPatternCentersForVersion(version) {
|
|
|
124
139
|
return centers;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
export function primaryAlignmentPatternForVersion(version) {
|
|
128
|
-
const centers = alignmentPatternCentersForVersion(version);
|
|
142
|
+
export function primaryAlignmentPatternForVersion(version, options = {}) {
|
|
143
|
+
const centers = alignmentPatternCentersForVersion(version, options);
|
|
129
144
|
return centers.find((pattern) => pattern.primary) ?? centers[centers.length - 1];
|
|
130
145
|
}
|
|
131
146
|
|
|
@@ -138,11 +153,11 @@ export function alignmentPatternIsBlack(pattern, rowOffset, colOffset) {
|
|
|
138
153
|
if (Math.abs(rowOffset) > radius || Math.abs(colOffset) > radius) return null;
|
|
139
154
|
|
|
140
155
|
if (pattern.size === 3) {
|
|
141
|
-
//
|
|
156
|
+
// Legacy format-v5 secondary marker: black 3x3 ring with a white center.
|
|
142
157
|
return rowOffset !== 0 || colOffset !== 0;
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
//
|
|
160
|
+
// QR-style 5x5 marker: black outer ring, white inner ring, black center.
|
|
146
161
|
const outer = Math.abs(rowOffset) === 2 || Math.abs(colOffset) === 2;
|
|
147
162
|
const center = rowOffset === 0 && colOffset === 0;
|
|
148
163
|
return outer || center;
|