Added --exiftool-option to CLI, closes #298

This commit is contained in:
Rhet Turnbull 2020-12-21 07:32:38 -08:00
parent aca19f4063
commit a4bbb6492d
8 changed files with 137 additions and 13 deletions

View File

@ -342,6 +342,16 @@ Options:
(see also --ignore-date-modified);
QuickTime:GPSCoordinates;
UserData:GPSCoordinates.
--exiftool-option OPTION Optional flag/option to pass to exiftool
when using --exiftool. For example,
--exiftool-option '-m' to ignore minor
warnings. Specify these as you would on the
exiftool command line. See exiftool docs at
https://exiftool.org/exiftool_pod.html for
full list of options. More than one option
may be specified with by repeating the
option, e.g. --exiftool-option '-m'
--exiftool-option '-F'.
--ignore-date-modified If used with --exiftool or --sidecar, will
ignore the photo modification date and set
EXIF:ModifyDate to EXIF:DateTimeOriginal;

View File

@ -1366,6 +1366,17 @@ def query(
"(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); "
"QuickTime:GPSCoordinates; UserData:GPSCoordinates.",
)
@click.option(
"--exiftool-option",
multiple=True,
metavar="OPTION",
help="Optional flag/option to pass to exiftool when using --exiftool. "
"For example, --exiftool-option '-m' to ignore minor warnings. "
"Specify these as you would on the exiftool command line. "
"See exiftool docs at https://exiftool.org/exiftool_pod.html for full list of options. "
"More than one option may be specified with by repeating the option, e.g. "
"--exiftool-option '-m' --exiftool-option '-F'. ",
)
@click.option(
"--ignore-date-modified",
is_flag=True,
@ -1550,6 +1561,7 @@ def export(
download_missing,
dest,
exiftool,
exiftool_option,
ignore_date_modified,
portrait,
not_portrait,
@ -1747,6 +1759,7 @@ def export(
("missing", ("download_missing", "use_photos_export")),
("jpeg_quality", ("convert_to_jpeg")),
("ignore_signature", ("update")),
("exiftool_option", ("exiftool")),
]
try:
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
@ -1998,6 +2011,7 @@ def export(
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
exiftool_option=exiftool_option,
)
results += export_results
@ -2045,6 +2059,7 @@ def export(
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
exiftool_option=exiftool_option,
)
results += export_results
@ -2589,6 +2604,7 @@ def export_photo(
jpeg_quality=1.0,
ignore_date_modified=False,
use_photokit=False,
exiftool_option=None,
):
"""Helper function for export that does the actual export
@ -2622,6 +2638,7 @@ def export_photo(
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool
Returns:
list of path(s) of exported photo or None if photo was missing
@ -2767,12 +2784,21 @@ def export_photo(
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
)
results += export_results
for warning_ in export_results.exiftool_warning:
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results.exiftool_error:
click.echo(click.style(f"exiftool error for file {error_[0]}: {error_[1]}", fg=CLI_COLOR_ERROR),err=True)
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
except Exception as e:
click.echo(
@ -2862,12 +2888,21 @@ def export_photo(
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
)
results += export_results_edited
for warning_ in export_results_edited.exiftool_warning:
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
verbose_(
f"exiftool warning for file {warning_[0]}: {warning_[1]}"
)
for error_ in export_results_edited.exiftool_error:
click.echo(click.style(f"exiftool error for file {error_[0]}: {error_[1]}", fg=CLI_COLOR_ERROR),err=True)
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
except Exception as e:
click.echo(
click.style(

View File

@ -1,5 +1,5 @@
""" version info """
__version__ = "0.38.8"
__version__ = "0.38.9"

View File

@ -132,18 +132,21 @@ class _ExifToolProc:
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
def __init__(self, filepath, exiftool=None, overwrite=True):
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
""" Create ExifTool object
Args:
file: path to image file
exiftool: path to exiftool, if not specified will look in path
overwrite: if True, will overwrite image file without creating backup, default=False
file: path to image file
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)
Returns:
ExifTool instance
"""
self.file = filepath
self.overwrite = overwrite
self.flags = flags or []
self.data = {}
self.warning = None
self.error = None
@ -249,14 +252,21 @@ class ExifTool:
commands.append("-overwrite_original")
filename = os.fsencode(self.file) if not no_file else b""
command_str = (
if self.flags:
command_str = b"\n".join([f.encode("utf-8") for f in self.flags])
command_str += b"\n"
else:
command_str = b""
command_str += (
b"\n".join([c.encode("utf-8") for c in commands])
+ b"\n"
+ filename
+ b"\n"
+ b"-execute\n"
)
# send the command
self._process.stdin.write(command_str)
self._process.stdin.flush()

View File

@ -425,6 +425,7 @@ def export2(
ignore_date_modified=False,
use_photokit=False,
verbose=None,
exiftool_flags=None,
):
"""export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised
@ -469,6 +470,7 @@ def export2(
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
ignore_date_modified: for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
exiftool_flags: optional list of flags to pass to exiftool when using exiftool option, e.g ["-m", "-F"]
Returns: ExportResults class
ExportResults has attributes:
@ -972,6 +974,7 @@ def export2(
keyword_template=keyword_template,
description_template=description_template,
ignore_date_modified=ignore_date_modified,
flags=exiftool_flags,
)
if warning_:
exiftool_warning.append((exported_file, warning_))
@ -1006,6 +1009,7 @@ def export2(
keyword_template=keyword_template,
description_template=description_template,
ignore_date_modified=ignore_date_modified,
flags=exiftool_flags,
)
if warning_:
exiftool_warning.append((exported_file, warning_))
@ -1244,6 +1248,7 @@ def _write_exif_data(
keyword_template=None,
description_template=None,
ignore_date_modified=False,
flags=None,
):
"""write exif data to image file at filepath
@ -1253,6 +1258,7 @@ def _write_exif_data(
use_persons_as_keywords: treat person names as keywords
keyword_template: (list of strings); list of template strings to render as keywords
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)
Returns:
(warning, error) of warning and error strings if exiftool produces warnings or errors
@ -1267,7 +1273,7 @@ def _write_exif_data(
ignore_date_modified=ignore_date_modified,
)
with ExifTool(filepath) as exiftool:
with ExifTool(filepath, flags=flags) as exiftool:
for exiftag, val in exif_info.items():
if type(val) == list:
for v in val:

Binary file not shown.

View File

@ -1059,7 +1059,9 @@ def test_export_exiftool_ignore_date_modified():
).asdict()
for key in CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]:
if type(exif[key]) == list:
assert sorted(exif[key]) == sorted(CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key])
assert sorted(exif[key]) == sorted(
CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key]
)
else:
assert exif[key] == CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key]
@ -1172,6 +1174,43 @@ def test_export_exiftool_error():
assert exif[key] == CLI_EXIFTOOL[uuid][key]
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
def test_export_exiftool_option():
""" test --exiftool-option """
import glob
import os
import os.path
from osxphotos.__main__ import export
from osxphotos.exiftool import ExifTool
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
# first export with --exiftool, one file produces a warning
result = runner.invoke(
export, [os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--exiftool"]
)
assert result.exit_code == 0
assert "exiftool warning" in result.output
# run again with exiftool-option = "-m" (ignore minor warnings)
# shouldn't see the warning this time
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"-V",
"--exiftool",
"--exiftool-option",
"-m",
],
)
assert result.exit_code == 0
assert "exiftool warning" not in result.output
def test_export_edited_suffix():
""" test export with --edited-suffix """
import glob

View File

@ -3,6 +3,7 @@ from osxphotos.exiftool import get_exiftool_path
TEST_FILE_ONE_KEYWORD = "tests/test-images/wedding.jpg"
TEST_FILE_BAD_IMAGE = "tests/test-images/badimage.jpeg"
TEST_FILE_WARNING = "tests/test-images/exiftool_warning.heic"
TEST_FILE_MULTI_KEYWORD = "tests/test-images/Tulips.jpg"
TEST_MULTI_KEYWORDS = [
"Top Shot",
@ -200,6 +201,29 @@ def test_setvalue_context_manager_error():
assert exif.error
def test_flags():
# test that flags work
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_WARNING))
FileUtil.copy(TEST_FILE_WARNING, tempfile)
with osxphotos.exiftool.ExifTool(tempfile) as exif:
exif.setvalue("XMP:Subject", "foo/bar")
assert exif.warning
# test again with -m: ignore minor warnings
FileUtil.unlink(tempfile)
FileUtil.copy(TEST_FILE_WARNING, tempfile)
with osxphotos.exiftool.ExifTool(tempfile, flags=["-m"]) as exif:
exif.setvalue("XMP:Subject", "foo/bar")
assert not exif.warning
def test_clear_value():
# test clearing a tag value
import os.path