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

65 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 EXIF_datetime( 

5 info, 

6 /, 

7 *, 

8 debug = __debug__, 

9 maxDOP = 10.0, 

10 useDOP = False, 

11 useGPS = True, 

12): 

13 # NOTE: The Wikipedia article on DOP has a handy table on what are good or 

14 # bad DOP values: 

15 # * https://en.wikipedia.org/wiki/Dilution_of_precision_(navigation) 

16 

17 # NOTE: It is assumed that the dictionary of EXIF data is parsed from 

18 # "exiftool". There is a handy table on what all the keys are: 

19 # * https://exiftool.org/TagNames/EXIF.html 

20 

21 # Import standard modules ... 

22 import datetime 

23 

24 # Check input ... 

25 if "EXIF" not in info: 

26 if debug: 

27 print("DEBUG: There isn't a \"EXIF\" key.") 

28 return False 

29 

30 # Set flag ... 

31 actuallyUseGPS = False 

32 

33 # Check if the user wants to use GPS data ... 

34 if useGPS: 

35 # Check if the user wants to use GPS DOP data ... 

36 if useDOP: 

37 # Check that all the required keys are present ... 

38 if all( 

39 [ 

40 "GPSDOP" in info["EXIF"], 

41 "GPSDateStamp" in info["EXIF"], 

42 "GPSTimeStamp" in info["EXIF"], 

43 ] 

44 ): 

45 # Set flag ... 

46 actuallyUseGPS = bool(float(info["EXIF"]["GPSDOP"]) <= maxDOP) 

47 if not actuallyUseGPS and debug: 

48 print("DEBUG: There is GPS data but the DOP is not good enough.") 

49 elif debug: 

50 print("DEBUG: There aren't all of the \"EXIF::GPSDateStamp\" key, the \"EXIF::GPSTimeStamp\" key and the \"EXIF::GPSDOP\" key.") 

51 else: 

52 # Check that all the required keys are present ... 

53 if all( 

54 [ 

55 "GPSDateStamp" in info["EXIF"], 

56 "GPSTimeStamp" in info["EXIF"], 

57 ] 

58 ): 

59 # Set flag ... 

60 actuallyUseGPS = True 

61 elif debug: 

62 print("DEBUG: There aren't all of the \"EXIF::GPSDateStamp\" key and the \"EXIF::GPSTimeStamp\" key.") 

63 

64 # Check if GPS data is to be used ... 

65 if actuallyUseGPS: 

66 # Determine date/time that the photo was taken (assuming that the GPS 

67 # data is in UTC) ... 

68 # NOTE: My old Motorola Moto G3 took a photo at "2016:10:24 24:00:11Z" 

69 # once, who knows what time that actually was. Either way, 

70 # "strptime()" cannot parse it so I must work around it here. 

71 us = 0 # [μs] 

72 if "." in info["EXIF"]["GPSTimeStamp"]: 

73 us = info["EXIF"]["GPSTimeStamp"].split(".")[1] 

74 us = int(f"{us:<6s}".replace(" ", "0")) # [μs] 

75 elif debug: 

76 print("DEBUG: There isn't any subsecond informaton in the \"EXIF::GPSTimeStamp\" key.") 

77 ans = datetime.datetime.strptime( 

78 f'{info["EXIF"]["GPSDateStamp"]} {info["EXIF"]["GPSTimeStamp"].split(".")[0]}.{us:06d}'.replace(" 24:", " 00:"), 

79 "%Y:%m:%d %H:%M:%S.%f", 

80 ).replace(tzinfo = datetime.UTC) 

81 else: 

82 # Check input ... 

83 if "DateTimeOriginal" not in info["EXIF"]: 

84 if debug: 

85 print("DEBUG: There isn't a \"EXIF::DateTimeOriginal\" key.") 

86 return False 

87 

88 # Determine date/time that the photo was taken (assuming that the EXIF 

89 # data is in UTC) ... 

90 # NOTE: My old Motorola Moto G3 took a photo at "2016:10:24 24:00:11Z" 

91 # once, who knows what time that actually was. Either way, 

92 # "strptime()" cannot parse it so I must work around it here. 

93 ans = datetime.datetime.strptime( 

94 info["EXIF"]["DateTimeOriginal"].replace(" 24:", " 00:"), 

95 "%Y:%m:%d %H:%M:%S", 

96 ).replace(tzinfo = datetime.UTC) 

97 

98 # Assume that the offset is valid (if it is present) which means that 

99 # the date/time was in fact in the local time zone rather than in UTC ... 

100 if "OffsetTimeOriginal" in info["EXIF"]: 

101 if info["EXIF"]["OffsetTimeOriginal"] != "Z": 

102 hh, mm = info["EXIF"]["OffsetTimeOriginal"].split(":") 

103 ans -= datetime.timedelta( 

104 hours = int(hh), 

105 minutes = int(mm), 

106 ) 

107 elif "TimeZoneOffset" in info["EXIF"]: 

108 ans -= datetime.timedelta( 

109 hours = int(info["EXIF"]["TimeZoneOffset"].split(" ")[0]), 

110 ) 

111 elif debug: 

112 print("DEBUG: There isn't a \"EXIF::OffsetTimeOriginal\" key or a \"EXIF::TimeZoneOffset\" key.") 

113 

114 # Apply the sub-second offset (if it is present) ... 

115 if "SubSecTimeOriginal" in info["EXIF"]: 

116 match info["EXIF"]["SubSecTimeOriginal"]: 

117 case int(): 

118 ans += datetime.timedelta( 

119 milliseconds = info["EXIF"]["SubSecTimeOriginal"], 

120 ) 

121 case str(): 

122 match len(info["EXIF"]["SubSecTimeOriginal"]): 

123 case 1: 

124 ans += datetime.timedelta( 

125 microseconds = 100000 * int(info["EXIF"]["SubSecTimeOriginal"]), 

126 ) 

127 case 2: 

128 ans += datetime.timedelta( 

129 microseconds = 10000 * int(info["EXIF"]["SubSecTimeOriginal"]), 

130 ) 

131 case 3: 

132 ans += datetime.timedelta( 

133 microseconds = 1000 * int(info["EXIF"]["SubSecTimeOriginal"]), 

134 ) 

135 case 4: 

136 ans += datetime.timedelta( 

137 microseconds = 100 * int(info["EXIF"]["SubSecTimeOriginal"]), 

138 ) 

139 case 5: 

140 ans += datetime.timedelta( 

141 microseconds = 10 * int(info["EXIF"]["SubSecTimeOriginal"]), 

142 ) 

143 case 6: 

144 ans += datetime.timedelta( 

145 microseconds = int(info["EXIF"]["SubSecTimeOriginal"]), 

146 ) 

147 case _: 

148 raise Exception(f'\"{info["EXIF"]["SubSecTimeOriginal"]}\" is an unexpected length') from None 

149 case _: 

150 raise Exception(f'\"{repr(type(info["EXIF"]["SubSecTimeOriginal"]))}\" is an unexpected type') from None 

151 elif debug: 

152 print("DEBUG: There isn't a \"EXIF::SubSecTimeOriginal\" key.") 

153 

154 # Return answer ... 

155 return ans