cross-image 0.2.3 → 0.2.4

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 (72) hide show
  1. package/README.md +333 -289
  2. package/esm/mod.d.ts +2 -0
  3. package/esm/mod.js +2 -0
  4. package/esm/src/formats/apng.d.ts +13 -1
  5. package/esm/src/formats/apng.js +97 -0
  6. package/esm/src/formats/ascii.d.ts +11 -1
  7. package/esm/src/formats/ascii.js +24 -0
  8. package/esm/src/formats/avif.d.ts +96 -0
  9. package/esm/src/formats/avif.js +607 -0
  10. package/esm/src/formats/bmp.d.ts +11 -1
  11. package/esm/src/formats/bmp.js +73 -0
  12. package/esm/src/formats/dng.d.ts +13 -1
  13. package/esm/src/formats/dng.js +26 -4
  14. package/esm/src/formats/gif.d.ts +15 -2
  15. package/esm/src/formats/gif.js +146 -4
  16. package/esm/src/formats/heic.d.ts +96 -0
  17. package/esm/src/formats/heic.js +608 -0
  18. package/esm/src/formats/ico.d.ts +11 -1
  19. package/esm/src/formats/ico.js +28 -0
  20. package/esm/src/formats/jpeg.d.ts +7 -0
  21. package/esm/src/formats/jpeg.js +76 -0
  22. package/esm/src/formats/pam.d.ts +11 -1
  23. package/esm/src/formats/pam.js +66 -0
  24. package/esm/src/formats/pcx.d.ts +11 -1
  25. package/esm/src/formats/pcx.js +45 -0
  26. package/esm/src/formats/png.d.ts +13 -1
  27. package/esm/src/formats/png.js +87 -0
  28. package/esm/src/formats/ppm.d.ts +11 -1
  29. package/esm/src/formats/ppm.js +34 -0
  30. package/esm/src/formats/tiff.d.ts +7 -0
  31. package/esm/src/formats/tiff.js +134 -0
  32. package/esm/src/formats/webp.d.ts +7 -0
  33. package/esm/src/formats/webp.js +92 -0
  34. package/esm/src/image.d.ts +9 -0
  35. package/esm/src/image.js +28 -0
  36. package/esm/src/types.d.ts +18 -0
  37. package/package.json +18 -1
  38. package/script/mod.d.ts +2 -0
  39. package/script/mod.js +5 -1
  40. package/script/src/formats/apng.d.ts +13 -1
  41. package/script/src/formats/apng.js +97 -0
  42. package/script/src/formats/ascii.d.ts +11 -1
  43. package/script/src/formats/ascii.js +24 -0
  44. package/script/src/formats/avif.d.ts +96 -0
  45. package/script/src/formats/avif.js +611 -0
  46. package/script/src/formats/bmp.d.ts +11 -1
  47. package/script/src/formats/bmp.js +73 -0
  48. package/script/src/formats/dng.d.ts +13 -1
  49. package/script/src/formats/dng.js +26 -4
  50. package/script/src/formats/gif.d.ts +15 -2
  51. package/script/src/formats/gif.js +146 -4
  52. package/script/src/formats/heic.d.ts +96 -0
  53. package/script/src/formats/heic.js +612 -0
  54. package/script/src/formats/ico.d.ts +11 -1
  55. package/script/src/formats/ico.js +28 -0
  56. package/script/src/formats/jpeg.d.ts +7 -0
  57. package/script/src/formats/jpeg.js +76 -0
  58. package/script/src/formats/pam.d.ts +11 -1
  59. package/script/src/formats/pam.js +66 -0
  60. package/script/src/formats/pcx.d.ts +11 -1
  61. package/script/src/formats/pcx.js +45 -0
  62. package/script/src/formats/png.d.ts +13 -1
  63. package/script/src/formats/png.js +87 -0
  64. package/script/src/formats/ppm.d.ts +11 -1
  65. package/script/src/formats/ppm.js +34 -0
  66. package/script/src/formats/tiff.d.ts +7 -0
  67. package/script/src/formats/tiff.js +134 -0
  68. package/script/src/formats/webp.d.ts +7 -0
  69. package/script/src/formats/webp.js +92 -0
  70. package/script/src/image.d.ts +9 -0
  71. package/script/src/image.js +28 -0
  72. package/script/src/types.d.ts +18 -0
