geopic-tag-reader 1.0.5__py3-none-any.whl → 1.1.0__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.
@@ -1,4 +1,5 @@
1
1
  """
2
2
  GeoPicTagReader
3
3
  """
4
- __version__ = "1.0.5"
4
+
5
+ __version__ = "1.1.0"
geopic_tag_reader/main.py CHANGED
@@ -22,7 +22,7 @@ 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)
@@ -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,7 +45,7 @@ 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)
48
+ ts (datetime): The capture date (date & time with timezone)
45
49
  heading (int): Picture heading (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
@@ -58,7 +62,7 @@ class GeoPicTags:
58
62
 
59
63
  lat: float
60
64
  lon: float
61
- ts: float
65
+ ts: datetime.datetime
62
66
  heading: Optional[int]
63
67
  type: str
64
68
  make: Optional[str]
@@ -86,7 +90,7 @@ class PartialGeoPicTags:
86
90
 
87
91
  lat: Optional[float] = None
88
92
  lon: Optional[float] = None
89
- ts: Optional[float] = None
93
+ ts: Optional[datetime.datetime] = None
90
94
  heading: Optional[int] = None
91
95
  type: Optional[str] = None
92
96
  make: Optional[str] = None
@@ -142,6 +146,11 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
142
146
  except:
143
147
  pass
144
148
 
149
+ # Sanitize charset information
150
+ for k, v in data.items():
151
+ if isinstance(v, str):
152
+ data[k] = re.sub(r"charset=[^\s]+", "", v).strip()
153
+
145
154
  # Parse latitude/longitude
146
155
  lat, lon, llw = decodeLatLon(data, "Exif.GPSInfo")
147
156
  if len(llw) > 0:
@@ -163,13 +172,13 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
163
172
  raise InvalidExifException("Read longitude is out of WGS84 bounds (should be in [-180, 180])")
164
173
 
165
174
  # Parse date/time
166
- d, llw = decodeGPSDateTime(data, "Exif.GPSInfo")
175
+ d, llw = decodeGPSDateTime(data, "Exif.GPSInfo", lat, lon)
167
176
 
168
177
  if len(llw) > 0:
169
178
  warnings.extend(llw)
170
179
 
171
180
  if d is None:
172
- d, llw = decodeGPSDateTime(data, "Xmp.exif")
181
+ d, llw = decodeGPSDateTime(data, "Xmp.exif", lat, lon)
173
182
  if len(llw) > 0:
174
183
  warnings.extend(llw)
175
184
 
@@ -180,7 +189,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
180
189
  "Xmp.GPano.SourceImageCreateTime",
181
190
  ]:
182
191
  if d is None:
183
- d, llw = decodeDateTimeOriginal(data, exifField)
192
+ d, llw = decodeDateTimeOriginal(data, exifField, lat, lon)
184
193
  if len(llw) > 0:
185
194
  warnings.extend(llw)
186
195
 
