exiftool_vendored 13.04.0 → 13.08.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/Changes +48 -0
  3. data/bin/MANIFEST +1 -0
  4. data/bin/META.json +1 -1
  5. data/bin/META.yml +1 -1
  6. data/bin/README +2 -2
  7. data/bin/exiftool +30 -23
  8. data/bin/lib/Image/ExifTool/AIFF.pm +1 -1
  9. data/bin/lib/Image/ExifTool/APE.pm +1 -1
  10. data/bin/lib/Image/ExifTool/ASF.pm +1 -1
  11. data/bin/lib/Image/ExifTool/BuildTagLookup.pm +4 -3
  12. data/bin/lib/Image/ExifTool/Canon.pm +19 -1
  13. data/bin/lib/Image/ExifTool/DJI.pm +91 -29
  14. data/bin/lib/Image/ExifTool/Exif.pm +2 -2
  15. data/bin/lib/Image/ExifTool/FITS.pm +2 -2
  16. data/bin/lib/Image/ExifTool/FLIF.pm +2 -2
  17. data/bin/lib/Image/ExifTool/FlashPix.pm +11 -11
  18. data/bin/lib/Image/ExifTool/Font.pm +1 -1
  19. data/bin/lib/Image/ExifTool/Geolocation.pm +2 -1
  20. data/bin/lib/Image/ExifTool/GoPro.pm +3 -3
  21. data/bin/lib/Image/ExifTool/HP.pm +1 -1
  22. data/bin/lib/Image/ExifTool/ID3.pm +3 -3
  23. data/bin/lib/Image/ExifTool/IPTC.pm +2 -2
  24. data/bin/lib/Image/ExifTool/InDesign.pm +1 -1
  25. data/bin/lib/Image/ExifTool/JPEG.pm +19 -4
  26. data/bin/lib/Image/ExifTool/Jpeg2000.pm +6 -6
  27. data/bin/lib/Image/ExifTool/M2TS.pm +39 -9
  28. data/bin/lib/Image/ExifTool/MXF.pm +2 -2
  29. data/bin/lib/Image/ExifTool/Matroska.pm +1 -1
  30. data/bin/lib/Image/ExifTool/Microsoft.pm +1 -1
  31. data/bin/lib/Image/ExifTool/PDF.pm +15 -15
  32. data/bin/lib/Image/ExifTool/PLIST.pm +1 -1
  33. data/bin/lib/Image/ExifTool/PNG.pm +4 -4
  34. data/bin/lib/Image/ExifTool/Panasonic.pm +1 -1
  35. data/bin/lib/Image/ExifTool/PhaseOne.pm +3 -3
  36. data/bin/lib/Image/ExifTool/Photoshop.pm +63 -3
  37. data/bin/lib/Image/ExifTool/Protobuf.pm +242 -0
  38. data/bin/lib/Image/ExifTool/QuickTime.pm +23 -14
  39. data/bin/lib/Image/ExifTool/QuickTimeStream.pl +395 -109
  40. data/bin/lib/Image/ExifTool/README +4 -1
  41. data/bin/lib/Image/ExifTool/RIFF.pm +3 -3
  42. data/bin/lib/Image/ExifTool/RTF.pm +1 -1
  43. data/bin/lib/Image/ExifTool/Ricoh.pm +3 -3
  44. data/bin/lib/Image/ExifTool/Sony.pm +4 -3
  45. data/bin/lib/Image/ExifTool/TagInfoXML.pm +4 -3
  46. data/bin/lib/Image/ExifTool/TagLookup.pm +6988 -6967
  47. data/bin/lib/Image/ExifTool/TagNames.pod +85 -5
  48. data/bin/lib/Image/ExifTool/VCard.pm +2 -2
  49. data/bin/lib/Image/ExifTool/Validate.pm +3 -3
  50. data/bin/lib/Image/ExifTool/WriteExif.pl +2 -2
  51. data/bin/lib/Image/ExifTool/WriteQuickTime.pl +4 -4
  52. data/bin/lib/Image/ExifTool/WriteXMP.pl +2 -2
  53. data/bin/lib/Image/ExifTool/Writer.pl +17 -17
  54. data/bin/lib/Image/ExifTool/XMP.pm +20 -10
  55. data/bin/lib/Image/ExifTool/XMP2.pl +38 -0
  56. data/bin/lib/Image/ExifTool/ZIP.pm +1 -1
  57. data/bin/lib/Image/ExifTool.pm +109 -82
  58. data/bin/lib/Image/ExifTool.pod +8 -7
  59. data/bin/perl-Image-ExifTool.spec +1 -1
  60. data/lib/exiftool_vendored/version.rb +1 -1
  61. metadata +3 -2
@@ -94,6 +94,7 @@ my %insvDataLen = (
94
94
  # 0xb00 => 10, # ? (Insta360 X3)
95
95
  # 0xd00 => 10, # ? (Insta360 Ace Pro)
96
96
  # 0x1200 ? # ? (Insta360 Ace Pro)
97
+ # 0x1600 ? # ? (?)
97
98
  );
98
99
 
99
100
  # limit the default amount of data we read for some record types
@@ -109,7 +110,7 @@ my %insvLimit = (
109
110
  The tags below are extracted from timed metadata in QuickTime and other
110
111
  formats of video files when the ExtractEmbedded option is used. Although
111
112
  most of these tags are combined into the single table below, ExifTool
112
- currently reads 82 different formats of timed GPS metadata from video files.
113
+ currently reads 96 different types of timed GPS metadata from video files.
113
114
  },
114
115
  VARS => { NO_ID => 1 },
115
116
  GPSLatitude => { PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")', RawConv => '$$self{FoundGPSLatitude} = 1; $val' },
@@ -339,14 +340,22 @@ my %insvLimit = (
339
340
  Groups => { 2 => 'Preview' },
340
341
  RawConv => '$self->ValidateImage(\$val,$tag)',
341
342
  },
342
- # djmd - DJI AC003 Osmo Action 4 cam
343
- #TODO djmd => { SubDirectory => { TagTable => 'Image::ExifTool::DJI::djmd', ByteOrder => 'Little-Endian' } },
344
- # (also DJI_20240615181302_0006_D.LRF)
345
- # dbgi - DJI AC003 Osmo Action 4 cam -- lots more unknown stuff
343
+ djmd => { # (DJI AC003 Osmo Action 4 cam)
344
+ Name => 'DJIMetadata',
345
+ SubDirectory => { TagTable => 'Image::ExifTool::DJI::Protobuf' },
346
+ },
347
+ dbgi => { # (DJI AC003 Osmo Action 4 cam)
348
+ Name => 'DJIDebug',
349
+ Unknown => 2,
350
+ Notes => 'extracted only if Unknown option is 2 or greater',
351
+ SubDirectory => { TagTable => 'Image::ExifTool::DJI::Protobuf' },
352
+ },
346
353
  Unknown00 => { Unknown => 1 },
347
354
  Unknown01 => { Unknown => 1 },
348
355
  Unknown02 => { Unknown => 1 },
349
356
  Unknown03 => { Unknown => 1 },
357
+ M => { Name => 'Unknown_M', Unknown => 1 }, # (from LIGOGPSINFO)
358
+ H => { Name => 'Unknown_H', Unknown => 1 }, # (from LIGOGPSINFO)
350
359
  );
351
360
 
352
361
  # tags found in 'camm' type 0 timed metadata (ref 4)
@@ -894,7 +903,7 @@ sub FoundSomething($$;$$)
894
903
  #------------------------------------------------------------------------------
895
904
  # Approximate GPSDateTime value from sample time and CreateDate
896
905
  # Inputs: 0) ExifTool ref, 1) tag table ptr, 2) sample time (s)
897
- # 3) true if CreateDate is at end of video, 4) flag if CreateDate is UTC
906
+ # 3) true if CreateDate is UTC
898
907
  # Notes: Uses ExifTool CreateDateAtEnd as flag to subtract video duration
899
908
  sub SetGPSDateTime($$$;$)
