diff options
author | exiftool <exiftool@users.sourceforge.net> | 2018-10-09 12:24:48 -0400 |
---|---|---|
committer | exiftool <exiftool@users.sourceforge.net> | 2018-10-09 12:24:48 -0400 |
commit | 4d55678e0776c97648f15ca3635cd73ab2eb1924 (patch) | |
tree | 9c1edae34b34945898720eab78a181b75bf60142 /lib | |
parent | 5297201750bfe371010ee437f1d31aeec4ab49ab (diff) |
Update to 11.13
Diffstat (limited to 'lib')
-rw-r--r-- | lib/File/RandomAccess.pm | 54 | ||||
-rw-r--r-- | lib/File/RandomAccess.pod | 19 | ||||
-rw-r--r-- | lib/Image/ExifTool.pm | 4 | ||||
-rw-r--r-- | lib/Image/ExifTool.pod | 32 | ||||
-rw-r--r-- | lib/Image/ExifTool/Canon.pm | 3 | ||||
-rw-r--r-- | lib/Image/ExifTool/Exif.pm | 4 | ||||
-rw-r--r-- | lib/Image/ExifTool/Nikon.pm | 37 | ||||
-rw-r--r-- | lib/Image/ExifTool/PNG.pm | 54 | ||||
-rw-r--r-- | lib/Image/ExifTool/QuickTime.pm | 4 | ||||
-rw-r--r-- | lib/Image/ExifTool/QuickTimeStream.pl | 272 | ||||
-rw-r--r-- | lib/Image/ExifTool/RIFF.pm | 3 | ||||
-rw-r--r-- | lib/Image/ExifTool/TagNames.pod | 4 |
12 files changed, 340 insertions, 150 deletions
diff --git a/lib/File/RandomAccess.pm b/lib/File/RandomAccess.pm index 42ea3b89..c689f56b 100644 --- a/lib/File/RandomAccess.pm +++ b/lib/File/RandomAccess.pm @@ -16,6 +16,7 @@ # 11/26/2008 - P. Harvey Fixed bug in ReadLine when reading from a # scalar with a multi-character newline # 01/24/2009 - PH Protect against reading too much at once +# 10/04/2018 - PH Added NoBuffer option # # Notes: Calls the normal file i/o routines unless SeekTest() fails, in # which case the file is buffered in memory to allow random access. @@ -36,13 +37,14 @@ require 5.002; require Exporter; use vars qw($VERSION @ISA @EXPORT_OK); -$VERSION = '1.10'; +$VERSION = '1.11'; @ISA = qw(Exporter); sub Read($$$); # constants my $CHUNK_SIZE = 8192; # size of chunks to read from file (must be power of 2) +my $SKIP_SIZE = 65536; # size to skip when fast-forwarding over sequential data my $SLURP_CHUNKS = 16; # read this many chunks at a time when slurping #------------------------------------------------------------------------------ @@ -60,6 +62,7 @@ sub new($$;$) # string i/o $self = { BUFF_PT => $filePt, + BASE => 0, POS => 0, LEN => length($$filePt), TESTED => -1, @@ -71,8 +74,9 @@ sub new($$;$) $self = { FILE_PT => $filePt, # file pointer BUFF_PT => \$buff, # reference to file data - POS => 0, # current position in file - LEN => 0, # data length + BASE => 0, # location of start of buffer in file + POS => 0, # current position in buffer + LEN => 0, # length of data in buffer TESTED => 0, # 0=untested, 1=passed, -1=failed (requires buffering) }; bless $self, $class; @@ -118,7 +122,7 @@ sub Tell($) my $self = shift; my $rtnVal; if ($self->{TESTED} < 0) { - $rtnVal = $self->{POS}; + $rtnVal = $self->{POS} + $self->{BASE}; } else { $rtnVal = tell($self->{FILE_PT}); } @@ -141,9 +145,11 @@ sub Seek($$;$) if ($self->{TESTED} < 0) { my $newPos; if ($whence == 0) { - $newPos = $num; # from start of file + $newPos = $num - $self->{BASE}; # from start of file } elsif ($whence == 1) { $newPos = $num + $self->{POS}; # relative to current position + } elsif ($self->{NoBuffer} and $self->{FILE_PT}) { + $newPos = -1; # (can't seek relative to end if no buffering) } else { $self->Slurp(); # read whole file into buffer $newPos = $num + $self->{LEN}; # relative to end of file @@ -192,6 +198,8 @@ sub Read($$$) } # read through our buffer if necessary if ($self->{TESTED} < 0) { + # purge old data before reading in NoBuffer mode + $self->Purge() or return 0 if $self->{NoBuffer}; my $buff; my $newPos = $self->{POS} + $len; # number of bytes to read from file @@ -244,6 +252,7 @@ sub ReadLine($$) if ($self->{TESTED} < 0) { my ($num, $buff); + $self->Purge() or return 0 if $self->{NoBuffer}; my $pos = $self->{POS}; if ($fp) { # make sure we have some data after the current position @@ -311,9 +320,39 @@ sub Slurp($) } } +#------------------------------------------------------------------------------ +# Purge internal buffer [internal use only] +# Inputs: 0) reference to RandomAccess object +# Returns: 1 on success, or 0 if current buffer position is negative +# Notes: This is called only in NoBuffer mode +sub Purge($) +{ + my $self = shift; + return 1 unless $self->{FILE_PT}; + return 0 if $self->{POS} < 0; # error if we can't read from here + if ($self->{POS} > $CHUNK_SIZE) { + my $purge = $self->{POS} - ($self->{POS} % $CHUNK_SIZE); + if ($purge >= $self->{LEN}) { + # read up to current position in 64k chunks, discarding as we go + while ($self->{POS} > $self->{LEN}) { + $self->{BASE} += $self->{LEN}; + $self->{POS} -= $self->{LEN}; + ${$self->{BUFF_PT}} = ''; + $self->{LEN} = read($self->{FILE_PT}, ${$self->{BUFF_PT}}, $SKIP_SIZE); + last if $self->{LEN} < $SKIP_SIZE; + } + } elsif ($purge > 0) { + ${$self->{BUFF_PT}} = substr ${$self->{BUFF_PT}}, $purge; + $self->{BASE} += $purge; + $self->{POS} -= $purge; + $self->{LEN} -= $purge; + } + } + return 1; +} #------------------------------------------------------------------------------ -# set binary mode +# Set binary mode # Inputs: 0) reference to RandomAccess object sub BinMode($) { @@ -322,7 +361,7 @@ sub BinMode($) } #------------------------------------------------------------------------------ -# close the file and free the buffer +# Close the file and free the buffer # Inputs: 0) reference to RandomAccess object sub Close($) { @@ -370,6 +409,7 @@ sub Close($) # reset the buffer my $emptyBuff = ''; $self->{BUFF_PT} = \$emptyBuff; + $self->{BASE} = 0; $self->{LEN} = 0; $self->{POS} = 0; } diff --git a/lib/File/RandomAccess.pod b/lib/File/RandomAccess.pod index 860f8a3a..d3a26f37 100644 --- a/lib/File/RandomAccess.pod +++ b/lib/File/RandomAccess.pod @@ -215,6 +215,25 @@ Nothing. =back +=head1 OPTIONS + +=over 4 + +=item B<NoBuffer> + +Avoid buffering sequential files. + + $raf->{NoBuffer} = 1; + +When this option is set, old data is purged from the internal buffer before +a read operation on a sequential file. In this mode, memory requirements +may be significantly reduced when reading sequential files, but seeking +backward is limited to within the size of the internal buffer (which will be +at least as large as the last returned data block), and seeking relative to +the end of file is not allowed. + +=back + =head1 AUTHOR Copyright 2003-2018 Phil Harvey (phil at owl.phy.queensu.ca) diff --git a/lib/Image/ExifTool.pm b/lib/Image/ExifTool.pm index 4b6990a6..df2f2e33 100644 --- a/lib/Image/ExifTool.pm +++ b/lib/Image/ExifTool.pm @@ -27,7 +27,7 @@ use vars qw($VERSION $RELEASE @ISA @EXPORT_OK %EXPORT_TAGS $AUTOLOAD @fileTypes %mimeType $swapBytes $swapWords $currentByteOrder %unpackStd %jpegMarker %specialTags %fileTypeLookup); -$VERSION = '11.12'; +$VERSION = '11.13'; $RELEASE = ''; @ISA = qw(Exporter); %EXPORT_TAGS = ( @@ -5726,6 +5726,8 @@ sub ProcessJPEG($$) return 1 if $fast and $fast == 3; # don't process file when FastScan == 3 $$self{LOW_PRIORITY_DIR}{IFD1} = 1; # lower priority of IFD1 tags } + $$raf{NoBuffer} = 1 if $self->Options('FastScan'); # disable buffering in FastScan mode + $dumpParms{MaxLen} = 128 if $verbose < 4; if ($htmlDump) { $dumpEnd = $raf->Tell(); diff --git a/lib/Image/ExifTool.pod b/lib/Image/ExifTool.pod index 0636429d..98050c42 100644 --- a/lib/Image/ExifTool.pod +++ b/lib/Image/ExifTool.pod @@ -617,12 +617,13 @@ this option set to 1, ExifTool will not scan to the end of a JPEG image to check for an AFCP, CanonVRD, FotoStation, PhotoMechanic, MIE or PreviewImage trailer. This also stops the parsing after the first comment in GIF images, and at the audio/video data with RIFF-format files (AVI, WAV, etc), so any -trailing metadata (eg. XMP written by some utilities) may be missed. When -combined with the ScanForXMP option, prevents scanning for XMP in recognized -file types. With a value of 2, ExifTool will also avoid extracting any EXIF -MakerNote information. When set to 3, the file is not actually parsed, and -only an initial guess at FileType and some pseudo tags are returned. -Default is undef. +trailing metadata (eg. XMP written by some utilities) may be missed. Also +disables input buffering for some types of files to reduce memory usage when +reading from a non-seekable stream. When combined with the ScanForXMP +option, prevents scanning for XMP in recognized file types. With a value of +2, ExifTool will also avoid extracting any EXIF MakerNote information. When +set to 3, the file is not actually parsed, and only an initial guess at +FileType and some pseudo tags are returned. Default is undef. =item Filter @@ -805,10 +806,10 @@ Flag set to assume that QuickTime date/time values are stored as UTC, causing conversion to local time when they are extracted and from local time when written. According to the QuickTime specification date/time values should be UTC, but many digital cameras store local time instead (presumably -because they don't know the time zone), so the default is undef. This -option also disables the autodetection of incorrect time-zero offsets in -QuickTime date/time values, and enforces a time zero of 1904 as per the -QuickTime specification. +because they don't know the time zone), so the default is to not convert +these times. This option also disables the autodetection of incorrect +time-zero offsets in QuickTime date/time values, and enforces a time zero of +1904 as per the QuickTime specification. =item RequestAll @@ -864,9 +865,14 @@ the DateFormat option is used. Default is undef. 0 - Return date/time value unchanged if it can't be converted 1 - Return undef if date/time value can't be converted -When set to 1 while writing a PrintConv date/time value, the value is -written only if POSIX::strptime or Time::Piece is available and can -successfully convert the value. +When set to 1 while writing a PrintConv date/time value with the DateFormat +option set, the value is written only if POSIX::strptime or Time::Piece is +available and can successfully convert the value. + +For PNG CreationTime, this option has the additional effect of causing the +date/time to be reformatted according to PNG 1.2 recommendation (RFC-1123) +when writing, and a warning to be issued for any non-standard value when +reading. =item Struct diff --git a/lib/Image/ExifTool/Canon.pm b/lib/Image/ExifTool/Canon.pm index fa789fef..8931cef5 100644 --- a/lib/Image/ExifTool/Canon.pm +++ b/lib/Image/ExifTool/Canon.pm @@ -87,7 +87,7 @@ sub ProcessCTMD($$$); sub ProcessExifInfo($$$); sub SwapWords($); -$VERSION = '3.99'; +$VERSION = '4.00'; # Note: Removed 'USM' from 'L' lenses since it is redundant - PH # (or is it? Ref 32 shows 5 non-USM L-type lenses) @@ -432,6 +432,7 @@ $VERSION = '3.99'; 368 => 'Sigma 14-24mm f/2.8 DG HSM | A or other Sigma Lens', #IB (A018) 368.1 => 'Sigma 20mm f/1.4 DG HSM | A', #50 (newer firmware) 368.2 => 'Sigma 50mm f/1.4 DG HSM | A', #50 + 368.3 => 'Sigma 40mm f/1.4 DG HSM | A', #IB (018) # Note: LensType 488 (0x1e8) is reported as 232 (0xe8) in 7D CameraSettings 488 => 'Canon EF-S 15-85mm f/3.5-5.6 IS USM', #PH 489 => 'Canon EF 70-300mm f/4-5.6L IS USM', #Gerald Kapounek diff --git a/lib/Image/ExifTool/Exif.pm b/lib/Image/ExifTool/Exif.pm index b2afe8ee..bd60d23a 100644 --- a/lib/Image/ExifTool/Exif.pm +++ b/lib/Image/ExifTool/Exif.pm @@ -55,7 +55,7 @@ use vars qw($VERSION $AUTOLOAD @formatSize @formatName %formatNumber %intFormat use Image::ExifTool qw(:DataAccess :Utils); use Image::ExifTool::MakerNotes; -$VERSION = '4.05'; +$VERSION = '4.06'; sub ProcessExif($$$); sub WriteExif($$$); @@ -4149,7 +4149,7 @@ my %subSecConv = ( 3 => 'FocusDistance', # focus distance in metres (0 is infinity) 4 => 'SubjectDistance', 5 => 'ObjectDistance', - 6 => 'ApproximateFocusDistance ', + 6 => 'ApproximateFocusDistance', 7 => 'FocusDistanceLower', 8 => 'FocusDistanceUpper', }, diff --git a/lib/Image/ExifTool/Nikon.pm b/lib/Image/ExifTool/Nikon.pm index b3a0628d..a26f2f7f 100644 --- a/lib/Image/ExifTool/Nikon.pm +++ b/lib/Image/ExifTool/Nikon.pm @@ -59,7 +59,7 @@ use Image::ExifTool qw(:DataAccess :Utils); use Image::ExifTool::Exif; use Image::ExifTool::GPS; -$VERSION = '3.51'; +$VERSION = '3.52'; sub LensIDConv($$$); sub ProcessNikonAVI($$$); @@ -930,6 +930,27 @@ my %afPoints153 = ( 31 => 'B8', 62 => 'H13', 93 => 'C17', 124 => 'G4', ); +my %cropHiSpeed = ( #IB + 0 => 'Off', + 1 => '1.3x Crop', # (1.3x Crop, Large) + 2 => 'DX Crop', + 3 => '5:4 Crop', + 4 => '3:2 Crop', + 6 => '16:9 Crop', + 9 => 'DX Movie Crop', # (DX during movie recording, Large) + 11 => 'FX Uncropped', + 12 => 'DX Uncropped', + 17 => '1:1 Crop', + OTHER => sub { + my ($val, $inv, $conv) = @_; + return undef if $inv; + my @a = split ' ', $val; + return "Unknown ($val)" unless @a == 7; + $a[0] = $$conv{$a[0]} || "Unknown ($a[0])"; + return "$a[0] ($a[1]x$a[2] cropped to $a[3]x$a[4] at pixel $a[5],$a[6])"; + }, +); + my %offOn = ( 0 => 'Off', 1 => 'On' ); # common attributes for writable BinaryData directories @@ -1138,12 +1159,7 @@ my %binaryDataAttrs = ( Name => 'CropHiSpeed', Writable => 'int16u', Count => 7, - PrintConv => q{ - my @a = split ' ', $val; - return "Unknown ($val)" unless @a == 7; - $a[0] = $a[0] ? "On" : "Off"; - return "$a[0] ($a[1]x$a[2] cropped to $a[3]x$a[4] at pixel $a[5],$a[6])"; - } + PrintConv => \%cropHiSpeed, }, 0x001c => { #28 (D3 "the application of CSb6 to the selected metering mode") Name => 'ExposureTuning', @@ -7981,12 +7997,7 @@ my %nikonFocalConversions = ( Name => 'CropHiSpeed', Writable => 'int16u', Count => 7, - PrintConv => q{ - my @a = split ' ', $val; - return "Unknown ($val)" unless @a == 7; - $a[0] = $a[0] ? "On" : "Off"; - return "$a[0] ($a[1]x$a[2] cropped to $a[3]x$a[4] at pixel $a[5],$a[6])"; - } + PrintConv => \%cropHiSpeed, }, 0x200001e => { Name => 'ColorSpace', diff --git a/lib/Image/ExifTool/PNG.pm b/lib/Image/ExifTool/PNG.pm index ec48baec..c25962ce 100644 --- a/lib/Image/ExifTool/PNG.pm +++ b/lib/Image/ExifTool/PNG.pm @@ -27,7 +27,7 @@ use strict; use vars qw($VERSION $AUTOLOAD %stdCase); use Image::ExifTool qw(:DataAccess :Utils); -$VERSION = '1.45'; +$VERSION = '1.46'; sub ProcessPNG_tEXt($$$); sub ProcessPNG_iTXt($$$); @@ -452,7 +452,7 @@ my %unreg = ( Notes => 'unregistered' ); Groups => { 2 => 'Time' }, Shift => 'Time', Notes => 'stored in RFC-1123 format and converted to/from EXIF format by ExifTool', - ValueConv => \&ConvertPNGDate, + RawConv => \&ConvertPNGDate, ValueConvInv => \&InversePNGDate, PrintConv => '$self->ConvertDateTime($val)', PrintConvInv => '$self->InverseDateTime($val,undef,1)', @@ -651,22 +651,25 @@ my %tzConv = ( ); sub ConvertPNGDate($$) { - my $val = shift; - # standard format is like "Mon, 1 Jan 2018 12:10:22 EST" - if ($val =~ /(\d+)\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*(\d+)\s+(\d+):(\d{2})(:\d{2})?\s*(\S*)/i) { + my ($val, $et) = @_; + # standard format is like "Mon, 1 Jan 2018 12:10:22 EST" (RFC-1123 section 5.2.14) + while ($val =~ /(\d+)\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*(\d+)\s+(\d+):(\d{2})(:\d{2})?\s*(\S*)/i) { my ($day,$mon,$yr,$hr,$min,$sec,$tz) = ($1,$2,$3,$4,$5,$6,$7); $yr += $yr > 70 ? 1900 : 2000 if $yr < 100; # boost year to 4 digits if necessary $mon = $monthNum{ucfirst lc $mon} or return $val; if (not $tz) { $tz = ''; - } elsif ($tzConv{$tz}) { - $tz = $tzConv{$tz}; + } elsif ($tzConv{uc $tz}) { + $tz = $tzConv{uc $tz}; } elsif ($tz =~ /^([-+]\d+):?(\d{2})/) { $tz = $1 . ':' . $2; } else { - return $val; # (non-standard date) + last; # (non-standard date) } - $val = sprintf("%.4d:%.2d:%.2d %.2d:%.2d%s%s",$yr,$mon,$day,$hr,$min,$sec||':00',$tz); + return sprintf("%.4d:%.2d:%.2d %.2d:%.2d%s%s",$yr,$mon,$day,$hr,$min,$sec||':00',$tz); + } + if (($et->Options('StrictDate') and not $$et{TAGS_FROM_FILE}) or $et->Options('Validate')) { + $et->Warn('Non standard PNG date/time format', 1); } return $val; } @@ -678,19 +681,24 @@ sub ConvertPNGDate($$) sub InversePNGDate($$) { my ($val, $et) = @_; - my $err; - if ($val =~ /^(\d{4}):(\d{2}):(\d{2}) (\d{2})(:\d{2})(:\d{2})?(?:\.\d*)?\s*(\S*)/) { - my ($yr,$mon,$day,$hr,$min,$sec,$tz) = ($1,$2,$3,$4,$5,$6,$7); - $sec or $sec = ''; - my %monName = map { $monthNum{$_} => $_ } keys %monthNum; - $mon = $monName{$mon + 0} or $err = 1; - $tz =~ /^(Z|[-+]\d{2}:?\d{2})/ or $err = 1 if length $tz; - $tz =~ tr/://d; - $val = "$day $mon $yr $hr$min$sec $tz" unless $err; - } - if ($err and $et->Options('StrictDate')) { - warn "Invalid date/time (use YYYY:mm:dd HH:MM:SS[.ss][+/-HH:MM|Z])\n"; - undef $val; + if ($et->Options('StrictDate')) { + my $err; + if ($val =~ /^(\d{4}):(\d{2}):(\d{2}) (\d{2})(:\d{2})(:\d{2})?(?:\.\d*)?\s*(\S*)/) { + my ($yr,$mon,$day,$hr,$min,$sec,$tz) = ($1,$2,$3,$4,$5,$6,$7); + $sec or $sec = ''; + my %monName = map { $monthNum{$_} => $_ } keys %monthNum; + $mon = $monName{$mon + 0} or $err = 1; + if (length $tz) { + $tz =~ /^(Z|[-+]\d{2}:?\d{2})/ or $err = 1; + $tz =~ tr/://d; + $tz = ' ' . $tz; + } + $val = "$day $mon $yr $hr$min$sec$tz" unless $err; + } + if ($err) { + warn "Invalid date/time (use YYYY:mm:dd HH:MM:SS[.ss][+/-HH:MM|Z])\n"; + undef $val; + } } return $val; } @@ -1231,6 +1239,8 @@ sub ProcessPNG($$) # check to be sure this is a valid PNG/MNG/JNG image return 0 unless $raf->Read($sig,8) == 8 and $pngLookup{$sig}; + $$raf{NoBuffer} = 1 if $et->Options('FastScan'); # disable buffering in FastScan mode + my $earlyXMP = $et->Options('PNGEarlyXMP'); if ($outfile) { delete $$et{TextChunkType}; diff --git a/lib/Image/ExifTool/QuickTime.pm b/lib/Image/ExifTool/QuickTime.pm index 410723ed..58d0d428 100644 --- a/lib/Image/ExifTool/QuickTime.pm +++ b/lib/Image/ExifTool/QuickTime.pm @@ -42,7 +42,7 @@ use Image::ExifTool qw(:DataAccess :Utils); use Image::ExifTool::Exif; use Image::ExifTool::GPS; -$VERSION = '2.18'; +$VERSION = '2.19'; sub ProcessMOV($$;$); sub ProcessKeys($$$); @@ -7513,6 +7513,8 @@ sub ProcessMOV($$;$) SetByteOrder('MM'); $$et{PRIORITY_DIR} = 'XMP'; # have XMP take priority } + $$raf{NoBuffer} = 1 if $et->Options('FastScan'); # disable buffering in FastScan mode + if ($$et{OPTIONS}{ExtractEmbedded}) { $ee = 1; $unkOpt = $$et{OPTIONS}{Unknown}; diff --git a/lib/Image/ExifTool/QuickTimeStream.pl b/lib/Image/ExifTool/QuickTimeStream.pl index 6a4eaa51..8de1b38e 100644 --- a/lib/Image/ExifTool/QuickTimeStream.pl +++ b/lib/Image/ExifTool/QuickTimeStream.pl @@ -225,7 +225,7 @@ my %processByMetaFormat = ( }, 4 => { Name => 'AngleAxis', - Notes => 'angle axis orientation in radians', + Notes => 'angle axis orientation in radians in local coordinate system', Format => 'float[3]', }, ); @@ -280,6 +280,7 @@ my %processByMetaFormat = ( FIRST_ENTRY => 0, 4 => { Name => 'Position', + Notes => 'X, Y, Z position in local coordinate system', Format => 'float[3]', }, ); @@ -684,6 +685,10 @@ sub ProcessSamples($) # based on known value of 4th-last char = '*' my $dif = ord('*') - ord(substr($buff, -4, 1)); my $tmp = pack 'C*',map { $_=($_+$dif)&0xff } unpack 'C*',substr $buff,1,-1; + if ($verbose > 2) { + $et->VPrint(0, "[decrypted text]\n"); + $et->VerboseDump(\$tmp); + } if ($tmp =~ /^(.*?)(\$[A-Z]{2}RMC.*)/s) { ($val, $buff) = ($1, $2); $val =~ tr/\t/ /; @@ -776,7 +781,7 @@ sub ProcessSamples($) #------------------------------------------------------------------------------ # Process "freeGPS " data blocks referenced by a 'gps ' (GPSDataList) atom -# Inputs: 0) ExifTool ref, 1) dirInfo ref (with SampleTime and SampleDuration), 2) tagTable ref +# Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,SampleTime,SampleDuration}, 2) tagTable ref # Returns: 1 on success (or 0 on unrecognized or "measurement-void" GPS data) # Notes: # - also see ProcessFreeGPS2() below for processing of other types of freeGPS blocks @@ -790,7 +795,12 @@ sub ProcessFreeGPS($$$) return 0 unless length $$dataPt >= 92; if (substr($$dataPt,12,1) eq "\x05") { + # decode encrypted ASCII-based GPS (DashCam Azdome GS63H, ref 5) + # header looks like this in my sample: + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 05 01 00 00 [....freeGPS ....] + # 0010: 01 03 aa aa f2 e1 f0 ee 54 54 98 9a 9b 92 9a 93 [........TT......] + # 0020: 98 9e 98 98 9e 93 98 92 a6 9f 9f 9c 9d ed fa 8a [................] my $n = length($$dataPt) - 18; $n = 0x101 if $n > 0x101; my $buf2 = pack 'C*', map { $_ ^ 0xaa } unpack 'C*', substr($$dataPt,18,$n); @@ -806,8 +816,14 @@ sub ProcessFreeGPS($$$) push @xtra, UserLabel => $lbl if length $lbl; # extract accelerometer data (ref PH) @acc = ($1/100,$2/100,$3/100) if $buf2 =~ /^.{173}([-+]\d{3})([-+]\d{3})([-+]\d{3})/s; + } elsif ($$dataPt =~ /^.{52}(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/) { + # decode NMEA-format GPS data (NextBase 512GW dashcam, ref PH) + # header looks like this in my sample: + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 40 01 00 00 [....freeGPS @...] + # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................] + # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................] push @xtra, CameraDateTime => "$1:$2:$3 $4:$5:$6"; if ($$dataPt =~ /\$[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d+\.\d+),([NS]),(\d+\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/s) { ($lat,$latRef,$lon,$lonRef) = ($5,$6,$7,$8); @@ -828,9 +844,15 @@ sub ProcessFreeGPS($$$) # change to signed integer and divide by 256 map { $_ = $_ - 4294967296 if $_ >= 2147483648; $_ /= 256 } @acc; } + } else { + # decode binary GPS format (Viofo A119S, ref 2) - # (see comments in ScanMovieData() below for structure details) + # header looks like this in my sample: + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...] + # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................] + # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................] + # (records are same structure as Type 3 Novatek GPS in ProcessFreeGPS2() below) ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef,$lat,$lon,$spd,$trk) = unpack('x48V6a1a1a1x1V4', $$dataPt); # ignore invalid fixes @@ -841,6 +863,9 @@ sub ProcessFreeGPS($$$) $spd *= $knotsToKph; # convert speed to km/h # ($trk is not confirmed; may be GPSImageDirection, ref PH) } +# +# save tag values extracted by above code +# FoundSomething($et, $tagTbl, $$dirInfo{SampleTime}, $$dirInfo{SampleDuration}); # lat/long are in DDDmm.mmmm format my $deg = int($lat / 100); @@ -876,7 +901,7 @@ sub ProcessFreeGPS($$$) #------------------------------------------------------------------------------ # Process "freeGPS " data blocks _not_ referenced by a 'gps ' atom -# Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tagTable ref +# Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,DataPos}, 2) tagTable ref # Returns: 1 on success # Notes: # - also see ProcessFreeGPS() above @@ -885,15 +910,25 @@ sub ProcessFreeGPS2($$$) { my ($et, $dirInfo, $tagTbl) = @_; my $dataPt = $$dirInfo{DataPt}; + my ($yr, $mon, $day, $hr, $min, $sec, $pos); + my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, $ddd, $unk); if (substr($$dataPt,0x45,3) eq 'ATC') { + # header looks like this: (sample 1) + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 38 06 00 00 [....freeGPS 8...] + # 0010: 49 51 53 32 30 31 33 30 33 30 36 42 00 00 00 00 [IQS20130306B....] + # 0020: 4d 61 79 20 31 35 20 32 30 31 35 2c 20 31 39 3a [May 15 2015, 19:] + # (sample 2) + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 06 00 00 [....freeGPS L...] + # 0010: 32 30 31 33 30 33 31 38 2e 30 31 00 00 00 00 00 [20130318.01.....] + # 0020: 4d 61 72 20 31 38 20 32 30 31 33 2c 20 31 34 3a [Mar 18 2013, 14:] + my ($recPos, $lastRecPos, $foundNew); my $verbose = $et->Options('Verbose'); my $dataPos = $$dirInfo{DataPos}; my $then = $$et{FreeGPS2}{Then}; $then or $then = $$et{FreeGPS2}{Then} = [ (0) x 6 ]; - # Loop through records in the ATC-type GPS block until we find the most recent. # If we have already found one, then we only need to check the first record # (in case the buffer wrapped around), and the record after the position of @@ -932,12 +967,12 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $gpsBlockSize; $recPos += 52) { next if $now[$i] == $$then[$i]; # we found a more recent record -- extract it and remember its location if ($verbose) { - $et->VPrint(2, " Encrypted GPS record:\n"); + $et->VPrint(2, " + [encrypted GPS record]\n"); $et->VerboseDump(\$a, DataPos => $dataPos + $recPos); - $et->VPrint(0, " Decrypted GPS record:\n"); + $et->VPrint(2, " + [decrypted GPS record]\n"); $et->VerboseDump(\$b); - my @v = unpack 'H8VVC4V!CA3V!CA3VvvV!vCCCCH4', $b; - $et->VPrint(1, " Unpacked: @v\n"); + #my @v = unpack 'H8VVC4V!CA3V!CA3VvvV!vCCCCH4', $b; + #$et->VPrint(2, " + [unpacked: @v]\n"); # values unpacked above (ref PH): # 0) 0x00 4 bytes - byte 0=1, 1=counts to 255, 2=record index, 3=0 (ref 3) # 1) 0x04 4 bytes - int32u: bits 0-4=day, 5-8=mon, 9-19=year (ref 3) @@ -965,7 +1000,7 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $gpsBlockSize; $recPos += 52) { } @$then = @now; $$et{DOC_NUM} = ++$$et{DOC_COUNT}; - my $trk = Get16s(\$b, 0x24) / 100; + $trk = Get16s(\$b, 0x24) / 100; $trk += 360 if $trk < 0; my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', @now[3..5, 0..2]); $et->HandleTag($tagTbl, GPSDateTime => $time); @@ -989,90 +1024,153 @@ ATCRec: for ($recPos = 0x30; $recPos + 52 < $gpsBlockSize; $recPos += 52) { } # save position of most recent record (needed when parsing the next freeGPS block) $$et{FreeGPS2}{RecentRecPos} = $lastRecPos; + return 1; + + } elsif ($$dataPt =~ /^.{60}A\0.{10}([NS])\0.{14}([EW])\0/s) { + + # header looks like this in my sample: + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 08 01 00 00 [....freeGPS ....] + # 0010: 32 30 31 33 30 38 31 35 2e 30 31 00 00 00 00 00 [20130815.01.....] + # 0020: 4a 75 6e 20 31 30 20 32 30 31 37 2c 20 31 34 3a [Jun 10 2017, 14:] + + # Type 2 (ref PH): + # 0x30 - int32u hour + # 0x34 - int32u minute + # 0x38 - int32u second + # 0x3c - int32u GPS status ('A' or 'V') + # 0x40 - double latitude (DDMM.MMMMMM) + # 0x48 - int32u latitude ref ('N' or 'S') + # 0x50 - double longitude (DDMM.MMMMMM) + # 0x58 - int32u longitude ref ('E' or 'W') + # 0x60 - double speed (knots) + # 0x68 - double heading (deg) + # 0x70 - int32u year - 2000 + # 0x74 - int32u month + # 0x78 - int32u day + ($latRef, $lonRef) = ($1, $2); + ($hr,$min,$sec,$yr,$mon,$day) = unpack('x48V3x52V3', $$dataPt); + $lat = GetDouble($dataPt, 0x40); + $lon = GetDouble($dataPt, 0x50); + $spd = GetDouble($dataPt, 0x60) * $knotsToKph; + $trk = GetDouble($dataPt, 0x68); + + } elsif ($$dataPt =~ /^.{72}A([NS])([EW])/s) { + + # Type 3 (Novatek GPS, ref 2): (in case it wasn't decoded via 'gps ' atom) + # 0x30 - int32u hour + # 0x34 - int32u minute + # 0x38 - int32u second + # 0x3c - int32u year - 2000 + # 0x40 - int32u month + # 0x44 - int32u day + # 0x48 - int8u GPS status ('A' or 'V') + # 0x49 - int8u latitude ref ('N' or 'S') + # 0x4a - int8u longitude ref ('E' or 'W') + # 0x4b - 0 + # 0x4c - float latitude (DDMM.MMMMMM) + # 0x50 - float longitude (DDMM.MMMMMM) + # 0x54 - float speed (knots) + # 0x58 - float heading (deg) + # Type 3b, same as above for 0x30-0x4a (ref PH) + # 0x4c - int32s latitude (decimal degrees * 1e7) + # 0x50 - int32s longitude (decimal degrees * 1e7) + # 0x54 - int32s speed (m/s * 100) + # 0x58 - float altitude (m * 1000, NC) + ($latRef, $lonRef) = ($1, $2); + ($hr,$min,$sec,$yr,$mon,$day) = unpack('x48V6', $$dataPt); + if (substr($$dataPt, 16, 3) eq 'IQS') { + # Type 3b (ref PH) + # header looks like this in my sample: + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...] + # 0010: 49 51 53 5f 41 37 5f 32 30 31 35 30 34 31 37 00 [IQS_A7_20150417.] + # 0020: 4d 61 72 20 32 39 20 32 30 31 37 2c 20 31 36 3a [Mar 29 2017, 16:] + $ddd = 1; + $lat = abs Get32s($dataPt, 0x4c) / 1e7; + $lon = abs Get32s($dataPt, 0x50) / 1e7; + $spd = Get32s($dataPt, 0x54) / 100 * $mpsToKph; + $alt = GetFloat($dataPt, 0x58) / 1000; # (NC) + } else { + # Type 3 (ref 2) + $lat = GetFloat($dataPt, 0x4c); + $lon = GetFloat($dataPt, 0x50); + $spd = GetFloat($dataPt, 0x54) * $knotsToKph; + $trk = GetFloat($dataPt, 0x58); + } } else { - my ($yr, $mon, $day, $hr, $min, $sec); - my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, $ddd); - if ($$dataPt =~ /^.{60}A\0.{10}([NS])\0.{14}([EW])\0/s) { - # Type 2 (ref PH): - # 0x30 - int32u hour - # 0x34 - int32u minute - # 0x38 - int32u second - # 0x3c - int32u GPS status ('A' or 'V') - # 0x40 - double latitude (DDMM.MMMMMM) - # 0x48 - int32u latitude ref ('N' or 'S') - # 0x50 - double longitude (DDMM.MMMMMM) - # 0x58 - int32u longitude ref ('E' or 'W') - # 0x60 - double speed (knots) - # 0x68 - double heading (deg) - # 0x70 - int32u year - 2000 - # 0x74 - int32u month - # 0x78 - int32u day - ($latRef, $lonRef) = ($1, $2); - ($hr,$min,$sec,$yr,$mon,$day) = unpack('x' . 0x30 . 'V3x52V3', $$dataPt); - $lat = GetDouble($dataPt, 0x40); - $lon = GetDouble($dataPt, 0x50); - $spd = GetDouble($dataPt, 0x60) * $knotsToKph; - $trk = GetDouble($dataPt, 0x68); - } elsif ($$dataPt =~ /^.{72}A([NS])([EW])/s) { - # Type 3 (Novatek GPS, ref 2): (in case it wasn't decoded via 'gps ' atom) - # 0x30 - int32u hour - # 0x34 - int32u minute - # 0x38 - int32u second - # 0x3c - int32u year - 2000 - # 0x40 - int32u month - # 0x44 - int32u day - # 0x48 - int8u GPS status ('A' or 'V') - # 0x49 - int8u latitude ref ('N' or 'S') - # 0x4a - int8u longitude ref ('E' or 'W') - # 0x4b - 0 - # 0x4c - float latitude (DDMM.MMMMMM) - # 0x50 - float longitude (DDMM.MMMMMM) - # 0x54 - float speed (knots) - # 0x58 - float heading (deg) - ($latRef, $lonRef) = ($1, $2); - ($hr,$min,$sec,$yr,$mon,$day) = unpack('x' . 0x30 . 'V6', $$dataPt); - if (substr($$dataPt, 16, 3) eq 'IQS') { - $ddd = 1; - $lat = abs Get32s($dataPt, 0x4c) / 1e7; - $lon = abs Get32s($dataPt, 0x50) / 1e7; - $spd = Get32s($dataPt, 0x54) / 100 * $mpsToKph; - $alt = GetFloat($dataPt, 0x58) / 1000; # (NC) - } else { - $lat = GetFloat($dataPt, 0x4c); - $lon = GetFloat($dataPt, 0x50); - $spd = GetFloat($dataPt, 0x54) * $knotsToKph; - $trk = GetFloat($dataPt, 0x58); - } - } else { - return 0; # not a recognized or valid freeGPS block - } - return 0 if $mon < 1 or $mon > 12; # quick sanity check - $$et{DOC_NUM} = ++$$et{DOC_COUNT}; - $yr += 2000 if $yr < 2000; - my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $yr, $mon, $day, $hr, $min, $sec); - # convert from DDMM.MMMMMM to DD.DDDDDD format if necessary - unless ($ddd) { - my $deg = int($lat / 100); - $lat = $deg + ($lat - $deg * 100) / 60; - $deg = int($lon / 100); - $lon = $deg + ($lon - $deg * 100) / 60; - } - $et->HandleTag($tagTbl, GPSDateTime => $time); - $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1)); - $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1)); - $et->HandleTag($tagTbl, GPSSpeed => $spd); # (now in km/h) - $et->HandleTag($tagTbl, GPSSpeedRef => 'K'); - if (defined $trk) { + # (look for binary GPS as stored by NextBase 512G, ref PH) + # header looks like this in my sample: + # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 01 00 00 [....freeGPS x...] + # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............] + # 0020: 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 [00000...........] + + # followed by a number of 32-byte records in this format (big endian!): + # 0x30 - int16u unknown (seen: 0x24 0x53 = "$S") + # 0x32 - int16u speed (m/s * 100) + # 0x34 - int16s heading (deg * 100) (or GPSImgDirection?) + # 0x36 - int16u year + # 0x38 - int8u month + # 0x39 - int8u day + # 0x3a - int8u hour + # 0x3b - int8u min + # 0x3c - int16u sec * 10 + # 0x3e - int8u unknown (seen: 2) + # 0x3f - int32s latitude (decimal degrees * 1e7) + # 0x43 - int32s longitude (decimal degrees * 1e7) + # 0x47 - int8u unknown (seen: 16) + # 0x48-0x4f - all zero + for ($pos=0x32; ; ) { + ($spd,$trk,$yr,$mon,$day,$hr,$min,$sec,$unk,$lat,$lon) = unpack "x${pos}nnnCCCCnCNN", $$dataPt; + # validate record using date/time + last if $yr < 2000 or $yr > 2200 or + $mon < 1 or $mon > 12 or + $day < 1 or $day > 31 or + $hr > 59 or $min > 59 or $sec > 600; + # change lat/lon to signed integer and divide by 1e7 + map { $_ = $_ - 4294967296 if $_ >= 2147483648; $_ /= 1e7 } $lat, $lon; + $trk -= 0x10000 if $trk >= 0x8000; # make it signed + $trk /= 100; + $trk += 360 if $trk < 0; + my $time = sprintf("%.4d:%.2d:%.2d %.2d:%.2d:%04.1fZ", $yr, $mon, $day, $hr, $min, $sec/10); + $$et{DOC_NUM} = ++$$et{DOC_COUNT}; + $et->HandleTag($tagTbl, GPSDateTime => $time); + $et->HandleTag($tagTbl, GPSLatitude => $lat); + $et->HandleTag($tagTbl, GPSLongitude => $lon); + $et->HandleTag($tagTbl, GPSSpeed => $spd / 100 * $mpsToKph); + $et->HandleTag($tagTbl, GPSSpeedRef => 'K'); $et->HandleTag($tagTbl, GPSTrack => $trk); $et->HandleTag($tagTbl, GPSTrackRef => 'T'); + last if $pos += 0x20 > length($$dataPt) - 0x1e; } - if (defined $alt) { - $et->HandleTag($tagTbl, GPSAltitude => $alt); - } + return $$et{DOC_NUM} ? 1 : 0; # return 0 if nothing extracted + } +# +# save tag values extracted by above code +# + return 0 if $mon < 1 or $mon > 12; # quick sanity check + $$et{DOC_NUM} = ++$$et{DOC_COUNT}; + $yr += 2000 if $yr < 2000; + my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $yr, $mon, $day, $hr, $min, $sec); + # convert from DDMM.MMMMMM to DD.DDDDDD format if necessary + unless ($ddd) { + my $deg = int($lat / 100); + $lat = $deg + ($lat - $deg * 100) / 60; + $deg = int($lon / 100); + $lon = $deg + ($lon - $deg * 100) / 60; + } + $et->HandleTag($tagTbl, GPSDateTime => $time); + $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1)); + $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1)); + $et->HandleTag($tagTbl, GPSSpeed => $spd); # (now in km/h) + $et->HandleTag($tagTbl, GPSSpeedRef => 'K'); + if (defined $trk) { + $et->HandleTag($tagTbl, GPSTrack => $trk); + $et->HandleTag($tagTbl, GPSTrackRef => 'T'); + } + if (defined $alt) { + $et->HandleTag($tagTbl, GPSAltitude => $alt); } - return 1; } diff --git a/lib/Image/ExifTool/RIFF.pm b/lib/Image/ExifTool/RIFF.pm index 11dc1f82..38a23666 100644 --- a/lib/Image/ExifTool/RIFF.pm +++ b/lib/Image/ExifTool/RIFF.pm @@ -29,7 +29,7 @@ use strict; use vars qw($VERSION); use Image::ExifTool qw(:DataAccess :Utils); -$VERSION = '1.48'; +$VERSION = '1.49'; sub ConvertTimecode($); sub ProcessSGLT($$$); @@ -1607,6 +1607,7 @@ sub ProcessRIFF($$) $buff .= $buf2; return 0 unless $buff =~ /WAVE(.{4})?fmt /sg and $raf->Seek(pos($buff) - 4, 0); } + $$raf{NoBuffer} = 1 if $et->Options('FastScan'); # disable buffering in FastScan mode $mime = $riffMimeType{$type} if $type; $et->SetFileType($type, $mime); $$et{VALUE}{FileType} .= ' (RF64)' if $rf64; diff --git a/lib/Image/ExifTool/TagNames.pod b/lib/Image/ExifTool/TagNames.pod index 5f837868..9051c204 100644 --- a/lib/Image/ExifTool/TagNames.pod +++ b/lib/Image/ExifTool/TagNames.pod @@ -10655,7 +10655,7 @@ DiMAGE X and Xt. 0x0018 FlashExposureBracketValue undef[4] 0x0019 ExposureBracketValue rational64s 0x001a ImageProcessing string - 0x001b CropHiSpeed int16u[7]~ + 0x001b CropHiSpeed int16u[7] 0x001c ExposureTuning undef[3] 0x001d SerialNumber string! 0x001e ColorSpace int16u @@ -11783,7 +11783,7 @@ videos from some Nikon cameras. 0x2000005 WhiteBalance no 0x2000007 FocusMode string 0x200000b WhiteBalanceFineTune no - 0x200001b CropHiSpeed int16u[7]~ + 0x200001b CropHiSpeed int16u[7] 0x200001e ColorSpace no 0x200001f VRInfo Nikon VRInfo 0x2000022 ActiveD-Lighting int16u |