Added XMP sidecar to export

This commit is contained in:
Rhet Turnbull 2020-01-26 09:34:53 -08:00
parent ba224af3fb
commit 4dfb131a21
6 changed files with 180 additions and 20 deletions

View File

@ -622,13 +622,17 @@ def query(
)
@click.option(
"--sidecar",
is_flag=True,
help="Create JSON sidecar for each photo exported "
f"in format useable by exiftool ({_EXIF_TOOL_URL}) "
default=None,
multiple=True,
metavar="FORMAT",
type=click.Choice(['xmp', 'json'], case_sensitive=False),
help="Create sidecar for each photo exported; valid FORMAT values: xmp, json; "
f"--sidecar json: create JSON sidecar useable by exiftool ({_EXIF_TOOL_URL}) "
"The sidecar file can be used to apply metadata to the file with exiftool, for example: "
'"exiftool -j=photoname.jpg.json photoname.jpg" '
"The sidecar file is named in format photoname.ext.json where ext is extension of the photo (e.g. jpg). "
"Note: this does not create an XMP sidecar as used by Lightroom, etc.",
"--sidecar xmp: create XMP sidecar used by Adobe Lightroom, etc."
"The sidecar file is named in format photoname.ext.xmp where ext is extension of the photo (e.g. jpg). "
)
@click.option(
"--download-missing",
@ -1060,7 +1064,7 @@ def export_photo(
dest: destination path as string
verbose: boolean; print verbose output
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
sidecar: boolean; create json sidecar file with export
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
overwrite: boolean; overwrite dest file if it already exists
original_name: boolean; use original filename instead of current filename
export_live: boolean; also export live video component if photo is a live photo
@ -1100,10 +1104,18 @@ def export_photo(
date_created = photo.date.timetuple()
dest = create_path_by_date(dest, date_created)
sidecar = [s.lower() for s in sidecar]
sidecar_json = sidecar_xmp = False
if "json" in sidecar:
sidecar_json = True
if "xmp" in sidecar:
sidecar_xmp = True
photo_path = photo.export(
dest,
filename,
sidecar=sidecar,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
overwrite=overwrite,
use_photos_export=download_missing,
)
@ -1119,7 +1131,8 @@ def export_photo(
photo.export(
dest,
edited_name,
sidecar=sidecar,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
overwrite=overwrite,
edited=True,
use_photos_export=download_missing,

View File

@ -2,6 +2,7 @@
Constants used by osxphotos
"""
import os.path
# which Photos library database versions have been tested
# Photos 2.0 (10.12.6) == 2622
@ -30,3 +31,6 @@ _PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
_PHOTO_TYPE = 0
_MOVIE_TYPE = 1
# Name of XMP template file
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"

View File

@ -15,18 +15,21 @@ from datetime import timedelta, timezone
from pprint import pformat
import yaml
from mako.template import Template
from ._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION,
_TEMPLATE_DIR,
_XMP_TEMPLATE_NAME,
)
from .utils import (
_copy_file,
_export_photo_uuid_applescript,
_get_resource_loc,
dd_to_dms_str,
_export_photo_uuid_applescript,
)
# TODO: check pylint output
@ -458,7 +461,8 @@ class PhotoInfo:
edited=False,
overwrite=False,
increment=True,
sidecar=False,
sidecar_json=False,
sidecar_xmp=False,
use_photos_export=False,
timeout=120,
):
@ -470,8 +474,10 @@ class PhotoInfo:
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
if overwrite=False and increment=False, export will fail if destination file already exists
sidecar: (boolean, default = False); if True will also write a json sidecar with EXIF data in format readable by exiftool
sidecar_json: (boolean, default = False); if True will also write a json sidecar with IPTC data in format readable by exiftool
sidecar filename will be dest/filename.ext.json where ext is suffix of the image file (e.g. jpeg or jpg)
sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data
sidecar filename will be dest/filename.ext.xmp where ext is suffix of the image file (e.g. jpeg or jpg)
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
returns the full path to the exported file """
@ -589,20 +595,47 @@ class PhotoInfo:
if exported is None:
logging.warning(f"Error exporting photo {self.uuid} to {dest}")
if sidecar:
if sidecar_json:
logging.debug("writing exiftool_json_sidecar")
sidecar_filename = f"{dest}.json"
json_sidecar_str = self._exiftool_json_sidecar()
sidecar_str = self._exiftool_json_sidecar()
try:
self._write_sidecar_car(sidecar_filename, json_sidecar_str)
self._write_sidecar(sidecar_filename, sidecar_str)
except Exception as e:
logging.critical(f"Error writing json sidecar to {sidecar_filename}")
logging.warning(f"Error writing json sidecar to {sidecar_filename}")
raise e
if sidecar_xmp:
logging.debug("writing xmp_sidecar")
sidecar_filename = f"{dest}.xmp"
sidecar_str = self._xmp_sidecar()
try:
self._write_sidecar(sidecar_filename, sidecar_str)
except Exception as e:
logging.warning(f"Error writing xmp sidecar to {sidecar_filename}")
raise e
return str(dest)
def _exiftool_json_sidecar(self):
""" return json string of EXIF details in exiftool sidecar format """
""" return json string of EXIF details in exiftool sidecar format
Does not include all the EXIF fields as those are likely already in the image
Exports the following:
FileName
ImageDescription
Description
Title
TagsList
Keywords
Subject
PersonInImage
GPSLatitude, GPSLongitude
GPSPosition
GPSLatitudeRef, GPSLongitudeRef
DateTimeOriginal
OffsetTimeOriginal
ModifyDate """
exif = {}
exif["FileName"] = self.filename
@ -658,17 +691,27 @@ class PhotoInfo:
json_str = json.dumps([exif])
return json_str
def _write_sidecar_car(self, filename, json_str):
if not filename and not json_str:
def _xmp_sidecar(self):
""" returns string for XMP sidecar """
xmp_template = Template(
filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME)
)
xmp_str = xmp_template.render(photo=self)
return xmp_str
def _write_sidecar(self, filename, sidecar_str):
""" write sidecar_str to filename
used for exporting sidecar info """
if not filename and not sidecar_str:
raise (
ValueError(
f"filename {filename} and json_str {json_str} must not be None"
f"filename {filename} and sidecar_str {sidecar_str} must not be None"
)
)
# TODO: catch exception?
f = open(filename, "w")
f.write(json_str)
f.write(sidecar_str)
f.close()
@property

View File

@ -0,0 +1,99 @@
<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
<%def name="dc_description(desc)">
% if desc is None:
<dc:description></dc:description>
% else:
<dc:description>${desc}</dc:description>
% endif
</%def>
<%def name="dc_title(title)">
% if title is None:
<dc:title></dc:title>
% else:
<dc:title>${title}</dc:title>
% endif
</%def>
<%def name="dc_subject(subject)">
% if subject:
<!-- keywords and persons listed in <dc:subject> as Photos does -->
<dc:subject>
<rdf:Seq>
% for subj in subject:
<rdf:li>${subj}</rdf:li>
% endfor
</rdf:Seq>
</dc:subject>
% endif
</%def>
<%def name="dc_datecreated(date)">
% if date is not None:
<photoshop:DateCreated>${date.isoformat()}</photoshop:DateCreated>
% endif
</%def>
<%def name="iptc_personinimage(persons)">
% if persons:
<Iptc4xmpExt:PersonInImage>
<rdf:Bag>
% for person in persons:
<rdf:li>${person}</rdf:li>
% endfor
</rdf:Bag>
</Iptc4xmpExt:PersonInImage>
% endif
</%def>
<%def name="dk_tagslist(keywords)">
% if keywords:
<digiKam:TagsList>
<rdf:Seq>
% for keyword in keywords:
<rdf:li>${keyword}</rdf:li>
% endfor
</rdf:Seq>
</digiKam:TagsList>
% endif
</%def>
<%def name="adobe_createdate(date)">
% if date is not None:
<xmp:CreateDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:CreateDate>
% endif
</%def>
<%def name="adobe_modifydate(date)">
% if date is not None:
<xmp:ModifyDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:ModifyDate>
% endif
</%def>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
${dc_description(photo.description)}
${dc_title(photo.title)}
${dc_subject(photo.keywords + photo.persons)}
${dc_datecreated(photo.date)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
${iptc_personinimage(photo.persons)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
${dk_tagslist(photo.keywords)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
${adobe_createdate(photo.date)}
${adobe_modifydate(photo.date)}
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>

View File

@ -143,3 +143,4 @@ termcolor==1.1.0
wcwidth==0.1.7
wrapt==1.11.1
zipp==0.5.2
Mako==1.1.1

View File

@ -61,6 +61,6 @@ setup(
"Programming Language :: Python :: 3.6",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=["pyobjc>=6.0.1", "Click>=7", "PyYAML>=5.1.2"],
install_requires=["pyobjc>=6.0.1", "Click>=7", "PyYAML>=5.1.2", "Mako>=1.1.1"],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
)