File Coverage

blib/lib/Image/ExifTool/GoPro.pm
Criterion Covered Total %
statement 52 145 35.8
branch 21 96 21.8
condition 21 57 36.8
subroutine 5 10 50.0
pod 0 6 0.0
total 99 314 31.5


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             # 3) https://github.com/gopro/gpmf-parser
11             #------------------------------------------------------------------------------
12              
13             package Image::ExifTool::GoPro;
14              
15 1     1   6654 use strict;
  1         3  
  1         120  
16 1     1   7 use vars qw($VERSION);
  1         1  
  1         56  
17 1     1   5 use Image::ExifTool qw(:DataAccess :Utils);
  1         2  
  1         235  
18 1     1   1964 use Image::ExifTool::QuickTime;
  1         5  
  1         2980  
19              
20             $VERSION = '1.15';
21              
22             sub ProcessGoPro($$$);
23             sub ProcessString($$$);
24             sub ScaleValues($$);
25             sub AddUnits($$$);
26             sub ConvertSystemTime($$);
27              
28             # GoPro data types that have ExifTool equivalents (ref 1)
29             my %goProFmt = ( # format codes
30             # 0x00 - container (subdirectory)
31             0x62 => 'int8s', # 'b'
32             0x42 => 'int8u', # 'B'
33             0x63 => 'string', # 'c' (possibly null terminated)
34             0x73 => 'int16s', # 's'
35             0x53 => 'int16u', # 'S'
36             0x6c => 'int32s', # 'l'
37             0x4c => 'int32u', # 'L'
38             0x66 => 'float', # 'f'
39             0x64 => 'double', # 'd'
40             0x46 => 'undef', # 'F' (4-char ID)
41             0x47 => 'undef', # 'G' (16-byte uuid)
42             0x6a => 'int64s', # 'j'
43             0x4a => 'int64u', # 'J'
44             0x71 => 'fixed32s', # 'q'
45             0x51 => 'fixed64s', # 'Q'
46             0x55 => 'undef', # 'U' (16-byte date)
47             0x3f => 'undef', # '?' (complex structure)
48             );
49              
50             # sizes of format codes if different than what FormatSize() would return
51             my %goProSize = (
52             0x46 => 4,
53             0x47 => 16,
54             0x55 => 16,
55             );
56              
57             # tagInfo elements to add units to PrintConv value
58             my %addUnits = (
59             AddUnits => 1,
60             PrintConv => 'Image::ExifTool::GoPro::AddUnits($self, $val, $tag)',
61             );
62              
63             my %noYes = ( N => 'No', Y => 'Yes' );
64              
65             # Tags found in the GPMF box of Hero6 mp4 videos (ref PH), and
66             # the gpmd-format timed metadata of Hero5 and Hero6 videos (ref 1)
67             %Image::ExifTool::GoPro::GPMF = (
68             PROCESS_PROC => \&ProcessGoPro,
69             GROUPS => { 2 => 'Camera' },
70             NOTES => q{
71             Tags extracted from the GPMF box of GoPro MP4 videos, the APP6 "GoPro"
72             segment of JPEG files, and from the "gpmd" timed metadata if the
73             L (-ee) option is enabled. Many more tags exist, but are
74             currently unknown and extracted only with the L (-u) option. Please
75             let me know if you discover the meaning of any of these unknown tags. See
76             L for details about this format.
77             },
78             ABSC => 'AutoBoostScore', #3
79             ACCL => { #2 (gpmd)
80             Name => 'Accelerometer',
81             Notes => 'accelerator readings in m/s2',
82             Binary => 1,
83             },
84             # ANGX (GPMF-GEOC) - seen -0.05 (fmt d, Max)
85             # ANGY (GPMF-GEOC) - seen 179.9 (fmt d, Max)
86             # ANGZ (GPMF-GEOC) - seen 0.152 (fmt d, Max)
87             ALLD => 'AutoLowLightDuration', #1 (gpmd) (untested)
88             APTO => 'AudioProtuneOption', #3
89             ARUW => 'AspectRatioUnwarped', #3
90             ARWA => 'AspectRatioWarped', #3
91             ATTD => { #PH (Karma)
92             Name => 'Attitude',
93             # UNIT=s,rad,rad,rad,rad/s,rad/s,rad/s,
94             # TYPE=LffffffB
95             # SCAL=1000 1 1 1 1 1 1 1
96             Binary => 1,
97             },
98             ATTR => { #PH (Karma)
99             Name => 'AttitudeTarget',
100             # UNIT=s,rad,rad,rad,
101             # TYPE=Jffff
102             # SCAL=1000 1 1 1 1
103             Binary => 1,
104             },
105             AUBT => { Name => 'AudioBlueTooth', PrintConv => \%noYes }, #3
106             AUDO => 'AudioSetting', #PH (GPMF - seen: 'WIND', fmt c)
107             AUPT => { Name => 'AutoProtune', PrintConv => \%noYes },
108             BITR => 'BitrateSetting', #3
109             BPOS => { #PH (Karma)
110             Name => 'Controller',
111             Unknown => 1,
112             # UNIT=deg,deg,m,deg,deg,m,m,m
113             # TYPE=lllfffff
114             # SCAL=10000000 10000000 1000 1 1 1 1 1
115             %addUnits,
116             },
117             # BRID (GPMF) - seen: 0 (fmt B)
118             # BROD (GPMF) - seen: 'ASK','' (fmt c)
119             # CALH (GPMF-GEOC) - seen 3040 (fmt L, Max)
120             # CALW (GPMF-GEOC) - seen 4056 (fmt L, Max)
121             CASN => 'CameraSerialNumber', #PH (GPMF - seen: 'C3221324545448', fmt c)
122             CDAT => { #3
123             Name => 'CreationDate',
124             Groups => { 2 => 'Time' },
125             RawConv => 'ConvertUnixTime($val)',
126             PrintConv => '$self->ConvertDateTime($val)',
127             },
128             CDTM => 'CaptureDelayTimer', #3
129             # CINF (GPMF) - seen: 0x67376be7709bc8876a8baf3940908618, 0xe230988539b30cf5f016627ae8fc5395,
130             # 0x8bcbe424acc5b37d7d77001635198b3b (fmt B) (Camera INFormation?)
131             CLDP => { Name => 'ClassificationDataPresent', PrintConv => \%noYes },
132             # CLKC (GPMF) - seen: 0 (fmt L)
133             # CLKS (GPMF) - seen: 2 (fmt B)
134             # CMOD (GPMF) - seen: 12,13,17 [12 360 video, 13 time-laps video, 17 JPEG] (fmt B) - CameraMode (ref 3)
135             # CPID (GPMF) - seen: '1194885996 3387225026 733916448 2433577768' (fmt L)
136             # CPIN (GPMF) - seen: 1
137             CPIN => 'ChapterNumber',
138             # CRTX (GPMF-BACK/FRNT) - double[1]
139             # CRTY (GPMF-BACK/FRNT) - double[1]
140             CSEN => { #PH (Karma)
141             Name => 'CoyoteSense',
142             # UNIT=s,rad/s,rad/s,rad/s,g,g,g,,,,
143             # TYPE=LffffffLLLL
144             # SCAL=1000 1 1 1 1 1 1 1 1 1 1
145             Binary => 1,
146             },
147             CTRL => 'ControlLevel', #3
148             CYTS => { #PH (Karma)
149             Name => 'CoyoteStatus',
150             # UNIT=s,,,,,rad,rad,rad,,
151             # TYPE=LLLLLfffBB
152             # SCAL=1000 1 1 1 1 1 1 1 1 1
153             Binary => 1,
154             },
155             DEVC => { #PH (gpmd,GPMF, fmt \0)
156             Name => 'DeviceContainer',
157             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
158             # (Max) DVID=1,DVNM='Global Settings',VERS,FMWR,LINF,CINF,CASN,MINF,MUID,CMOD,MTYP,OREN,
159             # DZOM,DZST,SMTR,PRTN,PTWB,PTSH,PTCL,EXPT,PIMX,PIMN,PTEV,RATE,SROT,ZFOV,VLTE,VLTA,
160             # EISE,EISA,AUPT,AUDO,BROD,BRID,PVUL,PRJT,SOFF
161             # (Max) DVID='GEOC',DVNM='Geometry Calibrations',SHFX,SHFY,SHFZ,ANGX,ANGY,ANGZ,CALW,CALH
162             # (Max) DVID='BACK',DVNM='Back Lens',KLNS,CTRX,CTRY,MFOV,SFTR
163             # (Max) DVID='FRNT',DVNM='Front Lens',KLNS,CTRX,CTRY,MFOV,SFTR
164             # (Max) DVID='HLMT',DVNM='Highlights'
165             },
166             # DNSC (GPMF) - seen: 'HIGH' (fmt c)
167             DUST => 'DurationSetting', #3
168             # DVID (GPMF) - DeviceID; seen: 1 (fmt L), HLMT (fmt F), GEOC (fmt F), 'BACK' (fmt F, Max)
169             DVID => { Name => 'DeviceID', Unknown => 1 }, #2 (gpmd)
170             # DVNM (GPMF) seen: 'Video Global Settings' (fmt c), 'Highlights' (fmt c), 'Geometry Calibrations' (Max)
171             # DVNM (gpmd) seen: 'Camera' (Hero5), 'Hero6 Black' (Hero6), 'GoPro Karma v1.0' (Karma)
172             DVNM => 'DeviceName', #PH (n/c)
173             DZOM => { #PH (GPMF - seen: 'Y', fmt c)
174             Name => 'DigitalZoomOn',
175             PrintConv => \%noYes,
176             },
177             DZMX => 'DigitalZoomAmount', #3
178             DZST => 'DigitalZoom', #3
179             EISA => { #PH (GPMF) - seen: 'Y','N','HS EIS','N/A' (fmt c) [N was for a time-lapse video]
180             Name => 'ElectronicImageStabilization',
181             },
182             EISE => { Name => 'ElectronicStabilizationOn', PrintConv => \%noYes }, #3
183             EMPT => { Name => 'Empty', Unknown => 1 }, #2 (gpmd)
184             ESCS => { #PH (Karma)
185             Name => 'EscapeStatus',
186             # UNIT=s,rpm,rpm,rpm,rpm,rpm,rpm,rpm,rpm,degC,degC,degC,degC,V,V,V,V,A,A,A,A,,,,,,,,,
187             # TYPE=JSSSSSSSSssssSSSSSSSSSSSSSSSSB
188             # (no SCAL!)
189             Unknown => 1,
190             %addUnits,
191             },
192             EXPT => 'ExposureType', #3
193             FACE => 'FaceDetected', #PH (gpmd)
194             FCNM => 'FaceNumbers', #PH (gpmd) (faces counted per frame, ref 1)
195             FMWR => 'FirmwareVersion', #PH (GPMF - seen: HD6.01.01.51.00, fmt c)
196             FWVS => 'OtherFirmware', #PH (NC) (gpmd - seen: '1.1.11.0', Karma)
197             GLPI => { #PH (gpmd, Karma)
198             Name => 'GPSPos',
199             # UNIT=s,deg,deg,m,m,m/s,m/s,m/s,deg
200             # TYPE=LllllsssS
201             # SCAL=1000 10000000 10000000 1000 1000 100 100 100 100
202             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
203             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GLPI' },
204             },
205             GPRI => { #PH (gpmd, Karma)
206             Name => 'GPSRaw',
207             # UNIT=s,deg,deg,m,m,m,m/s,deg,,
208             # TYPE=JlllSSSSBB
209             # SCAL=1000000,10000000,10000000,1000,100,100,100,100,1,1
210             Unknown => 1,
211             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
212             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPRI' },
213             },
214             GPS5 => { #2 (gpmd)
215             Name => 'GPSInfo',
216             # UNIT=deg,deg,m,m/s,m/s
217             # TYPE=l
218             # SCAL=10000000,10000000,1000,1000,100
219             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
220             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPS5' },
221             },
222             GPS9 => { #PH (gpmd, Hero 13)
223             Name => 'GPSInfo9',
224             # UNIT=deg,deg,m,m/s,m/s,,s,,
225             # TYPE=lllllllSS
226             # SCAL=10000000 10000000 1000 1000 100 1 1000 100 1
227             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
228             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPS9' },
229             },
230             GPSF => { #2 (gpmd)
231             Name => 'GPSMeasureMode',
232             PrintConv => {
233             2 => '2-Dimensional Measurement',
234             3 => '3-Dimensional Measurement',
235             },
236             },
237             GPSP => { #2 (gpmd)
238             Name => 'GPSHPositioningError',
239             Description => 'GPS Horizontal Positioning Error',
240             ValueConv => '$val / 100', # convert from cm to m
241             },
242             GPSU => { #2 (gpmd)
243             Name => 'GPSDateTime',
244             Groups => { 2 => 'Time' },
245             # (HERO5 writes this in 'c' format, HERO6 writes 'U')
246             ValueConv => '$val =~ s/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/20$1:$2:$3 $4:$5:/; $val',
247             PrintConv => '$self->ConvertDateTime($val)',
248             },
249             GYRO => { #2 (gpmd)
250             Name => 'Gyroscope',
251             Notes => 'gyroscope readings in rad/s',
252             Binary => 1,
253             },
254             LOGS => 'HealthLogs',
255             HCTL => 'HorizonControl', #3
256             HDRV => { Name => 'HDRVideo', PrintConv => \%noYes }, #3/PH (NC)
257             # HFLG (APP6) - seen: 0
258             HSGT => 'HindsightSettings', #3
259             ISOE => 'ISOSpeeds', #PH (gpmd)
260             ISOG => { #2 (gpmd)
261             Name => 'ImageSensorGain',
262             Binary => 1,
263             },
264             KBAT => { #PH (gpmd) (Karma)
265             Name => 'BatteryStatus',
266             # UNIT=A,Ah,J,degC,V,V,V,V,s,%,,,,,%
267             # TYPE=lLlsSSSSSSSBBBb
268             # SCAL=1000,1000,0.00999999977648258,100,1000,1000,1000,1000,0.0166666675359011,1,1,1,1,1,1
269             RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
270             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::KBAT' },
271             },
272             # KLNS (GPMF-BACK/FRNT) - double[5] (fmt d, Max)
273             # LINF (GPMF) - seen: LAJ7061916601668,C3341326002180,C33632245450981 (fmt c) (Lens INFormation?)
274             # LMOD (GPMF) - seen: 'NONE' (fmt F)
275             LNED => { #PH (Karma)
276             Name => 'LocalPositionNED',
277             # UNIT=s,m,m,m,m/s,m/s,m/s
278             # TYPE=Lffffff
279             # SCAL=1000 1 1 1 1 1 1
280             Binary => 1,
281             },
282             MAGN => 'Magnetometer', #1 (gpmd) (units of uT)
283             MAPX => 'MappingXCoefficients', #3
284             MAPY => 'MappingYCoefficients', #3
285             # MFOV (GPMF-BACK/FRNT) - seen: 100 (fmt d, Max)
286             MINF => { #PH (GPMF - seen: HERO6 Black, fmt c)
287             Name => 'Model',
288             Groups => { 2 => 'Camera' },
289             Description => 'Camera Model Name',
290             },
291             MMOD => 'MediaMode', #3
292             # MTYP (GPMF) - seen: 0,1,5,11 [1 for time-lapse video, 5 for 360 video, 11 for JPEG] (fmt B) - MediaType (ref 3)
293             MUID => { Name => 'MediaUID', ValueConv => 'join("-", unpack("H8H4H4H4H12", $val))' },
294             MXCF => 'MappingXMode', #3
295             MYCF => 'MappingYMode', #3
296             ORDP => { Name => 'OrientationDataPresent', PrintConv => \%noYes }, #3
297             OREN => { #PH (GPMF - seen: 'U', fmt c)
298             Name => 'AutoRotation',
299             PrintConv => {
300             U => 'Up',
301             D => 'Down', # (NC)
302             A => 'Auto', # (NC)
303             },
304             },
305             # (most of the "P" tags are Protune settings - PH)
306             PHDR => 'HDRSetting', #PH (APP6 - seen: 0)
307             PIMD => 'ProtuneISOMode', #3
308             PIMN => 'AutoISOMin', #PH (GPMF - seen: 100, fmt L)
309             PIMX => 'AutoISOMax', #PH (GPMF - seen: 1600, fmt L)
310             POLY => 'PolynomialCoefficients', #3
311             # PRAW (APP6) - seen: 0, 'N', 'Y' (fmt c)
312             # PRCN (GPMF) - seen: 65 zeros (fmt B)
313             PRES => 'PhotoResolution', #PH (APP6 - seen: '12MP_W')
314             PRJT => 'LensProjection', #3
315             # PRNA (GPMF) - seen 10 (fmt B) - PresetIDs (ref 3)
316             # PRNU (GPMF) - seen 0 (fmt B) - PresetIDs (ref 3)
317             PRTN => { #PH (GPMF - seen: 'N', fmt c)
318             Name => 'Protune',
319             PrintConv => {
320             N => 'Off',
321             Y => 'On', # (NC)
322             },
323             },
324             PTCL => 'ColorMode', #PH (GPMF - seen: 'GOPRO', fmt c' APP6: 'FLAT')
325             PTEV => 'ExposureCompensation', #PH (GPMF - seen: '0.0', fmt c)
326             PTSH => 'Sharpness', #PH (GPMF - seen: 'HIGH', fmt c)
327             PTWB => 'WhiteBalance', #PH (GPMF - seen: 'AUTO', fmt c)
328             # PVUL (APP6) - seen: 'F' (fmt c, Hero8, Max)
329             PWPR => 'PowerProfile', #3
330             PYCF => 'PolynomialPower', #3
331             RAMP => 'SpeedRampSetting', #3
332             RATE => 'Rate', #PH (GPMF - seen: '0_5SEC', fmt c; APP6 - seen: '4_1SEC')
333             RMRK => { #2 (gpmd)
334             Name => 'Comments',
335             ValueConv => '$self->Decode($val, "Latin")',
336             },
337             SCAL => { #2 (gpmd) scale factor for subsequent data
338             Name => 'ScaleFactor',
339             Unknown => 1,
340             },
341             SCAP => { Name => 'ScheduleCapture', PrintConv => \%noYes }, #3
342             SCPR => { #PH (Karma) [stream was empty]
343             Name => 'ScaledPressure',
344             # UNIT=s,Pa,Pa,degC
345             # TYPE=Lffs
346             # SCAL=1000 0.00999999977648258 0.00999999977648258 100
347             %addUnits,
348             },
349             SCTM => 'ScheduleCaptureTime', #3 (seconds since UTC midnight)
350             # SFTR (GPMF-BACK/FRNT) - seen 0.999,1.00004 (fmt d, Max)
351             # SHFX (GPMF-GEOC) - seen 22.92 (fmt d, Max)
352             # SHFY (GPMF-GEOC) - seen 0.123 (fmt d, Max)
353             # SHFZ (GPMF-GEOC) - seen 36.06 (fmt d, Max)
354             SHUT => { #2 (gpmd)
355             Name => 'ExposureTimes',
356             PrintConv => q{
357             my @a = split ' ', $val;
358             $_ = Image::ExifTool::Exif::PrintExposureTime($_) foreach @a;
359             return join ' ', @a;
360             },
361             },
362             SIMU => { #PH (Karma)
363             Name => 'ScaledIMU',
364             # UNIT=s,g,g,g,rad/s,rad/s,rad/s,T,T,T
365             # TYPE=Lsssssssss
366             # SCAL=1000 1000 1000 1000 1000 1000 1000 1000 1000 1000
367             %addUnits,
368             },
369             SIUN => { #2 (gpmd - seen : 'm/s2','rad/s')
370             Name => 'SIUnits',
371             Unknown => 1,
372             ValueConv => '$self->Decode($val, "Latin")',
373             },
374             SMTR => { Name => 'SpotMeter', PrintConv => \%noYes }, #3
375             # SOFF (APP6) - seen: 0 (fmt L, Hero8, Max)
376             SROT => 'SensorReadoutTime', #3
377             STMP => { #1 (gpmd)
378             Name => 'TimeStamp',
379             ValueConv => '$val / 1e6',
380             },
381             STRM => { #2 (gpmd,GPMF, fmt \0)
382             Name => 'NestedSignalStream',
383             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
384             },
385             STNM => { #2 (gpmd)
386             Name => 'StreamName',
387             Unknown => 1,
388             ValueConv => '$self->Decode($val, "Latin")',
389             },
390             SYST => { #PH (Karma)
391             Name => 'SystemTime',
392             # UNIT=s,s
393             # TYPE=JJ
394             # SCAL=1000000 1000
395             # save system time calibrations for later
396             RawConv => q{
397             my @v = split ' ', $val;
398             if (@v == 2) {
399             my $s = $$self{SystemTimeList};
400             $s or $s = $$self{SystemTimeList} = [ ];
401             push @$s, \@v;
402             }
403             return $val;
404             },
405             },
406             # TICK => { Name => 'InTime', Unknown => 1, ValueConv => '$val/1000' }, #1 (gpmd)
407             TMPC => { #2 (gpmd)
408             Name => 'CameraTemperature',
409             PrintConv => '"$val C"',
410             },
411             # TOCK => { Name => 'OutTime', Unknown => 1, ValueConv => '$val/1000' }, #1 (gpmd)
412             TSMP => { Name => 'TotalSamples', Unknown => 1 }, #2 (gpmd)
413             TIMO => 'TimeOffset',
414             TYPE => { Name => 'StructureType', Unknown => 1 }, #2 (gpmd,GPMF - eg 'LLLllfFff', fmt c)
415             TZON => { # (GPMF) - seen: 60 (fmt s)
416             Name => 'TimeZone',
417             PrintConv => 'Image::ExifTool::TimeZoneString($val)',
418             },
419             UNIT => { #2 (gpmd) alternative units
420             Name => 'Units',
421             Unknown => 1,
422             ValueConv => '$self->Decode($val, "Latin")',
423             },
424             VERS => {
425             Name => 'MetadataVersion',
426             PrintConv => '$val =~ tr/ /./; $val',
427             },
428             VFOV => { #PH (GPMF - seen: 'W', fmt c)
429             Name => 'FieldOfView',
430             PrintConv => {
431             W => 'Wide',
432             S => 'Super View', # (NC, not seen)
433             L => 'Linear', # (NC, not seen)
434             },
435             },
436             # VLTA (GPMF) - seen: 78 ('N') (fmt B -- wrong format?)
437             VFPS => { Name => 'VideoFrameRate', PrintConv => '$val=~s( )(/);$val' }, #PH (GPMF, fmt L)
438             VFRH => { #PH (Karma)
439             Name => 'VisualFlightRulesHUD',
440             BinaryData => 1,
441             # UNIT=m/s,m/s,m,m/s,deg,%
442             # TYPE=ffffsS
443             },
444             # VLTE (GPMF) - seen: 'Y','N' (fmt c)
445             VRES => { Name => 'VideoFrameSize', PrintConv => '$val=~s/ /x/;$val' }, #PH (GPMF, fmt L)
446             WBAL => 'ColorTemperatures', #PH (gpmd)
447             WRGB => { #PH (gpmd)
448             Name => 'WhiteBalanceRGB',
449             Binary => 1,
450             },
451             ZFOV => 'DiagonalFieldOfView', #3
452             ZMPL => 'ZoomScaleNormalization', #3
453             #
454             # the following ref forum12825
455             #
456             MUID => {
457             Name => 'MediaUniqueID',
458             PrintConv => q{
459             my @a = split ' ', $val;
460             $_ = sprintf('%.8x',$_) foreach @a;
461             return join('', @a);
462             },
463             },
464             MTRX => 'AccelerometerMatrix',
465             ORIN => 'InputOrientation',
466             ORIO => 'OutputOrientation',
467             UNIF => 'InputUniformity',
468             SROT => 'SensorReadoutTime',
469             # the following are ref https://exiftool.org/forum/index.php?topic=15517.0
470             CORI => { Name => 'CameraOrientation', Binary => 1, Notes => 'quaternions 0-1' },
471             AALP => { Name => 'AudioLevel', Notes => 'dBFS' },
472             GPSA => 'GPSAltitudeSystem', # (eg. 'MSLV')
473             GRAV => { Name => 'GravityVector', Binary => 1 },
474             # DISP - Disparity track
475             HUES => 'PredominantHue',
476             IORI => { Name => 'ImageOrientation', Binary => 1, Notes => 'quaternions 0-1' },
477             # LRVO - ? Part of LRV Frame Skip
478             # LRVS - ? Part of LRV Frame Skip
479             # LSKP - LRV Frame Skip
480             # MSKP - MRV Frame Skip
481             MWET => 'MicrophoneWet',
482             SCEN => 'SceneClassification', # (SNOW,URBA,INDO,WATR,VEGE,BEAC + probability)
483             WNDM => 'WindProcessing',
484             YAVG => 'LumaAverage',
485             );
486              
487             # GoPro GPS5 tags (ref 2) (Hero5,Hero6,Hero9)
488             %Image::ExifTool::GoPro::GPS5 = (
489             PROCESS_PROC => \&ProcessString,
490             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
491             VARS => { ID_FMT => 'dec', ID_LABEL => 'Index' },
492             0 => { # (unit='deg')
493             Name => 'GPSLatitude',
494             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
495             },
496             1 => { # (unit='deg')
497             Name => 'GPSLongitude',
498             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
499             },
500             2 => { # (unit='m')
501             Name => 'GPSAltitude',
502             PrintConv => '"$val m"',
503             },
504             3 => {
505             Name => 'GPSSpeed',
506             Notes => 'stored as m/s but converted to km/h when extracted',
507             ValueConv => '$val * 3.6',
508             },
509             4 => {
510             Name => 'GPSSpeed3D',
511             Notes => 'stored as m/s but converted to km/h when extracted',
512             ValueConv => '$val * 3.6',
513             },
514             );
515              
516             # GoPro GPS9 tags (Hero13)
517             %Image::ExifTool::GoPro::GPS9 = (
518             PROCESS_PROC => \&ProcessString,
519             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
520             VARS => { ID_FMT => 'dec', ID_LABEL => 'Index' },
521             0 => { # (unit='deg')
522             Name => 'GPSLatitude',
523             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
524             },
525             1 => { # (unit='deg')
526             Name => 'GPSLongitude',
527             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
528             },
529             2 => { # (unit='m')
530             Name => 'GPSAltitude',
531             PrintConv => '"$val m"',
532             },
533             3 => {
534             Name => 'GPSSpeed',
535             Notes => 'stored as m/s but converted to km/h when extracted',
536             ValueConv => '$val * 3.6',
537             },
538             4 => {
539             Name => 'GPSSpeed3D',
540             Notes => 'stored as m/s but converted to km/h when extracted',
541             ValueConv => '$val * 3.6',
542             },
543             5 => { # days since 2000
544             Name => 'GPSDays',
545             RawConv => '$$self{GPSDays} = $val; undef',
546             Hidden => 1,
547             },
548             6 => { # seconds of date/time
549             Name => 'GPSDateTime',
550             Groups => { 2 => 'Time' },
551             # (10957 days from Jan 1 1970 to Jan 1 2000)
552             RawConv => 'ConvertUnixTime(($$self{GPSDays} + 10957) * 24 * 3600 + $val, undef, 3)',
553             PrintConv => '$self->ConvertDateTime($val)',
554             },
555             7 => 'GPSDOP',
556             8 => {
557             Name => 'GPSMeasureMode', #PH (NC)
558             PrintConv => {
559             2 => '2-Dimensional Measurement',
560             3 => '3-Dimensional Measurement',
561             },
562             },
563             );
564              
565             # GoPro GPRI tags (ref PH) (Karma)
566             %Image::ExifTool::GoPro::GPRI = (
567             PROCESS_PROC => \&ProcessString,
568             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
569             VARS => { ID_FMT => 'dec', ID_LABEL => 'Index' },
570             0 => { # (unit='s')
571             Name => 'GPSDateTimeRaw',
572             Groups => { 2 => 'Time' },
573             ValueConv => \&ConvertSystemTime, # convert to date/time based on SystemTime clock
574             PrintConv => '$self->ConvertDateTime($val)',
575             },
576             1 => { # (unit='deg')
577             Name => 'GPSLatitudeRaw',
578             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
579             },
580             2 => { # (unit='deg')
581             Name => 'GPSLongitudeRaw',
582             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
583             },
584             3 => {
585             Name => 'GPSAltitudeRaw', # (NC)
586             PrintConv => '"$val m"',
587             },
588             # (unknown tags must be defined so that ProcessString() will iterate through all values)
589             4 => { Name => 'GPRI_Unknown4', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
590             5 => { Name => 'GPRI_Unknown5', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
591             6 => 'GPSSpeedRaw', # (NC) # (unit='m/s' -- should convert to other units?)
592             7 => 'GPSTrackRaw', # (NC) # (unit='deg')
593             8 => { Name => 'GPRI_Unknown8', Unknown => 1, Hidden => 1 }, # (no units)
594             9 => { Name => 'GPRI_Unknown9', Unknown => 1, Hidden => 1 }, # (no units)
595             );
596              
597             # GoPro GLPI tags (ref PH) (Karma)
598             %Image::ExifTool::GoPro::GLPI = (
599             PROCESS_PROC => \&ProcessString,
600             GROUPS => { 1 => 'GoPro', 2 => 'Location' },
601             VARS => { ID_FMT => 'dec', ID_LABEL => 'Index' },
602             0 => { # (unit='s')
603             Name => 'GPSDateTime',
604             Groups => { 2 => 'Time' },
605             ValueConv => \&ConvertSystemTime, # convert to date/time based on SystemTime clock
606             PrintConv => '$self->ConvertDateTime($val)',
607             },
608             1 => { # (unit='deg')
609             Name => 'GPSLatitude',
610             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
611             },
612             2 => { # (unit='deg')
613             Name => 'GPSLongitude',
614             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
615             },
616             3 => { # (unit='m')
617             Name => 'GPSAltitude', # (NC)
618             PrintConv => '"$val m"',
619             },
620             # (unknown tags must be defined so that ProcessString() will iterate through all values)
621             4 => { Name => 'GLPI_Unknown4', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
622             5 => { Name => 'GPSSpeedX', PrintConv => '"$val m/s"' }, # (NC)
623             6 => { Name => 'GPSSpeedY', PrintConv => '"$val m/s"' }, # (NC)
624             7 => { Name => 'GPSSpeedZ', PrintConv => '"$val m/s"' }, # (NC)
625             8 => { Name => 'GPSTrack' }, # (unit='deg')
626             );
627              
628             # GoPro KBAT tags (ref PH)
629             %Image::ExifTool::GoPro::KBAT = (
630             PROCESS_PROC => \&ProcessString,
631             GROUPS => { 1 => 'GoPro', 2 => 'Camera' },
632             VARS => { ID_FMT => 'dec', ID_LABEL => 'Index' },
633             NOTES => 'Battery status information found in GoPro Karma videos.',
634             0 => { Name => 'BatteryCurrent', PrintConv => '"$val A"' },
635             1 => { Name => 'BatteryCapacity', PrintConv => '"$val Ah"' },
636             2 => { Name => 'KBAT_Unknown2', PrintConv => '"$val J"', Unknown => 1, Hidden => 1 },
637             3 => { Name => 'BatteryTemperature', PrintConv => '"$val C"' },
638             4 => { Name => 'BatteryVoltage1', PrintConv => '"$val V"' },
639             5 => { Name => 'BatteryVoltage2', PrintConv => '"$val V"' },
640             6 => { Name => 'BatteryVoltage3', PrintConv => '"$val V"' },
641             7 => { Name => 'BatteryVoltage4', PrintConv => '"$val V"' },
642             8 => { Name => 'BatteryTime', PrintConv => 'ConvertDuration(int($val + 0.5))' }, # (NC)
643             9 => { Name => 'KBAT_Unknown9', PrintConv => '"$val %"', Unknown => 1, Hidden => 1, },
644             10 => { Name => 'KBAT_Unknown10', Unknown => 1, Hidden => 1 }, # (no units)
645             11 => { Name => 'KBAT_Unknown11', Unknown => 1, Hidden => 1 }, # (no units)
646             12 => { Name => 'KBAT_Unknown12', Unknown => 1, Hidden => 1 }, # (no units)
647             13 => { Name => 'KBAT_Unknown13', Unknown => 1, Hidden => 1 }, # (no units)
648             14 => { Name => 'BatteryLevel', PrintConv => '"$val %"' },
649             );
650              
651             # GoPro fdsc tags written by the Hero5 and Hero6 (ref PH)
652             %Image::ExifTool::GoPro::fdsc = (
653             GROUPS => { 2 => 'Camera' },
654             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
655             NOTES => q{
656             Tags extracted from the MP4 "fdsc" timed metadata when the L
657             (-ee) option is used.
658             },
659             0x08 => { Name => 'FirmwareVersion', Format => 'string[15]' },
660             0x17 => { Name => 'SerialNumber', Format => 'string[16]' },
661             0x57 => { Name => 'OtherSerialNumber', Format => 'string[15]' }, # (NC)
662             0x66 => {
663             Name => 'Model',
664             Description => 'Camera Model Name',
665             Format => 'string[16]',
666             },
667             # ...
668             # after this there are lots of interesting values also found in the GPMF box,
669             # but this block is lacking tag ID's and any directory structure, so the
670             # value offsets are therefore presumably firmware dependent :(
671             );
672              
673             #------------------------------------------------------------------------------
674             # Convert system time to date/time string
675             # Inputs: 0) system time value, 1) ExifTool ref
676             # Returns: EXIF-format date/time string with milliseconds
677             sub ConvertSystemTime($$)
678             {
679 0     0 0 0 my ($val, $et) = @_;
680 0 0       0 my $s = $$et{SystemTimeList} or return '';
681 0 0       0 unless ($$et{SystemTimeListSorted}) {
682 0         0 $s = $$et{SystemTimeList} = [ sort { $$a[0] <=> $$b[0] } @$s ];
  0         0  
683 0         0 $$et{SystemTimeListSorted} = 1;
684             }
685 0         0 my ($i, $j) = (0, $#$s);
686             # perform binary search to find this system time value
687 0         0 while ($j - $i > 1) {
688 0         0 my $t = int(($i + $j) / 2);
689 0 0       0 ($val < $$s[$t][0] ? $j : $i) = $t;
690             }
691 0 0 0     0 if ($i == $j or $$s[$j][0] == $$s[$i][0]) {
692 0         0 $val = $$s[$i][1];
693             } else {
694             # interpolate between values
695 0         0 $val = $$s[$i][1] + ($$s[$j][1] - $$s[$i][1]) * ($val - $$s[$i][0]) / ($$s[$j][0] - $$s[$i][0]);
696             }
697             # (a bit tricky to remove fractional seconds then add them back again after
698             # the date/time conversion while avoiding round-off errors which could
699             # put the seconds out by 1...)
700 0         0 my ($t, $f) = ("$val" =~ /^(\d+)(\.\d+)/);
701 0         0 return Image::ExifTool::ConvertUnixTime($t, $$et{OPTIONS}{QuickTimeUTC}) . $f;
702             }
703              
704             #------------------------------------------------------------------------------
705             # Scale values by last 'SCAL' constants
706             # Inputs: 0) value or list of values, 1) string of scale factors
707             # Returns: nothing, but updates values
708             sub ScaleValues($$)
709             {
710 0     0 0 0 my ($val, $scl) = @_;
711 0 0 0     0 return unless $val and $scl;
712 0 0       0 my @scl = split ' ', $scl or return;
713 0         0 my @scaled;
714 0 0       0 my $v = (ref $val eq 'ARRAY') ? $val : [ $val ];
715 0         0 foreach $val (@$v) {
716 0         0 my @a = split ' ', $val;
717 0         0 $a[$_] /= $scl[$_ % @scl] foreach 0..$#a;
718 0         0 push @scaled, join(' ', @a);
719             }
720 0 0       0 $_[0] = @scaled > 1 ? \@scaled : $scaled[0];
721             }
722              
723             #------------------------------------------------------------------------------
724             # Add units to values for human-readable output
725             # Inputs: 0) ExifTool ref, 1) value, 2) tag key
726             # Returns: converted value
727             sub AddUnits($$$)
728             {
729 0     0 0 0 my ($et, $val, $tag) = @_;
730 0 0 0     0 if ($et and $$et{TAG_EXTRA}{$tag}{Units}) {
731 0         0 my $u = $$et{TAG_EXTRA}{$tag}{Units};
732 0 0       0 $u = [ $u ] unless ref $u eq 'ARRAY';
733 0         0 my @a = split ' ', $val;
734 0 0       0 if (@$u == @a) {
735 0         0 my $i;
736 0         0 for ($i=0; $i<@a; ++$i) {
737 0 0       0 $a[$i] .= ' ' . $$u[$i] if $$u[$i];
738             }
739 0         0 $val = join ' ', @a;
740             }
741             }
742 0         0 return $val;
743             }
744              
745             #------------------------------------------------------------------------------
746             # Process string of values (or array of strings) to extract as separate tags
747             # Inputs: 0) ExifTool object ref, 1) directory information ref, 2) tag table ref
748             # Returns: 1 on success
749             sub ProcessString($$$)
750             {
751 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
752 0         0 my $dataPt = $$dirInfo{DataPt};
753 0 0       0 my @list = ref $$dataPt eq 'ARRAY' ? @{$$dataPt} : ( $$dataPt );
  0         0  
754 0         0 my ($string, $val);
755 0         0 $et->VerboseDir('GoPro structure');
756 0         0 my $docNum = $$et{DOC_NUM};
757 0         0 my $subDoc = 0;
758 0         0 foreach $string (@list) {
759 0         0 my @val = split ' ', $string;
760 0         0 my $i = 0;
761 0         0 foreach $val (@val) {
762 0         0 $et->HandleTag($tagTablePtr, $i, $val);
763 0 0       0 next if $$tagTablePtr{++$i};
764             # increment subdoc for records stored as string of values (eg. GPS5)
765 0         0 $i = 0;
766 0         0 ++$subDoc;
767 0         0 $$et{DOC_NUM} = "$docNum-$subDoc";
768             }
769 0 0       0 if ($i) {
770             # increment subdoc for records stored as array of strings (eg. GPS9)
771 0         0 ++$subDoc;
772 0         0 $$et{DOC_NUM} = "$docNum-$subDoc";
773             }
774             }
775 0         0 $$et{DOC_NUM} = $docNum;
776 0         0 return 1;
777             }
778              
779             #------------------------------------------------------------------------------
780             # Process "GP\x06\0" records in MP4 'mdat' atom
781             # Inputs: 0) ExifTool object ref, 1) dirInfo ref (RAF and DirLen)
782             # Returns: size of GoPro record, or 0 on error
783             sub ProcessGP6($$)
784             {
785 0     0 0 0 my ($et, $dirInfo) = @_;
786 0         0 my $raf = $$dirInfo{RAF};
787 0         0 my $len = $$dirInfo{DirLen};
788 0         0 my $buff;
789 0         0 while ($len > 16) {
790 0 0       0 $raf->Read($buff, 16) == 16 or last;
791 0         0 my ($tag, $size) = unpack('a4N', $buff);
792 0 0 0     0 last if $size + 16 > $len or $buff !~ /^GP..\0/s;
793 0 0       0 $raf->Read($buff, $size) == $size or last;
794 0 0       0 if ($buff =~ /^DEVC/) {
795 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
796 0         0 my $tagTbl = GetTagTable('Image::ExifTool::GoPro::GPMF');
797 0         0 ProcessGoPro($et, { DataPt => \$buff, DataPos => $raf->Tell()-$size }, $tagTbl);
798             }
799 0         0 $len -= $size + 16;
800             }
801 0         0 delete $$et{DOC_NUM};
802 0         0 return $$dirInfo{DirLen} - $len;
803             }
804              
805             #------------------------------------------------------------------------------
806             # Process GoPro metadata (gpmd samples, GPMF box, or APP6) (ref PH/1/2)
807             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
808             # Returns: 1 on success
809             # - with hack to check for encrypted text in gpmd data (Rove Stealth 4K)
810             sub ProcessGoPro($$$)
811             {
812 3     3 0 6 my ($et, $dirInfo, $tagTablePtr) = @_;
813 3         4 my $dataPt = $$dirInfo{DataPt};
814 3         4 my $base = $$dirInfo{Base};
815 3   50     6 my $pos = $$dirInfo{DirStart} || 0;
816 3   33     8 my $dirEnd = $pos + ($$dirInfo{DirLen} || (length($$dataPt) - $pos));
817 3         8 my $verbose = $et->Options('Verbose');
818 3   33     36 my $unknown = $verbose || $et->Options('Unknown');
819 3         4 my ($size, $type, $unit, $scal, $setGroup0);
820              
821 3 50 0     5 $et->VerboseDir($$dirInfo{DirName} || 'GPMF', undef, $dirEnd-$pos) if $verbose;
822 3         6 $$et{FoundEmbedded} = 1;
823 3 50       5 if ($pos) {
824 3         4 my $parent = $$dirInfo{Parent};
825 3 100 66     9 $setGroup0 = $$et{SET_GROUP0} = 'APP6' if $parent and $parent eq 'APP6';
826             } else {
827             # set group0 to "QuickTime" unless group1 is being changed (to Track#)
828 0 0       0 $setGroup0 = $$et{SET_GROUP0} = 'QuickTime' unless $$et{SET_GROUP1};
829             }
830              
831 3         7 for (; $pos+8<=$dirEnd; $pos+=($size+3)&0xfffffffc) {
832 32         117 my ($tag,$fmt,$len,$count) = unpack("x${pos}a4CCn", $$dataPt);
833 32 50       87 if ($tag =~ /[^-_a-zA-Z0-9 ]/) {
834 0 0       0 $et->Warn('Unrecognized GoPro record') unless $tag eq "\0\0\0\0";
835 0         0 last;
836             }
837 32         69 $size = $len * $count;
838 32         37 $pos += 8;
839 32 50       46 if ($pos + $size > $dirEnd) {
840 0         0 $et->Warn('Truncated GoPro record');
841 0         0 last;
842             }
843 32         69 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
844 32 50       64 last if $tag eq "\0\0\0\0"; # stop at null tag
845 32 50 33     55 next unless $size or $verbose; # don't save empty values unless verbose
846 32   100     83 my $format = $goProFmt{$fmt} || 'undef';
847 32         43 my ($val, $i, $j, $p, @v);
848 32 50 33     148 if ($fmt == 0x3f and defined $type) {
    50 100        
      100        
      66        
849             # decode structure with format given by previous 'TYPE'
850 0         0 for ($i=0; $i<$count; ++$i) {
851 0         0 my (@s, $l);
852 0         0 for ($j=0, $p=0; $j
853 0         0 my $b = Get8u(\$type, $j);
854 0 0       0 my $f = $goProFmt{$b} or last;
855 0 0 0     0 $l = $goProSize{$b} || Image::ExifTool::FormatSize($f) or last;
856 0 0       0 last if $p + $l > $len;
857 0         0 my $s = ReadValue($dataPt, $pos+$i*$len+$p, $f, undef, $l);
858 0 0       0 last unless defined $s;
859 0         0 push @s, $s;
860             }
861 0 0       0 push @v, join ' ', @s if @s;
862             }
863 0 0       0 $val = @v > 1 ? \@v : $v[0];
864             } elsif (($format eq 'undef' or $format eq 'string') and $count > 1 and $len > 1) {
865             # unpack multiple undef/string values as a list
866 0 0       0 my $a = $format eq 'undef' ? 'a' : 'A';
867 0         0 $val = [ unpack("x${pos}".("$a$len" x $count), $$dataPt) ];
868             } else {
869 32         67 $val = ReadValue($dataPt, $pos, $format, undef, $size);
870             }
871             # save TYPE, UNIT/SIUN and SCAL values for later
872 32 50       56 $type = $val if $tag eq 'TYPE';
873 32 50 33     81 $unit = $val if $tag eq 'UNIT' or $tag eq 'SIUN';
874 32 50       50 $scal = $val if $tag eq 'SCAL';
875              
876 32 100       51 unless ($tagInfo) {
877 7 50       12 next unless $unknown;
878 7         21 my $name = Image::ExifTool::QuickTime::PrintableTagID($tag);
879 7         30 $tagInfo = { Name => "GoPro_$name", Description => "GoPro $name", Unknown => 1 };
880 7 50       11 $$tagInfo{SubDirectory} = { TagTable => 'Image::ExifTool::GoPro::GPMF' } if not $fmt;
881 7         18 AddTagToTable($tagTablePtr, $tag, $tagInfo);
882             }
883             # apply scaling if available to last tag in this container
884 32 0 33     52 ScaleValues($val, $scal) if $scal and $tag ne 'SCAL' and $pos+$size+3>=$dirEnd;
      33        
885             my $key = $et->HandleTag($tagTablePtr, $tag, $val,
886             DataPt => $dataPt,
887             DataPos => $$dirInfo{DataPos},
888 32 0       144 Base => $base,
    50          
889             Start => $pos,
890             Size => $size,
891             TagInfo => $tagInfo,
892             Format => $format,
893             Extra => $verbose ? ", type='".($fmt ? chr($fmt) : '\0')."' size=$len count=$count" : undef,
894             );
895             # save units for adding in print conversion if specified
896 32 50 33     122 $$et{TAG_EXTRA}{$key}{Units} = $unit if $$tagInfo{AddUnits} and $key;
897             }
898 3 100       5 delete $$et{SET_GROUP0} if $setGroup0;
899 3         8 return 1;
900             }
901              
902             1; # end
903              
904             __END__