diff --git a/API_README.md b/API_README.md index b3fccb1e..8fe72c04 100644 --- a/API_README.md +++ b/API_README.md @@ -2050,10 +2050,11 @@ True ['Keyword1', 'Keyword2', 'Keyword3'] ``` -`ExifTool(filepath, exiftool=None)` +`ExifTool(filepath, exiftool=None, large_file_support=True)` * `filepath`: str, path to photo * `exiftool`: str, optional path to `exiftool`; if not provided, will look for `exiftool` in the system path +* `large_file_support`: bool, if True, enables large file support in exiftool (`-api largefilesupport=1`) #### ExifTool methods diff --git a/osxphotos/exiftool.py b/osxphotos/exiftool.py index ffa375ff..de7b12a4 100644 --- a/osxphotos/exiftool.py +++ b/osxphotos/exiftool.py @@ -6,7 +6,9 @@ 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 """ + import atexit +import contextlib import html import json import logging @@ -104,9 +106,12 @@ class _ExifToolProc: return cls.instance - def __init__(self, exiftool=None): + def __init__(self, exiftool=None, large_file_support=True): """construct _ExifToolProc singleton object or return instance of already created object - exiftool: optional path to exiftool binary (if not provided, will search path to find it) + + Args: + exiftool: optional path to exiftool binary (if not provided, will search path to find it) + large_file_support: if True, enables large file support (>4GB) via `-api largefilesupport=1` """ if hasattr(self, "_process_running") and self._process_running: @@ -119,16 +124,14 @@ class _ExifToolProc: return self._process_running = False self._exiftool = exiftool or get_exiftool_path() - self._start_proc() + self._start_proc(large_file_support=large_file_support) @property def process(self): """return the exiftool subprocess""" - if self._process_running: - return self._process - else: + if not self._process_running: self._start_proc() - return self._process + return self._process @property def pid(self): @@ -140,7 +143,7 @@ class _ExifToolProc: """return path to exiftool process""" return self._exiftool - def _start_proc(self): + def _start_proc(self, large_file_support): """start exiftool in batch mode""" if self._process_running: @@ -151,11 +154,13 @@ class _ExifToolProc: # make sure /usr/bin at start of path so exiftool can find xattr (see #636) env = os.environ.copy() env["PATH"] = f'/usr/bin/:{env["PATH"]}' + large_file_args = ["-api", "largefilesupport=1"] if large_file_support else [] self._process = subprocess.Popen( [ self._exiftool, "-stay_open", # keep process open in batch mode "True", # -stay_open=True, keep process open in batch mode + *large_file_args, "-@", # read command-line arguments from file "-", # read from stdin "-common_args", # specifies args common to all commands subsequently run @@ -179,13 +184,10 @@ class _ExifToolProc: if not self._process_running: return - try: + with contextlib.suppress(Exception): self._process.stdin.write(b"-stay_open\n") self._process.stdin.write(b"False\n") self._process.stdin.flush() - except Exception as e: - pass - try: self._process.communicate(timeout=5) except subprocess.TimeoutExpired: @@ -199,7 +201,14 @@ class _ExifToolProc: class ExifTool: """Basic exiftool interface for reading and writing EXIF tags""" - def __init__(self, filepath, exiftool=None, overwrite=True, flags=None): + def __init__( + self, + filepath, + exiftool=None, + overwrite=True, + flags=None, + large_file_support=True, + ): """Create ExifTool object Args: @@ -207,6 +216,7 @@ class ExifTool: exiftool: path to exiftool, if not specified will look in path overwrite: if True, will overwrite image file without creating backup, default=False flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F) + large_file_support: if True, enables large file support in exiftool (`-api largefilesupport=1`) Returns: ExifTool instance @@ -219,7 +229,9 @@ class ExifTool: self.error = None # if running as a context manager, self._context_mgr will be True self._context_mgr = False - self._exiftoolproc = _ExifToolProc(exiftool=exiftool) + self._exiftoolproc = _ExifToolProc( + exiftool=exiftool, large_file_support=large_file_support + ) self._read_exif() @property @@ -327,7 +339,7 @@ class ExifTool: commands = list(commands) commands.append("-overwrite_original") - filename = os.fsencode(self.file) if not no_file else b"" + filename = b"" if no_file else os.fsencode(self.file) if self.flags: # need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"] @@ -423,8 +435,7 @@ class ExifTool: def _read_exif(self): """read exif data from file""" - data = self.asdict() - self.data = {k: v for k, v in data.items()} + self.data = self.asdict().copy() def __str__(self): return f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}" diff --git a/tests/test_exiftool.py b/tests/test_exiftool.py index ce42390a..4545f2b8 100644 --- a/tests/test_exiftool.py +++ b/tests/test_exiftool.py @@ -557,3 +557,49 @@ def test_unescape_str(): assert quoted_str == QUOTED_JSON_STRING_UNESCAPED quoted_json = json.loads(quoted_str) assert quoted_json == QUOTED_JSON_LOADED + + +def test_large_file_support(): + """test large file support flag""" + # doesn't actually test against a large file, just that exiftool runs correctly + # See #722 + + 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) + + exif = osxphotos.exiftool.ExifTool(tempfile, large_file_support=True) + exif.setvalue("IPTC:Keywords", "test") + assert not exif.error + + exif._read_exif() + assert exif.data["IPTC:Keywords"] == "test" + + +def test_large_file_support_disabled(): + """test large file support flag disabled""" + # doesn't actually test against a large file, just that exiftool runs correctly + # See #722 + + 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) + + exif = osxphotos.exiftool.ExifTool(tempfile, large_file_support=False) + exif.setvalue("IPTC:Keywords", "test") + assert not exif.error + + exif._read_exif() + assert exif.data["IPTC:Keywords"] == "test"