quadqr-js 1.2.0 → 1.4.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.
@@ -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
+ }