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
@@ -935,5 +935,170 @@ class TIFFFormat {
935
935
  }
936
936
  return rgba;
937
937
  }
938
+ /**
939
+ * Get list of metadata fields supported by TIFF format
940
+ * TIFF supports extensive EXIF metadata including GPS and InteropIFD
941
+ */
942
+ getSupportedMetadata() {
943
+ return [
944
+ "creationDate",
945
+ "description",
946
+ "author",
947
+ "copyright",
948
+ "cameraMake",
949
+ "cameraModel",
950
+ "orientation",
951
+ "software",
952
+ "iso",
953
+ "exposureTime",
954
+ "fNumber",
955
+ "focalLength",
956
+ "flash",
957
+ "whiteBalance",
958
+ "lensMake",
959
+ "lensModel",
960
+ "userComment",
961
+ "latitude",
962
+ "longitude",
963
+ "dpiX",
964
+ "dpiY",
965
+ "physicalWidth",
966
+ "physicalHeight",
967
+ ];
968
+ }
969
+ /**
970
+ * Extract metadata from TIFF data without fully decoding the pixel data
971
+ * This quickly parses IFD entries to extract metadata
972
+ * @param data Raw TIFF data
973
+ * @returns Extracted metadata or undefined
974
+ */
975
+ extractMetadata(data) {
976
+ if (!this.canDecode(data)) {
977
+ return Promise.resolve(undefined);
978
+ }
979
+ // Determine byte order
980
+ const isLittleEndian = data[0] === 0x49 && data[1] === 0x49;
981
+ if (!isLittleEndian && !(data[0] === 0x4d && data[1] === 0x4d)) {
982
+ return Promise.resolve(undefined);
983
+ }
984
+ // Read IFD offset
985
+ const ifdOffset = this.readUint32(data, 4, isLittleEndian);
986
+ if (ifdOffset >= data.length) {
987
+ return Promise.resolve(undefined);
988
+ }
989
+ // Get dimensions for DPI calculation
990
+ const width = this.getIFDValue(data, ifdOffset, 0x0100, isLittleEndian);
991
+ const height = this.getIFDValue(data, ifdOffset, 0x0101, isLittleEndian);
992
+ if (!width || !height) {
993
+ return Promise.resolve(undefined);
994
+ }
995
+ // Extract metadata from TIFF tags
996
+ const metadata = {
997
+ format: "tiff",
998
+ frameCount: 1,
999
+ bitDepth: 8,
1000
+ };
1001
+ // Get compression type
1002
+ const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
1003
+ if (compression === 1) {
1004
+ metadata.compression = "none";
1005
+ }
1006
+ else if (compression === 5) {
1007
+ metadata.compression = "lzw";
1008
+ }
1009
+ else if (compression === 7) {
1010
+ metadata.compression = "jpeg";
1011
+ }
1012
+ else if (compression === 32773) {
1013
+ metadata.compression = "packbits";
1014
+ }
1015
+ else if (compression) {
1016
+ metadata.compression = `unknown-${compression}`;
1017
+ }
1018
+ // Get bits per sample
1019
+ const bitsPerSample = this.getIFDValue(data, ifdOffset, 0x0102, isLittleEndian);
1020
+ if (bitsPerSample) {
1021
+ metadata.bitDepth = bitsPerSample;
1022
+ }
1023
+ // Get photometric interpretation for color type
1024
+ const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
1025
+ const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
1026
+ if (photometric === 0 || photometric === 1) {
1027
+ metadata.colorType = "grayscale";
1028
+ }
1029
+ else if (photometric === 2) {
1030
+ if (samplesPerPixel === 3) {
1031
+ metadata.colorType = "rgb";
1032
+ }
1033
+ else if (samplesPerPixel === 4) {
1034
+ metadata.colorType = "rgba";
1035
+ }
1036
+ }
1037
+ else if (photometric === 3) {
1038
+ metadata.colorType = "indexed";
1039
+ }
1040
+ // Count IFDs (pages/frames) by following the chain
1041
+ let currentIfdOffset = ifdOffset;
1042
+ let frameCount = 0;
1043
+ while (currentIfdOffset > 0 && currentIfdOffset < data.length &&
1044
+ frameCount < 1000) {
1045
+ frameCount++;
1046
+ // Read number of entries in this IFD
1047
+ const numEntries = this.readUint16(data, currentIfdOffset, isLittleEndian);
1048
+ // Next IFD offset is after all entries (2 + numEntries * 12 bytes)
1049
+ const nextIfdOffsetPos = currentIfdOffset + 2 + (numEntries * 12);
1050
+ if (nextIfdOffsetPos + 4 > data.length)
1051
+ break;
1052
+ currentIfdOffset = this.readUint32(data, nextIfdOffsetPos, isLittleEndian);
1053
+ }
1054
+ metadata.frameCount = frameCount;
1055
+ // XResolution (0x011a) and YResolution (0x011b) for DPI
1056
+ const xResOffset = this.getIFDValue(data, ifdOffset, 0x011a, isLittleEndian);
1057
+ const yResOffset = this.getIFDValue(data, ifdOffset, 0x011b, isLittleEndian);
1058
+ if (xResOffset && xResOffset < data.length - 8) {
1059
+ const numerator = this.readUint32(data, xResOffset, isLittleEndian);
1060
+ const denominator = this.readUint32(data, xResOffset + 4, isLittleEndian);
1061
+ if (denominator > 0) {
1062
+ metadata.dpiX = Math.round(numerator / denominator);
1063
+ }
1064
+ }
1065
+ if (yResOffset && yResOffset < data.length - 8) {
1066
+ const numerator = this.readUint32(data, yResOffset, isLittleEndian);
1067
+ const denominator = this.readUint32(data, yResOffset + 4, isLittleEndian);
1068
+ if (denominator > 0) {
1069
+ metadata.dpiY = Math.round(numerator / denominator);
1070
+ }
1071
+ }
1072
+ // Calculate physical dimensions if DPI is available
1073
+ if (metadata.dpiX && metadata.dpiY) {
1074
+ metadata.physicalWidth = width / metadata.dpiX;
1075
+ metadata.physicalHeight = height / metadata.dpiY;
1076
+ }
1077
+ // ImageDescription (0x010e)
1078
+ const descOffset = this.getIFDValue(data, ifdOffset, 0x010e, isLittleEndian);
1079
+ if (descOffset && descOffset < data.length) {
1080
+ metadata.description = this.readString(data, descOffset);
1081
+ }
1082
+ // Artist (0x013b)
1083
+ const artistOffset = this.getIFDValue(data, ifdOffset, 0x013b, isLittleEndian);
1084
+ if (artistOffset && artistOffset < data.length) {
1085
+ metadata.author = this.readString(data, artistOffset);
1086
+ }
1087
+ // Copyright (0x8298)
1088
+ const copyrightOffset = this.getIFDValue(data, ifdOffset, 0x8298, isLittleEndian);
1089
+ if (copyrightOffset && copyrightOffset < data.length) {
1090
+ metadata.copyright = this.readString(data, copyrightOffset);
1091
+ }
1092
+ // DateTime (0x0132)
1093
+ const dateTimeOffset = this.getIFDValue(data, ifdOffset, 0x0132, isLittleEndian);
1094
+ if (dateTimeOffset && dateTimeOffset < data.length) {
1095
+ const dateStr = this.readString(data, dateTimeOffset);
1096
+ const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
1097
+ if (match) {
1098
+ metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
1099
+ }
1100
+ }
1101
+ return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
1102
+ }
938
1103
  }
