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,385 @@
|
|
|
1
|
+
import { GIFDecoder } from "../utils/gif_decoder.js";
|
|
2
|
+
import { GIFEncoder } from "../utils/gif_encoder.js";
|
|
3
|
+
/**
|
|
4
|
+
* GIF format handler
|
|
5
|
+
* Now includes pure-JS implementation with custom LZW compression/decompression
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - LZW compression/decompression
|
|
9
|
+
* - Color quantization and palette generation for encoding
|
|
10
|
+
* - Interlacing support
|
|
11
|
+
* - Transparency support
|
|
12
|
+
* - Multi-frame animation support (decoding and encoding)
|
|
13
|
+
* - Falls back to runtime APIs when pure-JS fails
|
|
14
|
+
*/
|
|
15
|
+
export class GIFFormat {
|
|
16
|
+
constructor() {
|
|
17
|
+
Object.defineProperty(this, "name", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: "gif"
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "mimeType", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: "image/gif"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
supportsMultipleFrames() {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
canDecode(data) {
|
|
34
|
+
// GIF signature: "GIF87a" or "GIF89a"
|
|
35
|
+
return data.length >= 6 &&
|
|
36
|
+
data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 && // "GIF"
|
|
37
|
+
data[3] === 0x38 && // "8"
|
|
38
|
+
(data[4] === 0x37 || data[4] === 0x39) && // "7" or "9"
|
|
39
|
+
data[5] === 0x61; // "a"
|
|
40
|
+
}
|
|
41
|
+
async decode(data) {
|
|
42
|
+
if (!this.canDecode(data)) {
|
|
43
|
+
throw new Error("Invalid GIF signature");
|
|
44
|
+
}
|
|
45
|
+
// Try pure-JS decoder first
|
|
46
|
+
try {
|
|
47
|
+
const decoder = new GIFDecoder(data);
|
|
48
|
+
const result = decoder.decode();
|
|
49
|
+
// Extract metadata from comment extensions
|
|
50
|
+
const metadata = this.extractMetadata(data);
|
|
51
|
+
return {
|
|
52
|
+
width: result.width,
|
|
53
|
+
height: result.height,
|
|
54
|
+
data: result.data,
|
|
55
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Fall back to runtime decoder if pure-JS fails
|
|
60
|
+
console.warn("Pure-JS GIF decoder failed, falling back to runtime:", error);
|
|
61
|
+
let pos = 6; // Skip "GIF89a" or "GIF87a"
|
|
62
|
+
const width = this.readUint16LE(data, pos);
|
|
63
|
+
pos += 2;
|
|
64
|
+
const height = this.readUint16LE(data, pos);
|
|
65
|
+
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
66
|
+
const metadata = this.extractMetadata(data);
|
|
67
|
+
return {
|
|
68
|
+
width,
|
|
69
|
+
height,
|
|
70
|
+
data: rgba,
|
|
71
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
extractMetadata(data) {
|
|
76
|
+
const metadata = {};
|
|
77
|
+
let pos = 6; // Skip "GIF89a" or "GIF87a"
|
|
78
|
+
// Read logical screen descriptor
|
|
79
|
+
pos += 4; // Skip width and height
|
|
80
|
+
const packed = data[pos++];
|
|
81
|
+
const hasGlobalColorTable = (packed & 0x80) !== 0;
|
|
82
|
+
const globalColorTableSize = 2 << (packed & 0x07);
|
|
83
|
+
pos++; // background color
|
|
84
|
+
pos++; // aspect ratio
|
|
85
|
+
// Skip global color table if present
|
|
86
|
+
if (hasGlobalColorTable) {
|
|
87
|
+
pos += globalColorTableSize * 3;
|
|
88
|
+
}
|
|
89
|
+
// Parse extensions
|
|
90
|
+
while (pos < data.length) {
|
|
91
|
+
if (data[pos] === 0x21) { // Extension
|
|
92
|
+
const label = data[pos + 1];
|
|
93
|
+
pos += 2;
|
|
94
|
+
if (label === 0xfe) { // Comment Extension
|
|
95
|
+
const comment = this.readDataSubBlocks(data, pos);
|
|
96
|
+
if (comment.text) {
|
|
97
|
+
this.parseComment(comment.text, metadata);
|
|
98
|
+
}
|
|
99
|
+
pos = comment.endPos;
|
|
100
|
+
}
|
|
101
|
+
else if (label === 0xff) { // Application Extension
|
|
102
|
+
const appData = this.readDataSubBlocks(data, pos);
|
|
103
|
+
if (appData.text && appData.text.startsWith("XMP DataXMP") ||
|
|
104
|
+
appData.text.includes("<?xpacket")) {
|
|
105
|
+
this.parseXMP(appData.text, metadata);
|
|
106
|
+
}
|
|
107
|
+
pos = appData.endPos;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Skip other extensions
|
|
111
|
+
while (pos < data.length && data[pos] !== 0) {
|
|
112
|
+
const blockSize = data[pos++];
|
|
113
|
+
pos += blockSize;
|
|
114
|
+
}
|
|
115
|
+
pos++; // Skip block terminator
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (data[pos] === 0x3b) { // Trailer
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
pos++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return metadata;
|
|
126
|
+
}
|
|
127
|
+
async encode(imageData) {
|
|
128
|
+
const { width, height, data, metadata } = imageData;
|
|
129
|
+
// Try pure-JS encoder first
|
|
130
|
+
try {
|
|
131
|
+
const encoder = new GIFEncoder(width, height, data);
|
|
132
|
+
const encoded = encoder.encode();
|
|
133
|
+
// Inject metadata if present
|
|
134
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
135
|
+
const injected = this.injectMetadata(encoded, metadata);
|
|
136
|
+
return injected;
|
|
137
|
+
}
|
|
138
|
+
return encoded;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Fall back to runtime encoding if pure-JS fails
|
|
142
|
+
console.warn("Pure-JS GIF encoder failed, falling back to runtime:", error);
|
|
143
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
144
|
+
try {
|
|
145
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
146
|
+
const ctx = canvas.getContext("2d");
|
|
147
|
+
if (!ctx)
|
|
148
|
+
throw new Error("Could not get canvas context");
|
|
149
|
+
const imgData = ctx.createImageData(width, height);
|
|
150
|
+
const imgDataData = new Uint8ClampedArray(data);
|
|
151
|
+
imgData.data.set(imgDataData);
|
|
152
|
+
ctx.putImageData(imgData, 0, 0);
|
|
153
|
+
const blob = await canvas.convertToBlob({
|
|
154
|
+
type: "image/gif",
|
|
155
|
+
});
|
|
156
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
157
|
+
const encoded = new Uint8Array(arrayBuffer);
|
|
158
|
+
// Inject metadata if present
|
|
159
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
160
|
+
const injected = this.injectMetadata(encoded, metadata);
|
|
161
|
+
return injected;
|
|
162
|
+
}
|
|
163
|
+
return encoded;
|
|
164
|
+
}
|
|
165
|
+
catch (runtimeError) {
|
|
166
|
+
throw new Error(`GIF encoding failed: ${runtimeError}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new Error("GIF encoding requires pure-JS support or OffscreenCanvas API");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Decode all frames from an animated GIF
|
|
174
|
+
*/
|
|
175
|
+
decodeFrames(data) {
|
|
176
|
+
if (!this.canDecode(data)) {
|
|
177
|
+
throw new Error("Invalid GIF signature");
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const decoder = new GIFDecoder(data);
|
|
181
|
+
const result = decoder.decodeAllFrames();
|
|
182
|
+
// Extract metadata from comment extensions
|
|
183
|
+
const metadata = this.extractMetadata(data);
|
|
184
|
+
return Promise.resolve({
|
|
185
|
+
width: result.width,
|
|
186
|
+
height: result.height,
|
|
187
|
+
frames: result.frames.map((frame) => ({
|
|
188
|
+
width: frame.width,
|
|
189
|
+
height: frame.height,
|
|
190
|
+
data: frame.data,
|
|
191
|
+
frameMetadata: {
|
|
192
|
+
left: frame.left,
|
|
193
|
+
top: frame.top,
|
|
194
|
+
// Convert GIF delay from centiseconds (1/100s) to milliseconds
|
|
195
|
+
delay: frame.delay * 10,
|
|
196
|
+
disposal: this.mapDisposalMethod(frame.disposal),
|
|
197
|
+
},
|
|
198
|
+
})),
|
|
199
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
throw new Error(`GIF multi-frame decoding failed: ${error}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Encode multi-frame image data to animated GIF
|
|
208
|
+
* Note: Currently not implemented, will encode only first frame
|
|
209
|
+
*/
|
|
210
|
+
encodeFrames(imageData, _options) {
|
|
211
|
+
// For now, just encode the first frame using the existing encoder
|
|
212
|
+
// Full multi-frame encoding would require a more complex GIFEncoder
|
|
213
|
+
if (imageData.frames.length === 0) {
|
|
214
|
+
throw new Error("No frames to encode");
|
|
215
|
+
}
|
|
216
|
+
const firstFrame = imageData.frames[0];
|
|
217
|
+
const singleFrameData = {
|
|
218
|
+
width: firstFrame.width,
|
|
219
|
+
height: firstFrame.height,
|
|
220
|
+
data: firstFrame.data,
|
|
221
|
+
metadata: imageData.metadata,
|
|
222
|
+
};
|
|
223
|
+
return this.encode(singleFrameData);
|
|
224
|
+
}
|
|
225
|
+
mapDisposalMethod(disposal) {
|
|
226
|
+
switch (disposal) {
|
|
227
|
+
case 0:
|
|
228
|
+
case 1:
|
|
229
|
+
return "none";
|
|
230
|
+
case 2:
|
|
231
|
+
return "background";
|
|
232
|
+
case 3:
|
|
233
|
+
return "previous";
|
|
234
|
+
default:
|
|
235
|
+
return "none";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
readUint16LE(data, offset) {
|
|
239
|
+
return data[offset] | (data[offset + 1] << 8);
|
|
240
|
+
}
|
|
241
|
+
async decodeUsingRuntime(data, _width, _height) {
|
|
242
|
+
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
243
|
+
if (typeof ImageDecoder !== "undefined") {
|
|
244
|
+
try {
|
|
245
|
+
const decoder = new ImageDecoder({ data, type: "image/gif" });
|
|
246
|
+
const result = await decoder.decode();
|
|
247
|
+
const bitmap = result.image;
|
|
248
|
+
// Create a canvas to extract pixel data
|
|
249
|
+
const canvas = new OffscreenCanvas(bitmap.displayWidth, bitmap.displayHeight);
|
|
250
|
+
const ctx = canvas.getContext("2d");
|
|
251
|
+
if (!ctx)
|
|
252
|
+
throw new Error("Could not get canvas context");
|
|
253
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
254
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
255
|
+
bitmap.close();
|
|
256
|
+
return new Uint8Array(imageData.data.buffer);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
throw new Error(`GIF decoding failed: ${error}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
throw new Error("GIF decoding requires ImageDecoder API or equivalent runtime support");
|
|
263
|
+
}
|
|
264
|
+
// Metadata parsing and injection methods
|
|
265
|
+
readDataSubBlocks(data, pos) {
|
|
266
|
+
const blocks = [];
|
|
267
|
+
while (pos < data.length) {
|
|
268
|
+
const blockSize = data[pos++];
|
|
269
|
+
if (blockSize === 0)
|
|
270
|
+
break;
|
|
271
|
+
for (let i = 0; i < blockSize && pos < data.length; i++) {
|
|
272
|
+
blocks.push(data[pos++]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
text: new TextDecoder().decode(new Uint8Array(blocks)),
|
|
277
|
+
endPos: pos,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
parseComment(comment, metadata) {
|
|
281
|
+
// Try to parse structured comments like "Title: xxx" or JSON
|
|
282
|
+
const lines = comment.split("\n");
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
const colonIdx = line.indexOf(":");
|
|
285
|
+
if (colonIdx > 0) {
|
|
286
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
287
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
288
|
+
switch (key) {
|
|
289
|
+
case "title":
|
|
290
|
+
metadata.title = value;
|
|
291
|
+
break;
|
|
292
|
+
case "description":
|
|
293
|
+
metadata.description = value;
|
|
294
|
+
break;
|
|
295
|
+
case "author":
|
|
296
|
+
case "artist":
|
|
297
|
+
metadata.author = value;
|
|
298
|
+
break;
|
|
299
|
+
case "copyright":
|
|
300
|
+
metadata.copyright = value;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// If no structured data, use entire comment as description
|
|
306
|
+
if (!metadata.title && !metadata.description && !metadata.author &&
|
|
307
|
+
!metadata.copyright) {
|
|
308
|
+
metadata.description = comment.trim();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
parseXMP(xmpStr, metadata) {
|
|
312
|
+
// Simple XMP parsing for common fields
|
|
313
|
+
try {
|
|
314
|
+
const titleMatch = xmpStr.match(/<dc:title[^>]*>([^<]+)<\/dc:title>/);
|
|
315
|
+
if (titleMatch)
|
|
316
|
+
metadata.title = titleMatch[1].trim();
|
|
317
|
+
const descMatch = xmpStr.match(/<dc:description[^>]*>([^<]+)<\/dc:description>/);
|
|
318
|
+
if (descMatch)
|
|
319
|
+
metadata.description = descMatch[1].trim();
|
|
320
|
+
const creatorMatch = xmpStr.match(/<dc:creator[^>]*>([^<]+)<\/dc:creator>/);
|
|
321
|
+
if (creatorMatch)
|
|
322
|
+
metadata.author = creatorMatch[1].trim();
|
|
323
|
+
const rightsMatch = xmpStr.match(/<dc:rights[^>]*>([^<]+)<\/dc:rights>/);
|
|
324
|
+
if (rightsMatch)
|
|
325
|
+
metadata.copyright = rightsMatch[1].trim();
|
|
326
|
+
}
|
|
327
|
+
catch (_e) {
|
|
328
|
+
// Ignore XMP parsing errors
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
injectMetadata(gifData, metadata) {
|
|
332
|
+
// GIF structure: Header + Logical Screen Descriptor + [Global Color Table] + Data + Trailer
|
|
333
|
+
// We'll inject a Comment Extension after the Logical Screen Descriptor
|
|
334
|
+
const result = [];
|
|
335
|
+
// Copy header and logical screen descriptor
|
|
336
|
+
let pos = 0;
|
|
337
|
+
for (let i = 0; i < 13; i++) {
|
|
338
|
+
result.push(gifData[pos++]);
|
|
339
|
+
}
|
|
340
|
+
// Check if there's a global color table
|
|
341
|
+
const packed = gifData[10];
|
|
342
|
+
const hasGlobalColorTable = (packed & 0x80) !== 0;
|
|
343
|
+
const globalColorTableSize = 2 << (packed & 0x07);
|
|
344
|
+
// Copy global color table if present
|
|
345
|
+
if (hasGlobalColorTable) {
|
|
346
|
+
for (let i = 0; i < globalColorTableSize * 3; i++) {
|
|
347
|
+
result.push(gifData[pos++]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Inject Comment Extension with metadata
|
|
351
|
+
const commentText = this.createCommentText(metadata);
|
|
352
|
+
if (commentText) {
|
|
353
|
+
result.push(0x21); // Extension Introducer
|
|
354
|
+
result.push(0xfe); // Comment Label
|
|
355
|
+
// Write comment in sub-blocks (max 255 bytes per block)
|
|
356
|
+
const commentBytes = new TextEncoder().encode(commentText);
|
|
357
|
+
for (let i = 0; i < commentBytes.length; i += 255) {
|
|
358
|
+
const blockSize = Math.min(255, commentBytes.length - i);
|
|
359
|
+
result.push(blockSize);
|
|
360
|
+
for (let j = 0; j < blockSize; j++) {
|
|
361
|
+
result.push(commentBytes[i + j]);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
result.push(0); // Block Terminator
|
|
365
|
+
}
|
|
366
|
+
// Copy rest of the GIF data
|
|
367
|
+
while (pos < gifData.length) {
|
|
368
|
+
result.push(gifData[pos++]);
|
|
369
|
+
}
|
|
370
|
+
return new Uint8Array(result);
|
|
371
|
+
}
|
|
372
|
+
createCommentText(metadata) {
|
|
373
|
+
const parts = [];
|
|
374
|
+
if (metadata.title)
|
|
375
|
+
parts.push(`Title: ${metadata.title}`);
|
|
376
|
+
if (metadata.description) {
|
|
377
|
+
parts.push(`Description: ${metadata.description}`);
|
|
378
|
+
}
|
|
379
|
+
if (metadata.author)
|
|
380
|
+
parts.push(`Author: ${metadata.author}`);
|
|
381
|
+
if (metadata.copyright)
|
|
382
|
+
parts.push(`Copyright: ${metadata.copyright}`);
|
|
383
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* JPEG format handler
|
|
4
|
+
* Implements a basic JPEG decoder and encoder
|
|
5
|
+
*/
|
|
6
|
+
export declare class JPEGFormat implements ImageFormat {
|
|
7
|
+
readonly name = "jpeg";
|
|
8
|
+
readonly mimeType = "image/jpeg";
|
|
9
|
+
canDecode(data: Uint8Array): boolean;
|
|
10
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
11
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
12
|
+
private injectMetadata;
|
|
13
|
+
private decodeUsingRuntime;
|
|
14
|
+
private parseJFIF;
|
|
15
|
+
private parseEXIF;
|
|
16
|
+
private createEXIFData;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=jpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jpeg.d.ts","sourceRoot":"","sources":["../../../src/src/formats/jpeg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAiB,MAAM,aAAa,CAAC;AAKzE;;;GAGG;AACH,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,IAAI,UAAU;IACvB,QAAQ,CAAC,QAAQ,gBAAgB;IAEjC,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAM9B,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IA2E5C,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAgDvD,OAAO,CAAC,cAAc;YA+CR,kBAAkB;IAgDhC,OAAO,CAAC,SAAS;IAkCjB,OAAO,CAAC,SAAS;IAyIjB,OAAO,CAAC,cAAc;CAgHvB"}
|