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.
- package/README.md +128 -7
- package/esm/src/formats/jpeg.d.ts +12 -1
- package/esm/src/formats/jpeg.js +633 -4
- package/esm/src/formats/png_base.d.ts +8 -0
- package/esm/src/formats/png_base.js +176 -3
- package/esm/src/formats/tiff.d.ts +6 -1
- package/esm/src/formats/tiff.js +31 -0
- package/esm/src/formats/webp.d.ts +9 -2
- package/esm/src/formats/webp.js +211 -62
- package/esm/src/image.d.ts +51 -0
- package/esm/src/image.js +225 -5
- package/esm/src/types.d.ts +41 -1
- package/esm/src/utils/image_processing.d.ts +55 -0
- package/esm/src/utils/image_processing.js +210 -0
- package/esm/src/utils/metadata/xmp.d.ts +52 -0
- package/esm/src/utils/metadata/xmp.js +325 -0
- package/esm/src/utils/resize.d.ts +4 -0
- package/esm/src/utils/resize.js +74 -0
- package/package.json +1 -1
- package/script/src/formats/jpeg.d.ts +12 -1
- package/script/src/formats/jpeg.js +633 -4
- package/script/src/formats/png_base.d.ts +8 -0
- package/script/src/formats/png_base.js +176 -3
- package/script/src/formats/tiff.d.ts +6 -1
- package/script/src/formats/tiff.js +31 -0
- package/script/src/formats/webp.d.ts +9 -2
- package/script/src/formats/webp.js +211 -62
- package/script/src/image.d.ts +51 -0
- package/script/src/image.js +223 -3
- package/script/src/types.d.ts +41 -1
- package/script/src/utils/image_processing.d.ts +55 -0
- package/script/src/utils/image_processing.js +216 -0
- package/script/src/utils/metadata/xmp.d.ts +52 -0
- package/script/src/utils/metadata/xmp.js +333 -0
- package/script/src/utils/resize.d.ts +4 -0
- 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
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|