939
1104
  exports.TIFFFormat = TIFFFormat;
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageFormat, WebPEncodeOptions } from "../types.js";
1
+ import type { ImageData, ImageFormat, ImageMetadata, WebPEncodeOptions } from "../types.js";
2
2
  /**
3
3
  * WebP format handler
4
4
  * Implements a basic WebP decoder and encoder
@@ -30,10 +30,24 @@ export declare class WebPFormat implements ImageFormat {
30
30
  private readUint24LE;
31
31
  private decodeUsingRuntime;
32
32
  private parseEXIF;
33
+ private parseGPSIFD;
34
+ private readRational;
33
35
  private parseXMP;
34
36
  private injectMetadata;
35
37
  private createEXIFChunk;
38
+ private createGPSIFD;
39
+ private writeRational;
36
40
  private createXMPChunk;
37
- private escapeXML;
41
+ /**
42
+ * Get the list of metadata fields supported by WebP format
43
+ */
44
+ getSupportedMetadata(): Array<keyof ImageMetadata>;
45
+ /**
46
+ * Extract metadata from WebP data without fully decoding the pixel data
47
+ * This quickly parses RIFF chunks to extract EXIF and XMP metadata
48
+ * @param data Raw WebP data
49
+ * @returns Extracted metadata or undefined
50
+ */
51
+ extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
38
52
  }
