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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
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)
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
21 # Import standard modules ...
22 import datetime
24 # Check input ...
25 if "EXIF" not in info:
26 if debug:
27 print("DEBUG: There isn't a \"EXIF\" key.")
28 return False
30 # Set flag ...
31 actuallyUseGPS = False
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.")
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
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)
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.")
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.")
154 # Return answer ...
155 return ans