cross-image 0.2.2 → 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 (88) hide show
  1. package/README.md +333 -168
  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 +19 -1
  21. package/esm/src/formats/jpeg.js +709 -4
  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/png_base.d.ts +8 -0
  29. package/esm/src/formats/png_base.js +176 -3
  30. package/esm/src/formats/ppm.d.ts +11 -1
  31. package/esm/src/formats/ppm.js +34 -0
  32. package/esm/src/formats/tiff.d.ts +13 -1
  33. package/esm/src/formats/tiff.js +165 -0
  34. package/esm/src/formats/webp.d.ts +16 -2
  35. package/esm/src/formats/webp.js +303 -62
  36. package/esm/src/image.d.ts +60 -0
  37. package/esm/src/image.js +253 -5
  38. package/esm/src/types.d.ts +59 -1
  39. package/esm/src/utils/image_processing.d.ts +55 -0
  40. package/esm/src/utils/image_processing.js +210 -0
  41. package/esm/src/utils/metadata/xmp.d.ts +52 -0
  42. package/esm/src/utils/metadata/xmp.js +325 -0
  43. package/esm/src/utils/resize.d.ts +4 -0
  44. package/esm/src/utils/resize.js +74 -0
  45. package/package.json +18 -1
  46. package/script/mod.d.ts +2 -0
  47. package/script/mod.js +5 -1
  48. package/script/src/formats/apng.d.ts +13 -1
  49. package/script/src/formats/apng.js +97 -0
  50. package/script/src/formats/ascii.d.ts +11 -1
  51. package/script/src/formats/ascii.js +24 -0
  52. package/script/src/formats/avif.d.ts +96 -0
  53. package/script/src/formats/avif.js +611 -0
  54. package/script/src/formats/bmp.d.ts +11 -1
  55. package/script/src/formats/bmp.js +73 -0
  56. package/script/src/formats/dng.d.ts +13 -1
  57. package/script/src/formats/dng.js +26 -4
  58. package/script/src/formats/gif.d.ts +15 -2
  59. package/script/src/formats/gif.js +146 -4
  60. package/script/src/formats/heic.d.ts +96 -0
  61. package/script/src/formats/heic.js +612 -0
  62. package/script/src/formats/ico.d.ts +11 -1
  63. package/script/src/formats/ico.js +28 -0
  64. package/script/src/formats/jpeg.d.ts +19 -1
  65. package/script/src/formats/jpeg.js +709 -4
  66. package/script/src/formats/pam.d.ts +11 -1
  67. package/script/src/formats/pam.js +66 -0
  68. package/script/src/formats/pcx.d.ts +11 -1
  69. package/script/src/formats/pcx.js +45 -0
  70. package/script/src/formats/png.d.ts +13 -1
  71. package/script/src/formats/png.js +87 -0
  72. package/script/src/formats/png_base.d.ts +8 -0
  73. package/script/src/formats/png_base.js +176 -3
  74. package/script/src/formats/ppm.d.ts +11 -1
  75. package/script/src/formats/ppm.js +34 -0
  76. package/script/src/formats/tiff.d.ts +13 -1
  77. package/script/src/formats/tiff.js +165 -0
  78. package/script/src/formats/webp.d.ts +16 -2
  79. package/script/src/formats/webp.js +303 -62
  80. package/script/src/image.d.ts +60 -0
  81. package/script/src/image.js +251 -3
  82. package/script/src/types.d.ts +59 -1
  83. package/script/src/utils/image_processing.d.ts +55 -0
  84. package/script/src/utils/image_processing.js +216 -0
  85. package/script/src/utils/metadata/xmp.d.ts +52 -0
  86. package/script/src/utils/metadata/xmp.js +333 -0
  87. package/script/src/utils/resize.d.ts +4 -0
  88. package/script/src/utils/resize.js +75 -0
@@ -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
  * PAM format handler
4
4
  * Implements the Netpbm PAM (Portable Arbitrary Map) format.