@@ -0,0 +1,608 @@
1
+ import { validateImageDimensions } from "../utils/security.js";
2
+ /**
3
+ * HEIC format handler
4
+ * Supports HEIC/HEIF images using runtime APIs (ImageDecoder/OffscreenCanvas)
5
+ * Note: Pure JavaScript encode/decode is not supported due to complexity
6
+ */
7
+ export class HEICFormat {
8
+ constructor() {
9
+ /** Format name identifier */
10
+ Object.defineProperty(this, "name", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: "heic"
15
+ });
16
+ /** MIME type for HEIC images */
17
+ Object.defineProperty(this, "mimeType", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: "image/heic"
22
+ });
23
+ }
24
+ /**
25
+ * Check if the given data is a HEIC/HEIF image
26
+ * @param data Raw image data to check
27
+ * @returns true if data has HEIC/HEIF signature
28
+ */
29
+ canDecode(data) {
30
+ // HEIC/HEIF files are ISO Base Media File Format (ISOBMFF) containers
31
+ // They start with ftyp box which contains brand identifiers
32
+ if (data.length < 12)
33
+ return false;
34
+ // Check for ftyp box at the start
35
+ // Bytes 4-7 should be "ftyp"
36
+ if (data[4] === 0x66 && data[5] === 0x74 && // "ft"
37
+ data[6] === 0x79 && data[7] === 0x70 // "yp"
38
+ ) {
39
+ // Check for HEIC/HEIF brand identifiers
40
+ // Common brands: heic, heix, hevc, hevx, mif1, msf1
41
+ const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
42
+ return brand === "heic" || brand === "heix" || brand === "hevc" ||
43
+ brand === "hevx" || brand === "mif1" || brand === "msf1";
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Decode HEIC image data to RGBA
49
+ * Uses runtime APIs (ImageDecoder) for decoding
50
+ * @param data Raw HEIC image data
51
+ * @returns Decoded image data with RGBA pixels
52
+ */
53
+ async decode(data) {
54
+ if (!this.canDecode(data)) {
55
+ throw new Error("Invalid HEIC signature");
56
+ }
57
+ // Extract metadata before decoding pixels
58
+ const metadata = await this.extractMetadata(data);
59
+ // Use runtime decoder
60
+ const { width, height, rgba } = await this.decodeUsingRuntime(data);
61
+ // Validate dimensions for security
62
+ validateImageDimensions(width, height);
63
+ return {
64
+ width,
65
+ height,
66
+ data: rgba,
67
+ metadata,
68
+ };
69
+ }
70
+ /**
71
+ * Encode RGBA image data to HEIC format
72
+ * Uses runtime APIs (OffscreenCanvas) for encoding
73
+ *
74
+ * Note: Metadata injection is not currently implemented. Metadata may be lost during encoding
75
+ * as it would require parsing and modifying the ISOBMFF container structure.
76
+ *
77
+ * @param imageData Image data to encode
78
+ * @returns Encoded HEIC image bytes
79
+ */
80
+ async encode(imageData) {
81
+ const { width, height, data, metadata } = imageData;
82
+ // Try to use runtime encoding if available
83
+ if (typeof OffscreenCanvas !== "undefined") {
84
+ try {
85
+ const canvas = new OffscreenCanvas(width, height);
86
+ const ctx = canvas.getContext("2d");
87
+ if (ctx) {
88
+ const imgData = ctx.createImageData(width, height);
89
+ const imgDataData = new Uint8ClampedArray(data);
90
+ imgData.data.set(imgDataData);
91
+ ctx.putImageData(imgData, 0, 0);
92
+ // Try to encode as HEIC
93
+ const blob = await canvas.convertToBlob({
94
+ type: "image/heic",
95
+ });
96
+ const arrayBuffer = await blob.arrayBuffer();
97
+ const encoded = new Uint8Array(arrayBuffer);
98
+ // Note: Metadata injection for HEIC is complex and would require
99
+ // parsing and modifying the ISOBMFF container structure
100
+ // For now, we rely on the runtime encoder to preserve metadata
101
+ // if it was passed through the canvas
102
+ if (metadata) {
103
+ // Future enhancement: inject metadata into HEIC container
104
+ console.warn("HEIC metadata injection not yet implemented, metadata may be lost");
105
+ }
106
+ return encoded;
107
+ }
108
+ }
109
+ catch (error) {
110
+ throw new Error(`HEIC encoding failed: ${error}`);
111
+ }
112
+ }
113
+ throw new Error("HEIC encoding requires OffscreenCanvas API (not available in this runtime)");
114
+ }
115
+ /**
116
+ * Decode using runtime APIs
117
+ * @param data Raw HEIC data
118
+ * @returns Decoded image dimensions and pixel data
119
+ */
120
+ async decodeUsingRuntime(data) {
121
+ // Try to use ImageDecoder API if available
122
+ if (typeof ImageDecoder !== "undefined") {
123
+ try {
124
+ const decoder = new ImageDecoder({ data, type: "image/heic" });
125
+ const result = await decoder.decode();
126
+ const bitmap = result.image;
127
+ // Create a canvas to extract pixel data
128
+ const canvas = new OffscreenCanvas(bitmap.displayWidth, bitmap.displayHeight);
129
+ const ctx = canvas.getContext("2d");
130
+ if (!ctx)
131
+ throw new Error("Could not get canvas context");
132
+ ctx.drawImage(bitmap, 0, 0);
133
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
134
+ bitmap.close();
135
+ return {
136
+ width: canvas.width,
137
+ height: canvas.height,
138
+ rgba: new Uint8Array(imageData.data.buffer),
139
+ };
140
+ }
141
+ catch (error) {
142
+ throw new Error(`HEIC decoding with ImageDecoder failed: ${error}`);
143
+ }
144
+ }
145
+ throw new Error("HEIC decoding requires ImageDecoder API (not available in this runtime)");
146
+ }
147
+ /**
148
+ * Parse EXIF metadata from HEIC data
149
+ *
150
+ * Note: This is a simplified implementation that searches for EXIF headers linearly.
151
+ * A full implementation would require navigating the ISOBMFF box structure to find
152
+ * the 'meta' box and then the 'Exif' item. This simplified approach may not work
153
+ * in all cases but is suitable for basic metadata extraction when runtime APIs are
154
+ * not available or as a fallback.
155
+ *
156
+ * @param data Raw HEIC data
157
+ * @param metadata Metadata object to populate
158
+ */
159
+ parseEXIF(data, metadata) {
160
+ // For now, we'll attempt a simple search for EXIF header
161
+ // This is a simplified approach and may not work in all cases
162
+ try {
163
+ // Look for Exif header
164
+ for (let i = 0; i < data.length - 6; i++) {
165
+ if (data[i] === 0x45 && data[i + 1] === 0x78 && // "Ex"
166
+ data[i + 2] === 0x69 && data[i + 3] === 0x66 && // "if"
167
+ data[i + 4] === 0x00 && data[i + 5] === 0x00 // padding
168
+ ) {
169
+ // Found EXIF header, parse TIFF structure
170
+ const exifData = data.slice(i + 6);
171
+ this.parseTIFFExif(exifData, metadata);
172
+ break;
173
+ }
174
+ }
175
+ }
176
+ catch (_e) {
177
+ // Ignore EXIF parsing errors
178
+ }
179
+ }
180
+ /**
181
+ * Parse TIFF-formatted EXIF data
182
+ * @param data EXIF data in TIFF format
183
+ * @param metadata Metadata object to populate
184
+ */
185
+ parseTIFFExif(data, metadata) {
186
+ if (data.length < 8)
187
+ return;
188
+ try {
189
+ const byteOrder = String.fromCharCode(data[0], data[1]);
190
+ const littleEndian = byteOrder === "II";
191
+ const ifd0Offset = littleEndian
192
+ ? data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24)
193
+ : (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
194
+ if (ifd0Offset + 2 > data.length)
195
+ return;
196
+ const numEntries = littleEndian
197
+ ? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
198
+ : (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
199
+ let gpsIfdOffset = 0;
200
+ // Parse IFD0 entries
201
+ for (let i = 0; i < numEntries && i < 100; i++) {
202
+ const entryOffset = ifd0Offset + 2 + i * 12;
203
+ if (entryOffset + 12 > data.length)
204
+ break;
205
+ const tag = littleEndian
206
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
207
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
208
+ const type = littleEndian
209
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
210
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
211
+ // DateTime (0x0132)
212
+ if (tag === 0x0132) {
213
+ const valueOffset = littleEndian
214
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
215
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
216
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
217
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
218
+ if (valueOffset < data.length) {
219
+ const endIndex = data.indexOf(0, valueOffset);
220
+ if (endIndex > valueOffset) {
221
+ const dateStr = new TextDecoder().decode(data.slice(valueOffset, endIndex));
222
+ const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
223
+ if (match) {
224
+ metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
225
+ }
226
+ }
227
+ }
228
+ }
229
+ // ImageDescription (0x010E)
230
+ if (tag === 0x010e) {
231
+ const valueOffset = littleEndian
232
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
233
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
234
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
235
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
236
+ if (valueOffset < data.length) {
237
+ const endIndex = data.indexOf(0, valueOffset);
238
+ if (endIndex > valueOffset) {
239
+ metadata.description = new TextDecoder().decode(data.slice(valueOffset, endIndex));
240
+ }
241
+ }
242
+ }
243
+ // Artist (0x013B)
244
+ if (tag === 0x013b) {
245
+ const valueOffset = littleEndian
246
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
247
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
248
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
249
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
250
+ if (valueOffset < data.length) {
251
+ const endIndex = data.indexOf(0, valueOffset);
252
+ if (endIndex > valueOffset) {
253
+ metadata.author = new TextDecoder().decode(data.slice(valueOffset, endIndex));
254
+ }
255
+ }
256
+ }
257
+ // Copyright (0x8298)
258
+ if (tag === 0x8298) {
259
+ const valueOffset = littleEndian
260
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
261
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
262
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
263
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
264
+ if (valueOffset < data.length) {
265
+ const endIndex = data.indexOf(0, valueOffset);
266
+ if (endIndex > valueOffset) {
267
+ metadata.copyright = new TextDecoder().decode(data.slice(valueOffset, endIndex));
268
+ }
269
+ }
270
+ }
271
+ // Make (0x010F)
272
+ if (tag === 0x010f) {
273
+ const valueOffset = littleEndian
274
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
275
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
276
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
277
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
278
+ if (valueOffset < data.length) {
279
+ const endIndex = data.indexOf(0, valueOffset);
280
+ if (endIndex > valueOffset) {
281
+ metadata.cameraMake = new TextDecoder().decode(data.slice(valueOffset, endIndex));
282
+ }
283
+ }
284
+ }
285
+ // Model (0x0110)
286
+ if (tag === 0x0110) {
287
+ const valueOffset = littleEndian
288
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
289
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
290
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
291
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
292
+ if (valueOffset < data.length) {
293
+ const endIndex = data.indexOf(0, valueOffset);
294
+ if (endIndex > valueOffset) {
295
+ metadata.cameraModel = new TextDecoder().decode(data.slice(valueOffset, endIndex));
296
+ }
297
+ }
298
+ }
299
+ // Orientation (0x0112)
300
+ if (tag === 0x0112) {
301
+ const value = littleEndian
302
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
303
+ : (data[entryOffset + 8] << 8) | data[entryOffset + 9];
304
+ metadata.orientation = value;
305
+ }
306
+ // Software (0x0131)
307
+ if (tag === 0x0131) {
308
+ const valueOffset = littleEndian
309
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
310
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
311
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
312
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
313
+ if (valueOffset < data.length) {
314
+ const endIndex = data.indexOf(0, valueOffset);
315
+ if (endIndex > valueOffset) {
316
+ metadata.software = new TextDecoder().decode(data.slice(valueOffset, endIndex));
317
+ }
318
+ }
319
+ }
320
+ // GPS IFD Pointer (0x8825)
321
+ if (tag === 0x8825) {
322
+ gpsIfdOffset = littleEndian
323
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
324
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
325
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
326
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
327
+ }
328
+ // ExifIFD Pointer (0x8769)
329
+ if (tag === 0x8769 && type === 4) {
330
+ const exifIfdOffset = littleEndian
331
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
332
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
333
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
334
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
335
+ if (exifIfdOffset > 0 && exifIfdOffset + 2 <= data.length) {
336
+ this.parseExifSubIFD(data, exifIfdOffset, littleEndian, metadata);
337
+ }
338
+ }
339
+ }
340
+ // Parse GPS IFD if present
341
+ if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
342
+ this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
343
+ }
344
+ }
345
+ catch (_e) {
346
+ // Ignore parsing errors
347
+ }
348
+ }
349
+ /**
350
+ * Parse Exif Sub-IFD for camera settings
351
+ * @param data EXIF data
352
+ * @param exifIfdOffset Offset to Exif Sub-IFD
353
+ * @param littleEndian Byte order
354
+ * @param metadata Metadata object to populate
355
+ */
356
+ parseExifSubIFD(data, exifIfdOffset, littleEndian, metadata) {
357
+ try {
358
+ const numEntries = littleEndian
359
+ ? data[exifIfdOffset] | (data[exifIfdOffset + 1] << 8)
360
+ : (data[exifIfdOffset] << 8) | data[exifIfdOffset + 1];
361
+ for (let i = 0; i < numEntries && i < 100; i++) {
362
+ const entryOffset = exifIfdOffset + 2 + i * 12;
363
+ if (entryOffset + 12 > data.length)
364
+ break;
365
+ const tag = littleEndian
366
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
367
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
368
+ const type = littleEndian
369
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
370
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
371
+ // ExposureTime (0x829A)
372
+ if (tag === 0x829a && type === 5) {
373
+ const valueOffset = littleEndian
374
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
375
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
376
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
377
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
378
+ if (valueOffset + 8 <= data.length) {
379
+ metadata.exposureTime = this.readRational(data, valueOffset, littleEndian);
380
+ }
381
+ }
382
+ // FNumber (0x829D)
383
+ if (tag === 0x829d && type === 5) {
384
+ const valueOffset = littleEndian
385
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
386
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
387
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
388
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
389
+ if (valueOffset + 8 <= data.length) {
390
+ metadata.fNumber = this.readRational(data, valueOffset, littleEndian);
391
+ }
392
+ }
393
+ // ISOSpeedRatings (0x8827)
394
+ if (tag === 0x8827 && type === 3) {
395
+ metadata.iso = littleEndian
396
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
397
+ : (data[entryOffset + 8] << 8) | data[entryOffset + 9];
398
+ }
399
+ // FocalLength (0x920A)
400
+ if (tag === 0x920a && type === 5) {
401
+ const valueOffset = littleEndian
402
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
403
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
404
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
405
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
406
+ if (valueOffset + 8 <= data.length) {
407
+ metadata.focalLength = this.readRational(data, valueOffset, littleEndian);
408
+ }
409
+ }
410
+ // UserComment (0x9286)
411
+ if (tag === 0x9286) {
412
+ const count = littleEndian
413
+ ? data[entryOffset + 4] | (data[entryOffset + 5] << 8) |
414
+ (data[entryOffset + 6] << 16) | (data[entryOffset + 7] << 24)
415
+ : (data[entryOffset + 4] << 24) | (data[entryOffset + 5] << 16) |
416
+ (data[entryOffset + 6] << 8) | data[entryOffset + 7];
417
+ const valueOffset = littleEndian
418
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
419
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
420
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
421
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
422
+ if (valueOffset + count <= data.length && count > 8) {
423
+ const commentData = data.slice(valueOffset + 8, valueOffset + count);
424
+ metadata.userComment = new TextDecoder().decode(commentData)
425
+ .replace(/\0+$/, "");
426
+ }
427
+ }
428
+ // Flash (0x9209)
429
+ if (tag === 0x9209 && type === 3) {
430
+ metadata.flash = littleEndian
431
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
432
+ : (data[entryOffset + 8] << 8) | data[entryOffset + 9];
433
+ }
434
+ // WhiteBalance (0xA403)
435
+ if (tag === 0xa403 && type === 3) {
436
+ metadata.whiteBalance = littleEndian
437
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
438
+ : (data[entryOffset + 8] << 8) | data[entryOffset + 9];
439
+ }
440
+ // LensMake (0xA433)
441
+ if (tag === 0xa433 && type === 2) {
442
+ const valueOffset = littleEndian
443
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
444
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
445
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
446
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
447
+ if (valueOffset < data.length) {
448
+ const endIndex = data.indexOf(0, valueOffset);
449
+ if (endIndex > valueOffset) {
450
+ metadata.lensMake = new TextDecoder().decode(data.slice(valueOffset, endIndex));
451
+ }
452
+ }
453
+ }
454
+ // LensModel (0xA434)
455
+ if (tag === 0xa434 && type === 2) {
456
+ const valueOffset = littleEndian
457
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
458
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
459
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
460
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
461
+ if (valueOffset < data.length) {
462
+ const endIndex = data.indexOf(0, valueOffset);
463
+ if (endIndex > valueOffset) {
464
+ metadata.lensModel = new TextDecoder().decode(data.slice(valueOffset, endIndex));
465
+ }
466
+ }
467
+ }
468
+ }
469
+ }
470
+ catch (_e) {
471
+ // Ignore parsing errors
472
+ }
473
+ }
474
+ /**
475
+ * Parse GPS IFD for location data
476
+ * @param data EXIF data
477
+ * @param gpsIfdOffset Offset to GPS IFD
478
+ * @param littleEndian Byte order
479
+ * @param metadata Metadata object to populate
480
+ */
481
+ parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
482
+ try {
483
+ const numEntries = littleEndian
484
+ ? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
485
+ : (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
486
+ let latRef = "";
487
+ let lonRef = "";
488
+ let latitude;
489
+ let longitude;
490
+ for (let i = 0; i < numEntries && i < 100; i++) {
491
+ const entryOffset = gpsIfdOffset + 2 + i * 12;
492
+ if (entryOffset + 12 > data.length)
493
+ break;
494
+ const tag = littleEndian
495
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
496
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
497
+ const type = littleEndian
498
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
499
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
500
+ // GPSLatitudeRef (0x0001)
501
+ if (tag === 0x0001 && type === 2) {
502
+ latRef = String.fromCharCode(data[entryOffset + 8]);
503
+ }
504
+ // GPSLatitude (0x0002)
505
+ if (tag === 0x0002 && type === 5) {
506
+ const valueOffset = littleEndian
507
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
508
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
509
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
510
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
511
+ if (valueOffset + 24 <= data.length) {
512
+ const degrees = this.readRational(data, valueOffset, littleEndian);
513
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
514
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
515
+ latitude = degrees + minutes / 60 + seconds / 3600;
516
+ }
517
+ }
518
+ // GPSLongitudeRef (0x0003)
519
+ if (tag === 0x0003 && type === 2) {
520
+ lonRef = String.fromCharCode(data[entryOffset + 8]);
521
+ }
522
+ // GPSLongitude (0x0004)
523
+ if (tag === 0x0004 && type === 5) {
524
+ const valueOffset = littleEndian
525
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
526
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
527
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
528
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
529
+ if (valueOffset + 24 <= data.length) {
530
+ const degrees = this.readRational(data, valueOffset, littleEndian);
531
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
532
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
533
+ longitude = degrees + minutes / 60 + seconds / 3600;
534
+ }
535
+ }
536
+ }
537
+ // Apply reference direction
538
+ if (latitude !== undefined) {
539
+ metadata.latitude = latRef === "S" ? -latitude : latitude;
540
+ }
541
+ if (longitude !== undefined) {
542
+ metadata.longitude = lonRef === "W" ? -longitude : longitude;
543
+ }
544
+ }
545
+ catch (_e) {
546
+ // Ignore parsing errors
547
+ }
548
+ }
549
+ /**
550
+ * Read a rational value (numerator/denominator)
551
+ * @param data Data buffer
552
+ * @param offset Offset to rational
553
+ * @param littleEndian Byte order
554
+ * @returns Decimal value
555
+ */
556
+ readRational(data, offset, littleEndian) {
557
+ const numerator = littleEndian
558
+ ? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
559
+ (data[offset + 3] << 24)
560
+ : (data[offset] << 24) | (data[offset + 1] << 16) |
561
+ (data[offset + 2] << 8) | data[offset + 3];
562
+ const denominator = littleEndian
563
+ ? data[offset + 4] | (data[offset + 5] << 8) |
564
+ (data[offset + 6] << 16) | (data[offset + 7] << 24)
565
+ : (data[offset + 4] << 24) | (data[offset + 5] << 16) |
566
+ (data[offset + 6] << 8) | data[offset + 7];
567
+ return denominator !== 0 ? numerator / denominator : 0;
568
+ }
569
+ /**
570
+ * Get the list of metadata fields supported by HEIC format
571
+ */
572
+ getSupportedMetadata() {
573
+ return [
574
+ "creationDate",
575
+ "description",
576
+ "author",
577
+ "copyright",
578
+ "latitude",
579
+ "longitude",
580
+ "cameraMake",
581
+ "cameraModel",
582
+ "iso",
583
+ "exposureTime",
584
+ "fNumber",
585
+ "focalLength",
586
+ "flash",
587
+ "whiteBalance",
588
+ "lensMake",
589
+ "lensModel",
590
+ "orientation",
591
+ "software",
592
+ "userComment",
593
+ ];
594
+ }
595
+ /**
596
+ * Extract metadata from HEIC data without fully decoding the pixel data
597
+ * @param data Raw HEIC data
598
+ * @returns Extracted metadata or undefined
599
+ */
600
+ extractMetadata(data) {
601
+ if (!this.canDecode(data)) {
602
+ return Promise.resolve(undefined);
603
+ }
604
+ const metadata = {};
605
+ this.parseEXIF(data, metadata);
606
+ return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
607
+ }
608
+ }
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageFormat } from "../types.js";
1
+ import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
2
2
  /**
3
3
  * ICO format handler
4
4
  * Implements a pure JavaScript ICO (Windows Icon) decoder and encoder
@@ -37,5 +37,15 @@ export declare class ICOFormat implements ImageFormat {
37
37
  * @returns Encoded ICO image bytes
38
38
  */