900
909
  {
@@ -905,9 +914,9 @@ sub SetGPSDateTime($$$;$)
905
914
  if ($$et{CreateDateAtEnd}) { # adjust if CreateDate is at end of video
906
915
  return unless $$value{TimeScale} and $$value{Duration};
907
916
  $sampleTime -= $$value{Duration} / $$value{TimeScale};
908
- $et->WarnOnce('Approximating GPSDateTime as CreateDate - Duration + SampleTime', 1);
917
+ $et->Warn('Approximating GPSDateTime as CreateDate - Duration + SampleTime', 1);
909
918
  } else {
910
- $et->WarnOnce('Approximating GPSDateTime as CreateDate + SampleTime', 1);
919
+ $et->Warn('Approximating GPSDateTime as CreateDate + SampleTime', 1);
911
920
  }
912
921
  my $utc = $et->Options('QuickTimeUTC');
913
922
  $utc = $isUTC unless defined $utc; # (allow QuickTimeUTC=0 to override $isUTC default)
@@ -1268,7 +1277,7 @@ sub ProcessSamples($)
1268
1277
  ($startChunk, $samplesPerChunk, $descIdx) = @{shift @$stsc};
1269
1278
  $nextChunk = $$stsc[0][0] if @$stsc;
1270
1279
  }
1271
- @$size < @$start + $samplesPerChunk and $et->WarnOnce('Sample size error'), last;
1280
+ @$size < @$start + $samplesPerChunk and $et->Warn('Sample size error'), last;
1272
1281
  last unless defined $chunkStart and length $chunkStart;
1273
1282
  my $sampleStart = $chunkStart;
1274
1283
  my $chunkSize = 0;
@@ -1296,7 +1305,7 @@ Sample: for ($i=0; ; ) {
1296
1305
  push @chunkSize, $chunkSize;
1297
1306
  ++$iChunk;
1298
1307
  }
1299
- @$start == @$size or $et->WarnOnce('Incorrect sample start/size count'), return;
1308
+ @$start == @$size or $et->Warn('Incorrect sample start/size count'), return;
1300
1309
  # process as chunks if we are only interested in calculating hash
