Coverage for pyguymer3/image/load_GPS_EXIF2.py: 2%

48 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-08 18:47 +0000

1#!/usr/bin/env python3 

2 

3# Define function ... 

4def load_GPS_EXIF2( 

5 fname, 

6 /, 

7 *, 

8 compressed = False, 

9 exiftoolPath = None, 

10 timeout = 60.0, 

11): 

12 # Import standard modules ... 

13 import datetime 

14 import json 

15 import math 

16 import shutil 

17 import subprocess 

18 

19 # ************************************************************************** 

20 

21 # Try to find the paths if the user did not provide them ... 

22 if exiftoolPath is None: 

23 exiftoolPath = shutil.which("exiftool") 

24 assert exiftoolPath is not None, "\"exiftool\" is not installed" 

25 

26 # Create "exiftool" command ... 

27 cmd = [ 

28 exiftoolPath, 

29 "-api", "largefilesupport=1", 

30 "-json", 

31 ] 

32 if compressed: 

33 cmd += [ 

34 "-zip", 

35 ] 

36 cmd += [ 

37 "-coordFormat", "%+.12f", 

38 "-dateFormat", "%Y-%m-%dT%H:%M:%S.%.6f", # should be the same as datetime.isoformat(sep = "T", timespec = "microseconds") 

39 "-groupNames", 

40 "-struct", 

41 "--printConv", 

42 "-GPSDateTime", 

43 "-GPSAltitude", 

44 "-GPSLongitude", 

45 "-GPSLatitude", 

46 "-GPSHPositioningError", 

47 fname, 

48 ] 

49 

50 # Run "exiftool" and load it as JSON ... 

51 # NOTE: Don't merge standard out and standard error together as the result 

52 # will probably not be valid JSON if standard error is not empty. 

53 dat = json.loads( 

54 subprocess.run( 

55 cmd, 

56 check = True, 

57 encoding = "utf-8", 

58 stderr = subprocess.DEVNULL, 

59 stdout = subprocess.PIPE, 

60 timeout = timeout, 

61 ).stdout 

62 )[0] 

63 

64 # Create default dictionary answer ... 

65 ans = {} 

66 

67 # Populate dictionary ... 

68 if "Composite:GPSLongitude" in dat: 

69 ans["lon"] = float(dat["Composite:GPSLongitude"]) # [°] 

70 if "Composite:GPSLatitude" in dat: 

71 ans["lat"] = float(dat["Composite:GPSLatitude"]) # [°] 

72 if "Composite:GPSAltitude" in dat: 

73 ans["alt"] = float(dat["Composite:GPSAltitude"]) # [m] 

74 if "Composite:GPSHPositioningError" in dat: 

75 ans["loc_err"] = float(dat["Composite:GPSHPositioningError"]) # [m] 

76 if "Composite:GPSDateTime" in dat: 

77 date, time = dat["Composite:GPSDateTime"].removesuffix("Z").split(" ") 

78 tmp1 = date.split(":") 

79 tmp2 = time.split(":") 

80 ye = int(tmp1[0]) # [year] 

81 mo = int(tmp1[1]) # [month] 

82 da = int(tmp1[2]) # [day] 

83 hr = int(tmp2[0]) # [hour] 

84 mi = int(tmp2[1]) # [minute] 

85 se = int(math.floor(float(tmp2[2]))) # [s] 

86 us = int(1.0e6 * (float(tmp2[2]) - se)) # [μs] 

87 if hr > 23: 

88 # HACK: This particular gem is due to my Motorola Moto G3 smartphone. 

89 hr = hr % 24 # [hour] 

90 ans["datetime"] = datetime.datetime( 

91 year = ye, 

92 month = mo, 

93 day = da, 

94 hour = hr, 

95 minute = mi, 

96 second = se, 

97 microsecond = us, 

98 tzinfo = datetime.UTC, 

99 ) 

100 

101 # Check that there is location information ... 

102 if "lon" in ans and "lat" in ans: 

103 # Check that there is date/time information ... 

104 if "datetime" in ans: 

105 # Check that there is altitude information ... 

106 if "alt" in ans: 

107 # Make a pretty string ... 

108 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°, {ans["alt"]:.1f}m ASL) at \"{ans["datetime"].isoformat(sep = " ", timespec = "microseconds")}\".' 

109 else: 

110 # Make a pretty string ... 

111 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°) at \"{ans["datetime"].isoformat(sep = " ", timespec = "microseconds")}\".' 

112 else: 

113 # Check that there is altitude information ... 

114 if "alt" in ans: 

115 # Make a pretty string ... 

116 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°, {ans["alt"]:.1f}m ASL).' 

117 else: 

118 # Make a pretty string ... 

119 ans["pretty"] = f'GPS fix returned ({ans["lon"]:.6f}°, {ans["lat"]:.6f}°).' 

120 

121 # Return answer ... 

122 if not ans: 

123 return False 

124 return ans