Skip to content

Commit 3610242

Browse files
Merge pull request #5 from DalenW/GeoData
- Write `geoData` from `.json`s to exif
2 parents 5d2dd5f + 82e15fe commit 3610242

File tree

1 file changed

+107
-20
lines changed

1 file changed

+107
-20
lines changed

google_photos_takeout_helper/__main__.py

Lines changed: 107 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ def main():
77
from datetime import datetime as _datetime
88

99
import piexif as _piexif
10+
from fractions import Fraction # piexif requires some values to be stored as rationals
11+
import math
1012

1113
parser = _argparse.ArgumentParser(
1214
prog='Photos takeout helper',
@@ -85,12 +87,11 @@ def main():
8587

8688
_os.makedirs(FIXED_DIR, exist_ok=True)
8789

88-
8990
def for_all_files_recursive(
90-
dir,
91-
file_function=lambda fo, fi: True,
92-
folder_function=lambda fo: True,
93-
filter_fun=lambda file: True
91+
dir,
92+
file_function=lambda fo, fi: True,
93+
folder_function=lambda fo: True,
94+
filter_fun=lambda file: True
9495
):
9596
for file in _os.listdir(dir):
9697
file = dir + '/' + file
@@ -104,21 +105,18 @@ def for_all_files_recursive(
104105
print('Found something weird...')
105106
print(file)
106107

107-
108108
def is_photo(file):
109109
what = _os.path.splitext(file.lower())[1]
110110
if what not in photo_formats:
111111
return False
112112
return True
113113

114-
115114
def is_video(file):
116115
what = _os.path.splitext(file.lower())[1]
117116
if what not in video_formats:
118117
return False
119118
return True
120119

121-
122120
# PART 1: removing duplicates
123121

124122
# THIS IS PARTLY COPIED FROM STACKOVERFLOW
@@ -165,15 +163,13 @@ def find_duplicates(path, filter_fun=lambda file: True):
165163

166164
return duplicates
167165

168-
169166
# Removes all duplicates in folder
170167
def remove_duplicates(dir):
171168
duplicates = find_duplicates(dir, lambda f: (is_photo(f) or is_video(f)))
172169
for file in duplicates:
173170
_os.remove(file)
174171
return True
175172

176-
177173
# PART 2: Fixing metadata and date-related stuff
178174

179175
# Returns json dict
@@ -189,7 +185,6 @@ def find_json_for_file(dir, file):
189185
else:
190186
raise FileNotFoundError('Couldnt find json for file: ' + file)
191187

192-
193188
# Returns date in 2019:01:01 23:59:59 format
194189
def get_date_from_folder_name(dir):
195190
dir = _os.path.basename(_os.path.normpath(dir))
@@ -225,7 +220,6 @@ def set_creation_date_from_str(file, str_datetime):
225220
print(e)
226221
_os.utime(file, (timestamp, timestamp))
227222

228-
229223
def set_creation_date_from_exif(file):
230224
exif_dict = _piexif.load(file)
231225
tags = [['0th', TAG_DATE_TIME], ['Exif', TAG_DATE_TIME_ORIGINAL], ['Exif', TAG_DATE_TIME_DIGITIZED]]
@@ -240,7 +234,6 @@ def set_creation_date_from_exif(file):
240234
raise IOError('No DateTime in given exif')
241235
set_creation_date_from_str(file, datetime_str)
242236

243-
244237
def set_file_exif_date(file, creation_date):
245238
try:
246239
exif_dict = _piexif.load(file)
@@ -255,15 +248,111 @@ def set_file_exif_date(file, creation_date):
255248
try:
256249
_piexif.insert(_piexif.dump(exif_dict), file)
257250
except Exception as e:
258-
print('Couldnt insert exif!')
251+
print("Couldn't insert exif!")
259252
print(e)
260253

261-
262254
def get_date_str_from_json(json):
263255
return _datetime.fromtimestamp(
264256
int(json['photoTakenTime']['timestamp'])
265257
).strftime('%Y:%m:%d %H:%M:%S')
266258

259+
def change_to_rational(number):
260+
"""convert a number to rantional
261+
Keyword arguments: number
262+
return: tuple like (1, 2), (numerator, denominator)
263+
"""
264+
f = Fraction(str(number))
265+
return f.numerator, f.denominator
266+
267+
# got this here https://github.com/hMatoba/piexifjs/issues/1#issuecomment-260176317
268+
def degToDmsRational(degFloat):
269+
min_float = degFloat % 1 * 60
270+
sec_float = min_float % 1 * 60
271+
deg = math.floor(degFloat)
272+
deg_min = math.floor(min_float)
273+
sec = round(sec_float * 100)
274+
275+
return [(deg, 1), (deg_min, 1), (sec, 100)]
276+
277+
def set_file_geo_data(file, json):
278+
"""
279+
Reads the geoData from google and saves it to the EXIF. This works assuming that the geodata looks like -100.12093, 50.213143. Something like that.
280+
281+
Written by DalenW.
282+
:param file:
283+
:param json:
284+
:return:
285+
"""
286+
287+
# prevents crashes
288+
try:
289+
exif_dict = _piexif.load(file)
290+
except (_piexif.InvalidImageDataError, ValueError):
291+
exif_dict = {'0th': {}, 'Exif': {}}
292+
293+
# fetches geo data from the photos editor first.
294+
longitude = float(json['geoData']['longitude'])
295+
latitude = float(json['geoData']['latitude'])
296+
altitude = float(json['geoData']['altitude'])
297+
298+
# fallbacks to GeoData Exif if it wasn't set in the photos editor.
299+
# https://github.com/TheLastGimbus/GooglePhotosTakeoutHelper/pull/5#discussion_r531792314
300+
longitude = float(json['geoData']['longitude'])
301+
latitude = float(json['geoData']['latitude'])
302+
altitude = json['geoData']['altitude']
303+
# Prioritise geoData set from GPhotos editor
304+
if longitude == 0 and latitude == 0:
305+
longitude = float(json['geoDataExif']['longitude'])
306+
latitude = float(json['geoDataExif']['latitude'])
307+
altitude = json['geoDataExif']['altitude']
308+
309+
# latitude >= 0: North latitude -> "N"
310+
# latitude < 0: South latitude -> "S"
311+
# longitude >= 0: East longitude -> "E"
312+
# longitude < 0: West longitude -> "W"
313+
314+
if longitude >= 0:
315+
longitude_ref = 'E'
316+
else:
317+
longitude_ref = 'W'
318+
longitude = longitude * -1
319+
320+
if latitude >= 0:
321+
latitude_ref = 'N'
322+
else:
323+
latitude_ref = 'S'
324+
latitude = latitude * -1
325+
326+
# referenced from https://gist.github.com/c060604/8a51f8999be12fc2be498e9ca56adc72
327+
gps_ifd = {
328+
_piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0)
329+
}
330+
331+
# skips it if it's empty
332+
if latitude != 0 or longitude != 0:
333+
gps_ifd.update({
334+
_piexif.GPSIFD.GPSLatitudeRef: latitude_ref,
335+
_piexif.GPSIFD.GPSLatitude: degToDmsRational(latitude),
336+
337+
_piexif.GPSIFD.GPSLongitudeRef: longitude_ref,
338+
_piexif.GPSIFD.GPSLongitude: degToDmsRational(longitude)
339+
})
340+
341+
if altitude != 0:
342+
gps_ifd.update({
343+
_piexif.GPSIFD.GPSAltitudeRef: 1,
344+
_piexif.GPSIFD.GPSAltitude: change_to_rational(round(altitude))
345+
})
346+
347+
gps_exif = {"GPS": gps_ifd}
348+
exif_dict.update(gps_exif)
349+
350+
try:
351+
_piexif.insert(_piexif.dump(exif_dict), file)
352+
except Exception as e:
353+
print("Couldn't insert geo exif!")
354+
# local variable 'new_value' referenced before assignment means that one of the GPS values is incorrect
355+
print(e)
267356

268357
# Fixes ALL metadata, takes just file and dir and figures it out
269358
def fix_metadata(dir, file):
@@ -282,12 +371,13 @@ def fix_metadata(dir, file):
282371
try:
283372
google_json = find_json_for_file(dir, file)
284373
date = get_date_str_from_json(google_json)
374+
set_file_geo_data(file, google_json)
285375
set_file_exif_date(file, date)
286376
set_creation_date_from_str(file, date)
287377
has_nice_date = True
288378
return
289379
except FileNotFoundError:
290-
print('Couldnt find json for file :/')
380+
print("Couldn't find json for file :/")
291381

292382
if has_nice_date:
293383
return
@@ -298,7 +388,6 @@ def fix_metadata(dir, file):
298388
set_creation_date_from_str(file, date)
299389
return True
300390

301-
302391
# PART 3: Copy all photos and videos to target folder
303392

304393
# Makes a new name like 'photo(1).jpg'
@@ -316,15 +405,13 @@ def new_name_if_exists(file_name, watch_for_duplicates=True):
316405
new_name = split[0] + '(' + str(i) + ')' + split[1]
317406
i += 1
318407

319-
320408
def copy_to_target(dir, file):
321409
if is_photo(file) or is_video(file):
322410
new_file = new_name_if_exists(FIXED_DIR + '/' + _os.path.basename(file),
323411
watch_for_duplicates=not args.keep_duplicates)
324412
_shutil.copy2(file, new_file)
325413
return True
326414

327-
328415
def copy_to_target_and_divide(dir, file):
329416
creation_date = _os.path.getmtime(file)
330417
date = _datetime.fromtimestamp(creation_date)
@@ -337,7 +424,6 @@ def copy_to_target_and_divide(dir, file):
337424
_shutil.copy2(file, new_file)
338425
return True
339426

340-
341427
if not args.keep_duplicates:
342428
print('=====================')
343429
print('Removing duplicates...')
@@ -383,5 +469,6 @@ def copy_to_target_and_divide(dir, file):
383469
print('Sooo... what now? You can see README.md for what nice G Photos alternatives I found and recommend')
384470
print('Have a nice day!')
385471

472+
386473
if __name__ == '__main__':
387474
main()

0 commit comments

Comments
 (0)