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
@@ -174,4 +174,70 @@ export class PAMFormat {
174
174
  output.set(data, headerBytes.length);
175
175
  return Promise.resolve(output);
176
176
  }
177
+ /**
178
+ * Get the list of metadata fields supported by PAM format
179
+ */
180
+ getSupportedMetadata() {
181
+ return []; // PAM format doesn't support metadata preservation
182
+ }
183
+ /**
184
+ * Extract metadata from PAM data without fully decoding the pixel data
185
+ * @param data Raw PAM data
186
+ * @returns Extracted metadata or undefined
187
+ */
188
+ extractMetadata(data) {
189
+ if (!this.canDecode(data)) {
190
+ return Promise.resolve(undefined);
191
+ }
192
+ const metadata = {
193
+ format: "pam",
194
+ compression: "none",
195
+ frameCount: 1,
196
+ bitDepth: 8, // PAM typically uses 8 bits per channel
197
+ colorType: "rgba",
198
+ };
199
+ // Try to parse the header to get actual color type
200
+ const decoder = new TextDecoder();
201
+ const headerText = decoder.decode(data.slice(0, Math.min(200, data.length)));
202
+ // Look for TUPLTYPE to determine color type
203
+ const tupltypeMatch = headerText.match(/TUPLTYPE\s+(\S+)/);
204
+ if (tupltypeMatch) {
205
+ const tupltype = tupltypeMatch[1];
206
+ if (tupltype === "GRAYSCALE") {
207
+ metadata.colorType = "grayscale";
208
+ }
209
+ else if (tupltype === "RGB") {
210
+ metadata.colorType = "rgb";
211
+ }
212
+ else if (tupltype === "RGB_ALPHA") {
213
+ metadata.colorType = "rgba";
214
+ }
215
+ }
216
+ // Look for DEPTH to determine number of channels
217
+ const depthMatch = headerText.match(/DEPTH\s+(\d+)/);
218
+ if (depthMatch) {
219
+ const depth = parseInt(depthMatch[1]);
220
+ if (depth === 1) {
221
+ metadata.colorType = "grayscale";
222
+ }
223
+ else if (depth === 3) {
224
+ metadata.colorType = "rgb";
225
+ }
226
+ else if (depth === 4) {
227
+ metadata.colorType = "rgba";
228
+ }
229
+ }
230
+ // Look for MAXVAL to determine bit depth
231
+ const maxvalMatch = headerText.match(/MAXVAL\s+(\d+)/);
232
+ if (maxvalMatch) {
233
+ const maxval = parseInt(maxvalMatch[1]);
234
+ if (maxval === 255) {
235
+ metadata.bitDepth = 8;
236
+ }
237
+ else if (maxval === 65535) {
238
+ metadata.bitDepth = 16;
239
+ }
240
+ }
241
+ return Promise.resolve(metadata);
242
+ }
177
243
  }
@@ -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
@@ -201,4 +201,49 @@ export class PCXFormat {
201
201
  result.set(rleData, header.length);
202
202
  return Promise.resolve(result);
203
203
  }
204
+ /**
205
+ * Get the list of metadata fields supported by PCX format
206
+ */
207
+ getSupportedMetadata() {
208
+ return [
209
+ "dpiX", // DPI in header
210
+ "dpiY", // DPI in header
211
+ ];
212
+ }
213
+ /**
214
+ * Extract metadata from PCX data without fully decoding the pixel data
215
+ * @param data Raw PCX data
216
+ * @returns Extracted metadata or undefined
217
+ */
218
+ extractMetadata(data) {
219
+ if (!this.canDecode(data)) {
220
+ return Promise.resolve(undefined);
221
+ }
222
+ const metadata = {
223
+ format: "pcx",
224
+ compression: "rle",
225
+ frameCount: 1,
226
+ bitDepth: data[3], // Bits per pixel per plane
227
+ colorType: "rgb",
228
+ };
229
+ // Check number of planes to determine color type
230
+ const numPlanes = data[65];
231
+ if (numPlanes === 1) {
232
+ metadata.colorType = "indexed";
233
+ }
234
+ else if (numPlanes === 3) {
235
+ metadata.colorType = "rgb";
236
+ }
237
+ else if (numPlanes === 4) {
238
+ metadata.colorType = "rgba";
239
+ }
240
+ // DPI information
241
+ const dpiX = data[12] | (data[13] << 8);
242
+ const dpiY = data[14] | (data[15] << 8);
243
+ if (dpiX > 0)
244
+ metadata.dpiX = dpiX;
245
+ if (dpiY > 0)
246
+ metadata.dpiY = dpiY;
247
+ return Promise.resolve(metadata);
248
+ }
204
249
  }
