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.
Files changed (48) hide show
  1. package/README.md +22 -16
  2. package/esm/mod.d.ts +3 -1
  3. package/esm/mod.js +3 -1
  4. package/esm/src/formats/apng.d.ts +50 -0
  5. package/esm/src/formats/apng.js +364 -0
  6. package/esm/src/formats/bmp.d.ts +0 -6
  7. package/esm/src/formats/bmp.js +24 -47
  8. package/esm/src/formats/dng.js +4 -4
  9. package/esm/src/formats/gif.d.ts +0 -2
  10. package/esm/src/formats/gif.js +10 -16
  11. package/esm/src/formats/ico.d.ts +41 -0
  12. package/esm/src/formats/ico.js +214 -0
  13. package/esm/src/formats/pcx.js +1 -1
  14. package/esm/src/formats/png.d.ts +2 -21
  15. package/esm/src/formats/png.js +5 -429
  16. package/esm/src/formats/png_base.d.ts +108 -0
  17. package/esm/src/formats/png_base.js +487 -0
  18. package/esm/src/formats/webp.d.ts +0 -1
  19. package/esm/src/formats/webp.js +4 -7
  20. package/esm/src/image.js +4 -0
  21. package/esm/src/utils/byte_utils.d.ts +30 -0
  22. package/esm/src/utils/byte_utils.js +50 -0
  23. package/esm/src/utils/gif_encoder.d.ts +3 -2
  24. package/esm/src/utils/gif_encoder.js +115 -48
  25. package/package.json +1 -1
  26. package/script/mod.d.ts +3 -1
  27. package/script/mod.js +6 -2
  28. package/script/src/formats/apng.d.ts +50 -0
  29. package/script/src/formats/apng.js +368 -0
  30. package/script/src/formats/bmp.d.ts +0 -6
  31. package/script/src/formats/bmp.js +24 -47
  32. package/script/src/formats/dng.js +4 -4
  33. package/script/src/formats/gif.d.ts +0 -2
  34. package/script/src/formats/gif.js +10 -16
  35. package/script/src/formats/ico.d.ts +41 -0
  36. package/script/src/formats/ico.js +218 -0
  37. package/script/src/formats/pcx.js +1 -1
  38. package/script/src/formats/png.d.ts +2 -21
  39. package/script/src/formats/png.js +5 -429
  40. package/script/src/formats/png_base.d.ts +108 -0
  41. package/script/src/formats/png_base.js +491 -0
  42. package/script/src/formats/webp.d.ts +0 -1
  43. package/script/src/formats/webp.js +4 -7
  44. package/script/src/image.js +4 -0
  45. package/script/src/utils/byte_utils.d.ts +30 -0
  46. package/script/src/utils/byte_utils.js +58 -0
  47. package/script/src/utils/gif_encoder.d.ts +3 -2
  48. 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 = this.readUint32LE(data, 2);
49
- const dataOffset = this.readUint32LE(data, 10);
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 = this.readUint32LE(data, 14);
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 = this.readInt32LE(data, 18);
60
- height = this.readInt32LE(data, 22);
61
- bitDepth = this.readUint16LE(data, 28);
62
- compression = this.readUint32LE(data, 30);
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 = this.readInt32LE(data, 38);
65
- const yPixelsPerMeter = this.readInt32LE(data, 42);
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
- this.writeUint32LE(result, 2, fileSize); // File size
141
- this.writeUint32LE(result, 6, 0); // Reserved
142
- this.writeUint32LE(result, 10, 54); // Offset to pixel data (14 + 40)
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
- this.writeUint32LE(result, 14, 40); // DIB header size
145
- this.writeInt32LE(result, 18, width); // Width
146
- this.writeInt32LE(result, 22, height); // Height (positive = bottom-up)
147
- this.writeUint16LE(result, 26, 1); // Planes
148
- this.writeUint16LE(result, 28, 32); // Bits per pixel
149
- this.writeUint32LE(result, 30, 0); // Compression (0 = uncompressed)
150
- this.writeUint32LE(result, 34, pixelDataSize); // Image size
151
- this.writeInt32LE(result, 38, xPixelsPerMeter); // X pixels per meter
152
- this.writeInt32LE(result, 42, yPixelsPerMeter); // Y pixels per meter
153
- this.writeUint32LE(result, 46, 0); // Colors in palette
154
- this.writeUint32LE(result, 50, 0); // Important colors
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
- async encode(imageData) {
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 bitsPerSampleOffset = dataOffset;
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 modelNameOffset = dataOffset;
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 = this.readUint16LE(data, pos);
85
+ const width = (0, byte_utils_js_1.readUint16LE)(data, pos);
85
86
  pos += 2;
86
- const height = this.readUint16LE(data, pos);
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 firstFrame = imageData.frames[0];
246
- const singleFrameData = {
247
- width: firstFrame.width,
248
- height: firstFrame.height,
249
- data: firstFrame.data,
250
- metadata: imageData.metadata,
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