diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 1cb6f908..5c53d9e1 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,4 +1,4 @@ """ version info """ -__version__ = "0.36.6" +__version__ = "0.36.7" diff --git a/osxphotos/exiftool.py b/osxphotos/exiftool.py index b613d4d4..3cdb27af 100644 --- a/osxphotos/exiftool.py +++ b/osxphotos/exiftool.py @@ -2,6 +2,7 @@ I rolled my own for following reasons: 1. I wanted something under MIT license (best alternative was licensed under GPL/BSD) 2. I wanted singleton behavior so only a single exiftool process was ever running + 3. When used as a context manager, I wanted the operations to batch until exiting the context (improved performance) If these aren't important to you, I highly recommend you use Sven Marnach's excellent pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """ @@ -10,10 +11,8 @@ import logging import os import shutil import subprocess -import sys from functools import lru_cache # pylint: disable=syntax-error -from .utils import _debug # exiftool -stay_open commands outputs this EOF marker after command is run EXIFTOOL_STAYOPEN_EOF = "{ready}" @@ -23,9 +22,7 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF) @lru_cache(maxsize=1) def get_exiftool_path(): """ return path of exiftool, cache result """ - exiftool_path = shutil.which('exiftool') - if _debug(): - logging.debug("exiftool path = %s" % (exiftool_path)) + exiftool_path = shutil.which("exiftool") if exiftool_path: return exiftool_path.rstrip() else: @@ -59,7 +56,7 @@ class _ExifToolProc: ) return - self._exiftool = exiftool if exiftool else get_exiftool_path() + self._exiftool = exiftool or get_exiftool_path() self._process_running = False self._start_proc() @@ -98,7 +95,7 @@ class _ExifToolProc: "-", # read from stdin "-common_args", # specifies args common to all commands subsequently run "-n", # no print conversion (e.g. print tag values in machine readable format) - "-P", # Preserve file modification date/time (possible interfere w/ --touch-file) + "-P", # Preserve file modification date/time "-G", # print group name for each tag ], stdin=subprocess.PIPE, @@ -143,6 +140,8 @@ class ExifTool: self.file = filepath self.overwrite = overwrite self.data = {} + # if running as a context manager, self._context_mgr will be True + self._context_mgr = False self._exiftoolproc = _ExifToolProc(exiftool=exiftool) self._process = self._exiftoolproc.process self._read_exif() @@ -154,9 +153,12 @@ class ExifTool: if value is None: value = "" command = [f"-{tag}={value}"] - if self.overwrite: + if self.overwrite and not self._context_mgr: command.append("-overwrite_original") - self.run_commands(*command) + if self._context_mgr: + self._commands.extend(command) + else: + self.run_commands(*command) def addvalues(self, tag, *values): """ Add one or more value(s) to tag @@ -178,10 +180,12 @@ class ExifTool: raise ValueError("Can't add None value to tag") command.append(f"-{tag}+={value}") - if self.overwrite: + if self.overwrite and not self._context_mgr: command.append("-overwrite_original") - if command: + if self._context_mgr: + self._commands.extend(command) + else: self.run_commands(*command) def run_commands(self, *commands, no_file=False): @@ -195,6 +199,10 @@ class ExifTool: if not commands: raise TypeError("must provide one or more command to run") + if self._context_mgr and self.overwrite: + commands = list(commands) + commands.append("-overwrite_original") + filename = os.fsencode(self.file) if not no_file else b"" command_str = ( b"\n".join([c.encode("utf-8") for c in commands]) @@ -204,9 +212,6 @@ class ExifTool: + b"-execute\n" ) - if _debug(): - logging.debug(command_str) - # send the command self._process.stdin.write(command_str) self._process.stdin.flush() @@ -250,3 +255,14 @@ class ExifTool: def __str__(self): return f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}" + + def __enter__(self): + self._context_mgr = True + self._commands = [] + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type: + return False + elif self._commands: + self.run_commands(*self._commands) diff --git a/osxphotos/photoinfo/_photoinfo_export.py b/osxphotos/photoinfo/_photoinfo_export.py index 4bb0fd05..61f00d74 100644 --- a/osxphotos/photoinfo/_photoinfo_export.py +++ b/osxphotos/photoinfo/_photoinfo_export.py @@ -1019,7 +1019,6 @@ def _write_exif_data( """ if not os.path.exists(filepath): raise FileNotFoundError(f"Could not find file {filepath}") - exiftool = ExifTool(filepath) exif_info = self._exiftool_dict( use_albums_as_keywords=use_albums_as_keywords, use_persons_as_keywords=use_persons_as_keywords, @@ -1027,17 +1026,16 @@ def _write_exif_data( description_template=description_template, ignore_date_modified=ignore_date_modified, ) - for exiftag, val in exif_info.items(): - if exiftag == "_CreatedBy": - continue - if type(val) == list: - # more than one, set first value the add additional values - exiftool.setvalue(exiftag, val.pop(0)) - if val: - # add any remaining items - exiftool.addvalues(exiftag, *val) - else: - exiftool.setvalue(exiftag, val) + + with ExifTool(filepath) as exiftool: + for exiftag, val in exif_info.items(): + if exiftag == "_CreatedBy": + continue + elif type(val) == list: + for v in val: + exiftool.setvalue(exiftag, v) + else: + exiftool.setvalue(exiftag, val) def _exiftool_dict( diff --git a/tests/test_exiftool.py b/tests/test_exiftool.py index ed1d8b15..073458a7 100644 --- a/tests/test_exiftool.py +++ b/tests/test_exiftool.py @@ -107,6 +107,30 @@ def test_setvalue_1(): assert exif.data["IPTC:Keywords"] == "test" +def test_setvalue_context_manager(): + # test setting a tag value as context manager + import os.path + import tempfile + import osxphotos.exiftool + from osxphotos.fileutil import FileUtil + + tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_") + tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD)) + FileUtil.copy(TEST_FILE_ONE_KEYWORD, tempfile) + + with osxphotos.exiftool.ExifTool(tempfile) as exif: + exif.setvalue("IPTC:Keywords", "test1") + exif.setvalue("IPTC:Keywords", "test2") + exif.setvalue("XMP:Title", "title") + exif.setvalue("XMP:Subject", "subject") + + exif2 = osxphotos.exiftool.ExifTool(tempfile) + exif2._read_exif() + assert sorted(exif2.data["IPTC:Keywords"]) == ["test1", "test2"] + assert exif2.data["XMP:Title"] == "title" + assert exif2.data["XMP:Subject"] == "subject" + + def test_clear_value(): # test clearing a tag value import os.path