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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JavaScript GIF decoder implementation
|
|
3
|
+
* Supports GIF87a and GIF89a formats with LZW decompression
|
|
4
|
+
*/
|
|
5
|
+
import { LZWDecoder } from "./lzw.js";
|
|
6
|
+
export class GIFDecoder {
|
|
7
|
+
constructor(data) {
|
|
8
|
+
Object.defineProperty(this, "data", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "pos", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
20
|
+
this.data = data;
|
|
21
|
+
this.pos = 0;
|
|
22
|
+
}
|
|
23
|
+
readByte() {
|
|
24
|
+
if (this.pos >= this.data.length) {
|
|
25
|
+
throw new Error("Unexpected end of GIF data");
|
|
26
|
+
}
|
|
27
|
+
return this.data[this.pos++];
|
|
28
|
+
}
|
|
29
|
+
readUint16LE() {
|
|
30
|
+
const low = this.readByte();
|
|
31
|
+
const high = this.readByte();
|
|
32
|
+
return low | (high << 8);
|
|
33
|
+
}
|
|
34
|
+
readBytes(count) {
|
|
35
|
+
if (this.pos + count > this.data.length) {
|
|
36
|
+
throw new Error("Unexpected end of GIF data");
|
|
37
|
+
}
|
|
38
|
+
const result = this.data.slice(this.pos, this.pos + count);
|
|
39
|
+
this.pos += count;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
readColorTable(size) {
|
|
43
|
+
// Each color is 3 bytes (RGB)
|
|
44
|
+
return this.readBytes(size * 3);
|
|
45
|
+
}
|
|
46
|
+
readDataSubBlocks() {
|
|
47
|
+
const blocks = [];
|
|
48
|
+
while (true) {
|
|
49
|
+
const blockSize = this.readByte();
|
|
50
|
+
if (blockSize === 0)
|
|
51
|
+
break;
|
|
52
|
+
const blockData = this.readBytes(blockSize);
|
|
53
|
+
blocks.push(...blockData);
|
|
54
|
+
}
|
|
55
|
+
return new Uint8Array(blocks);
|
|
56
|
+
}
|
|
57
|
+
decode() {
|
|
58
|
+
// Verify GIF signature
|
|
59
|
+
const signature = this.readBytes(3);
|
|
60
|
+
const version = this.readBytes(3);
|
|
61
|
+
if (signature[0] !== 0x47 || signature[1] !== 0x49 || signature[2] !== 0x46 ||
|
|
62
|
+
(version[0] !== 0x38) ||
|
|
63
|
+
(version[1] !== 0x37 && version[1] !== 0x39) ||
|
|
64
|
+
(version[2] !== 0x61)) {
|
|
65
|
+
throw new Error("Invalid GIF signature");
|
|
66
|
+
}
|
|
67
|
+
// Read Logical Screen Descriptor
|
|
68
|
+
const width = this.readUint16LE();
|
|
69
|
+
const height = this.readUint16LE();
|
|
70
|
+
const packed = this.readByte();
|
|
71
|
+
const backgroundColorIndex = this.readByte();
|
|
72
|
+
const _aspectRatio = this.readByte();
|
|
73
|
+
const hasGlobalColorTable = (packed & 0x80) !== 0;
|
|
74
|
+
const _colorResolution = ((packed & 0x70) >> 4) + 1;
|
|
75
|
+
const _sortFlag = (packed & 0x08) !== 0;
|
|
76
|
+
const globalColorTableSize = 2 << (packed & 0x07);
|
|
77
|
+
let globalColorTable = null;
|
|
78
|
+
if (hasGlobalColorTable) {
|
|
79
|
+
globalColorTable = this.readColorTable(globalColorTableSize);
|
|
80
|
+
}
|
|
81
|
+
// Parse data stream
|
|
82
|
+
let imageWidth = 0;
|
|
83
|
+
let imageHeight = 0;
|
|
84
|
+
let imageLeft = 0;
|
|
85
|
+
let imageTop = 0;
|
|
86
|
+
let localColorTable = null;
|
|
87
|
+
let transparentColorIndex = null;
|
|
88
|
+
let interlaced = false;
|
|
89
|
+
while (this.pos < this.data.length) {
|
|
90
|
+
const separator = this.readByte();
|
|
91
|
+
if (separator === 0x21) {
|
|
92
|
+
// Extension
|
|
93
|
+
const label = this.readByte();
|
|
94
|
+
if (label === 0xf9) {
|
|
95
|
+
// Graphic Control Extension
|
|
96
|
+
const _blockSize = this.readByte();
|
|
97
|
+
const packed = this.readByte();
|
|
98
|
+
const hasTransparent = (packed & 0x01) !== 0;
|
|
99
|
+
const _delayTime = this.readUint16LE();
|
|
100
|
+
const transparentIndex = this.readByte();
|
|
101
|
+
const _terminator = this.readByte();
|
|
102
|
+
if (hasTransparent) {
|
|
103
|
+
transparentColorIndex = transparentIndex;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Skip other extensions
|
|
108
|
+
this.readDataSubBlocks();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (separator === 0x2c) {
|
|
112
|
+
// Image Descriptor
|
|
113
|
+
imageLeft = this.readUint16LE();
|
|
114
|
+
imageTop = this.readUint16LE();
|
|
115
|
+
imageWidth = this.readUint16LE();
|
|
116
|
+
imageHeight = this.readUint16LE();
|
|
117
|
+
const packed = this.readByte();
|
|
118
|
+
const hasLocalColorTable = (packed & 0x80) !== 0;
|
|
119
|
+
interlaced = (packed & 0x40) !== 0;
|
|
120
|
+
const localColorTableSize = 2 << (packed & 0x07);
|
|
121
|
+
if (hasLocalColorTable) {
|
|
122
|
+
localColorTable = this.readColorTable(localColorTableSize);
|
|
123
|
+
}
|
|
124
|
+
// Read image data
|
|
125
|
+
const minCodeSize = this.readByte();
|
|
126
|
+
const compressedData = this.readDataSubBlocks();
|
|
127
|
+
// Decompress using LZW
|
|
128
|
+
const decoder = new LZWDecoder(minCodeSize, compressedData);
|
|
129
|
+
const indexedData = decoder.decompress();
|
|
130
|
+
// Convert indexed to RGBA
|
|
131
|
+
const colorTable = localColorTable || globalColorTable;
|
|
132
|
+
if (!colorTable) {
|
|
133
|
+
throw new Error("No color table available");
|
|
134
|
+
}
|
|
135
|
+
return this.indexedToRGBA(indexedData, imageWidth, imageHeight, colorTable, transparentColorIndex, interlaced, width, height, imageLeft, imageTop, backgroundColorIndex);
|
|
136
|
+
}
|
|
137
|
+
else if (separator === 0x3b) {
|
|
138
|
+
// Trailer - end of GIF
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
else if (separator === 0x00) {
|
|
142
|
+
// Skip null bytes
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw new Error(`Unknown separator: 0x${separator.toString(16)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw new Error("No image data found in GIF");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Decode all frames from an animated GIF
|
|
153
|
+
* @returns Object with canvas dimensions and array of frames
|
|
154
|
+
*/
|
|
155
|
+
decodeAllFrames() {
|
|
156
|
+
// Reset position
|
|
157
|
+
this.pos = 0;
|
|
158
|
+
// Verify GIF signature
|
|
159
|
+
const signature = this.readBytes(3);
|
|
160
|
+
const version = this.readBytes(3);
|
|
161
|
+
if (signature[0] !== 0x47 || signature[1] !== 0x49 || signature[2] !== 0x46 ||
|
|
162
|
+
(version[0] !== 0x38) ||
|
|
163
|
+
(version[1] !== 0x37 && version[1] !== 0x39) ||
|
|
164
|
+
(version[2] !== 0x61)) {
|
|
165
|
+
throw new Error("Invalid GIF signature");
|
|
166
|
+
}
|
|
167
|
+
// Read Logical Screen Descriptor
|
|
168
|
+
const width = this.readUint16LE();
|
|
169
|
+
const height = this.readUint16LE();
|
|
170
|
+
const packed = this.readByte();
|
|
171
|
+
const _backgroundColorIndex = this.readByte();
|
|
172
|
+
const _aspectRatio = this.readByte();
|
|
173
|
+
const hasGlobalColorTable = (packed & 0x80) !== 0;
|
|
174
|
+
// Color table size: 2^(n+1) where n is the 3 least significant bits
|
|
175
|
+
const globalColorTableSize = 2 << (packed & 0x07);
|
|
176
|
+
let globalColorTable = null;
|
|
177
|
+
if (hasGlobalColorTable) {
|
|
178
|
+
globalColorTable = this.readColorTable(globalColorTableSize);
|
|
179
|
+
}
|
|
180
|
+
const frames = [];
|
|
181
|
+
// Parse data stream for all frames
|
|
182
|
+
let transparentColorIndex = null;
|
|
183
|
+
let delayTime = 0;
|
|
184
|
+
let disposalMethod = 0;
|
|
185
|
+
while (this.pos < this.data.length) {
|
|
186
|
+
const separator = this.readByte();
|
|
187
|
+
if (separator === 0x21) {
|
|
188
|
+
// Extension
|
|
189
|
+
const label = this.readByte();
|
|
190
|
+
if (label === 0xf9) {
|
|
191
|
+
// Graphic Control Extension
|
|
192
|
+
const _blockSize = this.readByte();
|
|
193
|
+
const packed = this.readByte();
|
|
194
|
+
disposalMethod = (packed >> 2) & 0x07;
|
|
195
|
+
const hasTransparent = (packed & 0x01) !== 0;
|
|
196
|
+
delayTime = this.readUint16LE();
|
|
197
|
+
const transparentIndex = this.readByte();
|
|
198
|
+
const _terminator = this.readByte();
|
|
199
|
+
if (hasTransparent) {
|
|
200
|
+
transparentColorIndex = transparentIndex;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Skip other extensions
|
|
205
|
+
this.readDataSubBlocks();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (separator === 0x2c) {
|
|
209
|
+
// Image Descriptor
|
|
210
|
+
const imageLeft = this.readUint16LE();
|
|
211
|
+
const imageTop = this.readUint16LE();
|
|
212
|
+
const imageWidth = this.readUint16LE();
|
|
213
|
+
const imageHeight = this.readUint16LE();
|
|
214
|
+
const packed = this.readByte();
|
|
215
|
+
const hasLocalColorTable = (packed & 0x80) !== 0;
|
|
216
|
+
const interlaced = (packed & 0x40) !== 0;
|
|
217
|
+
// Color table size: 2^(n+1) where n is the 3 least significant bits
|
|
218
|
+
const localColorTableSize = 2 << (packed & 0x07);
|
|
219
|
+
let localColorTable = null;
|
|
220
|
+
if (hasLocalColorTable) {
|
|
221
|
+
localColorTable = this.readColorTable(localColorTableSize);
|
|
222
|
+
}
|
|
223
|
+
// Read image data
|
|
224
|
+
const minCodeSize = this.readByte();
|
|
225
|
+
const compressedData = this.readDataSubBlocks();
|
|
226
|
+
// Decompress using LZW
|
|
227
|
+
const decoder = new LZWDecoder(minCodeSize, compressedData);
|
|
228
|
+
const indexedData = decoder.decompress();
|
|
229
|
+
// Convert indexed to RGBA
|
|
230
|
+
const colorTable = localColorTable || globalColorTable;
|
|
231
|
+
if (!colorTable) {
|
|
232
|
+
throw new Error("No color table available");
|
|
233
|
+
}
|
|
234
|
+
// Deinterlace if necessary
|
|
235
|
+
const deinterlaced = interlaced
|
|
236
|
+
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
237
|
+
: indexedData;
|
|
238
|
+
// Create frame with just the image data (not full canvas)
|
|
239
|
+
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
240
|
+
for (let y = 0; y < imageHeight; y++) {
|
|
241
|
+
for (let x = 0; x < imageWidth; x++) {
|
|
242
|
+
const srcIdx = y * imageWidth + x;
|
|
243
|
+
if (srcIdx >= deinterlaced.length)
|
|
244
|
+
continue;
|
|
245
|
+
const colorIndex = deinterlaced[srcIdx];
|
|
246
|
+
const dstIdx = (y * imageWidth + x) * 4;
|
|
247
|
+
if (transparentColorIndex !== null &&
|
|
248
|
+
colorIndex === transparentColorIndex) {
|
|
249
|
+
// Transparent pixel
|
|
250
|
+
frameData[dstIdx] = 0;
|
|
251
|
+
frameData[dstIdx + 1] = 0;
|
|
252
|
+
frameData[dstIdx + 2] = 0;
|
|
253
|
+
frameData[dstIdx + 3] = 0;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Copy color from color table
|
|
257
|
+
const colorOffset = colorIndex * 3;
|
|
258
|
+
if (colorOffset + 2 < colorTable.length) {
|
|
259
|
+
frameData[dstIdx] = colorTable[colorOffset];
|
|
260
|
+
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
261
|
+
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
262
|
+
frameData[dstIdx + 3] = 255;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
frames.push({
|
|
268
|
+
width: imageWidth,
|
|
269
|
+
height: imageHeight,
|
|
270
|
+
left: imageLeft,
|
|
271
|
+
top: imageTop,
|
|
272
|
+
data: frameData,
|
|
273
|
+
delay: delayTime,
|
|
274
|
+
disposal: disposalMethod,
|
|
275
|
+
});
|
|
276
|
+
// Reset graphic control extension state
|
|
277
|
+
transparentColorIndex = null;
|
|
278
|
+
delayTime = 0;
|
|
279
|
+
disposalMethod = 0;
|
|
280
|
+
}
|
|
281
|
+
else if (separator === 0x3b) {
|
|
282
|
+
// Trailer - end of GIF
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
else if (separator === 0x00) {
|
|
286
|
+
// Skip null bytes
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
throw new Error(`Unknown separator: 0x${separator.toString(16)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (frames.length === 0) {
|
|
294
|
+
throw new Error("No image data found in GIF");
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
width,
|
|
298
|
+
height,
|
|
299
|
+
frames,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
indexedToRGBA(indexedData, imageWidth, imageHeight, colorTable, transparentColorIndex, interlaced, canvasWidth, canvasHeight, imageLeft, imageTop, backgroundColorIndex) {
|
|
303
|
+
// Create RGBA buffer for full canvas
|
|
304
|
+
const rgba = new Uint8Array(canvasWidth * canvasHeight * 4);
|
|
305
|
+
// Fill with background color
|
|
306
|
+
const bgR = colorTable[backgroundColorIndex * 3] || 0;
|
|
307
|
+
const bgG = colorTable[backgroundColorIndex * 3 + 1] || 0;
|
|
308
|
+
const bgB = colorTable[backgroundColorIndex * 3 + 2] || 0;
|
|
309
|
+
for (let i = 0; i < rgba.length; i += 4) {
|
|
310
|
+
rgba[i] = bgR;
|
|
311
|
+
rgba[i + 1] = bgG;
|
|
312
|
+
rgba[i + 2] = bgB;
|
|
313
|
+
rgba[i + 3] = 255;
|
|
314
|
+
}
|
|
315
|
+
// Deinterlace if necessary
|
|
316
|
+
const deinterlaced = interlaced
|
|
317
|
+
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
318
|
+
: indexedData;
|
|
319
|
+
// Copy image data to canvas
|
|
320
|
+
for (let y = 0; y < imageHeight; y++) {
|
|
321
|
+
for (let x = 0; x < imageWidth; x++) {
|
|
322
|
+
const srcIdx = y * imageWidth + x;
|
|
323
|
+
if (srcIdx >= deinterlaced.length)
|
|
324
|
+
continue;
|
|
325
|
+
const colorIndex = deinterlaced[srcIdx];
|
|
326
|
+
const canvasX = imageLeft + x;
|
|
327
|
+
const canvasY = imageTop + y;
|
|
328
|
+
if (canvasX >= canvasWidth || canvasY >= canvasHeight)
|
|
329
|
+
continue;
|
|
330
|
+
const dstIdx = (canvasY * canvasWidth + canvasX) * 4;
|
|
331
|
+
if (transparentColorIndex !== null && colorIndex === transparentColorIndex) {
|
|
332
|
+
// Transparent pixel
|
|
333
|
+
rgba[dstIdx + 3] = 0;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Copy color from color table
|
|
337
|
+
const colorOffset = colorIndex * 3;
|
|
338
|
+
if (colorOffset + 2 < colorTable.length) {
|
|
339
|
+
rgba[dstIdx] = colorTable[colorOffset];
|
|
340
|
+
rgba[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
341
|
+
rgba[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
342
|
+
rgba[dstIdx + 3] = 255;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
width: canvasWidth,
|
|
349
|
+
height: canvasHeight,
|
|
350
|
+
data: rgba,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
deinterlace(data, width, height) {
|
|
354
|
+
const deinterlaced = new Uint8Array(data.length);
|
|
355
|
+
const passes = [
|
|
356
|
+
{ start: 0, step: 8 }, // Pass 1: every 8th row, starting with row 0
|
|
357
|
+
{ start: 4, step: 8 }, // Pass 2: every 8th row, starting with row 4
|
|
358
|
+
{ start: 2, step: 4 }, // Pass 3: every 4th row, starting with row 2
|
|
359
|
+
{ start: 1, step: 2 }, // Pass 4: every 2nd row, starting with row 1
|
|
360
|
+
];
|
|
361
|
+
let srcIdx = 0;
|
|
362
|
+
for (const pass of passes) {
|
|
363
|
+
for (let y = pass.start; y < height; y += pass.step) {
|
|
364
|
+
for (let x = 0; x < width; x++) {
|
|
365
|
+
if (srcIdx >= data.length)
|
|
366
|
+
break;
|
|
367
|
+
const dstIdx = y * width + x;
|
|
368
|
+
deinterlaced[dstIdx] = data[srcIdx++];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return deinterlaced;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JavaScript GIF encoder implementation
|
|
3
|
+
* Supports GIF89a format with LZW compression
|
|
4
|
+
*/
|
|
5
|
+
export declare class GIFEncoder {
|
|
6
|
+
private width;
|
|
7
|
+
private height;
|
|
8
|
+
private data;
|
|
9
|
+
constructor(width: number, height: number, data: Uint8Array);
|
|
10
|
+
private writeBytes;
|
|
11
|
+
private writeUint16LE;
|
|
12
|
+
private writeString;
|
|
13
|
+
/**
|
|
14
|
+
* Quantize a color channel value to a specified number of levels
|
|
15
|
+
* @param value - The input color channel value (0-255)
|
|
16
|
+
* @param levels - Number of quantization levels minus 1 (e.g., 7 for 8 levels)
|
|
17
|
+
* @param step - Step size for quantization (e.g., 255/7)
|
|
18
|
+
* @returns Quantized integer value
|
|
19
|
+
*/
|
|
20
|
+
private quantizeChannel;
|
|
21
|
+
/**
|
|
22
|
+
* Quantize RGBA image to 256 colors using median cut algorithm
|
|
23
|
+
*/
|
|
24
|
+
private quantize;
|
|
25
|
+
private nextPowerOf2;
|
|
26
|
+
private getBitsPerColor;
|
|
27
|
+
encode(): Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=gif_encoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gif_encoder.d.ts","sourceRoot":"","sources":["../../../src/src/utils/gif_encoder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAa;gBAEb,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU;IAM3D,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,WAAW;IAMnB;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAMvB;;OAEG;IACH,OAAO,CAAC,QAAQ;IA6GhB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,eAAe;IAQvB,MAAM,IAAI,UAAU;CA+ErB"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure JavaScript GIF encoder implementation
|
|
3
|
+
* Supports GIF89a format with LZW compression
|
|
4
|
+
*/
|
|
5
|
+
import { LZWEncoder } from "./lzw.js";
|
|
6
|
+
export class GIFEncoder {
|
|
7
|
+
constructor(width, height, data) {
|
|
8
|
+
Object.defineProperty(this, "width", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(this, "height", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: void 0
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "data", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: void 0
|
|
25
|
+
});
|
|
26
|
+
this.width = width;
|
|
27
|
+
this.height = height;
|
|
28
|
+
this.data = data;
|
|
29
|
+
}
|
|
30
|
+
writeBytes(output, bytes) {
|
|
31
|
+
output.push(...bytes);
|
|
32
|
+
}
|
|
33
|
+
writeUint16LE(output, value) {
|
|
34
|
+
output.push(value & 0xff);
|
|
35
|
+
output.push((value >> 8) & 0xff);
|
|
36
|
+
}
|
|
37
|
+
writeString(output, str) {
|
|
38
|
+
for (let i = 0; i < str.length; i++) {
|
|
39
|
+
output.push(str.charCodeAt(i));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Quantize a color channel value to a specified number of levels
|
|
44
|
+
* @param value - The input color channel value (0-255)
|
|
45
|
+
* @param levels - Number of quantization levels minus 1 (e.g., 7 for 8 levels)
|
|
46
|
+
* @param step - Step size for quantization (e.g., 255/7)
|
|
47
|
+
* @returns Quantized integer value
|
|
48
|
+
*/
|
|
49
|
+
quantizeChannel(value, levels, step) {
|
|
50
|
+
// First round to get quantization level (0-levels)
|
|
51
|
+
// Then multiply by step size and round to ensure integer result
|
|
52
|
+
return Math.round(Math.round(value * levels / 255) * step);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Quantize RGBA image to 256 colors using median cut algorithm
|
|
56
|
+
*/
|
|
57
|
+
quantize() {
|
|
58
|
+
// Simple quantization: collect unique colors and build palette
|
|
59
|
+
const colorMap = new Map();
|
|
60
|
+
const colors = [];
|
|
61
|
+
// Color quantization parameters for 8-bit palette (256 colors)
|
|
62
|
+
// R/G: 8 levels (0-7) using 3 bits, B: 4 levels (0-3) using 2 bits (8*8*4=256)
|
|
63
|
+
const RG_LEVELS = 7; // Max level for R/G (8 total levels: 0-7)
|
|
64
|
+
const B_LEVELS = 3; // Max level for B (4 total levels: 0-3)
|
|
65
|
+
const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
|
|
66
|
+
const bStep = 255 / B_LEVELS; // Step size for B quantization
|
|
67
|
+
// Collect unique colors
|
|
68
|
+
for (let i = 0; i < this.data.length; i += 4) {
|
|
69
|
+
const r = this.data[i];
|
|
70
|
+
const g = this.data[i + 1];
|
|
71
|
+
const b = this.data[i + 2];
|
|
72
|
+
const key = `${r},${g},${b}`;
|
|
73
|
+
if (!colorMap.has(key) && colors.length < 256) {
|
|
74
|
+
colorMap.set(key, colors.length);
|
|
75
|
+
colors.push({ r, g, b });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Track if color reduction was applied
|
|
79
|
+
let useColorReduction = false;
|
|
80
|
+
// If we have too many colors, use simple color reduction
|
|
81
|
+
if (colors.length >= 256) {
|
|
82
|
+
// Downsample colors to 256 by reducing color depth
|
|
83
|
+
colorMap.clear();
|
|
84
|
+
colors.length = 0;
|
|
85
|
+
useColorReduction = true;
|
|
86
|
+
for (let i = 0; i < this.data.length; i += 4) {
|
|
87
|
+
// Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
|
|
88
|
+
// This gives us 8 bits total = 256 possible colors
|
|
89
|
+
const r = this.quantizeChannel(this.data[i], RG_LEVELS, rgStep);
|
|
90
|
+
const g = this.quantizeChannel(this.data[i + 1], RG_LEVELS, rgStep);
|
|
91
|
+
const b = this.quantizeChannel(this.data[i + 2], B_LEVELS, bStep);
|
|
92
|
+
const key = `${r},${g},${b}`;
|
|
93
|
+
if (!colorMap.has(key)) {
|
|
94
|
+
if (colors.length < 256) {
|
|
95
|
+
colorMap.set(key, colors.length);
|
|
96
|
+
colors.push({ r, g, b });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Pad to power of 2
|
|
102
|
+
const paletteSize = Math.max(2, this.nextPowerOf2(colors.length));
|
|
103
|
+
while (colors.length < paletteSize) {
|
|
104
|
+
colors.push({ r: 0, g: 0, b: 0 });
|
|
105
|
+
}
|
|
106
|
+
// Create palette
|
|
107
|
+
const palette = new Uint8Array(colors.length * 3);
|
|
108
|
+
for (let i = 0; i < colors.length; i++) {
|
|
109
|
+
palette[i * 3] = colors[i].r;
|
|
110
|
+
palette[i * 3 + 1] = colors[i].g;
|
|
111
|
+
palette[i * 3 + 2] = colors[i].b;
|
|
112
|
+
}
|
|
113
|
+
// Create indexed data
|
|
114
|
+
const indexed = new Uint8Array(this.width * this.height);
|
|
115
|
+
for (let i = 0, j = 0; i < this.data.length; i += 4, j++) {
|
|
116
|
+
let r = this.data[i];
|
|
117
|
+
let g = this.data[i + 1];
|
|
118
|
+
let b = this.data[i + 2];
|
|
119
|
+
// Apply color reduction if it was used for building the palette
|
|
120
|
+
if (useColorReduction) {
|
|
121
|
+
r = this.quantizeChannel(r, RG_LEVELS, rgStep);
|
|
122
|
+
g = this.quantizeChannel(g, RG_LEVELS, rgStep);
|
|
123
|
+
b = this.quantizeChannel(b, B_LEVELS, bStep);
|
|
124
|
+
}
|
|
125
|
+
const key = `${r},${g},${b}`;
|
|
126
|
+
// Try fast O(1) lookup first
|
|
127
|
+
if (colorMap.has(key)) {
|
|
128
|
+
indexed[j] = colorMap.get(key);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Fallback: find closest color in palette (shouldn't happen often)
|
|
132
|
+
let minDist = Infinity;
|
|
133
|
+
let bestIdx = 0;
|
|
134
|
+
for (let k = 0; k < colors.length; k++) {
|
|
135
|
+
const dr = r - colors[k].r;
|
|
136
|
+
const dg = g - colors[k].g;
|
|
137
|
+
const db = b - colors[k].b;
|
|
138
|
+
const dist = dr * dr + dg * dg + db * db;
|
|
139
|
+
if (dist < minDist) {
|
|
140
|
+
minDist = dist;
|
|
141
|
+
bestIdx = k;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
indexed[j] = bestIdx;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { palette, indexed };
|
|
148
|
+
}
|
|
149
|
+
nextPowerOf2(n) {
|
|
150
|
+
let power = 1;
|
|
151
|
+
while (power < n) {
|
|
152
|
+
power *= 2;
|
|
153
|
+
}
|
|
154
|
+
return power;
|
|
155
|
+
}
|
|
156
|
+
getBitsPerColor(paletteSize) {
|
|
157
|
+
let bits = 1;
|
|
158
|
+
while ((1 << bits) < paletteSize) {
|
|
159
|
+
bits++;
|
|
160
|
+
}
|
|
161
|
+
return Math.max(2, bits);
|
|
162
|
+
}
|
|
163
|
+
encode() {
|
|
164
|
+
const output = [];
|
|
165
|
+
// Quantize image
|
|
166
|
+
const { palette, indexed } = this.quantize();
|
|
167
|
+
const paletteSize = palette.length / 3;
|
|
168
|
+
const bitsPerColor = this.getBitsPerColor(paletteSize);
|
|
169
|
+
// Header
|
|
170
|
+
this.writeString(output, "GIF89a");
|
|
171
|
+
// Logical Screen Descriptor
|
|
172
|
+
this.writeUint16LE(output, this.width);
|
|
173
|
+
this.writeUint16LE(output, this.height);
|
|
174
|
+
// Packed field:
|
|
175
|
+
// - Global Color Table Flag (1 bit): 1
|
|
176
|
+
// - Color Resolution (3 bits): bitsPerColor - 1
|
|
177
|
+
// - Sort Flag (1 bit): 0
|
|
178
|
+
// - Size of Global Color Table (3 bits): bitsPerColor - 1
|
|
179
|
+
const packed = 0x80 | ((bitsPerColor - 1) << 4) | (bitsPerColor - 1);
|
|
180
|
+
output.push(packed);
|
|
181
|
+
// Background Color Index
|
|
182
|
+
output.push(0);
|
|
183
|
+
// Pixel Aspect Ratio
|
|
184
|
+
output.push(0);
|
|
185
|
+
// Global Color Table
|
|
186
|
+
// The GCT size is 2^(n+1) where n is the value in the packed field
|
|
187
|
+
// So we need to write that many colors, padding if necessary
|
|
188
|
+
const gctSize = 1 << bitsPerColor;
|
|
189
|
+
const paddedPalette = new Uint8Array(gctSize * 3);
|
|
190
|
+
paddedPalette.set(palette);
|
|
191
|
+
this.writeBytes(output, paddedPalette);
|
|
192
|
+
// Image Descriptor
|
|
193
|
+
output.push(0x2c); // Image Separator
|
|
194
|
+
// Image position and dimensions
|
|
195
|
+
this.writeUint16LE(output, 0); // Left
|
|
196
|
+
this.writeUint16LE(output, 0); // Top
|
|
197
|
+
this.writeUint16LE(output, this.width);
|
|
198
|
+
this.writeUint16LE(output, this.height);
|
|
199
|
+
// Packed field:
|
|
200
|
+
// - Local Color Table Flag (1 bit): 0
|
|
201
|
+
// - Interlace Flag (1 bit): 0
|
|
202
|
+
// - Sort Flag (1 bit): 0
|
|
203
|
+
// - Reserved (2 bits): 0
|
|
204
|
+
// - Size of Local Color Table (3 bits): 0
|
|
205
|
+
output.push(0);
|
|
206
|
+
// LZW Minimum Code Size
|
|
207
|
+
const minCodeSize = Math.max(2, bitsPerColor);
|
|
208
|
+
output.push(minCodeSize);
|
|
209
|
+
// Compress image data with LZW
|
|
210
|
+
const encoder = new LZWEncoder(minCodeSize);
|
|
211
|
+
const compressed = encoder.compress(indexed);
|
|
212
|
+
// Write compressed data in sub-blocks (max 255 bytes per block)
|
|
213
|
+
for (let i = 0; i < compressed.length; i += 255) {
|
|
214
|
+
const blockSize = Math.min(255, compressed.length - i);
|
|
215
|
+
output.push(blockSize);
|
|
216
|
+
for (let j = 0; j < blockSize; j++) {
|
|
217
|
+
output.push(compressed[i + j]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Block Terminator
|
|
221
|
+
output.push(0);
|
|
222
|
+
// Trailer
|
|
223
|
+
output.push(0x3b);
|
|
224
|
+
return new Uint8Array(output);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic baseline JPEG decoder implementation
|
|
3
|
+
* Supports baseline DCT JPEG images (the most common format)
|
|
4
|
+
*
|
|
5
|
+
* This is a simplified implementation that handles common JPEG files.
|
|
6
|
+
* For complex or non-standard JPEGs, the ImageDecoder API fallback is preferred.
|
|
7
|
+
*/
|
|
8
|
+
export declare class JPEGDecoder {
|
|
9
|
+
private data;
|
|
10
|
+
private pos;
|
|
11
|
+
private width;
|
|
12
|
+
private height;
|
|
13
|
+
private components;
|
|
14
|
+
private qTables;
|
|
15
|
+
private dcTables;
|
|
16
|
+
private acTables;
|
|
17
|
+
private restartInterval;
|
|
18
|
+
private bitBuffer;
|
|
19
|
+
private bitCount;
|
|
20
|
+
constructor(data: Uint8Array);
|
|
21
|
+
decode(): Uint8Array;
|
|
22
|
+
private readMarker;
|
|
23
|
+
private readUint16;
|
|
24
|
+
private skipSegment;
|
|
25
|
+
private parseDQT;
|
|
26
|
+
private parseDHT;
|
|
27
|
+
private buildHuffmanTable;
|
|
28
|
+
private parseSOF;
|
|
29
|
+
private parseSOS;
|
|
30
|
+
private parseDRI;
|
|
31
|
+
private decodeScan;
|
|
32
|
+
private decodeBlock;
|
|
33
|
+
private decodeHuffman;
|
|
34
|
+
private readBit;
|
|
35
|
+
private receiveBits;
|
|
36
|
+
private idct;
|
|
37
|
+
private convertToRGB;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=jpeg_decoder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jpeg_decoder.d.ts","sourceRoot":"","sources":["../../../src/src/utils/jpeg_decoder.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkGH,qBAAa,WAAW;IACtB,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,QAAQ,CAAa;gBAEjB,IAAI,EAAE,UAAU;IAI5B,MAAM,IAAI,UAAU;IAwDpB,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,QAAQ;IAsBhB,OAAO,CAAC,QAAQ;IAgChB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,QAAQ;IAmChB,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,UAAU;IAiDlB,OAAO,CAAC,WAAW;IAiDnB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,OAAO;IA2Bf,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,IAAI;IAqCZ,OAAO,CAAC,YAAY;CAsGrB"}
|