@@ -39,5 +39,15 @@ export declare class PAMFormat implements ImageFormat {
39
39
  * @returns Encoded PAM image bytes
40
40
  */
41
41
  encode(imageData: ImageData): Promise<Uint8Array>;
42
+ /**
43
+ * Get the list of metadata fields supported by PAM format
44
+ */
45
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
46
+ /**
47
+ * Extract metadata from PAM data without fully decoding the pixel data
48
+ * @param data Raw PAM data
49
+ * @returns Extracted metadata or undefined
50
+ */
51
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
42
52
  }
43
53
  //# sourceMappingURL=pam.d.ts.map
@@ -177,5 +177,71 @@ class PAMFormat {
177
177
  output.set(data, headerBytes.length);
178
178
  return Promise.resolve(output);
179
179
  }
180
+ /**
181
+ * Get the list of metadata fields supported by PAM format
182
+ */
183
+ getSupportedMetadata() {
184
+ return []; // PAM format doesn't support metadata preservation
185
+ }
186
+ /**
187
+ * Extract metadata from PAM data without fully decoding the pixel data
188
+ * @param data Raw PAM data
189
+ * @returns Extracted metadata or undefined
190
+ */
191
+ extractMetadata(data) {
192
+ if (!this.canDecode(data)) {
193
+ return Promise.resolve(undefined);
194
+ }
195
+ const metadata = {
196
+ format: "pam",
197
+ compression: "none",
198
+ frameCount: 1,
199
+ bitDepth: 8, // PAM typically uses 8 bits per channel
200
+ colorType: "rgba",
201
+ };
202
+ // Try to parse the header to get actual color type
203
+ const decoder = new TextDecoder();
204
+ const headerText = decoder.decode(data.slice(0, Math.min(200, data.length)));
205
+ // Look for TUPLTYPE to determine color type
206
+ const tupltypeMatch = headerText.match(/TUPLTYPE\s+(\S+)/);
207
+ if (tupltypeMatch) {
208
+ const tupltype = tupltypeMatch[1];
209
+ if (tupltype === "GRAYSCALE") {
210
+ metadata.colorType = "grayscale";
211
+ }
212
+ else if (tupltype === "RGB") {
213
+ metadata.colorType = "rgb";
214
+ }
215
+ else if (tupltype === "RGB_ALPHA") {
216
+ metadata.colorType = "rgba";
217
+ }
218
+ }
219
+ // Look for DEPTH to determine number of channels
220
+ const depthMatch = headerText.match(/DEPTH\s+(\d+)/);
221
+ if (depthMatch) {
222
+ const depth = parseInt(depthMatch[1]);
223
+ if (depth === 1) {
224
+ metadata.colorType = "grayscale";
225
+ }
226
+ else if (depth === 3) {
227
+ metadata.colorType = "rgb";
228
+ }
229
+ else if (depth === 4) {
230
+ metadata.colorType = "rgba";
231
+ }
232
+ }
233
+ // Look for MAXVAL to determine bit depth
234
+ const maxvalMatch = headerText.match(/MAXVAL\s+(\d+)/);
235
+ if (maxvalMatch) {
236
+ const maxval = parseInt(maxvalMatch[1]);
237
+ if (maxval === 255) {
238
+ metadata.bitDepth = 8;
239
+ }
240
+ else if (maxval === 65535) {
241
+ metadata.bitDepth = 16;
242
+ }
243
+ }
244
+ return Promise.resolve(metadata);
245
+ }
180
246
  }
181
247
  exports.PAMFormat = PAMFormat;
@@ -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
  * PCX format handler
4
4
  * Implements PCX decoder and encoder
@@ -9,5 +9,15 @@ export declare class PCXFormat implements ImageFormat {
9
9
  canDecode(data: Uint8Array): boolean;
10
10
  decode(data: Uint8Array): Promise<ImageData>;
11
11
  encode(image: ImageData): Promise<Uint8Array>;
12
+ /**
13
+ * Get the list of metadata fields supported by PCX format
14
+ */
15
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
16
+ /**
17
+ * Extract metadata from PCX data without fully decoding the pixel data
18
+ * @param data Raw PCX data
19
+ * @returns Extracted metadata or undefined
20
+ */
21
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
12
22
  }
