cross-image 0.2.1 → 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 +160 -32
- package/esm/mod.d.ts +2 -1
- package/esm/mod.js +2 -1
- 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/ppm.d.ts +50 -0
- package/esm/src/formats/ppm.js +242 -0
- package/esm/src/formats/tiff.d.ts +10 -1
- package/esm/src/formats/tiff.js +194 -44
- package/esm/src/formats/webp.d.ts +9 -2
- package/esm/src/formats/webp.js +211 -62
- package/esm/src/image.d.ts +81 -0
- package/esm/src/image.js +282 -5
- package/esm/src/types.d.ts +41 -1
- package/esm/src/utils/image_processing.d.ts +98 -0
- package/esm/src/utils/image_processing.js +440 -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/mod.d.ts +2 -1
- package/script/mod.js +4 -2
- 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/ppm.d.ts +50 -0
- package/script/src/formats/ppm.js +246 -0
- package/script/src/formats/tiff.d.ts +10 -1
- package/script/src/formats/tiff.js +194 -44
- package/script/src/formats/webp.d.ts +9 -2
- package/script/src/formats/webp.js +211 -62
- package/script/src/image.d.ts +81 -0
- package/script/src/image.js +280 -3
- package/script/src/types.d.ts +41 -1
- package/script/src/utils/image_processing.d.ts +98 -0
- package/script/src/utils/image_processing.js +451 -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
package/esm/src/formats/jpeg.js
CHANGED
|
@@ -269,6 +269,7 @@ export class JPEGFormat {
|
|
|
269
269
|
const numEntries = littleEndian
|
|
270
270
|
? exifData[ifd0Offset] | (exifData[ifd0Offset + 1] << 8)
|
|
271
271
|
: (exifData[ifd0Offset] << 8) | exifData[ifd0Offset + 1];
|
|
272
|
+
let gpsIfdOffset = 0;
|
|
272
273
|
// Parse entries
|
|
273
274
|
for (let i = 0; i < numEntries; i++) {
|
|
274
275
|
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
@@ -345,12 +346,306 @@ export class JPEGFormat {
|
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
}
|
|
349
|
+
// Make tag (0x010F)
|
|
350
|
+
if (tag === 0x010f) {
|
|
351
|
+
const valueOffset = littleEndian
|
|
352
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
353
|
+
(exifData[entryOffset + 10] << 16) |
|
|
354
|
+
(exifData[entryOffset + 11] << 24)
|
|
355
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
356
|
+
(exifData[entryOffset + 9] << 16) |
|
|
357
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
358
|
+
if (valueOffset < exifData.length) {
|
|
359
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
360
|
+
if (endIndex > valueOffset) {
|
|
361
|
+
metadata.cameraMake = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Model tag (0x0110)
|
|
366
|
+
if (tag === 0x0110) {
|
|
367
|
+
const valueOffset = littleEndian
|
|
368
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
369
|
+
(exifData[entryOffset + 10] << 16) |
|
|
370
|
+
(exifData[entryOffset + 11] << 24)
|
|
371
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
372
|
+
(exifData[entryOffset + 9] << 16) |
|
|
373
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
374
|
+
if (valueOffset < exifData.length) {
|
|
375
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
376
|
+
if (endIndex > valueOffset) {
|
|
377
|
+
metadata.cameraModel = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Orientation tag (0x0112)
|
|
382
|
+
if (tag === 0x0112) {
|
|
383
|
+
const value = littleEndian
|
|
384
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
385
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
386
|
+
metadata.orientation = value;
|
|
387
|
+
}
|
|
388
|
+
// Software tag (0x0131)
|
|
389
|
+
if (tag === 0x0131) {
|
|
390
|
+
const valueOffset = littleEndian
|
|
391
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
392
|
+
(exifData[entryOffset + 10] << 16) |
|
|
393
|
+
(exifData[entryOffset + 11] << 24)
|
|
394
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
395
|
+
(exifData[entryOffset + 9] << 16) |
|
|
396
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
397
|
+
if (valueOffset < exifData.length) {
|
|
398
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
399
|
+
if (endIndex > valueOffset) {
|
|
400
|
+
metadata.software = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
// GPS IFD Pointer tag (0x8825)
|
|
405
|
+
if (tag === 0x8825) {
|
|
406
|
+
gpsIfdOffset = littleEndian
|
|
407
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
408
|
+
(exifData[entryOffset + 10] << 16) |
|
|
409
|
+
(exifData[entryOffset + 11] << 24)
|
|
410
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
411
|
+
(exifData[entryOffset + 9] << 16) |
|
|
412
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
413
|
+
}
|
|
414
|
+
// ExifIFD Pointer tag (0x8769) - points to EXIF Sub-IFD
|
|
415
|
+
const type = littleEndian
|
|
416
|
+
? exifData[entryOffset + 2] | (exifData[entryOffset + 3] << 8)
|
|
417
|
+
: (exifData[entryOffset + 2] << 8) | exifData[entryOffset + 3];
|
|
418
|
+
if (tag === 0x8769 && type === 4) {
|
|
419
|
+
const exifIfdOffset = littleEndian
|
|
420
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
421
|
+
(exifData[entryOffset + 10] << 16) |
|
|
422
|
+
(exifData[entryOffset + 11] << 24)
|
|
423
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
424
|
+
(exifData[entryOffset + 9] << 16) |
|
|
425
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
426
|
+
if (exifIfdOffset > 0 && exifIfdOffset + 2 <= exifData.length) {
|
|
427
|
+
this.parseExifSubIFD(exifData, exifIfdOffset, littleEndian, metadata);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Parse GPS IFD if present
|
|
432
|
+
if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= exifData.length) {
|
|
433
|
+
this.parseGPSIFD(exifData, gpsIfdOffset, littleEndian, metadata);
|
|
348
434
|
}
|
|
349
435
|
}
|
|
350
436
|
catch (_e) {
|
|
351
437
|
// Ignore EXIF parsing errors
|
|
352
438
|
}
|
|
353
439
|
}
|
|
440
|
+
parseExifSubIFD(exifData, exifIfdOffset, littleEndian, metadata) {
|
|
441
|
+
try {
|
|
442
|
+
const numEntries = littleEndian
|
|
443
|
+
? exifData[exifIfdOffset] | (exifData[exifIfdOffset + 1] << 8)
|
|
444
|
+
: (exifData[exifIfdOffset] << 8) | exifData[exifIfdOffset + 1];
|
|
445
|
+
for (let i = 0; i < numEntries; i++) {
|
|
446
|
+
const entryOffset = exifIfdOffset + 2 + i * 12;
|
|
447
|
+
if (entryOffset + 12 > exifData.length)
|
|
448
|
+
break;
|
|
449
|
+
const tag = littleEndian
|
|
450
|
+
? exifData[entryOffset] | (exifData[entryOffset + 1] << 8)
|
|
451
|
+
: (exifData[entryOffset] << 8) | exifData[entryOffset + 1];
|
|
452
|
+
const type = littleEndian
|
|
453
|
+
? exifData[entryOffset + 2] | (exifData[entryOffset + 3] << 8)
|
|
454
|
+
: (exifData[entryOffset + 2] << 8) | exifData[entryOffset + 3];
|
|
455
|
+
// ExposureTime tag (0x829A) - RATIONAL
|
|
456
|
+
if (tag === 0x829a && type === 5) {
|
|
457
|
+
const valueOffset = littleEndian
|
|
458
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
459
|
+
(exifData[entryOffset + 10] << 16) |
|
|
460
|
+
(exifData[entryOffset + 11] << 24)
|
|
461
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
462
|
+
(exifData[entryOffset + 9] << 16) |
|
|
463
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
464
|
+
if (valueOffset + 8 <= exifData.length) {
|
|
465
|
+
metadata.exposureTime = this.readRational(exifData, valueOffset, littleEndian);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// FNumber tag (0x829D) - RATIONAL
|
|
469
|
+
if (tag === 0x829d && type === 5) {
|
|
470
|
+
const valueOffset = littleEndian
|
|
471
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
472
|
+
(exifData[entryOffset + 10] << 16) |
|
|
473
|
+
(exifData[entryOffset + 11] << 24)
|
|
474
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
475
|
+
(exifData[entryOffset + 9] << 16) |
|
|
476
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
477
|
+
if (valueOffset + 8 <= exifData.length) {
|
|
478
|
+
metadata.fNumber = this.readRational(exifData, valueOffset, littleEndian);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// ISOSpeedRatings tag (0x8827) - SHORT
|
|
482
|
+
if (tag === 0x8827 && type === 3) {
|
|
483
|
+
metadata.iso = littleEndian
|
|
484
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
485
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
486
|
+
}
|
|
487
|
+
// FocalLength tag (0x920A) - RATIONAL
|
|
488
|
+
if (tag === 0x920a && type === 5) {
|
|
489
|
+
const valueOffset = littleEndian
|
|
490
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
491
|
+
(exifData[entryOffset + 10] << 16) |
|
|
492
|
+
(exifData[entryOffset + 11] << 24)
|
|
493
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
494
|
+
(exifData[entryOffset + 9] << 16) |
|
|
495
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
496
|
+
if (valueOffset + 8 <= exifData.length) {
|
|
497
|
+
metadata.focalLength = this.readRational(exifData, valueOffset, littleEndian);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// UserComment tag (0x9286) - UNDEFINED
|
|
501
|
+
if (tag === 0x9286) {
|
|
502
|
+
const count = littleEndian
|
|
503
|
+
? exifData[entryOffset + 4] | (exifData[entryOffset + 5] << 8) |
|
|
504
|
+
(exifData[entryOffset + 6] << 16) |
|
|
505
|
+
(exifData[entryOffset + 7] << 24)
|
|
506
|
+
: (exifData[entryOffset + 4] << 24) |
|
|
507
|
+
(exifData[entryOffset + 5] << 16) |
|
|
508
|
+
(exifData[entryOffset + 6] << 8) | exifData[entryOffset + 7];
|
|
509
|
+
if (count > 8) {
|
|
510
|
+
const valueOffset = littleEndian
|
|
511
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
512
|
+
(exifData[entryOffset + 10] << 16) |
|
|
513
|
+
(exifData[entryOffset + 11] << 24)
|
|
514
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
515
|
+
(exifData[entryOffset + 9] << 16) |
|
|
516
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
517
|
+
if (valueOffset + count <= exifData.length) {
|
|
518
|
+
// Skip 8-byte character code prefix
|
|
519
|
+
const commentData = exifData.slice(valueOffset + 8, valueOffset + count);
|
|
520
|
+
metadata.userComment = new TextDecoder().decode(commentData)
|
|
521
|
+
.replace(/\0+$/, "");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Flash tag (0x9209) - SHORT
|
|
526
|
+
if (tag === 0x9209 && type === 3) {
|
|
527
|
+
metadata.flash = littleEndian
|
|
528
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
529
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
530
|
+
}
|
|
531
|
+
// WhiteBalance tag (0xA403) - SHORT
|
|
532
|
+
if (tag === 0xa403 && type === 3) {
|
|
533
|
+
metadata.whiteBalance = littleEndian
|
|
534
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8)
|
|
535
|
+
: (exifData[entryOffset + 8] << 8) | exifData[entryOffset + 9];
|
|
536
|
+
}
|
|
537
|
+
// LensMake tag (0xA433) - ASCII
|
|
538
|
+
if (tag === 0xa433 && type === 2) {
|
|
539
|
+
const valueOffset = littleEndian
|
|
540
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
541
|
+
(exifData[entryOffset + 10] << 16) |
|
|
542
|
+
(exifData[entryOffset + 11] << 24)
|
|
543
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
544
|
+
(exifData[entryOffset + 9] << 16) |
|
|
545
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
546
|
+
if (valueOffset < exifData.length) {
|
|
547
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
548
|
+
if (endIndex > valueOffset) {
|
|
549
|
+
metadata.lensMake = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// LensModel tag (0xA434) - ASCII
|
|
554
|
+
if (tag === 0xa434 && type === 2) {
|
|
555
|
+
const valueOffset = littleEndian
|
|
556
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
557
|
+
(exifData[entryOffset + 10] << 16) |
|
|
558
|
+
(exifData[entryOffset + 11] << 24)
|
|
559
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
560
|
+
(exifData[entryOffset + 9] << 16) |
|
|
561
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
562
|
+
if (valueOffset < exifData.length) {
|
|
563
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
564
|
+
if (endIndex > valueOffset) {
|
|
565
|
+
metadata.lensModel = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch (_e) {
|
|
572
|
+
// Ignore EXIF Sub-IFD parsing errors
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
parseGPSIFD(exifData, gpsIfdOffset, littleEndian, metadata) {
|
|
576
|
+
try {
|
|
577
|
+
const numEntries = littleEndian
|
|
578
|
+
? exifData[gpsIfdOffset] | (exifData[gpsIfdOffset + 1] << 8)
|
|
579
|
+
: (exifData[gpsIfdOffset] << 8) | exifData[gpsIfdOffset + 1];
|
|
580
|
+
let latRef = "";
|
|
581
|
+
let lonRef = "";
|
|
582
|
+
let latitude;
|
|
583
|
+
let longitude;
|
|
584
|
+
for (let i = 0; i < numEntries; i++) {
|
|
585
|
+
const entryOffset = gpsIfdOffset + 2 + i * 12;
|
|
586
|
+
if (entryOffset + 12 > exifData.length)
|
|
587
|
+
break;
|
|
588
|
+
const tag = littleEndian
|
|
589
|
+
? exifData[entryOffset] | (exifData[entryOffset + 1] << 8)
|
|
590
|
+
: (exifData[entryOffset] << 8) | exifData[entryOffset + 1];
|
|
591
|
+
const type = littleEndian
|
|
592
|
+
? exifData[entryOffset + 2] | (exifData[entryOffset + 3] << 8)
|
|
593
|
+
: (exifData[entryOffset + 2] << 8) | exifData[entryOffset + 3];
|
|
594
|
+
const valueOffset = littleEndian
|
|
595
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
596
|
+
(exifData[entryOffset + 10] << 16) |
|
|
597
|
+
(exifData[entryOffset + 11] << 24)
|
|
598
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
599
|
+
(exifData[entryOffset + 9] << 16) |
|
|
600
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
601
|
+
// GPSLatitudeRef (0x0001) - 'N' or 'S'
|
|
602
|
+
if (tag === 0x0001 && type === 2) {
|
|
603
|
+
latRef = String.fromCharCode(exifData[entryOffset + 8]);
|
|
604
|
+
}
|
|
605
|
+
// GPSLatitude (0x0002) - three rationals: degrees, minutes, seconds
|
|
606
|
+
if (tag === 0x0002 && type === 5 && valueOffset + 24 <= exifData.length) {
|
|
607
|
+
const degrees = this.readRational(exifData, valueOffset, littleEndian);
|
|
608
|
+
const minutes = this.readRational(exifData, valueOffset + 8, littleEndian);
|
|
609
|
+
const seconds = this.readRational(exifData, valueOffset + 16, littleEndian);
|
|
610
|
+
latitude = degrees + minutes / 60 + seconds / 3600;
|
|
611
|
+
}
|
|
612
|
+
// GPSLongitudeRef (0x0003) - 'E' or 'W'
|
|
613
|
+
if (tag === 0x0003 && type === 2) {
|
|
614
|
+
lonRef = String.fromCharCode(exifData[entryOffset + 8]);
|
|
615
|
+
}
|
|
616
|
+
// GPSLongitude (0x0004) - three rationals: degrees, minutes, seconds
|
|
617
|
+
if (tag === 0x0004 && type === 5 && valueOffset + 24 <= exifData.length) {
|
|
618
|
+
const degrees = this.readRational(exifData, valueOffset, littleEndian);
|
|
619
|
+
const minutes = this.readRational(exifData, valueOffset + 8, littleEndian);
|
|
620
|
+
const seconds = this.readRational(exifData, valueOffset + 16, littleEndian);
|
|
621
|
+
longitude = degrees + minutes / 60 + seconds / 3600;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Apply hemisphere references
|
|
625
|
+
if (latitude !== undefined && latRef) {
|
|
626
|
+
metadata.latitude = latRef === "S" ? -latitude : latitude;
|
|
627
|
+
}
|
|
628
|
+
if (longitude !== undefined && lonRef) {
|
|
629
|
+
metadata.longitude = lonRef === "W" ? -longitude : longitude;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch (_e) {
|
|
633
|
+
// Ignore GPS parsing errors
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
readRational(data, offset, littleEndian) {
|
|
637
|
+
const numerator = littleEndian
|
|
638
|
+
? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
|
|
639
|
+
(data[offset + 3] << 24)
|
|
640
|
+
: (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
641
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
642
|
+
const denominator = littleEndian
|
|
643
|
+
? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
|
|
644
|
+
(data[offset + 7] << 24)
|
|
645
|
+
: (data[offset + 4] << 24) | (data[offset + 5] << 16) |
|
|
646
|
+
(data[offset + 6] << 8) | data[offset + 7];
|
|
647
|
+
return denominator !== 0 ? numerator / denominator : 0;
|
|
648
|
+
}
|
|
354
649
|
createEXIFData(metadata) {
|
|
355
650
|
const entries = [];
|
|
356
651
|
// Add DateTime if available
|
|
@@ -387,7 +682,55 @@ export class JPEGFormat {
|
|
|
387
682
|
value: new TextEncoder().encode(metadata.copyright + "\0"),
|
|
388
683
|
});
|
|
389
684
|
}
|
|
390
|
-
|
|
685
|
+
// Add Make (camera manufacturer)
|
|
686
|
+
if (metadata.cameraMake) {
|
|
687
|
+
entries.push({
|
|
688
|
+
tag: 0x010f,
|
|
689
|
+
type: 2,
|
|
690
|
+
value: new TextEncoder().encode(metadata.cameraMake + "\0"),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
// Add Model (camera model)
|
|
694
|
+
if (metadata.cameraModel) {
|
|
695
|
+
entries.push({
|
|
696
|
+
tag: 0x0110,
|
|
697
|
+
type: 2,
|
|
698
|
+
value: new TextEncoder().encode(metadata.cameraModel + "\0"),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// Add Orientation
|
|
702
|
+
if (metadata.orientation !== undefined) {
|
|
703
|
+
const orientationBytes = new Uint8Array(2);
|
|
704
|
+
orientationBytes[0] = metadata.orientation & 0xff;
|
|
705
|
+
orientationBytes[1] = (metadata.orientation >> 8) & 0xff;
|
|
706
|
+
entries.push({
|
|
707
|
+
tag: 0x0112,
|
|
708
|
+
type: 3, // SHORT
|
|
709
|
+
value: orientationBytes,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// Add Software
|
|
713
|
+
if (metadata.software) {
|
|
714
|
+
entries.push({
|
|
715
|
+
tag: 0x0131,
|
|
716
|
+
type: 2,
|
|
717
|
+
value: new TextEncoder().encode(metadata.software + "\0"),
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
// Check if we have GPS data
|
|
721
|
+
const hasGPS = metadata.latitude !== undefined &&
|
|
722
|
+
metadata.longitude !== undefined;
|
|
723
|
+
// Check if we have Exif Sub-IFD data
|
|
724
|
+
const hasExifSubIFD = metadata.iso !== undefined ||
|
|
725
|
+
metadata.exposureTime !== undefined ||
|
|
726
|
+
metadata.fNumber !== undefined ||
|
|
727
|
+
metadata.focalLength !== undefined ||
|
|
728
|
+
metadata.flash !== undefined ||
|
|
729
|
+
metadata.whiteBalance !== undefined ||
|
|
730
|
+
metadata.lensMake !== undefined ||
|
|
731
|
+
metadata.lensModel !== undefined ||
|
|
732
|
+
metadata.userComment !== undefined;
|
|
733
|
+
if (entries.length === 0 && !hasGPS && !hasExifSubIFD)
|
|
391
734
|
return [];
|
|
392
735
|
// Build EXIF structure
|
|
393
736
|
const exif = [];
|
|
@@ -396,10 +739,12 @@ export class JPEGFormat {
|
|
|
396
739
|
exif.push(0x2a, 0x00); // 42
|
|
397
740
|
// Offset to IFD0 (8 bytes from start)
|
|
398
741
|
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
399
|
-
// Number of entries
|
|
400
|
-
|
|
742
|
+
// Number of entries (add GPS IFD pointer and Exif Sub-IFD pointer if needed)
|
|
743
|
+
const ifd0Entries = entries.length + (hasGPS ? 1 : 0) +
|
|
744
|
+
(hasExifSubIFD ? 1 : 0);
|
|
745
|
+
exif.push(ifd0Entries & 0xff, (ifd0Entries >> 8) & 0xff);
|
|
401
746
|
// Calculate data offset
|
|
402
|
-
let dataOffset = 8 + 2 +
|
|
747
|
+
let dataOffset = 8 + 2 + ifd0Entries * 12 + 4;
|
|
403
748
|
for (const entry of entries) {
|
|
404
749
|
// Tag
|
|
405
750
|
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
@@ -419,6 +764,26 @@ export class JPEGFormat {
|
|
|
419
764
|
dataOffset += entry.value.length;
|
|
420
765
|
}
|
|
421
766
|
}
|
|
767
|
+
// Add Exif Sub-IFD pointer if we have camera metadata
|
|
768
|
+
let exifSubIfdOffset = 0;
|
|
769
|
+
if (hasExifSubIFD) {
|
|
770
|
+
exifSubIfdOffset = dataOffset;
|
|
771
|
+
// Exif IFD Pointer tag (0x8769), type 4 (LONG), count 1
|
|
772
|
+
exif.push(0x69, 0x87); // Tag
|
|
773
|
+
exif.push(0x04, 0x00); // Type
|
|
774
|
+
exif.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
775
|
+
exif.push(exifSubIfdOffset & 0xff, (exifSubIfdOffset >> 8) & 0xff, (exifSubIfdOffset >> 16) & 0xff, (exifSubIfdOffset >> 24) & 0xff);
|
|
776
|
+
}
|
|
777
|
+
// Add GPS IFD pointer if we have GPS data
|
|
778
|
+
let gpsIfdOffset = 0;
|
|
779
|
+
if (hasGPS) {
|
|
780
|
+
gpsIfdOffset = dataOffset;
|
|
781
|
+
// GPS IFD Pointer tag (0x8825), type 4 (LONG), count 1
|
|
782
|
+
exif.push(0x25, 0x88); // Tag
|
|
783
|
+
exif.push(0x04, 0x00); // Type
|
|
784
|
+
exif.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
785
|
+
exif.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
|
|
786
|
+
}
|
|
422
787
|
// Next IFD offset (0 = no more IFDs)
|
|
423
788
|
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
424
789
|
// Append data for entries that didn't fit in value field
|
|
@@ -429,6 +794,270 @@ export class JPEGFormat {
|
|
|
429
794
|
}
|
|
430
795
|
}
|
|
431
796
|
}
|
|
797
|
+
// Add Exif Sub-IFD if we have camera metadata
|
|
798
|
+
if (hasExifSubIFD) {
|
|
799
|
+
const exifSubIfd = this.createExifSubIFD(metadata, exifSubIfdOffset);
|
|
800
|
+
for (const byte of exifSubIfd) {
|
|
801
|
+
exif.push(byte);
|
|
802
|
+
}
|
|
803
|
+
// Update GPS offset since we added the Sub-IFD
|
|
804
|
+
if (hasGPS) {
|
|
805
|
+
gpsIfdOffset = exif.length;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Add GPS IFD if we have GPS data
|
|
809
|
+
if (hasGPS) {
|
|
810
|
+
const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
|
|
811
|
+
for (const byte of gpsIfd) {
|
|
812
|
+
exif.push(byte);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
432
815
|
return exif;
|
|
433
816
|
}
|
|
817
|
+
createGPSIFD(metadata, gpsIfdStart) {
|
|
818
|
+
const gps = [];
|
|
819
|
+
// We'll create 4 GPS entries: LatitudeRef, Latitude, LongitudeRef, Longitude
|
|
820
|
+
const numEntries = 4;
|
|
821
|
+
gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
|
|
822
|
+
const latitude = metadata.latitude;
|
|
823
|
+
const longitude = metadata.longitude;
|
|
824
|
+
// Convert to absolute values for DMS calculation
|
|
825
|
+
const absLat = Math.abs(latitude);
|
|
826
|
+
const absLon = Math.abs(longitude);
|
|
827
|
+
// Calculate degrees, minutes, seconds
|
|
828
|
+
const latDeg = Math.floor(absLat);
|
|
829
|
+
const latMin = Math.floor((absLat - latDeg) * 60);
|
|
830
|
+
const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
|
|
831
|
+
const lonDeg = Math.floor(absLon);
|
|
832
|
+
const lonMin = Math.floor((absLon - lonDeg) * 60);
|
|
833
|
+
const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
|
|
834
|
+
// Calculate offset for rational data (relative to start of EXIF data, not GPS IFD)
|
|
835
|
+
let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
|
|
836
|
+
// Entry 1: GPSLatitudeRef (tag 0x0001)
|
|
837
|
+
gps.push(0x01, 0x00); // Tag
|
|
838
|
+
gps.push(0x02, 0x00); // Type (ASCII)
|
|
839
|
+
gps.push(0x02, 0x00, 0x00, 0x00); // Count (2 bytes including null)
|
|
840
|
+
// Value stored inline: 'N' or 'S' + null
|
|
841
|
+
gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00); // 'N' = 78, 'S' = 83
|
|
842
|
+
// Entry 2: GPSLatitude (tag 0x0002)
|
|
843
|
+
gps.push(0x02, 0x00); // Tag
|
|
844
|
+
gps.push(0x05, 0x00); // Type (RATIONAL)
|
|
845
|
+
gps.push(0x03, 0x00, 0x00, 0x00); // Count (3 rationals)
|
|
846
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
847
|
+
dataOffset += 24; // 3 rationals * 8 bytes
|
|
848
|
+
// Entry 3: GPSLongitudeRef (tag 0x0003)
|
|
849
|
+
gps.push(0x03, 0x00); // Tag
|
|
850
|
+
gps.push(0x02, 0x00); // Type (ASCII)
|
|
851
|
+
gps.push(0x02, 0x00, 0x00, 0x00); // Count
|
|
852
|
+
gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00); // 'E' = 69, 'W' = 87
|
|
853
|
+
// Entry 4: GPSLongitude (tag 0x0004)
|
|
854
|
+
gps.push(0x04, 0x00); // Tag
|
|
855
|
+
gps.push(0x05, 0x00); // Type (RATIONAL)
|
|
856
|
+
gps.push(0x03, 0x00, 0x00, 0x00); // Count
|
|
857
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
858
|
+
// Next IFD offset (0 = no more IFDs)
|
|
859
|
+
gps.push(0x00, 0x00, 0x00, 0x00);
|
|
860
|
+
// Write latitude rationals (degrees, minutes, seconds)
|
|
861
|
+
this.writeRational(gps, latDeg, 1);
|
|
862
|
+
this.writeRational(gps, latMin, 1);
|
|
863
|
+
this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
|
|
864
|
+
// Write longitude rationals
|
|
865
|
+
this.writeRational(gps, lonDeg, 1);
|
|
866
|
+
this.writeRational(gps, lonMin, 1);
|
|
867
|
+
this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
|
|
868
|
+
return gps;
|
|
869
|
+
}
|
|
870
|
+
createExifSubIFD(metadata, exifIfdStart) {
|
|
871
|
+
const entries = [];
|
|
872
|
+
// ISO Speed Ratings (0x8827) - SHORT
|
|
873
|
+
if (metadata.iso !== undefined) {
|
|
874
|
+
entries.push({
|
|
875
|
+
tag: 0x8827,
|
|
876
|
+
type: 3,
|
|
877
|
+
data: [metadata.iso & 0xff, (metadata.iso >> 8) & 0xff],
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
// Exposure Time (0x829A) - RATIONAL
|
|
881
|
+
if (metadata.exposureTime !== undefined) {
|
|
882
|
+
entries.push({ tag: 0x829a, type: 5, data: [] }); // Will add offset later
|
|
883
|
+
}
|
|
884
|
+
// FNumber (0x829D) - RATIONAL
|
|
885
|
+
if (metadata.fNumber !== undefined) {
|
|
886
|
+
entries.push({ tag: 0x829d, type: 5, data: [] }); // Will add offset later
|
|
887
|
+
}
|
|
888
|
+
// Flash (0x9209) - SHORT
|
|
889
|
+
if (metadata.flash !== undefined) {
|
|
890
|
+
entries.push({
|
|
891
|
+
tag: 0x9209,
|
|
892
|
+
type: 3,
|
|
893
|
+
data: [metadata.flash & 0xff, (metadata.flash >> 8) & 0xff],
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
// Focal Length (0x920A) - RATIONAL
|
|
897
|
+
if (metadata.focalLength !== undefined) {
|
|
898
|
+
entries.push({ tag: 0x920a, type: 5, data: [] }); // Will add offset later
|
|
899
|
+
}
|
|
900
|
+
// User Comment (0x9286) - UNDEFINED
|
|
901
|
+
if (metadata.userComment !== undefined) {
|
|
902
|
+
entries.push({ tag: 0x9286, type: 7, data: [] }); // Will add offset later
|
|
903
|
+
}
|
|
904
|
+
// White Balance (0xA403) - SHORT
|
|
905
|
+
if (metadata.whiteBalance !== undefined) {
|
|
906
|
+
entries.push({
|
|
907
|
+
tag: 0xa403,
|
|
908
|
+
type: 3,
|
|
909
|
+
data: [
|
|
910
|
+
metadata.whiteBalance & 0xff,
|
|
911
|
+
(metadata.whiteBalance >> 8) & 0xff,
|
|
912
|
+
],
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
// Lens Make (0xA433) - ASCII
|
|
916
|
+
if (metadata.lensMake !== undefined) {
|
|
917
|
+
entries.push({ tag: 0xa433, type: 2, data: [] }); // Will add offset later
|
|
918
|
+
}
|
|
919
|
+
// Lens Model (0xA434) - ASCII
|
|
920
|
+
if (metadata.lensModel !== undefined) {
|
|
921
|
+
entries.push({ tag: 0xa434, type: 2, data: [] }); // Will add offset later
|
|
922
|
+
}
|
|
923
|
+
const exifSubIfd = [];
|
|
924
|
+
const numEntries = entries.length;
|
|
925
|
+
exifSubIfd.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
|
|
926
|
+
let dataOffset = exifIfdStart + 2 + numEntries * 12 + 4;
|
|
927
|
+
// Write entry headers
|
|
928
|
+
for (const entry of entries) {
|
|
929
|
+
exifSubIfd.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
930
|
+
exifSubIfd.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
931
|
+
if (entry.tag === 0x829a && metadata.exposureTime !== undefined) {
|
|
932
|
+
// Exposure Time - 1 RATIONAL
|
|
933
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
934
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
935
|
+
dataOffset += 8;
|
|
936
|
+
}
|
|
937
|
+
else if (entry.tag === 0x829d && metadata.fNumber !== undefined) {
|
|
938
|
+
// FNumber - 1 RATIONAL
|
|
939
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
940
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
941
|
+
dataOffset += 8;
|
|
942
|
+
}
|
|
943
|
+
else if (entry.tag === 0x920a && metadata.focalLength !== undefined) {
|
|
944
|
+
// Focal Length - 1 RATIONAL
|
|
945
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
946
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
947
|
+
dataOffset += 8;
|
|
948
|
+
}
|
|
949
|
+
else if (entry.tag === 0x9286 && metadata.userComment !== undefined) {
|
|
950
|
+
// User Comment - UNDEFINED with character code
|
|
951
|
+
const commentBytes = new TextEncoder().encode(metadata.userComment);
|
|
952
|
+
const totalLength = 8 + commentBytes.length; // 8 bytes for character code
|
|
953
|
+
exifSubIfd.push(totalLength & 0xff, (totalLength >> 8) & 0xff, (totalLength >> 16) & 0xff, (totalLength >> 24) & 0xff);
|
|
954
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
955
|
+
dataOffset += totalLength;
|
|
956
|
+
}
|
|
957
|
+
else if (entry.tag === 0xa433 && metadata.lensMake !== undefined) {
|
|
958
|
+
// Lens Make - ASCII
|
|
959
|
+
const bytes = new TextEncoder().encode(metadata.lensMake + "\0");
|
|
960
|
+
exifSubIfd.push(bytes.length & 0xff, (bytes.length >> 8) & 0xff, (bytes.length >> 16) & 0xff, (bytes.length >> 24) & 0xff);
|
|
961
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
962
|
+
dataOffset += bytes.length;
|
|
963
|
+
}
|
|
964
|
+
else if (entry.tag === 0xa434 && metadata.lensModel !== undefined) {
|
|
965
|
+
// Lens Model - ASCII
|
|
966
|
+
const bytes = new TextEncoder().encode(metadata.lensModel + "\0");
|
|
967
|
+
exifSubIfd.push(bytes.length & 0xff, (bytes.length >> 8) & 0xff, (bytes.length >> 16) & 0xff, (bytes.length >> 24) & 0xff);
|
|
968
|
+
exifSubIfd.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
969
|
+
dataOffset += bytes.length;
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
// SHORT types stored inline
|
|
973
|
+
exifSubIfd.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
974
|
+
exifSubIfd.push(...entry.data, 0x00, 0x00); // Value (4 bytes)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Next IFD offset
|
|
978
|
+
exifSubIfd.push(0x00, 0x00, 0x00, 0x00);
|
|
979
|
+
// Write data for RATIONAL and ASCII types
|
|
980
|
+
for (const entry of entries) {
|
|
981
|
+
if (entry.tag === 0x829a && metadata.exposureTime !== undefined) {
|
|
982
|
+
// Convert exposure time to rational (numerator/denominator)
|
|
983
|
+
const [num, den] = this.toRational(metadata.exposureTime);
|
|
984
|
+
this.writeRational(exifSubIfd, num, den);
|
|
985
|
+
}
|
|
986
|
+
else if (entry.tag === 0x829d && metadata.fNumber !== undefined) {
|
|
987
|
+
const [num, den] = this.toRational(metadata.fNumber);
|
|
988
|
+
this.writeRational(exifSubIfd, num, den);
|
|
989
|
+
}
|
|
990
|
+
else if (entry.tag === 0x920a && metadata.focalLength !== undefined) {
|
|
991
|
+
const [num, den] = this.toRational(metadata.focalLength);
|
|
992
|
+
this.writeRational(exifSubIfd, num, den);
|
|
993
|
+
}
|
|
994
|
+
else if (entry.tag === 0x9286 && metadata.userComment !== undefined) {
|
|
995
|
+
// Character code: ASCII (8 bytes)
|
|
996
|
+
exifSubIfd.push(0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00);
|
|
997
|
+
const commentBytes = new TextEncoder().encode(metadata.userComment);
|
|
998
|
+
for (const byte of commentBytes) {
|
|
999
|
+
exifSubIfd.push(byte);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
else if (entry.tag === 0xa433 && metadata.lensMake !== undefined) {
|
|
1003
|
+
const bytes = new TextEncoder().encode(metadata.lensMake + "\0");
|
|
1004
|
+
for (const byte of bytes) {
|
|
1005
|
+
exifSubIfd.push(byte);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
else if (entry.tag === 0xa434 && metadata.lensModel !== undefined) {
|
|
1009
|
+
const bytes = new TextEncoder().encode(metadata.lensModel + "\0");
|
|
1010
|
+
for (const byte of bytes) {
|
|
1011
|
+
exifSubIfd.push(byte);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return exifSubIfd;
|
|
1016
|
+
}
|
|
1017
|
+
toRational(value) {
|
|
1018
|
+
// Convert decimal to rational representation
|
|
1019
|
+
// Try to find a reasonable denominator
|
|
1020
|
+
const denominators = [1, 10, 100, 1000, 10000, 100000, 1000000];
|
|
1021
|
+
for (const den of denominators) {
|
|
1022
|
+
const num = Math.round(value * den);
|
|
1023
|
+
if (Math.abs(num / den - value) < 0.000001) {
|
|
1024
|
+
return [num, den];
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// Fallback
|
|
1028
|
+
return [Math.round(value * 1000000), 1000000];
|
|
1029
|
+
}
|
|
1030
|
+
writeRational(output, numerator, denominator) {
|
|
1031
|
+
// Write as little endian
|
|
1032
|
+
output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
|
|
1033
|
+
output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Get the list of metadata fields supported by JPEG format
|
|
1037
|
+
*/
|
|
1038
|
+
getSupportedMetadata() {
|
|
1039
|
+
return [
|
|
1040
|
+
"creationDate",
|
|
1041
|
+
"description",
|
|
1042
|
+
"author",
|
|
1043
|
+
"copyright",
|
|
1044
|
+
"cameraMake",
|
|
1045
|
+
"cameraModel",
|
|
1046
|
+
"orientation",
|
|
1047
|
+
"software",
|
|
1048
|
+
"latitude",
|
|
1049
|
+
"longitude",
|
|
1050
|
+
"iso",
|
|
1051
|
+
"exposureTime",
|
|
1052
|
+
"fNumber",
|
|
1053
|
+
"focalLength",
|
|
1054
|
+
"flash",
|
|
1055
|
+
"whiteBalance",
|
|
1056
|
+
"lensMake",
|
|
1057
|
+
"lensModel",
|
|
1058
|
+
"userComment",
|
|
1059
|
+
"dpiX",
|
|
1060
|
+
"dpiY",
|
|
1061
|
+
];
|
|
1062
|
+
}
|
|
434
1063
|
}
|