39
39
  encode(imageData: ImageData): Promise<Uint8Array>;
40
+ /**
41
+ * Get the list of metadata fields supported by ICO format
42
+ */
43
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
44
+ /**
45
+ * Extract metadata from ICO data without fully decoding the pixel data
46
+ * @param data Raw ICO data
47
+ * @returns Extracted metadata or undefined
48
+ */
49
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
40
50
  }
41
51
  //# sourceMappingURL=ico.d.ts.map
@@ -211,4 +211,32 @@ export class ICOFormat {
211
211
  result.set(pngData, 22);
212
212
  return result;
213
213
  }
214
+ /**
215
+ * Get the list of metadata fields supported by ICO format
216
+ */
217
+ getSupportedMetadata() {
218
+ return [
219
+ "frameCount", // Number of images in ICO file
220
+ ];
221
+ }
222
+ /**
223
+ * Extract metadata from ICO data without fully decoding the pixel data
224
+ * @param data Raw ICO data
225
+ * @returns Extracted metadata or undefined
226
+ */
227
+ extractMetadata(data) {
228
+ if (!this.canDecode(data)) {
229
+ return Promise.resolve(undefined);
230
+ }
231
+ // ICO files can contain multiple images, count them
232
+ const count = readUint16LE(data, 4);
233
+ const metadata = {
234
+ format: "ico",
235
+ compression: "none", // ICO typically contains uncompressed PNG or BMP
236
+ frameCount: count,
237
+ bitDepth: 32, // Most modern ICOs use 32-bit RGBA
238
+ colorType: "rgba",
239
+ };
240
+ return Promise.resolve(metadata);
241
+ }
214
242
  }
@@ -42,5 +42,12 @@ export declare class JPEGFormat implements ImageFormat {
42
42
  * Get the list of metadata fields supported by JPEG format
43
43
  */
44
44
  getSupportedMetadata(): Array<keyof ImageMetadata>;
45
+ /**
46
+ * Extract metadata from JPEG data without fully decoding the pixel data
47
+ * This quickly parses JFIF and EXIF markers to extract metadata
48
+ * @param data Raw JPEG data
49
+ * @returns Extracted metadata or undefined
50
+ */
51
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
45
52
  }
46
53
  //# sourceMappingURL=jpeg.d.ts.map