13
23
  //# sourceMappingURL=pcx.d.ts.map
@@ -204,5 +204,50 @@ class PCXFormat {
204
204
  result.set(rleData, header.length);
205
205
  return Promise.resolve(result);
206
206
  }
207
+ /**
208
+ * Get the list of metadata fields supported by PCX format
209
+ */
210
+ getSupportedMetadata() {
211
+ return [
212
+ "dpiX", // DPI in header
213
+ "dpiY", // DPI in header
214
+ ];
215
+ }
216
+ /**
217
+ * Extract metadata from PCX data without fully decoding the pixel data
218
+ * @param data Raw PCX data
219
+ * @returns Extracted metadata or undefined
220
+ */
221
+ extractMetadata(data) {
222
+ if (!this.canDecode(data)) {
223
+ return Promise.resolve(undefined);
224
+ }
225
+ const metadata = {
226
+ format: "pcx",
227
+ compression: "rle",
228
+ frameCount: 1,
229
+ bitDepth: data[3], // Bits per pixel per plane
230
+ colorType: "rgb",
231
+ };
232
+ // Check number of planes to determine color type
233
+ const numPlanes = data[65];
234
+ if (numPlanes === 1) {
235
+ metadata.colorType = "indexed";
236
+ }
237
+ else if (numPlanes === 3) {
238
+ metadata.colorType = "rgb";
239
+ }
240
+ else if (numPlanes === 4) {
241
+ metadata.colorType = "rgba";
242
+ }
243
+ // DPI information
244
+ const dpiX = data[12] | (data[13] << 8);
245
+ const dpiY = data[14] | (data[15] << 8);
246
+ if (dpiX > 0)
247
+ metadata.dpiX = dpiX;
248
+ if (dpiY > 0)
249
+ metadata.dpiY = dpiY;
250
+ return Promise.resolve(metadata);
251
+ }
207
252
  }
208
253
  exports.PCXFormat = PCXFormat;
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageFormat } from "../types.js";
1
+ import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
2
2
  import { PNGBase } from "./png_base.js";
3
3
  /**
4
4
  * PNG format handler
@@ -27,5 +27,17 @@ export declare class PNGFormat extends PNGBase implements ImageFormat {
27
27
  * @returns Encoded PNG image bytes
28
28
  */
29
29
  encode(imageData: ImageData): Promise<Uint8Array>;
30
+ /**
31
+ * Get the list of metadata fields supported by PNG format
32
+ * Delegates to PNGBase implementation
33
+ */
34
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
35
+ /**
36
+ * Extract metadata from PNG data without fully decoding the pixel data
37
+ * This quickly parses PNG chunks to extract metadata
38
+ * @param data Raw PNG data
39
+ * @returns Extracted metadata or undefined
40
+ */
41
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
30
42
  }
31
43
  //# sourceMappingURL=png.d.ts.map
@@ -140,5 +140,92 @@ class PNGFormat extends png_base_js_1.PNGBase {
140
140
  // Concatenate all chunks
141
141
  return this.concatenateArrays(chunks);
142
142
  }
