exiftool-vendored.exe 12.89.0 → 12.96.0

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.
@@ -48,7 +48,7 @@ use Image::ExifTool qw(:DataAccess :Utils);
48
48
  use Image::ExifTool::Exif;
49
49
  use Image::ExifTool::GPS;
50
50
 
51
- $VERSION = '2.98';
51
+ $VERSION = '3.02';
52
52
 
53
53
  sub ProcessMOV($$;$);
54
54
  sub ProcessKeys($$$);
@@ -6552,7 +6552,7 @@ my %userDefined = (
6552
6552
  PROCESS_PROC => \&ProcessKeys,
6553
6553
  WRITE_PROC => \&WriteKeys,
6554
6554
  CHECK_PROC => \&CheckQTValue,
6555
- VARS => { LONG_TAGS => 7 },
6555
+ VARS => { LONG_TAGS => 8 },
6556
6556
  WRITABLE => 1,
6557
6557
  # (not PREFERRED when writing)
6558
6558
  GROUPS => { 1 => 'Keys' },
@@ -6606,6 +6606,8 @@ my %userDefined = (
6606
6606
  PrintConv => '$val * 1e6 . " microseconds"',
6607
6607
  PrintConvInv => '$val =~ s/ .*//; $val * 1e-6',
6608
6608
  },
6609
+ # 'camera.focal_length.35mm_equivalent' - not top level (written to Keys in video track)
6610
+ # 'camera.lens_model' - not top level (written to Keys in video track)
6609
6611
  'location.ISO6709' => {
6610
6612
  Name => 'GPSCoordinates',
6611
6613
  Groups => { 2 => 'Location' },
@@ -6668,7 +6670,12 @@ my %userDefined = (
6668
6670
  #
6669
6671
  'com.apple.photos.captureMode' => 'CaptureMode',
6670
6672
  'com.android.version' => 'AndroidVersion',
6671
- 'com.android.capture.fps' => 'AndroidCaptureFPS',
6673
+ 'com.android.capture.fps' => { Name => 'AndroidCaptureFPS', Writable => 'float' },
6674
+ 'com.android.manufacturer' => 'AndroidMake',
6675
+ 'com.android.model' => 'AndroidModel',
6676
+ 'com.xiaomi.preview_video_cover' => { Name => 'XiaomiPreviewVideoCover', Writable => 'int32s' },
6677
+ 'xiaomi.exifInfo.videoinfo' => 'XiaomiExifInfo',
6678
+ 'com.xiaomi.hdr10' => { Name => 'XiaomiHDR10', Writable => 'int32s' },
6672
6679
  #
6673
6680
  # also seen
6674
6681
  #
@@ -9484,7 +9491,7 @@ sub ProcessKeys($$$)
9484
9491
  $$newInfo{KeysID} = $tag; # save original ID for use in family 7 group name
9485
9492
  AddTagToTable($itemList, $id, $newInfo);
9486
9493
  $msg or $msg = '';
9487
- $out and print $out "$$et{INDENT}Added ItemList Tag $id = ($ns) $tag$msg\n";
9494
+ $out and print $out "$$et{INDENT}Added ItemList Tag $id = ($ns) $full$msg\n";
9488
9495
  }
9489
9496
  $pos += $len;
9490
9497
  ++$index;
@@ -9521,7 +9528,7 @@ sub ProcessMOV($$;$)
9521
9528
  my $dirID = $$dirInfo{DirID} || '';
9522
9529
  my $charsetQuickTime = $et->Options('CharsetQuickTime');
9523
9530
  my ($buff, $tag, $size, $track, $isUserData, %triplet, $doDefaultLang, $index);
9524
- my ($dirEnd, $unkOpt, %saveOptions, $atomCount, $warnStr);
9531
+ my ($dirEnd, $unkOpt, %saveOptions, $atomCount, $warnStr, $trailer);
9525
9532
 
9526
9533
  my $topLevel = not $$et{InQuickTime};
9527
9534
  $$et{InQuickTime} = 1;
@@ -9550,6 +9557,17 @@ sub ProcessMOV($$;$)
9550
9557
  $tagTablePtr = GetTagTable('Image::ExifTool::QuickTime::Main');
9551
9558
  }
9552
9559
  ($size, $tag) = unpack('Na4', $buff);
9560
+ my $fast = $$et{OPTIONS}{FastScan} || 0;
9561
+ # check for Insta360 trailer
9562
+ if ($topLevel and not $fast) {
9563
+ my $pos = $raf->Tell();
9564
+ if ($raf->Seek(-40, 2) and $raf->Read($buff, 40) == 40 and
9565
+ substr($buff, 8) eq '8db42d694ccc418790edff439fe026bf')
9566
+ {
9567
+ $trailer = [ 'Insta360', $raf->Tell() - unpack('V',$buff) ];
9568
+ }
9569
+ $raf->Seek($pos,0) or return 0;
9570
+ }
9553
9571
  if ($dataPt) {
9554
9572
  $verbose and $et->VerboseDir($$dirInfo{DirName});
9555
9573
  } else {
@@ -9588,7 +9606,6 @@ sub ProcessMOV($$;$)
9588
9606
  # have XMP take priority except for HEIC
9589
9607
  $$et{PRIORITY_DIR} = 'XMP' unless $fileType and $fileType eq 'HEIC';
9590
9608
  }
9591
- my $fast = $$et{OPTIONS}{FastScan} || 0;
9592
9609
  $$raf{NoBuffer} = 1 if $fast; # disable buffering in FastScan mode
9593
9610
 
9594
9611
  my $ee = $$et{OPTIONS}{ExtractEmbedded};
@@ -9737,7 +9754,7 @@ sub ProcessMOV($$;$)
9737
9754
  if ($size > 0x2000000) { # start to get worried above 32 MiB
9738
9755
  # check for RIFF trailer (written by Auto-Vox dashcam)
9739
9756
  if ($buff =~ /^(gpsa|gps0|gsen|gsea)...\0/s) { # (yet seen only gpsa as first record)
9740
- $et->VPrint(0, "Found RIFF trailer");
9757
+ $et->VPrint(0, sprintf("Found RIFF trailer at offset 0x%x",$lastPos));
9741
9758
  if ($et->Options('ExtractEmbedded')) {
9742
9759
  $raf->Seek(-8, 1) or last; # seek back to start of trailer
9743
9760
  my $tbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
@@ -9746,6 +9763,11 @@ sub ProcessMOV($$;$)
9746
9763
  EEWarn($et);
9747
9764
  }
9748
9765
  last;
9766
+ } elsif ($buff eq 'CCCCCCCC') {
9767
+ $et->VPrint(0, sprintf("Found Kenwood trailer at offset 0x%x",$lastPos));
9768
+ my $tbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
9769
+ ProcessKenwoodTrailer($et, { RAF => $raf }, $tbl);
9770
+ last;
9749
9771
  }
9750
9772
  $ignore = 1;
9751
9773
  if ($tagInfo and not $$tagInfo{Unknown} and not $eeTag) {
@@ -10124,6 +10146,10 @@ ItemID: foreach $id (reverse sort { $a <=> $b } keys %$items) {
10124
10146
  $dataPos += $size + 8; # point to start of next atom data
10125
10147
  last if $dirEnd and $dataPos >= $dirEnd; # (note: ignores last value if 0 bytes)
10126
10148
  $lastPos = $raf->Tell() + $dirBase;
10149
+ if ($trailer and $lastPos >= $$trailer[1]) {
10150
+ $et->Warn(sprintf('%s trailer at offset 0x%x', @$trailer), 1);
10151
+ last;
10152
+ }
10127
10153
  $raf->Read($buff, 8) == 8 or last;
10128
10154
  $lastTag = $tag if $$tagTablePtr{$tag} and $tag ne 'free'; # (Insta360 sometimes puts free block before trailer)
10129
10155
  ($size, $tag) = unpack('Na4', $buff);
@@ -10132,14 +10158,10 @@ ItemID: foreach $id (reverse sort { $a <=> $b } keys %$items) {
10132
10158
  if ($warnStr) {
10133
10159
  # assume this is an unknown trailer if it comes immediately after
10134
10160
  # mdat or moov and has a tag name we don't recognize
10135
- if (($lastTag eq 'mdat' or $lastTag eq 'moov') and (not $$tagTablePtr{$tag} or
10136
- ref $$tagTablePtr{$tag} eq 'HASH' and $$tagTablePtr{$tag}{Unknown}))
10161
+ if (($lastTag eq 'mdat' or $lastTag eq 'moov') and
10162
+ (not $$tagTablePtr{$tag} or ref $$tagTablePtr{$tag} eq 'HASH' and $$tagTablePtr{$tag}{Unknown}))
10137
10163
  {
10138
- if ($size == 0x1000000 - 8 and $tag =~ /^(\x94\xc0\x7e\0|\0\x02\0\0)/) {
10139
- $et->Warn(sprintf('Insta360 trailer at offset 0x%x', $lastPos), 1);
10140
- } else {
10141
- $et->Warn('Unknown trailer with '.lcfirst($warnStr));
10142
- }
10164
+ $et->Warn('Unknown trailer with '.lcfirst($warnStr));
10143
10165
  } else {
10144
10166
  $et->Warn($warnStr);
10145
10167
  }
@@ -10166,7 +10188,10 @@ QTLang: foreach $tag (@{$$et{QTLang}}) {
10166
10188
  for ($i=0, $key=$name; $$infoHash{$key}; ++$i, $key="$name ($i)") {
10167
10189
  next QTLang if $et->GetGroup($key, 0) eq 'QuickTime';
10168
10190
  }
10169
- $et->FoundTag($tagInfo, $$et{VALUE}{$tag});
10191
+ $key = $et->FoundTag($tagInfo, $$et{VALUE}{$tag});
10192
+ # copy extra tag information (groups, etc) to the synthetic tag
10193
+ $$et{TAG_EXTRA}{$key} = $$et{TAG_EXTRA}{$tag};
10194
+ $et->VPrint(0, "(synthesized default-language tag for QuickTime:$$tagInfo{Name})");
10170
10195
  }
10171
10196
  delete $$et{QTLang};
10172
10197
  }
@@ -109,7 +109,7 @@ my %insvLimit = (
109
109
  The tags below are extracted from timed metadata in QuickTime and other
110
110
  formats of video files when the ExtractEmbedded option is used. Although
111
111
  most of these tags are combined into the single table below, ExifTool
112
- currently reads 74 different formats of timed GPS metadata from video files.
112
+ currently reads 77 different formats of timed GPS metadata from video files.
113
113
  },
114
114
  VARS => { NO_ID => 1 },
115
115
  GPSLatitude => { PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")', RawConv => '$$self{FoundGPSLatitude} = 1; $val' },
@@ -757,6 +757,15 @@ my %insvLimit = (
757
757
  10 => { Name => 'FusionYPR', Format => 'float[3]' },
758
758
  );
759
759
 
760
+ #------------------------------------------------------------------------------
761
+ # Convert unsigned 32-bit integer to signed
762
+ # Inputs: <none> (uses value in $_)
763
+ # Returns: signed integer
764
+ sub SignedInt32()
765
+ {
766
+ return $_ < 0x80000000 ? $_ : $_ - 4294967296;
767
+ }
768
+
760
769
  #------------------------------------------------------------------------------
761
770
  # Save information from keys in OtherSampleDesc directory for processing timed metadata
762
771
  # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
@@ -1420,9 +1429,10 @@ Sample: for ($i=0; ; ) {
1420
1429
  } elsif ($type eq 'gps ') { # (ie. GPSDataList tag)
1421
1430
 
1422
1431
  if ($buff =~ /^....freeGPS /s) {
1423
- # process by brute scan instead if ExtractEmbedded >= 3
1424
- # (some videos don't reference all freeGPS info from 'gps ' table, eg. INNOV)
1425
- last if $eeOpt >= 3;
1432
+ # parse freeGPS data unless done already in brute-force scan
1433
+ # (some videos don't reference all freeGPS info from 'gps ' table, eg. INNOV,
1434
+ # and some videos don't put 'gps ' data in mdat, eg XGODY 12" 4K Dashcam)
1435
+ last if $$et{FoundGPSByScan};
1426
1436
  # decode "freeGPS " data (Novatek and others)
1427
1437
  ProcessFreeGPS($et, {
1428
1438
  DataPt => \$buff,
@@ -1473,6 +1483,31 @@ sub ConvertLatLon($$)
1473
1483
  $_[1] = $deg + ($_[1] - $deg * 100) / 60;
1474
1484
  }
1475
1485
 
1486
+ #------------------------------------------------------------------------------
1487
+ # Decrypt Lucky data
1488
+ # Inputs: 0) string to decrypt, 1) encryption key
1489
+ # Returns: decrypted string
1490
+ my @luckyKeys = ('luckychip gps', 'customer ## gps');
1491
+ sub DecryptLucky($$) {
1492
+ my ($str, $key) = @_;
1493
+ my @str = unpack('C*', $str);
1494
+ my @key = unpack('C*', $key);
1495
+ my @enc = (0..255);
1496
+ my ($i, $j, $k) = (0, 0, 0);
1497
+ do {
1498
+ $j = ($j + $enc[$i] + $key[$i % length($key)]) & 0xff;
1499
+ @enc[$i,$j] = @enc[$j,$i];
1500
+ } while (++$i < 256);
1501
+ ($i, $j, $k) = (0, 0, 0);
1502
+ do {
1503
+ $j = ($j + 1) & 0xff;
1504
+ $k = ($k + $enc[$j]) & 0xff;
1505
+ @enc[$j,$k] = @enc[$k,$j];
1506
+ $str[$i] ^= $enc[($enc[$j] + $enc[$k]) & 0xff];
1507
+ } while (++$i < @str);
1508
+ return pack('C*', @str);
1509
+ }
1510
+
1476
1511
  #------------------------------------------------------------------------------
1477
1512
  # Process "freeGPS " data blocks
1478
1513
  # Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,SampleTime,SampleDuration}, 2) tagTable ref
@@ -1588,40 +1623,87 @@ sub ProcessFreeGPS($$$)
1588
1623
  }
1589
1624
  if (defined $lat) {
1590
1625
  # extract accelerometer readings if GPS was valid
1591
- @acc = unpack('x68V3', $$dataPt);
1592
- # change to signed integer and divide by 256
1593
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 256 } @acc;
1626
+ # and change to signed integer and divide by 256
1627
+ @acc = map { SignedInt32 / 256 } unpack('x68V3', $$dataPt);
1594
1628
  }
1595
1629
 
1596
- } elsif ($$dataPt =~ /^.{37}\0\0\0A([NS])([EW])/s) {
1630
+ } elsif ($$dataPt =~ /^.{37}\0\0\0A([NS])([EW])\0/s) {
1597
1631
 
1598
- $debug and $et->FoundTag(GPSType => 3);
1599
- # decode freeGPS from ViofoA119v3 dashcam (similar to Novatek GPS format)
1600
- # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1601
- # 0010: 05 00 00 00 2f 00 00 00 03 00 00 00 13 00 00 00 [..../...........]
1602
- # 0020: 09 00 00 00 1b 00 00 00 41 4e 57 00 25 d1 99 45 [........ANW.%..E]
1603
- # 0030: f1 47 40 46 66 66 d2 41 85 eb 83 41 00 00 00 00 [.G@Fff.A...A....]
1604
1632
  ($latRef, $lonRef) = ($1, $2);
1605
1633
  ($hr,$min,$sec,$yr,$mon,$day) = unpack('x16V6', $$dataPt);
1606
- if ($yr >= 2000) {
1607
- # Kenwood dashcam sometimes stores absolute year and local time
1608
- # (but sometimes year since 2000 and UTC time in same video!)
1609
- require Time::Local;
1610
- my $time = Image::ExifTool::TimeLocal($sec,$min,$hr,$day,$mon-1,$yr);
1611
- ($sec,$min,$hr,$day,$mon,$yr) = gmtime($time);
1612
- $yr += 1900;
1613
- ++$mon;
1614
- $et->WarnOnce('Converting GPSDateTime to UTC based on local time zone',1);
1615
- }
1616
- $lat = GetFloat($dataPt, 0x2c);
1617
- $lon = GetFloat($dataPt, 0x30);
1618
- $spd = GetFloat($dataPt, 0x34) * $knotsToKph; # (convert knots to km/h)
1619
- $trk = GetFloat($dataPt, 0x38);
1620
- # (may be all zeros or int16u counting from 1 to 6 if not valid)
1621
- my $tmp = substr($$dataPt, 60, 12);
1622
- if ($tmp ne "\0\0\0\0\0\0\0\0\0\0\0\0" and $tmp ne "\x01\0\x02\0\x03\0\x04\0\x05\0\x06\0") {
1623
- @acc = unpack('V3', $tmp);
1624
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 256 } @acc;
1634
+ # test for base64-encoded and encrypted lucky gps strings
1635
+ my ($notEnc, $notStr, $lt, $ln);
1636
+ if (length($$dataPt) < 0x78) {
1637
+ $notEnc = $notStr = 1;
1638
+ } else {
1639
+ $lt = substr($$dataPt, 0x2c, 20), # latitude
1640
+ $ln = substr($$dataPt, 0x40, 20), # longitude
1641
+ /^[A-Za-z0-9+\/]{8,20}={0,2}\0*$/ or $notEnc = 1, last foreach ($lt, $ln);
1642
+ /^\d{1,5}\.\d+\0*$/ or $notStr = 1, last foreach ($lt, $ln);
1643
+ }
1644
+ if ($notEnc and $notStr) {
1645
+
1646
+ $debug and $et->FoundTag(GPSType => '3a');
1647
+ # decode freeGPS from ViofoA119v3 dashcam (similar to Novatek GPS format)
1648
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1649
+ # 0010: 05 00 00 00 2f 00 00 00 03 00 00 00 13 00 00 00 [..../...........]
1650
+ # 0020: 09 00 00 00 1b 00 00 00 41 4e 57 00 25 d1 99 45 [........ANW.%..E]
1651
+ # 0030: f1 47 40 46 66 66 d2 41 85 eb 83 41 00 00 00 00 [.G@Fff.A...A....]
1652
+ if ($yr >= 2000) {
1653
+ # Kenwood dashcam sometimes stores absolute year and local time
1654
+ # (but sometimes year since 2000 and UTC time in same video!)
1655
+ require Time::Local;
1656
+ my $time = Image::ExifTool::TimeLocal($sec,$min,$hr,$day,$mon-1,$yr);
1657
+ ($sec,$min,$hr,$day,$mon,$yr) = gmtime($time);
1658
+ $yr += 1900;
1659
+ ++$mon;
1660
+ $et->WarnOnce('Converting GPSDateTime to UTC based on local time zone',1);
1661
+ }
1662
+ $lat = GetFloat($dataPt, 0x2c);
1663
+ $lon = GetFloat($dataPt, 0x30);
1664
+ $spd = GetFloat($dataPt, 0x34) * $knotsToKph;
1665
+ $trk = GetFloat($dataPt, 0x38);
1666
+ # (may be all zeros or int16u counting from 1 to 6 if not valid)
1667
+ my $tmp = substr($$dataPt, 60, 12);
1668
+ if ($tmp ne "\0\0\0\0\0\0\0\0\0\0\0\0" and $tmp ne "\x01\0\x02\0\x03\0\x04\0\x05\0\x06\0") {
1669
+ @acc = map { SignedInt32 / 256 } unpack('V3', $tmp);
1670
+ }
1671
+
1672
+ } else {
1673
+
1674
+ $debug and $et->FoundTag(GPSType => '3b');
1675
+ # decode freeGPS from E-ACE B44 dashcam
1676
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1677
+ # 0010: 08 00 00 00 22 00 00 00 01 00 00 00 18 00 00 00 [...."...........]
1678
+ # 0020: 08 00 00 00 10 00 00 00 41 4e 45 00 67 4e 69 69 [........ANE.gNii]
1679
+ # 0030: 5a 38 4a 54 74 48 63 61 36 74 77 3d 00 00 00 00 [Z8JTtHca6tw=....]
1680
+ # 0040: 68 74 75 69 5a 4d 4a 53 73 58 55 58 37 4e 6f 3d [htuiZMJSsXUX7No=]
1681
+ # 0050: 00 00 00 00 64 3b ac 41 e1 3a 1d 43 2b 01 00 00 [....d;.A.:.C+...]
1682
+ # 0060: fd ff ff ff 43 00 00 00 32 4a 37 31 50 70 55 48 [....C...2J71PpUH]
1683
+ # 0070: 37 69 68 66 00 00 00 00 00 00 00 00 00 00 00 00 [7ihf............]
1684
+ # (16-byte string at 0x68 is base64 encoded and encrypted 'luckychip' string)
1685
+ $spd = GetFloat($dataPt, 0x54) * $knotsToKph;
1686
+ $trk = GetFloat($dataPt, 0x58);
1687
+ @acc = map SignedInt32, unpack('x92V3', $$dataPt);
1688
+ # (accelerometer scaling is roughly 1G=250-300, but it varies depending on the axis,
1689
+ # so leave the values as raw. The axes are positive acceleration up,left,forward)
1690
+ if ($notEnc) { # (not encrypted)
1691
+ ($lat = $lt) =~ s/\0+$//;
1692
+ ($lon = $ln) =~ s/\0+$//;
1693
+ } else {
1694
+ # decode base64 strings
1695
+ require Image::ExifTool::XMP;
1696
+ $_ = ${Image::ExifTool::XMP::DecodeBase64($_)} foreach ($lt, $ln);
1697
+ # try various keys to decrypt lat/lon
1698
+ my ($i, $ch, $key) = (0, 'a', $luckyKeys[0]);
1699
+ for (; $i<20; ++$i) {
1700
+ $i and ($key = $luckyKeys[1]) =~ s/#/$ch/g, ++$ch;
1701
+ ($lat = DecryptLucky($lt, $key)) =~ /^\d{1,4}\.\d+$/ or undef($lat), next;
1702
+ ($lon = DecryptLucky($ln, $key)) =~ /^\d{1,5}\.\d+$/ or undef($lon), next;
1703
+ last;
1704
+ }
1705
+ $lon or $et->WarnOnce('Unknown encryption for latitude/longitude');
1706
+ }
1625
1707
  }
1626
1708
 
1627
1709
  } elsif ($$dataPt =~ /^.{21}\0\0\0A([NS])([EW])/s) {
@@ -1682,7 +1764,7 @@ sub ProcessFreeGPS($$$)
1682
1764
  $trk -= 360 if $trk >= 360;
1683
1765
  undef @acc;
1684
1766
  } else {
1685
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc; # (NC)
1767
+ @acc = map { SignedInt32 / 1000 } @acc; # (NC)
1686
1768
  }
1687
1769
 
1688
1770
  } elsif ($$dataPt =~ /^.{60}4W`b]S</s and length($$dataPt) >= 140) {
@@ -1786,7 +1868,7 @@ sub ProcessFreeGPS($$$)
1786
1868
  return 0;
1787
1869
  }
1788
1870
  # (not sure about acc scaling)
1789
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc;
1871
+ @acc = map { SignedInt32 / 1000 } @acc;
1790
1872
  $lon = GetFloat($dataPt, 0x5c);
1791
1873
  $lat = GetFloat($dataPt, 0x60);
1792
1874
  $spd = GetFloat($dataPt, 0x64) * $knotsToKph;
@@ -1931,7 +2013,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
1931
2013
  # 0x7c - int32s[3] accelerometer * 1000
1932
2014
  ($latRef, $lonRef) = ($1, $2);
1933
2015
  ($hr,$min,$sec,$yr,$mon,$day,@acc) = unpack('x48V3x52V6', $$dataPt);
1934
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc;
2016
+ @acc = map { SignedInt32 / 1000 } @acc;
1935
2017
  $lat = GetDouble($dataPt, 0x40);
1936
2018
  $lon = GetDouble($dataPt, 0x50);
1937
2019
  $spd = GetDouble($dataPt, 0x60) * $knotsToKph;
@@ -1947,8 +2029,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
1947
2029
  $lon = abs(GetFloat(\$dat, 8)); # (abs just to be safe)
1948
2030
  $spd = GetFloat(\$dat, 12) * $knotsToKph;
1949
2031
  $trk = GetFloat(\$dat, 16);
1950
- @acc = unpack('x20V3', $dat);
1951
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000 } @acc;
2032
+ @acc = map SignedInt32, unpack('x20V3', $dat);
1952
2033
  ConvertLatLon($lat, $lon);
1953
2034
  $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1954
2035
  $et->HandleTag($tagTbl, GPSLatitude => $lat * (substr($dat,1,1) eq 'S' ? -1 : 1));
@@ -1982,7 +2063,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
1982
2063
  $lon = abs(GetDouble($dataPt, 48)); # (abs just to be safe)
1983
2064
  $spd = GetDouble($dataPt, 64) * $knotsToKph;
1984
2065
  $trk = GetDouble($dataPt, 72);
1985
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc; # (NC)
2066
+ @acc = map { SignedInt32 / 1000 } @acc; # (NC)
1986
2067
  # (not necessary to read RMC sentence because we already have it all)
1987
2068
 
1988
2069
  } elsif ($$dataPt =~ /^.{72}A[NS][EW]\0/s) {
@@ -2049,9 +2130,41 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2049
2130
  }
2050
2131
  }
2051
2132
 
2052
- } else {
2133
+ } elsif ($$dataPt =~ m<^.{23}(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2}) [N|S]>s) {
2053
2134
 
2054
2135
  $debug and $et->FoundTag(GPSType => 16);
2136
+ # XGODY 12" 4K Dashcam
2137
+ # 0000: 00 00 00 a8 66 72 65 65 47 50 53 20 98 00 00 00 [....freeGPS ....]
2138
+ # 0010: 6e 6f 72 6d 61 6c 3a 32 30 32 34 2f 30 35 2f 32 [normal:2024/05/2]
2139
+ # 0020: 32 20 30 32 3a 35 34 3a 32 39 20 4e 3a 34 32 2e [2 02:54:29 N:42.]
2140
+ # 0030: 33 38 32 34 37 30 20 57 3a 38 33 2e 33 38 39 35 [382470 W:83.3895]
2141
+ # 0040: 37 30 20 35 33 2e 36 20 6b 6d 2f 68 20 78 3a 2d [70 53.6 km/h x:-]
2142
+ # 0050: 30 2e 30 32 20 79 3a 30 2e 39 39 20 7a 3a 30 2e [0.02 y:0.99 z:0.]
2143
+ # 0060: 31 30 20 41 3a 32 36 39 2e 32 20 48 3a 32 34 35 [10 A:269.2 H:245]
2144
+ # 0070: 2e 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [.5..............]
2145
+ ($yr,$mon,$day,$hr,$min,$sec) = ($1,$2,$3,$4,$5,$6);
2146
+ $$dataPt =~ s/\0+$//; # remove trailing nulls
2147
+ my @a = split ' ', substr($$dataPt,43);
2148
+ $ddd = 1;
2149
+ foreach (@a) {
2150
+ unless (/^([A-Z]):([-+]?\d+(\.\d+)?)$/i) {
2151
+ # (the "km/h" after spd is display units? because the value is stored in knots)
2152
+ defined $lon and not defined $spd and /^\d+\.\d+$/ and $spd = $_ * $knotsToKph;
2153
+ next;
2154
+ }
2155
+ ($1 eq 'N' or $1 eq 'S') and $lat = $2, $latRef = $1, next;
2156
+ ($1 eq 'E' or $1 eq 'W') and $lon = $2, $lonRef = $1, next;
2157
+ ($1 eq 'x' or $1 eq 'y' or $1 eq 'z') and push(@acc,$2), next;
2158
+ $1 eq 'A' and $trk = $2, next; # (verified, but why 'A'?)
2159
+ # seen 'H' - one might expect altitude ('H'eight), but it doesn't fit
2160
+ # the sample data, so save all other information as an "Unknown_X" tag
2161
+ $$tagTbl{$1} or AddTagToTable($tagTbl, $1, { Name => "Unknown_$1", Unknown => 1 });
2162
+ push(@xtra, $1 => $2), next;
2163
+ }
2164
+
2165
+ } else {
2166
+
2167
+ $debug and $et->FoundTag(GPSType => 17);
2055
2168
  # (look for binary GPS as stored by Nextbase 512G, ref PH)
2056
2169
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 01 00 00 [....freeGPS x...]
2057
2170
  # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............]
@@ -2084,7 +2197,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2084
2197
  $day < 1 or $day > 31 or
2085
2198
  $hr > 59 or $min > 59 or $sec > 600;
2086
2199
  # change lat/lon to signed integer and divide by 1e7
2087
- map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1e7 } $lat, $lon;
2200
+ ($lat, $lon) = map { SignedInt32 / 1e7 } $lat, $lon;
2088
2201
  $trk -= 0x10000 if $trk >= 0x8000; # make it signed
2089
2202
  $trk /= 100;
2090
2203
  $trk += 360 if $trk < 0;
@@ -2115,7 +2228,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2115
2228
  my $time = sprintf('%.2d:%.2d:%sZ',$hr,$min,$sec);
2116
2229
  $et->HandleTag($tagTbl, GPSTimeStamp => $time);
2117
2230
  }
2118
- if (defined $lat) {
2231
+ if (defined $lat and defined $lon) {
2119
2232
  # lat/long are in DDDMM.MMMM format unless $ddd is set
2120
2233
  ConvertLatLon($lat, $lon) unless $ddd;
2121
2234
  $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
@@ -2680,6 +2793,53 @@ sub ProcessRIFFTrailer($$$)
2680
2793
  return 1;
2681
2794
  }
2682
2795
 
2796
+ #------------------------------------------------------------------------------
2797
+ # Process Kenwood Dashcam trailer (forum16229)
2798
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2799
+ # Returns: 1 on success
2800
+ # Sample data (chained 512-byte records starting like this):
2801
+ # 0000: 43 43 43 43 43 43 43 43 43 43 43 43 43 43 47 50 [CCCCCCCCCCCCCCGP]
2802
+ # 0010: 53 44 41 54 41 2d 2d 32 30 32 34 30 37 31 31 31 [SDATA--202407111]
2803
+ # 0020: 32 30 34 31 32 4e 35 30 2e 36 31 32 33 38 36 30 [20412N50.6123860]
2804
+ # 0030: 36 37 37 45 38 2e 37 30 32 37 31 38 30 39 38 39 [677E8.7027180989]
2805
+ # 0040: 35 33 33 2e 30 30 30 30 30 30 30 30 30 30 30 30 [533.000000000000]
2806
+ # 0050: 2e 30 30 30 30 30 30 30 30 30 30 30 30 30 2e 30 [.0000000000000.0]
2807
+ # 0060: 31 39 39 39 39 39 39 39 35 35 33 2d 30 2e 30 39 [19999999553-0.09]
2808
+ # 0070: 30 30 30 30 30 30 33 35 37 2d 30 2e 31 34 30 30 [000000357-0.1400]
2809
+ # 0080: 30 30 30 30 30 35 39 47 50 53 44 41 54 41 2d 2d [0000059GPSDATA--]
2810
+ sub ProcessKenwoodTrailer($$$)
2811
+ {
2812
+ my ($et, $dirInfo, $tagTbl) = @_;
2813
+ my $raf = $$dirInfo{RAF};
2814
+ my $buff;
2815
+ # current file position is 8 bytes into the 14 C's, so test the next 6:
2816
+ $raf->Read($buff, 14) and $buff eq 'CCCCCCCCCCCCCC' or return 0;
2817
+ $et->VerboseDir('Kenwood trailer', undef, undef);
2818
+ unless ($$et{OPTIONS}{ExtractEmbedded}) {
2819
+ $et->WarnOnce('Use the ExtractEmbedded option to extract timed GPSData from Kenwood trailer',3);
2820
+ return 1;
2821
+ }
2822
+ while ($raf->Read($buff, 121) and $buff =~ /^GPSDATA--(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/) {
2823
+ FoundSomething($et, $tagTbl);
2824
+ $et->HandleTag($tagTbl, GPSDateTime => "$1:$2:$3 $4:$5:$6");
2825
+ my $i = 9 + 14;
2826
+ my ($val, @acc, $tag);
2827
+ foreach $tag (qw(GPSLatitude GPSLongitude GPSSpeed unk acc acc acc)) {
2828
+ $val = substr($buff, $i, 14); $i += 14;
2829
+ next if $tag eq 'unk';
2830
+ my $hemi;
2831
+ $hemi = $1 if $val =~ s/^([NSEW])//;
2832
+ $val =~ /^[-+]?\d+\.\d+$/ or next;
2833
+ $tag eq 'acc' and push(@acc,$val), next;
2834
+ $val = -$val if $hemi and ($hemi eq 'S' or $hemi eq 'W');
2835
+ $et->HandleTag($tagTbl, $tag => $val);
2836
+ }
2837
+ $et->HandleTag($tagTbl, Accelerometer => "@acc") if @acc == 3;
2838
+ }
2839
+ delete $$et{DOC_NUM};
2840
+ return 1;
2841
+ }
2842
+
2683
2843
  #------------------------------------------------------------------------------
2684
2844
  # Process 'gps ' atom containing NMEA from Pittasoft Blackvue dashcam (ref PH)
2685
2845
  # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
@@ -3353,6 +3513,7 @@ sub ScanMediaData($)
3353
3513
  }
3354
3514
  my $dirInfo = { DataPt => \$buff, DataPos => $pos + $dataPos, DirLen => $len };
3355
3515
  ProcessFreeGPS($et, $dirInfo, $tagTbl);
3516
+ $$et{FoundGPSByScan} = 1;
3356
3517
  }
3357
3518
  $pos += $len;
3358
3519
  $buf2 = substr($buff, $len);
@@ -22,13 +22,13 @@ use vars qw($VERSION %samsungLensTypes);
22
22
  use Image::ExifTool qw(:DataAccess :Utils);
23
23
  use Image::ExifTool::Exif;
24
24
 
25
- $VERSION = '1.56';
25
+ $VERSION = '1.58';
26
26
 
27
27
  sub WriteSTMN($$$);
28
28
  sub ProcessINFO($$$);
29
29
  sub ProcessSamsungMeta($$$);
30
30
  sub ProcessSamsungIFD($$$);
31
- sub ProcessSamsung($$$);
31
+ sub ProcessSamsung($$;$);
32
32
 
33
33
  # Samsung LensType lookup
34
34
  %samsungLensTypes = (
@@ -943,25 +943,25 @@ my %formatMinMax = (
943
943
  );
944
944
 
945
945
  # information extracted from Samsung trailer (ie. Samsung SM-T805 "Sound & Shot" JPEG) (ref PH)
946
+ # NOTE: These tags may use $$self{SamsungTagName} in a Condition statement
947
+ # if necessary to differentiate tags with the same ID but different names
946
948
  %Image::ExifTool::Samsung::Trailer = (
947
949
  GROUPS => { 0 => 'MakerNotes', 2 => 'Other' },
948
950
  VARS => { NO_ID => 1, HEX_ID => 0 },
949
951
  PROCESS_PROC => \&ProcessSamsung,
952
+ TAG_PREFIX => 'SamsungTrailer',
950
953
  PRIORITY => 0, # (first one takes priority so DepthMapWidth/Height match first DepthMapData)
951
954
  NOTES => q{
952
- Tags extracted from the trailer of JPEG images written when using certain
953
- features (such as "Sound & Shot" or "Shot & More") from Samsung models such
954
- as the Galaxy S4 and Tab S, and from the 'sefd' atom in HEIC images from the
955
- Samsung S10+.
956
- },
957
- '0x0001-name' => {
958
- Name => 'EmbeddedImageName', # ("DualShot_1","DualShot_2")
959
- RawConv => '$$self{EmbeddedImageName} = $val',
955
+ Tags extracted from the SEFT trailer of JPEG and PNG images written when
956
+ using certain features (such as "Sound & Shot" or "Shot & More") from
957
+ Samsung models such as the Galaxy S4 and Tab S, and from the 'sefd' atom in
958
+ HEIC images from models such as the S10+.
960
959
  },
960
+ '0x0001-name' => 'EmbeddedImageName', # ("DualShot_1","DualShot_2","SingleShot")
961
961
  '0x0001' => [
962
962
  {
963
963
  Name => 'EmbeddedImage',
964
- Condition => '$$self{EmbeddedImageName} eq "DualShot_1"',
964
+ Condition => '$$self{SamsungTagName} ne "DualShot_2"',
965
965
  Groups => { 2 => 'Preview' },
966
966
  Binary => 1,
967
967
  },
@@ -970,6 +970,7 @@ my %formatMinMax = (
970
970
  Groups => { 2 => 'Preview' },
971
971
  Binary => 1,
972
972
  },
973
+ # (have also seen the string "BOKEH" here (SM-A226B)
973
974
  ],
974
975
  '0x0100-name' => 'EmbeddedAudioFileName', # ("SoundShot_000")
975
976
  '0x0100' => { Name => 'EmbeddedAudioFile', Groups => { 2 => 'Audio' }, Binary => 1 },
@@ -1265,6 +1266,7 @@ my %formatMinMax = (
1265
1266
  '0x0b40' => { # (SM-N975X front camera)
1266
1267
  Name => 'SingleShotMeta',
1267
1268
  SubDirectory => { TagTable => 'Image::ExifTool::Samsung::SingleShotMeta' },
1269
+ # (have also see the string "BOKEH_INFO" here (SM-A226B)
1268
1270
  },
1269
1271
  # 0x0b41-name - seen 'SingeShot_DepthMap_1' (Yes, "Singe") (SM-N975X front camera)
1270
1272
  '0x0b41' => { Name => 'SingleShotDepthMap', Binary => 1 },
@@ -1277,8 +1279,10 @@ my %formatMinMax = (
1277
1279
  # 0x0bd0-name - seen 'Dual_Relighting_Bokeh_Info' #forum16086
1278
1280
  # 0x0be0-name - seen 'Livefocus_JDM_Info' #forum16086
1279
1281
  # 0x0bf0-name - seen 'Remaster_Info' #forum16086
1282
+ '0x0bf0' => 'RemasterInfo', #forum16086/16242
1280
1283
  # 0x0c21-name - seen 'Portrait_Effect_Info' #forum16086
1281
1284
  # 0x0c51-name - seen 'Samsung_Capture_Info' #forum16086
1285
+ '0x0c51' => 'SamsungCaptureInfo', #forum16086/16242
1282
1286
  # 0x0c61-name - seen 'Camera_Capture_Mode_Info' #forum16086
1283
1287
  # 0x0c71-name - seen 'Pro_White_Balance_Info' #forum16086
1284
1288
  # 0x0c81-name - seen 'Watermark_Info' #forum16086
@@ -1289,7 +1293,11 @@ my %formatMinMax = (
1289
1293
  # 0x0d11-name - seen 'Video_Snapshot_Info' #forum16086
1290
1294
  # 0x0d21-name - seen 'Camera_Scene_Info' #forum16086
1291
1295
  # 0x0d31-name - seen 'Food_Blur_Effect_Info' #forum16086
1292
- # 0x0d91-name - seen 'PEg_Info' #forum16086
1296
+ '0x0d91' => { #forum16086/16242
1297
+ Name => 'PEg_Info',
1298
+ Description => 'PEg Info',
1299
+ SubDirectory => { TagTable => 'Image::ExifTool::JSON::Main' },
1300
+ },
1293
1301
  # 0x0da1-name - seen 'Captured_App_Info' #forum16086
1294
1302
  # 0xa050-name - seen 'Jpeg360_2D_Info' (Samsung Gear 360)
1295
1303
  # 0xa050 - seen 'Jpeg3602D' (Samsung Gear 360)
@@ -1487,7 +1495,7 @@ sub ProcessSamsungMeta($$$)
1487
1495
  my $pos = $$dirInfo{DirStart};
1488
1496
  my $end = $$dirInfo{DirLen} + $pos;
1489
1497
  unless ($pos + 8 <= $end and substr($$dataPt, $pos, 4) eq 'DOFS') {
1490
- $et->Warn("Unrecognized $dirName data");
1498
+ $et->Warn("Unrecognized $dirName data", 1);
1491
1499
  return 0;
1492
1500
  }
1493
1501
  my $ver = Get32u($dataPt, $pos + 4);
@@ -1563,7 +1571,7 @@ sub ProcessSamsungIFD($$$)
1563
1571
  # Returns: 1 on success, 0 not valid Samsung trailer, or -1 error writing
1564
1572
  # - updates DataPos to point to start of Samsung trailer
1565
1573
  # - updates DirLen to existing trailer length
1566
- sub ProcessSamsung($$$)
1574
+ sub ProcessSamsung($$;$)
1567
1575
  {
1568
1576
  my ($et, $dirInfo) = @_;
1569
1577
  my $raf = $$dirInfo{RAF};
@@ -1653,8 +1661,13 @@ SamBlock:
1653
1661
  $audioSize = $size - 8 - $len;
1654
1662
  next;
1655
1663
  }
1656
- # add unknown tags if necessary
1664
+ last unless $raf->Seek($dirPos-$noff, 0) and $raf->Read($buf2, $size) == $size;
1665
+ # (could validate the first 4 bytes of the block because they
1666
+ # are the same as the first 4 bytes of the directory entry)
1667
+ $len = Get32u(\$buf2, 4);
1668
+ last if $len + 8 > $size;
1657
1669
  my $tag = sprintf("0x%.4x", $type);
1670
+ # add unknown tags if necessary
1658
1671
  unless ($$tagTablePtr{$tag}) {
1659
1672
  next unless $unknown or $verbose;
1660
1673
  my %tagInfo = (
@@ -1673,11 +1686,8 @@ SamBlock:
1673
1686
  );
1674
1687
  AddTagToTable($tagTablePtr, "$tag-name", \%tagInfo2);
1675
1688
  }
1676
- last unless $raf->Seek($dirPos-$noff, 0) and $raf->Read($buf2, $size) == $size;
1677
- # (could validate the first 4 bytes of the block because they
1678
- # are the same as the first 4 bytes of the directory entry)
1679
- $len = Get32u(\$buf2, 4);
1680
- last if $len + 8 > $size;
1689
+ # set SamsungTagName ExifTool member for use in tag Condition
1690
+ $$et{SamsungTagName} = substr($buf2, 8, $len);
1681
1691
  # extract tag name and value
1682
1692
  $et->HandleTag($tagTablePtr, "$tag-name", undef,
1683
1693
  DataPt => \$buf2,
@@ -1691,6 +1701,7 @@ SamBlock:
1691
1701
  Start => 8 + $len,
1692
1702
  Size => $size - (8 + $len),
1693
1703
  );
1704
+ delete $$et{SamsungTagName};
1694
1705
  }
1695
1706
  if ($outfile) {
1696
1707
  last unless $raf->Seek($dataPos, 0) and $raf->Read($buff, $dirLen) == $dirLen;