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