geopic-tag-reader 1.0.6__tar.gz → 1.1.1__tar.gz
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.
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/CHANGELOG.md +17 -1
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/PKG-INFO +4 -4
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/API_USAGE.md +1 -1
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/model.md +3 -1
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/reader.md +38 -25
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/__init__.py +1 -1
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/main.py +3 -1
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/reader.py +92 -14
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/writer.py +2 -2
- geopic_tag_reader-1.1.1/tests/fixtures/charset.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/test_reader.py +47 -26
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/test_writer.py +3 -3
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/.gitignore +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/.gitlab-ci.yml +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/.pre-commit-config.yaml +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/CODE_OF_CONDUCT.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/LICENSE +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/Makefile +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/README.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/.pages +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/CLI_USAGE.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/Develop.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/Install.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/camera.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/docs/writer.md +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/camera.py +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/model.py +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/geopic_tag_reader/py.typed +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/pyproject.toml +3 -3
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/pytest.ini +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/__init__.py +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/conftest.py +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/1.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/IMG_20210720_144918.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/IMG_20210720_161352.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/a1.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/b1.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/broken_makernotes.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/c1.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/cropped.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/d1.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/datetime_ms_float.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/datetime_offset.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/e1.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/flat.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/gopromax_flat.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/gps_date_slash.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/gps_date_time_stamp.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_Ricoh_Theta.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_V4MPack.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_categorisee.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_datetimeoriginal.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_gps_date_string.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_gps_datestamp.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_gps_sotm.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_invalid_gps_date.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_without_coord.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_without_dt.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_without_exif_tags.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/insta360_date.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/int_long_tag.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/out_of_bounds_lat.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/out_of_bounds_lon.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/pic_with_float_lat.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/ricoh_theta_no_projection.jpg +0 -0
- {geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/test_main.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.1] - 2024-04-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Reader now handles pitch & roll values from various EXIF/XMP tags.
|
|
15
|
+
|
|
16
|
+
## [1.1.0] - 2024-04-17
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Encoding information (`charset=...`) is now stripped out of text EXIF tags.
|
|
21
|
+
- `GeoPicTags` objects returns `ts` as a Python `datetime` object (instead of decimal epoch).
|
|
22
|
+
- Improved timezone handling in reader, GPS coordinates are used to find appropriate timezone when no timezone is defined in EXIF tags. If a UTC fallback is done, a warning is thrown.
|
|
23
|
+
|
|
10
24
|
## [1.0.6] - 2024-04-02
|
|
11
25
|
|
|
12
26
|
### Changed
|
|
@@ -152,7 +166,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
152
166
|
|
|
153
167
|
- EXIF tag reading methods extracted from [GeoVisio API](https://gitlab.com/geovisio/api)
|
|
154
168
|
|
|
155
|
-
[Unreleased]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.
|
|
169
|
+
[Unreleased]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.1.1...main
|
|
170
|
+
[1.1.1]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.1.0...1.1.1
|
|
171
|
+
[1.1.0]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.0.6...1.1.0
|
|
156
172
|
[1.0.6]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.0.5...1.0.6
|
|
157
173
|
[1.0.5]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.0.4...1.0.5
|
|
158
174
|
[1.0.4]: https://gitlab.com/geovisio/geo-picture-tag-reader/-/compare/1.0.3...1.0.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: geopic-tag-reader
|
|
3
|
-
Version: 1.
|
|
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
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
## Classes
|
|
13
13
|
|
|
14
|
-
- [`model.PictureType`](./model.md#class-picturetype)
|
|
14
|
+
- [`model.PictureType`](./model.md#class-picturetype)
|
|
15
15
|
- [`reader.CropValues`](./reader.md#class-cropvalues): Cropped equirectangular pictures metadata
|
|
16
16
|
- [`reader.GeoPicTags`](./reader.md#class-geopictags): Tags associated to a geolocated picture
|
|
17
17
|
- [`reader.InvalidExifException`](./reader.md#class-invalidexifexception): Exception for invalid EXIF information from image
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
13
|
+
<a href="../geopic_tag_reader/reader.py#L124"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
14
14
|
|
|
15
15
|
## <kbd>function</kbd> `readPictureMetadata`
|
|
16
16
|
|
|
@@ -35,7 +35,7 @@ Extracts metadata from picture file
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
38
|
+
<a href="../geopic_tag_reader/reader.py#L394"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
39
39
|
|
|
40
40
|
## <kbd>function</kbd> `decodeMakeModel`
|
|
41
41
|
|
|
@@ -48,7 +48,7 @@ Python 2/3 compatible decoding of make/model field.
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
51
|
+
<a href="../geopic_tag_reader/reader.py#L405"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
52
52
|
|
|
53
53
|
## <kbd>function</kbd> `isValidManyFractions`
|
|
54
54
|
|
|
@@ -63,7 +63,7 @@ isValidManyFractions(value: str) → bool
|
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
66
|
+
<a href="../geopic_tag_reader/reader.py#L412"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
67
67
|
|
|
68
68
|
## <kbd>function</kbd> `decodeManyFractions`
|
|
69
69
|
|
|
@@ -76,7 +76,7 @@ Try to decode a list of fractions, separated by spaces
|
|
|
76
76
|
|
|
77
77
|
---
|
|
78
78
|
|
|
79
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
79
|
+
<a href="../geopic_tag_reader/reader.py#L425"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
80
80
|
|
|
81
81
|
## <kbd>function</kbd> `decodeLatLon`
|
|
82
82
|
|
|
@@ -92,14 +92,16 @@ Reads GPS info from given group to get latitude/longitude as float coordinates
|
|
|
92
92
|
|
|
93
93
|
---
|
|
94
94
|
|
|
95
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
95
|
+
<a href="../geopic_tag_reader/reader.py#L480"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
96
96
|
|
|
97
97
|
## <kbd>function</kbd> `decodeDateTimeOriginal`
|
|
98
98
|
|
|
99
99
|
```python
|
|
100
100
|
decodeDateTimeOriginal(
|
|
101
101
|
data: dict,
|
|
102
|
-
datetimeField: str
|
|
102
|
+
datetimeField: str,
|
|
103
|
+
lat: Optional[float] = None,
|
|
104
|
+
lon: Optional[float] = None
|
|
103
105
|
) → Tuple[Optional[datetime], List[str]]
|
|
104
106
|
```
|
|
105
107
|
|
|
@@ -110,7 +112,7 @@ decodeDateTimeOriginal(
|
|
|
110
112
|
|
|
111
113
|
---
|
|
112
114
|
|
|
113
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
115
|
+
<a href="../geopic_tag_reader/reader.py#L534"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
114
116
|
|
|
115
117
|
## <kbd>function</kbd> `decodeTimeOffset`
|
|
116
118
|
|
|
@@ -125,12 +127,17 @@ decodeTimeOffset(data: dict, offsetTimeField: str) → Optional[tzinfo]
|
|
|
125
127
|
|
|
126
128
|
---
|
|
127
129
|
|
|
128
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
130
|
+
<a href="../geopic_tag_reader/reader.py#L540"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
129
131
|
|
|
130
132
|
## <kbd>function</kbd> `decodeGPSDateTime`
|
|
131
133
|
|
|
132
134
|
```python
|
|
133
|
-
decodeGPSDateTime(
|
|
135
|
+
decodeGPSDateTime(
|
|
136
|
+
data: dict,
|
|
137
|
+
group: str,
|
|
138
|
+
lat: Optional[float] = None,
|
|
139
|
+
lon: Optional[float] = None
|
|
140
|
+
) → Tuple[Optional[datetime], List[str]]
|
|
134
141
|
```
|
|
135
142
|
|
|
136
143
|
|
|
@@ -140,7 +147,7 @@ decodeGPSDateTime(data: dict, group: str) → Tuple[Optional[datetime], List[str
|
|
|
140
147
|
|
|
141
148
|
---
|
|
142
149
|
|
|
143
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
150
|
+
<a href="../geopic_tag_reader/reader.py#L591"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
144
151
|
|
|
145
152
|
## <kbd>function</kbd> `decodeSecondsAndMicroSeconds`
|
|
146
153
|
|
|
@@ -158,7 +165,7 @@ decodeSecondsAndMicroSeconds(
|
|
|
158
165
|
|
|
159
166
|
---
|
|
160
167
|
|
|
161
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
168
|
+
<a href="../geopic_tag_reader/reader.py#L617"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
162
169
|
|
|
163
170
|
## <kbd>function</kbd> `isExifTagUsable`
|
|
164
171
|
|
|
@@ -185,7 +192,7 @@ Is a given EXIF tag usable (not null and not an empty string)
|
|
|
185
192
|
|
|
186
193
|
---
|
|
187
194
|
|
|
188
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
195
|
+
<a href="../geopic_tag_reader/reader.py#L20"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
189
196
|
|
|
190
197
|
## <kbd>class</kbd> `CropValues`
|
|
191
198
|
Cropped equirectangular pictures metadata
|
|
@@ -226,7 +233,7 @@ __init__(
|
|
|
226
233
|
|
|
227
234
|
---
|
|
228
235
|
|
|
229
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
236
|
+
<a href="../geopic_tag_reader/reader.py#L41"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
230
237
|
|
|
231
238
|
## <kbd>class</kbd> `GeoPicTags`
|
|
232
239
|
Tags associated to a geolocated picture
|
|
@@ -237,8 +244,8 @@ Tags associated to a geolocated picture
|
|
|
237
244
|
|
|
238
245
|
- <b>`lat`</b> (float): GPS Latitude (in WGS84)
|
|
239
246
|
- <b>`lon`</b> (float): GPS Longitude (in WGS84)
|
|
240
|
-
- <b>`ts`</b> (
|
|
241
|
-
- <b>`heading`</b> (int): Picture heading (in degrees, North = 0°, East = 90°, South = 180°, West = 270°)
|
|
247
|
+
- <b>`ts`</b> (datetime): The capture date (date & time with timezone)
|
|
248
|
+
- <b>`heading`</b> (int): Picture heading/yaw (in degrees, North = 0°, East = 90°, South = 180°, West = 270°)
|
|
242
249
|
- <b>`type`</b> (str): The kind of picture (flat, equirectangular)
|
|
243
250
|
- <b>`make`</b> (str): The camera manufacturer name
|
|
244
251
|
- <b>`model`</b> (str): The camera model name
|
|
@@ -247,6 +254,8 @@ Tags associated to a geolocated picture
|
|
|
247
254
|
- <b>`exif`</b> (dict[str, str]): Raw EXIF tags from picture (following Exiv2 naming scheme, see https://exiv2.org/metadata.html)
|
|
248
255
|
- <b>`tagreader_warnings`</b> (list[str]): List of thrown warnings during metadata reading
|
|
249
256
|
- <b>`altitude`</b> (float): altitude (in m) (optional)
|
|
257
|
+
- <b>`pitch`</b> (float): Picture pitch angle, compared to horizon (in degrees, bottom = -90°, horizon = 0°, top = 90°)
|
|
258
|
+
- <b>`roll`</b> (float): Picture roll angle, on a right/left axis (in degrees, left-arm down = -90°, flat = 0°, right-arm down = 90°)
|
|
250
259
|
|
|
251
260
|
|
|
252
261
|
|
|
@@ -260,7 +269,7 @@ Implementation note: this needs to be sync with the PartialGeoPicTags structure
|
|
|
260
269
|
__init__(
|
|
261
270
|
lat: float,
|
|
262
271
|
lon: float,
|
|
263
|
-
ts:
|
|
272
|
+
ts: datetime,
|
|
264
273
|
heading: Optional[int],
|
|
265
274
|
type: str,
|
|
266
275
|
make: Optional[str],
|
|
@@ -269,7 +278,9 @@ __init__(
|
|
|
269
278
|
crop: Optional[CropValues],
|
|
270
279
|
exif: Dict[str, str] = <factory>,
|
|
271
280
|
tagreader_warnings: List[str] = <factory>,
|
|
272
|
-
altitude: Optional[float] = None
|
|
281
|
+
altitude: Optional[float] = None,
|
|
282
|
+
pitch: Optional[float] = None,
|
|
283
|
+
roll: Optional[float] = None
|
|
273
284
|
) → None
|
|
274
285
|
```
|
|
275
286
|
|
|
@@ -283,12 +294,12 @@ __init__(
|
|
|
283
294
|
|
|
284
295
|
---
|
|
285
296
|
|
|
286
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
297
|
+
<a href="../geopic_tag_reader/reader.py#L81"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
287
298
|
|
|
288
299
|
## <kbd>class</kbd> `InvalidExifException`
|
|
289
300
|
Exception for invalid EXIF information from image
|
|
290
301
|
|
|
291
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
302
|
+
<a href="../geopic_tag_reader/reader.py#L84"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
292
303
|
|
|
293
304
|
### <kbd>method</kbd> `__init__`
|
|
294
305
|
|
|
@@ -306,7 +317,7 @@ __init__(msg)
|
|
|
306
317
|
|
|
307
318
|
---
|
|
308
319
|
|
|
309
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
320
|
+
<a href="../geopic_tag_reader/reader.py#L88"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
310
321
|
|
|
311
322
|
## <kbd>class</kbd> `PartialGeoPicTags`
|
|
312
323
|
Tags associated to a geolocated picture when not all tags have been found
|
|
@@ -321,7 +332,7 @@ Implementation note: this needs to be sync with the GeoPicTags structure
|
|
|
321
332
|
__init__(
|
|
322
333
|
lat: Optional[float] = None,
|
|
323
334
|
lon: Optional[float] = None,
|
|
324
|
-
ts: Optional[
|
|
335
|
+
ts: Optional[datetime] = None,
|
|
325
336
|
heading: Optional[int] = None,
|
|
326
337
|
type: Optional[str] = None,
|
|
327
338
|
make: Optional[str] = None,
|
|
@@ -330,7 +341,9 @@ __init__(
|
|
|
330
341
|
crop: Optional[CropValues] = None,
|
|
331
342
|
exif: Dict[str, str] = <factory>,
|
|
332
343
|
tagreader_warnings: List[str] = <factory>,
|
|
333
|
-
altitude: Optional[float] = None
|
|
344
|
+
altitude: Optional[float] = None,
|
|
345
|
+
pitch: Optional[float] = None,
|
|
346
|
+
roll: Optional[float] = None
|
|
334
347
|
) → None
|
|
335
348
|
```
|
|
336
349
|
|
|
@@ -344,14 +357,14 @@ __init__(
|
|
|
344
357
|
|
|
345
358
|
---
|
|
346
359
|
|
|
347
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
360
|
+
<a href="../geopic_tag_reader/reader.py#L111"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
348
361
|
|
|
349
362
|
## <kbd>class</kbd> `PartialExifException`
|
|
350
363
|
Exception for partial / missing EXIF information from image
|
|
351
364
|
|
|
352
365
|
Contains a PartialGeoPicTags with all tags that have been read and the list of missing tags
|
|
353
366
|
|
|
354
|
-
<a href="../geopic_tag_reader/reader.py#
|
|
367
|
+
<a href="../geopic_tag_reader/reader.py#L118"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
|
|
355
368
|
|
|
356
369
|
### <kbd>method</kbd> `__init__`
|
|
357
370
|
|
|
@@ -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 (
|
|
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:
|
|
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[
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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).
|
|
Binary file
|
|
@@ -7,16 +7,23 @@ from .conftest import FIXTURE_DIR, openImg
|
|
|
7
7
|
def assertGeoPicTagsEquals(gpt, expectedDict):
|
|
8
8
|
assert gpt.lat == expectedDict.get("lat")
|
|
9
9
|
assert gpt.lon == expectedDict.get("lon")
|
|
10
|
-
assert gpt.ts == expectedDict.get("ts")
|
|
11
10
|
assert gpt.heading == expectedDict.get("heading")
|
|
12
11
|
assert gpt.type == expectedDict.get("type")
|
|
13
12
|
assert gpt.make == expectedDict.get("make")
|
|
14
13
|
assert gpt.model == expectedDict.get("model")
|
|
15
14
|
assert gpt.focal_length == expectedDict.get("focal_length")
|
|
16
15
|
assert gpt.altitude == expectedDict.get("altitude")
|
|
16
|
+
assert gpt.pitch == expectedDict.get("pitch")
|
|
17
|
+
assert gpt.roll == expectedDict.get("roll")
|
|
17
18
|
assert gpt.tagreader_warnings == expectedDict.get("tagreader_warnings", [])
|
|
18
19
|
assert len(gpt.exif) > 0
|
|
19
20
|
|
|
21
|
+
if expectedDict.get("ts") is not None:
|
|
22
|
+
assert gpt.ts is not None
|
|
23
|
+
assert gpt.ts.isoformat() == expectedDict["ts"]
|
|
24
|
+
else:
|
|
25
|
+
assert gpt.ts is None
|
|
26
|
+
|
|
20
27
|
if gpt.crop:
|
|
21
28
|
assert expectedDict.get("crop") is not None
|
|
22
29
|
assert gpt.crop.fullWidth == expectedDict["crop"].get("fullWidth")
|
|
@@ -37,13 +44,15 @@ def test_readPictureMetadata(datafiles):
|
|
|
37
44
|
{
|
|
38
45
|
"lat": 49.00688961988304,
|
|
39
46
|
"lon": 1.9191854417991367,
|
|
40
|
-
"ts":
|
|
47
|
+
"ts": "2021-07-29T11:16:54+02:00",
|
|
41
48
|
"heading": 349,
|
|
42
49
|
"type": "equirectangular",
|
|
43
50
|
"make": "GoPro",
|
|
44
51
|
"model": "Max",
|
|
45
52
|
"focal_length": 3,
|
|
46
53
|
"altitude": 93,
|
|
54
|
+
"roll": 0,
|
|
55
|
+
"pitch": 0,
|
|
47
56
|
},
|
|
48
57
|
)
|
|
49
58
|
|
|
@@ -56,13 +65,15 @@ def test_readPictureMetadata_negCoords(datafiles):
|
|
|
56
65
|
{
|
|
57
66
|
"lat": 48.33756428166505,
|
|
58
67
|
"lon": -1.9331088333333333,
|
|
59
|
-
"ts":
|
|
68
|
+
"ts": "2022-05-13T16:53:00+02:00",
|
|
60
69
|
"heading": 32,
|
|
61
70
|
"type": "equirectangular",
|
|
62
71
|
"make": "GoPro",
|
|
63
72
|
"model": "Max",
|
|
64
73
|
"focal_length": 3,
|
|
65
74
|
"altitude": 79,
|
|
75
|
+
"roll": 0,
|
|
76
|
+
"pitch": 0,
|
|
66
77
|
},
|
|
67
78
|
)
|
|
68
79
|
|
|
@@ -75,7 +86,7 @@ def test_readPictureMetadata_flat(datafiles):
|
|
|
75
86
|
{
|
|
76
87
|
"lat": 48.139852239480945,
|
|
77
88
|
"lon": -1.9499731060073981,
|
|
78
|
-
"ts":
|
|
89
|
+
"ts": "2015-04-25T15:37:48+02:00",
|
|
79
90
|
"heading": 155,
|
|
80
91
|
"type": "flat",
|
|
81
92
|
"make": "OLYMPUS IMAGING CORP.",
|
|
@@ -93,7 +104,7 @@ def test_readPictureMetadata_flat2(datafiles):
|
|
|
93
104
|
{
|
|
94
105
|
"lat": 48.85779642035038,
|
|
95
106
|
"lon": 2.3392783047650747,
|
|
96
|
-
"ts":
|
|
107
|
+
"ts": "2015-05-04T13:08:52+02:00",
|
|
97
108
|
"heading": 302,
|
|
98
109
|
"type": "flat",
|
|
99
110
|
"make": "Canon",
|
|
@@ -111,7 +122,7 @@ def test_readPictureMetadata_xmpHeading(datafiles):
|
|
|
111
122
|
{
|
|
112
123
|
"lat": 50.87070833333333,
|
|
113
124
|
"lon": -1.5260916666666666,
|
|
114
|
-
"ts":
|
|
125
|
+
"ts": "2020-09-13T15:40:19.767000+01:00",
|
|
115
126
|
"heading": 67,
|
|
116
127
|
"type": "equirectangular",
|
|
117
128
|
"make": "Google",
|
|
@@ -130,7 +141,7 @@ def test_readPictureMetadata_noHeading(datafiles):
|
|
|
130
141
|
{
|
|
131
142
|
"lat": 48.15506638888889,
|
|
132
143
|
"lon": -1.6844680555555556,
|
|
133
|
-
"ts":
|
|
144
|
+
"ts": "2022-10-19T09:56:34+02:00",
|
|
134
145
|
"heading": None,
|
|
135
146
|
"type": "flat",
|
|
136
147
|
"make": "SONY",
|
|
@@ -154,8 +165,10 @@ def test_readPictureMetadata_ricoh_theta(datafiles):
|
|
|
154
165
|
"lon": 2.3205357914890987,
|
|
155
166
|
"make": "RICOH",
|
|
156
167
|
"model": "THETA m15",
|
|
157
|
-
"ts":
|
|
168
|
+
"ts": "2016-03-25T14:12:13+01:00",
|
|
158
169
|
"type": "equirectangular",
|
|
170
|
+
"roll": 3,
|
|
171
|
+
"pitch": 1,
|
|
159
172
|
},
|
|
160
173
|
)
|
|
161
174
|
|
|
@@ -173,7 +186,7 @@ def test_readPictureMetadata_v4mpack(datafiles):
|
|
|
173
186
|
"lon": -1.2761512389983616,
|
|
174
187
|
"make": "STFMANI",
|
|
175
188
|
"model": "V4MPOD 1",
|
|
176
|
-
"ts":
|
|
189
|
+
"ts": "2019-04-16T14:20:13+02:00",
|
|
177
190
|
"type": "equirectangular",
|
|
178
191
|
"altitude": 34,
|
|
179
192
|
},
|
|
@@ -192,7 +205,7 @@ def test_readPictureMetadata_a5000(datafiles):
|
|
|
192
205
|
"lon": 2.51197323068765,
|
|
193
206
|
"make": "OnePlus",
|
|
194
207
|
"model": "ONEPLUS A5000",
|
|
195
|
-
"ts":
|
|
208
|
+
"ts": "2021-07-20T16:13:52.199995+02:00",
|
|
196
209
|
"type": "flat",
|
|
197
210
|
"altitude": 0,
|
|
198
211
|
},
|
|
@@ -227,7 +240,7 @@ def test_readPictureMetadata_int_long_tag(datafiles):
|
|
|
227
240
|
"lon": 4.700622,
|
|
228
241
|
"make": None,
|
|
229
242
|
"model": None,
|
|
230
|
-
"ts":
|
|
243
|
+
"ts": "2023-01-12T09:17:00+01:00",
|
|
231
244
|
"type": "flat",
|
|
232
245
|
"tagreader_warnings": ["GPSLongitudeRef not found, assuming GPSLongitudeRef is East"],
|
|
233
246
|
},
|
|
@@ -249,7 +262,7 @@ def test_readPictureMetadata_invalidGpsDate(datafiles):
|
|
|
249
262
|
"lon": 2.358506155368721,
|
|
250
263
|
"make": "samsung",
|
|
251
264
|
"model": None,
|
|
252
|
-
"ts":
|
|
265
|
+
"ts": "2021-09-08T11:43:57.075400+02:00",
|
|
253
266
|
"type": "flat",
|
|
254
267
|
"altitude": 73,
|
|
255
268
|
"tagreader_warnings": [
|
|
@@ -271,7 +284,7 @@ def test_readPictureMetadata_gpsDateStamp(datafiles):
|
|
|
271
284
|
"lon": 2.4833194444444446,
|
|
272
285
|
"make": "Apple",
|
|
273
286
|
"model": "iPhone 12 Pro",
|
|
274
|
-
"ts":
|
|
287
|
+
"ts": "2023-04-29T18:30:51.565000+02:00",
|
|
275
288
|
"type": "flat",
|
|
276
289
|
"altitude": 36,
|
|
277
290
|
"tagreader_warnings": [
|
|
@@ -293,7 +306,7 @@ def test_readPictureMetadata_gpsDateString(datafiles):
|
|
|
293
306
|
"lon": -1.0015555555555555,
|
|
294
307
|
"make": "Apple",
|
|
295
308
|
"model": "iPhone 8 Plus",
|
|
296
|
-
"ts":
|
|
309
|
+
"ts": "2019-07-28T13:25:42.529000+02:00",
|
|
297
310
|
"type": "flat",
|
|
298
311
|
"altitude": 19,
|
|
299
312
|
"tagreader_warnings": [
|
|
@@ -322,7 +335,7 @@ def test_readPictureMetadata_cropped(datafiles):
|
|
|
322
335
|
"lon": 1.919185441804927,
|
|
323
336
|
"make": "GoPro",
|
|
324
337
|
"model": "Max",
|
|
325
|
-
"ts":
|
|
338
|
+
"ts": "2021-07-29T11:16:54+02:00",
|
|
326
339
|
"type": "equirectangular",
|
|
327
340
|
"altitude": 93,
|
|
328
341
|
"crop": {"fullWidth": 4032, "fullHeight": 2016, "width": 2150, "height": 1412, "top": 134, "left": 538},
|
|
@@ -341,7 +354,7 @@ def test_readPictureMetadata_datetimeoriginal(datafiles):
|
|
|
341
354
|
"lon": -1.382302762883527,
|
|
342
355
|
"make": "Motorola",
|
|
343
356
|
"model": "XT1052",
|
|
344
|
-
"ts":
|
|
357
|
+
"ts": "2020-08-31T09:36:28+02:00",
|
|
345
358
|
"type": "flat",
|
|
346
359
|
"altitude": 72,
|
|
347
360
|
"tagreader_warnings": [
|
|
@@ -362,7 +375,7 @@ def test_readPictureMetadata_datetimeoriginal_decimal_milliseconds(datafiles):
|
|
|
362
375
|
"lon": 121.15072633333334,
|
|
363
376
|
"make": "BlackVue",
|
|
364
377
|
"model": "DR900S-1CH",
|
|
365
|
-
"ts":
|
|
378
|
+
"ts": "2023-11-12T00:25:19.023000+08:00",
|
|
366
379
|
"type": "flat",
|
|
367
380
|
"altitude": 29,
|
|
368
381
|
"tagreader_warnings": [],
|
|
@@ -401,7 +414,7 @@ def test_readPictureWithoutCoord(datafiles):
|
|
|
401
414
|
"lon": None,
|
|
402
415
|
"make": "Apple",
|
|
403
416
|
"model": "iPhone 12 Pro",
|
|
404
|
-
"ts":
|
|
417
|
+
"ts": "2023-04-29T16:30:51.565000+00:00",
|
|
405
418
|
"type": "flat",
|
|
406
419
|
"altitude": 36,
|
|
407
420
|
"tagreader_warnings": [
|
|
@@ -472,7 +485,7 @@ def test_readPictureWithLatitudeAsFloat(datafiles):
|
|
|
472
485
|
"lon": 7.683081,
|
|
473
486
|
"make": None,
|
|
474
487
|
"model": "PULSAR",
|
|
475
|
-
"ts":
|
|
488
|
+
"ts": "2021-11-16T16:18:16.890000+01:00",
|
|
476
489
|
"type": "equirectangular",
|
|
477
490
|
"altitude": None,
|
|
478
491
|
"tagreader_warnings": [],
|
|
@@ -492,7 +505,7 @@ def test_readPictureMetadata_gps_date_time_stamp(datafiles):
|
|
|
492
505
|
"lon": 7.683081,
|
|
493
506
|
"make": None,
|
|
494
507
|
"model": "PULSAR",
|
|
495
|
-
"ts":
|
|
508
|
+
"ts": "2021-11-16T16:18:16.890000+01:00",
|
|
496
509
|
"type": "equirectangular",
|
|
497
510
|
"altitude": None,
|
|
498
511
|
"tagreader_warnings": [],
|
|
@@ -512,7 +525,7 @@ def test_readPictureMetadata_insta360(datafiles):
|
|
|
512
525
|
"lon": 3.482755555555556,
|
|
513
526
|
"make": "Insta360",
|
|
514
527
|
"model": "One X2.PHOTO_NORMAL",
|
|
515
|
-
"ts":
|
|
528
|
+
"ts": "2023-11-22T14:17:49+01:00",
|
|
516
529
|
"type": "equirectangular",
|
|
517
530
|
"altitude": 84,
|
|
518
531
|
"tagreader_warnings": [],
|
|
@@ -532,7 +545,7 @@ def test_readPictureMetadata_ricoh_noproj(datafiles):
|
|
|
532
545
|
"lon": -1.5464416503906249,
|
|
533
546
|
"make": "Ricoh",
|
|
534
547
|
"model": "Theta S",
|
|
535
|
-
"ts":
|
|
548
|
+
"ts": "2023-01-01T13:15:42+01:00",
|
|
536
549
|
"type": "equirectangular",
|
|
537
550
|
"altitude": None,
|
|
538
551
|
"tagreader_warnings": [],
|
|
@@ -549,7 +562,7 @@ def test_readPictureMetadata_gopromax_flat(datafiles):
|
|
|
549
562
|
{
|
|
550
563
|
"lat": 47.22555109997222,
|
|
551
564
|
"lon": -1.5631604999722222,
|
|
552
|
-
"ts":
|
|
565
|
+
"ts": "2024-02-17T13:46:32+01:00",
|
|
553
566
|
"heading": None,
|
|
554
567
|
"type": "flat",
|
|
555
568
|
"make": "GoPro",
|
|
@@ -572,7 +585,7 @@ def test_readPictureMetadata_broken_makernotes(datafiles):
|
|
|
572
585
|
"lon": -1.7841980555555554,
|
|
573
586
|
"make": "SONY",
|
|
574
587
|
"model": "FDR-X1000V",
|
|
575
|
-
"ts":
|
|
588
|
+
"ts": "2020-09-03T08:50:20+02:00",
|
|
576
589
|
"type": "flat",
|
|
577
590
|
"altitude": 99,
|
|
578
591
|
"focal_length": 2.8,
|
|
@@ -590,13 +603,15 @@ def test_readPictureMetadata_datetime_offset(datafiles):
|
|
|
590
603
|
{
|
|
591
604
|
"lat": 48.33756428166505,
|
|
592
605
|
"lon": -1.9331088333333333,
|
|
593
|
-
"ts":
|
|
606
|
+
"ts": "2022-05-13T16:54:11+02:00",
|
|
594
607
|
"heading": 32,
|
|
595
608
|
"type": "equirectangular",
|
|
596
609
|
"make": "GoPro",
|
|
597
610
|
"model": "Max",
|
|
598
611
|
"focal_length": 3,
|
|
599
612
|
"altitude": 79,
|
|
613
|
+
"roll": 0,
|
|
614
|
+
"pitch": 0,
|
|
600
615
|
},
|
|
601
616
|
)
|
|
602
617
|
|
|
@@ -610,7 +625,7 @@ def test_readPictureMetadata_gps_date_slash(datafiles):
|
|
|
610
625
|
{
|
|
611
626
|
"lat": 43.42541569992266,
|
|
612
627
|
"lon": 1.3766216000638112,
|
|
613
|
-
"ts":
|
|
628
|
+
"ts": "2023-06-13T09:15:00+02:00",
|
|
614
629
|
"heading": None,
|
|
615
630
|
"type": "flat",
|
|
616
631
|
"make": None,
|
|
@@ -623,3 +638,9 @@ def test_readPictureMetadata_gps_date_slash(datafiles):
|
|
|
623
638
|
],
|
|
624
639
|
},
|
|
625
640
|
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@pytest.mark.datafiles(os.path.join(FIXTURE_DIR, "charset.jpg"))
|
|
644
|
+
def test_readPictureMetadata_charset(datafiles):
|
|
645
|
+
result = reader.readPictureMetadata(openImg(str(datafiles) + "/charset.jpg"))
|
|
646
|
+
assert result.exif["Exif.Photo.UserComment"] == "CD31 31_D0003_51_600"
|
|
@@ -18,7 +18,7 @@ def test_writePictureMetadata_capture_time(datafiles):
|
|
|
18
18
|
image_file_upd = writer.writePictureMetadata(img_orig, writer.PictureMetadata(capture_time=capture_time))
|
|
19
19
|
tags = reader.readPictureMetadata(image_file_upd)
|
|
20
20
|
|
|
21
|
-
assert
|
|
21
|
+
assert tags.ts == capture_time
|
|
22
22
|
|
|
23
23
|
# we also check specific tags:
|
|
24
24
|
assert tags.exif["Exif.Photo.DateTimeOriginal"] == "2023-06-01 12:48:01"
|
|
@@ -36,7 +36,7 @@ def test_writePictureMetadata_capture_time_no_timezone(datafiles):
|
|
|
36
36
|
tags = reader.readPictureMetadata(image_file_upd)
|
|
37
37
|
|
|
38
38
|
paris = pytz.timezone("Europe/Paris")
|
|
39
|
-
assert
|
|
39
|
+
assert tags.ts == paris.localize(capture_time).astimezone(pytz.UTC)
|
|
40
40
|
|
|
41
41
|
# DateTimeOriginal should be a local time, so 12:48:01 localized in Europe/Paris timezome (since it's where the picture has been taken)
|
|
42
42
|
assert tags.exif["Exif.Photo.DateTimeOriginal"] == "2023-06-01 12:48:01"
|
|
@@ -78,7 +78,7 @@ def test_writePictureMetadata_capture_time_no_position_in_file_but_overriden(dat
|
|
|
78
78
|
tags = reader.readPictureMetadata(image_file_upd)
|
|
79
79
|
|
|
80
80
|
paris = pytz.timezone("Europe/Paris")
|
|
81
|
-
assert
|
|
81
|
+
assert tags.ts == paris.localize(capture_time).astimezone(pytz.UTC)
|
|
82
82
|
|
|
83
83
|
# DateTimeOriginal should be a local time, so 12:48:01 localized in Europe/Paris timezome (since it's where the picture has been taken)
|
|
84
84
|
assert tags.exif["Exif.Photo.DateTimeOriginal"] == "2023-06-01 12:48:01"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -11,6 +11,9 @@ dependencies = [
|
|
|
11
11
|
"typer ~= 0.12",
|
|
12
12
|
"xmltodict ~= 0.13",
|
|
13
13
|
"pyexiv2 == 2.8.3",
|
|
14
|
+
"timezonefinder == 6.2.0",
|
|
15
|
+
"pytz ~= 2023.3",
|
|
16
|
+
"types-pytz ~= 2023.3.0.1",
|
|
14
17
|
]
|
|
15
18
|
requires-python = ">=3.8"
|
|
16
19
|
|
|
@@ -33,9 +36,6 @@ dev = [
|
|
|
33
36
|
build = ["flit ~= 3.8.0"]
|
|
34
37
|
# optional dependencies to be able to write exif tags
|
|
35
38
|
write-exif = [
|
|
36
|
-
"timezonefinder == 6.2.0",
|
|
37
|
-
"pytz ~= 2023.3",
|
|
38
|
-
"types-pytz ~= 2023.3.0.1",
|
|
39
39
|
"python-dateutil ~= 2.8.2",
|
|
40
40
|
]
|
|
41
41
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/img_without_exif_tags.jpg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geopic_tag_reader-1.0.6 → geopic_tag_reader-1.1.1}/tests/fixtures/ricoh_theta_no_projection.jpg
RENAMED
|
File without changes
|
|
File without changes
|