Guest User

Untitled

a guest
Apr 23rd, 2018
79
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.15 KB | None | 0 0
  1. '''
  2. shift_exif_timezone.py - Convert local timestamps in Exif to UTC timestamps
  3.  
  4. Timestamps in Exif records are not timezone-aware. They are naive format. So
  5. there will be protocol mismatch; some camera writes "local" timestamp in Exif,
  6. but some software recognizes it as "UTC" on the other hand. To resolve the
  7. situation, this snippet converts timestamps in '0th' and 'Exif' IFD to UTC
  8. timestamps.
  9.  
  10. Note: GPS always uses UTC timestamps, so timestamps in 'GPS' IFD are not
  11. problematic.
  12. '''
  13. import argparse
  14. import datetime
  15. import pathlib
  16. import re
  17. import shutil
  18. import subprocess
  19.  
  20. import piexif
  21. import pytz
  22. import tzlocal
  23.  
  24.  
  25. def shift_timestamp(local_timestamp_obj, from_zone=None):
  26. '''
  27. Convert local zone `datetime.datetime` object to UTC zone
  28. `datetime.datetime` object.
  29.  
  30. :param local_timestamp_obj: Naive `datetime.datetime` object that
  31. should be treated as local zone.
  32. :param from_zone: Timezone object for the given local timestamp. Can
  33. be None to use system locale instead.
  34. :retvap: Converted UTC-aware `datetime.datetime` object.
  35. '''
  36. if from_zone is None:
  37. from_zone = tzlocal.get_localzone()
  38.  
  39. return from_zone.localize(local_timestamp_obj).astimezone(pytz.utc)
  40.  
  41.  
  42. def shift_exif_timestamp(local_timestamp, from_zone=None):
  43. '''
  44. Convert local Exif timestamp bytes to UTC timestamp bytes.
  45.  
  46. :param local_timestamp: Exif-format timestamp (yyyy:mm:dd hh:mm:ss),
  47. in bytes object.
  48. :param from_zone: Timezone object for the given local timestamp. Can
  49. be None to use system locale instead.
  50. :retvap: Converted UTC timestamp in byts object.
  51. '''
  52. FORMAT = '%Y:%m:%d %H:%M:%S'
  53.  
  54. return shift_timestamp(
  55. datetime.datetime.strptime(local_timestamp.decode('utf-8'), FORMAT),
  56. from_zone=from_zone
  57. ).strftime(FORMAT).encode('utf-8')
  58.  
  59.  
  60. def shift_mov_timestamp(local_timestamp, from_zone=None):
  61. '''
  62. Convert local MOV timestamp string to UTC timestamp string.
  63.  
  64. :param local_timestamp: MOV-format timestamp (yyyy-mm-dd hh:mm:ss),
  65. in str object.
  66. :param from_zone: Timezone object for the given local timestamp. Can
  67. be None to use system locale instead.
  68. :retvap: Converted UTC timestamp in str object.
  69. '''
  70. FORMAT = '%Y-%m-%d %H:%M:%S'
  71.  
  72. return shift_timestamp(
  73. datetime.datetime.strptime(local_timestamp, FORMAT),
  74. from_zone=from_zone
  75. ).strftime(FORMAT)
  76.  
  77.  
  78. if __name__ == '__main__':
  79. # Parse command line arguments.
  80. parser = argparse.ArgumentParser(
  81. description=(
  82. 'Convert local timestamps in Exif to UTC timestamps. '
  83. '"*.JPG" and "*.MOV" files are supported '
  84. '(ffmpeg is required to convert "*.MOV" files). '
  85. 'Note: "1st" and "thumbnail" IFDs will be removed because they '
  86. 'can produce corrupted output files.'
  87. )
  88. )
  89. parser.add_argument(
  90. '-z', '--zone',
  91. type=str,
  92. help=(
  93. 'tz database timezone name of source timestamps. Omit to use '
  94. 'system locale timezone.'
  95. )
  96. )
  97. parser.add_argument(
  98. 'target_dir',
  99. type=str,
  100. help=(
  101. 'Path to the directory that contains target *.JPG / *.MOV files.'
  102. )
  103. )
  104. parser.add_argument(
  105. 'output_dir',
  106. type=str,
  107. nargs='?',
  108. help=(
  109. 'Path to the output directory. If omitted, "output" dirctory '
  110. 'will be created under the target directory.'
  111. )
  112. )
  113.  
  114. args = parser.parse_args()
  115.  
  116. # Wrap arguments with appropriate objects.
  117. if args.zone is not None:
  118. args.zone = pytz.timezone(args.zone)
  119.  
  120. args.target_dir = pathlib.Path(args.target_dir).resolve()
  121.  
  122. if not args.target_dir.is_dir():
  123. raise RuntimeError('Target directory is not found.')
  124.  
  125. if args.output_dir is None:
  126. args.output_dir = args.target_dir / 'output'
  127. else:
  128. args.output_dir = pathlib.Path(args.output_dir).resolve()
  129.  
  130. args.output_dir.mkdir(parents=False, exist_ok=True)
  131.  
  132. print('Target:', str(args.target_dir))
  133. print('Output:', str(args.output_dir))
  134.  
  135. # Convert *.JPG files.
  136. for in_path in args.target_dir.glob('*.jpg'):
  137. out_path = args.output_dir / in_path.name
  138. print('Processing:', in_path.name)
  139.  
  140. exif = piexif.load(str(in_path))
  141.  
  142. for ifd_name, ifd_value in (
  143. ('0th', piexif.ImageIFD.DateTime),
  144. ('Exif', piexif.ExifIFD.DateTimeOriginal),
  145. ('Exif', piexif.ExifIFD.DateTimeDigitized),
  146. ):
  147. if ifd_name in exif and ifd_value in exif[ifd_name]:
  148. exif[ifd_name][ifd_value] = shift_exif_timestamp(
  149. exif[ifd_name][ifd_value],
  150. args.zone
  151. )
  152.  
  153. del exif['1st']
  154. del exif['thumbnail']
  155.  
  156. shutil.copy(in_path, out_path)
  157. piexif.insert(piexif.dump(exif), str(out_path))
  158.  
  159. print(' OK')
  160.  
  161. # Convert *.MOV files
  162. for in_path in args.target_dir.glob('*.mov'):
  163. out_path = args.output_dir / in_path.name
  164. print('Processing:', in_path.name)
  165.  
  166. r = subprocess.run(
  167. ['ffprobe', str(in_path)],
  168. stdout=subprocess.PIPE,
  169. stderr=subprocess.PIPE,
  170. check=True
  171. )
  172. found_stamp = None
  173.  
  174. for line in r.stderr.decode('utf-8').split('\n'):
  175. if line.strip().startswith('creation_time'):
  176. found_stamp = line.split(':', 1)[1].strip()
  177. break
  178.  
  179. if found_stamp is not None:
  180. found_stamp = shift_mov_timestamp(found_stamp, args.zone)
  181.  
  182. subprocess.run(
  183. [
  184. 'ffmpeg',
  185. '-i',
  186. str(in_path),
  187. '-acodec',
  188. 'copy',
  189. '-vcodec',
  190. 'copy',
  191. '-metadata',
  192. 'creation_time=%s' % found_stamp,
  193. str(out_path),
  194. ],
  195. check=True,
  196. stdout=subprocess.PIPE,
  197. stderr=subprocess.PIPE
  198. )
  199. else:
  200. shutil.copy(in_path, out_path)
  201.  
  202. print(' OK')
Add Comment
Please, Sign In to add comment