From 4795f29022c5ef62abebd94e63cacadfe661e498 Mon Sep 17 00:00:00 2001 From: nkacoroski Date: Wed, 13 Nov 2019 19:26:20 -0800 Subject: [PATCH] Remove [] around cmd so package works on Windows. --- build/lib/pyexif/__init__.py | 381 ++++++++++++++++++ dist/pyexif-0.5.0-py3.6.egg | Bin 0 -> 10762 bytes dist/pyexif-0.5.0-py3.7.egg | Bin 0 -> 10739 bytes pyexif.egg-info/PKG-INFO | 25 ++ pyexif.egg-info/SOURCES.txt | 7 + pyexif.egg-info/dependency_links.txt | 1 + pyexif.egg-info/top_level.txt | 1 + .../.ipynb_checkpoints/__init__-checkpoint.py | 381 ++++++++++++++++++ pyexif/__init__.py | 10 +- 9 files changed, 801 insertions(+), 5 deletions(-) create mode 100644 build/lib/pyexif/__init__.py create mode 100644 dist/pyexif-0.5.0-py3.6.egg create mode 100644 dist/pyexif-0.5.0-py3.7.egg create mode 100644 pyexif.egg-info/PKG-INFO create mode 100644 pyexif.egg-info/SOURCES.txt create mode 100644 pyexif.egg-info/dependency_links.txt create mode 100644 pyexif.egg-info/top_level.txt create mode 100644 pyexif/.ipynb_checkpoints/__init__-checkpoint.py diff --git a/build/lib/pyexif/__init__.py b/build/lib/pyexif/__init__.py new file mode 100644 index 0000000..eaf61c7 --- /dev/null +++ b/build/lib/pyexif/__init__.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import datetime +import json +import os +import re +import six +import subprocess +import sys + + +def _install_exiftool_info(): + print(""" +Cannot find 'exiftool'. + +The ExifEditor class requires that the 'exiftool' command-line +utility is installed in order to work. Information on obtaining +this excellent utility can be found at: + +http://www.sno.phy.queensu.ca/~phil/exiftool/ +""") + + +def _runproc(cmd, fpath=None): + # if not _EXIFTOOL_INSTALLED: + # _install_exiftool_info() + # msg = "Running this class requires that exiftool is installed" + # raise RuntimeError(msg) + pipe = subprocess.PIPE + proc = subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe, + stderr=pipe, close_fds=True) + proc.wait() + err = proc.stderr.read() + if err: + # See if it's a damaged EXIF directory. If so, fix it and re-try + if (err.startswith(b"Warning: Bad ExifIFD directory") + and fpath is not None): + fixcmd = ('exiftool -overwrite_original_in_place -all= ' + '-tagsfromfile @ -all:all -unsafe "{fpath}"'.format( + **locals())) + try: + _runproc(fixcmd) + except RuntimeError: + # It will always raise a warning, so ignore it + pass + # Retry + return _runproc(cmd, fpath) + raise RuntimeError(err) + else: + return proc.stdout.read() + + +# Test that the exiftool is installed +_EXIFTOOL_INSTALLED = True +try: + out = _runproc("exiftool -ver") +except RuntimeError as e: + # If the tool is installed, the error should be 'File not found'. + # Otherwise, assume it isn't installed. + err = "{0}".format(e).strip() + if "File not found" not in err: + _EXIFTOOL_INSTALLED = False + _install_exiftool_info() + + + +class ExifEditor(object): + def __init__(self, photo=None, save_backup=False, extra_opts=None): + self.save_backup = save_backup + extra_opts = extra_opts or [] + if not save_backup: + extra_opts.append("-overwrite_original_in_place") + self._optExpr = " ".join(extra_opts) + if not isinstance(photo, six.string_types): + photo = photo.decode("utf-8") + self.photo = photo + # Tuples of (degrees, mirrored) + self._rotations = { + 0: (0, 0), + 1: (0, 0), + 2: (0, 1), + 3: (180, 0), + 4: (180, 1), + 5: (90, 1), + 6: (90, 0), + 7: (270, 1), + 8: (270, 0)} + self._invertedRotations = dict([[v, k] for k, v in self._rotations.items()]) + # DateTime patterns + self._datePattern = re.compile(r"\d{4}:[01]\d:[0-3]\d$") + self._dateTimePattern = re.compile(r"\d{4}:[01]\d:[0-3]\d [0-2]\d:[0-5]\d:[0-5]\d$") + self._badTagPat = re.compile(r"Warning: Tag '[^']+' does not exist") + + super(ExifEditor, self).__init__() + + + def rotateCCW(self, num=1, calc_only=False): + """Rotate left in 90 degree increments""" + return self._rotate(-90 * num, calc_only) + + + def rotateCW(self, num=1, calc_only=False): + """Rotate right in 90 degree increments""" + return self._rotate(90 * num, calc_only) + + + def getOrientation(self): + """Returns the current Orientation tag number.""" + return self.getTag("Orientation#", 1) + + + def _rotate(self, deg, calc_only=False): + currOrient = self.getOrientation() + currRot, currMirror = self._rotations[currOrient] + dummy, newRot = divmod(currRot + deg, 360) + currOrient = self.getOrientation() + currRot, currMirror = self._rotations[currOrient] + dummy, newRot = divmod(currRot + deg, 360) + newOrient = self._invertedRotations[(newRot, currMirror)] + if calc_only: + return newOrient + self.setOrientation(newOrient) + + + def mirrorVertically(self): + """Flips the image top to bottom.""" + # First, rotate 180 + currOrient = self.rotateCW(2, calc_only=True) + currRot, currMirror = self._rotations[currOrient] + newMirror = currMirror ^ 1 + newOrient = self._invertedRotations[(currRot, newMirror)] + self.setOrientation(newOrient) + + + def mirrorHorizontally(self): + """Flips the image left to right.""" + currOrient = self.getOrientation() + currRot, currMirror = self._rotations[currOrient] + newMirror = currMirror ^ 1 + newOrient = self._invertedRotations[(currRot, newMirror)] + self.setOrientation(newOrient) + + + def setOrientation(self, val): + """Orientation codes: + Rot Img + 1: 0 Normal + 2: 0 Mirrored + 3: 180 Normal + 4: 180 Mirrored + 5: +90 Mirrored + 6: +90 Normal + 7: -90 Mirrored + 8: -90 Normal + """ + cmd = """exiftool {self._optExpr} -Orientation#='{val}' "{self.photo}" """.format(**locals()) + _runproc(cmd, self.photo) + + + def addKeyword(self, kw): + """Add the passed string to the image's keyword tag, preserving existing keywords.""" + self.addKeywords([kw]) + + + def addKeywords(self, kws): + """Add the passed list of strings to the image's keyword tag, preserving + existing keywords. + """ + kws = ["-iptc:keywords+={0}".format(kw.replace(" ", r"\ ")) for kw in kws] + kwopt = " ".join(kws) + cmd = """exiftool {self._optExpr} {kwopt} "{self.photo}" """.format(**locals()) + _runproc(cmd, self.photo) + + + def getKeywords(self): + """Returns the current keywords for the image as a list.""" + ret = self.getTag("Keywords") + if not ret: + return [] + if isinstance(ret, six.string_types): + return [ret] + return sorted(ret) + + + def setKeywords(self, kws): + """Sets the image's keyword list to the passed list of strings. Any + existing keywords are overwritten. + """ + self.clearKeywords() + self.addKeywords(kws) + + + def clearKeywords(self): + """Removes all keywords from the image.""" + self.setTag("Keywords", "") + + + def clearKeyword(self, kw): + """Removes a single keyword from the image. If the keyword does not + exist, this call is a no-op. + """ + kws = self.getKeywords() + try: + kws.remove(kw) + except ValueError: + pass + self.setKeywords(kws) + + + def getTag(self, tag, default=None): + """Returns the value of the specified tag, or the default value + if the tag does not exist. + """ + cmd = """exiftool -j -d "%Y:%m:%d %H:%M:%S" -{tag} "{self.photo}" """.format(**locals()) + out = _runproc(cmd, self.photo) + if not isinstance(out, six.string_types): + out = out.decode("utf-8") + info = json.loads(out)[0] + ret = info.get(tag, default) + return ret + + + def getTags(self, just_names=False, include_empty=True): + """Returns a list of all the tags for the current image.""" + cmd = """exiftool -j -d "%Y:%m:%d %H:%M:%S" "{self.photo}" """.format(**locals()) + out = _runproc(cmd, self.photo) + if not isinstance(out, six.string_types): + out = out.decode("utf-8") + info = json.loads(out)[0] + if include_empty: + if just_names: + ret = list(info.keys()) + else: + ret = list(info.items()) + else: + # Exclude those tags with empty values + if just_names: + ret = [tag for tag in info.keys() if info.get(tag)] + else: + ret = [(tag, val) for tag, val in info.items() if val] + return sorted(ret) + + + def getDictTags(self, include_empty=True): + """Returns a dict of all the tags for the current image, with the tag + name as the key and the tag value as the value. + """ + tags = self.getTags(include_empty=include_empty) + return {k:v for k, v in tags} + + + def setTag(self, tag, val): + """Sets the specified tag to the passed value. You can set multiple values + for the same tag by passing those values in as a list. + """ + if not isinstance(val, (list, tuple)): + val = [val] + vallist = ['-{0}="{1}"'.format(tag, + v.replace('"', '\\"') if isinstance(v, six.string_types) else v) for v in val] + valstr = " ".join(vallist) + cmd = """exiftool {self._optExpr} {valstr} "{self.photo}" """.format(**locals()) + try: + out = _runproc(cmd, self.photo) + except RuntimeError as e: + err = "{0}".format(e).strip() + if self._badTagPat.match(err): + print("Tag '{tag}' is invalid.".format(**locals())) + else: + raise + + + def setTags(self, tags_dict): + """Sets the specified tags_dict ({tag: val, tag_n: val_n}) tag value combinations. + Used to set more than one tag, val value in a single call. + """ + if not isinstance(tags_dict, dict): + raise TypeError('tags_dict is not instance of dict') + vallist = [] + for tag in tags_dict: + val = tags_dict[tag] + # escape double quotes in case of string type + if isinstance(val, six.string_types): + val = val.replace('"', '\\"') + vallist.append('-{0}="{1}"'.format(tag, val)) + valstr = " ".join(vallist) + cmd = """exiftool {self._optExpr} {valstr} "{self.photo}" """.format(**locals()) + try: + out = _runproc(cmd, self.photo) + except RuntimeError as e: + err = "{0}".format(e).strip() + if self._badTagPat.match(err): + print("Tag '{tag}' is invalid.".format(**locals())) + else: + raise + + + def getOriginalDateTime(self): + """Get the image's original date/time value (i.e., when the picture + was 'taken'). + """ + return self._getDateTimeField("DateTimeOriginal") + + + def setOriginalDateTime(self, dttm=None): + """Set the image's original date/time (i.e., when the picture + was 'taken') to the passed value. If no value is passed, set + it to the current datetime. + """ + self._setDateTimeField("DateTimeOriginal", dttm) + + + def getModificationDateTime(self): + """Get the image's modification date/time value.""" + return self._getDateTimeField("FileModifyDate") + + + def setModificationDateTime(self, dttm=None): + """Set the image's modification date/time to the passed value. + If no value is passed, set it to the current datetime (i.e., + like 'touch'. + """ + self._setDateTimeField("FileModifyDate", dttm) + + + def _getDateTimeField(self, fld): + """Generic getter for datetime values.""" + # Convert to string format if needed +# if isinstance(dttm, (datetime.datetime, datetime.date)): +# dtstring = dttm.strftime("%Y:%m:%d %H:%M:%S") +# else: +# dtstring = self._formatDateTime(dttm) + ret = self.getTag(fld) + if ret is not None: + # It will be a string in exif std datetime format + ret = datetime.datetime.strptime(ret, "%Y:%m:%d %H:%M:%S") + return ret + + + def _setDateTimeField(self, fld, dttm): + """Generic setter for datetime values.""" + if dttm is None: + dttm = datetime.datetime.now() + # Convert to string format if needed + if isinstance(dttm, (datetime.datetime, datetime.date)): + dtstring = dttm.strftime("%Y:%m:%d %H:%M:%S") + else: + dtstring = self._formatDateTime(dttm) + cmd = """exiftool {self._optExpr} -{fld}='{dtstring}' "{self.photo}" """.format(**locals()) + _runproc(cmd, self.photo) + + + def _formatDateTime(self, dt): + """Accepts a string representation of a date or datetime, + and returns a string correctly formatted for EXIF datetimes. + """ + if self._datePattern.match(dt): + # Add the time portion + return "{0} 00:00:00".format(dt) + elif self._dateTimePattern.match(dt): + # Leave as-is + return dt + else: + raise ValueError("Incorrect datetime value '{0}' received".format(dt)) + + +def usage(): + print(""" +To use this module, create an instance of the ExifEditor class, passing +in a path to the image to be handled. You may also pass in whether you +want the program to automatically keep a backup of your original photo +(default=False). If a backup is created, it will be in the same location +as the original, with "_ORIGINAL" appended to the file name. + +Once you have an editor instance, you call its methods to get information +about the image, or to modify the image's metadata. +""") + + +if __name__ == "__main__": + usage() diff --git a/dist/pyexif-0.5.0-py3.6.egg b/dist/pyexif-0.5.0-py3.6.egg new file mode 100644 index 0000000000000000000000000000000000000000..70a69670ff5f8e1d00f925fd40832b148f533faa GIT binary patch literal 10762 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 literal 0 HcmV?d00001 diff --git a/dist/pyexif-0.5.0-py3.7.egg b/dist/pyexif-0.5.0-py3.7.egg new file mode 100644 index 0000000000000000000000000000000000000000..3b53c916803a4ae520c5bd968a039e99af67f476 GIT binary patch literal 10739 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 Up`ia_Vg8(le~h>X>c5`;AJPlMApigX literal 0 HcmV?d00001 diff --git a/pyexif.egg-info/PKG-INFO b/pyexif.egg-info/PKG-INFO new file mode 100644 index 0000000..010e039 --- /dev/null +++ b/pyexif.egg-info/PKG-INFO @@ -0,0 +1,25 @@ +Metadata-Version: 1.1 +Name: pyexif +Version: 0.5.0 +Summary: Python module to read/write EXIF image data +Home-page: https://github.com/EdLeafe/pyexif +Author: Ed Leafe +Author-email: ed@leafe.com +License: Python Software Foundation License +Description: Python module for working with EXIF image data. + + It does its work mainly though the command-line "exiftool", which is required + for this module to work. Information on obtaining and installing exiftool are + in the README file. + +Keywords: exif image metadata photo +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Topic :: Utilities +Classifier: Operating System :: OS Independent +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Natural Language :: English +Classifier: Topic :: Multimedia :: Graphics diff --git a/pyexif.egg-info/SOURCES.txt b/pyexif.egg-info/SOURCES.txt new file mode 100644 index 0000000..71838ce --- /dev/null +++ b/pyexif.egg-info/SOURCES.txt @@ -0,0 +1,7 @@ +README +setup.py +pyexif/__init__.py +pyexif.egg-info/PKG-INFO +pyexif.egg-info/SOURCES.txt +pyexif.egg-info/dependency_links.txt +pyexif.egg-info/top_level.txt \ No newline at end of file diff --git a/pyexif.egg-info/dependency_links.txt b/pyexif.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pyexif.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pyexif.egg-info/top_level.txt b/pyexif.egg-info/top_level.txt new file mode 100644 index 0000000..e3ce0d2 --- /dev/null +++ b/pyexif.egg-info/top_level.txt @@ -0,0 +1 @@ +pyexif diff --git a/pyexif/.ipynb_checkpoints/__init__-checkpoint.py b/pyexif/.ipynb_checkpoints/__init__-checkpoint.py new file mode 100644 index 0000000..51a866d --- /dev/null +++ b/pyexif/.ipynb_checkpoints/__init__-checkpoint.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import datetime +import json +import os +import re +import six +import subprocess +import sys + + +def _install_exiftool_info(): + print(""" +Cannot find 'exiftool'. + +The ExifEditor class requires that the 'exiftool' command-line +utility is installed in order to work. Information on obtaining +this excellent utility can be found at: + +http://www.sno.phy.queensu.ca/~phil/exiftool/ +""") + + +def _runproc(cmd, fpath=None): + # if not _EXIFTOOL_INSTALLED: + # _install_exiftool_info() + # msg = "Running this class requires that exiftool is installed" + # raise RuntimeError(msg) + pipe = subprocess.PIPE + proc = subprocess.Popen(cmd, shell=True, stdin=pipe, stdout=pipe, + stderr=pipe, close_fds=True) + proc.wait() + err = proc.stderr.read() + if err: + # See if it's a damaged EXIF directory. If so, fix it and re-try + if (err.startswith(b"Warning: Bad ExifIFD directory") + and fpath is not None): + fixcmd = ('exiftool -overwrite_original_in_place -all= ' + '-tagsfromfile @ -all:all -unsafe "{fpath}"'.format( + **locals())) + try: + _runproc(fixcmd) + except RuntimeError: + # It will always raise a warning, so ignore it + pass + # Retry + return _runproc(cmd, fpath) + raise RuntimeError(err) + else: + return proc.stdout.read() + + +# Test that the exiftool is installed +_EXIFTOOL_INSTALLED = True +try: + out = _runproc("exiftool -ver") +except RuntimeError as e: + # If the tool is installed, the error should be 'File not found'. + # Otherwise, assume it isn't installed. + err = "{0}".format(e).strip() + if "File not found" not in err: + _EXIFTOOL_INSTALLED = False + _install_exiftool_info() + + + +class ExifEditor(object): + def __init__(self, photo=None, save_backup=False, extra_opts=None): + self.save_backup = save_backup + extra_opts = extra_opts or [] + if not save_backup: + extra_opts.append("-overwrite_original_in_place") + self._optExpr = " ".join(extra_opts) + if not isinstance(photo, six.string_types): + photo = photo.decode("utf-8") + self.photo = photo + # Tuples of (degrees, mirrored) + self._rotations = { + 0: (0, 0), + 1: (0, 0), + 2: (0, 1), + 3: (180, 0), + 4: (180, 1), + 5: (90, 1), + 6: (90, 0), + 7: (270, 1), + 8: (270, 0)} + self._invertedRotations = dict([[v, k] for k, v in self._rotations.items()]) + # DateTime patterns + self._datePattern = re.compile(r"\d{4}:[01]\d:[0-3]\d$") + self._dateTimePattern = re.compile(r"\d{4}:[01]\d:[0-3]\d [0-2]\d:[0-5]\d:[0-5]\d$") + self._badTagPat = re.compile(r"Warning: Tag '[^']+' does not exist") + + super(ExifEditor, self).__init__() + + + def rotateCCW(self, num=1, calc_only=False): + """Rotate left in 90 degree increments""" + return self._rotate(-90 * num, calc_only) + + + def rotateCW(self, num=1, calc_only=False): + """Rotate right in 90 degree increments""" + return self._rotate(90 * num, calc_only) + + + def getOrientation(self): + """Returns the current Orientation tag number.""" + return self.getTag("Orientation#", 1) + + + def _rotate(self, deg, calc_only=False): + currOrient = self.getOrientation() + currRot, currMirror = self._rotations[currOrient] + dummy, newRot = divmod(currRot + deg, 360) + currOrient = self.getOrientation() + currRot, currMirror = self._rotations[currOrient] + dummy, newRot = divmod(currRot + deg, 360) + newOrient = self._invertedRotations[(newRot, currMirror)] + if calc_only: + return newOrient + self.setOrientation(newOrient) + + + def mirrorVertically(self): + """Flips the image top to bottom.""" + # First, rotate 180 + currOrient = self.rotateCW(2, calc_only=True) + currRot, currMirror = self._rotations[currOrient] + newMirror = currMirror ^ 1 + newOrient = self._invertedRotations[(currRot, newMirror)] + self.setOrientation(newOrient) + + + def mirrorHorizontally(self): + """Flips the image left to right.""" + currOrient = self.getOrientation() + currRot, currMirror = self._rotations[currOrient] + newMirror = currMirror ^ 1 + newOrient = self._invertedRotations[(currRot, newMirror)] + self.setOrientation(newOrient) + + + def setOrientation(self, val): + """Orientation codes: + Rot Img + 1: 0 Normal + 2: 0 Mirrored + 3: 180 Normal + 4: 180 Mirrored + 5: +90 Mirrored + 6: +90 Normal + 7: -90 Mirrored + 8: -90 Normal + """ + cmd = """exiftool {self._optExpr} -Orientation#='{val}' "{self.photo}" """.format(**locals()) + _runproc(cmd, self.photo) + + + def addKeyword(self, kw): + """Add the passed string to the image's keyword tag, preserving existing keywords.""" + self.addKeywords([kw]) + + + def addKeywords(self, kws): + """Add the passed list of strings to the image's keyword tag, preserving + existing keywords. + """ + kws = ["-iptc:keywords+={0}".format(kw.replace(" ", r"\ ")) for kw in kws] + kwopt = " ".join(kws) + cmd = """exiftool {self._optExpr} {kwopt} "{self.photo}" """.format(**locals()) + _runproc(cmd, self.photo) + + + def getKeywords(self): + """Returns the current keywords for the image as a list.""" + ret = self.getTag("Keywords") + if not ret: + return [] + if isinstance(ret, six.string_types): + return [ret] + return sorted(ret) + + + def setKeywords(self, kws): + """Sets the image's keyword list to the passed list of strings. Any + existing keywords are overwritten. + """ + self.clearKeywords() + self.addKeywords(kws) + + + def clearKeywords(self): + """Removes all keywords from the image.""" + self.setTag("Keywords", "") + + + def clearKeyword(self, kw): + """Removes a single keyword from the image. If the keyword does not + exist, this call is a no-op. + """ + kws = self.getKeywords() + try: + kws.remove(kw) + except ValueError: + pass + self.setKeywords(kws) + + + def getTag(self, tag, default=None): + """Returns the value of the specified tag, or the default value + if the tag does not exist. + """ + cmd = """exiftool -j -d "%Y:%m:%d %H:%M:%S" -{tag} "{self.photo}" """.format(**locals()) + out = _runproc(cmd, self.photo) + if not isinstance(out, six.string_types): + out = out.decode("utf-8") + info = json.loads(out)[0] + ret = info.get(tag, default) + return ret + + + def getTags(self, just_names=False, include_empty=True): + """Returns a list of all the tags for the current image.""" + cmd = """exiftool -j -d "%Y:%m:%d %H:%M:%S" "{self.photo}" """.format(**locals()) + out = _runproc(cmd, self.photo) + if not isinstance(out, six.string_types): + out = out.decode("utf-8") + info = json.loads(out)[0] + if include_empty: + if just_names: + ret = list(info.keys()) + else: + ret = list(info.items()) + else: + # Exclude those tags with empty values + if just_names: + ret = [tag for tag in info.keys() if info.get(tag)] + else: + ret = [(tag, val) for tag, val in info.items() if val] + return sorted(ret) + + + def getDictTags(self, include_empty=True): + """Returns a dict of all the tags for the current image, with the tag + name as the key and the tag value as the value. + """ + tags = self.getTags(include_empty=include_empty) + return {k:v for k, v in tags} + + + def setTag(self, tag, val): + """Sets the specified tag to the passed value. You can set multiple values + for the same tag by passing those values in as a list. + """ + if not isinstance(val, (list, tuple)): + val = [val] + vallist = ['-{0}="{1}"'.format(tag, + v.replace('"', '\\"') if isinstance(v, six.string_types) else v) for v in val] + valstr = " ".join(vallist) + cmd = """exiftool {self._optExpr} {valstr} "{self.photo}" """.format(**locals()) + try: + out = _runproc(cmd, self.photo) + except RuntimeError as e: + err = "{0}".format(e).strip() + if self._badTagPat.match(err): + print("Tag '{tag}' is invalid.".format(**locals())) + else: + raise + + + def setTags(self, tags_dict): + """Sets the specified tags_dict ({tag: val, tag_n: val_n}) tag value combinations. + Used to set more than one tag, val value in a single call. + """ + if not isinstance(tags_dict, dict): + raise TypeError('tags_dict is not instance of dict') + vallist = [] + for tag in tags_dict: + val = tags_dict[tag] + # escape double quotes in case of string type + if isinstance(val, six.string_types): + val = val.replace('"', '\\"') + vallist.append('-{0}="{1}"'.format(tag, val)) + valstr = " ".join(vallist) + cmd = """exiftool {self._optExpr} {valstr} "{self.photo}" """.format(**locals()) + try: + out = _runproc(cmd, self.photo) + except RuntimeError as e: + err = "{0}".format(e).strip() + if self._badTagPat.match(err): + print("Tag '{tag}' is invalid.".format(**locals())) + else: + raise + + + def getOriginalDateTime(self): + """Get the image's original date/time value (i.e., when the picture + was 'taken'). + """ + return self._getDateTimeField("DateTimeOriginal") + + + def setOriginalDateTime(self, dttm=None): + """Set the image's original date/time (i.e., when the picture + was 'taken') to the passed value. If no value is passed, set + it to the current datetime. + """ + self._setDateTimeField("DateTimeOriginal", dttm) + + + def getModificationDateTime(self): + """Get the image's modification date/time value.""" + return self._getDateTimeField("FileModifyDate") + + + def setModificationDateTime(self, dttm=None): + """Set the image's modification date/time to the passed value. + If no value is passed, set it to the current datetime (i.e., + like 'touch'. + """ + self._setDateTimeField("FileModifyDate", dttm) + + + def _getDateTimeField(self, fld): + """Generic getter for datetime values.""" + # Convert to string format if needed +# if isinstance(dttm, (datetime.datetime, datetime.date)): +# dtstring = dttm.strftime("%Y:%m:%d %H:%M:%S") +# else: +# dtstring = self._formatDateTime(dttm) + ret = self.getTag(fld) + if ret is not None: + # It will be a string in exif std datetime format + ret = datetime.datetime.strptime(ret, "%Y:%m:%d %H:%M:%S") + return ret + + + def _setDateTimeField(self, fld, dttm): + """Generic setter for datetime values.""" + if dttm is None: + dttm = datetime.datetime.now() + # Convert to string format if needed + if isinstance(dttm, (datetime.datetime, datetime.date)): + dtstring = dttm.strftime("%Y:%m:%d %H:%M:%S") + else: + dtstring = self._formatDateTime(dttm) + cmd = """exiftool {self._optExpr} -{fld}='{dtstring}' "{self.photo}" """.format(**locals()) + _runproc(cmd, self.photo) + + + def _formatDateTime(self, dt): + """Accepts a string representation of a date or datetime, + and returns a string correctly formatted for EXIF datetimes. + """ + if self._datePattern.match(dt): + # Add the time portion + return "{0} 00:00:00".format(dt) + elif self._dateTimePattern.match(dt): + # Leave as-is + return dt + else: + raise ValueError("Incorrect datetime value '{0}' received".format(dt)) + + +def usage(): + print(""" +To use this module, create an instance of the ExifEditor class, passing +in a path to the image to be handled. You may also pass in whether you +want the program to automatically keep a backup of your original photo +(default=False). If a backup is created, it will be in the same location +as the original, with "_ORIGINAL" appended to the file name. + +Once you have an editor instance, you call its methods to get information +about the image, or to modify the image's metadata. +""") + + +if __name__ == "__main__": + usage() diff --git a/pyexif/__init__.py b/pyexif/__init__.py index 85945bc..51a866d 100644 --- a/pyexif/__init__.py +++ b/pyexif/__init__.py @@ -23,12 +23,12 @@ def _install_exiftool_info(): def _runproc(cmd, fpath=None): - if not _EXIFTOOL_INSTALLED: - _install_exiftool_info() - msg = "Running this class requires that exiftool is installed" - raise RuntimeError(msg) + # if not _EXIFTOOL_INSTALLED: + # _install_exiftool_info() + # msg = "Running this class requires that exiftool is installed" + # raise RuntimeError(msg) pipe = subprocess.PIPE - proc = subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe, + proc = subprocess.Popen(cmd, shell=True, stdin=pipe, stdout=pipe, stderr=pipe, close_fds=True) proc.wait() err = proc.stderr.read()