Added XMP sidecar option to export, closes #51
This commit is contained in:
@@ -629,10 +629,10 @@ def query(
|
||||
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). "
|
||||
'"exiftool -j=photoname.json photoname.jpg" '
|
||||
"The sidecar file is named in format photoname.json "
|
||||
"--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). "
|
||||
"The sidecar file is named in format photoname.xmp"
|
||||
)
|
||||
@click.option(
|
||||
"--download-missing",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.22.6"
|
||||
__version__ = "0.22.7"
|
||||
|
||||
@@ -4,6 +4,7 @@ Represents a single photo in the Photos library and provides access to the photo
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
@@ -32,8 +33,6 @@ from .utils import (
|
||||
dd_to_dms_str,
|
||||
)
|
||||
|
||||
# TODO: check pylint output
|
||||
|
||||
|
||||
class PhotoInfo:
|
||||
"""
|
||||
@@ -525,13 +524,20 @@ class PhotoInfo:
|
||||
dest = dest / filename
|
||||
|
||||
# check to see if file exists and if so, add (1), (2), etc until we find one that works
|
||||
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
|
||||
# e.g. exporting sidecar for file1.png and file1.jpeg
|
||||
# if file1.png exists and exporting file1.jpeg,
|
||||
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
||||
if increment and not overwrite:
|
||||
count = 1
|
||||
dest_new = dest
|
||||
while dest_new.exists():
|
||||
dest_new = dest.parent / f"{dest.stem} ({count}){dest.suffix}"
|
||||
glob_str = str(dest.parent / f"{dest.stem}*")
|
||||
dest_files = glob.glob(glob_str)
|
||||
dest_files = [pathlib.Path(f).stem for f in dest_files]
|
||||
dest_new = dest.stem
|
||||
while dest_new in dest_files:
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
count += 1
|
||||
dest = dest_new
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
|
||||
# if overwrite==False and #increment==False, export should fail if file exists
|
||||
if dest.exists() and not overwrite and not increment:
|
||||
@@ -597,7 +603,7 @@ class PhotoInfo:
|
||||
|
||||
if sidecar_json:
|
||||
logging.debug("writing exiftool_json_sidecar")
|
||||
sidecar_filename = f"{dest}.json"
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.json")
|
||||
sidecar_str = self._exiftool_json_sidecar()
|
||||
try:
|
||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
||||
@@ -607,7 +613,7 @@ class PhotoInfo:
|
||||
|
||||
if sidecar_xmp:
|
||||
logging.debug("writing xmp_sidecar")
|
||||
sidecar_filename = f"{dest}.xmp"
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.xmp")
|
||||
sidecar_str = self._xmp_sidecar()
|
||||
try:
|
||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
||||
@@ -637,6 +643,7 @@ class PhotoInfo:
|
||||
ModifyDate """
|
||||
|
||||
exif = {}
|
||||
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
|
||||
exif["FileName"] = self.filename
|
||||
|
||||
if self.description:
|
||||
@@ -693,10 +700,14 @@ class PhotoInfo:
|
||||
|
||||
def _xmp_sidecar(self):
|
||||
""" returns string for XMP sidecar """
|
||||
# TODO: add additional fields to XMP file?
|
||||
|
||||
xmp_template = Template(
|
||||
filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME)
|
||||
)
|
||||
xmp_str = xmp_template.render(photo=self)
|
||||
# remove extra lines that mako inserts from template
|
||||
xmp_str = "\n".join([line for line in xmp_str.split("\n") if line.strip() != ""])
|
||||
return xmp_str
|
||||
|
||||
def _write_sidecar(self, filename, sidecar_str):
|
||||
|
||||
@@ -35,6 +35,10 @@ CLI_EXPORT_FILENAMES = [
|
||||
"wedding_edited.jpg",
|
||||
]
|
||||
|
||||
CLI_EXPORT_UUID = "D79B8D77-BFFC-460B-9312-034F2877D35B"
|
||||
|
||||
CLI_EXPORT_SIDECAR_FILENAMES = ["Pumkins2.jpg", "Pumpkins2.json", "Pumpkins2.xmp"]
|
||||
|
||||
|
||||
def test_osxphotos():
|
||||
import osxphotos
|
||||
@@ -118,10 +122,36 @@ def test_query_date():
|
||||
"--db",
|
||||
"./tests/Test-10.15.1.photoslibrary",
|
||||
"--from-date=2018-09-28",
|
||||
"--to-date=2018-09-28T23:00:00"
|
||||
"--to-date=2018-09-28T23:00:00",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
json_got = json.loads(result.output)
|
||||
assert len(json_got) == 4
|
||||
assert len(json_got) == 4
|
||||
|
||||
|
||||
def test_export_sidecar():
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, "tests/Test-10.15.1.photoslibrary"),
|
||||
".",
|
||||
"--original-name",
|
||||
"-V",
|
||||
"--sidecar json",
|
||||
"--sidecar xmp",
|
||||
f"--uuid {CLI_EXPORT_UUID}",
|
||||
],
|
||||
)
|
||||
files = glob.glob("*")
|
||||
assert files.sort() == CLI_EXPORT_SIDECAR_FILENAMES.sort()
|
||||
|
||||
@@ -56,6 +56,7 @@ UUID_DICT = {
|
||||
"external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||
"xmp": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
}
|
||||
|
||||
|
||||
@@ -471,3 +472,62 @@ def test_exiftool_json_sidecar():
|
||||
else:
|
||||
assert item[0][1] == item[1][1]
|
||||
|
||||
|
||||
def test_xmp_sidecar():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
|
||||
|
||||
xmp_expected = """<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
|
||||
<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>Girls with pumpkins</dc:description>
|
||||
<dc:title>Can we carry this?</dc:title>
|
||||
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||
<dc:subject>
|
||||
<rdf:Seq>
|
||||
<rdf:li>Kids</rdf:li>
|
||||
<rdf:li>Suzy</rdf:li>
|
||||
<rdf:li>Katie</rdf:li>
|
||||
</rdf:Seq>
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
<rdf:li>Suzy</rdf:li>
|
||||
<rdf:li>Katie</rdf:li>
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
<rdf:li>Kids</rdf:li>
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ UUID_DICT = {
|
||||
"no_adjustments": "15uNd7%8RguTEgNPKHfTWw",
|
||||
"export": "15uNd7%8RguTEgNPKHfTWw",
|
||||
"location": "3Jn73XpSQQCluzRBMWRsMA",
|
||||
"xmp": "8SOE9s0XQVGsuq4ONohTng",
|
||||
}
|
||||
|
||||
|
||||
@@ -413,3 +414,63 @@ def test_exiftool_json_sidecar():
|
||||
assert sorted(item[0][1]) == sorted(item[1][1])
|
||||
else:
|
||||
assert item[0][1] == item[1][1]
|
||||
|
||||
|
||||
def test_xmp_sidecar():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
|
||||
|
||||
xmp_expected = """<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
|
||||
<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>Girls with pumpkins</dc:description>
|
||||
<dc:title>Can we carry this?</dc:title>
|
||||
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||
<dc:subject>
|
||||
<rdf:Seq>
|
||||
<rdf:li>Kids</rdf:li>
|
||||
<rdf:li>Suzy</rdf:li>
|
||||
<rdf:li>Katie</rdf:li>
|
||||
</rdf:Seq>
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
<rdf:li>Suzy</rdf:li>
|
||||
<rdf:li>Katie</rdf:li>
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
<rdf:li>Kids</rdf:li>
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user