@@ -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
@@ -137,4 +137,91 @@ export class PNGFormat extends PNGBase {
137
137
  // Concatenate all chunks
138
138
  return this.concatenateArrays(chunks);
139
139
  }
140
+ /**
141
+ * Get the list of metadata fields supported by PNG format
142
+ * Delegates to PNGBase implementation
143
+ */
144
+ getSupportedMetadata() {
145
+ return super.getSupportedMetadata();
146
+ }
147
+ /**
148
+ * Extract metadata from PNG data without fully decoding the pixel data
149
+ * This quickly parses PNG chunks to extract metadata
150
+ * @param data Raw PNG data
151
+ * @returns Extracted metadata or undefined
152
+ */
153
+ extractMetadata(data) {
154
+ if (!this.canDecode(data)) {
155
+ return Promise.resolve(undefined);
156
+ }
157
+ let pos = 8; // Skip PNG signature
158
+ let width = 0;
159
+ let height = 0;
160
+ const metadata = {
161
+ format: "png",
162
+ compression: "deflate",
163
+ frameCount: 1,
164
+ };
165
+ // Parse chunks for metadata only
166
+ while (pos < data.length) {
167
+ if (pos + 8 > data.length)
168
+ break;
169
+ const length = this.readUint32(data, pos);
170
+ pos += 4;
171
+ const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
172
+ pos += 4;
173
+ if (pos + length + 4 > data.length)
174
+ break;
175
+ const chunkData = data.slice(pos, pos + length);
176
+ pos += length;
177
+ pos += 4; // Skip CRC
178
+ if (type === "IHDR") {
179
+ width = this.readUint32(chunkData, 0);
180
+ height = this.readUint32(chunkData, 4);
181
+ // Parse bit depth and color type from IHDR
182
+ if (chunkData.length >= 9) {
183
+ metadata.bitDepth = chunkData[8];
184
+ const colorTypeCode = chunkData[9];
185
+ // PNG color types: 0=grayscale, 2=rgb, 3=indexed, 4=grayscale+alpha, 6=rgba
186
+ switch (colorTypeCode) {
187
+ case 0:
188
+ metadata.colorType = "grayscale";
189
+ break;
190
+ case 2:
191
+ metadata.colorType = "rgb";
192
+ break;
193
+ case 3:
194
+ metadata.colorType = "indexed";
195
+ break;
196
+ case 4:
197
+ metadata.colorType = "grayscale-alpha";
198
+ break;
199
+ case 6:
200
+ metadata.colorType = "rgba";
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ else if (type === "pHYs") {
206
+ // Physical pixel dimensions
207
+ this.parsePhysChunk(chunkData, metadata, width, height);
208
+ }
209
+ else if (type === "tEXt") {
210
+ // Text chunk
211
+ this.parseTextChunk(chunkData, metadata);
212
+ }
213
+ else if (type === "iTXt") {
214
+ // International text chunk
215
+ this.parseITxtChunk(chunkData, metadata);
216
+ }
217
+ else if (type === "eXIf") {
218
+ // EXIF chunk
219
+ this.parseExifChunk(chunkData, metadata);
220
+ }
221
+ else if (type === "IEND") {
222
+ break;
223
+ }
224
+ }
225
+ return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
226
+ }
140
227
  }
@@ -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
@@ -315,6 +315,7 @@ export class PNGBase {
315
315
  const numEntries = littleEndian
316
316
  ? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
317
317
  : (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
318
+ let gpsIfdOffset = 0;
318
319
  for (let i = 0; i < numEntries; i++) {
319
320
  const entryOffset = ifd0Offset + 2 + i * 12;
320
321
  if (entryOffset + 12 > data.length)
@@ -339,12 +340,96 @@ export class PNGBase {
339
340
  }
340
341
  }
341
342
  }
343
+ // GPS IFD Pointer tag (0x8825)
344
+ if (tag === 0x8825) {
345
+ gpsIfdOffset = littleEndian
346
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
347
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
348
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
349
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
350
+ }
351
+ }
352
+ // Parse GPS IFD if present
353
+ if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
354
+ this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
342
355
  }
343
356
  }
344
357
  catch (_e) {
345
358
  // Ignore EXIF parsing errors
346
359
  }
347
360
  }