1301
1310
  if ($type eq 'soun' or $type eq 'vide') {
1302
1311
  $start = $stco;
@@ -1326,7 +1335,7 @@ Sample: for ($i=0; ; ) {
1326
1335
  $hdrFmt = ($hdrLen == 4 ? 'N' : $hdrLen == 2 ? 'n' : 'C');
1327
1336
  require Image::ExifTool::H264;
1328
1337
  }
1329
-
1338
+
1330
1339
  # loop through all samples
1331
1340
  for ($i=0; $i<@$start and $i<@$size; ++$i) {
1332
1341
 
@@ -1346,11 +1355,11 @@ Sample: for ($i=0; ; ) {
1346
1355
  }
1347
1356
  }
1348
1357
  # read the sample data
1349
- $raf->Seek($$start[$i], 0) or $et->WarnOnce("Seek error in $type data"), next;
1358
+ $raf->Seek($$start[$i], 0) or $et->Warn("Seek error in $type data"), next;
1350
1359
  my $buff;
1351
1360
  my $n = $raf->Read($buff, $size);
1352
1361
  unless ($n == $size) {
1353
- $et->WarnOnce("Error reading $type data");
1362
+ $et->Warn("Error reading $type data");
1354
1363
  next unless $n;
1355
1364
  $size = $n;
1356
1365
  }
@@ -1432,7 +1441,7 @@ Sample: for ($i=0; ; ) {
1432
1441
 
1433
1442
  if ($$tagTbl{$metaFormat}) {
1434
1443
  my $tagInfo = $et->GetTagInfo($tagTbl, $metaFormat, \$buff);
1435
- if ($tagInfo) {
1444
+ if ($tagInfo and (not $$tagInfo{Unknown} or $$et{OPTIONS}{Unknown} >= $$tagInfo{Unknown})) {
1436
1445
  FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
1437
1446
  $$et{ee} = $ee; # need ee information for 'keys'
1438
1447
  $et->HandleTag($tagTbl, $metaFormat, undef,
@@ -1442,6 +1451,15 @@ Sample: for ($i=0; ; ) {
1442
1451
  TagInfo => $tagInfo,
1443
1452
  );
1444
1453
  delete $$et{ee};
1454
+ # synthesize GPSDateTime if necessary for djmd metadata
1455
+ if ($metaFormat eq 'djmd') {
1456
+ if (defined $$et{GPSLatitude} and defined $$et{GPSLongitude} and not $$et{GPSDateTime}) {
1457
+ SetGPSDateTime($et, $tagTbl, $time[$i], 1); # (NC)
1458
+ }
1459
+ delete $$et{GPSLatitude};
1460
+ delete $$et{GPSLongitude};
1461
+ delete $$et{GPSDateTime};
1462
+ }
1445
1463
  } elsif ($metaFormat eq 'camm' and $buff =~ /^X/) {
1446
1464
  # seen 'camm' metadata in this format (X/Y/Z acceleration and G force? + GPRMC + ?)
1447
1465
  # "X0000.0000Y0000.0000Z0000.0000G0000.0000$GPRMC,000125,V,,,,,000.0,,280908,002.1,N*71~, 794021 \x0a"
@@ -1545,7 +1563,7 @@ sub ProcessFreeGPS($$$)
1545
1563
  my ($et, $dirInfo, $tagTbl) = @_;
1546
1564
  my $dataPt = $$dirInfo{DataPt};
1547
1565
  my $dirLen = length $$dataPt;
1548
- my ($yr, $mon, $day, $hr, $min, $sec, $stat, $lbl, $ddd);
1566
+ my ($yr, $mon, $day, $hr, $min, $sec, $ss, $stat, $lbl, $ddd, $done);
1549
1567
  my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, @acc, @xtra);
1550
1568
 
1551
1569
  return 0 if $dirLen < 82;
@@ -1670,7 +1688,7 @@ sub ProcessFreeGPS($$$)
1670
1688
  }
1671
1689
  if ($notEnc and $notStr) {
1672
1690
 
1673
- $debug and $et->FoundTag(GPSType => '3a');
1691
+ $debug and $et->FoundTag(GPSType => 3);
1674
1692
  # decode freeGPS from ViofoA119v3 dashcam (similar to Novatek GPS format)
1675
1693
  # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1676
1694
  # 0010: 05 00 00 00 2f 00 00 00 03 00 00 00 13 00 00 00 [..../...........]
@@ -1684,7 +1702,7 @@ sub ProcessFreeGPS($$$)
1684
1702
  ($sec,$min,$hr,$day,$mon,$yr) = gmtime($time);
1685
1703
  $yr += 1900;
1686
1704
  ++$mon;
1687
- $et->WarnOnce('Converting GPSDateTime to UTC based on local time zone',1);
1705
+ $et->Warn('Converting GPSDateTime to UTC based on local time zone',1);
1688
1706
  }
1689
1707
  $lat = GetFloat($dataPt, 0x2c);
1690
1708
  $lon = GetFloat($dataPt, 0x30);
@@ -1698,7 +1716,7 @@ sub ProcessFreeGPS($$$)
1698
1716
 
1699
1717
  } else {
1700
1718
 
1701
- $debug and $et->FoundTag(GPSType => '3b');
1719
+ $debug and $et->FoundTag(GPSType => 4);
1702
1720
  # decode freeGPS from E-ACE B44 dashcam
1703
1721
  # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1704
1722
  # 0010: 08 00 00 00 22 00 00 00 01 00 00 00 18 00 00 00 [...."...........]
@@ -1729,40 +1747,73 @@ sub ProcessFreeGPS($$$)
1729
1747
  ($lon = DecryptLucky($ln, $key)) =~ /^\d{1,5}\.\d+$/ or undef($lon), next;
1730
1748
  last;
1731
1749
  }
1732
- $lon or $et->WarnOnce('Unknown encryption for latitude/longitude');
1750
+ $lon or $et->Warn('Unknown encryption for latitude/longitude');
1733
1751
  }
1734
1752
  }
1735
1753
 
1736
- } elsif ($$dataPt =~ /^.{21}\0\0\0A([NS])([EW])/s) {
1754
+ } elsif ($$dataPt =~ /^(.{16}|.{48}|.{80})LIGOGPSINFO\0/s and length($$dataPt) >= length($1) + 0x84) {
1737
1755
 
1738
- $debug and $et->FoundTag(GPSType => 4);
1739
- # also decode 'gpmd' chunk from Kingslim D4 dashcam videos
1740
- # 0000: 0a 00 00 00 0b 00 00 00 07 00 00 00 e5 07 00 00 [................]
1741
- # 0010: 06 00 00 00 03 00 00 00 41 4e 57 31 91 52 83 45 [........ANW1.R.E]
1742
- # 0020: 15 70 fe c5 29 5c c3 41 ae c7 af 42 00 00 d1 be [.p..)\.A...B....]
1743
- # 0030: 00 00 80 3b 00 00 2c 3e 00 00 00 00 00 00 00 00 [...;..,>........]
1744
- # 0040: 00 00 00 00 00 00 00 00 00 00 00 00 26 26 26 26 [............&&&&]
1745
- # 0050: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1746
- # 0060: 01 00 00 00 23 23 23 23 75 00 00 00 c0 22 20 20 [....####u...." ]
1747
- # 0070: 20 f0 12 10 12 21 e5 0e 10 12 2f 90 10 13 01 f2 [ ....!..../.....]
1748
- ($latRef, $lonRef) = ($1, $2);
1749
- ($hr,$min,$sec,$yr,$mon,$day) = unpack("V6", $$dataPt);
1750
- # lat/lon aren't decoded properly, but spd,trk,acc are
1751
- $lat = GetFloat($dataPt, 0x1c);
1752
- $lon = GetFloat($dataPt, 0x20);
1753
- $et->VPrint(0, sprintf("Raw lat/lon = %.9f %.9f\n", $lat, $lon));
1754
- $et->WarnOnce('GPSLatitude/Longitude encryption is not yet known, so these will be wrong');
1755
- $lat = abs $lat;
1756
- $lon = abs $lon;
1757
- $spd = GetFloat($dataPt, 0x24) * $knotsToKph; # (convert knots to km/h)
1758
- $trk = GetFloat($dataPt, 0x28);
1759
- $acc[0] = GetFloat($dataPt, 0x2c);
1760
- $acc[1] = GetFloat($dataPt, 0x30);
1761
- $acc[2] = GetFloat($dataPt, 0x34);
1756
+ $debug and $et->FoundTag(GPSType => 5);
1757
+ my $pos = length $1;
1758
+ # iiway s1 dual dash cam
1759
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1760
+ # 0010: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1761
+ # 0020: 0a 00 00 00 23 23 23 23 6a 00 00 00 c0 20 20 20 [....####j.... ]
1762
+ # 0030: 20 f0 12 10 12 22 e1 0e 10 12 2f 90 10 13 02 f2 [ ...."..../.....]
1763
+ # ABASK A8 4K Dashcam (different scaling factor)
1764
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1765
+ # 0010: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1766
+ # 0020: 00 00 00 00 23 23 23 23 69 00 00 00 c0 20 20 20 [....####i.... ]
1767
+ # 0030: 20 f0 12 10 12 23 e5 0e 10 12 2f 99 10 11 02 f2 [ ....#..../.....]
1768
+ # XGODY 12" 4K Dashcam
1769
+ # 0000: 00 00 00 a8 66 72 65 65 47 50 53 20 98 00 00 00 [....freeGPS ....]
1770
+ # 0010: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1771
+ # 0020: cd 61 00 00 23 23 23 23 6d 00 00 00 c1 ec 41 20 [.a..####m.....A ]
1772
+ # 0030: 20 f0 12 10 12 24 e5 0e 10 11 2f 92 10 12 00 f6 [ ....$..../.....]
1773
+ # Rexing dashcam V1GW-4K
1774
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1775
+ # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1776
+ # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1777
+ # 0030: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1778
+ # 0040: 01 00 00 00 23 23 23 23 73 00 00 00 c0 20 20 20 [....####s.... ]
1779
+ # 0050: 20 f0 12 10 12 23 e5 0e 10 12 2f 95 10 12 01 f3 [ ....#..../.....]
1780
+ # 0060: 16 18 10 26 b4 1a 10 04 f2 6e 18 12 20 f0 0e 11 [...&.....n.. ...]
1781
+ # 0070: 13 22 b3 16 10 01 fb 76 18 10 24 fa 0e 11 10 22 [.".....v..$...."]
1782
+ # Kingslim D4 dashcam
1783
+ # 0000: 0a 00 00 00 0b 00 00 00 07 00 00 00 e5 07 00 00 [................]
1784
+ # 0010: 06 00 00 00 03 00 00 00 41 4e 57 31 91 52 83 45 [........ANW1.R.E]
1785
+ # 0020: 15 70 fe c5 29 5c c3 41 ae c7 af 42 00 00 d1 be [.p..)\.A...B....]
1786
+ # 0030: 00 00 80 3b 00 00 2c 3e 00 00 00 00 00 00 00 00 [...;..,>........]
1787
+ # 0040: 00 00 00 00 00 00 00 00 00 00 00 00 26 26 26 26 [............&&&&]
1788
+ # 0050: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1789
+ # 0060: 01 00 00 00 23 23 23 23 75 00 00 00 c0 22 20 20 [....####u...." ]
1790
+ # 0070: 20 f0 12 10 12 21 e5 0e 10 12 2f 90 10 13 01 f2 [ ....!..../.....]
1791
+ my %dirInfo = ( DataPt => $dataPt, DirStart => $pos, DirName => "LigoGPS_$pos" );
1792
+ # (this is weak, but the only difference I could find between these 2 headers)
1793
+ # (NOTE: ../testpics/gps_video/forum16229.mp4 uses this word for a counter!)
1794
+ $$et{LigoGPSScale} = 3 if $pos == 16 and $$dataPt =~ /^.{12}\xf0\x03\0\0.{16}\0{4}/s;
1795
+ ProcessLigoGPS($et, \%dirInfo, $tagTbl);
1796
+ $done = 1;
1797
+
1798
+ # also... when offset is 0x50 (Kingslim), the GPS also exists in this format:
1799
+ # ($latRef, $lonRef) = ($1, $2);
1800
+ # ($hr,$min,$sec,$yr,$mon,$day) = unpack("V6", $$dataPt);
1801
+ # # lat/lon aren't decoded properly, but spd,trk,acc are
1802
+ # $lat = GetFloat($dataPt, 0x1c);
1803
+ # $lon = GetFloat($dataPt, 0x20);
1804
+ # $et->VPrint(0, sprintf("Raw lat/lon = %.9f %.9f\n", $lat, $lon));
1805
+ # $et->Warn('GPSLatitude/Longitude encryption is not yet known, so these will be wrong');
1806
+ # $lat = abs $lat;
1807
+ # $lon = abs $lon;
1808
+ # $spd = GetFloat($dataPt, 0x24) * $knotsToKph; # (convert knots to km/h)
1809
+ # $trk = GetFloat($dataPt, 0x28);
1810
+ # $acc[0] = GetFloat($dataPt, 0x2c);
1811
+ # $acc[1] = GetFloat($dataPt, 0x30);
1812
+ # $acc[2] = GetFloat($dataPt, 0x34);
1762
1813
 
1763
1814
  } elsif ($$dataPt =~ /^.{60}A\0{3}.{4}([NS])\0{3}.{4}([EW])\0{3}/s) {
1764
1815
 
1765
- $debug and $et->FoundTag(GPSType => 5);
1816
+ $debug and $et->FoundTag(GPSType => 6);
1766
1817
  # decode freeGPS from Akaso dashcam
1767
1818
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 60 00 00 00 [....freeGPS `...]
1768
1819
  # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............]
@@ -1796,7 +1847,7 @@ sub ProcessFreeGPS($$$)
1796
1847
 
1797
1848
  } elsif ($$dataPt =~ /^.{60}4W`b]S</s and length($$dataPt) >= 140) {
1798
1849
 
1799
- $debug and $et->FoundTag(GPSType => 6);
1850
+ $debug and $et->FoundTag(GPSType => 7);
1800
1851
  # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 01 00 00 [..@.freeGPS ....]
1801
1852
  # 0010: 5a 58 53 42 4e 58 59 53 00 00 00 00 00 00 00 00 [ZXSBNXYS........]
1802
1853
  # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
@@ -1806,18 +1857,18 @@ sub ProcessFreeGPS($$$)
1806
1857
  # 0060: 42 3e 49 49 40 42 45 3c 55 3c 45 47 3e 45 43 41 [B>II@BE<U<EG>ECA]
1807
1858
  # decipher $GPRMC by subtracting 16 from each character value
1808
1859
  $_ = pack 'C*', map { $_>=16 and $_-=16 } unpack('x60C80', $$dataPt);
1809
- unless (/[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d*?\d{1,2}\.\d+),([NS]),(\d*?\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/) {
1810
- SetByteOrder($oldOrder);
1811
- return 0;
1860
+ if (/[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d*?\d{1,2}\.\d+),([NS]),(\d*?\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/) {
1861
+ ($yr,$mon,$day,$hr,$min,$sec,$lat,$latRef,$lon,$lonRef) = ($13,$12,$11,$1,$2,$3,$5,$6,$7,$8);
1862
+ $yr += ($yr >= 70 ? 1900 : 2000);
1863
+ $spd = $9 * $knotsToKph if length $9;
1864
+ $trk = $10 if length $10;
1865
+ } else {
1866
+ $done = 1;
1812
1867
  }
1813
- ($yr,$mon,$day,$hr,$min,$sec,$lat,$latRef,$lon,$lonRef) = ($13,$12,$11,$1,$2,$3,$5,$6,$7,$8);
1814
- $yr += ($yr >= 70 ? 1900 : 2000);
1815
- $spd = $9 * $knotsToKph if length $9;
1816
- $trk = $10 if length $10;
1817
1868
 
1818
1869
  } elsif ($$dataPt =~ /^.{64}[\x01-\x0c]\0{3}[\x01-\x1f]\0{3}A[NS][EW]\0{5}/s) {
1819
1870
 
1820
- $debug and $et->FoundTag(GPSType => 7);
1871
+ $debug and $et->FoundTag(GPSType => 8);
1821
1872
  # Akaso V1 dascham
1822
1873
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 00 00 00 [....freeGPS x...]
1823
1874
  # 0010: 59 6e 64 41 6b 61 73 6f 43 61 72 00 00 00 00 00 [YndAkasoCar.....]
@@ -1842,7 +1893,7 @@ sub ProcessFreeGPS($$$)
1842
1893
  ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef) =
1843
1894
  unpack('x48V6a1a1a1x1', $$dataPt);
1844
1895
 
1845
- $et->WarnOnce('GPSLatitude/Longitude encryption is not yet known, so these will be wrong');
1896
+ $et->Warn('GPSLatitude/Longitude encryption is not yet known, so these will be wrong');
1846
1897
  # (see https://exiftool.org/forum/index.php?topic=11320.0)
1847
1898
 
1848
1899
  $spd = GetFloat($dataPt, 0x60);
@@ -1850,11 +1901,11 @@ sub ProcessFreeGPS($$$)
1850
1901
  $lat = GetDouble($dataPt, 0x50); # latitude is here, but encrypted somehow
1851
1902
  $lon = GetDouble($dataPt, 0x58); # longitude is here, but encrypted somehow
1852
1903
  $ddd = 1; # don't convert until we know what the format is
1853
- #my $serialNum = substr($$dataPt, 0x68, 20);
1904
+ #my $serialNum = substr($$dataPt, 0x68, 20); # (confirmed)
1854
1905
 
1855
1906
  } elsif ($$dataPt =~ /^.{12}\xac\0\0\0.{44}(.{72})/s) {
1856
1907
 
1857
- $debug and $et->FoundTag(GPSType => 8);
1908
+ $debug and $et->FoundTag(GPSType => 9);
1858
1909
  # EACHPAI dash cam
1859
1910
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 ac 00 00 00 [....freeGPS ....]
1860
1911
  # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
@@ -1866,19 +1917,18 @@ sub ProcessFreeGPS($$$)
1866
1917
  # 0070: 43 41 3c 40 42 40 46 42 40 3c 3c 3c 51 3a 47 46 [CA<@B@FB@<<<Q:GF]
1867
1918
  # 0080: 00 2a 36 35 00 00 00 00 00 00 00 00 00 00 00 00 [.*65............]
1868
1919
 
1869
- $et->WarnOnce("Can't yet decrypt EACHPAI timed GPS", 1);
1920
+ $et->Warn("Can't yet decrypt EACHPAI timed GPS", 1);
1870
1921
  # (see https://exiftool.org/forum/index.php?topic=5095.msg61266#msg61266)
1871
- SetByteOrder($oldOrder);
1872
- return 1;
1922
+ $done = 1;
1873
1923
 
1874
- my $time = pack 'C*', map { $_ ^= 0 } unpack 'C*', $1;
1875
- # bytes 7-12 are the timestamp in ASCII HHMMSS after xor-ing with 0x70
1876
- substr($time,7,6) = pack 'C*', map { $_ ^= 0x70 } unpack 'C*', substr($time,7,6);
1877
- # (other values are currently unknown)
1924
+ # my $time = pack 'C*', map { $_ ^= 0 } unpack 'C*', $1;
1925
+ # # bytes 7-12 are the timestamp in ASCII HHMMSS after xor-ing with 0x70
1926
+ # substr($time,7,6) = pack 'C*', map { $_ ^= 0x70 } unpack 'C*', substr($time,7,6);
1927
+ # # (other values are currently unknown)
1878
1928
 
1879
1929
  } elsif ($$dataPt =~ /^.{64}A([NS])([EW])\0/s) {
1880
1930
 
1881
- $debug and $et->FoundTag(GPSType => 9);
1931
+ $debug and $et->FoundTag(GPSType => 10);
1882
1932
  # Vantrue S1 dashcam
1883
1933
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 00 00 00 [....freeGPS x...]
1884
1934
  # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
@@ -1890,21 +1940,21 @@ sub ProcessFreeGPS($$$)
1890
1940
  # 0070: 05 00 00 00 7f 00 00 00 07 01 00 00 00 00 00 00 [................]
1891
1941
  ($latRef, $lonRef) = ($1, $2);
1892
1942
  ($yr,$mon,$day,$hr,$min,$sec,@acc) = unpack('x68V6x20V3', $$dataPt);
1893
- unless ($mon>=1 and $mon<=12 and $day>=1 and $day<=31) {
1894
- SetByteOrder($oldOrder);
1895
- return 0;
1943
+ if ($mon>=1 and $mon<=12 and $day>=1 and $day<=31) {
1944
+ # (not sure about acc scaling)
1945
+ @acc = map { SignedInt32 / 1000 } @acc;
1946
+ $lon = GetFloat($dataPt, 0x5c);
1947
+ $lat = GetFloat($dataPt, 0x60);
1948
+ $spd = GetFloat($dataPt, 0x64) * $knotsToKph;
1949
+ $trk = GetFloat($dataPt, 0x68);
1950
+ $alt = GetFloat($dataPt, 0x6c);
1951
+ } else {
1952
+ $done = 1;
1896
1953
  }
1897
- # (not sure about acc scaling)
1898
- @acc = map { SignedInt32 / 1000 } @acc;
1899
- $lon = GetFloat($dataPt, 0x5c);
1900
- $lat = GetFloat($dataPt, 0x60);
1901
- $spd = GetFloat($dataPt, 0x64) * $knotsToKph;
1902
- $trk = GetFloat($dataPt, 0x68);
1903
- $alt = GetFloat($dataPt, 0x6c);
1904
1954
 
1905
1955
  } elsif (substr($$dataPt,0x45,3) eq 'ATC') {
1906
1956
 
1907
- $debug and $et->FoundTag(GPSType => 10);
1957
+ $debug and $et->FoundTag(GPSType => 11);
1908
1958
  # header looks like this: (sample 1)
1909
1959
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 38 06 00 00 [....freeGPS 8...]
1910
1960
  # 0010: 49 51 53 32 30 31 33 30 33 30 36 42 00 00 00 00 [IQS20130306B....]
@@ -1945,7 +1995,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
1945
1995
  my $i;
1946
1996
  for ($i=0; $i<@dateMax; ++$i) {
1947
1997
  next if $now[$i] <= $dateMax[$i];
1948
- $et->WarnOnce('Invalid GPS date/time');
1998
+ $et->Warn('Invalid GPS date/time');
1949
1999
  next ATCRec; # ignore this record
1950
2000
  }
1951
2001
  # look for next ATC record in temporal sequence
@@ -2012,12 +2062,11 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2012
2062
  }
2013
2063
  # save position of most recent record (needed when parsing the next freeGPS block)
2014
2064
  $$et{FreeGPS2}{RecentRecPos} = $lastRecPos;
2015
- SetByteOrder($oldOrder);
2016
- return 1;
2065
+ $done = 1;
2017
2066
 
2018
2067
  } elsif ($$dataPt =~ /^.{60}A\0.{10}([NS])\0.{14}([EW])\0/s and $dirLen >= 0x88) {
2019
2068
 
2020
- $debug and $et->FoundTag(GPSType => 11);
2069
+ $debug and $et->FoundTag(GPSType => 12);
2021
2070
  # header looks like this in my sample:
2022
2071
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 08 01 00 00 [....freeGPS ....]
2023
2072
  # 0010: 32 30 31 33 30 38 31 35 2e 30 31 00 00 00 00 00 [20130815.01.....]
@@ -2048,9 +2097,14 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2048
2097
 
2049
2098
  } elsif ($$dataPt =~ /^.{16}A([NS])([EW])\0/s) {
2050
2099
 
2051
- $debug and $et->FoundTag(GPSType => 12);
2100
+ $debug and $et->FoundTag(GPSType => 13);
2052
2101
  # INNOVV MP4 video (same format as INNOVV TS)
2053
- while ($$dataPt =~ /(A[NS][EW]\0.{28})/g) {
2102
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
2103
+ # 0010: 41 4e 45 00 e4 56 96 45 86 b1 ca 44 5c 8f e2 40 [ANE..V.E...D\..@]
2104
+ # 0020: 33 33 58 43 c3 00 00 00 30 00 00 00 a0 fe ff ff [33XC....0.......]
2105
+ # 0030: 41 4e 45 00 e3 56 96 45 82 b1 ca 44 5c 8f fa 40 [ANE..V.E...D\..@]
2106
+ # 0040: c3 75 56 43 8c ff ff ff 8c 00 00 00 c3 fd ff ff [.uVC............]
2107
+ while ($$dataPt =~ /(A[NS][EW]\0.{28})/sg) {
2054
2108
  my $dat = $1;
2055
2109
  $lat = abs(GetFloat(\$dat, 4)); # (abs just to be safe)
2056
2110
  $lon = abs(GetFloat(\$dat, 8)); # (abs just to be safe)
@@ -2065,12 +2119,35 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2065
2119
  $et->HandleTag($tagTbl, GPSTrack => $trk);
2066
2120
  $et->HandleTag($tagTbl, Accelerometer => "@acc");
2067
2121
  }
2068
- SetByteOrder($oldOrder);
2069
- return 1;
2122
+ $done = 1;
2123
+
2124
+ } elsif ($$dataPt =~ /^.{20}[\0-\x18][\0-\x3b]{2}[\0-\x09]A([NS])([EW])/s) {
2125
+
2126
+ $debug and $et->FoundTag(GPSType => 14);
2127
+ # XBHT motorcycle dashcam Model XB702
2128
+ # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
2129
+ # 0010: 00 17 05 11 0d 25 18 00 41 4e 45 64 83 3f 00 00 [.....%..ANEd.?..]
2130
+ # 0020: 44 3d c5 02 48 6d ff 07 df 03 00 00 6b 00 00 00 [D=..Hm......k...]
2131
+ # 0030: 00 00 00 00 00 17 05 11 0d 25 18 01 41 4e 45 64 [.........%..ANEd]
2132
+ # 0040: 8b 3f 00 00 30 3d c5 02 50 6d ff 07 df 03 00 00 [.?..0=..Pm......]
2133
+ while ($$dataPt =~ /(.{7}[\0-\x09]A[NS][EW].{25})/sg) {
2134
+ my $dat = $1;
2135
+ ($yr,$mon,$day,$hr,$min,$sec,$ss,$latRef,$lonRef,$lat,$lon,$spd) =
2136
+ unpack('xC7xCCx5VVx4v', $dat);
2137
+ $yr += 2000; $lat /= 1e4; $lon /= 1e4;
2138
+ ConvertLatLon($lat, $lon);
2139
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2140
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2d.%d',$yr,$mon,$day,$hr,$min,$sec,$ss);
2141
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
2142
+ $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
2143
+ $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
2144
+ $et->HandleTag($tagTbl, GPSSpeed => $spd);
2145
+ }
2146
+ $done = 1;
2070
2147
 
2071
2148
  } elsif ($$dataPt =~ /^.{28}A.{11}([NS]).{15}([EW])/s) {
2072
2149
 
2073
- $debug and $et->FoundTag(GPSType => 13);
2150
+ $debug and $et->FoundTag(GPSType => 15);
2074
2151
  # Vantrue N4 dashcam
2075
2152
  # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
2076
2153
  # 0010: 0d 00 00 00 16 00 00 00 1e 00 00 00 41 00 00 00 [............A...]
@@ -2127,7 +2204,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2127
2204
  ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef) =
2128
2205
  unpack('x48V6a1a1a1x1V4', $$dataPt);
2129
2206
  if (substr($$dataPt, 16, 3) eq 'IQS') {
2130
- $debug and $et->FoundTag(GPSType => 14);
2207
+ $debug and $et->FoundTag(GPSType => 16);
2131
2208
  # Type 3b (ref PH)
2132
2209
  # header looks like this in my sample:
2133
2210
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...]
@@ -2139,7 +2216,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2139
2216
  $spd = Get32s($dataPt, 0x54) / 100 * $mpsToKph;
2140
2217
  $alt = GetFloat($dataPt, 0x58) / 1000; # (NC)
2141
2218
  } else {
2142
- $debug and $et->FoundTag(GPSType => 15);
2219
+ $debug and $et->FoundTag(GPSType => 17);
2143
2220
  $lat = GetFloat($dataPt, 0x4c);
2144
2221
  $lon = GetFloat($dataPt, 0x50);
2145
2222
  $spd = GetFloat($dataPt, 0x54) * $knotsToKph;
@@ -2159,7 +2236,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2159
2236
 
2160
2237
  } elsif ($$dataPt =~ m<^.{23}(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2}) [N|S]>s) {
2161
2238
 
2162
- $debug and $et->FoundTag(GPSType => 16);
2239
+ $debug and $et->FoundTag(GPSType => 18);
2163
2240
  # XGODY 12" 4K Dashcam
2164
2241
  # 0000: 00 00 00 a8 66 72 65 65 47 50 53 20 98 00 00 00 [....freeGPS ....]
2165
2242
  # 0010: 6e 6f 72 6d 61 6c 3a 32 30 32 34 2f 30 35 2f 32 [normal:2024/05/2]
@@ -2190,8 +2267,8 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2190
2267
  }
2191
2268
 
2192
2269
  } elsif ($$dataPt =~ m/^.{30}A.{20}VV/) {
2193
-
2194
- $debug and $et->FoundTag(GPSType => 17);
2270
+
2271
+ $debug and $et->FoundTag(GPSType => 19);
2195
2272
  # 70mai A810 dashcam (note: no timestamps in the samples I have)
2196
2273
  # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 ed 01 00 00 [..@.freeGPS ....]
2197
2274
  # 0010: 03 00 ed 01 00 00 00 0f 00 00 70 08 00 00 41 66 [..........p...Af]
@@ -2208,7 +2285,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2208
2285
 
2209
2286
  } else {
2210
2287
 
2211
- $debug and $et->FoundTag(GPSType => 18);
2288
+ $debug and $et->FoundTag(GPSType => 20);
2212
2289
  # (look for binary GPS as stored by Nextbase 512G, ref PH)
2213
2290
  # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 01 00 00 [....freeGPS x...]
2214
2291
  # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............]
@@ -2254,14 +2331,14 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
2254
2331
  $et->HandleTag($tagTbl, GPSTrack => $trk);
2255
2332
  last if $pos += 0x20 > length($$dataPt) - 0x1e;
2256
2333
  }
