cross-image 0.1.5 → 0.2.0

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