Added --exiftool-option to CLI, closes #298
This commit is contained in:
parent
aca19f4063
commit
a4bbb6492d
10
README.md
10
README.md
@ -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;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.38.8"
|
||||
__version__ = "0.38.9"
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
BIN
tests/test-images/exiftool_warning.heic
Normal file
BIN
tests/test-images/exiftool_warning.heic
Normal file
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user