2257
- SetByteOrder($oldOrder);
2258
- return $$et{DOC_NUM} ? 1 : 0; # return 0 if nothing extracted
2334
+ $done = 1;
2259
2335
  }
2336
+ SetByteOrder($oldOrder);
2337
+ return $$et{DOC_NUM} ? 1 : 0 if $done;
2338
+ return 0 if defined $yr and $mon < 1 or $mon > 12; # quick sanity check
2260
2339
  #
2261
2340
  # save tag values extracted by above code
2262
2341
  #
2263
- SetByteOrder($oldOrder);
2264
- return 0 if defined $yr and $mon < 1 or $mon > 12; # quick sanity check
2265
2342
  FoundSomething($et, $tagTbl, $$dirInfo{SampleTime}, $$dirInfo{SampleDuration});
2266
2343
  $sec = '0' . $sec unless $sec =~ /^\d{2}/; # pad integer part of seconds to 2 digits
2267
2344
  if (defined $yr) {
@@ -2401,8 +2478,8 @@ sub Process_tx3g($$$)
2401
2478
  if ($text =~ /^HOME\(/) {
2402
2479
  # --- sample text from Autel Evo II drone ---
2403
2480
  # HOME(W: 109.318642, N: 40.769371) 2023-09-12 10:28:07
2404
- # GPS(W: 109.339287, N: 40.768574, 2371.76m)
2405
- # HDR ISO:100 SHUTTER:1000 EV:-0.7 F-NUM:1.8
2481
+ # GPS(W: 109.339287, N: 40.768574, 2371.76m)
2482
+ # HDR ISO:100 SHUTTER:1000 EV:-0.7 F-NUM:1.8
2406
2483
  # F.PRY (1.0\xc2\xb0, -3.7\xc2\xb0, -59.0\xc2\xb0), G.PRY (-51.1\xc2\xb0, 0.0\xc2\xb0, -58.9\xc2\xb0)
2407
2484
  my $line;
2408
2485
  foreach $line (split /\x0a/, $text) {
@@ -2479,7 +2556,7 @@ sub Process_mebx($$$)
2479
2556
  Size => $len - 8,
2480
2557
  );
2481
2558
  } else {
2482
- $et->WarnOnce('No key information for mebx ID ' . PrintableTagID($id,1));
2559
+ $et->Warn('No key information for mebx ID ' . PrintableTagID($id,1));
2483
2560
  }
2484
2561
  }
2485
2562
  return 1;
@@ -2602,7 +2679,7 @@ sub Process_gdat($$$)
2602
2679
  {
2603
2680
  my ($et, $dirInfo, $tagTbl) = @_;
2604
2681
  unless ($$et{OPTIONS}{ExtractEmbedded}) {
2605
- $et->WarnOnce('Use the ExtractEmbedded option to extract timed GPSData',3);
2682
+ $et->Warn('Use the ExtractEmbedded option to extract timed GPSData',3);
2606
2683
  return 0;
2607
2684
  }
2608
2685
  my $dataPt = $$dirInfo{DataPt};
@@ -2653,13 +2730,220 @@ sub Process_nbmt($$$)
2653
2730
  delete $$et{NoMoreTextDecoding};
2654
2731
  delete $$et{DOC_NUM};
2655
2732
  } else {
2656
- $et->WarnOnce('Use the ExtractEmbedded option to extract timed GPSData',3);
2733
+ $et->Warn('Use the ExtractEmbedded option to extract timed GPSData',3);
2734
+ }
2735
+ return 1;
2736
+ }
2737
+
2738
+ #------------------------------------------------------------------------------
2739
+ # Un-do LIGOGPS fuzzing
2740
+ # Inputs: 0) fuzzed latitude, 1) fuzzed longitude, 2) scale factor
2741
+ # Returns: 0) latitude, 1) longitude
2742
+ sub UnfuzzLigoGPS($$$)
2743
+ {
2744
+ my ($lat, $lon, $scl) = @_;
2745
+ my $lat2 = int($lat / 10) * 10;
2746
+ my $lon2 = int($lon / 10) * 10;
2747
+ return($lat2 + ($lon - $lon2) * $scl, $lon2 + ($lat - $lat2) * $scl);
2748
+ }
2749
+
2750
+ #------------------------------------------------------------------------------
2751
+ # Decrypt LIGOGPSINFO record (starting with "####")
2752
+ # Inputs: 0) encrypted GPS record incuding 8-byte header
2753
+ # Returns: decrypted record including 4-byte uint32 header, or undef on error
2754
+ sub DecryptLigoGPS($)
2755
+ {
2756
+ my $str = shift;
2757
+ my $num = unpack('x4V',$str);
2758
+ return undef if $num < 4;
2759
+ $num = 0x84 if $num > 0x84; # (be safe)
2760
+ my @in = unpack("x8C$num",$str);
2761
+ my @out;
2762
+ while (@in) {
2763
+ my $b = shift @in;
2764
+ my $steeringBits = $b & 0xe0;
2765
+ if ($steeringBits >= 0xc0) {
2766
+ return undef if @in < 4;
2767
+ push @out, (shift(@in) | $b & 0x01) ^ 0x20,
2768
+ (shift(@in) | $b & 0x02) ^ 0x20,
2769
+ (shift(@in) | $b & 0x0c) ^ 0x20,
2770
+ shift(@in) ^ 0x20 | $b & 0x30;
2771
+ } elsif ($steeringBits >= 0x40) {
2772
+ return undef if @in < 3;
2773
+ if ($steeringBits == 0x40) {
2774
+ push @out, 0x20,
2775
+ (shift(@in) | $b & 0x01) ^ 0x20,
2776
+ (shift(@in) | $b & 0x06) ^ 0x20,
2777
+ (shift(@in) | $b & 0x18) ^ 0x20;
2778
+ } elsif ($steeringBits == 0x60) {
2779
+ push @out, (shift(@in) | $b & 0x03) ^ 0x20,
2780
+ 0x20,
2781
+ (shift(@in) | $b & 0x04) ^ 0x20,
2782
+ (shift(@in) | $b & 0x18) ^ 0x20;
2783
+ } elsif ($steeringBits == 0x80) {
2784
+ push @out, (shift(@in) | $b & 0x03) ^ 0x20,
2785
+ (shift(@in) | $b & 0x0c) ^ 0x20,
2786
+ 0x20,
2787
+ (shift(@in) | $b & 0x10) ^ 0x20;
2788
+ } else {
2789
+ push @out, (shift(@in) | $b & 0x01) ^ 0x20,
2790
+ (shift(@in) | $b & 0x06) ^ 0x20,
2791
+ (shift(@in) | $b & 0x18) ^ 0x20,
2792
+ 0x20;
2793
+ }
2794
+ } elsif ($steeringBits == 0x00) {
2795
+ return undef if @in < 1;
2796
+ push @out, shift(@in) | $b & 0x13;
2797
+ } else {
2798
+ return undef; # (shouldn't happen)
2799
+ }
2800
+ }
2801
+ return pack 'C*', @out;
2802
+ }
2803
+
2804
+ #------------------------------------------------------------------------------
2805
+ # Decipher and parse LIGOGPSINFO record (starting with "####")
2806
+ # Inputs: 0) ExifTool ref, 1) enciphered string, 2) tag table ref
2807
+ # Returns: true if this looked like an enciphered string
2808
+ # Notes: handles contained tags, but may defer handling until full cipher is known
2809
+ sub DecipherLigoGPS($$$)
2810
+ {
2811
+ my ($et, $str, $tagTbl) = @_;
2812
+
2813
+ # (enciphered characters must be in the range 0x30-0x5f ('0' - '_'))
2814
+ $str =~ m[^####.{4}([0-_])[0-_]{3}/[0-_]{2}/[0-_]{2} ..([0-_])..([0-_]).([0-_]) ]s or return undef;
2815
+ return undef unless $2 eq $3; # (colons in time string must be the same)
2816
+
2817
+ my $cipherInfo = $$et{LigoCipher};
2818
+ $cipherInfo or $cipherInfo = $$et{LigoCipher} = { cache => [ ], secs => [ ], two => -1 };
2819
+ my $decipher = $$cipherInfo{decipher};
2820
+ my $cache = $$cipherInfo{cache};
2821
+
2822
+ # determine the cipher code table based on the advancing 1's digit of seconds
2823
+ unless ($decipher) {
2824
+ push @$cache, $str; # cache records until we can decipher them
2825
+ my ($millennium, $colon) = ($1, $2);
2826
+ # determine the Caesar cipher lookup table
2827
+ # (only characters in range 0x30-0x5f are encrypted)
2828
+ my $secs = $$cipherInfo{secs};
2829
+ push @$secs, $4 unless @$secs and $${secs}[-1] eq $4;
2830
+ $$cipherInfo{two} = $#$secs if $4 eq $millennium; # save index of enciphered '2'
2831
+ return 1 if @$secs < 10; # must cache the data until we know all 10 digits
2832
+ my $two = $$cipherInfo{two}; # (index of '2' in the array)
2833
+ my %decipher = ( $colon => ':' ); # (':' is the time separator)
2834
+ foreach (0..9) {
2835
+ my $ch = $$secs[($_ + $two - 2 + 10) % 10];
2836
+ if ($two < 0 or defined $decipher{$ch}) { # (must be a unique code for each digit)
2837
+ @$cipherInfo{'secs','two'} = ([ ], -1); # reset and try again
2838
+ $et->Warn('Hiccup while deciphering LIGOGPSINFO');
2839
+ return 1;
2840
+ }
2841
+ $decipher{$ch} = chr($_ + 0x30);
2842
+ }
2843
+ # also know the lat/lon quadrant from the signs of the coordinates
2844
+ if ($str =~ / ([0-_])$colon(-?).*? ([0-_])$colon(-?)/) {
2845
+ @decipher{$1,$3} = ($2 ? 'S' : 'N', $4 ? 'W' : 'E');
2846
+ }
2847
+ # fill in unknown entries with '?' (only chars 0x30-0x5f are enciphered)
2848
+ defined $decipher{$_} or $decipher{$_} = '?' foreach map(chr, 0x30..0x5f);
2849
+ $decipher = $$cipherInfo{decipher} = \%decipher;
2850
+ $str = shift @$cache; # start deciphering at oldest cache entry
2851
+ }
2852
+
2853
+ # apply reverse Caesar cipher and extract GPS information
2854
+ do {
2855
+ my $pre = substr($str, 4, 4); # save second 4 bytes of header
2856
+ ($str = substr($str,8)) =~ s/\0+$//; # remove 8-byte header and null padding
2857
+ $str =~ s/([0-_])/$$decipher{$1}/g; # decipher
2858
+ if ($$et{OPTIONS}{Verbose} > 1) {
2859
+ $et->VPrint(1, "$$et{INDENT}\(Deciphered: ".unpack('H8',$pre)." $str)\n");
2860
+ }
2861
+ # add back leading 4 bytes (int16u counter plus 2 unknown bytes), and parse
2862
+ # (not fuzzed in my only sample when found in standard 'skip' atom)
2863
+ ParseLigoGPS($et, "$pre$str", $tagTbl, $$et{LigoType} eq 'LigoGPSInfo');
2864
+ } while $str = shift @$cache;
2865
+
2866
+ return 1;
2867
+ }
2868
+
2869
+ #------------------------------------------------------------------------------
2870
+ # Parse decrypted/deciphered (but not defuzzed) LIGOGPSINFO record
2871
+ # (record starts with 4-byte int32u counter followed by date/time, etc)
2872
+ # Inputs: 0) ExifTool ref, 1) GPS string, 2) tag table ref, 3) not fuzzed
2873
+ # Returns: nothing
2874
+ sub ParseLigoGPS($$$;$)
2875
+ {
2876
+ my ($et, $str, $tagTbl, $noFuzz) = @_;
2877
+
2878
+ # example string input
2879
+ # "....2022/09/19 12:45:24 N:31.285065 W:124.759483 46.93 km/h x:-0.000 y:-0.000 z:-0.000"
2880
+ unless ($str=~ /^.{4}(\S+ \S+)\s+([NS?]):(-?)([.\d]+)\s+([EW?]):(-?)([\.\d]+)\s+([.\d]+)/s) {
2881
+ $et->Warn('LIGOGPSINFO format error');
2882
+ return;
2657
2883
  }
2884
+ my ($time,$latRef,$latNeg,$lat,$lonRef,$lonNeg,$lon,$spd) = ($1,$2,$3,$4,$5,$6,$7,$8);
2885
+ my %gpsScl = ( 1 => 1.524855137, 2 => 1.456027985, 3 => 1.15368 );
2886
+ my $spdScl = $noFuzz ? $knotsToKph : 1.85407333;
2887
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2888
+ $time =~ tr(/)(:);
2889
+ # convert from DDMM.MMMMMM to DD.DDDDDD if necessary
2890
+ # (speed wasn't scaled in my 1 sample with this format)
2891
+ $lat =~ /^\d{3}/ and ConvertLatLon($lat,$lon), $spdScl = 1;
2892
+ unless ($noFuzz) { # unfuzz the coordinates if necessary
2893
+ my $scl = $$et{OPTIONS}{LigoGPSScale} || $$et{LigoGPSScale} || 1;
2894
+ $scl = $gpsScl{$scl} if $gpsScl{$scl};
2895
+ ($lat, $lon) = UnfuzzLigoGPS($lat, $lon, $scl);
2896
+ }
2897
+ # a final sanity check
2898
+ ($lat > 90 or $lon > 180) and $et->Warn('LIGOGPSINFO coordinates out of range'), return;
2899
+ $$et{SET_GROUP1} = 'LIGO';
2900
+ $et->HandleTag($tagTbl, 'GPSDateTime', $time);
2901
+ # (ignore N/S/E/W if coordinate is signed)
2902
+ $et->HandleTag($tagTbl, 'GPSLatitude', $lat * (($latNeg or $latRef eq 'S') ? -1 : 1));
2903
+ $et->HandleTag($tagTbl, 'GPSLongitude', $lon * (($lonNeg or $lonRef eq 'W') ? -1 : 1));
2904
+ $et->HandleTag($tagTbl, 'GPSSpeed', $spd * $spdScl);
2905
+ $et->HandleTag($tagTbl, 'GPSTrack', $1) if $str =~ /\bA:(\S+)/;
2906
+ # (have a sample where tab is used to separate acc components)
2907
+ $et->HandleTag($tagTbl, 'Accelerometer',"$1 $2 $3") if $str =~ /x:(\S+)\sy:(\S+)\sz:(\S+)/;
2908
+ $et->HandleTag($tagTbl, 'M', $1) if $str =~ /\bM:(\S+)/;
2909
+ $et->HandleTag($tagTbl, 'H', $1) if $str =~ /\bH:(\S+)/;
2910
+ delete $$et{SET_GROUP1};
2911
+ }
2912
+
2913
+ #------------------------------------------------------------------------------
2914
+ # Process LIGOGPSINFO data (non-JSON format)
2915
+ # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2916
+ # 3) 1=LIGOGPS lat/lon/spd weren't fuzzed
2917
+ # Returns: 1 on success
2918
+ sub ProcessLigoGPS($$$;$)
2919
+ {
2920
+ my ($et, $dirInfo, $tagTbl, $noFuzz) = @_;
2921
+ my $dataPt = $$dirInfo{DataPt};
2922
+ my $pos = ($$dirInfo{DirStart} || 0) + 0x14;
2923
+ my $cipherInfo = $$et{LigoCipher};
2924
+ return undef if $pos > length $$dataPt;
2925
+ $$et{LigoType} = $$dirInfo{DirName} || 'LigoGPS';
2926
+ push @{$$et{PATH}}, $$et{LigoType} unless $$dirInfo{DirID};
2927
+ # not fuzzed if header is "LIGOGPSINFO\0\0\0\0\x01" (BlueSkySeaDV688)
2928
+ $noFuzz = 1 if substr($$dataPt, $pos-8, 4) eq "\0\0\0\x01";
2929
+ $et->VerboseDir($$et{LigoType});
2930
+ for (; $pos + 0x84 <= length($$dataPt); $pos+=0x84) {
2931
+ my $dat = substr($$dataPt, $pos, 0x84);
2932
+ $dat =~ /^####/ or next; # (have seen blank records filled with zeros, so keep trying)
2933
+ # decipher if we already know the encryption
2934
+ $cipherInfo and $$cipherInfo{decipher} and DecipherLigoGPS($et, $dat, $tagTbl) and next;
2935
+ my $str = DecryptLigoGPS($dat);
2936
+ defined $str or DecipherLigoGPS($et, $dat, $tagTbl), next; # try to decipher
2937
+ $et->VPrint(1, "$$et{INDENT}\(Decrypted: ",unpack('V',$str),' ',substr($str,4),")\n") if $$et{OPTIONS}{Verbose} > 1;
2938
+ ParseLigoGPS($et, $str, $tagTbl, $noFuzz);
2939
+ }
2940
+ pop @{$$et{PATH}} unless $$dirInfo{DirID};
2941
+ delete $$et{DOC_NUM};
2658
2942
  return 1;
2659
2943
  }
2660
2944
 
2661
2945
  #------------------------------------------------------------------------------
2662
- # Process LIGOGPS JSON-format GPS from Yada RoadCam Pro 4K BT58189
2946
+ # Process LIGOGPSINFO JSON-format GPS (Yada RoadCam Pro 4K BT58189)
2663
2947
  # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2664
2948
  # Returns: 1 on success
2665
2949
  # Sample data (chained 512-byte records starting like this):
@@ -2670,13 +2954,14 @@ sub Process_nbmt($$$)
2670
2954
  # 0040: 22 3a 20 22 32 30 32 33 22 2c 20 22 4d 6f 6e 74 [": "2023", "Mont]
2671
2955
  # 0050: 68 22 3a 20 22 31 32 22 2c 20 22 44 61 79 22 3a [h": "12", "Day":]
2672
2956
  # 0060: 20 22 32 38 22 2c 20 22 73 74 61 74 75 73 22 3a [ "28", "status":]
2673
- sub ProcessLIGO_JSON($$$)
2957
+ sub ProcessLigoJSON($$$)
2674
2958
  {
2675
2959
  my ($et, $dirInfo, $tagTbl) = @_;
2676
2960
  my $dataPt = $$dirInfo{DataPt};
2677
2961
  my $dirLen = $$dirInfo{DirLen};
2678
2962
  require Image::ExifTool::Import;
2679
2963
  $et->VerboseDir('LIGO_JSON', undef, length($$dataPt));
2964
+ $$et{SET_GROUP1} = 'LIGO';
2680
2965
  while ($$dataPt =~ /LIGOGPSINFO (\{.*?\})/g) {
2681
2966
  my $json = $1;
2682
2967
  my %dbase;
@@ -2726,11 +3011,12 @@ sub ProcessLIGO_JSON($$$)
2726
3011
  $et->HandleTag($tagTbl, GPSLongitude2 => $lon);
2727
3012
  }
2728
3013
  unless ($et->Options('ExtractEmbedded')) {
2729
- $et->WarnOnce('Use the ExtractEmbedded option to extract all timed GPS',3);
3014
+ $et->Warn('Use the ExtractEmbedded option to extract all timed GPS',3);
2730
3015
  last;
2731
3016
  }
2732
3017
  }
2733
3018
  delete $$et{DOC_NUM};
3019
+ delete $$et{SET_GROUP1};
2734
3020
  return 1;
2735
3021
  }