143
+ /**
144
+ * Get the list of metadata fields supported by PNG format
145
+ * Delegates to PNGBase implementation
146
+ */
147
+ getSupportedMetadata() {
148
+ return super.getSupportedMetadata();
149
+ }
150
+ /**
151
+ * Extract metadata from PNG data without fully decoding the pixel data
152
+ * This quickly parses PNG chunks to extract metadata
153
+ * @param data Raw PNG data
154
+ * @returns Extracted metadata or undefined
155
+ */
156
+ extractMetadata(data) {
157
+ if (!this.canDecode(data)) {
158
+ return Promise.resolve(undefined);
159
+ }
160
+ let pos = 8; // Skip PNG signature
161
+ let width = 0;
162
+ let height = 0;
163
+ const metadata = {
164
+ format: "png",
165
+ compression: "deflate",
166
+ frameCount: 1,
167
+ };
168
+ // Parse chunks for metadata only
169
+ while (pos < data.length) {
170
+ if (pos + 8 > data.length)
171
+ break;
172
+ const length = this.readUint32(data, pos);
173
+ pos += 4;
174
+ const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
175
+ pos += 4;
176
+ if (pos + length + 4 > data.length)
177
+ break;
178
+ const chunkData = data.slice(pos, pos + length);
179
+ pos += length;
180
+ pos += 4; // Skip CRC
181
+ if (type === "IHDR") {
182
+ width = this.readUint32(chunkData, 0);
183
+ height = this.readUint32(chunkData, 4);
184
+ // Parse bit depth and color type from IHDR
185
+ if (chunkData.length >= 9) {
186
+ metadata.bitDepth = chunkData[8];
187
+ const colorTypeCode = chunkData[9];
188
+ // PNG color types: 0=grayscale, 2=rgb, 3=indexed, 4=grayscale+alpha, 6=rgba
189
+ switch (colorTypeCode) {
190
+ case 0:
191
+ metadata.colorType = "grayscale";
192
+ break;
193
+ case 2:
194
+ metadata.colorType = "rgb";
195
+ break;
196
+ case 3:
197
+ metadata.colorType = "indexed";
198
+ break;
199
+ case 4:
200
+ metadata.colorType = "grayscale-alpha";
201
+ break;
202
+ case 6:
203
+ metadata.colorType = "rgba";
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ else if (type === "pHYs") {
209
+ // Physical pixel dimensions
210
+ this.parsePhysChunk(chunkData, metadata, width, height);
211
+ }
212
+ else if (type === "tEXt") {
213
+ // Text chunk
214
+ this.parseTextChunk(chunkData, metadata);
215
+ }
216
+ else if (type === "iTXt") {
217
+ // International text chunk
218
+ this.parseITxtChunk(chunkData, metadata);
219
+ }
220
+ else if (type === "eXIf") {
221
+ // EXIF chunk
222
+ this.parseExifChunk(chunkData, metadata);
223
+ }
224
+ else if (type === "IEND") {
225
+ break;
226
+ }
227
+ }
228
+ return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
229
+ }
143
230
  }
144
231
  exports.PNGFormat = PNGFormat;
@@ -76,6 +76,8 @@ export declare abstract class PNGBase {
76
76
  * Parse eXIf (EXIF) chunk
77
77
  */
78
78
  protected parseExifChunk(data: Uint8Array, metadata: ImageMetadata): void;
79
+ protected parseGPSIFD(data: Uint8Array, gpsIfdOffset: number, littleEndian: boolean, metadata: ImageMetadata): void;
80
+ protected readRational(data: Uint8Array, offset: number, littleEndian: boolean): number;
79
81
  /**
80
82
  * Create pHYs (physical pixel dimensions) chunk
81
83
  */
@@ -88,6 +90,8 @@ export declare abstract class PNGBase {
88
90
  * Create eXIf (EXIF) chunk
89
91
  */
90
92
  protected createExifChunk(metadata: ImageMetadata): Uint8Array | null;
93
+ protected createGPSIFD(metadata: ImageMetadata, gpsIfdStart: number): number[];
94
+ protected writeRational(output: number[], numerator: number, denominator: number): void;
91
95
  /**
92
96
  * Concatenate multiple byte arrays into a single Uint8Array
93
97
  */
@@ -104,5 +108,9 @@ export declare abstract class PNGBase {
104
108
  * Shared method to avoid duplication between PNG and APNG encoding
105
109
  */
106
110
  protected addMetadataChunks(chunks: Uint8Array[], metadata: ImageMetadata | undefined): void;
111
+ /**
112
+ * Get the list of metadata fields supported by PNG format
113
+ */
114
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
107
115
  }
108
116
  //# sourceMappingURL=png_base.d.ts.map
@@ -318,6 +318,7 @@ class PNGBase {
318
318
  const numEntries = littleEndian
319
319
  ? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
320
320
  : (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
321
+ let gpsIfdOffset = 0;
321
322
  for (let i = 0; i < numEntries; i++) {
322
323
  const entryOffset = ifd0Offset + 2 + i * 12;
323
324
  if (entryOffset + 12 > data.length)
@@ -342,12 +343,96 @@ class PNGBase {
342
343
  }
343
344
  }
344
345
  }
346
+ // GPS IFD Pointer tag (0x8825)
347
+ if (tag === 0x8825) {
348
+ gpsIfdOffset = littleEndian
349
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
350
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
351
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
352
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
353
+ }
354
+ }
355
+ // Parse GPS IFD if present
356
+ if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
357
+ this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
345
358
  }
346
359
  }
347
360
  catch (_e) {
348
361
  // Ignore EXIF parsing errors
349
362
  }
350
363
  }
364
+ parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
365
+ try {
366
+ const numEntries = littleEndian
367
+ ? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
368
+ : (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
369
+ let latRef = "";
370
+ let lonRef = "";
371
+ let latitude;
372
+ let longitude;
373
+ for (let i = 0; i < numEntries; i++) {
374
+ const entryOffset = gpsIfdOffset + 2 + i * 12;
375
+ if (entryOffset + 12 > data.length)
376
+ break;
377
+ const tag = littleEndian
378
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
379
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
380
+ const type = littleEndian
381
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
382
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
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
+ // GPSLatitudeRef (0x0001)
389
+ if (tag === 0x0001 && type === 2) {
390
+ latRef = String.fromCharCode(data[entryOffset + 8]);
391
+ }
392
+ // GPSLatitude (0x0002)
393
+ if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
394
+ const degrees = this.readRational(data, valueOffset, littleEndian);
395
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
396
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
397
+ latitude = degrees + minutes / 60 + seconds / 3600;
398
+ }
399
+ // GPSLongitudeRef (0x0003)
400
+ if (tag === 0x0003 && type === 2) {
401
+ lonRef = String.fromCharCode(data[entryOffset + 8]);
402
+ }
403
+ // GPSLongitude (0x0004)
404
+ if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
405
+ const degrees = this.readRational(data, valueOffset, littleEndian);
406
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
407
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
408
+ longitude = degrees + minutes / 60 + seconds / 3600;
409
+ }
410
+ }
411
+ // Apply hemisphere references
412
+ if (latitude !== undefined && latRef) {
413
+ metadata.latitude = latRef === "S" ? -latitude : latitude;
414
+ }
415
+ if (longitude !== undefined && lonRef) {
416
+ metadata.longitude = lonRef === "W" ? -longitude : longitude;
417
+ }
418
+ }
419
+ catch (_e) {
420
+ // Ignore GPS parsing errors
421
+ }
422
+ }
423
+ readRational(data, offset, littleEndian) {
424
+ const numerator = littleEndian
425
+ ? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
426
+ (data[offset + 3] << 24)
427
+ : (data[offset] << 24) | (data[offset + 1] << 16) |
428
+ (data[offset + 2] << 8) | data[offset + 3];
429
+ const denominator = littleEndian
430
+ ? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
431
+ (data[offset + 7] << 24)
432
+ : (data[offset + 4] << 24) | (data[offset + 5] << 16) |
433
+ (data[offset + 6] << 8) | data[offset + 7];
434
+ return denominator !== 0 ? numerator / denominator : 0;
435
+ }
351
436
  /**
352
437
  * Create pHYs (physical pixel dimensions) chunk
353
438
  */