361
+ parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
362
+ try {
363
+ const numEntries = littleEndian
364
+ ? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
365
+ : (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
366
+ let latRef = "";
367
+ let lonRef = "";
368
+ let latitude;
369
+ let longitude;
370
+ for (let i = 0; i < numEntries; i++) {
371
+ const entryOffset = gpsIfdOffset + 2 + i * 12;
372
+ if (entryOffset + 12 > data.length)
373
+ break;
374
+ const tag = littleEndian
375
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
376
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
377
+ const type = littleEndian
378
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
379
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
380
+ const valueOffset = littleEndian
381
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
382
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
383
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
384
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
385
+ // GPSLatitudeRef (0x0001)
386
+ if (tag === 0x0001 && type === 2) {
387
+ latRef = String.fromCharCode(data[entryOffset + 8]);
388
+ }
389
+ // GPSLatitude (0x0002)
390
+ if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
391
+ const degrees = this.readRational(data, valueOffset, littleEndian);
392
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
393
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
394
+ latitude = degrees + minutes / 60 + seconds / 3600;
395
+ }
396
+ // GPSLongitudeRef (0x0003)
397
+ if (tag === 0x0003 && type === 2) {
398
+ lonRef = String.fromCharCode(data[entryOffset + 8]);
399
+ }
400
+ // GPSLongitude (0x0004)
401
+ if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
402
+ const degrees = this.readRational(data, valueOffset, littleEndian);
403
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
404
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
405
+ longitude = degrees + minutes / 60 + seconds / 3600;
406
+ }
407
+ }
408
+ // Apply hemisphere references
409
+ if (latitude !== undefined && latRef) {
410
+ metadata.latitude = latRef === "S" ? -latitude : latitude;
411
+ }
412
+ if (longitude !== undefined && lonRef) {
413
+ metadata.longitude = lonRef === "W" ? -longitude : longitude;
414
+ }
415
+ }
416
+ catch (_e) {
417
+ // Ignore GPS parsing errors
418
+ }
419
+ }
420
+ readRational(data, offset, littleEndian) {
421
+ const numerator = littleEndian
422
+ ? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
423
+ (data[offset + 3] << 24)
424
+ : (data[offset] << 24) | (data[offset + 1] << 16) |
425
+ (data[offset + 2] << 8) | data[offset + 3];
426
+ const denominator = littleEndian
427
+ ? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
428
+ (data[offset + 7] << 24)
429
+ : (data[offset + 4] << 24) | (data[offset + 5] << 16) |
430
+ (data[offset + 6] << 8) | data[offset + 7];
431
+ return denominator !== 0 ? numerator / denominator : 0;
432
+ }
348
433
  /**
349
434
  * Create pHYs (physical pixel dimensions) chunk
350
435
  */
@@ -385,14 +470,19 @@ export class PNGBase {
385
470
  value: new TextEncoder().encode(dateStr),
386
471
  });
387
472
  }
388
- if (entries.length === 0)
473
+ // Check if we have GPS data
474
+ const hasGPS = metadata.latitude !== undefined &&
475
+ metadata.longitude !== undefined;
476
+ if (entries.length === 0 && !hasGPS)
389
477
  return null;
390
478
  const exif = [];
391
479
  exif.push(0x49, 0x49); // "II"
392
480
  exif.push(0x2a, 0x00); // 42
393
481
  exif.push(0x08, 0x00, 0x00, 0x00);
394
- exif.push(entries.length & 0xff, (entries.length >> 8) & 0xff);
395
- let dataOffset = 8 + 2 + entries.length * 12 + 4;
482
+ // Number of entries (add GPS IFD pointer if we have GPS data)
483
+ const ifd0Entries = entries.length + (hasGPS ? 1 : 0);
484
+ exif.push(ifd0Entries & 0xff, (ifd0Entries >> 8) & 0xff);
485
+ let dataOffset = 8 + 2 + ifd0Entries * 12 + 4;
396
486
  for (const entry of entries) {
397
487
  exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
398
488
  exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
@@ -408,6 +498,16 @@ export class PNGBase {
408
498
  dataOffset += entry.value.length;
409
499
  }
410
500
  }
501
+ // Add GPS IFD pointer if we have GPS data
502
+ let gpsIfdOffset = 0;
503
+ if (hasGPS) {
504
+ gpsIfdOffset = dataOffset;
505
+ // GPS IFD Pointer tag (0x8825), type 4 (LONG), count 1
506
+ exif.push(0x25, 0x88); // Tag
507
+ exif.push(0x04, 0x00); // Type
508
+ exif.push(0x01, 0x00, 0x00, 0x00); // Count
509
+ exif.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
510
+ }
411
511
  exif.push(0x00, 0x00, 0x00, 0x00);
412
512
  for (const entry of entries) {
413
513
  if (entry.value.length > 4) {
@@ -416,8 +516,65 @@ export class PNGBase {
416
516
  }
417
517
  }
418
518
  }
