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
« 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 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
19 # **************************************************************************
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"
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 ]
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]
64 # Create default dictionary answer ...
65 ans = {}
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 )
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}°).'
121 # Return answer ...
122 if not ans:
123 return False
124 return ans