@@ -388,14 +473,19 @@ class PNGBase {
388
473
  value: new TextEncoder().encode(dateStr),
389
474
  });
390
475
  }
391
- if (entries.length === 0)
476
+ // Check if we have GPS data
477
+ const hasGPS = metadata.latitude !== undefined &&
478
+ metadata.longitude !== undefined;
479
+ if (entries.length === 0 && !hasGPS)
392
480
  return null;
393
481
  const exif = [];
394
482
  exif.push(0x49, 0x49); // "II"
395
483
  exif.push(0x2a, 0x00); // 42
396
484
  exif.push(0x08, 0x00, 0x00, 0x00);
397
- exif.push(entries.length & 0xff, (entries.length >> 8) & 0xff);
398
- let dataOffset = 8 + 2 + entries.length * 12 + 4;
485
+ // Number of entries (add GPS IFD pointer if we have GPS data)
486
+ const ifd0Entries = entries.length + (hasGPS ? 1 : 0);
487
+ exif.push(ifd0Entries & 0xff, (ifd0Entries >> 8) & 0xff);
488
+ let dataOffset = 8 + 2 + ifd0Entries * 12 + 4;
399
489
  for (const entry of entries) {
400
490
  exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
401
491
  exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
@@ -411,6 +501,16 @@ class PNGBase {
411
501
  dataOffset += entry.value.length;
412
502
  }
413
503
  }
