Added --exiftool-option to CLI, closes #298
This commit is contained in:
10
README.md
10
README.md
@@ -342,6 +342,16 @@ Options:
|
|||||||
(see also --ignore-date-modified);
|
(see also --ignore-date-modified);
|
||||||
QuickTime:GPSCoordinates;
|
QuickTime:GPSCoordinates;
|
||||||
UserData: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-date-modified If used with --exiftool or --sidecar, will
|
||||||
ignore the photo modification date and set
|
ignore the photo modification date and set
|
||||||
EXIF:ModifyDate to EXIF:DateTimeOriginal;
|
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); "
|
"(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); "
|
||||||
"QuickTime:GPSCoordinates; UserData:GPSCoordinates.",
|
"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(
|
@click.option(
|
||||||
"--ignore-date-modified",
|
"--ignore-date-modified",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -1550,6 +1561,7 @@ def export(
|
|||||||
download_missing,
|
download_missing,
|
||||||
dest,
|
dest,
|
||||||
exiftool,
|
exiftool,
|
||||||
|
exiftool_option,
|
||||||
ignore_date_modified,
|
ignore_date_modified,
|
||||||
portrait,
|
portrait,
|
||||||
not_portrait,
|
not_portrait,
|
||||||
@@ -1747,6 +1759,7 @@ def export(
|
|||||||
("missing", ("download_missing", "use_photos_export")),
|
("missing", ("download_missing", "use_photos_export")),
|
||||||
("jpeg_quality", ("convert_to_jpeg")),
|
("jpeg_quality", ("convert_to_jpeg")),
|
||||||
("ignore_signature", ("update")),
|
("ignore_signature", ("update")),
|
||||||
|
("exiftool_option", ("exiftool")),
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
|
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
|
||||||
@@ -1998,6 +2011,7 @@ def export(
|
|||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
|
exiftool_option=exiftool_option,
|
||||||
)
|
)
|
||||||
results += export_results
|
results += export_results
|
||||||
|
|
||||||
@@ -2045,6 +2059,7 @@ def export(
|
|||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
|
exiftool_option=exiftool_option,
|
||||||
)
|
)
|
||||||
results += export_results
|
results += export_results
|
||||||
|
|
||||||
@@ -2589,6 +2604,7 @@ def export_photo(
|
|||||||
jpeg_quality=1.0,
|
jpeg_quality=1.0,
|
||||||
ignore_date_modified=False,
|
ignore_date_modified=False,
|
||||||
use_photokit=False,
|
use_photokit=False,
|
||||||
|
exiftool_option=None,
|
||||||
):
|
):
|
||||||
"""Helper function for export that does the actual export
|
"""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
|
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.
|
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
|
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:
|
Returns:
|
||||||
list of path(s) of exported photo or None if photo was missing
|
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,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
verbose=verbose_,
|
verbose=verbose_,
|
||||||
|
exiftool_flags=exiftool_option,
|
||||||
)
|
)
|
||||||
results += export_results
|
results += export_results
|
||||||
for warning_ in export_results.exiftool_warning:
|
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:
|
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:
|
except Exception as e:
|
||||||
click.echo(
|
click.echo(
|
||||||
@@ -2862,12 +2888,21 @@ def export_photo(
|
|||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
verbose=verbose_,
|
verbose=verbose_,
|
||||||
|
exiftool_flags=exiftool_option,
|
||||||
)
|
)
|
||||||
results += export_results_edited
|
results += export_results_edited
|
||||||
for warning_ in export_results_edited.exiftool_warning:
|
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:
|
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:
|
except Exception as e:
|
||||||
click.echo(
|
click.echo(
|
||||||
click.style(
|
click.style(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.38.8"
|
__version__ = "0.38.9"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -132,18 +132,21 @@ class _ExifToolProc:
|
|||||||
class ExifTool:
|
class ExifTool:
|
||||||
""" Basic exiftool interface for reading and writing EXIF tags """
|
""" 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
|
""" Create ExifTool object
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file: path to image file
|
file: path to image file
|
||||||
exiftool: path to exiftool, if not specified will look in path
|
exiftool: path to exiftool, if not specified will look in path
|
||||||
overwrite: if True, will overwrite image file without creating backup, default=False
|
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:
|
Returns:
|
||||||
ExifTool instance
|
ExifTool instance
|
||||||
"""
|
"""
|
||||||
self.file = filepath
|
self.file = filepath
|
||||||
self.overwrite = overwrite
|
self.overwrite = overwrite
|
||||||
|
self.flags = flags or []
|
||||||
self.data = {}
|
self.data = {}
|
||||||
self.warning = None
|
self.warning = None
|
||||||
self.error = None
|
self.error = None
|
||||||
@@ -249,7 +252,14 @@ class ExifTool:
|
|||||||
commands.append("-overwrite_original")
|
commands.append("-overwrite_original")
|
||||||
|
|
||||||
filename = os.fsencode(self.file) if not no_file else b""
|
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".join([c.encode("utf-8") for c in commands])
|
||||||
+ b"\n"
|
+ b"\n"
|
||||||
+ filename
|
+ filename
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ def export2(
|
|||||||
ignore_date_modified=False,
|
ignore_date_modified=False,
|
||||||
use_photokit=False,
|
use_photokit=False,
|
||||||
verbose=None,
|
verbose=None,
|
||||||
|
exiftool_flags=None,
|
||||||
):
|
):
|
||||||
"""export photo, like export but with update and dry_run options
|
"""export photo, like export but with update and dry_run options
|
||||||
dest: must be valid destination path or exception raised
|
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.
|
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
|
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.
|
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
|
Returns: ExportResults class
|
||||||
ExportResults has attributes:
|
ExportResults has attributes:
|
||||||
@@ -972,6 +974,7 @@ def export2(
|
|||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
flags=exiftool_flags,
|
||||||
)
|
)
|
||||||
if warning_:
|
if warning_:
|
||||||
exiftool_warning.append((exported_file, warning_))
|
exiftool_warning.append((exported_file, warning_))
|
||||||
@@ -1006,6 +1009,7 @@ def export2(
|
|||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
flags=exiftool_flags,
|
||||||
)
|
)
|
||||||
if warning_:
|
if warning_:
|
||||||
exiftool_warning.append((exported_file, warning_))
|
exiftool_warning.append((exported_file, warning_))
|
||||||
@@ -1244,6 +1248,7 @@ def _write_exif_data(
|
|||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
description_template=None,
|
description_template=None,
|
||||||
ignore_date_modified=False,
|
ignore_date_modified=False,
|
||||||
|
flags=None,
|
||||||
):
|
):
|
||||||
"""write exif data to image file at filepath
|
"""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
|
use_persons_as_keywords: treat person names as keywords
|
||||||
keyword_template: (list of strings); list of template strings to render 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
|
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:
|
Returns:
|
||||||
(warning, error) of warning and error strings if exiftool produces warnings or errors
|
(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,
|
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():
|
for exiftag, val in exif_info.items():
|
||||||
if type(val) == list:
|
if type(val) == list:
|
||||||
for v in val:
|
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()
|
).asdict()
|
||||||
for key in CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]:
|
for key in CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid]:
|
||||||
if type(exif[key]) == list:
|
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:
|
else:
|
||||||
assert exif[key] == CLI_EXIFTOOL_IGNORE_DATE_MODIFIED[uuid][key]
|
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]
|
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():
|
def test_export_edited_suffix():
|
||||||
""" test export with --edited-suffix """
|
""" test export with --edited-suffix """
|
||||||
import glob
|
import glob
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from osxphotos.exiftool import get_exiftool_path
|
|||||||
|
|
||||||
TEST_FILE_ONE_KEYWORD = "tests/test-images/wedding.jpg"
|
TEST_FILE_ONE_KEYWORD = "tests/test-images/wedding.jpg"
|
||||||
TEST_FILE_BAD_IMAGE = "tests/test-images/badimage.jpeg"
|
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_FILE_MULTI_KEYWORD = "tests/test-images/Tulips.jpg"
|
||||||
TEST_MULTI_KEYWORDS = [
|
TEST_MULTI_KEYWORDS = [
|
||||||
"Top Shot",
|
"Top Shot",
|
||||||
@@ -200,6 +201,29 @@ def test_setvalue_context_manager_error():
|
|||||||
assert exif.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():
|
def test_clear_value():
|
||||||
# test clearing a tag value
|
# test clearing a tag value
|
||||||
import os.path
|
import os.path
|
||||||
|
|||||||
Reference in New Issue
Block a user