File Coverage

blib/lib/Image/ExifTool/GoPro.pm
Criterion Covered Total %
statement 50 114 43.8
branch 20 82 24.3
condition 21 57 36.8
subroutine 5 9 55.5
pod 0 5 0.0
total 96 267 35.9


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: GoPro.pm
3             #
4             # Description: Read information from GoPro videos
5             #
6             # Revisions: 2018/01/12 - P. Harvey Created
7             #
8             # References: 1) https://github.com/gopro/gpmf-parser
9             # 2) https://github.com/stilldavid/gopro-utils
10             #------------------------------------------------------------------------------
11              
12             package Image::ExifTool::GoPro;
13              
14 1     1   4439 use strict;
  1         2  
  1         33  
15 1     1   5 use vars qw($VERSION);
  1         2  
  1         39  
16 1     1   6 use Image::ExifTool qw(:DataAccess :Utils);
  1         1  
  1         209  
17 1     1   1835 use Image::ExifTool::QuickTime;
  1         305  
  1         2503  
18              
19             $VERSION = '1.07';
20              
21             sub ProcessGoPro($$$);
22             sub ProcessString($$$);
23             sub ScaleValues($$);
24             sub AddUnits($$$);
25             sub ConvertSystemTime($$);
26              
27             # GoPro data types that have ExifTool equivalents (ref 1)
28             my %goProFmt = ( # format codes
29             # 0x00 - container (subdirectory)
30             0x62 => 'int8s', # 'b'
31             0x42 => 'int8u', # 'B'
32             0x63 => 'string', # 'c' (possibly null terminated)
33             0x73 => 'int16s', # 's'
34             0x53 => 'int16u', # 'S'
35             0x6c => 'int32s', # 'l'
36             0x4c => 'int32u', # 'L'
37             0x66 => 'float', # 'f'
38             0x64 => 'double', # 'd'
39             0x46 => 'undef', # 'F' (4-char ID)
40             0x47 => 'undef', # 'G' (16-byte uuid)
41             0x6a => 'int64s', # 'j'
42             0x4a => 'int64u', # 'J'
43             0x71 => 'fixed32s', # 'q'
44             0x51 => 'fixed64s', # 'Q'
45             0x55 => 'undef', # 'U' (16-byte date)
46             0x3f => 'undef', # '?' (complex structure)
47             );
48              
49             # sizes of format codes if different than what FormatSize() would return
50             my %goProSize = (
51             0x46 => 4,
52             0x47 => 16,
53             0x55 => 16,
54             );
55              
56             # tagInfo elements to add units to PrintConv value
57             my %addUnits = (
58             AddUnits => 1,
59             PrintConv => 'Image::ExifTool::GoPro::AddUnits($self, $val, $tag)',
60             );
61              
62             # Tags found in the GPMF box of Hero6 mp4 videos (ref PH), and
63             # the gpmd-format timed metadata of Hero5 and Hero6 videos (ref 1)
64             %Image::ExifTool::GoPro::GPMF = (
65             PROCESS_PROC => \&ProcessGoPro,
66             GROUPS => { 2 => 'Camera' },
67             NOTES => q{
68             Tags extracted from the GPMF box of GoPro MP4 videos, the APP6 "GoPro"
69             segment of JPEG files, and from the "gpmd" timed metadata if the
70             L (-ee) option is enabled. Many more tags exist, but are
71             currently unknown and extracted only with the L (-u) option. Please
72             let me know if you discover the meaning of any of these unknown tags. See
73             L for details about this format.
74             },
75             ACCL => { #2 (gpmd)
76             Name => 'Accelerometer',
77             Notes => 'accelerator readings in m/s2',
78             Binary => 1,
79             },
80             # ANGX (GPMF-GEOC) - seen -0.05 (fmt d, Max)
81             # ANGY (GPMF-GEOC) - seen 179.9 (fmt d, Max)
82             # ANGZ (GPMF-GEOC) - seen 0.152 (fmt d, Max)
83             ALLD => 'AutoLowLightDuration', #1 (gpmd) (untested)
84             # APTO (GPMF) - seen: 'RAW', 'DYNM' (fmt c)
85             ATTD => { #PH (Karma)
86             Name => 'Attitude',
87             # UNIT=s,rad,rad,rad,rad/s,rad/s,rad/s,
88             # TYPE=LffffffB
89             # SCAL=1000 1 1 1 1 1 1 1
90             Binary => 1,
91             },
92             ATTR => { #PH (Karma)
93             Name => 'AttitudeTarget',
94             # UNIT=s,rad,rad,rad,
95             # TYPE=Jffff
96             # SCAL=1000 1 1 1 1
97             Binary => 1,
98             },
99             AUDO => 'AudioSetting', #PH (GPMF - seen: 'WIND', fmt c)
100             # AUPT (GPMF) - seen: 'N','Y' (fmt c)
101             BPOS => { #PH (Karma)
102             Name => 'Controller',
103             Unknown => 1,
104             # UNIT=deg,deg,m,deg,deg,m,m,m
105             # TYPE=lllfffff
106             # SCAL=10000000 10000000 1000 1 1 1 1 1
107             %addUnits,
108             },
109             # BRID (GPMF) - seen: 0 (fmt B)
110             # BROD (GPMF) - seen: 'ASK','' (fmt c)
111             # CALH (GPMF-GEOC) - seen 3040 (fmt L, Max)
112             # CALW (GPMF-GEOC) - seen 4056 (fmt L, Max)
113             CASN => 'CameraSerialNumber', #PH (GPMF - seen: 'C3221324545448', fmt c)
114             # CINF (GPMF) - seen: 0x67376be7709bc8876a8baf3940908618, 0xe230988539b30cf5f016627ae8fc5395,
115             # 0x8bcbe424acc5b37d7d77001635198b3b (fmt B) (Camera INFormation?)
116             # CMOD (GPMF) - seen: 12,13,17 [12 360 video, 13 time-laps video, 17 JPEG] (fmt B)
117             # CRTX (GPMF-BACK/FRNT) - double[1]
118             # CRTY (GPMF-BACK/FRNT) - double[1]
119             CSEN => { #PH (Karma)
120             Name => 'CoyoteSense',
121             # UNIT=s,rad/s,rad/s,rad/s,g,g,g,,,,
122             # TYPE=LffffffLLLL
123             # SCAL=1000 1 1 1 1 1 1 1 1 1 1
124             Binary => 1,
125             },
126             CYTS => { #PH (Karma)
127             Name => 'CoyoteStatus',
128             # UNIT=s,,,,,rad,rad,rad,,
129             # TYPE=LLLLLfffBB
130             # SCAL=1000 1 1 1 1 1 1 1 1 1
131             Binary => 1,
132             },
133             DEVC => { #PH (gpmd,GPMF, fmt \0)
134             Name => 'DeviceContainer',
135             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
136             # (Max) DVID=1,DVNM='Global Settings',VERS,FMWR,LINF,CINF,CASN,MINF,MUID,CMOD,MTYP,OREN,
137             # DZOM,DZST,SMTR,PRTN,PTWB,PTSH,PTCL,EXPT,PIMX,PIMN,PTEV,RATE,SROT,ZFOV,VLTE,VLTA,
138             # EISE,EISA,AUPT,AUDO,BROD,BRID,PVUL,PRJT,SOFF
139             # (Max) DVID='GEOC',DVNM='Geometry Calibrations',SHFX,SHFY,SHFZ,ANGX,ANGY,ANGZ,CALW,CALH
140             # (Max) DVID='BACK',DVNM='Back Lens',KLNS,CTRX,CTRY,MFOV,SFTR
141             # (Max) DVID='FRNT',DVNM='Front Lens',KLNS,CTRX,CTRY,MFOV,SFTR
142             # (Max) DVID='HLMT',DVNM='Highlights'
143             },
144             # DVID (GPMF) - DeviceID; seen: 1 (fmt L), HLMT (fmt F), GEOC (fmt F), 'BACK' (fmt F, Max)
145             DVID => { Name => 'DeviceID', Unknown => 1 }, #2 (gpmd)
146             # DVNM (GPMF) seen: 'Video Global Settings' (fmt c), 'Highlights' (fmt c), 'Geometry Calibrations' (Max)
147             # DVNM (gpmd) seen: 'Camera' (Hero5), 'Hero6 Black' (Hero6), 'GoPro Karma v1.0' (Karma)
148             DVNM => 'DeviceName', #PH (n/c)
149             DZOM => { #PH (GPMF - seen: 'Y', fmt c)
150             Name => 'DigitalZoom',
151             PrintConv => { N => 'No', Y => 'Yes' },
152             },
153             # DZST (GPMF) - seen: 0 (fmt L) (something to do with digital zoom maybe?)
154             EISA => { #PH (GPMF) - seen: 'Y','N','HS EIS','N/A' (fmt c) [N was for a time-lapse video]
155             Name => 'ElectronicImageStabilization',
156             },
157             # EISE (GPMF) - seen: 'Y','N' (fmt c)
158             EMPT => { Name => 'Empty', Unknown => 1 }, #2 (gpmd)
159             ESCS => { #PH (Karma)
160             Name => 'EscapeStatus',
161             # UNIT=s,rpm,rpm,rpm,rpm,rpm,rpm,rpm,rpm,degC,degC,degC,degC,V,V,V,V,A,A,A,A,,,,,,,,,
162             # TYPE=JSSSSSSSSssssSSSSSSSSSSSSSSSSB
163             # (no SCAL!)
164             Unknown => 1,
165             %addUnits,
166             },
167             # EXPT (GPMF) - seen: '', 'AUTO' (fmt c)
168             FACE => 'FaceDetected', #PH (gpmd)
169             FCNM => 'FaceNumbers', #PH (gpmd) (faces counted per frame, ref 1)
170             FMWR => 'FirmwareVersion', #PH (GPMF - seen: HD6.01.01.51.00, fmt c)
171             FWVS => 'OtherFirmware', #PH (NC) (gpmd - seen: '1.1.11.0', Karma)
172             GLPI => { #PH (gpmd, Karma)
173             Name => 'GPSPos',
174             # UNIT=s,deg,deg,m,m,m/s,m/s,m/s,deg
175             # TYPE=LllllsssS
176             # SCAL=1000 10000000 10000000 1000 1000 100 100 100 100
177             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
178             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GLPI' },
179             },
180             GPRI => { #PH (gpmd, Karma)
181             Name => 'GPSRaw',
182             # UNIT=s,deg,deg,m,m,m,m/s,deg,,
183             # TYPE=JlllSSSSBB
184             # SCAL=1000000,10000000,10000000,1000,100,100,100,100,1,1
185             Unknown => 1,
186             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
187             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPRI' },
188             },
189             GPS5 => { #2 (gpmd)
190             Name => 'GPSInfo',
191             # SCAL=10000000,10000000,1000,1000,100
192             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
193             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPS5' },
194             },
195             GPSF => { #2 (gpmd)
196             Name => 'GPSMeasureMode',
197             PrintConv => {
198             2 => '2-Dimensional Measurement',
199             3 => '3-Dimensional Measurement',
200             },
201             },
202             GPSP => { #2 (gpmd)
203             Name => 'GPSHPositioningError',
204             Description => 'GPS Horizontal Positioning Error',
205             ValueConv => '$val / 100', # convert from cm to m
206             },
207             GPSU => { #2 (gpmd)
208             Name => 'GPSDateTime',
209             Groups => { 2 => 'Time' },
210             # (HERO5 writes this in 'c' format, HERO6 writes 'U')
211             ValueConv => '$val =~ s/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/20$1:$2:$3 $4:$5:/; $val',
212             PrintConv => '$self->ConvertDateTime($val)',
213             },
214             GYRO => { #2 (gpmd)
215             Name => 'Gyroscope',
216             Notes => 'gyroscope readings in rad/s',
217             Binary => 1,
218             },
219             # HFLG (APP6) - seen: 0
220             ISOE => 'ISOSpeeds', #PH (gpmd)
221             ISOG => { #2 (gpmd)
222             Name => 'ImageSensorGain',
223             Binary => 1,
224             },
225             KBAT => { #PH (gpmd) (Karma)
226             Name => 'BatteryStatus',
227             # UNIT=A,Ah,J,degC,V,V,V,V,s,%,,,,,%
228             # TYPE=lLlsSSSSSSSBBBb
229             # SCAL=1000,1000,0.00999999977648258,100,1000,1000,1000,1000,0.0166666675359011,1,1,1,1,1,1
230             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
231             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::KBAT' },
232             },
233             # KLNS (GPMF-BACK/FRNT) - double[5] (fmt d, Max)
234             # LINF (GPMF) - seen: LAJ7061916601668,C3341326002180,C33632245450981 (fmt c) (Lens INFormation?)
235             LNED => { #PH (Karma)
236             Name => 'LocalPositionNED',
237             # UNIT=s,m,m,m,m/s,m/s,m/s
238             # TYPE=Lffffff
239             # SCAL=1000 1 1 1 1 1 1
240             Binary => 1,
241             },
242             MAGN => 'Magnetometer', #1 (gpmd) (units of uT)
243             # MFOV (GPMF-BACK/FRNT) - seen: 100 (fmt d, Max)
244             MINF => { #PH (GPMF - seen: HERO6 Black, fmt c)
245             Name => 'Model',
246             Groups => { 2 => 'Camera' },
247             Description => 'Camera Model Name',
248             },
249             # MTYP (GPMF) - seen: 0,1,5,11 [1 for time-lapse video, 5 for 360 video, 11 for JPEG] (fmt B)
250             # MUID (GPMF) - seen: 3882563431 2278071152 967805802 411471936 0 0 0 0 (fmt L)
251             OREN => { #PH (GPMF - seen: 'U', fmt c)
252             Name => 'AutoRotation',
253             PrintConv => {
254             U => 'Up',
255             D => 'Down', # (NC)
256             A => 'Auto', # (NC)
257             },
258             },
259             # (most of the "P" tags are ProTune settings - PH)
260             PHDR => 'HDRSetting', #PH (APP6 - seen: 0)
261             PIMN => 'AutoISOMin', #PH (GPMF - seen: 100, fmt L)
262             PIMX => 'AutoISOMax', #PH (GPMF - seen: 1600, fmt L)
263             # PRAW (APP6) - seen: 0, 'N', 'Y' (fmt c)
264             PRES => 'PhotoResolution', #PH (APP6 - seen: '12MP_W')
265             # PRJT (APP6) - seen: 'GPRO','EACO' (fmt F, Hero8, Max)
266             PRTN => { #PH (GPMF - seen: 'N', fmt c)
267             Name => 'ProTune',
268             PrintConv => {
269             N => 'Off',
270             Y => 'On', # (NC)
271             },
272             },
273             PTCL => 'ColorMode', #PH (GPMF - seen: 'GOPRO', fmt c' APP6: 'FLAT')
274             PTEV => 'ExposureCompensation', #PH (GPMF - seen: '0.0', fmt c)
275             PTSH => 'Sharpness', #PH (GPMF - seen: 'HIGH', fmt c)
276             PTWB => 'WhiteBalance', #PH (GPMF - seen: 'AUTO', fmt c)
277             # PVUL (APP6) - seen: 'F' (fmt c, Hero8, Max)
278             RATE => 'Rate', #PH (GPMF - seen: '0_5SEC', fmt c; APP6 - seen: '4_1SEC')
279             RMRK => { #2 (gpmd)
280             Name => 'Comments',
281             ValueConv => '$self->Decode($val, "Latin")',
282             },
283             SCAL => { #2 (gpmd) scale factor for subsequent data
284             Name => 'ScaleFactor',
285             Unknown => 1,
286             },
287             SCPR => { #PH (Karma) [stream was empty]
288             Name => 'ScaledPressure',
289             # UNIT=s,Pa,Pa,degC
290             # TYPE=Lffs
291             # SCAL=1000 0.00999999977648258 0.00999999977648258 100
292             %addUnits,
293             },
294             # SFTR (GPMF-BACK/FRNT) - seen 0.999,1.00004 (fmt d, Max)
295             # SHFX (GPMF-GEOC) - seen 22.92 (fmt d, Max)
296             # SHFY (GPMF-GEOC) - seen 0.123 (fmt d, Max)
297             # SHFZ (GPMF-GEOC) - seen 36.06 (fmt d, Max)
298             SHUT => { #2 (gpmd)
299             Name => 'ExposureTimes',
300             PrintConv => q{
301             my @a = split ' ', $val;
302             $_ = Image::ExifTool::Exif::PrintExposureTime($_) foreach @a;
303             return join ' ', @a;
304             },
305             },
306             SIMU => { #PH (Karma)
307             Name => 'ScaledIMU',
308             # UNIT=s,g,g,g,rad/s,rad/s,rad/s,T,T,T
309             # TYPE=Lsssssssss
310             # SCAL=1000 1000 1000 1000 1000 1000 1000 1000 1000 1000
311             %addUnits,
312             },
313             SIUN => { #2 (gpmd - seen : 'm/s2','rad/s')
314             Name => 'SIUnits',
315             Unknown => 1,
316             ValueConv => '$self->Decode($val, "Latin")',
317             },
318             # SMTR (GPMF) - seen: 'N' (fmt c)
319             # SOFF (APP6) - seen: 0 (fmt L, Hero8, Max)
320             # SROT (GPMF) - seen 20.60 (fmt f, Max)
321             STMP => { #1 (gpmd)
322             Name => 'TimeStamp',
323             ValueConv => '$val / 1e6',
324             },
325             STRM => { #2 (gpmd,GPMF, fmt \0)
326             Name => 'NestedSignalStream',
327             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
328             },
329             STNM => { #2 (gpmd)
330             Name => 'StreamName',
331             Unknown => 1,
332             ValueConv => '$self->Decode($val, "Latin")',
333             },
334             SYST => { #PH (Karma)
335             Name => 'SystemTime',
336             # UNIT=s,s
337             # TYPE=JJ
338             # SCAL=1000000 1000
339             # save system time calibrations for later
340             RawConv => q{
341             my @v = split ' ', $val;
342             if (@v == 2) {
343             my $s = $$self{SystemTimeList};
344             $s or $s = $$self{SystemTimeList} = [ ];
345             push @$s, \@v;
346             }
347             return $val;
348             },
349             },
350             # TICK => { Name => 'InTime', Unknown => 1, ValueConv => '$val/1000' }, #1 (gpmd)
351             TMPC => { #2 (gpmd)
352             Name => 'CameraTemperature',
353             PrintConv => '"$val C"',
354             },
355             # TOCK => { Name => 'OutTime', Unknown => 1, ValueConv => '$val/1000' }, #1 (gpmd)
356             TSMP => { Name => 'TotalSamples', Unknown => 1 }, #2 (gpmd)
357             TYPE => { Name => 'StructureType', Unknown => 1 }, #2 (gpmd,GPMF - eg 'LLLllfFff', fmt c)
358             UNIT => { #2 (gpmd) alternative units
359             Name => 'Units',
360             Unknown => 1,
361             ValueConv => '$self->Decode($val, "Latin")',
362             },
363             VERS => {
364             Name => 'MetadataVersion',
365             PrintConv => '$val =~ tr/ /./; $val',
366             },
367             VFOV => { #PH (GPMF - seen: 'W', fmt c)
368             Name => 'FieldOfView',
369             PrintConv => {
370             W => 'Wide',
371             S => 'Super View', # (NC, not seen)
372             L => 'Linear', # (NC, not seen)
373             },
374             },
375             # VLTA (GPMF) - seen: 78 ('N') (fmt B -- wrong format?)
376             VFRH => { #PH (Karma)
377             Name => 'VisualFlightRulesHUD',
378             BinaryData => 1,
379             # UNIT=m/s,m/s,m,m/s,deg,%
380             # TYPE=ffffsS
381             },
382             # VLTE (GPMF) - seen: 'Y','N' (fmt c)
383             WBAL => 'ColorTemperatures', #PH (gpmd)
384             WRGB => { #PH (gpmd)
385             Name => 'WhiteBalanceRGB',
386             Binary => 1,
387             },
388             # ZFOV (APP6,GPMF) - seen: 148.34, 0 (fmt f, Hero8, Max)
389             # the following ref forum12825
390             MUID => {
391             Name => 'MediaUniqueID',
392             PrintConv => q{
393             my @a = split ' ', $val;
394             $_ = sprintf('%.8x',$_) foreach @a;
395             return join('', @a);
396             },
397             },
398             EXPT => 'MaximumShutterAngle',
399             MTRX => 'AccelerometerMatrix',
400             ORIN => 'InputOrientation',
401             ORIO => 'OutputOrientation',
402             UNIF => 'InputUniformity',
403             SROT => 'SensorReadoutTime',
404             );
405              
406             # GoPro GPS5 tags (ref 2) (Hero5,Hero6)
407             %Image::ExifTool::GoPro::GPS5 = (
408             PROCESS_PROC => \&ProcessString,
409             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
410             VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
411             0 => { # (unit='deg')
412             Name => 'GPSLatitude',
413             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
414             },
415             1 => { # (unit='deg')
416             Name => 'GPSLongitude',
417             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
418             },
419             2 => { # (unit='m')
420             Name => 'GPSAltitude',
421             PrintConv => '"$val m"',
422             },
423             3 => 'GPSSpeed', # (unit='m/s')
424             4 => 'GPSSpeed3D', # (unit='m/s')
425             );
426              
427             # GoPro GPRI tags (ref PH) (Karma)
428             %Image::ExifTool::GoPro::GPRI = (
429             PROCESS_PROC => \&ProcessString,
430             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
431             VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
432             0 => { # (unit='s')
433             Name => 'GPSDateTimeRaw',
434             Groups => { 2 => 'Time' },
435             ValueConv => \&ConvertSystemTime, # convert to date/time based on SystemTime clock
436             PrintConv => '$self->ConvertDateTime($val)',
437             },
438             1 => { # (unit='deg')
439             Name => 'GPSLatitudeRaw',
440             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
441             },
442             2 => { # (unit='deg')
443             Name => 'GPSLongitudeRaw',
444             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
445             },
446             3 => {
447             Name => 'GPSAltitudeRaw', # (NC)
448             PrintConv => '"$val m"',
449             },
450             # (unknown tags must be defined so that ProcessString() will iterate through all values)
451             4 => { Name => 'GPRI_Unknown4', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
452             5 => { Name => 'GPRI_Unknown5', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
453             6 => 'GPSSpeedRaw', # (NC) # (unit='m/s' -- should convert to other units?)
454             7 => 'GPSTrackRaw', # (NC) # (unit='deg')
455             8 => { Name => 'GPRI_Unknown8', Unknown => 1, Hidden => 1 }, # (no units)
456             9 => { Name => 'GPRI_Unknown9', Unknown => 1, Hidden => 1 }, # (no units)
457             );
458              
459             # GoPro GLPI tags (ref PH) (Karma)
460             %Image::ExifTool::GoPro::GLPI = (
461             PROCESS_PROC => \&ProcessString,
462             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
463             VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
464             0 => { # (unit='s')
465             Name => 'GPSDateTime',
466             Groups => { 2 => 'Time' },
467             ValueConv => \&ConvertSystemTime, # convert to date/time based on SystemTime clock
468             PrintConv => '$self->ConvertDateTime($val)',
469             },
470             1 => { # (unit='deg')
471             Name => 'GPSLatitude',
472             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
473             },
474             2 => { # (unit='deg')
475             Name => 'GPSLongitude',
476             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
477             },
478             3 => { # (unit='m')
479             Name => 'GPSAltitude', # (NC)
480             PrintConv => '"$val m"',
481             },
482             # (unknown tags must be defined so that ProcessString() will iterate through all values)
483             4 => { Name => 'GLPI_Unknown4', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
484             5 => { Name => 'GPSSpeedX', PrintConv => '"$val m/s"' }, # (NC)
485             6 => { Name => 'GPSSpeedY', PrintConv => '"$val m/s"' }, # (NC)
486             7 => { Name => 'GPSSpeedZ', PrintConv => '"$val m/s"' }, # (NC)
487             8 => { Name => 'GPSTrack' }, # (unit='deg')
488             );
489              
490             # GoPro KBAT tags (ref PH)
491             %Image::ExifTool::GoPro::KBAT = (
492             PROCESS_PROC => \&ProcessString,
493             GROUPS => { 1 => 'GoPro', 2 => 'Camera' },
494             VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
495             NOTES => 'Battery status information found in GoPro Karma videos.',
496             0 => { Name => 'BatteryCurrent', PrintConv => '"$val A"' },
497             1 => { Name => 'BatteryCapacity', PrintConv => '"$val Ah"' },
498             2 => { Name => 'KBAT_Unknown2', PrintConv => '"$val J"', Unknown => 1, Hidden => 1 },
499             3 => { Name => 'BatteryTemperature', PrintConv => '"$val C"' },
500             4 => { Name => 'BatteryVoltage1', PrintConv => '"$val V"' },
501             5 => { Name => 'BatteryVoltage2', PrintConv => '"$val V"' },
502             6 => { Name => 'BatteryVoltage3', PrintConv => '"$val V"' },
503             7 => { Name => 'BatteryVoltage4', PrintConv => '"$val V"' },
504             8 => { Name => 'BatteryTime', PrintConv => 'ConvertDuration(int($val + 0.5))' }, # (NC)
505             9 => { Name => 'KBAT_Unknown9', PrintConv => '"$val %"', Unknown => 1, Hidden => 1, },
506             10 => { Name => 'KBAT_Unknown10', Unknown => 1, Hidden => 1 }, # (no units)
507             11 => { Name => 'KBAT_Unknown11', Unknown => 1, Hidden => 1 }, # (no units)
508             12 => { Name => 'KBAT_Unknown12', Unknown => 1, Hidden => 1 }, # (no units)
509             13 => { Name => 'KBAT_Unknown13', Unknown => 1, Hidden => 1 }, # (no units)
510             14 => { Name => 'BatteryLevel', PrintConv => '"$val %"' },
511             );
512              
513             # GoPro fdsc tags written by the Hero5 and Hero6 (ref PH)
514             %Image::ExifTool::GoPro::fdsc = (
515             GROUPS => { 2 => 'Camera' },
516             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
517             NOTES => q{
518             Tags extracted from the MP4 "fdsc" timed metadata when the L
519             (-ee) option is used.
520             },
521             0x08 => { Name => 'FirmwareVersion', Format => 'string[15]' },
522             0x17 => { Name => 'SerialNumber', Format => 'string[16]' },
523             0x57 => { Name => 'OtherSerialNumber', Format => 'string[15]' }, # (NC)
524             0x66 => {
525             Name => 'Model',
526             Description => 'Camera Model Name',
527             Format => 'string[16]',
528             },
529             # ...
530             # after this there are lots of interesting values also found in the GPMF box,
531             # but this block is lacking tag ID's and any directory structure, so the
532             # value offsets are therefore presumably firmware dependent :(
533             );
534              
535             #------------------------------------------------------------------------------
536             # Convert system time to date/time string
537             # Inputs: 0) system time value, 1) ExifTool ref
538             # Returns: EXIF-format date/time string with milliseconds
539             sub ConvertSystemTime($$)
540             {
541 0     0 0 0 my ($val, $et) = @_;
542 0 0       0 my $s = $$et{SystemTimeList} or return '';
543 0 0       0 unless ($$et{SystemTimeListSorted}) {
544 0         0 $s = $$et{SystemTimeList} = [ sort { $$a[0] <=> $$b[0] } @$s ];
  0         0  
545 0         0 $$et{SystemTimeListSorted} = 1;
546             }
547 0         0 my ($i, $j) = (0, $#$s);
548             # perform binary search to find this system time value
549 0         0 while ($j - $i > 1) {
550 0         0 my $t = int(($i + $j) / 2);
551 0 0       0 ($val < $$s[$t][0] ? $j : $i) = $t;
552             }
553 0 0 0     0 if ($i == $j or $$s[$j][0] == $$s[$i][0]) {
554 0         0 $val = $$s[$i][1];
555             } else {
556             # interpolate between values
557 0         0 $val = $$s[$i][1] + ($$s[$j][1] - $$s[$i][1]) * ($val - $$s[$i][0]) / ($$s[$j][0] - $$s[$i][0]);
558             }
559             # (a bit tricky to remove fractional seconds then add them back again after
560             # the date/time conversion while avoiding round-off errors which could
561             # put the seconds out by 1...)
562 0         0 my ($t, $f) = ("$val" =~ /^(\d+)(\.\d+)/);
563 0         0 return Image::ExifTool::ConvertUnixTime($t, $$et{OPTIONS}{QuickTimeUTC}) . $f;
564             }
565              
566             #------------------------------------------------------------------------------
567             # Scale values by last 'SCAL' constants
568             # Inputs: 0) value or list of values, 1) string of scale factors
569             # Returns: nothing, but updates values
570             sub ScaleValues($$)
571             {
572 0     0 0 0 my ($val, $scl) = @_;
573 0 0 0     0 return unless $val and $scl;
574 0 0       0 my @scl = split ' ', $scl or return;
575 0         0 my @scaled;
576 0 0       0 my $v = (ref $val eq 'ARRAY') ? $val : [ $val ];
577 0         0 foreach $val (@$v) {
578 0         0 my @a = split ' ', $val;
579 0         0 $a[$_] /= $scl[$_ % @scl] foreach 0..$#a;
580 0         0 push @scaled, join(' ', @a);
581             }
582 0 0       0 $_[0] = @scaled > 1 ? \@scaled : $scaled[0];
583             }
584              
585             #------------------------------------------------------------------------------
586             # Add units to values for human-readable output
587             # Inputs: 0) ExifTool ref, 1) value, 2) tag key
588             # Returns: converted value
589             sub AddUnits($$$)
590             {
591 0     0 0 0 my ($et, $val, $tag) = @_;
592 0 0 0     0 if ($et and $$et{TAG_EXTRA}{$tag} and $$et{TAG_EXTRA}{$tag}{Units}) {
      0        
593 0         0 my $u = $$et{TAG_EXTRA}{$tag}{Units};
594 0 0       0 $u = [ $u ] unless ref $u eq 'ARRAY';
595 0         0 my @a = split ' ', $val;
596 0 0       0 if (@$u == @a) {
597 0         0 my $i;
598 0         0 for ($i=0; $i<@a; ++$i) {
599 0 0       0 $a[$i] .= ' ' . $$u[$i] if $$u[$i];
600             }
601 0         0 $val = join ' ', @a;
602             }
603             }
604 0         0 return $val;
605             }
606              
607             #------------------------------------------------------------------------------
608             # Process string of values (or array of strings) to extract as separate tags
609             # Inputs: 0) ExifTool object ref, 1) directory information ref, 2) tag table ref
610             # Returns: 1 on success
611             sub ProcessString($$$)
612             {
613 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
614 0         0 my $dataPt = $$dirInfo{DataPt};
615 0 0       0 my @list = ref $$dataPt eq 'ARRAY' ? @{$$dataPt} : ( $$dataPt );
  0         0  
616 0         0 my ($string, $val);
617 0         0 $et->VerboseDir('GoPro structure');
618 0         0 foreach $string (@list) {
619 0         0 my @val = split ' ', $string;
620 0         0 my $i = 0;
621 0         0 foreach $val (@val) {
622 0         0 $et->HandleTag($tagTablePtr, $i, $val);
623 0 0       0 $$tagTablePtr{++$i} or $i = 0;
624             }
625             }
626 0         0 return 1;
627             }
628              
629             #------------------------------------------------------------------------------
630             # Process GoPro metadata (gpmd samples, GPMF box, or APP6) (ref PH/1/2)
631             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
632             # Returns: 1 on success
633             # - with hack to check for encrypted text in gpmd data (Rove Stealth 4K)
634             sub ProcessGoPro($$$)
635             {
636 3     3 0 9 my ($et, $dirInfo, $tagTablePtr) = @_;
637 3         7 my $dataPt = $$dirInfo{DataPt};
638 3         5 my $base = $$dirInfo{Base};
639 3   50     8 my $pos = $$dirInfo{DirStart} || 0;
640 3   33     7 my $dirEnd = $pos + ($$dirInfo{DirLen} || (length($$dataPt) - $pos));
641 3         23 my $verbose = $et->Options('Verbose');
642 3   33     12 my $unknown = $verbose || $et->Options('Unknown');
643 3         7 my ($size, $type, $unit, $scal, $setGroup0);
644              
645 3 50 0     15 $et->VerboseDir($$dirInfo{DirName} || 'GPMF', undef, $dirEnd-$pos) if $verbose;
646 3 50       8 if ($pos) {
647 3         5 my $parent = $$dirInfo{Parent};
648 3 100 66     14 $setGroup0 = $$et{SET_GROUP0} = 'APP6' if $parent and $parent eq 'APP6';
649             } else {
650             # set group0 to "QuickTime" unless group1 is being changed (to Track#)
651 0 0       0 $setGroup0 = $$et{SET_GROUP0} = 'QuickTime' unless $$et{SET_GROUP1};
652             }
653              
654 3         14 for (; $pos+8<=$dirEnd; $pos+=($size+3)&0xfffffffc) {
655 32         149 my ($tag,$fmt,$len,$count) = unpack("x${pos}a4CCn", $$dataPt);
656 32         63 $size = $len * $count;
657 32         45 $pos += 8;
658 32 50       62 last if $pos + $size > $dirEnd;
659 32         85 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
660 32 50       70 last if $tag eq "\0\0\0\0"; # stop at null tag
661 32 50 33     65 next unless $size or $verbose; # don't save empty values unless verbose
662 32   100     86 my $format = $goProFmt{$fmt} || 'undef';
663 32         54 my ($val, $i, $j, $p, @v);
664 32 50 33     177 if ($fmt == 0x3f and defined $type) {
    50 100        
      100        
      66        
665             # decode structure with format given by previous 'TYPE'
666 0         0 for ($i=0; $i<$count; ++$i) {
667 0         0 my (@s, $l);
668 0         0 for ($j=0, $p=0; $j
669 0         0 my $b = Get8u(\$type, $j);
670 0 0       0 my $f = $goProFmt{$b} or last;
671 0 0 0     0 $l = $goProSize{$b} || Image::ExifTool::FormatSize($f) or last;
672 0 0       0 last if $p + $l > $len;
673 0         0 my $s = ReadValue($dataPt, $pos+$i*$len+$p, $f, undef, $l);
674 0 0       0 last unless defined $s;
675 0         0 push @s, $s;
676             }
677 0 0       0 push @v, join ' ', @s if @s;
678             }
679 0 0       0 $val = @v > 1 ? \@v : $v[0];
680             } elsif (($format eq 'undef' or $format eq 'string') and $count > 1 and $len > 1) {
681             # unpack multiple undef/string values as a list
682 0 0       0 my $a = $format eq 'undef' ? 'a' : 'A';
683 0         0 $val = [ unpack("x${pos}".("$a$len" x $count), $$dataPt) ];
684             } else {
685 32         78 $val = ReadValue($dataPt, $pos, $format, undef, $size);
686             }
687             # save TYPE, UNIT/SIUN and SCAL values for later
688 32 50       68 $type = $val if $tag eq 'TYPE';
689 32 50 33     105 $unit = $val if $tag eq 'UNIT' or $tag eq 'SIUN';
690 32 50       55 $scal = $val if $tag eq 'SCAL';
691              
692 32 100       60 unless ($tagInfo) {
693 9 50       19 next unless $unknown;
694 9         25 my $name = Image::ExifTool::QuickTime::PrintableTagID($tag);
695 9         42 $tagInfo = { Name => "GoPro_$name", Description => "GoPro $name", Unknown => 1 };
696 9 50       20 $$tagInfo{SubDirectory} = { TagTable => 'Image::ExifTool::GoPro::GPMF' } if not $fmt;
697 9         25 AddTagToTable($tagTablePtr, $tag, $tagInfo);
698             }
699             # apply scaling if available to last tag in this container
700 32 0 33     74 ScaleValues($val, $scal) if $scal and $tag ne 'SCAL' and $pos+$size+3>=$dirEnd;
      33        
701 32 0       131 my $key = $et->HandleTag($tagTablePtr, $tag, $val,
    50          
702             DataPt => $dataPt,
703             Base => $base,
704             Start => $pos,
705             Size => $size,
706             TagInfo => $tagInfo,
707             Format => $format,
708             Extra => $verbose ? ", type='".($fmt ? chr($fmt) : '\0')."' size=$len count=$count" : undef,
709             );
710             # save units for adding in print conversion if specified
711 32 50 33     125 $$et{TAG_EXTRA}{$key}{Units} = $unit if $$tagInfo{AddUnits} and $key;
712             }
713 3 100       10 delete $$et{SET_GROUP0} if $setGroup0;
714 3         9 return 1;
715             }
716              
717             1; # end
718              
719             __END__