504
+ // Add GPS IFD pointer if we have GPS data
505
+ let gpsIfdOffset = 0;
506
+ if (hasGPS) {
507
+ gpsIfdOffset = dataOffset;
508
+ // GPS IFD Pointer tag (0x8825), type 4 (LONG), count 1
509
+ exif.push(0x25, 0x88); // Tag
510
+ exif.push(0x04, 0x00); // Type
511
+ exif.push(0x01, 0x00, 0x00, 0x00); // Count
512
+ exif.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
513
+ }
414
514
  exif.push(0x00, 0x00, 0x00, 0x00);
415
515
  for (const entry of entries) {
416
516
  if (entry.value.length > 4) {
@@ -419,8 +519,65 @@ class PNGBase {
419
519
  }
420
520
  }
421
521
  }
522
+ // Add GPS IFD if we have GPS data
523
+ if (hasGPS) {
524
+ const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
525
+ for (const byte of gpsIfd) {
526
+ exif.push(byte);
527
+ }
528
+ }
422
529
  return new Uint8Array(exif);
423
530
  }
531
+ createGPSIFD(metadata, gpsIfdStart) {
532
+ const gps = [];
533
+ const numEntries = 4;
534
+ gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
535
+ const latitude = metadata.latitude;
536
+ const longitude = metadata.longitude;
537
+ const absLat = Math.abs(latitude);
538
+ const absLon = Math.abs(longitude);
539
+ const latDeg = Math.floor(absLat);
540
+ const latMin = Math.floor((absLat - latDeg) * 60);
541
+ const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
542
+ const lonDeg = Math.floor(absLon);
543
+ const lonMin = Math.floor((absLon - lonDeg) * 60);
544
+ const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
545
+ let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
546
+ // Entry 1: GPSLatitudeRef
547
+ gps.push(0x01, 0x00);
548
+ gps.push(0x02, 0x00);
549
+ gps.push(0x02, 0x00, 0x00, 0x00);
550
+ gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
551
+ // Entry 2: GPSLatitude
552
+ gps.push(0x02, 0x00);
553
+ gps.push(0x05, 0x00);
554
+ gps.push(0x03, 0x00, 0x00, 0x00);
555
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
556
+ dataOffset += 24;
557
+ // Entry 3: GPSLongitudeRef
558
+ gps.push(0x03, 0x00);
559
+ gps.push(0x02, 0x00);
560
+ gps.push(0x02, 0x00, 0x00, 0x00);
561
+ gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
562
+ // Entry 4: GPSLongitude
563
+ gps.push(0x04, 0x00);
564
+ gps.push(0x05, 0x00);
565
+ gps.push(0x03, 0x00, 0x00, 0x00);
566
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
567
+ gps.push(0x00, 0x00, 0x00, 0x00);
568
+ // Write rationals
569
+ this.writeRational(gps, latDeg, 1);
570
+ this.writeRational(gps, latMin, 1);
571
+ this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
572
+ this.writeRational(gps, lonDeg, 1);
573
+ this.writeRational(gps, lonMin, 1);
574
+ this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
575
+ return gps;
576
+ }
577
+ writeRational(output, numerator, denominator) {
578
+ output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
579
+ output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
580
+ }
424
581
  /**
425
582
  * Concatenate multiple byte arrays into a single Uint8Array
426
583
  */