39
53
  //# sourceMappingURL=webp.d.ts.map
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.WebPFormat = void 0;
37
37
  const security_js_1 = require("../utils/security.js");
38
38
  const byte_utils_js_1 = require("../utils/byte_utils.js");
39
+ const xmp_js_1 = require("../utils/metadata/xmp.js");
39
40
  // Default quality for WebP encoding when not specified
40
41
  const DEFAULT_WEBP_QUALITY = 90;
41
42
  /**
@@ -255,6 +256,7 @@ class WebPFormat {
255
256
  const numEntries = littleEndian
256
257
  ? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
257
258
  : (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
259
+ let gpsIfdOffset = 0;
258
260
  // Parse basic EXIF tags (simplified version)
259
261
  for (let i = 0; i < numEntries && i < 50; i++) {
260
262
  const entryOffset = ifd0Offset + 2 + i * 12;
@@ -281,32 +283,103 @@ class WebPFormat {
281
283
  }
282
284
  }
283
285
  }
286
+ // GPS IFD Pointer tag (0x8825)
287
+ if (tag === 0x8825) {
288
+ gpsIfdOffset = littleEndian
289
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
290
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
291
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
292
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
293
+ }
294
+ }
295
+ // Parse GPS IFD if present
296
+ if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
297
+ this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
284
298
  }
285
299
  }
286
300
  catch (_e) {
287
301
  // Ignore EXIF parsing errors
288
302
  }
289
303
  }
304
+ parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
305
+ try {
306
+ const numEntries = littleEndian
307
+ ? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
308
+ : (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
309
+ let latRef = "";
310
+ let lonRef = "";
311
+ let latitude;
312
+ let longitude;
313
+ for (let i = 0; i < numEntries; i++) {
314
+ const entryOffset = gpsIfdOffset + 2 + i * 12;
315
+ if (entryOffset + 12 > data.length)
316
+ break;
317
+ const tag = littleEndian
318
+ ? data[entryOffset] | (data[entryOffset + 1] << 8)
319
+ : (data[entryOffset] << 8) | data[entryOffset + 1];
320
+ const type = littleEndian
321
+ ? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
322
+ : (data[entryOffset + 2] << 8) | data[entryOffset + 3];
323
+ const valueOffset = littleEndian
324
+ ? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
325
+ (data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
326
+ : (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
327
+ (data[entryOffset + 10] << 8) | data[entryOffset + 11];
328
+ // GPSLatitudeRef (0x0001)
329
+ if (tag === 0x0001 && type === 2) {
330
+ latRef = String.fromCharCode(data[entryOffset + 8]);
331
+ }
332
+ // GPSLatitude (0x0002)
333
+ if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
334
+ const degrees = this.readRational(data, valueOffset, littleEndian);
335
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
336
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
337
+ latitude = degrees + minutes / 60 + seconds / 3600;
338
+ }
339
+ // GPSLongitudeRef (0x0003)
340
+ if (tag === 0x0003 && type === 2) {
341
+ lonRef = String.fromCharCode(data[entryOffset + 8]);
342
+ }
343
+ // GPSLongitude (0x0004)
344
+ if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
345
+ const degrees = this.readRational(data, valueOffset, littleEndian);
346
+ const minutes = this.readRational(data, valueOffset + 8, littleEndian);
347
+ const seconds = this.readRational(data, valueOffset + 16, littleEndian);
348
+ longitude = degrees + minutes / 60 + seconds / 3600;
349
+ }
350
+ }
351
+ // Apply hemisphere references
352
+ if (latitude !== undefined && latRef) {
353
+ metadata.latitude = latRef === "S" ? -latitude : latitude;
354
+ }
355
+ if (longitude !== undefined && lonRef) {
356
+ metadata.longitude = lonRef === "W" ? -longitude : longitude;
357
+ }
358
+ }
359
+ catch (_e) {
360
+ // Ignore GPS parsing errors
361
+ }
362
+ }
363
+ readRational(data, offset, littleEndian) {
364
+ const numerator = littleEndian
365
+ ? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
366
+ (data[offset + 3] << 24)
367
+ : (data[offset] << 24) | (data[offset + 1] << 16) |
368
+ (data[offset + 2] << 8) | data[offset + 3];
369
+ const denominator = littleEndian
370
+ ? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
371
+ (data[offset + 7] << 24)
372
+ : (data[offset + 4] << 24) | (data[offset + 5] << 16) |
373
+ (data[offset + 6] << 8) | data[offset + 7];
374
+ return denominator !== 0 ? numerator / denominator : 0;
375
+ }
290
376
  parseXMP(data, metadata) {
291
- // XMP is XML-based metadata - simple parsing for common fields
377
+ // Parse XMP using the centralized utility
292
378
  try {
293
379
  const xmpStr = new TextDecoder().decode(data);
294
- // Extract title
295
- const titleMatch = xmpStr.match(/<dc:title[^>]*>([^<]+)<\/dc:title>/);
296
- if (titleMatch)
297
- metadata.title = titleMatch[1].trim();
298
- // Extract description
299
- const descMatch = xmpStr.match(/<dc:description[^>]*>([^<]+)<\/dc:description>/);
300
- if (descMatch)
301
- metadata.description = descMatch[1].trim();
302
- // Extract creator/author
303
- const creatorMatch = xmpStr.match(/<dc:creator[^>]*>([^<]+)<\/dc:creator>/);
304
- if (creatorMatch)
305
- metadata.author = creatorMatch[1].trim();
306
- // Extract rights/copyright
307
- const rightsMatch = xmpStr.match(/<dc:rights[^>]*>([^<]+)<\/dc:rights>/);
308
- if (rightsMatch)
309
- metadata.copyright = rightsMatch[1].trim();
380
+ const parsedMetadata = (0, xmp_js_1.parseXMP)(xmpStr);
381
+ // Merge parsed metadata into the existing metadata object
382
+ Object.assign(metadata, parsedMetadata);
310
383
  }
311
384
  catch (_e) {
312
385
  // Ignore XMP parsing errors
@@ -320,8 +393,9 @@ class WebPFormat {
320
393
  chunks.push(webpData.slice(0, 12));
321
394
  // Create metadata chunks
322
395
  const metadataChunks = [];
323
- // Create EXIF chunk if we have date or other EXIF data
324
- if (metadata.creationDate) {
396
+ // Create EXIF chunk if we have date or GPS data
397
+ if (metadata.creationDate ||
398
+ (metadata.latitude !== undefined && metadata.longitude !== undefined)) {
325
399
  const exifData = this.createEXIFChunk(metadata);
326
400
  if (exifData) {
327
401
  metadataChunks.push(exifData);
@@ -377,7 +451,10 @@ class WebPFormat {
377
451
  return finalData;
378
452
  }
379
453
  createEXIFChunk(metadata) {
380
- if (!metadata.creationDate)
454
+ const hasDate = metadata.creationDate !== undefined;
455
+ const hasGPS = metadata.latitude !== undefined &&
456
+ metadata.longitude !== undefined;
457
+ if (!hasDate && !hasGPS)
381
458
  return null;
382
459
  const exifData = [];
383
460
  // Byte order marker (little endian)
@@ -385,21 +462,47 @@ class WebPFormat {
385
462
  exifData.push(0x2a, 0x00); // 42
386
463
  // IFD0 offset
387
464
  exifData.push(0x08, 0x00, 0x00, 0x00);
388
- // Number of entries
389
- exifData.push(0x01, 0x00);
390
- // DateTime entry
391
- const date = metadata.creationDate;
392
- const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
393
- const dateBytes = new TextEncoder().encode(dateStr);
394
- // Tag 0x0132, Type 2 (ASCII), Count, Offset
395
- exifData.push(0x32, 0x01, 0x02, 0x00);
396
- exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
397
- exifData.push(0x12, 0x00, 0x00, 0x00); // Offset to data
465
+ // Number of entries (DateTime + GPS IFD pointer if needed)
466
+ const numEntries = (hasDate ? 1 : 0) + (hasGPS ? 1 : 0);
467
+ exifData.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
468
+ let dataOffset = 8 + 2 + numEntries * 12 + 4;
469
+ // DateTime entry (if present)
470
+ if (hasDate) {
471
+ const date = metadata.creationDate;
472
+ const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
473
+ const dateBytes = new TextEncoder().encode(dateStr);
474
+ // Tag 0x0132, Type 2 (ASCII), Count, Offset
475
+ exifData.push(0x32, 0x01, 0x02, 0x00);
476
+ exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
477
+ exifData.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
478
+ dataOffset += dateBytes.length;
479
+ }
480
+ // GPS IFD pointer (if present)
481
+ let gpsIfdOffset = 0;
482
+ if (hasGPS) {
483
+ gpsIfdOffset = dataOffset;
484
+ exifData.push(0x25, 0x88); // Tag 0x8825
485
+ exifData.push(0x04, 0x00); // Type LONG
486
+ exifData.push(0x01, 0x00, 0x00, 0x00); // Count 1
487
+ exifData.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
488
+ }
398
489
  // Next IFD
399
490
  exifData.push(0x00, 0x00, 0x00, 0x00);
400
- // Date string data
401
- for (const byte of dateBytes) {
402
- exifData.push(byte);
491
+ // Date string data (if present)
492
+ if (hasDate) {
493
+ const date = metadata.creationDate;
494
+ const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
495
+ const dateBytes = new TextEncoder().encode(dateStr);
496
+ for (const byte of dateBytes) {
497
+ exifData.push(byte);
498
+ }
499
+ }
500
+ // GPS IFD data (if present)
501
+ if (hasGPS) {
502
+ const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
503
+ for (const byte of gpsIfd) {
504
+ exifData.push(byte);
505
+ }
403
506
  }
404
507
  // Create chunk header
405
508
  const chunkData = new Uint8Array(exifData);
@@ -412,29 +515,59 @@ class WebPFormat {
412
515
  chunk.set(chunkData, 8);
413
516
  return chunk;
414
517
  }
518
+ createGPSIFD(metadata, gpsIfdStart) {
519
+ const gps = [];
520
+ const numEntries = 4;
521
+ gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
522
+ const latitude = metadata.latitude;
523
+ const longitude = metadata.longitude;
524
+ const absLat = Math.abs(latitude);
525
+ const absLon = Math.abs(longitude);
526
+ const latDeg = Math.floor(absLat);
527
+ const latMin = Math.floor((absLat - latDeg) * 60);
528
+ const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
529
+ const lonDeg = Math.floor(absLon);
530
+ const lonMin = Math.floor((absLon - lonDeg) * 60);
531
+ const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
532
+ let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
533
+ // GPSLatitudeRef
534
+ gps.push(0x01, 0x00);
535
+ gps.push(0x02, 0x00);
536
+ gps.push(0x02, 0x00, 0x00, 0x00);
537
+ gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
538
+ // GPSLatitude
539
+ gps.push(0x02, 0x00);
540
+ gps.push(0x05, 0x00);
541
+ gps.push(0x03, 0x00, 0x00, 0x00);
542
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
543
+ dataOffset += 24;
544
+ // GPSLongitudeRef
545
+ gps.push(0x03, 0x00);
546
+ gps.push(0x02, 0x00);
547
+ gps.push(0x02, 0x00, 0x00, 0x00);
548
+ gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
549
+ // GPSLongitude
550
+ gps.push(0x04, 0x00);
551
+ gps.push(0x05, 0x00);
552
+ gps.push(0x03, 0x00, 0x00, 0x00);
553
+ gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
554
+ gps.push(0x00, 0x00, 0x00, 0x00);
555
+ // Write rationals
556
+ this.writeRational(gps, latDeg, 1);
557
+ this.writeRational(gps, latMin, 1);
558
+ this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
559
+ this.writeRational(gps, lonDeg, 1);
560
+ this.writeRational(gps, lonMin, 1);
561
+ this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
562
+ return gps;
563
+ }
564
+ writeRational(output, numerator, denominator) {
565
+ output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
566
+ output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
567
+ }
415
568
  createXMPChunk(metadata) {
416
- const xmpParts = [];
417
- xmpParts.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
418
- xmpParts.push('<x:xmpmeta xmlns:x="adobe:ns:meta/">');
419
- xmpParts.push('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
420
- xmpParts.push('<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">');
421
- if (metadata.title) {
422
- xmpParts.push(`<dc:title>${this.escapeXML(metadata.title)}</dc:title>`);
423
- }
424
- if (metadata.description) {
425
- xmpParts.push(`<dc:description>${this.escapeXML(metadata.description)}</dc:description>`);
426
- }
427
- if (metadata.author) {
428
- xmpParts.push(`<dc:creator>${this.escapeXML(metadata.author)}</dc:creator>`);
429
- }
430
- if (metadata.copyright) {
431
- xmpParts.push(`<dc:rights>${this.escapeXML(metadata.copyright)}</dc:rights>`);
432
- }
433
- xmpParts.push("</rdf:Description>");
434
- xmpParts.push("</rdf:RDF>");
435
- xmpParts.push("</x:xmpmeta>");
436
- xmpParts.push('<?xpacket end="w"?>');
437
- const xmpStr = xmpParts.join("\n");
569
+ // Use the centralized XMP utility to create the XMP packet
570
+ const xmpStr = (0, xmp_js_1.createXMP)(metadata);
438
571
  const xmpData = new TextEncoder().encode(xmpStr);
439
572
  // Create chunk
440
573
  const chunk = new Uint8Array(8 + xmpData.length);
@@ -446,13 +579,121 @@ class WebPFormat {
446
579
  chunk.set(xmpData, 8);
447
580
  return chunk;
448
581
  }
449
- escapeXML(str) {
450
- return str
451
- .replace(/&/g, "&amp;")
452
- .replace(/</g, "&lt;")
453
- .replace(/>/g, "&gt;")
454
- .replace(/"/g, "&quot;")
455
- .replace(/'/g, "&apos;");
582
+ /**
583
+ * Get the list of metadata fields supported by WebP format
584
+ */
585
+ getSupportedMetadata() {
586
+ return [
587
+ // EXIF chunk
588
+ "creationDate",
589
+ "latitude",
590
+ "longitude",
591
+ // XMP chunk (enhanced support)
592
+ "title",
593
+ "description",
594
+ "author",
595
+ "copyright",
596
+ "cameraMake",
597
+ "cameraModel",
598
+ "orientation",
599
+ "software",
600
+ "iso",
601
+ "exposureTime",
602
+ "fNumber",
603
+ "focalLength",
604
+ ];
605
+ }
606
+ /**
607
+ * Extract metadata from WebP data without fully decoding the pixel data
608
+ * This quickly parses RIFF chunks to extract EXIF and XMP metadata
609
+ * @param data Raw WebP data
610
+ * @returns Extracted metadata or undefined
611
+ */
612
+ extractMetadata(data) {
613
+ if (!this.canDecode(data)) {
614
+ return Promise.resolve(undefined);
615
+ }
616
+ const metadata = {
617
+ format: "webp",
618
+ frameCount: 1,
619
+ bitDepth: 8,
620
+ };
621
+ let pos = 12; // Skip "RIFF" + size + "WEBP"
622
+ const readUint32LE = (data, offset) => {
623
+ return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
624
+ (data[offset + 3] << 24);
625
+ };
626
+ const readUint16LE = (data, offset) => {
627
+ return data[offset] | (data[offset + 1] << 8);
628
+ };
629
+ // Parse chunks for metadata
630
+ while (pos + 8 <= data.length) {
631
+ const chunkType = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
632
+ const chunkSize = readUint32LE(data, pos + 4);
633
+ pos += 8;
634
+ // Stop if we've gone past the end
635
+ if (pos + chunkSize > data.length)
636
+ break;
637
+ const chunkData = data.slice(pos, pos + chunkSize);
638
+ if (chunkType === "VP8 ") {
639
+ // Lossy VP8 chunk
640
+ metadata.compression = "vp8";
641
+ metadata.colorType = "rgb";
642
+ }
643
+ else if (chunkType === "VP8L") {
644
+ // Lossless VP8L chunk
645
+ metadata.compression = "vp8l";
646
+ metadata.colorType = "rgba";
647
+ }
648
+ else if (chunkType === "VP8X") {
649
+ // Extended format chunk - contains animation info
650
+ if (chunkData.length >= 10) {
651
+ const flags = chunkData[0];
652
+ const _hasAnimation = (flags & 0x02) !== 0;
653
+ const hasAlpha = (flags & 0x10) !== 0;
654
+ if (hasAlpha) {
655
+ metadata.colorType = "rgba";
656
+ }
657
+ else {
658
+ metadata.colorType = "rgb";
659
+ }
660
+ // Animation is handled in ANIM chunk
661
+ }
662
+ }
663
+ else if (chunkType === "ANIM") {
664
+ // Animation parameters chunk
665
+ if (chunkData.length >= 6) {
666
+ // Background color at bytes 0-3
667
+ // Loop count at bytes 4-5
668
+ const _loopCount = readUint16LE(chunkData, 4);
669
+ // Note: Frame count is not directly in ANIM chunk, need to count ANMF chunks
670
+ // Reset frame count to 0 to start counting ANMF frames
671
+ metadata.frameCount = 0;
672
+ }
673
+ }
674
+ else if (chunkType === "ANMF") {
675
+ // Animation frame - count frames
676
+ if (metadata.frameCount !== undefined) {
677
+ metadata.frameCount++;
678
+ }
679
+ else {
680
+ metadata.frameCount = 1;
681
+ }
682
+ }
683
+ else if (chunkType === "EXIF") {
684
+ // EXIF metadata chunk
685
+ this.parseEXIF(chunkData, metadata);
686
+ }
687
+ else if (chunkType === "XMP ") {
688
+ // XMP metadata chunk
689
+ this.parseXMP(chunkData, metadata);
690
+ }
691
+ pos += chunkSize;
692
+ // Chunks are padded to even length
693
+ if (chunkSize % 2 === 1)
694
+ pos++;
695
+ }
696
+ return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
456
697
  }
457
698
  }
458
699
  exports.WebPFormat = WebPFormat;