cross-image 0.1.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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +606 -0
  3. package/esm/mod.d.ts +33 -0
  4. package/esm/mod.d.ts.map +1 -0
  5. package/esm/mod.js +31 -0
  6. package/esm/package.json +3 -0
  7. package/esm/src/formats/ascii.d.ts +27 -0
  8. package/esm/src/formats/ascii.d.ts.map +1 -0
  9. package/esm/src/formats/ascii.js +172 -0
  10. package/esm/src/formats/bmp.d.ts +19 -0
  11. package/esm/src/formats/bmp.d.ts.map +1 -0
  12. package/esm/src/formats/bmp.js +174 -0
  13. package/esm/src/formats/gif.d.ts +40 -0
  14. package/esm/src/formats/gif.d.ts.map +1 -0
  15. package/esm/src/formats/gif.js +385 -0
  16. package/esm/src/formats/jpeg.d.ts +18 -0
  17. package/esm/src/formats/jpeg.d.ts.map +1 -0
  18. package/esm/src/formats/jpeg.js +414 -0
  19. package/esm/src/formats/png.d.ts +33 -0
  20. package/esm/src/formats/png.d.ts.map +1 -0
  21. package/esm/src/formats/png.js +544 -0
  22. package/esm/src/formats/raw.d.ts +23 -0
  23. package/esm/src/formats/raw.d.ts.map +1 -0
  24. package/esm/src/formats/raw.js +98 -0
  25. package/esm/src/formats/tiff.d.ts +58 -0
  26. package/esm/src/formats/tiff.d.ts.map +1 -0
  27. package/esm/src/formats/tiff.js +791 -0
  28. package/esm/src/formats/webp.d.ts +22 -0
  29. package/esm/src/formats/webp.d.ts.map +1 -0
  30. package/esm/src/formats/webp.js +403 -0
  31. package/esm/src/image.d.ts +124 -0
  32. package/esm/src/image.d.ts.map +1 -0
  33. package/esm/src/image.js +320 -0
  34. package/esm/src/types.d.ts +167 -0
  35. package/esm/src/types.d.ts.map +1 -0
  36. package/esm/src/types.js +1 -0
  37. package/esm/src/utils/gif_decoder.d.ts +42 -0
  38. package/esm/src/utils/gif_decoder.d.ts.map +1 -0
  39. package/esm/src/utils/gif_decoder.js +374 -0
  40. package/esm/src/utils/gif_encoder.d.ts +29 -0
  41. package/esm/src/utils/gif_encoder.d.ts.map +1 -0
  42. package/esm/src/utils/gif_encoder.js +226 -0
  43. package/esm/src/utils/jpeg_decoder.d.ts +39 -0
  44. package/esm/src/utils/jpeg_decoder.d.ts.map +1 -0
  45. package/esm/src/utils/jpeg_decoder.js +580 -0
  46. package/esm/src/utils/jpeg_encoder.d.ts +33 -0
  47. package/esm/src/utils/jpeg_encoder.d.ts.map +1 -0
  48. package/esm/src/utils/jpeg_encoder.js +1017 -0
  49. package/esm/src/utils/lzw.d.ts +43 -0
  50. package/esm/src/utils/lzw.d.ts.map +1 -0
  51. package/esm/src/utils/lzw.js +309 -0
  52. package/esm/src/utils/resize.d.ts +9 -0
  53. package/esm/src/utils/resize.d.ts.map +1 -0
  54. package/esm/src/utils/resize.js +52 -0
  55. package/esm/src/utils/tiff_lzw.d.ts +44 -0
  56. package/esm/src/utils/tiff_lzw.d.ts.map +1 -0
  57. package/esm/src/utils/tiff_lzw.js +306 -0
  58. package/esm/src/utils/webp_decoder.d.ts +39 -0
  59. package/esm/src/utils/webp_decoder.d.ts.map +1 -0
  60. package/esm/src/utils/webp_decoder.js +493 -0
  61. package/esm/src/utils/webp_encoder.d.ts +72 -0
  62. package/esm/src/utils/webp_encoder.d.ts.map +1 -0
  63. package/esm/src/utils/webp_encoder.js +627 -0
  64. package/package.json +41 -0
  65. package/script/mod.d.ts +33 -0
  66. package/script/mod.d.ts.map +1 -0
  67. package/script/mod.js +43 -0
  68. package/script/package.json +3 -0
  69. package/script/src/formats/ascii.d.ts +27 -0
  70. package/script/src/formats/ascii.d.ts.map +1 -0
  71. package/script/src/formats/ascii.js +176 -0
  72. package/script/src/formats/bmp.d.ts +19 -0
  73. package/script/src/formats/bmp.d.ts.map +1 -0
  74. package/script/src/formats/bmp.js +178 -0
  75. package/script/src/formats/gif.d.ts +40 -0
  76. package/script/src/formats/gif.d.ts.map +1 -0
  77. package/script/src/formats/gif.js +389 -0
  78. package/script/src/formats/jpeg.d.ts +18 -0
  79. package/script/src/formats/jpeg.d.ts.map +1 -0
  80. package/script/src/formats/jpeg.js +451 -0
  81. package/script/src/formats/png.d.ts +33 -0
  82. package/script/src/formats/png.d.ts.map +1 -0
  83. package/script/src/formats/png.js +548 -0
  84. package/script/src/formats/raw.d.ts +23 -0
  85. package/script/src/formats/raw.d.ts.map +1 -0
  86. package/script/src/formats/raw.js +102 -0
  87. package/script/src/formats/tiff.d.ts +58 -0
  88. package/script/src/formats/tiff.d.ts.map +1 -0
  89. package/script/src/formats/tiff.js +795 -0
  90. package/script/src/formats/webp.d.ts +22 -0
  91. package/script/src/formats/webp.d.ts.map +1 -0
  92. package/script/src/formats/webp.js +440 -0
  93. package/script/src/image.d.ts +124 -0
  94. package/script/src/image.d.ts.map +1 -0
  95. package/script/src/image.js +324 -0
  96. package/script/src/types.d.ts +167 -0
  97. package/script/src/types.d.ts.map +1 -0
  98. package/script/src/types.js +2 -0
  99. package/script/src/utils/gif_decoder.d.ts +42 -0
  100. package/script/src/utils/gif_decoder.d.ts.map +1 -0
  101. package/script/src/utils/gif_decoder.js +378 -0
  102. package/script/src/utils/gif_encoder.d.ts +29 -0
  103. package/script/src/utils/gif_encoder.d.ts.map +1 -0
  104. package/script/src/utils/gif_encoder.js +230 -0
  105. package/script/src/utils/jpeg_decoder.d.ts +39 -0
  106. package/script/src/utils/jpeg_decoder.d.ts.map +1 -0
  107. package/script/src/utils/jpeg_decoder.js +584 -0
  108. package/script/src/utils/jpeg_encoder.d.ts +33 -0
  109. package/script/src/utils/jpeg_encoder.d.ts.map +1 -0
  110. package/script/src/utils/jpeg_encoder.js +1021 -0
  111. package/script/src/utils/lzw.d.ts +43 -0
  112. package/script/src/utils/lzw.d.ts.map +1 -0
  113. package/script/src/utils/lzw.js +314 -0
  114. package/script/src/utils/resize.d.ts +9 -0
  115. package/script/src/utils/resize.d.ts.map +1 -0
  116. package/script/src/utils/resize.js +56 -0
  117. package/script/src/utils/tiff_lzw.d.ts +44 -0
  118. package/script/src/utils/tiff_lzw.d.ts.map +1 -0
  119. package/script/src/utils/tiff_lzw.js +311 -0
  120. package/script/src/utils/webp_decoder.d.ts +39 -0
  121. package/script/src/utils/webp_decoder.d.ts.map +1 -0
  122. package/script/src/utils/webp_decoder.js +497 -0
  123. package/script/src/utils/webp_encoder.d.ts +72 -0
  124. package/script/src/utils/webp_encoder.d.ts.map +1 -0
  125. package/script/src/utils/webp_encoder.js +631 -0
