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