519
+ // Add GPS IFD if we have GPS data
520
+ if (hasGPS) {
521
+ const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
522
+ for (const byte of gpsIfd) {
523
+ exif.push(byte);
524
+ }
525
+ }
419
526
  return new Uint8Array(exif);
420
527
  }
528
+ createGPSIFD(metadata, gpsIfdStart) {
529
+ const gps = [];
530
+ const numEntries = 4;
531
+ gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
532
+ const latitude = metadata.latitude;
533
+ const longitude = metadata.longitude;
534
+ const absLat = Math.abs(latitude);
535
+ const absLon = Math.abs(longitude);
536
+ const latDeg = Math.floor(absLat);
537
+ const latMin = Math.floor((absLat - latDeg) * 60);
538
+ const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
539
+ const lonDeg = Math.floor(absLon);
540
+ const lonMin = Math.floor((absLon - lonDeg) * 60);
541
+ const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
542
+ let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
543
+ // Entry 1: GPSLatitudeRef
544
+ gps.push(0x01, 0x00);
545
+ gps.push(0x02, 0x00);
546
+ gps.push(0x02, 0x00, 0x00, 0x00);
547
+ gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
548
+ // Entry 2: GPSLatitude
549
+ gps.push(0x02, 0x00);
550
+ gps.push(0x05, 0x00);
551
+ gps.push(0x03, 0x00, 0x00, 0x00);
552
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
553
+ dataOffset += 24;
554
+ // Entry 3: GPSLongitudeRef
555
+ gps.push(0x03, 0x00);
556
+ gps.push(0x02, 0x00);
557
+ gps.push(0x02, 0x00, 0x00, 0x00);
558
+ gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
559
+ // Entry 4: GPSLongitude
560
+ gps.push(0x04, 0x00);
561
+ gps.push(0x05, 0x00);
562
+ gps.push(0x03, 0x00, 0x00, 0x00);
563
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
564
+ gps.push(0x00, 0x00, 0x00, 0x00);
565
+ // Write rationals
566
+ this.writeRational(gps, latDeg, 1);
567
+ this.writeRational(gps, latMin, 1);
568
+ this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
569
+ this.writeRational(gps, lonDeg, 1);
570
+ this.writeRational(gps, lonMin, 1);
571
+ this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
572
+ return gps;
573
+ }
574
+ writeRational(output, numerator, denominator) {
575
+ output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
576
+ output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
577
+ }
421
578
  /**
422
579
  * Concatenate multiple byte arrays into a single Uint8Array
423
580
  */
@@ -484,4 +641,20 @@ export class PNGBase {
484
641
  }
485
642
  }
486
643
  }
644
+ /**
645
+ * Get the list of metadata fields supported by PNG format
646
+ */
647
+ getSupportedMetadata() {
648
+ return [
649
+ "creationDate", // eXIf chunk
650
+ "latitude", // eXIf chunk (GPS IFD)
651
+ "longitude", // eXIf chunk (GPS IFD)
652
+ "dpiX", // pHYs chunk
653
+ "dpiY", // pHYs chunk
654
+ "title", // tEXt chunk
655
+ "author", // tEXt chunk
656
+ "description", // tEXt chunk
657
+ "copyright", // tEXt chunk
658
+ ];
659
+ }
487
660
  }
@@ -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
@@ -239,4 +239,38 @@ export class PPMFormat {
239
239
  isWhitespace(byte) {
240
240
  return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
241
241
  }
242
+ /**
243
+ * Get the list of metadata fields supported by PPM format
244
+ */
245
+ getSupportedMetadata() {
246
+ return []; // PPM format doesn't support metadata preservation
247
+ }
248
+ /**
249
+ * Extract metadata from PPM data without fully decoding the pixel data
250
+ * @param data Raw PPM data
251
+ * @returns Extracted metadata or undefined
252
+ */
253
+ extractMetadata(data) {
254
+ if (!this.canDecode(data)) {
255
+ return Promise.resolve(undefined);
256
+ }
257
+ const metadata = {
258
+ format: "ppm",
259
+ compression: "none",
260
+ frameCount: 1,
261
+ bitDepth: 8,
262
+ colorType: "rgb",
263
+ };
264
+ // PPM is always RGB, uncompressed, and typically 8-bit
265
+ // P3 is ASCII, P6 is binary
266
+ if (data[1] === 0x33) {
267
+ // '3'
268
+ metadata.compression = "none"; // ASCII encoding
269
+ }
270
+ else if (data[1] === 0x36) {
271
+ // '6'
272
+ metadata.compression = "none"; // Binary encoding
273
+ }
274
+ return Promise.resolve(metadata);
275
+ }
242
276
  }
@@ -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