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
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APNGFormat = void 0;
|
|
4
|
+
const security_js_1 = require("../utils/security.js");
|
|
5
|
+
const png_base_js_1 = require("./png_base.js");
|
|
6
|
+
/**
|
|
7
|
+
* APNG (Animated PNG) format handler
|
|
8
|
+
* Implements support for animated PNG images with multiple frames
|
|
9
|
+
* APNG extends PNG with animation control chunks (acTL, fcTL, fdAT)
|
|
10
|
+
*/
|
|
11
|
+
class APNGFormat extends png_base_js_1.PNGBase {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
/** Format name identifier */
|
|
15
|
+
Object.defineProperty(this, "name", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: "apng"
|
|
20
|
+
});
|
|
21
|
+
/** MIME type for APNG images */
|
|
22
|
+
Object.defineProperty(this, "mimeType", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: "image/apng"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if this format supports multiple frames (animations)
|
|
31
|
+
* @returns true for APNG format
|
|
32
|
+
*/
|
|
33
|
+
supportsMultipleFrames() {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if the given data is an APNG image
|
|
38
|
+
* @param data Raw image data to check
|
|
39
|
+
* @returns true if data has PNG signature and contains acTL chunk
|
|
40
|
+
*/
|
|
41
|
+
canDecode(data) {
|
|
42
|
+
// PNG signature: 137 80 78 71 13 10 26 10
|
|
43
|
+
if (data.length < 8 ||
|
|
44
|
+
data[0] !== 137 || data[1] !== 80 ||
|
|
45
|
+
data[2] !== 78 || data[3] !== 71 ||
|
|
46
|
+
data[4] !== 13 || data[5] !== 10 ||
|
|
47
|
+
data[6] !== 26 || data[7] !== 10) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Check for acTL (animation control) chunk to identify APNG
|
|
51
|
+
let pos = 8;
|
|
52
|
+
while (pos + 8 < data.length) {
|
|
53
|
+
const length = this.readUint32(data, pos);
|
|
54
|
+
pos += 4;
|
|
55
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
56
|
+
pos += 4;
|
|
57
|
+
if (type === "acTL") {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
pos += length + 4; // Skip chunk data and CRC
|
|
61
|
+
if (type === "IDAT") {
|
|
62
|
+
// If we hit IDAT before acTL, it's not an APNG
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode APNG image data to RGBA (first frame only)
|
|
70
|
+
* @param data Raw APNG image data
|
|
71
|
+
* @returns Decoded image data with RGBA pixels of first frame
|
|
72
|
+
*/
|
|
73
|
+
async decode(data) {
|
|
74
|
+
const frames = await this.decodeFrames(data);
|
|
75
|
+
const firstFrame = frames.frames[0];
|
|
76
|
+
return {
|
|
77
|
+
width: firstFrame.width,
|
|
78
|
+
height: firstFrame.height,
|
|
79
|
+
data: firstFrame.data,
|
|
80
|
+
metadata: frames.metadata,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decode all frames from APNG image
|
|
85
|
+
* @param data Raw APNG image data
|
|
86
|
+
* @returns Decoded multi-frame image data
|
|
87
|
+
*/
|
|
88
|
+
async decodeFrames(data) {
|
|
89
|
+
if (!this.canDecode(data)) {
|
|
90
|
+
throw new Error("Invalid APNG signature or missing acTL chunk");
|
|
91
|
+
}
|
|
92
|
+
let pos = 8; // Skip PNG signature
|
|
93
|
+
let width = 0;
|
|
94
|
+
let height = 0;
|
|
95
|
+
let bitDepth = 0;
|
|
96
|
+
let colorType = 0;
|
|
97
|
+
const metadata = {};
|
|
98
|
+
const frames = [];
|
|
99
|
+
// First pass: parse structure and extract metadata
|
|
100
|
+
const chunkList = [];
|
|
101
|
+
while (pos < data.length) {
|
|
102
|
+
const length = this.readUint32(data, pos);
|
|
103
|
+
pos += 4;
|
|
104
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
105
|
+
pos += 4;
|
|
106
|
+
const chunkData = data.slice(pos, pos + length);
|
|
107
|
+
const chunkPos = pos;
|
|
108
|
+
pos += length;
|
|
109
|
+
pos += 4; // Skip CRC
|
|
110
|
+
chunkList.push({ type, data: chunkData, pos: chunkPos });
|
|
111
|
+
if (type === "IHDR") {
|
|
112
|
+
width = this.readUint32(chunkData, 0);
|
|
113
|
+
height = this.readUint32(chunkData, 4);
|
|
114
|
+
bitDepth = chunkData[8];
|
|
115
|
+
colorType = chunkData[9];
|
|
116
|
+
}
|
|
117
|
+
else if (type === "acTL") {
|
|
118
|
+
// Animation control chunk - we'll use frame count later if needed
|
|
119
|
+
// const numFrames = this.readUint32(chunkData, 0);
|
|
120
|
+
// const numPlays = this.readUint32(chunkData, 4);
|
|
121
|
+
}
|
|
122
|
+
else if (type === "pHYs") {
|
|
123
|
+
this.parsePhysChunk(chunkData, metadata, width, height);
|
|
124
|
+
}
|
|
125
|
+
else if (type === "tEXt") {
|
|
126
|
+
this.parseTextChunk(chunkData, metadata);
|
|
127
|
+
}
|
|
128
|
+
else if (type === "iTXt") {
|
|
129
|
+
this.parseITxtChunk(chunkData, metadata);
|
|
130
|
+
}
|
|
131
|
+
else if (type === "eXIf") {
|
|
132
|
+
this.parseExifChunk(chunkData, metadata);
|
|
133
|
+
}
|
|
134
|
+
else if (type === "IEND") {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (width === 0 || height === 0) {
|
|
139
|
+
throw new Error("Invalid APNG: missing IHDR chunk");
|
|
140
|
+
}
|
|
141
|
+
(0, security_js_1.validateImageDimensions)(width, height);
|
|
142
|
+
// Second pass: decode frames
|
|
143
|
+
let currentFrameControl = null;
|
|
144
|
+
let frameDataChunks = [];
|
|
145
|
+
let defaultImageChunks = [];
|
|
146
|
+
let hasSeenFcTL = false;
|
|
147
|
+
for (const chunk of chunkList) {
|
|
148
|
+
if (chunk.type === "fcTL") {
|
|
149
|
+
// If we have a previous frame to decode
|
|
150
|
+
if (frameDataChunks.length > 0 && currentFrameControl) {
|
|
151
|
+
const frameData = await this.decodeFrameData(frameDataChunks, currentFrameControl.width, currentFrameControl.height, bitDepth, colorType);
|
|
152
|
+
frames.push({
|
|
153
|
+
width: currentFrameControl.width,
|
|
154
|
+
height: currentFrameControl.height,
|
|
155
|
+
data: frameData,
|
|
156
|
+
frameMetadata: {
|
|
157
|
+
delay: currentFrameControl.delay,
|
|
158
|
+
disposal: currentFrameControl.disposal,
|
|
159
|
+
left: currentFrameControl.xOffset,
|
|
160
|
+
top: currentFrameControl.yOffset,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
frameDataChunks = [];
|
|
164
|
+
}
|
|
165
|
+
// Parse frame control
|
|
166
|
+
const _fcSeq = this.readUint32(chunk.data, 0);
|
|
167
|
+
const fcWidth = this.readUint32(chunk.data, 4);
|
|
168
|
+
const fcHeight = this.readUint32(chunk.data, 8);
|
|
169
|
+
const fcXOffset = this.readUint32(chunk.data, 12);
|
|
170
|
+
const fcYOffset = this.readUint32(chunk.data, 16);
|
|
171
|
+
const delayNum = this.readUint16(chunk.data, 20);
|
|
172
|
+
const delayDen = this.readUint16(chunk.data, 22);
|
|
173
|
+
const disposeOp = chunk.data[24];
|
|
174
|
+
const blendOp = chunk.data[25];
|
|
175
|
+
const delay = delayDen === 0
|
|
176
|
+
? delayNum * 10
|
|
177
|
+
: Math.round((delayNum / delayDen) * 1000);
|
|
178
|
+
let disposal = "none";
|
|
179
|
+
if (disposeOp === 1)
|
|
180
|
+
disposal = "background";
|
|
181
|
+
else if (disposeOp === 2)
|
|
182
|
+
disposal = "previous";
|
|
183
|
+
currentFrameControl = {
|
|
184
|
+
width: fcWidth,
|
|
185
|
+
height: fcHeight,
|
|
186
|
+
xOffset: fcXOffset,
|
|
187
|
+
yOffset: fcYOffset,
|
|
188
|
+
delay,
|
|
189
|
+
disposal,
|
|
190
|
+
blend: blendOp === 1 ? "over" : "source",
|
|
191
|
+
};
|
|
192
|
+
// If this is the first fcTL and we have default image data, use it for this frame
|
|
193
|
+
if (frames.length === 0 && defaultImageChunks.length > 0) {
|
|
194
|
+
frameDataChunks = defaultImageChunks;
|
|
195
|
+
defaultImageChunks = [];
|
|
196
|
+
}
|
|
197
|
+
hasSeenFcTL = true;
|
|
198
|
+
}
|
|
199
|
+
else if (chunk.type === "IDAT") {
|
|
200
|
+
if (!hasSeenFcTL) {
|
|
201
|
+
// Collect default image chunks
|
|
202
|
+
defaultImageChunks.push(chunk.data);
|
|
203
|
+
}
|
|
204
|
+
else if (currentFrameControl) {
|
|
205
|
+
// IDAT after first fcTL belongs to that frame
|
|
206
|
+
frameDataChunks.push(chunk.data);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (chunk.type === "fdAT") {
|
|
210
|
+
// Frame data chunk (skip sequence number)
|
|
211
|
+
const _frameSeq = this.readUint32(chunk.data, 0);
|
|
212
|
+
frameDataChunks.push(chunk.data.slice(4));
|
|
213
|
+
}
|
|
214
|
+
else if (chunk.type === "IEND") {
|
|
215
|
+
// Decode last frame if any
|
|
216
|
+
if (frameDataChunks.length > 0 && currentFrameControl) {
|
|
217
|
+
const frameData = await this.decodeFrameData(frameDataChunks, currentFrameControl.width, currentFrameControl.height, bitDepth, colorType);
|
|
218
|
+
frames.push({
|
|
219
|
+
width: currentFrameControl.width,
|
|
220
|
+
height: currentFrameControl.height,
|
|
221
|
+
data: frameData,
|
|
222
|
+
frameMetadata: {
|
|
223
|
+
delay: currentFrameControl.delay,
|
|
224
|
+
disposal: currentFrameControl.disposal,
|
|
225
|
+
left: currentFrameControl.xOffset,
|
|
226
|
+
top: currentFrameControl.yOffset,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else if (defaultImageChunks.length > 0) {
|
|
231
|
+
// Only default image, no fcTL found - treat as single frame
|
|
232
|
+
const frameData = await this.decodeFrameData(defaultImageChunks, width, height, bitDepth, colorType);
|
|
233
|
+
frames.push({
|
|
234
|
+
width,
|
|
235
|
+
height,
|
|
236
|
+
data: frameData,
|
|
237
|
+
frameMetadata: {
|
|
238
|
+
delay: 0,
|
|
239
|
+
disposal: "none",
|
|
240
|
+
left: 0,
|
|
241
|
+
top: 0,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
width,
|
|
250
|
+
height,
|
|
251
|
+
frames,
|
|
252
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Encode RGBA image data to APNG format (single frame)
|
|
257
|
+
* @param imageData Image data to encode
|
|
258
|
+
* @returns Encoded APNG image bytes
|
|
259
|
+
*/
|
|
260
|
+
encode(imageData) {
|
|
261
|
+
// For single frame, create a multi-frame with one frame
|
|
262
|
+
const multiFrame = {
|
|
263
|
+
width: imageData.width,
|
|
264
|
+
height: imageData.height,
|
|
265
|
+
frames: [{
|
|
266
|
+
width: imageData.width,
|
|
267
|
+
height: imageData.height,
|
|
268
|
+
data: imageData.data,
|
|
269
|
+
frameMetadata: { delay: 0 },
|
|
270
|
+
}],
|
|
271
|
+
metadata: imageData.metadata,
|
|
272
|
+
};
|
|
273
|
+
return this.encodeFrames(multiFrame);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Encode multi-frame image data to APNG format
|
|
277
|
+
* @param imageData Multi-frame image data to encode
|
|
278
|
+
* @returns Encoded APNG image bytes
|
|
279
|
+
*/
|
|
280
|
+
async encodeFrames(imageData) {
|
|
281
|
+
const { width, height, frames, metadata } = imageData;
|
|
282
|
+
if (frames.length === 0) {
|
|
283
|
+
throw new Error("No frames to encode");
|
|
284
|
+
}
|
|
285
|
+
// Prepare IHDR chunk
|
|
286
|
+
const ihdr = new Uint8Array(13);
|
|
287
|
+
this.writeUint32(ihdr, 0, width);
|
|
288
|
+
this.writeUint32(ihdr, 4, height);
|
|
289
|
+
ihdr[8] = 8; // bit depth
|
|
290
|
+
ihdr[9] = 6; // color type: RGBA
|
|
291
|
+
ihdr[10] = 0; // compression method
|
|
292
|
+
ihdr[11] = 0; // filter method
|
|
293
|
+
ihdr[12] = 0; // interlace method
|
|
294
|
+
// Build PNG
|
|
295
|
+
const chunks = [];
|
|
296
|
+
chunks.push(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])); // PNG signature
|
|
297
|
+
chunks.push(this.createChunk("IHDR", ihdr));
|
|
298
|
+
// Add acTL chunk for animation control
|
|
299
|
+
const actl = new Uint8Array(8);
|
|
300
|
+
this.writeUint32(actl, 0, frames.length); // num_frames
|
|
301
|
+
this.writeUint32(actl, 4, 0); // num_plays (0 = infinite)
|
|
302
|
+
chunks.push(this.createChunk("acTL", actl));
|
|
303
|
+
// Add metadata chunks if available
|
|
304
|
+
this.addMetadataChunks(chunks, metadata);
|
|
305
|
+
// Add frames
|
|
306
|
+
let sequenceNumber = 0;
|
|
307
|
+
for (let i = 0; i < frames.length; i++) {
|
|
308
|
+
const frame = frames[i];
|
|
309
|
+
const fctl = new Uint8Array(26);
|
|
310
|
+
this.writeUint32(fctl, 0, sequenceNumber++); // sequence_number
|
|
311
|
+
this.writeUint32(fctl, 4, frame.width); // width
|
|
312
|
+
this.writeUint32(fctl, 8, frame.height); // height
|
|
313
|
+
this.writeUint32(fctl, 12, frame.frameMetadata?.left ?? 0); // x_offset
|
|
314
|
+
this.writeUint32(fctl, 16, frame.frameMetadata?.top ?? 0); // y_offset
|
|
315
|
+
// Convert delay from milliseconds to fraction
|
|
316
|
+
const delay = frame.frameMetadata?.delay ?? 100;
|
|
317
|
+
// Use milliseconds directly if possible (up to ~65 seconds)
|
|
318
|
+
if (delay < 65536) {
|
|
319
|
+
this.writeUint16(fctl, 20, delay); // delay_num
|
|
320
|
+
this.writeUint16(fctl, 22, 1000); // delay_den (1/1000 sec)
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Fallback to 1/100 sec for longer delays
|
|
324
|
+
this.writeUint16(fctl, 20, Math.round(delay / 10)); // delay_num
|
|
325
|
+
this.writeUint16(fctl, 22, 100); // delay_den (1/100 sec)
|
|
326
|
+
}
|
|
327
|
+
// Disposal method
|
|
328
|
+
let disposeOp = 0; // APNG_DISPOSE_OP_NONE
|
|
329
|
+
if (frame.frameMetadata?.disposal === "background") {
|
|
330
|
+
disposeOp = 1; // APNG_DISPOSE_OP_BACKGROUND
|
|
331
|
+
}
|
|
332
|
+
else if (frame.frameMetadata?.disposal === "previous") {
|
|
333
|
+
disposeOp = 2; // APNG_DISPOSE_OP_PREVIOUS
|
|
334
|
+
}
|
|
335
|
+
fctl[24] = disposeOp;
|
|
336
|
+
fctl[25] = 0; // blend_op: APNG_BLEND_OP_SOURCE
|
|
337
|
+
chunks.push(this.createChunk("fcTL", fctl));
|
|
338
|
+
// Filter and compress frame data
|
|
339
|
+
const filtered = this.filterData(frame.data, frame.width, frame.height);
|
|
340
|
+
const compressed = await this.deflate(filtered);
|
|
341
|
+
if (i === 0) {
|
|
342
|
+
// First frame uses IDAT
|
|
343
|
+
chunks.push(this.createChunk("IDAT", compressed));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// Subsequent frames use fdAT with sequence number
|
|
347
|
+
const fdat = new Uint8Array(4 + compressed.length);
|
|
348
|
+
this.writeUint32(fdat, 0, sequenceNumber++);
|
|
349
|
+
fdat.set(compressed, 4);
|
|
350
|
+
chunks.push(this.createChunk("fdAT", fdat));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
chunks.push(this.createChunk("IEND", new Uint8Array(0)));
|
|
354
|
+
// Concatenate all chunks
|
|
355
|
+
return this.concatenateArrays(chunks);
|
|
356
|
+
}
|
|
357
|
+
// Helper methods for frame decoding
|
|
358
|
+
async decodeFrameData(chunks, width, height, bitDepth, colorType) {
|
|
359
|
+
// Concatenate chunks
|
|
360
|
+
const idatData = this.concatenateArrays(chunks);
|
|
361
|
+
// Decompress data
|
|
362
|
+
const decompressed = await this.inflate(idatData);
|
|
363
|
+
// Unfilter and convert to RGBA
|
|
364
|
+
const rgba = this.unfilterAndConvert(decompressed, width, height, bitDepth, colorType);
|
|
365
|
+
return rgba;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.APNGFormat = APNGFormat;
|
|
@@ -26,11 +26,5 @@ export declare class BMPFormat implements ImageFormat {
|
|
|
26
26
|
* @returns Encoded BMP image bytes
|
|
27
27
|
*/
|
|
28
28
|
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
29
|
-
private readUint16LE;
|
|
30
|
-
private readUint32LE;
|
|
31
|
-
private readInt32LE;
|
|
32
|
-
private writeUint16LE;
|
|
33
|
-
private writeUint32LE;
|
|
34
|
-
private writeInt32LE;
|
|
35
29
|
}
|
|
36
30
|
//# sourceMappingURL=bmp.d.ts.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BMPFormat = void 0;
|
|
4
4
|
const security_js_1 = require("../utils/security.js");
|
|
5
|
+
const byte_utils_js_1 = require("../utils/byte_utils.js");
|
|
5
6
|
// Constants for unit conversions
|
|
6
7
|
const INCHES_PER_METER = 39.3701;
|
|
7
8
|
/**
|
|
@@ -45,10 +46,10 @@ class BMPFormat {
|
|
|
45
46
|
throw new Error("Invalid BMP signature");
|
|
46
47
|
}
|
|
47
48
|
// Read BMP file header (14 bytes)
|
|
48
|
-
const _fileSize =
|
|
49
|
-
const dataOffset =
|
|
49
|
+
const _fileSize = (0, byte_utils_js_1.readUint32LE)(data, 2);
|
|
50
|
+
const dataOffset = (0, byte_utils_js_1.readUint32LE)(data, 10);
|
|
50
51
|
// Read DIB header (at least 40 bytes for BITMAPINFOHEADER)
|
|
51
|
-
const dibHeaderSize =
|
|
52
|
+
const dibHeaderSize = (0, byte_utils_js_1.readUint32LE)(data, 14);
|
|
52
53
|
let width;
|
|
53
54
|
let height;
|
|
54
55
|
let bitDepth;
|
|
@@ -56,13 +57,13 @@ class BMPFormat {
|
|
|
56
57
|
const metadata = {};
|
|
57
58
|
if (dibHeaderSize >= 40) {
|
|
58
59
|
// BITMAPINFOHEADER or later
|
|
59
|
-
width =
|
|
60
|
-
height =
|
|
61
|
-
bitDepth =
|
|
62
|
-
compression =
|
|
60
|
+
width = (0, byte_utils_js_1.readInt32LE)(data, 18);
|
|
61
|
+
height = (0, byte_utils_js_1.readInt32LE)(data, 22);
|
|
62
|
+
bitDepth = (0, byte_utils_js_1.readUint16LE)(data, 28);
|
|
63
|
+
compression = (0, byte_utils_js_1.readUint32LE)(data, 30);
|
|
63
64
|
// Read DPI information (pixels per meter)
|
|
64
|
-
const xPixelsPerMeter =
|
|
65
|
-
const yPixelsPerMeter =
|
|
65
|
+
const xPixelsPerMeter = (0, byte_utils_js_1.readInt32LE)(data, 38);
|
|
66
|
+
const yPixelsPerMeter = (0, byte_utils_js_1.readInt32LE)(data, 42);
|
|
66
67
|
if (xPixelsPerMeter > 0 && yPixelsPerMeter > 0) {
|
|
67
68
|
// Convert pixels per meter to DPI
|
|
68
69
|
metadata.dpiX = Math.round(xPixelsPerMeter / INCHES_PER_METER);
|
|
@@ -137,21 +138,21 @@ class BMPFormat {
|
|
|
137
138
|
// BMP File Header (14 bytes)
|
|
138
139
|
result[0] = 0x42; // 'B'
|
|
139
140
|
result[1] = 0x4d; // 'M'
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 2, fileSize); // File size
|
|
142
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 6, 0); // Reserved
|
|
143
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 10, 54); // Offset to pixel data (14 + 40)
|
|
143
144
|
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 14, 40); // DIB header size
|
|
146
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 18, width); // Width
|
|
147
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 22, height); // Height (positive = bottom-up)
|
|
148
|
+
(0, byte_utils_js_1.writeUint16LE)(result, 26, 1); // Planes
|
|
149
|
+
(0, byte_utils_js_1.writeUint16LE)(result, 28, 32); // Bits per pixel
|
|
150
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 30, 0); // Compression (0 = uncompressed)
|
|
151
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 34, pixelDataSize); // Image size
|
|
152
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 38, xPixelsPerMeter); // X pixels per meter
|
|
153
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 42, yPixelsPerMeter); // Y pixels per meter
|
|
154
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 46, 0); // Colors in palette
|
|
155
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 50, 0); // Important colors
|
|
155
156
|
// Write pixel data (bottom-to-top, BGR(A) format)
|
|
156
157
|
let offset = 54;
|
|
157
158
|
for (let y = height - 1; y >= 0; y--) {
|
|
@@ -170,29 +171,5 @@ class BMPFormat {
|
|
|
170
171
|
}
|
|
171
172
|
return Promise.resolve(result);
|
|
172
173
|
}
|
|
173
|
-
readUint16LE(data, offset) {
|
|
174
|
-
return data[offset] | (data[offset + 1] << 8);
|
|
175
|
-
}
|
|
176
|
-
readUint32LE(data, offset) {
|
|
177
|
-
return data[offset] | (data[offset + 1] << 8) |
|
|
178
|
-
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
179
|
-
}
|
|
180
|
-
readInt32LE(data, offset) {
|
|
181
|
-
const value = this.readUint32LE(data, offset);
|
|
182
|
-
return value > 0x7fffffff ? value - 0x100000000 : value;
|
|
183
|
-
}
|
|
184
|
-
writeUint16LE(data, offset, value) {
|
|
185
|
-
data[offset] = value & 0xff;
|
|
186
|
-
data[offset + 1] = (value >>> 8) & 0xff;
|
|
187
|
-
}
|
|
188
|
-
writeUint32LE(data, offset, value) {
|
|
189
|
-
data[offset] = value & 0xff;
|
|
190
|
-
data[offset + 1] = (value >>> 8) & 0xff;
|
|
191
|
-
data[offset + 2] = (value >>> 16) & 0xff;
|
|
192
|
-
data[offset + 3] = (value >>> 24) & 0xff;
|
|
193
|
-
}
|
|
194
|
-
writeInt32LE(data, offset, value) {
|
|
195
|
-
this.writeUint32LE(data, offset, value < 0 ? value + 0x100000000 : value);
|
|
196
|
-
}
|
|
197
174
|
}
|
|
198
175
|
exports.BMPFormat = BMPFormat;
|
|
@@ -81,7 +81,7 @@ class DNGFormat extends tiff_js_1.TIFFFormat {
|
|
|
81
81
|
* @param imageData Image data to encode
|
|
82
82
|
* @returns Encoded DNG image bytes
|
|
83
83
|
*/
|
|
84
|
-
|
|
84
|
+
encode(imageData) {
|
|
85
85
|
const { width, height, data } = imageData;
|
|
86
86
|
// We'll create a Linear DNG (demosaiced RGB)
|
|
87
87
|
// This is very similar to a standard TIFF but with specific tags.
|
|
@@ -127,7 +127,7 @@ class DNGFormat extends tiff_js_1.TIFFFormat {
|
|
|
127
127
|
// 4. BitsPerSample (0x0102) - 8, 8, 8, 8
|
|
128
128
|
this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
|
|
129
129
|
// Write the actual values later
|
|
130
|
-
const
|
|
130
|
+
const _bitsPerSampleOffset = dataOffset;
|
|
131
131
|
dataOffset += 8; // 4 * 2 bytes
|
|
132
132
|
// 5. Compression (0x0103) - 1 = Uncompressed
|
|
133
133
|
this.writeIFDEntry(result, 0x0103, 3, 1, 1);
|
|
@@ -164,7 +164,7 @@ class DNGFormat extends tiff_js_1.TIFFFormat {
|
|
|
164
164
|
const modelName = "Cross Image DNG\0";
|
|
165
165
|
const modelNameBytes = new TextEncoder().encode(modelName);
|
|
166
166
|
this.writeIFDEntry(result, 50708, 2, modelNameBytes.length, dataOffset);
|
|
167
|
-
const
|
|
167
|
+
const _modelNameOffset = dataOffset;
|
|
168
168
|
dataOffset += modelNameBytes.length;
|
|
169
169
|
// Next IFD offset (0)
|
|
170
170
|
this.writeUint32LE(result, 0);
|
|
@@ -189,7 +189,7 @@ class DNGFormat extends tiff_js_1.TIFFFormat {
|
|
|
189
189
|
for (let i = 0; i < modelNameBytes.length; i++) {
|
|
190
190
|
result.push(modelNameBytes[i]);
|
|
191
191
|
}
|
|
192
|
-
return new Uint8Array(result);
|
|
192
|
+
return Promise.resolve(new Uint8Array(result));
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
exports.DNGFormat = DNGFormat;
|
|
@@ -46,11 +46,9 @@ export declare class GIFFormat implements ImageFormat {
|
|
|
46
46
|
decodeFrames(data: Uint8Array): Promise<MultiFrameImageData>;
|
|
47
47
|
/**
|
|
48
48
|
* Encode multi-frame image data to animated GIF
|
|
49
|
-
* Note: Currently not implemented, will encode only first frame
|
|
50
49
|
*/
|
|
51
50
|
encodeFrames(imageData: MultiFrameImageData, _options?: unknown): Promise<Uint8Array>;
|
|
52
51
|
private mapDisposalMethod;
|
|
53
|
-
private readUint16LE;
|
|
54
52
|
private decodeUsingRuntime;
|
|
55
53
|
private readDataSubBlocks;
|
|
56
54
|
private parseComment;
|
|
@@ -4,6 +4,7 @@ exports.GIFFormat = void 0;
|
|
|
4
4
|
const gif_decoder_js_1 = require("../utils/gif_decoder.js");
|
|
5
5
|
const gif_encoder_js_1 = require("../utils/gif_encoder.js");
|
|
6
6
|
const security_js_1 = require("../utils/security.js");
|
|
7
|
+
const byte_utils_js_1 = require("../utils/byte_utils.js");
|
|
7
8
|
/**
|
|
8
9
|
* GIF format handler
|
|
9
10
|
* Now includes pure-JS implementation with custom LZW compression/decompression
|
|
@@ -81,9 +82,9 @@ class GIFFormat {
|
|
|
81
82
|
// Fall back to runtime decoder if pure-JS fails
|
|
82
83
|
console.warn("Pure-JS GIF decoder failed, falling back to runtime:", error);
|
|
83
84
|
let pos = 6; // Skip "GIF89a" or "GIF87a"
|
|
84
|
-
const width =
|
|
85
|
+
const width = (0, byte_utils_js_1.readUint16LE)(data, pos);
|
|
85
86
|
pos += 2;
|
|
86
|
-
const height =
|
|
87
|
+
const height = (0, byte_utils_js_1.readUint16LE)(data, pos);
|
|
87
88
|
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
88
89
|
(0, security_js_1.validateImageDimensions)(width, height);
|
|
89
90
|
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
@@ -234,22 +235,18 @@ class GIFFormat {
|
|
|
234
235
|
}
|
|
235
236
|
/**
|
|
236
237
|
* Encode multi-frame image data to animated GIF
|
|
237
|
-
* Note: Currently not implemented, will encode only first frame
|
|
238
238
|
*/
|
|
239
239
|
encodeFrames(imageData, _options) {
|
|
240
|
-
// For now, just encode the first frame using the existing encoder
|
|
241
|
-
// Full multi-frame encoding would require a more complex GIFEncoder
|
|
242
240
|
if (imageData.frames.length === 0) {
|
|
243
241
|
throw new Error("No frames to encode");
|
|
244
242
|
}
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return this.encode(singleFrameData);
|
|
243
|
+
const encoder = new gif_encoder_js_1.GIFEncoder(imageData.width, imageData.height);
|
|
244
|
+
for (const frame of imageData.frames) {
|
|
245
|
+
// Get delay from metadata (default to 100ms if not set)
|
|
246
|
+
const delay = frame.frameMetadata?.delay ?? 100;
|
|
247
|
+
encoder.addFrame(frame.data, delay);
|
|
248
|
+
}
|
|
249
|
+
return Promise.resolve(encoder.encode());
|
|
253
250
|
}
|
|
254
251
|
mapDisposalMethod(disposal) {
|
|
255
252
|
switch (disposal) {
|
|
@@ -264,9 +261,6 @@ class GIFFormat {
|
|
|
264
261
|
return "none";
|
|
265
262
|
}
|
|
266
263
|
}
|
|
267
|
-
readUint16LE(data, offset) {
|
|
268
|
-
return data[offset] | (data[offset + 1] << 8);
|
|
269
|
-
}
|
|
270
264
|
async decodeUsingRuntime(data, _width, _height) {
|
|
271
265
|
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
272
266
|
if (typeof ImageDecoder !== "undefined") {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* ICO format handler
|
|
4
|
+
* Implements a pure JavaScript ICO (Windows Icon) decoder and encoder
|
|
5
|
+
*
|
|
6
|
+
* ICO files can contain multiple images at different sizes.
|
|
7
|
+
* This implementation decodes the largest image and encodes as a single-image ICO.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ICOFormat implements ImageFormat {
|
|
10
|
+
/** Format name identifier */
|
|
11
|
+
readonly name = "ico";
|
|
12
|
+
/** MIME type for ICO images */
|
|
13
|
+
readonly mimeType = "image/x-icon";
|
|
14
|
+
private pngFormat;
|
|
15
|
+
/**
|
|
16
|
+
* Check if the given data is an ICO image
|
|
17
|
+
* @param data Raw image data to check
|
|
18
|
+
* @returns true if data has ICO/CUR signature
|
|
19
|
+
*/
|
|
20
|
+
canDecode(data: Uint8Array): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Decode ICO image data to RGBA
|
|
23
|
+
* Selects and decodes the largest image in the ICO file
|
|
24
|
+
* @param data Raw ICO image data
|
|
25
|
+
* @returns Decoded image data with RGBA pixels
|
|
26
|
+
*/
|
|
27
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
28
|
+
/**
|
|
29
|
+
* Decode a DIB (Device Independent Bitmap) format
|
|
30
|
+
* This is a BMP without the 14-byte file header
|
|
31
|
+
*/
|
|
32
|
+
private decodeDIB;
|
|
33
|
+
/**
|
|
34
|
+
* Encode RGBA image data to ICO format
|
|
35
|
+
* Creates an ICO file with a single PNG-encoded image
|
|
36
|
+
* @param imageData Image data to encode
|
|
37
|
+
* @returns Encoded ICO image bytes
|
|
38
|
+
*/
|
|
39
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=ico.d.ts.map
|