geopic-tag-reader 1.0.6__py3-none-any.whl → 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,4 +2,4 @@
2
2
  GeoPicTagReader
3
3
  """
4
4
 
5
- __version__ = "1.0.6"
5
+ __version__ = "1.1.1"
geopic_tag_reader/main.py CHANGED
@@ -22,13 +22,15 @@ def read(
22
22
 
23
23
  print("Latitude:", metadata.lat)
24
24
  print("Longitude:", metadata.lon)
25
- print("Timestamp:", metadata.ts)
25
+ print("Timestamp:", metadata.ts.isoformat())
26
26
  print("Heading:", metadata.heading)
27
27
  print("Type:", metadata.type)
28
28
  print("Make:", metadata.make)
29
29
  print("Model:", metadata.model)
30
30
  print("Focal length:", metadata.focal_length)
31
31
  print("Crop parameters:", metadata.crop)
32
+ print("Pitch:", metadata.pitch)
33
+ print("Roll:", metadata.roll)
32
34
 
33
35
  if len(metadata.tagreader_warnings) > 0:
34
36
  print("Warnings raised by reader:")
@@ -7,11 +7,15 @@ import re
7
7
  import json
8
8
  from fractions import Fraction
9
9
  from geopic_tag_reader import camera
10
+ import timezonefinder # type: ignore
11
+ import pytz
10
12
 
11
13
  # This is a fix for invalid MakerNotes leading to picture not read at all
12
14
  # https://github.com/LeoHsiao1/pyexiv2/issues/58
13
15
  pyexiv2.set_log_level(4)
14
16
 
17
+ tz_finder = timezonefinder.TimezoneFinder()
18
+
15
19
 
16
20
  @dataclass
17
21
  class CropValues:
@@ -41,8 +45,8 @@ class GeoPicTags:
41
45
  Attributes:
42
46
  lat (float): GPS Latitude (in WGS84)
43
47
  lon (float): GPS Longitude (in WGS84)
44
- ts (float): The capture date (as POSIX timestamp)
45
- heading (int): Picture heading (in degrees, North = 0°, East = 90°, South = 180°, West = 270°)
48
+ ts (datetime): The capture date (date & time with timezone)
49
+ heading (int): Picture heading/yaw (in degrees, North = 0°, East = 90°, South = 180°, West = 270°)
46
50
  type (str): The kind of picture (flat, equirectangular)
47
51
  make (str): The camera manufacturer name
48
52
  model (str): The camera model name
@@ -51,6 +55,8 @@ class GeoPicTags:
51
55
  exif (dict[str, str]): Raw EXIF tags from picture (following Exiv2 naming scheme, see https://exiv2.org/metadata.html)
52
56
  tagreader_warnings (list[str]): List of thrown warnings during metadata reading
53
57
  altitude (float): altitude (in m) (optional)
58
+ pitch (float): Picture pitch angle, compared to horizon (in degrees, bottom = -90°, horizon = 0°, top = 90°)
59
+ roll (float): Picture roll angle, on a right/left axis (in degrees, left-arm down = -90°, flat = 0°, right-arm down = 90°)
54
60
 
55
61
 
56
62
  Implementation note: this needs to be sync with the PartialGeoPicTags structure
@@ -58,7 +64,7 @@ class GeoPicTags:
58
64
 
59
65
  lat: float
60
66
  lon: float
61
- ts: float
67
+ ts: datetime.datetime
62
68
  heading: Optional[int]
63
69
  type: str
64
70
  make: Optional[str]
@@ -68,6 +74,8 @@ class GeoPicTags:
68
74
  exif: Dict[str, str] = field(default_factory=lambda: {})
69
75
  tagreader_warnings: List[str] = field(default_factory=lambda: [])
70
76
  altitude: Optional[float] = None
77
+ pitch: Optional[float] = None
78
+ roll: Optional[float] = None
71
79
 
72
80
 
73
81
  class InvalidExifException(Exception):
@@ -86,7 +94,7 @@ class PartialGeoPicTags:
86
94
 
87
95
  lat: Optional[float] = None
88
96
  lon: Optional[float] = None
89
- ts: Optional[float] = None
97
+ ts: Optional[datetime.datetime] = None
90
98
  heading: Optional[int] = None
91
99
  type: Optional[str] = None
92
100
  make: Optional[str] = None
@@ -96,6 +104,8 @@ class PartialGeoPicTags:
96
104
  exif: Dict[str, str] = field(default_factory=lambda: {})
97
105
  tagreader_warnings: List[str] = field(default_factory=lambda: [])
98
106
  altitude: Optional[float] = None
107
+ pitch: Optional[float] = None
108
+ roll: Optional[float] = None
99
109
 
100
110
 
101
111
  class PartialExifException(Exception):
@@ -142,6 +152,11 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
142
152
  except:
143
153
  pass
144
154
 
155
+ # Sanitize charset information
156
+ for k, v in data.items():
157
+ if isinstance(v, str):
158
+ data[k] = re.sub(r"charset=[^\s]+", "", v).strip()
159
+
145
160
  # Parse latitude/longitude
146
161
  lat, lon, llw = decodeLatLon(data, "Exif.GPSInfo")
147
162
  if len(llw) > 0:
@@ -163,13 +178,13 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
163
178
  raise InvalidExifException("Read longitude is out of WGS84 bounds (should be in [-180, 180])")
164
179
 
165
180
  # Parse date/time
166
- d, llw = decodeGPSDateTime(data, "Exif.GPSInfo")
181
+ d, llw = decodeGPSDateTime(data, "Exif.GPSInfo", lat, lon)
167
182
 
168
183
  if len(llw) > 0:
169
184
  warnings.extend(llw)
170
185
 
171
186
  if d is None:
172
- d, llw = decodeGPSDateTime(data, "Xmp.exif")
187
+ d, llw = decodeGPSDateTime(data, "Xmp.exif", lat, lon)
173
188
  if len(llw) > 0:
174
189
  warnings.extend(llw)
175
190
 
@@ -180,7 +195,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
180
195
  "Xmp.GPano.SourceImageCreateTime",
181
196
  ]:
182
197
  if d is None:
183
- d, llw = decodeDateTimeOriginal(data, exifField)
198
+ d, llw = decodeDateTimeOriginal(data, exifField, lat, lon)
184
199
  if len(llw) > 0:
185
200
  warnings.extend(llw)
186
201
 
@@ -204,7 +219,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
204
219
  except Exception as e:
205
220
  warnings.append("Skipping Mapillary date/time as it was not recognized:\n\t" + str(e))
206
221
 
207
- # Heading
222
+ # Heading/Yaw
208
223
  heading = None
209
224
  if isExifTagUsable(data, "Xmp.GPano.PoseHeadingDegrees", float) and isExifTagUsable(data, "Exif.GPSInfo.GPSImgDirection", Fraction):
210
225
  gpsDir = int(round(float(Fraction(data["Exif.GPSInfo.GPSImgDirection"]))))
@@ -227,6 +242,35 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
227
242
  elif "MAPCompassHeading" in data and isExifTagUsable(data["MAPCompassHeading"], "TrueHeading", float):
228
243
  heading = int(round(float(data["MAPCompassHeading"]["TrueHeading"])))
229
244
 
245
+ # Pitch & roll
246
+ pitch = None
247
+ roll = None
248
+ exifPRFields = ["Xmp.Camera.$$", "Exif.GPSInfo.GPS$$", "Xmp.GPano.Pose$$Degrees", "Xmp.GPano.InitialView$$Degrees"]
249
+ # For each potential EXIF field
250
+ for exifField in exifPRFields:
251
+ # Try out both Pitch & Roll variants
252
+ for checkField in ["Pitch", "Roll"]:
253
+ exifCheckField = exifField.replace("$$", checkField)
254
+ foundValue = None
255
+ # Look for float or fraction
256
+ if isExifTagUsable(data, exifCheckField, float):
257
+ foundValue = float(data[exifCheckField])
258
+ elif isExifTagUsable(data, exifCheckField, Fraction):
259
+ foundValue = float(Fraction(data[exifCheckField]))
260
+
261
+ # Save to correct variable (if not already set)
262
+ if foundValue is not None:
263
+ if checkField == "Pitch":
264
+ if pitch is None:
265
+ pitch = foundValue
266
+ else:
267
+ continue
268
+ elif checkField == "Roll":
269
+ if roll is None:
270
+ roll = foundValue
271
+ else:
272
+ continue
273
+
230
274
  # Make and model
231
275
  make = data.get("Exif.Image.Make") or data.get("MAPDeviceMake")
232
276
  model = data.get("Exif.Image.Model") or data.get("MAPDeviceModel")
@@ -313,7 +357,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
313
357
  PartialGeoPicTags(
314
358
  lat,
315
359
  lon,
316
- d.timestamp() if d else None,
360
+ d,
317
361
  heading,
318
362
  pic_type,
319
363
  make,
@@ -323,6 +367,8 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
323
367
  exif=data,
324
368
  tagreader_warnings=warnings,
325
369
  altitude=altitude,
370
+ pitch=pitch,
371
+ roll=roll,
326
372
  ),
327
373
  )
328
374
 
@@ -330,7 +376,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
330
376
  return GeoPicTags(
331
377
  lat,
332
378
  lon,
333
- d.timestamp(),
379
+ d,
334
380
  heading,
335
381
  pic_type,
336
382
  make,
@@ -340,6 +386,8 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
340
386
  exif=data,
341
387
  tagreader_warnings=warnings,
342
388
  altitude=altitude,
389
+ pitch=pitch,
390
+ roll=roll,
343
391
  )
344
392
 
345
393
 
@@ -429,7 +477,9 @@ def decodeLatLon(data: dict, group: str) -> Tuple[Optional[float], Optional[floa
429
477
  return (lat, lon, warnings)
430
478
 
431
479
 
432
- def decodeDateTimeOriginal(data: dict, datetimeField: str) -> Tuple[Optional[datetime.datetime], List[str]]:
480
+ def decodeDateTimeOriginal(
481
+ data: dict, datetimeField: str, lat: Optional[float] = None, lon: Optional[float] = None
482
+ ) -> Tuple[Optional[datetime.datetime], List[str]]:
433
483
  d = None
434
484
  warnings = []
435
485
 
@@ -442,7 +492,6 @@ def decodeDateTimeOriginal(data: dict, datetimeField: str) -> Tuple[Optional[dat
442
492
  secondsRaw, microsecondsRaw, msw = decodeSecondsAndMicroSeconds(
443
493
  timeRaw[2], data["Exif.Photo.SubSecTimeOriginal"] if isExifTagUsable(data, "Exif.Photo.SubSecTimeOriginal", float) else "0"
444
494
  )
445
- tz = decodeTimeOffset(data, f"Exif.Photo.OffsetTime{'Original' if 'DateTimeOriginal' in datetimeField else ''}")
446
495
  warnings += msw
447
496
 
448
497
  d = datetime.datetime.combine(
@@ -452,9 +501,30 @@ def decodeDateTimeOriginal(data: dict, datetimeField: str) -> Tuple[Optional[dat
452
501
  minutesRaw,
453
502
  secondsRaw,
454
503
  microsecondsRaw,
455
- tzinfo=tz or datetime.timezone.utc,
456
504
  ),
457
505
  )
506
+
507
+ # Timezone handling
508
+ # Try to read from EXIF
509
+ tz = decodeTimeOffset(data, f"Exif.Photo.OffsetTime{'Original' if 'DateTimeOriginal' in datetimeField else ''}")
510
+ if tz is not None:
511
+ d = d.replace(tzinfo=tz)
512
+
513
+ # Otherwise, try to deduct from coordinates
514
+ elif lon is not None and lat is not None:
515
+ tz_name = tz_finder.timezone_at(lng=lon, lat=lat)
516
+ if tz_name is not None:
517
+ d = pytz.timezone(tz_name).localize(d)
518
+ # Otherwise, default to UTC + warning
519
+ else:
520
+ d = d.replace(tzinfo=datetime.timezone.utc)
521
+ warnings.append("Precise timezone information not found, fallback to UTC")
522
+
523
+ # Otherwise, default to UTC + warning
524
+ else:
525
+ d = d.replace(tzinfo=datetime.timezone.utc)
526
+ warnings.append("Precise timezone information not found (and no GPS coordinates to help), fallback to UTC")
527
+
458
528
  except ValueError as e:
459
529
  warnings.append("Skipping original date/time (from " + datetimeField + ") as it was not recognized:\n\t" + str(e))
460
530
 
@@ -467,7 +537,9 @@ def decodeTimeOffset(data: dict, offsetTimeField: str) -> Optional[datetime.tzin
467
537
  return None
468
538
 
469
539
 
470
- def decodeGPSDateTime(data: dict, group: str) -> Tuple[Optional[datetime.datetime], List[str]]:
540
+ def decodeGPSDateTime(
541
+ data: dict, group: str, lat: Optional[float] = None, lon: Optional[float] = None
542
+ ) -> Tuple[Optional[datetime.datetime], List[str]]:
471
543
  d = None
472
544
  warnings = []
473
545
 
@@ -504,6 +576,12 @@ def decodeGPSDateTime(data: dict, group: str) -> Tuple[Optional[datetime.datetim
504
576
  ),
505
577
  )
506
578
 
579
+ # Set timezone from coordinates
580
+ if lon is not None and lat is not None:
581
+ tz_name = tz_finder.timezone_at(lng=lon, lat=lat)
582
+ if tz_name is not None:
583
+ d = d.astimezone(pytz.timezone(tz_name))
584
+
507
585
  except ValueError as e:
508
586
  warnings.append(f"Skipping GPS date/time ({group} group) as it was not recognized:\n\t{str(e)}")
509
587
 
@@ -3,11 +3,11 @@ from datetime import datetime, timedelta
3
3
  from dataclasses import dataclass
4
4
  from geopic_tag_reader.model import PictureType
5
5
  from enum import Enum
6
+ import timezonefinder # type: ignore
7
+ import pytz
6
8
 
7
9
  try:
8
10
  import pyexiv2 # type: ignore
9
- import timezonefinder # type: ignore
10
- import pytz
11
11
  except ImportError:
12
12
  raise Exception(
13
13
  """Impossible to write the exif tags without the '[write-exif]' dependency (that will need to install libexiv2).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geopic-tag-reader