@@ -0,0 +1,631 @@
1
+ "use strict";
2
+ /**
3
+ * WebP VP8L (Lossless) encoder implementation with quality-based quantization
4
+ *
5
+ * This module implements a pure JavaScript encoder for WebP lossless (VP8L) format.
6
+ * It supports:
7
+ * - Lossless encoding (quality=100) with Huffman coding
8
+ * - Lossy encoding (quality<100) using color quantization while still using VP8L format
9
+ * - Simple Huffman coding (1-2 symbols per channel)
10
+ * - Complex Huffman coding for channels with many unique values (3+ symbols)
11
+ * - Literal pixel encoding (no transforms applied)
12
+ *
13
+ * Current limitations:
14
+ * - Does not use transforms (predictor, color, subtract green, color indexing)
15
+ * - Does not use LZ77 backward references (planned for future)
16
+ * - Does not use color cache (planned for future)
17
+ * - Lossy mode uses simple quantization, not true VP8 lossy encoding
18
+ * - Intended as a fallback when OffscreenCanvas is not available
19
+ *
20
+ * This encoder produces valid WebP lossless files with optional quality-based
21
+ * color quantization for lossy compression. For true VP8 lossy encoding with
22
+ * better compression, use the runtime's OffscreenCanvas API when available.
23
+ *
24
+ * @see https://developers.google.com/speed/webp/docs/riff_container
25
+ * @see https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.WebPEncoder = void 0;
29
+ // Helper to write little-endian values
30
+ function writeUint32LE(value) {
31
+ return [
32
+ value & 0xff,
33
+ (value >> 8) & 0xff,
34
+ (value >> 16) & 0xff,
35
+ (value >> 24) & 0xff,
36
+ ];
37
+ }
38
+ // Bit writer for encoding (matches the WebP decoder's bit reading order)
39
+ class BitWriter {
40
+ constructor() {
41
+ Object.defineProperty(this, "bytes", {
42
+ enumerable: true,
43
+ configurable: true,
44
+ writable: true,
45
+ value: []
46
+ });
47
+ Object.defineProperty(this, "bits", {
48
+ enumerable: true,
49
+ configurable: true,
50
+ writable: true,
51
+ value: 0
52
+ });
53
+ Object.defineProperty(this, "bitCount", {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: 0
58
+ });
59
+ }
60
+ writeBits(value, numBits) {
61
+ // Pack bits to match how the decoder reads them
62
+ // The decoder reads from MSB to LSB of each byte
63
+ // So we write from MSB down as well
64
+ for (let i = 0; i < numBits; i++) {
65
+ const bit = (value >> i) & 1;
66
+ // Write bit at the current position (counting from MSB)
67
+ // bitCount represents how many bits we've written
68
+ // Position in current byte = 7 - (bitCount % 8)
69
+ if (this.bitCount % 8 === 0) {
70
+ this.bits = 0; // Start new byte
71
+ }
72
+ const bitPos = 7 - (this.bitCount % 8);
73
+ this.bits |= bit << bitPos;
74
+ this.bitCount++;
75
+ if (this.bitCount % 8 === 0) {
76
+ this.bytes.push(this.bits);
77
+ }
78
+ }
79
+ }
80
+ flush() {
81
+ if (this.bitCount % 8 !== 0) {
82
+ this.bytes.push(this.bits);
83
+ this.bits = 0;
84
+ this.bitCount = 0;
85
+ }
86
+ }
87
+ getBytes() {
88
+ return new Uint8Array(this.bytes);
89
+ }
90
+ getLength() {
91
+ return this.bytes.length;
92
+ }
93
+ }
94
+ class WebPEncoder {
95
+ constructor(width, height, rgba) {
96
+ Object.defineProperty(this, "width", {
97
+ enumerable: true,
98
+ configurable: true,
99
+ writable: true,
100
+ value: void 0
101
+ });
102
+ Object.defineProperty(this, "height", {
103
+ enumerable: true,
104
+ configurable: true,
105
+ writable: true,
106
+ value: void 0
107
+ });
108
+ Object.defineProperty(this, "data", {
109
+ enumerable: true,
110
+ configurable: true,
111
+ writable: true,
112
+ value: void 0
113
+ });
114
+ Object.defineProperty(this, "quality", {
115
+ enumerable: true,
116
+ configurable: true,
117
+ writable: true,
118
+ value: void 0
119
+ });
120
+ this.width = width;
121
+ this.height = height;
122
+ this.data = rgba;
123
+ this.quality = 100; // Default to lossless
124
+ }
125
+ encode(quality = 100) {
126
+ this.quality = Math.max(1, Math.min(100, quality));
127
+ // Build RIFF container
128
+ const output = [];
129
+ // RIFF header
130
+ output.push(0x52, 0x49, 0x46, 0x46); // "RIFF"
131
+ // File size placeholder (will be filled later)
132
+ const fileSizePos = output.length;
133
+ output.push(0, 0, 0, 0);
134
+ // WebP signature
135
+ output.push(0x57, 0x45, 0x42, 0x50); // "WEBP"
136
+ // VP8L chunk
137
+ output.push(0x56, 0x50, 0x38, 0x4c); // "VP8L"
138
+ // VP8L chunk size placeholder
139
+ const vp8lSizePos = output.length;
140
+ output.push(0, 0, 0, 0);
141
+ // Encode VP8L data
142
+ const vp8lData = this.encodeVP8L();
143
+ for (let i = 0; i < vp8lData.length; i++) {
144
+ output.push(vp8lData[i]);
145
+ }
146
+ // Add padding if chunk size is odd
147
+ if (vp8lData.length % 2 === 1) {
148
+ output.push(0);
149
+ }
150
+ // Update VP8L chunk size
151
+ const vp8lSize = vp8lData.length;
152
+ const vp8lSizeBytes = writeUint32LE(vp8lSize);
153
+ for (let i = 0; i < 4; i++) {
154
+ output[vp8lSizePos + i] = vp8lSizeBytes[i];
155
+ }
156
+ // Update file size (everything after RIFF header except the size field itself)
157
+ const fileSize = output.length - 8;
158
+ const fileSizeBytes = writeUint32LE(fileSize);
159
+ for (let i = 0; i < 4; i++) {
160
+ output[fileSizePos + i] = fileSizeBytes[i];
161
+ }
162
+ return new Uint8Array(output);
163
+ }
164
+ encodeVP8L() {
165
+ const output = [];
166
+ // VP8L signature (0x2f)
167
+ output.push(0x2f);
168
+ // Width, height, alpha, version (packed into 4 bytes)
169
+ // Width: 14 bits, Height: 14 bits, Alpha: 1 bit, Version: 3 bits
170
+ const hasAlpha = this.hasAlphaChannel() ? 1 : 0;
171
+ const bits = ((this.width - 1) & 0x3fff) |
172
+ (((this.height - 1) & 0x3fff) << 14) |
173
+ (hasAlpha << 28) |
174
+ (0 << 29); // version = 0
175
+ output.push(...writeUint32LE(bits));
176
+ // Encode image data
177
+ const imageData = this.encodeImageData(hasAlpha);
178
+ for (let i = 0; i < imageData.length; i++) {
179
+ output.push(imageData[i]);
180
+ }
181
+ return output;
182
+ }
183
+ hasAlphaChannel() {
184
+ // Check if any pixel has alpha != 255
185
+ for (let i = 3; i < this.data.length; i += 4) {
186
+ if (this.data[i] !== 255) {
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ /**
193
+ * Quantize image data based on quality setting
194
+ * Quality 100 = no quantization (lossless)
195
+ * Quality 1-99 = quantize colors to reduce bit depth
196
+ * This creates a "lossy" effect while still using VP8L format
197
+ */
198
+ quantizeImageData() {
199
+ if (this.quality === 100) {
200
+ // No quantization for lossless
201
+ return this.data;
202
+ }
203
+ // Calculate quantization level based on quality
204
+ // Quality 90-99: very light quantization (shift by 1 bit)
205
+ // Quality 70-89: light quantization (shift by 2 bits)
206
+ // Quality 50-69: medium quantization (shift by 3 bits)
207
+ // Quality 30-49: heavy quantization (shift by 4 bits)
208
+ // Quality 1-29: very heavy quantization (shift by 5 bits)
209
+ let shift;
210
+ if (this.quality >= 90) {
211
+ shift = 1;
212
+ }
213
+ else if (this.quality >= 70) {
214
+ shift = 2;
215
+ }
216
+ else if (this.quality >= 50) {
217
+ shift = 3;
218
+ }
219
+ else if (this.quality >= 30) {
220
+ shift = 4;
221
+ }
222
+ else {
223
+ shift = 5;
224
+ }
225
+ // Create quantized copy of the image data
226
+ const quantized = new Uint8Array(this.data.length);
227
+ const mask = 0xFF << shift; // Bitmask for quantization
228
+ for (let i = 0; i < this.data.length; i += 4) {
229
+ // Quantize RGB channels using bitwise AND with mask
230
+ quantized[i] = this.data[i] & mask; // R
231
+ quantized[i + 1] = this.data[i + 1] & mask; // G
232
+ quantized[i + 2] = this.data[i + 2] & mask; // B
233
+ // Keep alpha channel unquantized for better transparency handling
234
+ quantized[i + 3] = this.data[i + 3]; // A
235
+ }
236
+ return quantized;
237
+ }
238
+ encodeImageData(hasAlpha) {
239
+ const writer = new BitWriter();
240
+ // No transforms
241
+ writer.writeBits(0, 1);
242
+ // Color cache - disabled for simplicity in this basic encoder
243
+ const _useColorCache = false;
244
+ writer.writeBits(0, 1);
245
+ // No meta Huffman codes
246
+ writer.writeBits(0, 1);
247
+ // Number of code groups: Always 5 (green, red, blue, alpha, distance)
248
+ // Even without LZ77, we must provide all 5 Huffman codes
249
+ const numCodeGroups = 5;
250
+ writer.writeBits(numCodeGroups - 4, 4); // 1 means 5 groups
251
+ // Apply quantization if quality < 100
252
+ const encodingData = this.quantizeImageData();
253
+ // Collect symbol frequencies for each channel
254
+ const greenFreqs = new Map();
255
+ const redFreqs = new Map();
256
+ const blueFreqs = new Map();
257
+ const alphaFreqs = new Map();
258
+ const numPixels = this.width * this.height;
259
+ for (let i = 0; i < numPixels; i++) {
260
+ const offset = i * 4;
261
+ const r = encodingData[offset];
262
+ const g = encodingData[offset + 1];
263
+ const b = encodingData[offset + 2];
264
+ const a = encodingData[offset + 3];
265
+ greenFreqs.set(g, (greenFreqs.get(g) || 0) + 1);
266
+ redFreqs.set(r, (redFreqs.get(r) || 0) + 1);
267
+ blueFreqs.set(b, (blueFreqs.get(b) || 0) + 1);
268
+ if (hasAlpha) {
269
+ alphaFreqs.set(a, (alphaFreqs.get(a) || 0) + 1);
270
+ }
271
+ }
272
+ // Build Huffman codes for each channel
273
+ // Use simple codes for 1-2 symbols, complex codes for more
274
+ // Green channel can have symbols 0-279 (256 literals + 24 length codes)
275
+ // Other channels can have symbols 0-255
276
+ const greenCodes = this.writeHuffmanCode(writer, greenFreqs, 256 + 24);
277
+ const redCodes = this.writeHuffmanCode(writer, redFreqs, 256);
278
+ const blueCodes = this.writeHuffmanCode(writer, blueFreqs, 256);
279
+ const alphaCodes = hasAlpha
280
+ ? this.writeHuffmanCode(writer, alphaFreqs, 256)
281
+ : this.writeHuffmanCode(writer, new Map([[255, numPixels]]), 256);
282
+ // Distance Huffman code (not used without LZ77, but required by spec)
283
+ // Distance symbols are 0-39
284
+ this.writeHuffmanCode(writer, new Map([[0, 1]]), 40);
285
+ // Encode pixels using the Huffman codes
286
+ for (let i = 0; i < numPixels; i++) {
287
+ const offset = i * 4;
288
+ const r = encodingData[offset];
289
+ const g = encodingData[offset + 1];
290
+ const b = encodingData[offset + 2];
291
+ const a = encodingData[offset + 3];
292
+ // Write each channel using its Huffman code
293
+ this.writeSymbol(writer, greenCodes, g);
294
+ this.writeSymbol(writer, redCodes, r);
295
+ this.writeSymbol(writer, blueCodes, b);
296
+ if (hasAlpha) {
297
+ this.writeSymbol(writer, alphaCodes, a);
298
+ }
299
+ }
300
+ writer.flush();
301
+ return Array.from(writer.getBytes());
302
+ }
303
+ /**
304
+ * Write Huffman code for a channel (either simple or complex)
305
+ * Returns the Huffman codes for encoding pixels
306
+ */
307
+ writeHuffmanCode(writer, frequencies, maxSymbol) {
308
+ const symbols = Array.from(frequencies.keys()).sort((a, b) => a - b);
309
+ if (symbols.length === 0) {
310
+ // No symbols - shouldn't happen, write single symbol of 0
311
+ this.writeSimpleHuffmanCode(writer, [0]);
312
+ return new Map([[0, { code: 0, length: 0 }]]);
313
+ }
314
+ else if (symbols.length === 1) {
315
+ // Single symbol - use simple code
316
+ this.writeSimpleHuffmanCode(writer, [symbols[0]]);
317
+ return new Map([[symbols[0], { code: 0, length: 0 }]]);
318
+ }
319
+ else if (symbols.length === 2) {
320
+ // Two symbols - use simple code
321
+ this.writeSimpleHuffmanCode(writer, symbols);
322
+ return new Map([
323
+ [symbols[0], { code: 0, length: 1 }],
324
+ [symbols[1], { code: 1, length: 1 }],
325
+ ]);
326
+ }
327
+ else {
328
+ // More than 2 symbols - use complex code
329
+ return this.writeComplexHuffmanCode(writer, frequencies, maxSymbol);
330
+ }
331
+ }
332
+ /**
333
+ * Write a symbol using its Huffman code
334
+ */
335
+ writeSymbol(writer, codes, symbol) {
336
+ const huffCode = codes.get(symbol);
337
+ if (!huffCode) {
338
+ throw new Error(`No Huffman code for symbol ${symbol}`);
339
+ }
340
+ // Code length 0 means single symbol (no bits to write)
341
+ if (huffCode.length === 0)
342
+ return;
343
+ // Write the Huffman code bits from MSB to LSB
344
+ // This matches how the decoder's addCode builds the tree
345
+ for (let i = huffCode.length - 1; i >= 0; i--) {
346
+ writer.writeBits((huffCode.code >> i) & 1, 1);
347
+ }
348
+ }
349
+ writeSimpleHuffmanCode(writer, symbols) {
350
+ if (symbols.length === 0) {
351
+ // Shouldn't happen, but write a single symbol of 0
352
+ symbols = [0];
353
+ }
354
+ // Simple code
355
+ writer.writeBits(1, 1);
356
+ if (symbols.length === 1) {
357
+ // Single symbol
358
+ writer.writeBits(0, 1); // num_symbols = 1 (0 + 1)
359
+ writer.writeBits(0, 1); // is_first_8bits = 0
360
+ writer.writeBits(symbols[0], 8); // symbol
361
+ }
362
+ else if (symbols.length === 2) {
363
+ // Two symbols
364
+ writer.writeBits(1, 1); // num_symbols = 2 (1 + 1)
365
+ writer.writeBits(0, 1); // is_first_8bits = 0
366
+ writer.writeBits(symbols[0], 8); // first symbol
367
+ writer.writeBits(symbols[1], 8); // second symbol
368
+ }
369
+ else {
370
+ // Should not reach here - caller should use complex Huffman for >2 symbols
371
+ throw new Error(`Simple Huffman code does not support ${symbols.length} symbols`);
372
+ }
373
+ }
374
+ /**
375
+ * Calculate optimal code lengths for symbols using standard Huffman algorithm
376
+ * Returns an array where index is the symbol and value is the code length
377
+ */
378
+ calculateCodeLengths(frequencies, maxSymbol, maxCodeLength = 15) {
379
+ const codeLengths = new Array(maxSymbol).fill(0);
380
+ // Get symbols with non-zero frequencies
381
+ const symbols = Array.from(frequencies.keys()).sort((a, b) => a - b);
382
+ if (symbols.length === 0)
383
+ return codeLengths;
384
+ // For a single symbol, use code length 0
385
+ if (symbols.length === 1) {
386
+ codeLengths[symbols[0]] = 0;
387
+ return codeLengths;
388
+ }
389
+ // For two symbols, use code length 1 for both
390
+ if (symbols.length === 2) {
391
+ codeLengths[symbols[0]] = 1;
392
+ codeLengths[symbols[1]] = 1;
393
+ return codeLengths;
394
+ }
395
+ const nodes = symbols.map((symbol) => ({
396
+ freq: frequencies.get(symbol),
397
+ symbol,
398
+ }));
399
+ // Build tree by repeatedly combining two smallest nodes
400
+ while (nodes.length > 1) {
401
+ // Sort by frequency (smallest first)
402
+ nodes.sort((a, b) => a.freq - b.freq);
403
+ // Take two smallest nodes
404
+ const left = nodes.shift();
405
+ const right = nodes.shift();
406
+ // Create parent node
407
+ const parent = {
408
+ freq: left.freq + right.freq,
409
+ left,
410
+ right,
411
+ };
412
+ nodes.push(parent);
413
+ }
414
+ // Calculate code lengths by traversing tree (iterative to avoid deep recursion)
415
+ const root = nodes[0];
416
+ const stack = [{
417
+ node: root,
418
+ depth: 0,
419
+ }];
420
+ while (stack.length > 0) {
421
+ const { node, depth } = stack.pop();
422
+ if (node.symbol !== undefined) {
423
+ codeLengths[node.symbol] = Math.min(depth, maxCodeLength);
424
+ }
425
+ else {
426
+ if (node.left)
427
+ stack.push({ node: node.left, depth: depth + 1 });
428
+ if (node.right)
429
+ stack.push({ node: node.right, depth: depth + 1 });
430
+ }
431
+ }
432
+ // Handle edge case: depth 0 occurs when we have a single node tree (e.g., 2 symbols)
433
+ // In canonical Huffman coding, all symbols must have length >= 1
434
+ for (const symbol of symbols) {
435
+ if (codeLengths[symbol] === 0) {
436
+ codeLengths[symbol] = 1;
437
+ }
438
+ }
439
+ return codeLengths;
440
+ }
441
+ /**
442
+ * Build canonical Huffman codes from code lengths
443
+ * Returns a map from symbol to {code, length}
444
+ */
445
+ buildCanonicalCodes(codeLengths) {
446
+ const codes = new Map();
447
+ // Find max code length (avoid spread operator for large arrays)
448
+ let maxLength = 0;
449
+ for (const length of codeLengths) {
450
+ if (length > maxLength) {
451
+ maxLength = length;
452
+ }
453
+ }
454
+ // Count symbols at each length
455
+ const lengthCounts = new Array(maxLength + 1).fill(0);
456
+ for (let i = 0; i < codeLengths.length; i++) {
457
+ if (codeLengths[i] > 0) {
458
+ lengthCounts[codeLengths[i]]++;
459
+ }
460
+ }
461
+ // Generate first code for each length
462
+ let code = 0;
463
+ const nextCode = new Array(maxLength + 1).fill(0);
464
+ for (let len = 1; len <= maxLength; len++) {
465
+ code = (code + lengthCounts[len - 1]) << 1;
466
+ nextCode[len] = code;
467
+ }
468
+ // Assign codes to symbols
469
+ for (let symbol = 0; symbol < codeLengths.length; symbol++) {
470
+ const length = codeLengths[symbol];
471
+ if (length > 0) {
472
+ codes.set(symbol, { code: nextCode[length], length });
473
+ nextCode[length]++;
474
+ }
475
+ }
476
+ return codes;
477
+ }
478
+ /**
479
+ * RLE encode code lengths using special codes 16, 17, 18
480
+ */
481
+ rleEncodeCodeLengths(codeLengths) {
482
+ const encoded = [];
483
+ let i = 0;
484
+ while (i < codeLengths.length) {
485
+ const length = codeLengths[i];
486
+ if (length === 0) {
487
+ // Count consecutive zeros
488
+ let count = 0;
489
+ while (i + count < codeLengths.length && codeLengths[i + count] === 0) {
490
+ count++;
491
+ }
492
+ // Encode runs of zeros
493
+ while (count > 0) {
494
+ if (count >= 11) {
495
+ // Use code 18 for 11-138 zeros
496
+ const runLength = Math.min(count, 138);
497
+ encoded.push(18, runLength - 11);
498
+ count -= runLength;
499
+ }
500
+ else if (count >= 3) {
501
+ // Use code 17 for 3-10 zeros
502
+ const runLength = Math.min(count, 10);
503
+ encoded.push(17, runLength - 3);
504
+ count -= runLength;
505
+ }
506
+ else {
507
+ // Literal zero (1-2 zeros)
508
+ encoded.push(0);
509
+ count--;
510
+ }
511
+ }
512
+ // Move past all the zeros we just encoded
513
+ while (i < codeLengths.length && codeLengths[i] === 0) {
514
+ i++;
515
+ }
516
+ }
517
+ else {
518
+ // Non-zero length
519
+ encoded.push(length);
520
+ i++;
521
+ // Check for repeating previous length
522
+ let count = 0;
523
+ while (i + count < codeLengths.length &&
524
+ codeLengths[i + count] === length &&
525
+ count < 6) {
526
+ count++;
527
+ }
528
+ if (count >= 3) {
529
+ // Use code 16 for 3-6 repetitions
530
+ encoded.push(16, count - 3);
531
+ i += count;
532
+ }
533
+ }
534
+ }
535
+ return encoded;
536
+ }
537
+ /**
538
+ * Write complex Huffman code using code lengths
539
+ */
540
+ writeComplexHuffmanCode(writer, frequencies, maxSymbol) {
541
+ // Calculate optimal code lengths
542
+ const codeLengths = this.calculateCodeLengths(frequencies, maxSymbol);
543
+ // Build canonical codes
544
+ const codes = this.buildCanonicalCodes(codeLengths);
545
+ // Write complex code indicator
546
+ writer.writeBits(0, 1); // Not simple
547
+ // RLE encode code lengths
548
+ const rleEncoded = this.rleEncodeCodeLengths(codeLengths);
549
+ // Build code length codes - count frequency of each code in RLE stream
550
+ const codeLengthFreqs = new Map();
551
+ for (let i = 0; i < rleEncoded.length; i++) {
552
+ const code = rleEncoded[i];
553
+ codeLengthFreqs.set(code, (codeLengthFreqs.get(code) || 0) + 1);
554
+ // Skip extra bits for codes 16, 17, 18
555
+ if (code === 16 || code === 17 || code === 18) {
556
+ i++; // Skip the extra parameter
557
+ }
558
+ }
559
+ // Calculate code lengths for the code length alphabet (max 19 symbols)
560
+ const codeLengthCodeLengths = this.calculateCodeLengths(codeLengthFreqs, 19, 7); // Max 7 bits
561
+ // Code length code order (matches decoder)
562
+ const codeLengthCodeOrder = [
563
+ 17,
564
+ 18,
565
+ 0,
566
+ 1,
567
+ 2,
568
+ 3,
569
+ 4,
570
+ 5,
571
+ 16,
572
+ 6,
573
+ 7,
574
+ 8,
575
+ 9,
576
+ 10,
577
+ 11,
578
+ 12,
579
+ 13,
580
+ 14,
581
+ 15,
582
+ ];
583
+ // Find number of code length codes to write (trim trailing zeros)
584
+ let numCodeLengthCodes = 19;
585
+ for (let i = 18; i >= 4; i--) {
586
+ if (codeLengthCodeLengths[codeLengthCodeOrder[i]] === 0) {
587
+ numCodeLengthCodes = i;
588
+ }
589
+ else {
590
+ break;
591
+ }
592
+ }
593
+ numCodeLengthCodes = Math.max(4, numCodeLengthCodes);
594
+ // Write number of code length codes
595
+ writer.writeBits(numCodeLengthCodes - 4, 4);
596
+ // Write code length code lengths
597
+ for (let i = 0; i < numCodeLengthCodes; i++) {
598
+ writer.writeBits(codeLengthCodeLengths[codeLengthCodeOrder[i]], 3);
599
+ }
600
+ // Build canonical codes for code lengths
601
+ const codeLengthCodes = this.buildCanonicalCodes(codeLengthCodeLengths);
602
+ // Write RLE-encoded code lengths using code length codes
603
+ for (let i = 0; i < rleEncoded.length; i++) {
604
+ const code = rleEncoded[i];
605
+ const huffCode = codeLengthCodes.get(code);
606
+ if (!huffCode) {
607
+ throw new Error(`No Huffman code for symbol ${code}`);
608
+ }
609
+ // Write the Huffman code bits from MSB to LSB
610
+ // This matches how the decoder's addCode builds the tree
611
+ for (let bit = huffCode.length - 1; bit >= 0; bit--) {
612
+ writer.writeBits((huffCode.code >> bit) & 1, 1);
613
+ }
614
+ // Write extra bits for special codes
615
+ if (code === 16) {
616
+ // 2 extra bits for repeat count (3-6)
617
+ writer.writeBits(rleEncoded[++i], 2);
618
+ }
619
+ else if (code === 17) {
620
+ // 3 extra bits for zero run (3-10)
621
+ writer.writeBits(rleEncoded[++i], 3);
622
+ }
623
+ else if (code === 18) {
624
+ // 7 extra bits for zero run (11-138)
625
+ writer.writeBits(rleEncoded[++i], 7);
626
+ }
627
+ }
628
+ return codes;
629
+ }
630
+ }
631
+ exports.WebPEncoder = WebPEncoder;