Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
541c390b7b | ||
|
|
6ab0ad7e86 | ||
|
|
e5755c6144 | ||
|
|
7806e05673 | ||
|
|
bb4bc8fd96 | ||
|
|
59507077ba | ||
|
|
ff0328785f |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [v0.30.3](https://github.com/RhetTbull/osxphotos/compare/v0.30.2...v0.30.3)
|
||||||
|
|
||||||
|
> 29 June 2020
|
||||||
|
|
||||||
|
- Added --description-template to CLI, closes #166 [`#166`](https://github.com/RhetTbull/osxphotos/issues/166)
|
||||||
|
- Added expand_inplace to PhotoTemplate.render [`ff03287`](https://github.com/RhetTbull/osxphotos/commit/ff0328785f3ea14b1c8ae2b7d1a9b07e8aef0777)
|
||||||
|
- Updated README.md [`5950707`](https://github.com/RhetTbull/osxphotos/commit/59507077bafe39a17bc23babe6d6c52e1f502a53)
|
||||||
|
|
||||||
|
#### [v0.30.2](https://github.com/RhetTbull/osxphotos/compare/v0.30.1...v0.30.2)
|
||||||
|
|
||||||
|
> 28 June 2020
|
||||||
|
|
||||||
|
- Added --deleted, --deleted-only to CLI, closes #179 [`#179`](https://github.com/RhetTbull/osxphotos/issues/179)
|
||||||
|
|
||||||
#### [v0.30.1](https://github.com/RhetTbull/osxphotos/compare/v0.30.0...v0.30.1)
|
#### [v0.30.1](https://github.com/RhetTbull/osxphotos/compare/v0.30.0...v0.30.1)
|
||||||
|
|
||||||
> 27 June 2020
|
> 27 June 2020
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -253,6 +253,16 @@ Options:
|
|||||||
--keyword-template "{folder_album}"
|
--keyword-template "{folder_album}"
|
||||||
--keyword-template "{created.year}" See
|
--keyword-template "{created.year}" See
|
||||||
Templating System below.
|
Templating System below.
|
||||||
|
--description-template TEMPLATE
|
||||||
|
For use with --exiftool, --sidecar; specify
|
||||||
|
a template string to use as description in
|
||||||
|
the form '{name,DEFAULT}' This is the same
|
||||||
|
format as --directory. For example, if you
|
||||||
|
wanted to append 'exported with osxphotos on
|
||||||
|
[today's date]' to the description, you
|
||||||
|
could specify --description-template
|
||||||
|
"{descr} exported with osxphotos on
|
||||||
|
{today.date}" See Templating System below.
|
||||||
--current-name Use photo's current filename instead of
|
--current-name Use photo's current filename instead of
|
||||||
original filename for export. Note:
|
original filename for export. Note:
|
||||||
Starting with Photos 5, all photos are
|
Starting with Photos 5, all photos are
|
||||||
@@ -1038,6 +1048,27 @@ Returns the absolute path to the edited photo on disk as a string. If the photo
|
|||||||
|
|
||||||
**Note**: will also return None if the edited photo is missing on disk.
|
**Note**: will also return None if the edited photo is missing on disk.
|
||||||
|
|
||||||
|
#### `height`
|
||||||
|
Returns height of the photo in pixels. If image has been edited, returns height of the edited image, otherwise returns height of the original image. See also [original_height](#original_height).
|
||||||
|
|
||||||
|
#### `width`
|
||||||
|
Returns width of the photo in pixels. If image has been edited, returns width of the edited image, otherwise returns width of the original image. See also [original_width](#original_width).
|
||||||
|
|
||||||
|
#### `orientation`
|
||||||
|
Returns EXIF orientation value of the photo as integer. If image has been edited, returns orientation of the edited image, otherwise returns orientation of the original image. See also [original_orientation](#original_orientation).
|
||||||
|
|
||||||
|
#### `original_height`
|
||||||
|
Returns height of the original photo in pixels. See also [height](#height).
|
||||||
|
|
||||||
|
#### `original_width`
|
||||||
|
Returns width of the original photo in pixels. See also [width](#width).
|
||||||
|
|
||||||
|
#### `original_orientation`
|
||||||
|
Returns EXIF orientation value of the original photo as integer. See also [orientation](#orientation).
|
||||||
|
|
||||||
|
#### `original_filesize`
|
||||||
|
Returns size of the original photo in bytes as integer.
|
||||||
|
|
||||||
#### `ismissing`
|
#### `ismissing`
|
||||||
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted and user hasn't enabled "Copy items to the Photos library" in Photos preferences. **Note**: this status is computed based on data in the Photos library and `ismissing` does not verify if the photo is actually missing. See also [path](#path).
|
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted and user hasn't enabled "Copy items to the Photos library" in Photos preferences. **Note**: this status is computed based on data in the Photos library and `ismissing` does not verify if the photo is actually missing. See also [path](#path).
|
||||||
|
|
||||||
@@ -1256,11 +1287,13 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
|||||||
|
|
||||||
#### <a name="rendertemplate">`render_template()`</a>
|
#### <a name="rendertemplate">`render_template()`</a>
|
||||||
|
|
||||||
`render_template(template_str, none_str = "_", path_sep = None)`
|
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None)`
|
||||||
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
||||||
- `template_str`: str in form "{name,DEFAULT}" where name is one of the values in table below. The "," and default value that follows are optional. If specified, "DEFAULT" will be used if "name" is None. This is useful for values which are not always present, for example reverse geolocation data.
|
- `template_str`: str in form "{name,DEFAULT}" where name is one of the values in table below. The "," and default value that follows are optional. If specified, "DEFAULT" will be used if "name" is None. This is useful for values which are not always present, for example reverse geolocation data.
|
||||||
- `none_str`: optional str to use as substitution when template value is None and no default specified in the template string. default is "_".
|
- `none_str`: optional str to use as substitution when template value is None and no default specified in the template string. default is "_".
|
||||||
- `path_sep`: optional character to use as path separator, default is os.path.sep
|
- `path_sep`: optional character to use as path separator, default is os.path.sep
|
||||||
|
- `expand_inplace`: expand multi-valued substitutions in-place as a single string instead of returning individual strings
|
||||||
|
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
|
||||||
|
|
||||||
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
|
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
|
||||||
|
|
||||||
|
|||||||
@@ -1146,6 +1146,18 @@ def query(
|
|||||||
'--keyword-template "{created.year}" '
|
'--keyword-template "{created.year}" '
|
||||||
"See Templating System below.",
|
"See Templating System below.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--description-template",
|
||||||
|
metavar="TEMPLATE",
|
||||||
|
multiple=False,
|
||||||
|
default=None,
|
||||||
|
help="For use with --exiftool, --sidecar; specify a template string to use as "
|
||||||
|
"description in the form '{name,DEFAULT}' "
|
||||||
|
"This is the same format as --directory. For example, if you wanted to append "
|
||||||
|
"'exported with osxphotos on [today's date]' to the description, you could specify "
|
||||||
|
'--description-template "{descr} exported with osxphotos on {today.date}" '
|
||||||
|
"See Templating System below.",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--current-name",
|
"--current-name",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -1260,6 +1272,7 @@ def export(
|
|||||||
person_keyword,
|
person_keyword,
|
||||||
album_keyword,
|
album_keyword,
|
||||||
keyword_template,
|
keyword_template,
|
||||||
|
description_template,
|
||||||
current_name,
|
current_name,
|
||||||
sidecar,
|
sidecar,
|
||||||
only_photos,
|
only_photos,
|
||||||
@@ -1505,6 +1518,7 @@ def export(
|
|||||||
album_keyword=album_keyword,
|
album_keyword=album_keyword,
|
||||||
person_keyword=person_keyword,
|
person_keyword=person_keyword,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
export_db=export_db,
|
export_db=export_db,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
@@ -1541,6 +1555,7 @@ def export(
|
|||||||
album_keyword=album_keyword,
|
album_keyword=album_keyword,
|
||||||
person_keyword=person_keyword,
|
person_keyword=person_keyword,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
export_db=export_db,
|
export_db=export_db,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
@@ -2012,6 +2027,7 @@ def export_photo(
|
|||||||
album_keyword=None,
|
album_keyword=None,
|
||||||
person_keyword=None,
|
person_keyword=None,
|
||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
export_db=None,
|
export_db=None,
|
||||||
fileutil=FileUtil,
|
fileutil=FileUtil,
|
||||||
dry_run=None,
|
dry_run=None,
|
||||||
@@ -2039,6 +2055,7 @@ def export_photo(
|
|||||||
album_keyword: boolean; if True, exports album names as keywords in metadata
|
album_keyword: boolean; if True, exports album names as keywords in metadata
|
||||||
person_keyword: boolean; if True, exports person names as keywords in metadata
|
person_keyword: boolean; if True, exports person names as keywords in metadata
|
||||||
keyword_template: list of strings; if provided use rendered template strings as keywords
|
keyword_template: list of strings; if provided use rendered template strings as keywords
|
||||||
|
description_template: string; optional template string that will be rendered for use as photo description
|
||||||
export_db: export database instance compatible with ExportDB_ABC
|
export_db: export database instance compatible with ExportDB_ABC
|
||||||
fileutil: file util class compatible with FileUtilABC
|
fileutil: file util class compatible with FileUtilABC
|
||||||
dry_run: boolean; if True, doesn't actually export or update any files
|
dry_run: boolean; if True, doesn't actually export or update any files
|
||||||
@@ -2113,6 +2130,7 @@ def export_photo(
|
|||||||
use_albums_as_keywords=album_keyword,
|
use_albums_as_keywords=album_keyword,
|
||||||
use_persons_as_keywords=person_keyword,
|
use_persons_as_keywords=person_keyword,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
update=update,
|
update=update,
|
||||||
export_db=export_db,
|
export_db=export_db,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
@@ -2168,6 +2186,7 @@ def export_photo(
|
|||||||
use_albums_as_keywords=album_keyword,
|
use_albums_as_keywords=album_keyword,
|
||||||
use_persons_as_keywords=person_keyword,
|
use_persons_as_keywords=person_keyword,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
update=update,
|
update=update,
|
||||||
export_db=export_db,
|
export_db=export_db,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.30.2"
|
__version__ = "0.30.5"
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ def export(
|
|||||||
use_albums_as_keywords=False,
|
use_albums_as_keywords=False,
|
||||||
use_persons_as_keywords=False,
|
use_persons_as_keywords=False,
|
||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
):
|
):
|
||||||
""" export photo
|
""" export photo
|
||||||
dest: must be valid destination path (or exception raised)
|
dest: must be valid destination path (or exception raised)
|
||||||
@@ -250,6 +251,7 @@ def export(
|
|||||||
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||||
when exporting metadata with exiftool or sidecar
|
when exporting metadata with exiftool or sidecar
|
||||||
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||||
|
description_template: string; optional template string that will be rendered for use as photo description
|
||||||
returns: list of photos exported
|
returns: list of photos exported
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -273,6 +275,7 @@ def export(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
|
|
||||||
return results.exported
|
return results.exported
|
||||||
@@ -297,6 +300,7 @@ def export2(
|
|||||||
use_albums_as_keywords=False,
|
use_albums_as_keywords=False,
|
||||||
use_persons_as_keywords=False,
|
use_persons_as_keywords=False,
|
||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
update=False,
|
update=False,
|
||||||
export_db=None,
|
export_db=None,
|
||||||
fileutil=FileUtil,
|
fileutil=FileUtil,
|
||||||
@@ -336,6 +340,7 @@ def export2(
|
|||||||
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||||
when exporting metadata with exiftool or sidecar
|
when exporting metadata with exiftool or sidecar
|
||||||
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||||
|
description_template: string; optional template string that will be rendered for use as photo description
|
||||||
update: (boolean, default=False); if True export will run in update mode, that is, it will
|
update: (boolean, default=False); if True export will run in update mode, that is, it will
|
||||||
not export the photo if the current version already exists in the destination
|
not export the photo if the current version already exists in the destination
|
||||||
export_db: (ExportDB_ABC); instance of a class that conforms to ExportDB_ABC with methods
|
export_db: (ExportDB_ABC); instance of a class that conforms to ExportDB_ABC with methods
|
||||||
@@ -670,6 +675,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
try:
|
try:
|
||||||
@@ -685,6 +691,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
try:
|
try:
|
||||||
@@ -712,6 +719,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
)[0]
|
)[0]
|
||||||
if old_data != current_data:
|
if old_data != current_data:
|
||||||
@@ -727,6 +735,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
export_db.set_exifdata_for_file(
|
export_db.set_exifdata_for_file(
|
||||||
exported_file,
|
exported_file,
|
||||||
@@ -734,6 +743,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
export_db.set_stat_exif_for_file(
|
export_db.set_stat_exif_for_file(
|
||||||
@@ -749,6 +759,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
export_db.set_exifdata_for_file(
|
export_db.set_exifdata_for_file(
|
||||||
exported_file,
|
exported_file,
|
||||||
@@ -756,6 +767,7 @@ def export2(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
export_db.set_stat_exif_for_file(
|
export_db.set_stat_exif_for_file(
|
||||||
@@ -955,6 +967,7 @@ def _write_exif_data(
|
|||||||
use_albums_as_keywords=False,
|
use_albums_as_keywords=False,
|
||||||
use_persons_as_keywords=False,
|
use_persons_as_keywords=False,
|
||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
):
|
):
|
||||||
""" write exif data to image file at filepath
|
""" write exif data to image file at filepath
|
||||||
filepath: full path to the image file """
|
filepath: full path to the image file """
|
||||||
@@ -966,6 +979,7 @@ def _write_exif_data(
|
|||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
)
|
)
|
||||||
)[0]
|
)[0]
|
||||||
for exiftag, val in exif_info.items():
|
for exiftag, val in exif_info.items():
|
||||||
@@ -984,6 +998,7 @@ def _exiftool_json_sidecar(
|
|||||||
use_albums_as_keywords=False,
|
use_albums_as_keywords=False,
|
||||||
use_persons_as_keywords=False,
|
use_persons_as_keywords=False,
|
||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
):
|
):
|
||||||
""" 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
|
Does not include all the EXIF fields as those are likely already in the image
|
||||||
@@ -1009,7 +1024,13 @@ def _exiftool_json_sidecar(
|
|||||||
exif = {}
|
exif = {}
|
||||||
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
|
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
|
||||||
|
|
||||||
if self.description:
|
if description_template is not None:
|
||||||
|
description = self.render_template(
|
||||||
|
description_template, expand_inplace=True, inplace_sep=", "
|
||||||
|
)[0]
|
||||||
|
exif["EXIF:ImageDescription"] = description
|
||||||
|
exif["XMP:Description"] = description
|
||||||
|
elif self.description:
|
||||||
exif["EXIF:ImageDescription"] = self.description
|
exif["EXIF:ImageDescription"] = self.description
|
||||||
exif["XMP:Description"] = self.description
|
exif["XMP:Description"] = self.description
|
||||||
|
|
||||||
@@ -1082,7 +1103,6 @@ def _exiftool_json_sidecar(
|
|||||||
lat_str, lon_str = dd_to_dms_str(lat, lon)
|
lat_str, lon_str = dd_to_dms_str(lat, lon)
|
||||||
exif["EXIF:GPSLatitude"] = lat_str
|
exif["EXIF:GPSLatitude"] = lat_str
|
||||||
exif["EXIF:GPSLongitude"] = lon_str
|
exif["EXIF:GPSLongitude"] = lon_str
|
||||||
exif["Composite:GPSPosition"] = f"{lat_str}, {lon_str}"
|
|
||||||
lat_ref = "North" if lat >= 0 else "South"
|
lat_ref = "North" if lat >= 0 else "South"
|
||||||
lon_ref = "East" if lon >= 0 else "West"
|
lon_ref = "East" if lon >= 0 else "West"
|
||||||
exif["EXIF:GPSLatitudeRef"] = lat_ref
|
exif["EXIF:GPSLatitudeRef"] = lat_ref
|
||||||
@@ -1112,16 +1132,25 @@ def _xmp_sidecar(
|
|||||||
use_albums_as_keywords=False,
|
use_albums_as_keywords=False,
|
||||||
use_persons_as_keywords=False,
|
use_persons_as_keywords=False,
|
||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
):
|
):
|
||||||
""" returns string for XMP sidecar
|
""" returns string for XMP sidecar
|
||||||
use_albums_as_keywords: treat album names as keywords
|
use_albums_as_keywords: treat album names as keywords
|
||||||
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
|
||||||
|
description_template: string; optional template string that will be rendered for use as photo description """
|
||||||
|
|
||||||
# TODO: add additional fields to XMP file?
|
# TODO: add additional fields to XMP file?
|
||||||
|
|
||||||
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME))
|
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME))
|
||||||
|
|
||||||
|
if description_template is not None:
|
||||||
|
description = self.render_template(
|
||||||
|
description_template, expand_inplace=True, inplace_sep=", "
|
||||||
|
)[0]
|
||||||
|
else:
|
||||||
|
description = self.description if self.description is not None else ""
|
||||||
|
|
||||||
keyword_list = []
|
keyword_list = []
|
||||||
if self.keywords:
|
if self.keywords:
|
||||||
keyword_list.extend(self.keywords)
|
keyword_list.extend(self.keywords)
|
||||||
@@ -1178,7 +1207,11 @@ def _xmp_sidecar(
|
|||||||
subject_list = list(self.keywords) + person_list
|
subject_list = list(self.keywords) + person_list
|
||||||
|
|
||||||
xmp_str = xmp_template.render(
|
xmp_str = xmp_template.render(
|
||||||
photo=self, keywords=keyword_list, persons=person_list, subjects=subject_list
|
photo=self,
|
||||||
|
description=description,
|
||||||
|
keywords=keyword_list,
|
||||||
|
persons=person_list,
|
||||||
|
subjects=subject_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
# remove extra lines that mako inserts from template
|
# remove extra lines that mako inserts from template
|
||||||
|
|||||||
@@ -642,7 +642,49 @@ class PhotoInfo:
|
|||||||
otherwise returns False """
|
otherwise returns False """
|
||||||
return self._info["raw_is_original"]
|
return self._info["raw_is_original"]
|
||||||
|
|
||||||
def render_template(self, template_str, none_str="_", path_sep=None):
|
@property
|
||||||
|
def height(self):
|
||||||
|
""" returns height of the current photo version in pixels """
|
||||||
|
return self._info["height"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
""" returns width of the current photo version in pixels """
|
||||||
|
return self._info["width"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def orientation(self):
|
||||||
|
""" returns EXIF orientation of the current photo version as int """
|
||||||
|
return self._info["orientation"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def original_height(self):
|
||||||
|
""" returns height of the original photo version in pixels """
|
||||||
|
return self._info["original_height"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def original_width(self):
|
||||||
|
""" returns width of the original photo version in pixels """
|
||||||
|
return self._info["original_width"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def original_orientation(self):
|
||||||
|
""" returns EXIF orientation of the original photo version as int """
|
||||||
|
return self._info["original_orientation"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def original_filesize(self):
|
||||||
|
""" returns filesize of original photo in bytes as int """
|
||||||
|
return self._info["original_filesize"]
|
||||||
|
|
||||||
|
def render_template(
|
||||||
|
self,
|
||||||
|
template_str,
|
||||||
|
none_str="_",
|
||||||
|
path_sep=None,
|
||||||
|
expand_inplace=False,
|
||||||
|
inplace_sep=None,
|
||||||
|
):
|
||||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -650,12 +692,22 @@ class PhotoInfo:
|
|||||||
none_str: a str to use if template field renders to None, default is "_".
|
none_str: a str to use if template field renders to None, default is "_".
|
||||||
path_sep: a single character str to use as path separator when joining
|
path_sep: a single character str to use as path separator when joining
|
||||||
fields like folder_album; if not provided, defaults to os.path.sep
|
fields like folder_album; if not provided, defaults to os.path.sep
|
||||||
|
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||||
|
instead of returning individual strings
|
||||||
|
inplace_sep: optional string to use as separator between multi-valued keywords
|
||||||
|
with expand_inplace; default is ','
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
"""
|
"""
|
||||||
template = PhotoTemplate(self)
|
template = PhotoTemplate(self)
|
||||||
return template.render(template_str, none_str, path_sep)
|
return template.render(
|
||||||
|
template_str,
|
||||||
|
none_str=none_str,
|
||||||
|
path_sep=path_sep,
|
||||||
|
expand_inplace=expand_inplace,
|
||||||
|
inplace_sep=inplace_sep,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _longitude(self):
|
def _longitude(self):
|
||||||
|
|||||||
@@ -716,7 +716,14 @@ class PhotosDB:
|
|||||||
RKVersion.rawMasterUuid,
|
RKVersion.rawMasterUuid,
|
||||||
RKVersion.nonRawMasterUuid,
|
RKVersion.nonRawMasterUuid,
|
||||||
RKMaster.alternateMasterUuid,
|
RKMaster.alternateMasterUuid,
|
||||||
RKVersion.isInTrash
|
RKVersion.isInTrash,
|
||||||
|
RKVersion.processedHeight,
|
||||||
|
RKVersion.processedWidth,
|
||||||
|
RKVersion.orientation,
|
||||||
|
RKMaster.height,
|
||||||
|
RKMaster.width,
|
||||||
|
RKMaster.orientation,
|
||||||
|
RKMaster.fileSize
|
||||||
FROM RKVersion, RKMaster
|
FROM RKVersion, RKMaster
|
||||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||||
)
|
)
|
||||||
@@ -736,7 +743,14 @@ class PhotosDB:
|
|||||||
RKVersion.rawMasterUuid,
|
RKVersion.rawMasterUuid,
|
||||||
RKVersion.nonRawMasterUuid,
|
RKVersion.nonRawMasterUuid,
|
||||||
RKMaster.alternateMasterUuid,
|
RKMaster.alternateMasterUuid,
|
||||||
RKVersion.isInTrash
|
RKVersion.isInTrash,
|
||||||
|
RKVersion.processedHeight,
|
||||||
|
RKVersion.processedWidth,
|
||||||
|
RKVersion.orientation,
|
||||||
|
RKMaster.height,
|
||||||
|
RKMaster.width,
|
||||||
|
RKMaster.orientation,
|
||||||
|
RKMaster.originalFileSize
|
||||||
FROM RKVersion, RKMaster
|
FROM RKVersion, RKMaster
|
||||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||||
)
|
)
|
||||||
@@ -775,6 +789,13 @@ class PhotosDB:
|
|||||||
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
||||||
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
||||||
# 32 RKVersion.isInTrash
|
# 32 RKVersion.isInTrash
|
||||||
|
# 33 RKVersion.processedHeight,
|
||||||
|
# 34 RKVersion.processedWidth,
|
||||||
|
# 35 RKVersion.orientation,
|
||||||
|
# 36 RKMaster.height,
|
||||||
|
# 37 RKMaster.width,
|
||||||
|
# 38 RKMaster.orientation,
|
||||||
|
# 39 RKMaster.originalFileSize
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -920,6 +941,15 @@ class PhotosDB:
|
|||||||
# recently deleted items
|
# recently deleted items
|
||||||
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||||
|
|
||||||
|
# height/width/orientation
|
||||||
|
self._dbphotos[uuid]["height"] = row[33]
|
||||||
|
self._dbphotos[uuid]["width"] = row[34]
|
||||||
|
self._dbphotos[uuid]["orientation"] = row[35]
|
||||||
|
self._dbphotos[uuid]["original_height"] = row[36]
|
||||||
|
self._dbphotos[uuid]["original_width"] = row[37]
|
||||||
|
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||||
|
self._dbphotos[uuid]["original_filesize"] = row[39]
|
||||||
|
|
||||||
# get additional details from RKMaster, needed for RAW processing
|
# get additional details from RKMaster, needed for RAW processing
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
@@ -1457,7 +1487,14 @@ class PhotosDB:
|
|||||||
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
||||||
ZGENERICASSET.ZMOMENT,
|
ZGENERICASSET.ZMOMENT,
|
||||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||||
ZGENERICASSET.ZTRASHEDSTATE
|
ZGENERICASSET.ZTRASHEDSTATE,
|
||||||
|
ZGENERICASSET.ZHEIGHT,
|
||||||
|
ZGENERICASSET.ZWIDTH,
|
||||||
|
ZGENERICASSET.ZORIENTATION,
|
||||||
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALHEIGHT,
|
||||||
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
|
||||||
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||||
|
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||||
FROM ZGENERICASSET
|
FROM ZGENERICASSET
|
||||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||||
ORDER BY ZGENERICASSET.ZUUID """
|
ORDER BY ZGENERICASSET.ZUUID """
|
||||||
@@ -1493,6 +1530,13 @@ class PhotosDB:
|
|||||||
# 26 ZGENERICASSET.ZMOMENT -- FK for ZMOMENT.Z_PK
|
# 26 ZGENERICASSET.ZMOMENT -- FK for ZMOMENT.Z_PK
|
||||||
# 27 ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE -- 1 if associated RAW image is original else 0
|
# 27 ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE -- 1 if associated RAW image is original else 0
|
||||||
# 28 ZGENERICASSET.ZTRASHEDSTATE -- 0 if not in trash, 1 if in trash
|
# 28 ZGENERICASSET.ZTRASHEDSTATE -- 0 if not in trash, 1 if in trash
|
||||||
|
# 29 ZGENERICASSET.ZHEIGHT,
|
||||||
|
# 30 ZGENERICASSET.ZWIDTH,
|
||||||
|
# 31 ZGENERICASSET.ZORIENTATION,
|
||||||
|
# 32 ZADDITIONALASSETATTRIBUTES.ZORIGINALHEIGHT,
|
||||||
|
# 33 ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
|
||||||
|
# 34 ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||||
|
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -1644,6 +1688,15 @@ class PhotosDB:
|
|||||||
# recently deleted items
|
# recently deleted items
|
||||||
info["intrash"] = True if row[28] == 1 else False
|
info["intrash"] = True if row[28] == 1 else False
|
||||||
|
|
||||||
|
# height/width/orientation
|
||||||
|
info["height"] = row[29]
|
||||||
|
info["width"] = row[30]
|
||||||
|
info["orientation"] = row[31]
|
||||||
|
info["original_height"] = row[32]
|
||||||
|
info["original_width"] = row[33]
|
||||||
|
info["original_orientation"] = row[34]
|
||||||
|
info["original_filesize"] = row[35]
|
||||||
|
|
||||||
# associated RAW image info
|
# associated RAW image info
|
||||||
# will be filled in later
|
# will be filled in later
|
||||||
info["has_raw"] = False
|
info["has_raw"] = False
|
||||||
@@ -2232,8 +2285,7 @@ class PhotosDB:
|
|||||||
logging.debug(f"Could not find person '{person}' in database")
|
logging.debug(f"Could not find person '{person}' in database")
|
||||||
photos_sets.append(person_set)
|
photos_sets.append(person_set)
|
||||||
|
|
||||||
# sourcery off
|
if from_date or to_date: # sourcery off
|
||||||
if from_date or to_date:
|
|
||||||
dsel = self._dbphotos
|
dsel = self._dbphotos
|
||||||
if from_date:
|
if from_date:
|
||||||
dsel = {
|
dsel = {
|
||||||
|
|||||||
@@ -124,13 +124,24 @@ class PhotoTemplate:
|
|||||||
# gets initialized in get_template_value
|
# gets initialized in get_template_value
|
||||||
self.today = None
|
self.today = None
|
||||||
|
|
||||||
def render(self, template, none_str="_", path_sep=None):
|
def render(
|
||||||
|
self,
|
||||||
|
template,
|
||||||
|
none_str="_",
|
||||||
|
path_sep=None,
|
||||||
|
expand_inplace=False,
|
||||||
|
inplace_sep=None,
|
||||||
|
):
|
||||||
""" Render a filename or directory template
|
""" Render a filename or directory template
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
template: str template
|
template: str template
|
||||||
none_str: str to use default for None values, default is '_'
|
none_str: str to use default for None values, default is '_'
|
||||||
path_sep: optional character to use as path separator, default is os.path.sep
|
path_sep: optional character to use as path separator, default is os.path.sep
|
||||||
|
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||||
|
instead of returning individual strings
|
||||||
|
inplace_sep: optional string to use as separator between multi-valued keywords
|
||||||
|
with expand_inplace; default is ','
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
@@ -141,6 +152,9 @@ class PhotoTemplate:
|
|||||||
elif path_sep is not None and len(path_sep) != 1:
|
elif path_sep is not None and len(path_sep) != 1:
|
||||||
raise ValueError(f"path_sep must be single character: {path_sep}")
|
raise ValueError(f"path_sep must be single character: {path_sep}")
|
||||||
|
|
||||||
|
if inplace_sep is None:
|
||||||
|
inplace_sep = ","
|
||||||
|
|
||||||
# the rendering happens in two phases:
|
# the rendering happens in two phases:
|
||||||
# phase 1: handle all the single-value template substitutions
|
# phase 1: handle all the single-value template substitutions
|
||||||
# results in a single string with all the template fields replaced
|
# results in a single string with all the template fields replaced
|
||||||
@@ -226,13 +240,19 @@ class PhotoTemplate:
|
|||||||
for str_template in rendered_strings:
|
for str_template in rendered_strings:
|
||||||
if regex_multi.search(str_template):
|
if regex_multi.search(str_template):
|
||||||
values = self.get_template_value_multi(field, path_sep)
|
values = self.get_template_value_multi(field, path_sep)
|
||||||
for val in values:
|
if expand_inplace:
|
||||||
|
# instead of returning multiple strings, join values into a single string
|
||||||
|
val = (
|
||||||
|
inplace_sep.join(sorted(values))
|
||||||
|
if values and values[0]
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def lookup_template_value_multi(lookup_value, default):
|
def lookup_template_value_multi(lookup_value, default):
|
||||||
""" Closure passed to make_subst_function get_func
|
""" Closure passed to make_subst_function get_func
|
||||||
Capture val and field in the closure
|
Capture val and field in the closure
|
||||||
Allows make_subst_function to be re-used w/o modification
|
Allows make_subst_function to be re-used w/o modification
|
||||||
default is not used but required so signature matches get_template_value """
|
default is not used but required so signature matches get_template_value """
|
||||||
if lookup_value == field:
|
if lookup_value == field:
|
||||||
return val
|
return val
|
||||||
else:
|
else:
|
||||||
@@ -242,10 +262,33 @@ class PhotoTemplate:
|
|||||||
self, none_str, get_func=lookup_template_value_multi
|
self, none_str, get_func=lookup_template_value_multi
|
||||||
)
|
)
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
new_strings.add(new_string)
|
|
||||||
|
|
||||||
# update rendered_strings for the next field to process
|
# update rendered_strings for the next field to process
|
||||||
rendered_strings = new_strings
|
rendered_strings = {new_string}
|
||||||
|
else:
|
||||||
|
# create a new template string for each value
|
||||||
|
for val in values:
|
||||||
|
|
||||||
|
def lookup_template_value_multi(lookup_value, default):
|
||||||
|
""" Closure passed to make_subst_function get_func
|
||||||
|
Capture val and field in the closure
|
||||||
|
Allows make_subst_function to be re-used w/o modification
|
||||||
|
default is not used but required so signature matches get_template_value """
|
||||||
|
if lookup_value == field:
|
||||||
|
return val
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected value: {lookup_value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
subst = make_subst_function(
|
||||||
|
self, none_str, get_func=lookup_template_value_multi
|
||||||
|
)
|
||||||
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
|
new_strings.add(new_string)
|
||||||
|
|
||||||
|
# update rendered_strings for the next field to process
|
||||||
|
rendered_strings = new_strings
|
||||||
|
|
||||||
# find any {fields} that weren't replaced
|
# find any {fields} that weren't replaced
|
||||||
unmatched = []
|
unmatched = []
|
||||||
|
|||||||
@@ -71,29 +71,42 @@
|
|||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="gps_info(latitude, longitude)">
|
||||||
|
% if latitude is not None and longitude is not None:
|
||||||
|
<exif:GPSLongitudeRef>${"E" if longitude >= 0 else "W"}</exif:GPSLongitudeRef>
|
||||||
|
<exif:GPSLongitude>${abs(longitude)}</exif:GPSLongitude>
|
||||||
|
<exif:GPSLatitude>${abs(latitude)}</exif:GPSLatitude>
|
||||||
|
<exif:GPSLatitudeRef>${"N" if latitude >= 0 else "S"}</exif:GPSLatitudeRef>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
|
||||||
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
|
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
|
||||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||||
${dc_description(photo.description)}
|
${dc_description(description)}
|
||||||
${dc_title(photo.title)}
|
${dc_title(photo.title)}
|
||||||
${dc_subject(subjects)}
|
${dc_subject(subjects)}
|
||||||
${dc_datecreated(photo.date)}
|
${dc_datecreated(photo.date)}
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
${iptc_personinimage(persons)}
|
${iptc_personinimage(persons)}
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
${dk_tagslist(keywords)}
|
${dk_tagslist(keywords)}
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
${adobe_createdate(photo.date)}
|
${adobe_createdate(photo.date)}
|
||||||
${adobe_modifydate(photo.date)}
|
${adobe_modifydate(photo.date)}
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
${gps_info(*photo.location)}
|
||||||
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>
|
</x:xmpmeta>
|
||||||
@@ -241,6 +241,46 @@ def test_attributes():
|
|||||||
assert p.ismissing == False
|
assert p.ismissing == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_attributes_2():
|
||||||
|
""" Test attributes including height, width, etc """
|
||||||
|
import datetime
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
assert p.keywords == ["wedding"]
|
||||||
|
assert p.original_filename == "wedding.jpg"
|
||||||
|
assert p.filename == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg"
|
||||||
|
assert p.date == datetime.datetime(
|
||||||
|
2019,
|
||||||
|
4,
|
||||||
|
15,
|
||||||
|
14,
|
||||||
|
40,
|
||||||
|
24,
|
||||||
|
86000,
|
||||||
|
datetime.timezone(datetime.timedelta(seconds=-14400)),
|
||||||
|
)
|
||||||
|
assert p.description == "Bride Wedding day"
|
||||||
|
assert p.title is None
|
||||||
|
assert sorted(p.albums) == ["AlbumInFolder", "I have a deleted twin"]
|
||||||
|
assert p.persons == ["Maria"]
|
||||||
|
assert p.path.endswith(
|
||||||
|
"tests/Test-10.15.5.photoslibrary/originals/E/E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg"
|
||||||
|
)
|
||||||
|
assert not p.ismissing
|
||||||
|
assert p.hasadjustments
|
||||||
|
assert p.height == 1325
|
||||||
|
assert p.width == 1526
|
||||||
|
assert p.original_height == 1367
|
||||||
|
assert p.original_width == 2048
|
||||||
|
assert p.orientation == 1
|
||||||
|
assert p.original_orientation == 1
|
||||||
|
assert p.original_filesize == 460483
|
||||||
|
|
||||||
|
|
||||||
def test_missing():
|
def test_missing():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,13 @@ CLI_EXPORT_RAW_EDITED = [
|
|||||||
]
|
]
|
||||||
CLI_EXPORT_RAW_EDITED_ORIGINAL = ["IMG_0476_2.CR2", "IMG_0476_2_edited.jpeg"]
|
CLI_EXPORT_RAW_EDITED_ORIGINAL = ["IMG_0476_2.CR2", "IMG_0476_2_edited.jpeg"]
|
||||||
|
|
||||||
CLI_UUID_DICT_15_5 = {"intrash": "71E3E212-00EB-430D-8A63-5E294B268554"}
|
CLI_UUID_DICT_15_5 = {
|
||||||
|
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||||
|
"template": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||||
|
}
|
||||||
|
|
||||||
|
CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.json"
|
||||||
|
|
||||||
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
||||||
|
|
||||||
PHOTOS_NOT_IN_TRASH_LEN_14_6 = 7
|
PHOTOS_NOT_IN_TRASH_LEN_14_6 = 7
|
||||||
@@ -1095,6 +1101,48 @@ def test_export_sidecar():
|
|||||||
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
|
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_sidecar_templates():
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
from osxphotos.__main__ import cli
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"export",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||||
|
".",
|
||||||
|
"--sidecar=json",
|
||||||
|
f"--uuid={CLI_UUID_DICT_15_5['template']}",
|
||||||
|
"-V",
|
||||||
|
"--keyword-template",
|
||||||
|
"{person}",
|
||||||
|
"--description-template",
|
||||||
|
"{descr} {person} {keyword} {album}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert os.path.isfile(CLI_TEMPLATE_SIDECAR_FILENAME)
|
||||||
|
with open(CLI_TEMPLATE_SIDECAR_FILENAME, "r") as jsonfile:
|
||||||
|
exifdata = json.load(jsonfile)
|
||||||
|
assert (
|
||||||
|
exifdata[0]["XMP:Description"][0]
|
||||||
|
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
exifdata[0]["EXIF:ImageDescription"][0]
|
||||||
|
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_export_live():
|
def test_export_live():
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -455,7 +455,6 @@ def test_exiftool_json_sidecar():
|
|||||||
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
|
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
|
||||||
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
|
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
|
||||||
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
|
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
|
||||||
"Composite:GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
|
|
||||||
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
|
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
|
||||||
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
|
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
|
||||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||||
@@ -586,7 +585,7 @@ def test_xmp_sidecar():
|
|||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
@@ -595,7 +594,7 @@ def test_xmp_sidecar():
|
|||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -603,10 +602,13 @@ def test_xmp_sidecar():
|
|||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
@@ -647,7 +649,7 @@ def test_xmp_sidecar_use_persons_keyword():
|
|||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
@@ -656,7 +658,7 @@ def test_xmp_sidecar_use_persons_keyword():
|
|||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -666,11 +668,14 @@ def test_xmp_sidecar_use_persons_keyword():
|
|||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
|
|
||||||
@@ -710,7 +715,7 @@ def test_xmp_sidecar_use_albums_keyword():
|
|||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
@@ -719,7 +724,7 @@ def test_xmp_sidecar_use_albums_keyword():
|
|||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -729,11 +734,14 @@ def test_xmp_sidecar_use_albums_keyword():
|
|||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
|
|
||||||
@@ -746,3 +754,75 @@ def test_xmp_sidecar_use_albums_keyword():
|
|||||||
sorted(xmp_expected_lines), sorted(xmp_got_lines)
|
sorted(xmp_expected_lines), sorted(xmp_got_lines)
|
||||||
):
|
):
|
||||||
assert line_expected == line_got
|
assert line_expected == line_got
|
||||||
|
|
||||||
|
|
||||||
|
def test_xmp_sidecar_gps():
|
||||||
|
""" Test export XMP sidecar with GPS info """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["location"]])
|
||||||
|
|
||||||
|
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></dc:description>
|
||||||
|
<dc:title>St. James's Park</dc:title>
|
||||||
|
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>UK</rdf:li>
|
||||||
|
<rdf:li>England</rdf:li>
|
||||||
|
<rdf:li>London</rdf:li>
|
||||||
|
<rdf:li>United Kingdom</rdf:li>
|
||||||
|
<rdf:li>London 2018</rdf:li>
|
||||||
|
<rdf:li>St. James's Park</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</dc:subject>
|
||||||
|
<photoshop:DateCreated>2018-10-13T09:18:12.501000-04:00</photoshop:DateCreated>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
|
<digiKam:TagsList>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>UK</rdf:li>
|
||||||
|
<rdf:li>England</rdf:li>
|
||||||
|
<rdf:li>London</rdf:li>
|
||||||
|
<rdf:li>United Kingdom</rdf:li>
|
||||||
|
<rdf:li>London 2018</rdf:li>
|
||||||
|
<rdf:li>St. James's Park</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</digiKam:TagsList>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
|
<xmp:CreateDate>2018-10-13T09:18:12</xmp:CreateDate>
|
||||||
|
<xmp:ModifyDate>2018-10-13T09:18:12</xmp:ModifyDate>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
<exif:GPSLongitudeRef>W</exif:GPSLongitudeRef>
|
||||||
|
<exif:GPSLongitude>0.1318055</exif:GPSLongitude>
|
||||||
|
<exif:GPSLatitude>51.50357167</exif:GPSLatitude>
|
||||||
|
<exif:GPSLatitudeRef>N</exif:GPSLatitudeRef>
|
||||||
|
</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(
|
||||||
|
sorted(xmp_expected_lines), sorted(xmp_got_lines)
|
||||||
|
):
|
||||||
|
assert line_expected == line_got
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ def test_xmp_sidecar_keyword_template():
|
|||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
@@ -187,7 +187,7 @@ def test_xmp_sidecar_keyword_template():
|
|||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -198,11 +198,14 @@ def test_xmp_sidecar_keyword_template():
|
|||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
|
|
||||||
|
|||||||
@@ -380,7 +380,6 @@ def test_exiftool_json_sidecar():
|
|||||||
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
|
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
|
||||||
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
|
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
|
||||||
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
|
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
|
||||||
"Composite:GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
|
|
||||||
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
|
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
|
||||||
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
|
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
|
||||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||||
@@ -431,7 +430,7 @@ def test_xmp_sidecar():
|
|||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
@@ -440,7 +439,7 @@ def test_xmp_sidecar():
|
|||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -448,11 +447,14 @@ def test_xmp_sidecar():
|
|||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
|
|
||||||
@@ -490,7 +492,7 @@ def test_xmp_sidecar_keyword_template():
|
|||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
@@ -499,7 +501,7 @@ def test_xmp_sidecar_keyword_template():
|
|||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -510,11 +512,14 @@ def test_xmp_sidecar_keyword_template():
|
|||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
<rdf:Description rdf:about=""
|
||||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ UUID_DICT = {
|
|||||||
"date_invalid": "YZFCPY24TUySvpu7owiqxA",
|
"date_invalid": "YZFCPY24TUySvpu7owiqxA",
|
||||||
"intrash": "3tljdX43R8+k6peNHVrJNQ",
|
"intrash": "3tljdX43R8+k6peNHVrJNQ",
|
||||||
"not_intrash": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
"not_intrash": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||||
|
"has_adjustments": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||||
}
|
}
|
||||||
|
|
||||||
PHOTOS_DB_LEN = 8
|
PHOTOS_DB_LEN = 8
|
||||||
@@ -161,6 +162,44 @@ def test_attributes():
|
|||||||
assert p.ismissing == False
|
assert p.ismissing == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_attributes_2():
|
||||||
|
""" Test attributes including height, width, etc """
|
||||||
|
import datetime
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
assert p.keywords == ["wedding"]
|
||||||
|
assert p.original_filename == "wedding.jpg"
|
||||||
|
assert p.filename == "wedding.jpg"
|
||||||
|
assert p.date == datetime.datetime(
|
||||||
|
2019,
|
||||||
|
4,
|
||||||
|
15,
|
||||||
|
14,
|
||||||
|
40,
|
||||||
|
24,
|
||||||
|
86000,
|
||||||
|
datetime.timezone(datetime.timedelta(seconds=-14400)),
|
||||||
|
)
|
||||||
|
assert p.description == "Bride Wedding day"
|
||||||
|
assert p.title is None
|
||||||
|
assert sorted(p.albums) == []
|
||||||
|
assert p.persons == ["Maria"]
|
||||||
|
assert p.path.endswith("Masters/2019/07/27/20190727-131650/wedding.jpg")
|
||||||
|
assert not p.ismissing
|
||||||
|
assert p.hasadjustments
|
||||||
|
assert p.height == 1325
|
||||||
|
assert p.width == 1526
|
||||||
|
assert p.original_height == 1367
|
||||||
|
assert p.original_width == 2048
|
||||||
|
assert p.orientation == 1
|
||||||
|
assert p.original_orientation == 1
|
||||||
|
assert p.original_filesize == 460483
|
||||||
|
|
||||||
|
|
||||||
def test_missing():
|
def test_missing():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
@@ -168,8 +207,8 @@ def test_missing():
|
|||||||
photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"])
|
photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"])
|
||||||
assert len(photos) == 1
|
assert len(photos) == 1
|
||||||
p = photos[0]
|
p = photos[0]
|
||||||
assert p.path == None
|
assert p.path is None
|
||||||
assert p.ismissing == True
|
assert p.ismissing
|
||||||
|
|
||||||
|
|
||||||
def test_favorite():
|
def test_favorite():
|
||||||
|
|||||||
@@ -458,3 +458,48 @@ def test_subst_strftime():
|
|||||||
|
|
||||||
rendered, unmatched = photo.render_template("{created.strftime}")
|
rendered, unmatched = photo.render_template("{created.strftime}")
|
||||||
assert rendered[0] == "_"
|
assert rendered[0] == "_"
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_expand_inplace_1():
|
||||||
|
""" Test that substitutions are correct when expand_inplace=True """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||||
|
# one album, one keyword, two persons
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||||
|
|
||||||
|
template = "{person}"
|
||||||
|
expected = ["Katie,Suzy"]
|
||||||
|
rendered, unknown = photo.render_template(template, expand_inplace=True)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_expand_inplace_2():
|
||||||
|
""" Test that substitutions are correct when expand_inplace=True """
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||||
|
# one album, one keyword, two persons
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||||
|
|
||||||
|
template = "{person}-{keyword}"
|
||||||
|
expected = ["Katie,Suzy-Kids"]
|
||||||
|
rendered, unknown = photo.render_template(template, expand_inplace=True)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_expand_inplace_3():
|
||||||
|
""" Test that substitutions are correct when expand_inplace=True and inplace_sep specified"""
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||||
|
# one album, one keyword, two persons
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||||
|
|
||||||
|
template = "{person}-{keyword}"
|
||||||
|
expected = ["Katie; Suzy-Kids"]
|
||||||
|
rendered, unknown = photo.render_template(
|
||||||
|
template, expand_inplace=True, inplace_sep="; "
|
||||||
|
)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user