Fix for large files and exiftool, #722 (#723)

This commit is contained in:
Rhet Turnbull 2022-07-01 08:03:21 -07:00 committed by GitHub
parent c20a3994c0
commit 292fdf3c74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 18 deletions

View File

@ -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

View File

@ -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}"

View File

@ -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"