cross-image 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +3 -1
- package/esm/src/formats/apng.d.ts +50 -0
- package/esm/src/formats/apng.js +364 -0
- package/esm/src/formats/bmp.d.ts +0 -6
- package/esm/src/formats/bmp.js +24 -47
- package/esm/src/formats/dng.js +4 -4
- package/esm/src/formats/gif.d.ts +0 -2
- package/esm/src/formats/gif.js +10 -16
- package/esm/src/formats/ico.d.ts +41 -0
- package/esm/src/formats/ico.js +214 -0
- package/esm/src/formats/pcx.js +1 -1
- package/esm/src/formats/png.d.ts +2 -21
- package/esm/src/formats/png.js +5 -429
- package/esm/src/formats/png_base.d.ts +108 -0
- package/esm/src/formats/png_base.js +487 -0
- package/esm/src/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.js +4 -0
- package/esm/src/utils/byte_utils.d.ts +30 -0
- package/esm/src/utils/byte_utils.js +50 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -2
- package/esm/src/utils/gif_encoder.js +115 -48
- package/package.json +1 -1
- package/script/mod.d.ts +3 -1
- package/script/mod.js +6 -2
- package/script/src/formats/apng.d.ts +50 -0
- package/script/src/formats/apng.js +368 -0
- package/script/src/formats/bmp.d.ts +0 -6
- package/script/src/formats/bmp.js +24 -47
- package/script/src/formats/dng.js +4 -4
- package/script/src/formats/gif.d.ts +0 -2
- package/script/src/formats/gif.js +10 -16
- package/script/src/formats/ico.d.ts +41 -0
- package/script/src/formats/ico.js +218 -0
- package/script/src/formats/pcx.js +1 -1
- package/script/src/formats/png.d.ts +2 -21
- package/script/src/formats/png.js +5 -429
- package/script/src/formats/png_base.d.ts +108 -0
- package/script/src/formats/png_base.js +491 -0
- package/script/src/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.js +4 -0
- package/script/src/utils/byte_utils.d.ts +30 -0
- package/script/src/utils/byte_utils.js +58 -0
- package/script/src/utils/gif_encoder.d.ts +3 -2
- package/script/src/utils/gif_encoder.js +115 -48
|
@@ -20,15 +20,20 @@ class GIFEncoder {
|
|
|
20
20
|
writable: true,
|
|
21
21
|
value: void 0
|
|
22
22
|
});
|
|
23
|
-
Object.defineProperty(this, "
|
|
23
|
+
Object.defineProperty(this, "frames", {
|
|
24
24
|
enumerable: true,
|
|
25
25
|
configurable: true,
|
|
26
26
|
writable: true,
|
|
27
|
-
value:
|
|
27
|
+
value: []
|
|
28
28
|
});
|
|
29
29
|
this.width = width;
|
|
30
30
|
this.height = height;
|
|
31
|
-
|
|
31
|
+
if (data) {
|
|
32
|
+
this.addFrame(data);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
addFrame(data, delay = 0) {
|
|
36
|
+
this.frames.push({ data, delay });
|
|
32
37
|
}
|
|
33
38
|
writeBytes(output, bytes) {
|
|
34
39
|
output.push(...bytes);
|
|
@@ -57,7 +62,7 @@ class GIFEncoder {
|
|
|
57
62
|
/**
|
|
58
63
|
* Quantize RGBA image to 256 colors using median cut algorithm
|
|
59
64
|
*/
|
|
60
|
-
quantize() {
|
|
65
|
+
quantize(data) {
|
|
61
66
|
// Simple quantization: collect unique colors and build palette
|
|
62
67
|
const colorMap = new Map();
|
|
63
68
|
const colors = [];
|
|
@@ -68,10 +73,10 @@ class GIFEncoder {
|
|
|
68
73
|
const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
|
|
69
74
|
const bStep = 255 / B_LEVELS; // Step size for B quantization
|
|
70
75
|
// Collect unique colors
|
|
71
|
-
for (let i = 0; i <
|
|
72
|
-
const r =
|
|
73
|
-
const g =
|
|
74
|
-
const b =
|
|
76
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
77
|
+
const r = data[i];
|
|
78
|
+
const g = data[i + 1];
|
|
79
|
+
const b = data[i + 2];
|
|
75
80
|
const key = `${r},${g},${b}`;
|
|
76
81
|
if (!colorMap.has(key) && colors.length < 256) {
|
|
77
82
|
colorMap.set(key, colors.length);
|
|
@@ -86,12 +91,12 @@ class GIFEncoder {
|
|
|
86
91
|
colorMap.clear();
|
|
87
92
|
colors.length = 0;
|
|
88
93
|
useColorReduction = true;
|
|
89
|
-
for (let i = 0; i <
|
|
94
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
90
95
|
// Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
|
|
91
96
|
// This gives us 8 bits total = 256 possible colors
|
|
92
|
-
const r = this.quantizeChannel(
|
|
93
|
-
const g = this.quantizeChannel(
|
|
94
|
-
const b = this.quantizeChannel(
|
|
97
|
+
const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
|
|
98
|
+
const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
|
|
99
|
+
const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
|
|
95
100
|
const key = `${r},${g},${b}`;
|
|
96
101
|
if (!colorMap.has(key)) {
|
|
97
102
|
if (colors.length < 256) {
|
|
@@ -115,10 +120,10 @@ class GIFEncoder {
|
|
|
115
120
|
}
|
|
116
121
|
// Create indexed data
|
|
117
122
|
const indexed = new Uint8Array(this.width * this.height);
|
|
118
|
-
for (let i = 0, j = 0; i <
|
|
119
|
-
let r =
|
|
120
|
-
let g =
|
|
121
|
-
let b =
|
|
123
|
+
for (let i = 0, j = 0; i < data.length; i += 4, j++) {
|
|
124
|
+
let r = data[i];
|
|
125
|
+
let g = data[i + 1];
|
|
126
|
+
let b = data[i + 2];
|
|
122
127
|
// Apply color reduction if it was used for building the palette
|
|
123
128
|
if (useColorReduction) {
|
|
124
129
|
r = this.quantizeChannel(r, RG_LEVELS, rgStep);
|
|
@@ -164,10 +169,14 @@ class GIFEncoder {
|
|
|
164
169
|
return Math.max(2, bits);
|
|
165
170
|
}
|
|
166
171
|
encode() {
|
|
172
|
+
if (this.frames.length === 0) {
|
|
173
|
+
throw new Error("No frames to encode");
|
|
174
|
+
}
|
|
167
175
|
const output = [];
|
|
168
|
-
// Quantize
|
|
169
|
-
const
|
|
170
|
-
const
|
|
176
|
+
// Quantize first frame for Global Color Table
|
|
177
|
+
const firstFrame = this.frames[0];
|
|
178
|
+
const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
|
|
179
|
+
const paletteSize = globalPalette.length / 3;
|
|
171
180
|
const bitsPerColor = this.getBitsPerColor(paletteSize);
|
|
172
181
|
// Header
|
|
173
182
|
this.writeString(output, "GIF89a");
|
|
@@ -190,38 +199,96 @@ class GIFEncoder {
|
|
|
190
199
|
// So we need to write that many colors, padding if necessary
|
|
191
200
|
const gctSize = 1 << bitsPerColor;
|
|
192
201
|
const paddedPalette = new Uint8Array(gctSize * 3);
|
|
193
|
-
paddedPalette.set(
|
|
202
|
+
paddedPalette.set(globalPalette);
|
|
194
203
|
this.writeBytes(output, paddedPalette);
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
204
|
+
// Netscape Application Extension (Looping)
|
|
205
|
+
if (this.frames.length > 1) {
|
|
206
|
+
output.push(0x21); // Extension Introducer
|
|
207
|
+
output.push(0xff); // Application Extension Label
|
|
208
|
+
output.push(11); // Block Size
|
|
209
|
+
this.writeString(output, "NETSCAPE2.0");
|
|
210
|
+
output.push(3); // Sub-block Size
|
|
211
|
+
output.push(1); // Loop Indicator (1 = loop)
|
|
212
|
+
this.writeUint16LE(output, 0); // Loop Count (0 = infinite)
|
|
213
|
+
output.push(0); // Block Terminator
|
|
214
|
+
}
|
|
215
|
+
// Encode frames
|
|
216
|
+
for (let i = 0; i < this.frames.length; i++) {
|
|
217
|
+
const frame = this.frames[i];
|
|
218
|
+
let indexed;
|
|
219
|
+
let useLocalPalette = false;
|
|
220
|
+
let localPalette = null;
|
|
221
|
+
let localBitsPerColor = bitsPerColor;
|
|
222
|
+
if (i === 0) {
|
|
223
|
+
indexed = firstIndexed;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Quantize subsequent frames
|
|
227
|
+
// For simplicity, we use a Local Color Table for each frame to ensure colors are correct
|
|
228
|
+
const result = this.quantize(frame.data);
|
|
229
|
+
indexed = result.indexed;
|
|
230
|
+
localPalette = result.palette;
|
|
231
|
+
useLocalPalette = true;
|
|
232
|
+
const localPaletteSize = localPalette.length / 3;
|
|
233
|
+
localBitsPerColor = this.getBitsPerColor(localPaletteSize);
|
|
221
234
|
}
|
|
235
|
+
// Graphic Control Extension
|
|
236
|
+
output.push(0x21); // Extension Introducer
|
|
237
|
+
output.push(0xf9); // Graphic Control Label
|
|
238
|
+
output.push(4); // Byte Size
|
|
239
|
+
// Packed Field
|
|
240
|
+
// Reserved (3 bits)
|
|
241
|
+
// Disposal Method (3 bits): 2 (Restore to background) - usually safe for animation
|
|
242
|
+
// User Input Flag (1 bit): 0
|
|
243
|
+
// Transparent Color Flag (1 bit): 0
|
|
244
|
+
output.push(0x08); // Disposal method 2 (Restore to background)
|
|
245
|
+
// Delay Time (1/100ths of a second)
|
|
246
|
+
// Default to 10 (100ms) if not specified
|
|
247
|
+
const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
|
|
248
|
+
this.writeUint16LE(output, delay);
|
|
249
|
+
// Transparent Color Index
|
|
250
|
+
output.push(0);
|
|
251
|
+
output.push(0); // Block Terminator
|
|
252
|
+
// Image Descriptor
|
|
253
|
+
output.push(0x2c); // Image Separator
|
|
254
|
+
this.writeUint16LE(output, 0); // Left
|
|
255
|
+
this.writeUint16LE(output, 0); // Top
|
|
256
|
+
this.writeUint16LE(output, this.width);
|
|
257
|
+
this.writeUint16LE(output, this.height);
|
|
258
|
+
// Packed Field
|
|
259
|
+
if (useLocalPalette && localPalette) {
|
|
260
|
+
// LCT Flag: 1
|
|
261
|
+
// Interlace: 0
|
|
262
|
+
// Sort: 0
|
|
263
|
+
// Reserved: 0
|
|
264
|
+
// Size of LCT: localBitsPerColor - 1
|
|
265
|
+
const lctPacked = 0x80 | (localBitsPerColor - 1);
|
|
266
|
+
output.push(lctPacked);
|
|
267
|
+
// Write Local Color Table
|
|
268
|
+
const lctSize = 1 << localBitsPerColor;
|
|
269
|
+
const paddedLct = new Uint8Array(lctSize * 3);
|
|
270
|
+
paddedLct.set(localPalette);
|
|
271
|
+
this.writeBytes(output, paddedLct);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
output.push(0); // No LCT
|
|
275
|
+
}
|
|
276
|
+
// LZW Minimum Code Size
|
|
277
|
+
const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
|
|
278
|
+
output.push(minCodeSize);
|
|
279
|
+
// Compress image data with LZW
|
|
280
|
+
const encoder = new lzw_js_1.LZWEncoder(minCodeSize);
|
|
281
|
+
const compressed = encoder.compress(indexed);
|
|
282
|
+
// Write compressed data in sub-blocks (max 255 bytes per block)
|
|
283
|
+
for (let k = 0; k < compressed.length; k += 255) {
|
|
284
|
+
const blockSize = Math.min(255, compressed.length - k);
|
|
285
|
+
output.push(blockSize);
|
|
286
|
+
for (let j = 0; j < blockSize; j++) {
|
|
287
|
+
output.push(compressed[k + j]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
output.push(0); // Block Terminator
|
|
222
291
|
}
|
|
223
|
-
// Block Terminator
|
|
224
|
-
output.push(0);
|
|
225
292
|
// Trailer
|
|
226
293
|
output.push(0x3b);
|
|
227
294
|
return new Uint8Array(output);
|