cross-image 0.1.5 → 0.2.1

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 (72) hide show
  1. package/README.md +36 -18
  2. package/esm/mod.d.ts +30 -4
  3. package/esm/mod.js +30 -4
  4. package/esm/src/formats/apng.d.ts +50 -0
  5. package/esm/src/formats/apng.js +364 -0
  6. package/esm/src/formats/bmp.d.ts +0 -6
  7. package/esm/src/formats/bmp.js +24 -47
  8. package/esm/src/formats/dng.d.ts +27 -0
  9. package/esm/src/formats/dng.js +191 -0
  10. package/esm/src/formats/gif.d.ts +0 -2
  11. package/esm/src/formats/gif.js +10 -16
  12. package/esm/src/formats/ico.d.ts +41 -0
  13. package/esm/src/formats/ico.js +214 -0
  14. package/esm/src/formats/pam.d.ts +43 -0
  15. package/esm/src/formats/pam.js +177 -0
  16. package/esm/src/formats/pcx.d.ts +13 -0
  17. package/esm/src/formats/pcx.js +204 -0
  18. package/esm/src/formats/png.d.ts +2 -21
  19. package/esm/src/formats/png.js +5 -429
  20. package/esm/src/formats/png_base.d.ts +108 -0
  21. package/esm/src/formats/png_base.js +487 -0
  22. package/esm/src/formats/tiff.d.ts +7 -7
  23. package/esm/src/formats/webp.d.ts +0 -1
  24. package/esm/src/formats/webp.js +4 -7
  25. package/esm/src/image.d.ts +99 -0
  26. package/esm/src/image.js +204 -2
  27. package/esm/src/utils/byte_utils.d.ts +30 -0
  28. package/esm/src/utils/byte_utils.js +50 -0
  29. package/esm/src/utils/gif_encoder.d.ts +3 -2
  30. package/esm/src/utils/gif_encoder.js +115 -48
  31. package/esm/src/utils/image_processing.d.ts +91 -0
  32. package/esm/src/utils/image_processing.js +231 -0
  33. package/esm/src/utils/webp_decoder.js +47 -12
  34. package/esm/src/utils/webp_encoder.js +97 -39
  35. package/package.json +4 -1
  36. package/script/mod.d.ts +30 -4
  37. package/script/mod.js +36 -6
  38. package/script/src/formats/apng.d.ts +50 -0
  39. package/script/src/formats/apng.js +368 -0
  40. package/script/src/formats/bmp.d.ts +0 -6
  41. package/script/src/formats/bmp.js +24 -47
  42. package/script/src/formats/dng.d.ts +27 -0
  43. package/script/src/formats/dng.js +195 -0
  44. package/script/src/formats/gif.d.ts +0 -2
  45. package/script/src/formats/gif.js +10 -16
  46. package/script/src/formats/ico.d.ts +41 -0
  47. package/script/src/formats/ico.js +218 -0
  48. package/script/src/formats/pam.d.ts +43 -0
  49. package/script/src/formats/pam.js +181 -0
  50. package/script/src/formats/pcx.d.ts +13 -0
  51. package/script/src/formats/pcx.js +208 -0
  52. package/script/src/formats/png.d.ts +2 -21
  53. package/script/src/formats/png.js +5 -429
  54. package/script/src/formats/png_base.d.ts +108 -0
  55. package/script/src/formats/png_base.js +491 -0
  56. package/script/src/formats/tiff.d.ts +7 -7
  57. package/script/src/formats/webp.d.ts +0 -1
  58. package/script/src/formats/webp.js +4 -7
  59. package/script/src/image.d.ts +99 -0
  60. package/script/src/image.js +204 -2
  61. package/script/src/utils/byte_utils.d.ts +30 -0
  62. package/script/src/utils/byte_utils.js +58 -0
  63. package/script/src/utils/gif_encoder.d.ts +3 -2
  64. package/script/src/utils/gif_encoder.js +115 -48
  65. package/script/src/utils/image_processing.d.ts +91 -0
  66. package/script/src/utils/image_processing.js +242 -0
  67. package/script/src/utils/webp_decoder.js +47 -12
  68. package/script/src/utils/webp_encoder.js +97 -39
  69. package/esm/src/formats/raw.d.ts +0 -40
  70. package/esm/src/formats/raw.js +0 -118
  71. package/script/src/formats/raw.d.ts +0 -40
  72. package/script/src/formats/raw.js +0 -122
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ /**
3
+ * Image processing utilities for common operations like compositing,
4
+ * level adjustments, and color manipulations.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.composite = composite;
8
+ exports.adjustBrightness = adjustBrightness;
9
+ exports.adjustContrast = adjustContrast;
10
+ exports.adjustExposure = adjustExposure;
11
+ exports.adjustSaturation = adjustSaturation;
12
+ exports.invert = invert;
13
+ exports.grayscale = grayscale;
14
+ exports.fillRect = fillRect;
15
+ exports.crop = crop;
16
+ /**
17
+ * Composite one image on top of another at a specified position
18
+ * @param base Base image data (RGBA)
19
+ * @param baseWidth Base image width
20
+ * @param baseHeight Base image height
21
+ * @param overlay Overlay image data (RGBA)
22
+ * @param overlayWidth Overlay image width
23
+ * @param overlayHeight Overlay image height
24
+ * @param x X position to place overlay (can be negative)
25
+ * @param y Y position to place overlay (can be negative)
26
+ * @param opacity Opacity of overlay (0-1, default: 1)
27
+ * @returns New image data with overlay composited on base
28
+ */
29
+ function composite(base, baseWidth, baseHeight, overlay, overlayWidth, overlayHeight, x, y, opacity = 1) {
30
+ const result = new Uint8Array(base);
31
+ // Clamp opacity to valid range
32
+ const finalOpacity = Math.max(0, Math.min(1, opacity));
33
+ // Calculate the region to composite
34
+ const startX = Math.max(0, x);
35
+ const startY = Math.max(0, y);
36
+ const endX = Math.min(baseWidth, x + overlayWidth);
37
+ const endY = Math.min(baseHeight, y + overlayHeight);
38
+ // Iterate over the overlapping region
39
+ for (let py = startY; py < endY; py++) {
40
+ for (let px = startX; px < endX; px++) {
41
+ // Calculate indices
42
+ const baseIdx = (py * baseWidth + px) * 4;
43
+ const overlayX = px - x;
44
+ const overlayY = py - y;
45
+ const overlayIdx = (overlayY * overlayWidth + overlayX) * 4;
46
+ // Get overlay pixel with opacity
47
+ const overlayR = overlay[overlayIdx];
48
+ const overlayG = overlay[overlayIdx + 1];
49
+ const overlayB = overlay[overlayIdx + 2];
50
+ const overlayA = (overlay[overlayIdx + 3] / 255) * finalOpacity;
51
+ // Get base pixel
52
+ const baseR = result[baseIdx];
53
+ const baseG = result[baseIdx + 1];
54
+ const baseB = result[baseIdx + 2];
55
+ const baseA = result[baseIdx + 3] / 255;
56
+ // Alpha compositing using "over" operation
57
+ const outA = overlayA + baseA * (1 - overlayA);
58
+ if (outA > 0) {
59
+ result[baseIdx] = Math.round((overlayR * overlayA + baseR * baseA * (1 - overlayA)) / outA);
60
+ result[baseIdx + 1] = Math.round((overlayG * overlayA + baseG * baseA * (1 - overlayA)) / outA);
61
+ result[baseIdx + 2] = Math.round((overlayB * overlayA + baseB * baseA * (1 - overlayA)) / outA);
62
+ result[baseIdx + 3] = Math.round(outA * 255);
63
+ }
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+ /**
69
+ * Adjust brightness of an image
70
+ * @param data Image data (RGBA)
71
+ * @param amount Brightness adjustment (-1 to 1, where 0 is no change)
72
+ * @returns New image data with adjusted brightness
73
+ */
74
+ function adjustBrightness(data, amount) {
75
+ const result = new Uint8Array(data.length);
76
+ const adjust = Math.max(-1, Math.min(1, amount)) * 255;
77
+ for (let i = 0; i < data.length; i += 4) {
78
+ result[i] = Math.max(0, Math.min(255, data[i] + adjust)); // R
79
+ result[i + 1] = Math.max(0, Math.min(255, data[i + 1] + adjust)); // G
80
+ result[i + 2] = Math.max(0, Math.min(255, data[i + 2] + adjust)); // B
81
+ result[i + 3] = data[i + 3]; // A
82
+ }
83
+ return result;
84
+ }
85
+ /**
86
+ * Adjust contrast of an image
87
+ * @param data Image data (RGBA)
88
+ * @param amount Contrast adjustment (-1 to 1, where 0 is no change)
89
+ * @returns New image data with adjusted contrast
90
+ */
91
+ function adjustContrast(data, amount) {
92
+ const result = new Uint8Array(data.length);
93
+ const contrast = Math.max(-1, Math.min(1, amount));
94
+ const factor = (259 * (contrast * 255 + 255)) /
95
+ (255 * (259 - contrast * 255));
96
+ for (let i = 0; i < data.length; i += 4) {
97
+ result[i] = Math.max(0, Math.min(255, factor * (data[i] - 128) + 128)); // R
98
+ result[i + 1] = Math.max(0, Math.min(255, factor * (data[i + 1] - 128) + 128)); // G
99
+ result[i + 2] = Math.max(0, Math.min(255, factor * (data[i + 2] - 128) + 128)); // B
100
+ result[i + 3] = data[i + 3]; // A
101
+ }
102
+ return result;
103
+ }
104
+ /**
105
+ * Adjust exposure of an image
106
+ * @param data Image data (RGBA)
107
+ * @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
108
+ * @returns New image data with adjusted exposure
109
+ */
110
+ function adjustExposure(data, amount) {
111
+ const result = new Uint8Array(data.length);
112
+ const stops = Math.max(-3, Math.min(3, amount));
113
+ const multiplier = Math.pow(2, stops);
114
+ for (let i = 0; i < data.length; i += 4) {
115
+ result[i] = Math.max(0, Math.min(255, data[i] * multiplier)); // R
116
+ result[i + 1] = Math.max(0, Math.min(255, data[i + 1] * multiplier)); // G
117
+ result[i + 2] = Math.max(0, Math.min(255, data[i + 2] * multiplier)); // B
118
+ result[i + 3] = data[i + 3]; // A
119
+ }
120
+ return result;
121
+ }
122
+ /**
123
+ * Adjust saturation of an image
124
+ * @param data Image data (RGBA)
125
+ * @param amount Saturation adjustment (-1 to 1, where 0 is no change)
126
+ * @returns New image data with adjusted saturation
127
+ */
128
+ function adjustSaturation(data, amount) {
129
+ const result = new Uint8Array(data.length);
130
+ const sat = Math.max(-1, Math.min(1, amount)) + 1; // Convert to 0-2 range
131
+ for (let i = 0; i < data.length; i += 4) {
132
+ const r = data[i];
133
+ const g = data[i + 1];
134
+ const b = data[i + 2];
135
+ // Calculate grayscale value using luminosity method
136
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
137
+ // Interpolate between gray and original color based on saturation
138
+ result[i] = Math.max(0, Math.min(255, gray + (r - gray) * sat));
139
+ result[i + 1] = Math.max(0, Math.min(255, gray + (g - gray) * sat));
140
+ result[i + 2] = Math.max(0, Math.min(255, gray + (b - gray) * sat));
141
+ result[i + 3] = data[i + 3];
142
+ }
143
+ return result;
144
+ }
145
+ /**
146
+ * Invert colors of an image
147
+ * @param data Image data (RGBA)
148
+ * @returns New image data with inverted colors
149
+ */
150
+ function invert(data) {
151
+ const result = new Uint8Array(data.length);
152
+ for (let i = 0; i < data.length; i += 4) {
153
+ result[i] = 255 - data[i]; // R
154
+ result[i + 1] = 255 - data[i + 1]; // G
155
+ result[i + 2] = 255 - data[i + 2]; // B
156
+ result[i + 3] = data[i + 3]; // A
157
+ }
158
+ return result;
159
+ }
160
+ /**
161
+ * Convert image to grayscale
162
+ * @param data Image data (RGBA)
163
+ * @returns New image data in grayscale
164
+ */
165
+ function grayscale(data) {
166
+ const result = new Uint8Array(data.length);
167
+ for (let i = 0; i < data.length; i += 4) {
168
+ // Using luminosity method for grayscale conversion
169
+ const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
170
+ result[i] = gray; // R
171
+ result[i + 1] = gray; // G
172
+ result[i + 2] = gray; // B
173
+ result[i + 3] = data[i + 3]; // A
174
+ }
175
+ return result;
176
+ }
177
+ /**
178
+ * Fill a rectangular region with a color
179
+ * @param data Image data (RGBA)
180
+ * @param width Image width
181
+ * @param height Image height
182
+ * @param x Starting X position
183
+ * @param y Starting Y position
184
+ * @param fillWidth Width of the fill region
185
+ * @param fillHeight Height of the fill region
186
+ * @param r Red component (0-255)
187
+ * @param g Green component (0-255)
188
+ * @param b Blue component (0-255)
189
+ * @param a Alpha component (0-255)
190
+ * @returns Modified image data
191
+ */
192
+ function fillRect(data, width, height, x, y, fillWidth, fillHeight, r, g, b, a) {
193
+ const result = new Uint8Array(data);
194
+ // Calculate bounds
195
+ const startX = Math.max(0, x);
196
+ const startY = Math.max(0, y);
197
+ const endX = Math.min(width, x + fillWidth);
198
+ const endY = Math.min(height, y + fillHeight);
199
+ // Fill the region
200
+ for (let py = startY; py < endY; py++) {
201
+ for (let px = startX; px < endX; px++) {
202
+ const idx = (py * width + px) * 4;
203
+ result[idx] = r;
204
+ result[idx + 1] = g;
205
+ result[idx + 2] = b;
206
+ result[idx + 3] = a;
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+ /**
212
+ * Crop an image to a rectangular region
213
+ * @param data Image data (RGBA)
214
+ * @param width Image width
215
+ * @param height Image height
216
+ * @param x Starting X position
217
+ * @param y Starting Y position
218
+ * @param cropWidth Width of the crop region
219
+ * @param cropHeight Height of the crop region
220
+ * @returns Cropped image data and dimensions
221
+ */
222
+ function crop(data, width, height, x, y, cropWidth, cropHeight) {
223
+ // Clamp crop region to image bounds
224
+ const startX = Math.max(0, x);
225
+ const startY = Math.max(0, y);
226
+ const endX = Math.min(width, x + cropWidth);
227
+ const endY = Math.min(height, y + cropHeight);
228
+ const actualWidth = endX - startX;
229
+ const actualHeight = endY - startY;
230
+ const result = new Uint8Array(actualWidth * actualHeight * 4);
231
+ for (let py = 0; py < actualHeight; py++) {
232
+ for (let px = 0; px < actualWidth; px++) {
233
+ const srcIdx = ((startY + py) * width + (startX + px)) * 4;
234
+ const dstIdx = (py * actualWidth + px) * 4;
235
+ result[dstIdx] = data[srcIdx];
236
+ result[dstIdx + 1] = data[srcIdx + 1];
237
+ result[dstIdx + 2] = data[srcIdx + 2];
238
+ result[dstIdx + 3] = data[srcIdx + 3];
239
+ }
240
+ }
241
+ return { data: result, width: actualWidth, height: actualHeight };
242
+ }
@@ -54,6 +54,22 @@ class HuffmanTable {
54
54
  this.singleSymbol = symbol;
55
55
  return;
56
56
  }
57
+ // Build Huffman tree
58
+ // Note: WebP uses LSB-first bit packing for the bitstream, but Huffman codes
59
+ // are typically described MSB-first. However, the spec says:
60
+ // "The bits of the code are read from the stream LSB first."
61
+ // This means if code is 01 (binary), we read 1 then 0?
62
+ // Actually, standard canonical Huffman codes are read MSB to LSB from the stream.
63
+ // But WebP bit reader reads LSB to MSB from bytes.
64
+ // Let's check the spec carefully.
65
+ // "The bits of the code are read from the stream LSB first."
66
+ // This usually means the bit reader returns bits in order.
67
+ // If we have code 0x2 (binary 10) with length 2.
68
+ // If we write it MSB first: 1, then 0.
69
+ // If we read it: readBits(1) -> 1, readBits(1) -> 0.
70
+ // This matches how we build the tree (left=0, right=1).
71
+ // Wait, addCode uses (code >> i) & 1. This is MSB first.
72
+ // So we expect the first bit read to be the MSB of the code.
57
73
  let node = this.root;
58
74
  for (let i = codeLength - 1; i >= 0; i--) {
59
75
  const bit = (code >> i) & 1;
@@ -80,6 +96,7 @@ class HuffmanTable {
80
96
  const bit = reader.readBits(1);
81
97
  node = bit === 0 ? node.left : node.right;
82
98
  if (!node) {
99
+ // console.log("Invalid Huffman code - walked off tree");
83
100
  throw new Error("Invalid Huffman code");
84
101
  }
85
102
  }
@@ -114,21 +131,21 @@ class BitReader {
114
131
  });
115
132
  this.data = data;
116
133
  this.pos = offset;
117
- this.bitPos = 0;
134
+ this.bitPos = 8; // Start at 8 to trigger first byte read
118
135
  this.value = 0;
119
136
  }
120
137
  readBits(numBits) {
121
138
  let result = 0;
122
139
  for (let i = 0; i < numBits; i++) {
123
- if (this.bitPos === 0) {
140
+ if (this.bitPos === 8) {
124
141
  if (this.pos >= this.data.length) {
125
142
  throw new Error("Unexpected end of data");
126
143
  }
127
144
  this.value = this.data[this.pos++];
128
- this.bitPos = 8;
145
+ this.bitPos = 0;
129
146
  }
130
- result |= ((this.value >> (this.bitPos - 1)) & 1) << i;
131
- this.bitPos--;
147
+ result |= ((this.value >> this.bitPos) & 1) << i;
148
+ this.bitPos++;
132
149
  }
133
150
  return result;
134
151
  }
@@ -138,8 +155,8 @@ class BitReader {
138
155
  // Read bytes aligned to byte boundary
139
156
  readBytes(count) {
140
157
  // Align to byte boundary
141
- if (this.bitPos !== 0 && this.bitPos !== 8) {
142
- this.bitPos = 0;
158
+ if (this.bitPos !== 8) {
159
+ this.bitPos = 8; // Skip remaining bits in current byte
143
160
  }
144
161
  if (this.pos + count > this.data.length) {
145
162
  throw new Error("Unexpected end of data");
@@ -337,8 +354,6 @@ class WebPDecoder {
337
354
  }
338
355
  // Read the main Huffman codes
339
356
  // There are 5 Huffman code groups: green, red, blue, alpha, distance
340
- // But we read 4 + optional distance code
341
- const numCodeGroups = reader.readBits(4) + 4;
342
357
  const tables = {
343
358
  green: new HuffmanTable(),
344
359
  red: new HuffmanTable(),
@@ -353,7 +368,7 @@ class WebPDecoder {
353
368
  tables.alpha,
354
369
  tables.distance,
355
370
  ];
356
- for (let i = 0; i < numCodeGroups && i < 5; i++) {
371
+ for (let i = 0; i < 5; i++) {
357
372
  this.readHuffmanCode(reader, tableArray[i], useColorCache, colorCacheBits, i === 0);
358
373
  }
359
374
  return tables;
@@ -367,8 +382,8 @@ class WebPDecoder {
367
382
  const symbols = [];
368
383
  for (let i = 0; i < numSymbols; i++) {
369
384
  const symbolBits = isFirstEightBits
370
- ? 1 + reader.readBits(7)
371
- : reader.readBits(8);
385
+ ? reader.readBits(8)
386
+ : reader.readBits(1);
372
387
  symbols.push(symbolBits);
373
388
  }
374
389
  // Build simple Huffman table
@@ -420,6 +435,12 @@ class WebPDecoder {
420
435
  for (let i = 0; i < numCodeLengthCodes; i++) {
421
436
  codeLengthCodeLengths[codeLengthCodeOrder[i]] = reader.readBits(3);
422
437
  }
438
+ // Read max_symbol (trimmed length indicator)
439
+ // If 1, we read n_bit and then n_bit bits for max_symbol?
440
+ // Subagent said "write_trimmed_length".
441
+ // If 0, we don't trim.
442
+ // We just read 1 bit and ignore it for now (assuming 0).
443
+ const _trimmed = reader.readBits(1);
423
444
  // Build code length Huffman table
424
445
  const codeLengthTable = new HuffmanTable();
425
446
  this.buildHuffmanTable(codeLengthTable, codeLengthCodeLengths);
@@ -454,6 +475,20 @@ class WebPDecoder {
454
475
  return codeLengths;
455
476
  }
456
477
  buildHuffmanTable(table, codeLengths) {
478
+ // Check for single symbol optimization (VP8L specific)
479
+ let nonZeroCount = 0;
480
+ let singleSymbol = -1;
481
+ for (let i = 0; i < codeLengths.length; i++) {
482
+ if (codeLengths[i] > 0) {
483
+ nonZeroCount++;
484
+ singleSymbol = i;
485
+ }
486
+ }
487
+ if (nonZeroCount === 1) {
488
+ // If only one symbol, it has 0 length in the bitstream
489
+ table.addCode(singleSymbol, 0, 0);
490
+ return;
491
+ }
457
492
  // Build canonical Huffman codes
458
493
  const maxCodeLength = Math.max(...codeLengths);
459
494
  const lengthCounts = new Array(maxCodeLength + 1).fill(0);
@@ -58,30 +58,30 @@ class BitWriter {
58
58
  });
59
59
  }
60
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
61
+ // Pack bits LSB first (standard WebP/VP8L order)
64
62
  for (let i = 0; i < numBits; i++) {
65
63
  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
64
+ // If we've filled the current byte, push it and start a new one
65
+ if (this.bitCount > 0 && this.bitCount % 8 === 0) {
66
+ this.bytes.push(this.bits);
67
+ this.bits = 0;
71
68
  }
72
- const bitPos = 7 - (this.bitCount % 8);
69
+ const bitPos = this.bitCount % 8;
73
70
  this.bits |= bit << bitPos;
74
71
  this.bitCount++;
75
- if (this.bitCount % 8 === 0) {
76
- this.bytes.push(this.bits);
77
- }
78
72
  }
79
73
  }
80
74
  flush() {
81
75
  if (this.bitCount % 8 !== 0) {
82
76
  this.bytes.push(this.bits);
83
77
  this.bits = 0;
84
- this.bitCount = 0;
78
+ // Do not reset bitCount here as it tracks total bits written
79
+ }
80
+ else if (this.bitCount > 0 && this.bytes.length * 8 < this.bitCount) {
81
+ // Edge case: if we just finished a byte but haven't pushed it yet
82
+ // (The loop pushes at the START of the next bit, so we might have a full byte pending)
83
+ this.bytes.push(this.bits);
84
+ this.bits = 0;
85
85
  }
86
86
  }
87
87
  getBytes() {
@@ -244,10 +244,6 @@ class WebPEncoder {
244
244
  writer.writeBits(0, 1);
245
245
  // No meta Huffman codes
246
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
247
  // Apply quantization if quality < 100
252
248
  const encodingData = this.quantizeImageData();
253
249
  // Collect symbol frequencies for each channel
@@ -356,13 +352,13 @@ class WebPEncoder {
356
352
  if (symbols.length === 1) {
357
353
  // Single symbol
358
354
  writer.writeBits(0, 1); // num_symbols = 1 (0 + 1)
359
- writer.writeBits(0, 1); // is_first_8bits = 0
355
+ writer.writeBits(1, 1); // is_first_8bits = 1 (use 8 bits)
360
356
  writer.writeBits(symbols[0], 8); // symbol
361
357
  }
362
358
  else if (symbols.length === 2) {
363
359
  // Two symbols
364
360
  writer.writeBits(1, 1); // num_symbols = 2 (1 + 1)
365
- writer.writeBits(0, 1); // is_first_8bits = 0
361
+ writer.writeBits(1, 1); // is_first_8bits = 1 (use 8 bits)
366
362
  writer.writeBits(symbols[0], 8); // first symbol
367
363
  writer.writeBits(symbols[1], 8); // second symbol
368
364
  }
@@ -381,9 +377,11 @@ class WebPEncoder {
381
377
  const symbols = Array.from(frequencies.keys()).sort((a, b) => a - b);
382
378
  if (symbols.length === 0)
383
379
  return codeLengths;
384
- // For a single symbol, use code length 0
380
+ // For a single symbol, use code length 1
381
+ // (Canonical Huffman codes require length >= 1)
385
382
  if (symbols.length === 1) {
386
- codeLengths[symbols[0]] = 0;
383
+ // console.log(`Single symbol ${symbols[0]}, forcing length 1`);
384
+ codeLengths[symbols[0]] = 1;
387
385
  return codeLengths;
388
386
  }
389
387
  // For two symbols, use code length 1 for both
@@ -392,27 +390,64 @@ class WebPEncoder {
392
390
  codeLengths[symbols[1]] = 1;
393
391
  return codeLengths;
394
392
  }
395
- const nodes = symbols.map((symbol) => ({
393
+ let nodes = symbols.map((symbol) => ({
396
394
  freq: frequencies.get(symbol),
397
395
  symbol,
398
396
  }));
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);
397
+ // Helper to build tree
398
+ const buildTree = (leafs) => {
399
+ const queue = [...leafs];
400
+ while (queue.length > 1) {
401
+ queue.sort((a, b) => a.freq - b.freq);
402
+ const left = queue.shift();
403
+ const right = queue.shift();
404
+ queue.push({
405
+ freq: left.freq + right.freq,
406
+ left,
407
+ right,
408
+ });
409
+ }
410
+ return queue[0];
411
+ };
412
+ let root = buildTree(nodes);
413
+ // Check max depth
414
+ let maxDepth = 0;
415
+ const checkDepth = (node, depth) => {
416
+ if (node.symbol !== undefined) {
417
+ maxDepth = Math.max(maxDepth, depth);
418
+ }
419
+ else {
420
+ if (node.left)
421
+ checkDepth(node.left, depth + 1);
422
+ if (node.right)
423
+ checkDepth(node.right, depth + 1);
424
+ }
425
+ };
426
+ checkDepth(root, 0);
427
+ // If tree is too deep, flatten frequencies and rebuild
428
+ let attempts = 0;
429
+ while (maxDepth > maxCodeLength && attempts < 5) {
430
+ // console.log(`Tree too deep (${maxDepth} > ${maxCodeLength}), flattening...`);
431
+ attempts++;
432
+ // Add bias to frequencies to flatten the tree
433
+ // Increase bias with each attempt
434
+ const bias = (Math.ceil(root.freq / (symbols.length * 2)) || 1) *
435
+ attempts;
436
+ nodes = symbols.map((symbol) => ({
437
+ freq: frequencies.get(symbol) + bias,
438
+ symbol,
439
+ }));
440
+ root = buildTree(nodes);
441
+ // Re-check depth
442
+ maxDepth = 0;
443
+ checkDepth(root, 0);
444
+ }
445
+ if (maxDepth > maxCodeLength) {
446
+ console.warn(`Failed to reduce Huffman tree depth to ${maxCodeLength} (current: ${maxDepth})`);
447
+ // Force hard limit by sorting and assigning lengths?
448
+ // For now, let's just see if this is happening.
413
449
  }
414
450
  // Calculate code lengths by traversing tree (iterative to avoid deep recursion)
415
- const root = nodes[0];
416
451
  const stack = [{
417
452
  node: root,
418
453
  depth: 0,
@@ -420,6 +455,7 @@ class WebPEncoder {
420
455
  while (stack.length > 0) {
421
456
  const { node, depth } = stack.pop();
422
457
  if (node.symbol !== undefined) {
458
+ // Clamp depth to maxCodeLength (should be safe now with flattening heuristic)
423
459
  codeLengths[node.symbol] = Math.min(depth, maxCodeLength);
424
460
  }
425
461
  else {
@@ -592,13 +628,34 @@ class WebPEncoder {
592
628
  }
593
629
  numCodeLengthCodes = Math.max(4, numCodeLengthCodes);
594
630
  // Write number of code length codes
631
+ // console.log(`Complex Huffman: numCodeLengthCodes=${numCodeLengthCodes}, rleEncoded.length=${rleEncoded.length}`);
595
632
  writer.writeBits(numCodeLengthCodes - 4, 4);
596
633
  // Write code length code lengths
597
634
  for (let i = 0; i < numCodeLengthCodes; i++) {
598
635
  writer.writeBits(codeLengthCodeLengths[codeLengthCodeOrder[i]], 3);
599
636
  }
637
+ // Write max_symbol is encoded? No, it's write_trimmed_length
638
+ // VP8L spec says: "int max_symbol is read."
639
+ // Wait, subagent said "write_trimmed_length".
640
+ // Let's check the spec or libwebp source if possible.
641
+ // But assuming subagent is correct:
642
+ writer.writeBits(0, 1); // write_trimmed_length = 0 (no trimming)
600
643
  // Build canonical codes for code lengths
601
644
  const codeLengthCodes = this.buildCanonicalCodes(codeLengthCodeLengths);
645
+ // Check for single symbol optimization (VP8L specific)
646
+ let nonZeroCount = 0;
647
+ for (const len of codeLengthCodeLengths) {
648
+ if (len > 0)
649
+ nonZeroCount++;
650
+ }
651
+ if (nonZeroCount === 1) {
652
+ // If only one symbol is used in the code length alphabet,
653
+ // we don't write any bits for the code itself in the RLE stream.
654
+ // The symbol is implicit because it's the only one with non-zero length in the header.
655
+ for (const [_symbol, info] of codeLengthCodes) {
656
+ info.length = 0;
657
+ }
658
+ }
602
659
  // Write RLE-encoded code lengths using code length codes
603
660
  for (let i = 0; i < rleEncoded.length; i++) {
604
661
  const code = rleEncoded[i];
@@ -608,8 +665,9 @@ class WebPEncoder {
608
665
  }
609
666
  // Write the Huffman code bits from MSB to LSB
610
667
  // 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);
668
+ // (First bit written is MSB, which corresponds to top of tree)
669
+ for (let i = huffCode.length - 1; i >= 0; i--) {
670
+ writer.writeBits((huffCode.code >> i) & 1, 1);
613
671
  }
614
672
  // Write extra bits for special codes
615
673
  if (code === 16) {
@@ -1,40 +0,0 @@
1
- import type { ImageData, ImageFormat } from "../types.js";
2
- /**
3
- * RAW format handler
4
- * Implements a simple uncompressed RGBA format with a minimal header
5
- *
6
- * Format structure:
7
- * - Magic bytes (4 bytes): "RGBA" (0x52 0x47 0x42 0x41)
8
- * - Width (4 bytes, big-endian)
9
- * - Height (4 bytes, big-endian)
10
- * - RGBA pixel data (width * height * 4 bytes)
11
- */
12
- export declare class RAWFormat implements ImageFormat {
13
- /** Format name identifier */
14
- readonly name = "raw";
15
- /** MIME type for RAW images */
16
- readonly mimeType = "image/raw";
17
- private readonly MAGIC_BYTES;
18
- private readonly HEADER_SIZE;
19
- /**
20
- * Check if the given data is a RAW image
21
- * @param data Raw image data to check
22
- * @returns true if data has RAW signature
23
- */
24
- canDecode(data: Uint8Array): boolean;
25
- /**
26
- * Decode RAW image data to RGBA
27
- * @param data Raw RAW image data
28
- * @returns Decoded image data with RGBA pixels
29
- */
30
- decode(data: Uint8Array): Promise<ImageData>;
31
- /**
32
- * Encode RGBA image data to RAW format
33
- * @param imageData Image data to encode
34
- * @returns Encoded RAW image bytes
35
- */
36
- encode(imageData: ImageData): Promise<Uint8Array>;
37
- private readUint32;
38
- private writeUint32;
39
- }
40
- //# sourceMappingURL=raw.d.ts.map