3
- Version: 1.0.6
3
+ Version: 1.1.1
4
4
  Summary: GeoPicTagReader
5
5
  Author-email: Adrien PAVIE <panieravide@riseup.net>
6
6
  Requires-Python: >=3.8
@@ -9,6 +9,9 @@ Classifier: License :: OSI Approved :: MIT License
9
9
  Requires-Dist: typer ~= 0.12
10
10
  Requires-Dist: xmltodict ~= 0.13
11
11
  Requires-Dist: pyexiv2 == 2.8.3
12
+ Requires-Dist: timezonefinder == 6.2.0
13
+ Requires-Dist: pytz ~= 2023.3
14
+ Requires-Dist: types-pytz ~= 2023.3.0.1
12
15
  Requires-Dist: flit ~= 3.8.0 ; extra == "build"
13
16
  Requires-Dist: black ~= 24.3 ; extra == "dev"
14
17
  Requires-Dist: mypy ~= 1.9 ; extra == "dev"
@@ -17,9 +20,6 @@ Requires-Dist: pytest-datafiles ~= 3.0 ; extra == "dev"
17
20
  Requires-Dist: lazydocs ~= 0.4.8 ; extra == "dev"
18
21
  Requires-Dist: types-xmltodict ~= 0.13 ; extra == "dev"
