cross-image 0.2.2 → 0.2.3

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 (36) hide show
  1. package/README.md +128 -7
  2. package/esm/src/formats/jpeg.d.ts +12 -1
  3. package/esm/src/formats/jpeg.js +633 -4
  4. package/esm/src/formats/png_base.d.ts +8 -0
  5. package/esm/src/formats/png_base.js +176 -3
  6. package/esm/src/formats/tiff.d.ts +6 -1
  7. package/esm/src/formats/tiff.js +31 -0
  8. package/esm/src/formats/webp.d.ts +9 -2
  9. package/esm/src/formats/webp.js +211 -62
  10. package/esm/src/image.d.ts +51 -0
  11. package/esm/src/image.js +225 -5
  12. package/esm/src/types.d.ts +41 -1
  13. package/esm/src/utils/image_processing.d.ts +55 -0
  14. package/esm/src/utils/image_processing.js +210 -0
  15. package/esm/src/utils/metadata/xmp.d.ts +52 -0
  16. package/esm/src/utils/metadata/xmp.js +325 -0
  17. package/esm/src/utils/resize.d.ts +4 -0
  18. package/esm/src/utils/resize.js +74 -0
  19. package/package.json +1 -1
  20. package/script/src/formats/jpeg.d.ts +12 -1
  21. package/script/src/formats/jpeg.js +633 -4
  22. package/script/src/formats/png_base.d.ts +8 -0
  23. package/script/src/formats/png_base.js +176 -3
  24. package/script/src/formats/tiff.d.ts +6 -1
  25. package/script/src/formats/tiff.js +31 -0
  26. package/script/src/formats/webp.d.ts +9 -2
  27. package/script/src/formats/webp.js +211 -62
  28. package/script/src/image.d.ts +51 -0
  29. package/script/src/image.js +223 -3
  30. package/script/src/types.d.ts +41 -1
  31. package/script/src/utils/image_processing.d.ts +55 -0
  32. package/script/src/utils/image_processing.js +216 -0
  33. package/script/src/utils/metadata/xmp.d.ts +52 -0
  34. package/script/src/utils/metadata/xmp.js +333 -0
  35. package/script/src/utils/resize.d.ts +4 -0
  36. package/script/src/utils/resize.js +75 -0
@@ -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, 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,10 @@ 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>;
77
82
  }
78
83
  //# sourceMappingURL=tiff.d.ts.map
@@ -935,5 +935,36 @@ 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
+ }
938
969
  }
939
970
  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,17 @@ 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>;
38
45
  }
39
46
  //# sourceMappingURL=webp.d.ts.map