|
5 | 5 |
|
6 | 6 | import piexif |
7 | 7 | import PIL |
| 8 | +from geopy import Point |
8 | 9 | from piexif import TAGS |
9 | 10 | from piexif import TYPES as TAG_TYPES |
10 | 11 | from PIL.Image import Image |
@@ -122,48 +123,62 @@ def extract_metadata(self, image: Image) -> bool: |
122 | 123 | raise NotImplementedError() |
123 | 124 |
|
124 | 125 |
|
| 126 | +def extract_exif_value(exif_dict, tag_name: str): |
| 127 | + tag_id, tag_type = tag_name_to_id_type(tag_name) |
| 128 | + for src in exif_dict.values(): |
| 129 | + if isinstance(src, dict) and tag_id in src: |
| 130 | + tag_value = src[tag_id] |
| 131 | + return convert_tag_value(tag_type, tag_value) |
| 132 | + else: |
| 133 | + raise MissingMetadataError() |
| 134 | + |
| 135 | + |
| 136 | +def tag_name_to_id_type(tag_name: str) -> Tuple[int, int]: |
| 137 | + for _, tags in TAGS.items(): |
| 138 | + for tag_id, description in tags.items(): |
| 139 | + if description["name"] == tag_name: |
| 140 | + return tag_id, description["type"] |
| 141 | + raise ValueError(f"Could not find tag id for '{tag_name}'") |
| 142 | + |
| 143 | + |
| 144 | +def convert_tag_value(tag_type, tag_value): |
| 145 | + if tag_type in (TAG_TYPES.Rational, TAG_TYPES.SRational): |
| 146 | + if isinstance(tag_value[0], tuple): |
| 147 | + if len(tag_value) == 3: |
| 148 | + # We are probably dealing with GPS coordinates |
| 149 | + ( |
| 150 | + degrees, |
| 151 | + minutes, |
| 152 | + seconds, |
| 153 | + ) = (convert_tag_value(tag_type, v) for v in tag_value) |
| 154 | + return f"{degrees}°{minutes}′{seconds}″" |
| 155 | + return " ".join(str(convert_tag_value(tag_type, v)) for v in tag_value) |
| 156 | + else: |
| 157 | + if tag_value[1] == 1: |
| 158 | + return tag_value[0] |
| 159 | + else: |
| 160 | + return tag_value[0] / tag_value[1] |
| 161 | + if tag_type == TAG_TYPES.Ascii: |
| 162 | + return tag_value.decode("ascii") |
| 163 | + return tag_value |
| 164 | + |
| 165 | + |
125 | 166 | class ExifTag(Tag): |
126 | 167 | """Extract value of any EXIF tag""" |
127 | 168 |
|
128 | | - tag_id: int |
129 | | - tag_type: int |
| 169 | + tag_name: str |
130 | 170 |
|
131 | 171 | def configure(self, tag_name: str): # type: ignore |
132 | 172 | """ |
133 | 173 | :param tag_name: name of the tag to extract (e.g. 'FocalLength', 'DateTime', 'FNumber') |
134 | 174 | """ |
135 | 175 | # TODO: generate list of supported tags dynamically |
136 | 176 | assert tag_name, "expected non empty tag name" |
137 | | - self.tag_id, self.tag_type = self._tag_name_to_id_type(tag_name) |
138 | | - |
139 | | - @staticmethod |
140 | | - def _tag_name_to_id_type(tag_name: str) -> Tuple[int, int]: |
141 | | - for _, tags in TAGS.items(): |
142 | | - for tag_id, description in tags.items(): |
143 | | - if description["name"] == tag_name: |
144 | | - return tag_id, description["type"] |
145 | | - raise ValueError(f"Could not find tag id for '{tag_name}'") |
| 177 | + self.tag_name = tag_name |
146 | 178 |
|
147 | 179 | def process(self, file: File, context: Optional[str]) -> Any: |
148 | 180 | exif_dict = piexif.load(str(file.absolute_path)) |
149 | | - for src in exif_dict.values(): |
150 | | - if isinstance(src, dict) and self.tag_id in src: |
151 | | - return self._extract_value(self.tag_type, src[self.tag_id]) |
152 | | - else: |
153 | | - raise MissingMetadataError() |
154 | | - |
155 | | - def _extract_value(self, tag_type, tag_value): |
156 | | - if tag_type in (TAG_TYPES.Rational, TAG_TYPES.SRational): |
157 | | - if isinstance(tag_value[0], tuple): |
158 | | - return " ".join(str(self._extract_value(v)) for v in tag_value) |
159 | | - else: |
160 | | - if tag_value[1] == 1: |
161 | | - return tag_value[0] |
162 | | - else: |
163 | | - return round(tag_value[0] / tag_value[1], 1) |
164 | | - if tag_type == TAG_TYPES.Ascii: |
165 | | - return tag_value.decode("ascii") |
166 | | - return tag_value |
| 181 | + return extract_exif_value(exif_dict, self.tag_name) |
167 | 182 |
|
168 | 183 |
|
169 | 184 | _tag_type_map = { |
@@ -195,13 +210,32 @@ class ResolutionTagAlias(TagAlias): |
195 | 210 | """%Image.Width()x%Image.Height()""" |
196 | 211 |
|
197 | 212 |
|
198 | | -class GpsPositionTag(ExifTag): |
| 213 | +class GpsPositionTag(Tag): |
199 | 214 | """Latitude and longitude of place where photo was taken""" |
200 | 215 |
|
201 | | - def configure(self): |
202 | | - self.latitude_tag_id, self.latitude_tag_type = self._tag_name_to_id_type( |
203 | | - "GpsLatitude" |
204 | | - ) |
| 216 | + require_context = False |
| 217 | + |
| 218 | + use_decimal: bool |
| 219 | + |
| 220 | + def configure(self, decimal: bool = True): |
| 221 | + """ |
| 222 | + :param decimal: Use simpler decimal notation instead of degrees-minutes-seconds |
| 223 | + """ |
| 224 | + self.use_decimal = decimal |
| 225 | + |
| 226 | + def process(self, file: File, context: Optional[str]) -> Any: |
| 227 | + assert context is None |
| 228 | + |
| 229 | + exif_dict = piexif.load(str(file.absolute_path)) |
| 230 | + latitude = extract_exif_value(exif_dict, "GPSLatitude") |
| 231 | + latitude_ref = extract_exif_value(exif_dict, "GPSLatitudeRef") |
| 232 | + longitude = extract_exif_value(exif_dict, "GPSLongitude") |
| 233 | + longitude_ref = extract_exif_value(exif_dict, "GPSLongitudeRef") |
| 234 | + |
| 235 | + degrees_notation = f"{latitude}{latitude_ref}, {longitude}{longitude_ref}" |
| 236 | + if self.use_decimal: |
| 237 | + return Point.from_string(degrees_notation).format_decimal() |
| 238 | + return degrees_notation |
205 | 239 |
|
206 | 240 |
|
207 | 241 | class HasGpsPositionTagAlias(TagAlias): |
|
0 commit comments