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,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 =
|
|
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 ===
|
|
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 =
|
|
142
|
+
this.bitPos = 0;
|
|
126
143
|
}
|
|
127
|
-
result |= ((this.value >>
|
|
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 !==
|
|
139
|
-
this.bitPos =
|
|
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 <
|
|
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
|
-
?
|
|
368
|
-
: reader.readBits(
|
|
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
|
|
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
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
377
|
+
// For a single symbol, use code length 1
|
|
378
|
+
// (Canonical Huffman codes require length >= 1)
|
|
382
379
|
if (symbols.length === 1) {
|
|
383
|
-
|
|
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
|
-
|
|
390
|
+
let nodes = symbols.map((symbol) => ({
|
|
393
391
|
freq: frequencies.get(symbol),
|
|
394
392
|
symbol,
|
|
395
393
|
}));
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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.
|
|
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,
|
|
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
|
-
* //
|
|
16
|
-
* image
|
|
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 {
|
|
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
|