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.
- package/README.md +333 -168
- package/esm/mod.d.ts +2 -0
- package/esm/mod.js +2 -0
- package/esm/src/formats/apng.d.ts +13 -1
- package/esm/src/formats/apng.js +97 -0
- package/esm/src/formats/ascii.d.ts +11 -1
- package/esm/src/formats/ascii.js +24 -0
- package/esm/src/formats/avif.d.ts +96 -0
- package/esm/src/formats/avif.js +607 -0
- package/esm/src/formats/bmp.d.ts +11 -1
- package/esm/src/formats/bmp.js +73 -0
- package/esm/src/formats/dng.d.ts +13 -1
- package/esm/src/formats/dng.js +26 -4
- package/esm/src/formats/gif.d.ts +15 -2
- package/esm/src/formats/gif.js +146 -4
- package/esm/src/formats/heic.d.ts +96 -0
- package/esm/src/formats/heic.js +608 -0
- package/esm/src/formats/ico.d.ts +11 -1
- package/esm/src/formats/ico.js +28 -0
- package/esm/src/formats/jpeg.d.ts +19 -1
- package/esm/src/formats/jpeg.js +709 -4
- package/esm/src/formats/pam.d.ts +11 -1
- package/esm/src/formats/pam.js +66 -0
- package/esm/src/formats/pcx.d.ts +11 -1
- package/esm/src/formats/pcx.js +45 -0
- package/esm/src/formats/png.d.ts +13 -1
- package/esm/src/formats/png.js +87 -0
- package/esm/src/formats/png_base.d.ts +8 -0
- package/esm/src/formats/png_base.js +176 -3
- package/esm/src/formats/ppm.d.ts +11 -1
- package/esm/src/formats/ppm.js +34 -0
- package/esm/src/formats/tiff.d.ts +13 -1
- package/esm/src/formats/tiff.js +165 -0
- package/esm/src/formats/webp.d.ts +16 -2
- package/esm/src/formats/webp.js +303 -62
- package/esm/src/image.d.ts +60 -0
- package/esm/src/image.js +253 -5
- package/esm/src/types.d.ts +59 -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 +18 -1
- package/script/mod.d.ts +2 -0
- package/script/mod.js +5 -1
- package/script/src/formats/apng.d.ts +13 -1
- package/script/src/formats/apng.js +97 -0
- package/script/src/formats/ascii.d.ts +11 -1
- package/script/src/formats/ascii.js +24 -0
- package/script/src/formats/avif.d.ts +96 -0
- package/script/src/formats/avif.js +611 -0
- package/script/src/formats/bmp.d.ts +11 -1
- package/script/src/formats/bmp.js +73 -0
- package/script/src/formats/dng.d.ts +13 -1
- package/script/src/formats/dng.js +26 -4
- package/script/src/formats/gif.d.ts +15 -2
- package/script/src/formats/gif.js +146 -4
- package/script/src/formats/heic.d.ts +96 -0
- package/script/src/formats/heic.js +612 -0
- package/script/src/formats/ico.d.ts +11 -1
- package/script/src/formats/ico.js +28 -0
- package/script/src/formats/jpeg.d.ts +19 -1
- package/script/src/formats/jpeg.js +709 -4
- package/script/src/formats/pam.d.ts +11 -1
- package/script/src/formats/pam.js +66 -0
- package/script/src/formats/pcx.d.ts +11 -1
- package/script/src/formats/pcx.js +45 -0
- package/script/src/formats/png.d.ts +13 -1
- package/script/src/formats/png.js +87 -0
- package/script/src/formats/png_base.d.ts +8 -0
- package/script/src/formats/png_base.js +176 -3
- package/script/src/formats/ppm.d.ts +11 -1
- package/script/src/formats/ppm.js +34 -0
- package/script/src/formats/tiff.d.ts +13 -1
- package/script/src/formats/tiff.js +165 -0
- package/script/src/formats/webp.d.ts +16 -2
- package/script/src/formats/webp.js +303 -62
- package/script/src/image.d.ts +60 -0
- package/script/src/image.js +251 -3
- package/script/src/types.d.ts +59 -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
|
@@ -305,6 +305,7 @@ class JPEGFormat {
|
|
|
305
305
|
const numEntries = littleEndian
|
|
306
306
|
? exifData[ifd0Offset] | (exifData[ifd0Offset + 1] << 8)
|
|
307
307
|
: (exifData[ifd0Offset] << 8) | exifData[ifd0Offset + 1];
|
|
308
|
+
let gpsIfdOffset = 0;
|
|
308
309
|
// Parse entries
|
|
309
310
|
for (let i = 0; i < numEntries; i++) {
|
|
310
311
|
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
@@ -381,12 +382,306 @@ class JPEGFormat {
|
|
|
381
382
|
}
|
|
382
383
|
}
|
|
383
384
|
}
|
|
385
|
+
// Make tag (0x010F)
|
|
386
|
+
if (tag === 0x010f) {
|
|
387
|
+
const valueOffset = littleEndian
|
|
388
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
389
|
+
(exifData[entryOffset + 10] << 16) |
|
|
390
|
+
(exifData[entryOffset + 11] << 24)
|
|
391
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
392
|
+
(exifData[entryOffset + 9] << 16) |
|
|
393
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
394
|
+
if (valueOffset < exifData.length) {
|
|
395
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
396
|
+
if (endIndex > valueOffset) {
|
|
397
|
+
metadata.cameraMake = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Model tag (0x0110)
|
|
402
|
+
if (tag === 0x0110) {
|
|
403
|
+
const valueOffset = littleEndian
|
|
404
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
405
|
+
(exifData[entryOffset + 10] << 16) |
|
|
406
|
+
(exifData[entryOffset + 11] << 24)
|
|
407
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
408
|
+
(exifData[entryOffset + 9] << 16) |
|
|
409
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
410
|
+
if (valueOffset < exifData.length) {
|
|
411
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
412
|
+
if (endIndex > valueOffset) {
|
|
413
|
+
metadata.cameraModel = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Orientation tag (0x0112)
|
|
418
|
+
if (tag === 0x0112) {
|
|
419
|
+
const value = littleEndian
|
|
420
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
421
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
422
|
+
metadata.orientation = value;
|
|
423
|
+
}
|
|
424
|
+
// Software tag (0x0131)
|
|
425
|
+
if (tag === 0x0131) {
|
|
426
|
+
const valueOffset = littleEndian
|
|
427
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
428
|
+
(exifData[entryOffset + 10] << 16) |
|
|
429
|
+
(exifData[entryOffset + 11] << 24)
|
|
430
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
431
|
+
(exifData[entryOffset + 9] << 16) |
|
|
432
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
433
|
+
if (valueOffset < exifData.length) {
|
|
434
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
435
|
+
if (endIndex > valueOffset) {
|
|
436
|
+
metadata.software = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// GPS IFD Pointer tag (0x8825)
|
|
441
|
+
if (tag === 0x8825) {
|
|
442
|
+
gpsIfdOffset = littleEndian
|
|
443
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
444
|
+
(exifData[entryOffset + 10] << 16) |
|
|
445
|
+
(exifData[entryOffset + 11] << 24)
|
|
446
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
447
|
+
(exifData[entryOffset + 9] << 16) |
|
|
448
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
449
|
+
}
|
|
450
|
+
// ExifIFD Pointer tag (0x8769) - points to EXIF Sub-IFD
|
|
451
|
+
const type = littleEndian
|
|
452
|
+
? exifData[entryOffset + 2] | (exifData[entryOffset + 3] << 8)
|
|
453
|
+
: (exifData[entryOffset + 2] << 8) | exifData[entryOffset + 3];
|
|
454
|
+
if (tag === 0x8769 && type === 4) {
|
|
455
|
+
const exifIfdOffset = littleEndian
|
|
456
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
457
|
+
(exifData[entryOffset + 10] << 16) |
|
|
458
|
+
(exifData[entryOffset + 11] << 24)
|
|
459
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
460
|
+
(exifData[entryOffset + 9] << 16) |
|
|
461
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
462
|
+
if (exifIfdOffset > 0 && exifIfdOffset + 2 <= exifData.length) {
|
|
463
|
+
this.parseExifSubIFD(exifData, exifIfdOffset, littleEndian, metadata);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Parse GPS IFD if present
|
|
468
|
+
if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= exifData.length) {
|
|
469
|
+
this.parseGPSIFD(exifData, gpsIfdOffset, littleEndian, metadata);
|
|
384
470
|
}
|
|
385
471
|
}
|
|
386
472
|
catch (_e) {
|
|
387
473
|
// Ignore EXIF parsing errors
|
|
388
474
|
}
|
|
389
475
|
}
|
|
476
|
+
parseExifSubIFD(exifData, exifIfdOffset, littleEndian, metadata) {
|
|
477
|
+
try {
|
|
478
|
+
const numEntries = littleEndian
|
|
479
|
+
? exifData[exifIfdOffset] | (exifData[exifIfdOffset + 1] << 8)
|
|
480
|
+
: (exifData[exifIfdOffset] << 8) | exifData[exifIfdOffset + 1];
|
|
481
|
+
for (let i = 0; i < numEntries; i++) {
|
|
482
|
+
const entryOffset = exifIfdOffset + 2 + i * 12;
|
|
483
|
+
if (entryOffset + 12 > exifData.length)
|
|
484
|
+
break;
|
|
485
|
+
const tag = littleEndian
|
|
486
|
+
? exifData[entryOffset] | (exifData[entryOffset + 1] << 8)
|
|
487
|
+
: (exifData[entryOffset] << 8) | exifData[entryOffset + 1];
|
|
488
|
+
const type = littleEndian
|
|
489
|
+
? exifData[entryOffset + 2] | (exifData[entryOffset + 3] << 8)
|
|
490
|
+
: (exifData[entryOffset + 2] << 8) | exifData[entryOffset + 3];
|
|
491
|
+
// ExposureTime tag (0x829A) - RATIONAL
|
|
492
|
+
if (tag === 0x829a && type === 5) {
|
|
493
|
+
const valueOffset = littleEndian
|
|
494
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
495
|
+
(exifData[entryOffset + 10] << 16) |
|
|
496
|
+
(exifData[entryOffset + 11] << 24)
|
|
497
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
498
|
+
(exifData[entryOffset + 9] << 16) |
|
|
499
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
500
|
+
if (valueOffset + 8 <= exifData.length) {
|
|
501
|
+
metadata.exposureTime = this.readRational(exifData, valueOffset, littleEndian);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// FNumber tag (0x829D) - RATIONAL
|
|
505
|
+
if (tag === 0x829d && type === 5) {
|
|
506
|
+
const valueOffset = littleEndian
|
|
507
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
508
|
+
(exifData[entryOffset + 10] << 16) |
|
|
509
|
+
(exifData[entryOffset + 11] << 24)
|
|
510
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
511
|
+
(exifData[entryOffset + 9] << 16) |
|
|
512
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
513
|
+
if (valueOffset + 8 <= exifData.length) {
|
|
514
|
+
metadata.fNumber = this.readRational(exifData, valueOffset, littleEndian);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// ISOSpeedRatings tag (0x8827) - SHORT
|
|
518
|
+
if (tag === 0x8827 && type === 3) {
|
|
519
|
+
metadata.iso = littleEndian
|
|
520
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
521
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
522
|
+
}
|
|
523
|
+
// FocalLength tag (0x920A) - RATIONAL
|
|
524
|
+
if (tag === 0x920a && type === 5) {
|
|
525
|
+
const valueOffset = littleEndian
|
|
526
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
527
|
+
(exifData[entryOffset + 10] << 16) |
|
|
528
|
+
(exifData[entryOffset + 11] << 24)
|
|
529
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
530
|
+
(exifData[entryOffset + 9] << 16) |
|
|
531
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
532
|
+
if (valueOffset + 8 <= exifData.length) {
|
|
533
|
+
metadata.focalLength = this.readRational(exifData, valueOffset, littleEndian);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// UserComment tag (0x9286) - UNDEFINED
|
|
537
|
+
if (tag === 0x9286) {
|
|
538
|
+
const count = littleEndian
|
|
539
|
+
? exifData[entryOffset + 4] | (exifData[entryOffset + 5] << 8) |
|
|
540
|
+
(exifData[entryOffset + 6] << 16) |
|
|
541
|
+
(exifData[entryOffset + 7] << 24)
|
|
542
|
+
: (exifData[entryOffset + 4] << 24) |
|
|
543
|
+
(exifData[entryOffset + 5] << 16) |
|
|
544
|
+
(exifData[entryOffset + 6] << 8) | exifData[entryOffset + 7];
|
|
545
|
+
if (count > 8) {
|
|
546
|
+
const valueOffset = littleEndian
|
|
547
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
548
|
+
(exifData[entryOffset + 10] << 16) |
|
|
549
|
+
(exifData[entryOffset + 11] << 24)
|
|
550
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
551
|
+
(exifData[entryOffset + 9] << 16) |
|
|
552
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
553
|
+
if (valueOffset + count <= exifData.length) {
|
|
554
|
+
// Skip 8-byte character code prefix
|
|
555
|
+
const commentData = exifData.slice(valueOffset + 8, valueOffset + count);
|
|
556
|
+
metadata.userComment = new TextDecoder().decode(commentData)
|
|
557
|
+
.replace(/\0+$/, "");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Flash tag (0x9209) - SHORT
|
|
562
|
+
if (tag === 0x9209 && type === 3) {
|
|
563
|
+
metadata.flash = littleEndian
|
|
564
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
565
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
566
|
+
}
|
|
567
|
+
// WhiteBalance tag (0xA403) - SHORT
|
|
568
|
+
if (tag === 0xa403 && type === 3) {
|
|
569
|
+
metadata.whiteBalance = littleEndian
|
|
570
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
571
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
572
|
+
}
|
|
573
|
+
// LensMake tag (0xA433) - ASCII
|
|
574
|
+
if (tag === 0xa433 && type === 2) {
|
|
575
|
+
const valueOffset = littleEndian
|
|
576
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
577
|
+
(exifData[entryOffset + 10] << 16) |
|
|
578
|
+
(exifData[entryOffset + 11] << 24)
|
|
579
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
580
|
+
(exifData[entryOffset + 9] << 16) |
|
|
581
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
582
|
+
if (valueOffset < exifData.length) {
|
|
583
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
584
|
+
if (endIndex > valueOffset) {
|
|
585
|
+
metadata.lensMake = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// LensModel tag (0xA434) - ASCII
|
|
590
|
+
if (tag === 0xa434 && type === 2) {
|
|
591
|
+
const valueOffset = littleEndian
|
|
592
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
593
|
+
(exifData[entryOffset + 10] << 16) |
|
|
594
|
+
(exifData[entryOffset + 11] << 24)
|
|
595
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
596
|
+
(exifData[entryOffset + 9] << 16) |
|
|
597
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
598
|
+
if (valueOffset < exifData.length) {
|
|
599
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
600
|
+
if (endIndex > valueOffset) {
|
|
601
|
+
metadata.lensModel = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (_e) {
|
|
608
|
+
// Ignore EXIF Sub-IFD parsing errors
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
parseGPSIFD(exifData, gpsIfdOffset, littleEndian, metadata) {
|
|
612
|
+
try {
|
|
613
|
+
const numEntries = littleEndian
|
|
614
|
+
? exifData[gpsIfdOffset] | (exifData[gpsIfdOffset + 1] << 8)
|
|
615
|
+
: (exifData[gpsIfdOffset] << 8) | exifData[gpsIfdOffset + 1];
|
|
616
|
+
let latRef = "";
|
|
617
|
+
let lonRef = "";
|
|
618
|
+
let latitude;
|
|
619
|
+
let longitude;
|
|
620
|
+
for (let i = 0; i < numEntries; i++) {
|
|
621
|
+
const entryOffset = gpsIfdOffset + 2 + i * 12;
|
|
622
|
+
if (entryOffset + 12 > exifData.length)
|
|
623
|
+
break;
|
|
624
|
+
const tag = littleEndian
|
|
625
|
+
? exifData[entryOffset] | (exifData[entryOffset + 1] << 8)
|
|
626
|
+
: (exifData[entryOffset] << 8) | exifData[entryOffset + 1];
|
|
627
|
+
const type = littleEndian
|
|
628
|
+
? exifData[entryOffset + 2] | (exifData[entryOffset + 3] << 8)
|
|
629
|
+
: (exifData[entryOffset + 2] << 8) | exifData[entryOffset + 3];
|
|
630
|
+
const valueOffset = littleEndian
|
|
631
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
632
|
+
(exifData[entryOffset + 10] << 16) |
|
|
633
|
+
(exifData[entryOffset + 11] << 24)
|
|
634
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
635
|
+
(exifData[entryOffset + 9] << 16) |
|
|
636
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
637
|
+
// GPSLatitudeRef (0x0001) - 'N' or 'S'
|
|
638
|
+
if (tag === 0x0001 && type === 2) {
|
|
639
|
+
latRef = String.fromCharCode(exifData[entryOffset + 8]);
|
|
640
|
+
}
|
|
641
|
+
// GPSLatitude (0x0002) - three rationals: degrees, minutes, seconds
|
|
642
|
+
if (tag === 0x0002 && type === 5 && valueOffset + 24 <= exifData.length) {
|
|
643
|
+
const degrees = this.readRational(exifData, valueOffset, littleEndian);
|
|
644
|
+
const minutes = this.readRational(exifData, valueOffset + 8, littleEndian);
|
|
645
|
+
const seconds = this.readRational(exifData, valueOffset + 16, littleEndian);
|
|
646
|
+
latitude = degrees + minutes / 60 + seconds / 3600;
|
|
647
|
+
}
|
|
648
|
+
// GPSLongitudeRef (0x0003) - 'E' or 'W'
|
|
649
|
+
if (tag === 0x0003 && type === 2) {
|
|
650
|
+
lonRef = String.fromCharCode(exifData[entryOffset + 8]);
|
|
651
|
+
}
|
|
652
|
+
// GPSLongitude (0x0004) - three rationals: degrees, minutes, seconds
|
|
653
|
+
if (tag === 0x0004 && type === 5 && valueOffset + 24 <= exifData.length) {
|
|
654
|
+
const degrees = this.readRational(exifData, valueOffset, littleEndian);
|
|
655
|
+
const minutes = this.readRational(exifData, valueOffset + 8, littleEndian);
|
|
656
|
+
const seconds = this.readRational(exifData, valueOffset + 16, littleEndian);
|
|
657
|
+
longitude = degrees + minutes / 60 + seconds / 3600;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Apply hemisphere references
|
|
661
|
+
if (latitude !== undefined && latRef) {
|
|
662
|
+
metadata.latitude = latRef === "S" ? -latitude : latitude;
|
|
663
|
+
}
|
|
664
|
+
if (longitude !== undefined && lonRef) {
|
|
665
|
+
metadata.longitude = lonRef === "W" ? -longitude : longitude;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (_e) {
|
|
669
|
+
// Ignore GPS parsing errors
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
readRational(data, offset, littleEndian) {
|
|
673
|
+
const numerator = littleEndian
|
|
674
|
+
? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
|
|
675
|
+
(data[offset + 3] << 24)
|
|
676
|
+
: (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
677
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
678
|
+
const denominator = littleEndian
|
|
679
|
+
? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
|
|
680
|
+
(data[offset + 7] << 24)
|
|
681
|
+
: (data[offset + 4] << 24) | (data[offset + 5] << 16) |
|
|
682
|
+
(data[offset + 6] << 8) | data[offset + 7];
|
|
683
|
+
return denominator !== 0 ? numerator / denominator : 0;
|
|
684
|
+
}
|
|
390
685
|
createEXIFData(metadata) {
|
|
391
686
|
const entries = [];
|
|
392
687
|
// Add DateTime if available
|
|
@@ -423,7 +718,55 @@ class JPEGFormat {
|
|
|
423
718
|
value: new TextEncoder().encode(metadata.copyright + "\0"),
|
|
424
719
|
});
|
|
425
720
|
}
|
|
426
|
-
|
|
721
|
+
// Add Make (camera manufacturer)
|
|
722
|
+
if (metadata.cameraMake) {
|
|
723
|
+
entries.push({
|
|
724
|
+
tag: 0x010f,
|
|
725
|
+
type: 2,
|
|
726
|
+
value: new TextEncoder().encode(metadata.cameraMake + "\0"),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
// Add Model (camera model)
|
|
730
|
+
if (metadata.cameraModel) {
|
|
731
|
+
entries.push({
|
|
732
|
+
tag: 0x0110,
|
|
733
|
+
type: 2,
|
|
734
|
+
value: new TextEncoder().encode(metadata.cameraModel + "\0"),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// Add Orientation
|
|
738
|
+
if (metadata.orientation !== undefined) {
|
|
739
|
+
const orientationBytes = new Uint8Array(2);
|
|
740
|
+
orientationBytes[0] = metadata.orientation & 0xff;
|
|
741
|
+
orientationBytes[1] = (metadata.orientation >> 8) & 0xff;
|
|
742
|
+
entries.push({
|
|
743
|
+
tag: 0x0112,
|
|
744
|
+
type: 3, // SHORT
|
|
745
|
+
value: orientationBytes,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
// Add Software
|
|
749
|
+
if (metadata.software) {
|
|
750
|
+
entries.push({
|
|
751
|
+
tag: 0x0131,
|
|
752
|
+
type: 2,
|
|
753
|
+
value: new TextEncoder().encode(metadata.software + "\0"),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// Check if we have GPS data
|
|
757
|
+
const hasGPS = metadata.latitude !== undefined &&
|
|
758
|
+
metadata.longitude !== undefined;
|
|
759
|
+
// Check if we have Exif Sub-IFD data
|
|
760
|
+
const hasExifSubIFD = metadata.iso !== undefined ||
|
|
761
|
+
metadata.exposureTime !== undefined ||
|
|
762
|
+
metadata.fNumber !== undefined ||
|
|
763
|
+
metadata.focalLength !== undefined ||
|
|
764
|
+
metadata.flash !== undefined ||
|
|
765
|
+
metadata.whiteBalance !== undefined ||
|
|
766
|
+
metadata.lensMake !== undefined ||
|
|
767
|
+
metadata.lensModel !== undefined ||
|
|
768
|
+
metadata.userComment !== undefined;
|
|
769
|
+
if (entries.length === 0 && !hasGPS && !hasExifSubIFD)
|
|
427
770
|
return [];
|
|
428
771
|
// Build EXIF structure
|
|
429
772
|
const exif = [];
|
|
@@ -432,10 +775,12 @@ class JPEGFormat {
|
|
|
432
775
|
exif.push(0x2a, 0x00); // 42
|
|
433
776
|
// Offset to IFD0 (8 bytes from start)
|
|
434
777
|
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
435
|
-
// Number of entries
|
|
436
|
-
|
|
778
|
+
// Number of entries (add GPS IFD pointer and Exif Sub-IFD pointer if needed)
|
|
779
|
+
const ifd0Entries = entries.length + (hasGPS ? 1 : 0) +
|
|
780
|
+
(hasExifSubIFD ? 1 : 0);
|
|
781
|
+
exif.push(ifd0Entries & 0xff, (ifd0Entries >> 8) & 0xff);
|
|
437
782
|
// Calculate data offset
|
|
438
|
-
let dataOffset = 8 + 2 +
|
|
783
|
+
let dataOffset = 8 + 2 + ifd0Entries * 12 + 4;
|
|
439
784
|
for (const entry of entries) {
|
|
440
785
|
// Tag
|
|
441
786
|
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
@@ -455,6 +800,26 @@ class JPEGFormat {
|
|
|
455
800
|
dataOffset += entry.value.length;
|
|
456
801
|
}
|
|
457
802
|
}
|
|
803
|
+
// Add Exif Sub-IFD pointer if we have camera metadata
|
|
804
|
+
let exifSubIfdOffset = 0;
|
|
805
|
+
if (hasExifSubIFD) {
|
|
806
|
+
exifSubIfdOffset = dataOffset;
|
|
807
|
+
// Exif IFD Pointer tag (0x8769), type 4 (LONG), count 1
|
|
808
|
+
exif.push(0x69, 0x87); // Tag
|
|
809
|
+
exif.push(0x04, 0x00); // Type
|
|
810
|
+
exif.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
811
|
+
exif.push(exifSubIfdOffset & 0xff, (exifSubIfdOffset >> 8) & 0xff, (exifSubIfdOffset >> 16) & 0xff, (exifSubIfdOffset >> 24) & 0xff);
|
|
812
|
+
}
|
|
813
|
+
// Add GPS IFD pointer if we have GPS data
|
|
814
|
+
let gpsIfdOffset = 0;
|
|
815
|
+
if (hasGPS) {
|
|
816
|
+
gpsIfdOffset = dataOffset;
|
|
817
|
+
// GPS IFD Pointer tag (0x8825), type 4 (LONG), count 1
|
|
818
|
+
exif.push(0x25, 0x88); // Tag
|
|
819
|
+
exif.push(0x04, 0x00); // Type
|
|
820
|
+
exif.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
821
|
+
exif.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
|
|
822
|
+
}
|
|
458
823
|
// Next IFD offset (0 = no more IFDs)
|
|
459
824
|
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
460
825
|
// Append data for entries that didn't fit in value field
|
|
@@ -465,7 +830,347 @@ class JPEGFormat {
|
|
|
465
830
|
}
|
|
466
831
|
}
|
|
467
832
|
}
|
|
833
|
+
// Add Exif Sub-IFD if we have camera metadata
|
|
834
|
+
if (hasExifSubIFD) {
|
|
835
|
+
const exifSubIfd = this.createExifSubIFD(metadata, exifSubIfdOffset);
|
|
836
|
+
for (const byte of exifSubIfd) {
|
|
837
|
+
exif.push(byte);
|
|
838
|
+
}
|
|
839
|
+
// Update GPS offset since we added the Sub-IFD
|
|
840
|
+
if (hasGPS) {
|
|
841
|
+
gpsIfdOffset = exif.length;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Add GPS IFD if we have GPS data
|
|
845
|
+
if (hasGPS) {
|
|
846
|
+
const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
|
|
847
|
+
for (const byte of gpsIfd) {
|
|
848
|
+
exif.push(byte);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
468
851
|
return exif;
|
|
469
852
|
}
|
|
853
|
+
createGPSIFD(metadata, gpsIfdStart) {
|
|
854
|
+
const gps = [];
|
|
855
|
+
// We'll create 4 GPS entries: LatitudeRef, Latitude, LongitudeRef, Longitude
|
|
856
|
+
const numEntries = 4;
|
|
857
|
+
gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
|
|
858
|
+
const latitude = metadata.latitude;
|
|
859
|
+
const longitude = metadata.longitude;
|
|
860
|
+
// Convert to absolute values for DMS calculation
|
|
861
|
+
const absLat = Math.abs(latitude);
|
|
862
|
+
const absLon = Math.abs(longitude);
|
|
863
|
+
// Calculate degrees, minutes, seconds
|
|
864
|
+
const latDeg = Math.floor(absLat);
|
|
865
|
+
const latMin = Math.floor((absLat - latDeg) * 60);
|
|
866
|
+
const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
|
|
867
|
+
const lonDeg = Math.floor(absLon);
|
|
868
|
+
const lonMin = Math.floor((absLon - lonDeg) * 60);
|
|
869
|
+
const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
|
|
870
|
+
// Calculate offset for rational data (relative to start of EXIF data, not GPS IFD)
|
|
871
|
+
let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
|
|
872
|
+
// Entry 1: GPSLatitudeRef (tag 0x0001)
|
|
873
|
+
gps.push(0x01, 0x00); // Tag
|
|
874
|
+
gps.push(0x02, 0x00); // Type (ASCII)
|
|
875
|
+
gps.push(0x02, 0x00, 0x00, 0x00); // Count (2 bytes including null)
|
|
876
|
+
// Value stored inline: 'N' or 'S' + null
|
|
877
|
+
gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00); // 'N' = 78, 'S' = 83
|
|
878
|
+
// Entry 2: GPSLatitude (tag 0x0002)
|
|
879
|
+
gps.push(0x02, 0x00); // Tag
|
|
880
|
+
gps.push(0x05, 0x00); // Type (RATIONAL)
|
|
881
|
+
gps.push(0x03, 0x00, 0x00, 0x00); // Count (3 rationals)
|
|
882
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
883
|
+
dataOffset += 24; // 3 rationals * 8 bytes
|
|
884
|
+
// Entry 3: GPSLongitudeRef (tag 0x0003)
|
|
885
|
+
gps.push(0x03, 0x00); // Tag
|
|
886
|
+
gps.push(0x02, 0x00); // Type (ASCII)
|
|
887
|
+
gps.push(0x02, 0x00, 0x00, 0x00); // Count
|
|
888
|
+
gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00); // 'E' = 69, 'W' = 87
|
|
889
|
+
// Entry 4: GPSLongitude (tag 0x0004)
|
|
890
|
+
gps.push(0x04, 0x00); // Tag
|
|
891
|
+
gps.push(0x05, 0x00); // Type (RATIONAL)
|
|
892
|
+
gps.push(0x03, 0x00, 0x00, 0x00); // Count
|
|
893
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
894
|
+
// Next IFD offset (0 = no more IFDs)
|
|
895
|
+
gps.push(0x00, 0x00, 0x00, 0x00);
|
|
896
|
+
// Write latitude rationals (degrees, minutes, seconds)
|
|
897
|
+
this.writeRational(gps, latDeg, 1);
|
|
898
|
+
this.writeRational(gps, latMin, 1);
|
|
899
|
+
this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
|
|
900
|
+
// Write longitude rationals
|
|
901
|
+
this.writeRational(gps, lonDeg, 1);
|
|
902
|
+
this.writeRational(gps, lonMin, 1);
|
|
903
|
+
this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
|
|
904
|
+
return gps;
|
|
905
|
+
}
|
|
906
|
+
createExifSubIFD(metadata, exifIfdStart) {
|
|
907
|
+
const entries = [];
|
|
908
|
+
// ISO Speed Ratings (0x8827) - SHORT
|
|
909
|
+
if (metadata.iso !== undefined) {
|
|
910
|
+
entries.push({
|
|
911
|
+
tag: 0x8827,
|
|
912
|
+
type: 3,
|
|
913
|
+
data: [metadata.iso & 0xff, (metadata.iso >> 8) & 0xff],
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
// Exposure Time (0x829A) - RATIONAL
|
|
917
|
+
if (metadata.exposureTime !== undefined) {
|
|
918
|
+
entries.push({ tag: 0x829a, type: 5, data: [] }); // Will add offset later
|
|
919
|
+
}
|
|
920
|
+
// FNumber (0x829D) - RATIONAL
|
|
921
|
+
if (metadata.fNumber !== undefined) {
|
|
922
|
+
entries.push({ tag: 0x829d, type: 5, data: [] }); // Will add offset later
|
|
923
|
+
}
|
|
924
|
+
// Flash (0x9209) - SHORT
|
|
925
|
+
if (metadata.flash !== undefined) {
|
|
926
|
+
entries.push({
|
|
927
|
+
tag: 0x9209,
|
|
928
|
+
type: 3,
|
|
929
|
+
data: [metadata.flash & 0xff, (metadata.flash >> 8) & 0xff],
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
// Focal Length (0x920A) - RATIONAL
|
|
933
|
+
if (metadata.focalLength !== undefined) {
|
|
934
|
+
entries.push({ tag: 0x920a, type: 5, data: [] }); // Will add offset later
|
|
935
|
+
}
|
|
936
|
+
// User Comment (0x9286) - UNDEFINED
|
|
937
|
+
if (metadata.userComment !== undefined) {
|
|
938
|
+
entries.push({ tag: 0x9286, type: 7, data: [] }); // Will add offset later
|
|
939
|
+
}
|
|
940
|
+
// White Balance (0xA403) - SHORT
|
|
941
|
+
if (metadata.whiteBalance !== undefined) {
|
|
942
|
+
entries.push({
|
|
943
|
+
tag: 0xa403,
|
|
944
|
+
type: 3,
|
|
945
|
+
data: [
|
|
946
|
+
metadata.whiteBalance & 0xff,
|
|
947
|
+
(metadata.whiteBalance >> 8) & 0xff,
|
|
948
|
+
],
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
// Lens Make (0xA433) - ASCII
|
|
952
|
+
if (metadata.lensMake !== undefined) {
|
|
953
|
+
entries.push({ tag: 0xa433, type: 2, data: [] }); // Will add offset later
|
|
954
|
+
}
|
|
955
|
+
// Lens Model (0xA434) - ASCII
|
|
956
|
+
if (metadata.lensModel !== undefined) {
|
|
957
|
+
entries.push({ tag: 0xa434, type: 2, data: [] }); // Will add offset later
|
|
958
|
+
}
|
|
959
|
+
const exifSubIfd = [];
|
|
960
|
+
const numEntries = entries.length;
|
|
961
|
+
exifSubIfd.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
|
|
962
|
+
let dataOffset = exifIfdStart + 2 + numEntries * 12 + 4;
|
|
963
|
+
// Write entry headers
|
|
964
|
+
for (const entry of entries) {
|
|
965
|
+
exifSubIfd.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
966
|
+
exifSubIfd.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
967
|
+
if (entry.tag === 0x829a && metadata.exposureTime !== undefined) {
|
|
968
|
+
// Exposure Time - 1 RATIONAL
|
|
969
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
970
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
971
|
+
dataOffset += 8;
|
|
972
|
+
}
|
|
973
|
+
else if (entry.tag === 0x829d && metadata.fNumber !== undefined) {
|
|
974
|
+
// FNumber - 1 RATIONAL
|
|
975
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
976
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
977
|
+
dataOffset += 8;
|
|
978
|
+
}
|
|
979
|
+
else if (entry.tag === 0x920a && metadata.focalLength !== undefined) {
|
|
980
|
+
// Focal Length - 1 RATIONAL
|
|
981
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
982
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
983
|
+
dataOffset += 8;
|
|
984
|
+
}
|
|
985
|
+
else if (entry.tag === 0x9286 && metadata.userComment !== undefined) {
|
|
986
|
+
// User Comment - UNDEFINED with character code
|
|
987
|
+
const commentBytes = new TextEncoder().encode(metadata.userComment);
|
|
988
|
+
const totalLength = 8 + commentBytes.length; // 8 bytes for character code
|
|
989
|
+
exifSubIfd.push(totalLength & 0xff, (totalLength >> 8) & 0xff, (totalLength >> 16) & 0xff, (totalLength >> 24) & 0xff);
|
|
990
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
991
|
+
dataOffset += totalLength;
|
|
992
|
+
}
|
|
993
|
+
else if (entry.tag === 0xa433 && metadata.lensMake !== undefined) {
|
|
994
|
+
// Lens Make - ASCII
|
|
995
|
+
const bytes = new TextEncoder().encode(metadata.lensMake + "\0");
|
|
996
|
+
exifSubIfd.push(bytes.length & 0xff, (bytes.length >> 8) & 0xff, (bytes.length >> 16) & 0xff, (bytes.length >> 24) & 0xff);
|
|
997
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
998
|
+
dataOffset += bytes.length;
|
|
999
|
+
}
|
|
1000
|
+
else if (entry.tag === 0xa434 && metadata.lensModel !== undefined) {
|
|
1001
|
+
// Lens Model - ASCII
|
|
1002
|
+
const bytes = new TextEncoder().encode(metadata.lensModel + "\0");
|
|
1003
|
+
exifSubIfd.push(bytes.length & 0xff, (bytes.length >> 8) & 0xff, (bytes.length >> 16) & 0xff, (bytes.length >> 24) & 0xff);
|
|
1004
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
1005
|
+
dataOffset += bytes.length;
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
// SHORT types stored inline
|
|
1009
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
1010
|
+
exifSubIfd.push(...entry.data, 0x00, 0x00); // Value (4 bytes)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Next IFD offset
|
|
1014
|
+
exifSubIfd.push(0x00, 0x00, 0x00, 0x00);
|
|
1015
|
+
// Write data for RATIONAL and ASCII types
|
|
1016
|
+
for (const entry of entries) {
|
|
1017
|
+
if (entry.tag === 0x829a && metadata.exposureTime !== undefined) {
|
|
1018
|
+
// Convert exposure time to rational (numerator/denominator)
|
|
1019
|
+
const [num, den] = this.toRational(metadata.exposureTime);
|
|
1020
|
+
this.writeRational(exifSubIfd, num, den);
|
|
1021
|
+
}
|
|
1022
|
+
else if (entry.tag === 0x829d && metadata.fNumber !== undefined) {
|
|
1023
|
+
const [num, den] = this.toRational(metadata.fNumber);
|
|
1024
|
+
this.writeRational(exifSubIfd, num, den);
|
|
1025
|
+
}
|
|
1026
|
+
else if (entry.tag === 0x920a && metadata.focalLength !== undefined) {
|
|
1027
|
+
const [num, den] = this.toRational(metadata.focalLength);
|
|
1028
|
+
this.writeRational(exifSubIfd, num, den);
|
|
1029
|
+
}
|
|
1030
|
+
else if (entry.tag === 0x9286 && metadata.userComment !== undefined) {
|
|
1031
|
+
// Character code: ASCII (8 bytes)
|
|
1032
|
+
exifSubIfd.push(0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00);
|
|
1033
|
+
const commentBytes = new TextEncoder().encode(metadata.userComment);
|
|
1034
|
+
for (const byte of commentBytes) {
|
|
1035
|
+
exifSubIfd.push(byte);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
else if (entry.tag === 0xa433 && metadata.lensMake !== undefined) {
|
|
1039
|
+
const bytes = new TextEncoder().encode(metadata.lensMake + "\0");
|
|
1040
|
+
for (const byte of bytes) {
|
|
1041
|
+
exifSubIfd.push(byte);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
else if (entry.tag === 0xa434 && metadata.lensModel !== undefined) {
|
|
1045
|
+
const bytes = new TextEncoder().encode(metadata.lensModel + "\0");
|
|
1046
|
+
for (const byte of bytes) {
|
|
1047
|
+
exifSubIfd.push(byte);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return exifSubIfd;
|
|
1052
|
+
}
|
|
1053
|
+
toRational(value) {
|
|
1054
|
+
// Convert decimal to rational representation
|
|
1055
|
+
// Try to find a reasonable denominator
|
|
1056
|
+
const denominators = [1, 10, 100, 1000, 10000, 100000, 1000000];
|
|
1057
|
+
for (const den of denominators) {
|
|
1058
|
+
const num = Math.round(value * den);
|
|
1059
|
+
if (Math.abs(num / den - value) < 0.000001) {
|
|
1060
|
+
return [num, den];
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
// Fallback
|
|
1064
|
+
return [Math.round(value * 1000000), 1000000];
|
|
1065
|
+
}
|
|
1066
|
+
writeRational(output, numerator, denominator) {
|
|
1067
|
+
// Write as little endian
|
|
1068
|
+
output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
|
|
1069
|
+
output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Get the list of metadata fields supported by JPEG format
|
|
1073
|
+
*/
|
|
1074
|
+
getSupportedMetadata() {
|
|
1075
|
+
return [
|
|
1076
|
+
"creationDate",
|
|
1077
|
+
"description",
|
|
1078
|
+
"author",
|
|
1079
|
+
"copyright",
|
|
1080
|
+
"cameraMake",
|
|
1081
|
+
"cameraModel",
|
|
1082
|
+
"orientation",
|
|
1083
|
+
"software",
|
|
1084
|
+
"latitude",
|
|
1085
|
+
"longitude",
|
|
1086
|
+
"iso",
|
|
1087
|
+
"exposureTime",
|
|
1088
|
+
"fNumber",
|
|
1089
|
+
"focalLength",
|
|
1090
|
+
"flash",
|
|
1091
|
+
"whiteBalance",
|
|
1092
|
+
"lensMake",
|
|
1093
|
+
"lensModel",
|
|
1094
|
+
"userComment",
|
|
1095
|
+
"dpiX",
|
|
1096
|
+
"dpiY",
|
|
1097
|
+
];
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Extract metadata from JPEG data without fully decoding the pixel data
|
|
1101
|
+
* This quickly parses JFIF and EXIF markers to extract metadata
|
|
1102
|
+
* @param data Raw JPEG data
|
|
1103
|
+
* @returns Extracted metadata or undefined
|
|
1104
|
+
*/
|
|
1105
|
+
extractMetadata(data) {
|
|
1106
|
+
if (!this.canDecode(data)) {
|
|
1107
|
+
return Promise.resolve(undefined);
|
|
1108
|
+
}
|
|
1109
|
+
// Parse JPEG structure to extract metadata
|
|
1110
|
+
let pos = 2; // Skip initial FF D8
|
|
1111
|
+
const metadata = {
|
|
1112
|
+
format: "jpeg",
|
|
1113
|
+
compression: "dct",
|
|
1114
|
+
frameCount: 1,
|
|
1115
|
+
bitDepth: 8,
|
|
1116
|
+
colorType: "rgb",
|
|
1117
|
+
};
|
|
1118
|
+
let width = 0;
|
|
1119
|
+
let height = 0;
|
|
1120
|
+
while (pos < data.length - 1) {
|
|
1121
|
+
if (data[pos] !== 0xff) {
|
|
1122
|
+
pos++;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
const marker = data[pos + 1];
|
|
1126
|
+
pos += 2;
|
|
1127
|
+
// SOF markers (Start of Frame) - get dimensions for DPI calculation
|
|
1128
|
+
if (marker >= 0xc0 && marker <= 0xcf &&
|
|
1129
|
+
marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
1130
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1131
|
+
// precision at pos+2
|
|
1132
|
+
const precision = data[pos + 2];
|
|
1133
|
+
if (precision && precision !== 8) {
|
|
1134
|
+
metadata.bitDepth = precision;
|
|
1135
|
+
}
|
|
1136
|
+
height = (data[pos + 3] << 8) | data[pos + 4];
|
|
1137
|
+
width = (data[pos + 5] << 8) | data[pos + 6];
|
|
1138
|
+
// Check number of components
|
|
1139
|
+
const numComponents = data[pos + 7];
|
|
1140
|
+
if (numComponents === 1) {
|
|
1141
|
+
metadata.colorType = "grayscale";
|
|
1142
|
+
}
|
|
1143
|
+
// Don't break - continue parsing for metadata
|
|
1144
|
+
pos += length;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
// APP0 marker (JFIF)
|
|
1148
|
+
if (marker === 0xe0) {
|
|
1149
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1150
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
1151
|
+
this.parseJFIF(appData, metadata, width, height);
|
|
1152
|
+
pos += length;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
// APP1 marker (EXIF)
|
|
1156
|
+
if (marker === 0xe1) {
|
|
1157
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1158
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
1159
|
+
this.parseEXIF(appData, metadata);
|
|
1160
|
+
pos += length;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
// Skip other markers
|
|
1164
|
+
if (marker === 0xd9 || marker === 0xda)
|
|
1165
|
+
break; // EOI or SOS
|
|
1166
|
+
if (marker >= 0xd0 && marker <= 0xd8)
|
|
1167
|
+
continue; // RST markers have no length
|
|
1168
|
+
if (marker === 0x01)
|
|
1169
|
+
continue; // TEM has no length
|
|
1170
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1171
|
+
pos += length;
|
|
1172
|
+
}
|
|
1173
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
1174
|
+
}
|
|
470
1175
|
}
|
|
471
1176
|
exports.JPEGFormat = JPEGFormat;
|