diff --git a/README.md b/README.md
index c9c8a7a1..75da1621 100644
--- a/README.md
+++ b/README.md
@@ -217,6 +217,10 @@ Options:
photos if the RAW photo does not have an
associated jpeg image (e.g. the RAW file was
imported to Photos without a jpeg preview).
+ --person-keyword Use person in image as keyword/tag when
+ exporting metadata.
+ --album-keyword Use album name as keyword/tag when exporting
+ metadata.
--current-name Use photo's current filename instead of
original filename for export. Note:
Starting with Photos 5, all photos are
@@ -814,6 +818,12 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
>>>
```
+#### `use_persons_as_keywords`
+If True, person names (face/person in image) will be used as keywords when exporting metadata with [PhotoInfo](#PhotoInfo) [export()](#export).
+
+#### `use_albums_as_keywords`
+If True, album names will be used as keywords when exporting metadata with [PhotoInfo](#PhotoInfo) [export()](#export).
+
### PhotoInfo
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
@@ -958,7 +968,8 @@ Returns True if photo is a panorama, otherwise False.
#### `json()`
Returns a JSON representation of all photo info
-#### `export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False)`
+#### `export()`
+`export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False)`
Export photo from the Photos library to another destination on disk.
- dest: must be valid destination path as str (or exception raised).
diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py
index 4a1f2995..105a274e 100644
--- a/osxphotos/__main__.py
+++ b/osxphotos/__main__.py
@@ -893,6 +893,16 @@ def query(
"Note: this does not skip RAW photos if the RAW photo does not have an associated jpeg image "
"(e.g. the RAW file was imported to Photos without a jpeg preview).",
)
+@click.option(
+ "--person-keyword",
+ is_flag = True,
+ help = "Use person in image as keyword/tag when exporting metadata."
+)
+@click.option(
+ "--album-keyword",
+ is_flag = True,
+ help = "Use album name as keyword/tag when exporting metadata."
+)
@click.option(
"--current-name",
is_flag=True,
@@ -983,6 +993,8 @@ def export(
skip_bursts,
skip_live,
skip_raw,
+ person_keyword,
+ album_keyword,
current_name,
sidecar,
only_photos,
@@ -1145,6 +1157,16 @@ def export(
)
if photos:
+ # set the persons_as_keywords and albums_as_keywords on the database object
+ # to control metadata export
+ photosdb = photos[0]._db
+ if person_keyword:
+ # add persons as keywords
+ photosdb.use_persons_as_keywords = True
+ if album_keyword:
+ # add albums as keywords
+ photosdb.use_albums_as_keywords = True
+
if export_bursts:
# add the burst_photos to the export set
photos_burst = [p for p in photos if p.burst]
diff --git a/osxphotos/_version.py b/osxphotos/_version.py
index e33bb353..8f257e08 100644
--- a/osxphotos/_version.py
+++ b/osxphotos/_version.py
@@ -1,3 +1,3 @@
""" version info """
-__version__ = "0.28.6"
+__version__ = "0.28.7"
diff --git a/osxphotos/photoinfo.py b/osxphotos/photoinfo.py
index 8e44ac1a..307e407f 100644
--- a/osxphotos/photoinfo.py
+++ b/osxphotos/photoinfo.py
@@ -24,6 +24,7 @@ from ._constants import (
_PHOTOS_4_VERSION,
_PHOTOS_5_SHARED_PHOTO_PATH,
_TEMPLATE_DIR,
+ _UNKNOWN_PERSON,
_XMP_TEMPLATE_NAME,
)
from .exiftool import ExifTool
@@ -304,9 +305,7 @@ class PhotoInfo:
# Note: In Photos Version 5.0 (141.19.150), images not copied to Photos Library
# that are missing do not always trigger is_missing = True as happens
# in earlier version so it's possible for this check to fail, if so, return None
- logging.debug(
- f"Error getting path to RAW file: {filepath}/{glob_str}"
- )
+ logging.debug(f"Error getting path to RAW file: {filepath}/{glob_str}")
photopath = None
else:
photopath = os.path.join(filepath, raw_file[0])
@@ -934,18 +933,30 @@ class PhotoInfo:
if self.title:
exif["XMP:Title"] = self.title
+ keyword_list = []
if self.keywords:
- exif["XMP:TagsList"] = exif["IPTC:Keywords"] = list(self.keywords)
- # Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
- exif["XMP:Subject"] = list(self.keywords)
+ keyword_list.extend(self.keywords)
+ person_list = []
if self.persons:
- exif["XMP:PersonInImage"] = self.persons
+ # filter out _UNKNOWN_PERSON
+ person_list = [p for p in self.persons if p != _UNKNOWN_PERSON]
+
+ if self._db.use_persons_as_keywords and person_list:
+ keyword_list.extend(person_list)
+
+ if self._db.use_albums_as_keywords and self.albums:
+ keyword_list.extend(self.albums)
+
+ if keyword_list:
+ exif["XMP:TagsList"] = exif["IPTC:Keywords"] = keyword_list
+
+ if person_list:
+ exif["XMP:PersonInImage"] = person_list
+
+ if self.keywords or person_list:
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
- if "XMP:Subject" in exif:
- exif["XMP:Subject"].extend(self.persons)
- else:
- exif["XMP:Subject"] = self.persons
+ exif["XMP:Subject"] = list(self.keywords) + person_list
# if self.favorite():
# exif["Rating"] = 5
@@ -986,7 +997,34 @@ class PhotoInfo:
xmp_template = Template(
filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME)
)
- xmp_str = xmp_template.render(photo=self)
+
+ keyword_list = []
+ if self.keywords:
+ keyword_list.extend(self.keywords)
+
+ person_list = []
+ if self.persons:
+ # filter out _UNKNOWN_PERSON
+ person_list = [p for p in self.persons if p != _UNKNOWN_PERSON]
+
+ if self._db.use_persons_as_keywords and person_list:
+ keyword_list.extend(person_list)
+
+ if self._db.use_albums_as_keywords and self.albums:
+ keyword_list.extend(self.albums)
+
+ subject_list = []
+ if self.keywords or person_list:
+ # Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
+ subject_list = list(self.keywords) + person_list
+
+ xmp_str = xmp_template.render(
+ photo=self,
+ keywords=keyword_list,
+ persons=person_list,
+ subjects=subject_list,
+ )
+
# remove extra lines that mako inserts from template
xmp_str = "\n".join(
[line for line in xmp_str.split("\n") if line.strip() != ""]
diff --git a/osxphotos/photosdb.py b/osxphotos/photosdb.py
index 9f251c93..d2f18ab0 100644
--- a/osxphotos/photosdb.py
+++ b/osxphotos/photosdb.py
@@ -77,6 +77,12 @@ class PhotosDB:
# set up the data structures used to store all the Photo database info
+ # if True, will treat persons as keywords when exporting metadata
+ self.use_persons_as_keywords = False
+
+ # if True, will treat albums as keywords when exporting metadata
+ self.use_albums_as_keywords = False
+
# Path to the Photos library database file
# photos.db in the photos library database/ directory
self._dbfile = None
diff --git a/osxphotos/templates/xmp_sidecar.mako b/osxphotos/templates/xmp_sidecar.mako
index 1f2e1a8c..792cc831 100644
--- a/osxphotos/templates/xmp_sidecar.mako
+++ b/osxphotos/templates/xmp_sidecar.mako
@@ -79,16 +79,16 @@
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
${dc_description(photo.description)}
${dc_title(photo.title)}
- ${dc_subject(photo.keywords + photo.persons)}
+ ${dc_subject(subjects)}
${dc_datecreated(photo.date)}
- ${iptc_personinimage(photo.persons)}
+ ${iptc_personinimage(persons)}
- ${dk_tagslist(photo.keywords)}
+ ${dk_tagslist(keywords)}
diff --git a/tests/test_export_catalina_10_15_1.py b/tests/test_export_catalina_10_15_1.py
index fc8b06d0..590f9ef7 100644
--- a/tests/test_export_catalina_10_15_1.py
+++ b/tests/test_export_catalina_10_15_1.py
@@ -482,6 +482,90 @@ def test_exiftool_json_sidecar():
assert json_got[k] == v
+def test_exiftool_json_sidecar_use_persons_keyword():
+ import osxphotos
+ import json
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ photosdb.use_persons_as_keywords = True
+ photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
+
+ json_expected = json.loads(
+ """
+ [{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
+ "EXIF:ImageDescription": "Girls with pumpkins",
+ "XMP:Description": "Girls with pumpkins",
+ "XMP:Title": "Can we carry this?",
+ "XMP:TagsList": ["Kids", "Suzy", "Katie"],
+ "IPTC:Keywords": ["Kids", "Suzy", "Katie"],
+ "XMP:PersonInImage": ["Suzy", "Katie"],
+ "XMP:Subject": ["Kids", "Suzy", "Katie"],
+ "EXIF:DateTimeOriginal": "2018:09:28 15:35:49",
+ "EXIF:OffsetTimeOriginal": "-04:00",
+ "EXIF:ModifyDate": "2019:11:24 13:09:17"}]
+ """
+ )[0]
+
+ json_got = photos[0]._exiftool_json_sidecar()
+ json_got = json.loads(json_got)[0]
+
+ # some gymnastics to account for different sort order in different pythons
+ # some gymnastics to account for different sort order in different pythons
+ for k, v in json_got.items():
+ if type(v) in (list, tuple):
+ assert sorted(json_expected[k]) == sorted(v)
+ else:
+ assert json_expected[k] == v
+
+ for k, v in json_expected.items():
+ if type(v) in (list, tuple):
+ assert sorted(json_got[k]) == sorted(v)
+ else:
+ assert json_got[k] == v
+
+
+def test_exiftool_json_sidecar_use_albums_keyword():
+ import osxphotos
+ import json
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ photosdb.use_albums_as_keywords = True
+ photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
+
+ json_expected = json.loads(
+ """
+ [{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
+ "EXIF:ImageDescription": "Girls with pumpkins",
+ "XMP:Description": "Girls with pumpkins",
+ "XMP:Title": "Can we carry this?",
+ "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"],
+ "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"],
+ "XMP:PersonInImage": ["Suzy", "Katie"],
+ "XMP:Subject": ["Kids", "Suzy", "Katie"],
+ "EXIF:DateTimeOriginal": "2018:09:28 15:35:49",
+ "EXIF:OffsetTimeOriginal": "-04:00",
+ "EXIF:ModifyDate": "2019:11:24 13:09:17"}]
+ """
+ )[0]
+
+ json_got = photos[0]._exiftool_json_sidecar()
+ json_got = json.loads(json_got)[0]
+
+ # some gymnastics to account for different sort order in different pythons
+ # some gymnastics to account for different sort order in different pythons
+ for k, v in json_got.items():
+ if type(v) in (list, tuple):
+ assert sorted(json_expected[k]) == sorted(v)
+ else:
+ assert json_expected[k] == v
+
+ for k, v in json_expected.items():
+ if type(v) in (list, tuple):
+ assert sorted(json_got[k]) == sorted(v)
+ else:
+ assert json_got[k] == v
+
+
def test_xmp_sidecar():
import osxphotos
@@ -539,3 +623,125 @@ def test_xmp_sidecar():
for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
assert line_expected == line_got
+
+def test_xmp_sidecar_use_persons_keyword():
+ import osxphotos
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ photosdb.use_persons_as_keywords = True
+ photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
+
+ xmp_expected = """
+
+
+
+
+ Girls with pumpkins
+ Can we carry this?
+
+
+
+ Kids
+ Suzy
+ Katie
+
+
+ 2018-09-28T15:35:49.063000-04:00
+
+
+
+
+ Suzy
+ Katie
+
+
+
+
+
+
+ Kids
+ Suzy
+ Katie
+
+
+
+
+ 2018-09-28T15:35:49
+ 2018-09-28T15:35:49
+
+
+ """
+
+ xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
+
+ xmp_got = photos[0]._xmp_sidecar()
+ xmp_got_lines = [line.strip() for line in xmp_got.split("\n")]
+
+ for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
+ assert line_expected == line_got
+
+def test_xmp_sidecar_use_albums_keyword():
+ import osxphotos
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ photosdb.use_albums_as_keywords = True
+ photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
+
+ xmp_expected = """
+
+
+
+
+ Girls with pumpkins
+ Can we carry this?
+
+
+
+ Kids
+ Suzy
+ Katie
+
+
+ 2018-09-28T15:35:49.063000-04:00
+
+
+
+
+ Suzy
+ Katie
+
+
+
+
+
+
+ Kids
+ Pumpkin Farm
+ Test Album
+
+
+
+
+ 2018-09-28T15:35:49
+ 2018-09-28T15:35:49
+
+
+ """
+
+ xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
+
+ xmp_got = photos[0]._xmp_sidecar()
+ xmp_got_lines = [line.strip() for line in xmp_got.split("\n")]
+
+ for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
+ assert line_expected == line_got
\ No newline at end of file