2736
3022
 
@@ -2773,7 +3059,7 @@ sub ProcessKenwood($$$)
2773
3059
  }
2774
3060
  $et->HandleTag($tagTbl, Accelerometer => "@acc") if @acc;
2775
3061
  unless ($et->Options('ExtractEmbedded')) {
2776
- $et->WarnOnce('Use the ExtractEmbedded option to extract all timed GPS',3);
3062
+ $et->Warn('Use the ExtractEmbedded option to extract all timed GPS',3);
2777
3063
  last;
2778
3064
  }
2779
3065
  }
@@ -2896,7 +3182,7 @@ sub ProcessKenwoodTrailer($$$)
2896
3182
  $raf->Read($buff, 14) and $buff eq 'CCCCCCCCCCCCCC' or return 0;
2897
3183
  $et->VerboseDir('Kenwood trailer', undef, undef);
2898
3184
  unless ($$et{OPTIONS}{ExtractEmbedded}) {
2899
- $et->WarnOnce('Use the ExtractEmbedded option to extract timed GPSData from Kenwood trailer',3);
3185
+ $et->Warn('Use the ExtractEmbedded option to extract timed GPSData from Kenwood trailer',3);
2900
3186
  return 1;
2901
3187
  }
2902
3188
  while ($raf->Read($buff, 121) and $buff =~ /^GPSDATA--(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/) {
@@ -3119,7 +3405,7 @@ sub ProcessTTAD($$$)
3119
3405
  $et->HandleTag($tagTbl, "Unknown0$type" => "@a");
3120
3406
  }
3121
3407
  } else {
3122
- $et->WarnOnce("Unknown TTAD record type $type",1);
3408
+ $et->Warn("Unknown TTAD record type $type",1);
3123
3409
  }
