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