@@ -487,5 +644,21 @@ class PNGBase {
487
644
  }
488
645
  }
489
646
  }
647
+ /**
648
+ * Get the list of metadata fields supported by PNG format
649
+ */
650
+ getSupportedMetadata() {
651
+ return [
652
+ "creationDate", // eXIf chunk
653
+ "latitude", // eXIf chunk (GPS IFD)
654
+ "longitude", // eXIf chunk (GPS IFD)
655
+ "dpiX", // pHYs chunk
656
+ "dpiY", // pHYs chunk
657
+ "title", // tEXt chunk
658
+ "author", // tEXt chunk
659
+ "description", // tEXt chunk
660
+ "copyright", // tEXt chunk
661
+ ];
662
+ }
490
663
  }
491
664
  exports.PNGBase = PNGBase;
@@ -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
  * PPM format handler
4
4
  * Implements the Netpbm PPM (Portable PixMap) format.
@@ -46,5 +46,15 @@ export declare class PPMFormat implements ImageFormat {
46
46
  * Check if a byte is whitespace (space, tab, CR, LF)
47
47
  */
48
48
  private isWhitespace;
49
+ /**
50
+ * Get the list of metadata fields supported by PPM format
51
+ */
52
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
53
+ /**
54
+ * Extract metadata from PPM data without fully decoding the pixel data
55
+ * @param data Raw PPM data
56
+ * @returns Extracted metadata or undefined
57
+ */
58
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
49
59
  }
50
60
  //# sourceMappingURL=ppm.d.ts.map
@@ -242,5 +242,39 @@ class PPMFormat {
242
242
  isWhitespace(byte) {
243
243
  return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
244
244
  }
245
+ /**
246
+ * Get the list of metadata fields supported by PPM format
247
+ */
248
+ getSupportedMetadata() {
249
+ return []; // PPM format doesn't support metadata preservation
250
+ }
251
+ /**
252
+ * Extract metadata from PPM data without fully decoding the pixel data
253
+ * @param data Raw PPM data
254
+ * @returns Extracted metadata or undefined
255
+ */
256
+ extractMetadata(data) {
257
+ if (!this.canDecode(data)) {
258
+ return Promise.resolve(undefined);
259
+ }
260
+ const metadata = {
261
+ format: "ppm",
262
+ compression: "none",
263
+ frameCount: 1,
264
+ bitDepth: 8,
265
+ colorType: "rgb",
266
+ };
267
+ // PPM is always RGB, uncompressed, and typically 8-bit
268
+ // P3 is ASCII, P6 is binary
269
+ if (data[1] === 0x33) {
270
+ // '3'
271
+ metadata.compression = "none"; // ASCII encoding
272
+ }
273
+ else if (data[1] === 0x36) {
274
+ // '6'
275
+ metadata.compression = "none"; // Binary encoding
276
+ }
277
+ return Promise.resolve(metadata);
278
+ }
245
279
  }
246
280
  exports.PPMFormat = PPMFormat;
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageFormat, MultiFrameImageData } from "../types.js";
1
+ import type { ImageData, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
2
2
  /**
3
3
  * Options for TIFF encoding
4
4
  */
@@ -74,5 +74,17 @@ export declare class TIFFFormat implements ImageFormat {
74
74
  * Returns null if the TIFF uses unsupported features
75
75
  */
76
76
  private decodePureJSFromIFD;
77
+ /**
78
+ * Get list of metadata fields supported by TIFF format
79
+ * TIFF supports extensive EXIF metadata including GPS and InteropIFD
80
+ */
81
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
82
+ /**
83
+ * Extract metadata from TIFF data without fully decoding the pixel data
84
+ * This quickly parses IFD entries to extract metadata
85
+ * @param data Raw TIFF data
86
+ * @returns Extracted metadata or undefined
87
+ */
88
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
77
89
  }
78
90
  //# sourceMappingURL=tiff.d.ts.map