3124
3410
  # without -ee, stop after we find types 0,3,5 (ie. bitmask 0x29)
3125
3411
  $eeOpt or ($found & 0x29) != 0x29 or EEWarn($et), last;
@@ -3171,7 +3457,7 @@ sub ProcessInsta360($;$)
3171
3457
  }
3172
3458
  unless ($et->Options('ExtractEmbedded')) {
3173
3459
  # can arrive here when reading Insta360 trailer on JPEG image (INSP file)
3174
- $et->WarnOnce('Use ExtractEmbedded option to extract timed metadata from Insta360 trailer',3);
3460
+ $et->Warn('Use ExtractEmbedded option to extract timed metadata from Insta360 trailer',3);
3175
3461
  return 1;
3176
3462
  }
3177
3463
 
@@ -3368,8 +3654,8 @@ sub ProcessCAMM($$$)
3368
3654
  my $rtnVal = 0;
3369
3655
  while ($pos + 4 < $end) {
3370
3656
  my $type = Get16u($dataPt, $pos + 2);
3371
- my $size = $size{$type} or $et->WarnOnce("Unknown camm record type $type"), last;
3372
- $pos + $size > $end and $et->WarnOnce("Truncated camm record $type"), last;
3657
+ my $size = $size{$type} or $et->Warn("Unknown camm record type $type"), last;
3658
+ $pos + $size > $end and $et->Warn("Truncated camm record $type"), last;
3373
3659
  my $tagTbl = GetTagTable("Image::ExifTool::QuickTime::camm$type");
3374
3660
  $$dirInfo{DirStart} = $pos;
3375
3661
  $$dirInfo{DirLen} = $size;