19
22
  Requires-Dist: pre-commit ~= 3.3.3 ; extra == "dev"
20
- Requires-Dist: timezonefinder == 6.2.0 ; extra == "write-exif"
21
- Requires-Dist: pytz ~= 2023.3 ; extra == "write-exif"
22
- Requires-Dist: types-pytz ~= 2023.3.0.1 ; extra == "write-exif"
23
23
  Requires-Dist: python-dateutil ~= 2.8.2 ; extra == "write-exif"
24
24
  Project-URL: Home, https://gitlab.com/geovisio/geo-picture-tag-reader
25
25
  Provides-Extra: build
@@ -0,0 +1,12 @@
1
+ geopic_tag_reader/__init__.py,sha256=7hT1At7Ce4FtNeMuBZa7cDah_cJoIhJrnQWbEAF0BE8,47
2
+ geopic_tag_reader/camera.py,sha256=2Sr0jAt3RXUWazYMnkwF6J6lVnKvSp7Ac8g7yOHehVA,1643
3
+ geopic_tag_reader/main.py,sha256=ZEZaZEeaDxRjrVMwhR5lUYJWKkUcjd8avjqm7JxJdhM,3219
4
+ geopic_tag_reader/model.py,sha256=rsWVE3T1kpNsKXX8iv6xb_3PCVY6Ea7iU9WOqUgXklU,129
5
+ geopic_tag_reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ geopic_tag_reader/reader.py,sha256=4yoQU-ljOgLmaH_hc1m4o1YkSNgaPfM38NVmiM7rpbE,24543
7
+ geopic_tag_reader/writer.py,sha256=QmQqQpWgb6AL3Y0Kuzy7PF1asUsTVDq9buwJXfFSRq8,8672
8
+ geopic_tag_reader-1.1.1.dist-info/entry_points.txt,sha256=c9YwjCNhxveDf-61_aSRlzcpoutvM6KQCerlzaVt_JU,64
9
+ geopic_tag_reader-1.1.1.dist-info/LICENSE,sha256=oHWDwXkJJb9zJzThMN3F9Li4yFhz1qxOUByouY7L3bI,1070
10
+ geopic_tag_reader-1.1.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
11
+ geopic_tag_reader-1.1.1.dist-info/METADATA,sha256=iMhClAI5r1h2jpB_o-dwcSP3QVz4PuBPf4jWmZ_10Ks,6303
12
+ geopic_tag_reader-1.1.1.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- geopic_tag_reader/__init__.py,sha256=AB9t1irfcb0A7j8MHsCnqxKwAll8VkwJKfgEAPE4Jw4,47
2
- geopic_tag_reader/camera.py,sha256=2Sr0jAt3RXUWazYMnkwF6J6lVnKvSp7Ac8g7yOHehVA,1643
3
- geopic_tag_reader/main.py,sha256=LohJW0xX0A8DAbcUR_BnJ2UvN4qo6yydzOGtq3zIArA,3129
4
- geopic_tag_reader/model.py,sha256=rsWVE3T1kpNsKXX8iv6xb_3PCVY6Ea7iU9WOqUgXklU,129
5
- geopic_tag_reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- geopic_tag_reader/reader.py,sha256=PK1mN7TFFC8V70-pU_EM6cTESNcAed1GydHWUkqkT3A,21325
7
- geopic_tag_reader/writer.py,sha256=L13ewadZhZic13k66FsQvqGQ_s512nssKvvSwZp4Ggg,8680
8
- geopic_tag_reader-1.0.6.dist-info/entry_points.txt,sha256=c9YwjCNhxveDf-61_aSRlzcpoutvM6KQCerlzaVt_JU,64
9
- geopic_tag_reader-1.0.6.dist-info/LICENSE,sha256=oHWDwXkJJb9zJzThMN3F9Li4yFhz1qxOUByouY7L3bI,1070
10
- geopic_tag_reader-1.0.6.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
11
- geopic_tag_reader-1.0.6.dist-info/METADATA,sha256=US7A6UqzLzwlLvtCiHWPQszwLTNtNt_Pzka6DLJBbmg,6375
12
- geopic_tag_reader-1.0.6.dist-info/RECORD,,