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