cross-image 0.2.3 → 0.4.0

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