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.
- package/README.md +17 -5
- package/esm/mod.d.ts +28 -4
- package/esm/mod.js +28 -4
- package/esm/src/formats/dng.d.ts +27 -0
- package/esm/src/formats/dng.js +191 -0
- package/esm/src/formats/pam.d.ts +43 -0
- package/esm/src/formats/pam.js +177 -0
- package/esm/src/formats/pcx.d.ts +13 -0
- package/esm/src/formats/pcx.js +204 -0
- package/esm/src/formats/tiff.d.ts +7 -7
- package/esm/src/image.d.ts +99 -0
- package/esm/src/image.js +200 -2
- package/esm/src/utils/image_processing.d.ts +91 -0
- package/esm/src/utils/image_processing.js +231 -0
- package/esm/src/utils/webp_decoder.js +47 -12
- package/esm/src/utils/webp_encoder.js +97 -39
- package/package.json +4 -1
- package/script/mod.d.ts +28 -4
- package/script/mod.js +32 -6
- package/script/src/formats/dng.d.ts +27 -0
- package/script/src/formats/dng.js +195 -0
- package/script/src/formats/pam.d.ts +43 -0
- package/script/src/formats/pam.js +181 -0
- package/script/src/formats/pcx.d.ts +13 -0
- package/script/src/formats/pcx.js +208 -0
- package/script/src/formats/tiff.d.ts +7 -7
- package/script/src/image.d.ts +99 -0
- package/script/src/image.js +200 -2
- package/script/src/utils/image_processing.d.ts +91 -0
- package/script/src/utils/image_processing.js +242 -0
- package/script/src/utils/webp_decoder.js +47 -12
- package/script/src/utils/webp_encoder.js +97 -39
- package/esm/src/formats/raw.d.ts +0 -40
- package/esm/src/formats/raw.js +0 -118
- package/script/src/formats/raw.d.ts +0 -40
- 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 =
|
|
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 ===
|
|
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 =
|
|
145
|
+
this.bitPos = 0;
|
|
129
146
|
}
|
|
130
|
-
result |= ((this.value >>
|
|
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 !==
|
|
142
|
-
this.bitPos =
|
|
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 <
|
|
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
|
-
?
|
|
371
|
-
: reader.readBits(
|
|
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
|
|
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
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
380
|
+
// For a single symbol, use code length 1
|
|
381
|
+
// (Canonical Huffman codes require length >= 1)
|
|
385
382
|
if (symbols.length === 1) {
|
|
386
|
-
|
|
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
|
-
|
|
393
|
+
let nodes = symbols.map((symbol) => ({
|
|
396
394
|
freq: frequencies.get(symbol),
|
|
397
395
|
symbol,
|
|
398
396
|
}));
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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) {
|
package/esm/src/formats/raw.d.ts
DELETED
|
@@ -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
|