@@ -313,7 +322,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
313
322
  PartialGeoPicTags(
314
323
  lat,
315
324
  lon,
316
- d.timestamp() if d else None,
325
+ d,
317
326
  heading,
318
327
  pic_type,
319
328
  make,
@@ -330,7 +339,7 @@ def readPictureMetadata(picture: bytes) -> GeoPicTags:
330
339
  return GeoPicTags(
331
340
  lat,
332
341
  lon,
333
- d.timestamp(),
342
+ d,
334
343
  heading,
335
344
  pic_type,
336
345
  make,
@@ -429,7 +438,9 @@ def decodeLatLon(data: dict, group: str) -> Tuple[Optional[float], Optional[floa
429
438
  return (lat, lon, warnings)
430
439
 
431
440
 
432
- def decodeDateTimeOriginal(data: dict, datetimeField: str) -> Tuple[Optional[datetime.datetime], List[str]]:
441
+ def decodeDateTimeOriginal(
442
+ data: dict, datetimeField: str, lat: Optional[float] = None, lon: Optional[float] = None
443
+ ) -> Tuple[Optional[datetime.datetime], List[str]]:
433
444
  d = None
434
445
  warnings = []
435
446
 
@@ -442,7 +453,6 @@ def decodeDateTimeOriginal(data: dict, datetimeField: str) -> Tuple[Optional[dat
442
453
  secondsRaw, microsecondsRaw, msw = decodeSecondsAndMicroSeconds(
443
454
  timeRaw[2], data["Exif.Photo.SubSecTimeOriginal"] if isExifTagUsable(data, "Exif.Photo.SubSecTimeOriginal", float) else "0"
444
455
  )
445
- tz = decodeTimeOffset(data, f"Exif.Photo.OffsetTime{'Original' if 'DateTimeOriginal' in datetimeField else ''}")
446
456
  warnings += msw
447
457
 
448
458
  d = datetime.datetime.combine(
@@ -452,9 +462,30 @@ def decodeDateTimeOriginal(data: dict, datetimeField: str) -> Tuple[Optional[dat
452
462
  minutesRaw,
453
463
  secondsRaw,
454
464
  microsecondsRaw,
455
- tzinfo=tz or datetime.timezone.utc,
456
465
  ),
457
466
  )
467
+
468
+ # Timezone handling
469
+ # Try to read from EXIF
470
+ tz = decodeTimeOffset(data, f"Exif.Photo.OffsetTime{'Original' if 'DateTimeOriginal' in datetimeField else ''}")
471
+ if tz is not None:
472
+ d = d.replace(tzinfo=tz)
473
+
474
+ # Otherwise, try to deduct from coordinates
475
+ elif lon is not None and lat is not None:
476
+ tz_name = tz_finder.timezone_at(lng=lon, lat=lat)
477
+ if tz_name is not None:
478
+ d = pytz.timezone(tz_name).localize(d)
479
+ # Otherwise, default to UTC + warning
480
+ else:
481
+ d = d.replace(tzinfo=datetime.timezone.utc)
482
+ warnings.append("Precise timezone information not found, fallback to UTC")
483
+
484
+ # Otherwise, default to UTC + warning
485
+ else:
486
+ d = d.replace(tzinfo=datetime.timezone.utc)
487
+ warnings.append("Precise timezone information not found (and no GPS coordinates to help), fallback to UTC")
488
+
458
489
  except ValueError as e:
459
490
  warnings.append("Skipping original date/time (from " + datetimeField + ") as it was not recognized:\n\t" + str(e))
460
491
 
@@ -467,7 +498,9 @@ def decodeTimeOffset(data: dict, offsetTimeField: str) -> Optional[datetime.tzin
467
498
  return None
468
499
 
469
500
 
470
- def decodeGPSDateTime(data: dict, group: str) -> Tuple[Optional[datetime.datetime], List[str]]:
501
+ def decodeGPSDateTime(
502
+ data: dict, group: str, lat: Optional[float] = None, lon: Optional[float] = None
503
+ ) -> Tuple[Optional[datetime.datetime], List[str]]:
471
504
  d = None
472
505
  warnings = []
473
506
 
@@ -504,6 +537,12 @@ def decodeGPSDateTime(data: dict, group: str) -> Tuple[Optional[datetime.datetim
504
537
  ),
505
538
  )
506
539
 
540
+ # Set timezone from coordinates
541
+ if lon is not None and lat is not None:
542
+ tz_name = tz_finder.timezone_at(lng=lon, lat=lat)
543
+ if tz_name is not None:
544
+ d = d.astimezone(pytz.timezone(tz_name))
545
+
507
546
  except ValueError as e:
508
547
  warnings.append(f"Skipping GPS date/time ({group} group) as it was not recognized:\n\t{str(e)}")
509
548
 
@@ -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,26 +1,25 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geopic-tag-reader
3
- Version: 1.0.5
3
+ Version: 1.1.0
4
4
  Summary: GeoPicTagReader
5
5
  Author-email: Adrien PAVIE <panieravide@riseup.net>
6
6
  Requires-Python: >=3.8
7
7
  Description-Content-Type: text/markdown
8
8
  Classifier: License :: OSI Approved :: MIT License
9
- Requires-Dist: typer ~= 0.9.0
10
- Requires-Dist: xmltodict ~= 0.13.0
9
+ Requires-Dist: typer ~= 0.12
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
- Requires-Dist: black ~= 22.8.0 ; extra == "dev"
14
- Requires-Dist: mypy ~= 1.0.0 ; extra == "dev"
16
+ Requires-Dist: black ~= 24.3 ; extra == "dev"
17
+ Requires-Dist: mypy ~= 1.9 ; extra == "dev"
15
18
  Requires-Dist: pytest ~= 7.2.0 ; extra == "dev"
16
- Requires-Dist: pytest-datafiles ~= 2.0.1 ; extra == "dev"
17
- Requires-Dist: typer-cli-forked ~= 0.0.14 ; extra == "dev"
19
+ Requires-Dist: pytest-datafiles ~= 3.0 ; extra == "dev"
18
20
  Requires-Dist: lazydocs ~= 0.4.8 ; extra == "dev"
19
- Requires-Dist: types-xmltodict ~= 0.13.0 ; extra == "dev"
21
+ Requires-Dist: types-xmltodict ~= 0.13 ; extra == "dev"
20
22
  Requires-Dist: pre-commit ~= 3.3.3 ; extra == "dev"
21
- Requires-Dist: timezonefinder == 6.2.0 ; extra == "write-exif"
22
- Requires-Dist: pytz ~= 2023.3 ; extra == "write-exif"
23
- Requires-Dist: types-pytz ~= 2023.3.0.1 ; extra == "write-exif"
24
23
  Requires-Dist: python-dateutil ~= 2.8.2 ; extra == "write-exif"
25
24
  Project-URL: Home, https://gitlab.com/geovisio/geo-picture-tag-reader
26
25
  Provides-Extra: build
@@ -0,0 +1,12 @@
1
+ geopic_tag_reader/__init__.py,sha256=o8d20yL96tGKyj_0NXyBn_HJ2i1hofM-qTuKouf4ZkE,47
2
+ geopic_tag_reader/camera.py,sha256=2Sr0jAt3RXUWazYMnkwF6J6lVnKvSp7Ac8g7yOHehVA,1643
3
+ geopic_tag_reader/main.py,sha256=a0RmJIA0R5dTK1vzDZD3Q4MFFAiDv6pnRiFHjfIjeJU,3141
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=w19zJ9CvdJyk_UxWcYX2T0DkhdQmw2xMzlAo1sR1ir0,22867
7
+ geopic_tag_reader/writer.py,sha256=QmQqQpWgb6AL3Y0Kuzy7PF1asUsTVDq9buwJXfFSRq8,8672
8
+ geopic_tag_reader-1.1.0.dist-info/entry_points.txt,sha256=c9YwjCNhxveDf-61_aSRlzcpoutvM6KQCerlzaVt_JU,64
9
+ geopic_tag_reader-1.1.0.dist-info/LICENSE,sha256=oHWDwXkJJb9zJzThMN3F9Li4yFhz1qxOUByouY7L3bI,1070
10
+ geopic_tag_reader-1.1.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
11
+ geopic_tag_reader-1.1.0.dist-info/METADATA,sha256=DJ9OiZyvVGNMdveGev-V33kO9sqPPo-V9SADIezd0I4,6303
12
+ geopic_tag_reader-1.1.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- geopic_tag_reader/__init__.py,sha256=-e4m9yg23kCmlAui1EAcQWE_HYIXQOke2r5HMargz44,46
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.5.dist-info/entry_points.txt,sha256=c9YwjCNhxveDf-61_aSRlzcpoutvM6KQCerlzaVt_JU,64
9
- geopic_tag_reader-1.0.5.dist-info/LICENSE,sha256=oHWDwXkJJb9zJzThMN3F9Li4yFhz1qxOUByouY7L3bI,1070
10
- geopic_tag_reader-1.0.5.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
11
- geopic_tag_reader-1.0.5.dist-info/METADATA,sha256=bbj7fzMdaEMDjuq2cIo_N6ss1JFDPe89piKz-GPGAbk,6445
12
- geopic_tag_reader-1.0.5.dist-info/RECORD,,