Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffb9af1965 | ||
|
|
595307a003 | ||
|
|
79a50b9e50 | ||
|
|
515df0a5dc | ||
|
|
63bfa92563 | ||
|
|
44a1e3e7a7 | ||
|
|
6c84e476cc | ||
|
|
14fbe5e068 | ||
|
|
ebac9d0bfb | ||
|
|
29716c5272 | ||
|
|
fbe8229103 | ||
|
|
5ee6affc05 | ||
|
|
b3a7869bd3 | ||
|
|
e5f1c29974 | ||
|
|
70848e1ff6 | ||
|
|
4b7a53faa8 | ||
|
|
a78dd80af4 | ||
|
|
1316866dc4 | ||
|
|
30273509d4 | ||
|
|
15a3e69015 | ||
|
|
2691902d5c | ||
|
|
da47821fae | ||
|
|
6f38e2da49 | ||
|
|
857e3db6cc | ||
|
|
7ed3115f36 | ||
|
|
198addaa07 | ||
|
|
d91fc93737 | ||
|
|
5c3360f29d | ||
|
|
d4513832a6 | ||
|
|
f8616acf16 | ||
|
|
addd952aa3 | ||
|
|
773b619e24 | ||
|
|
683dfe7f3f | ||
|
|
7fa5fbaa5b | ||
|
|
e075868281 | ||
|
|
bf0589118b | ||
|
|
adc4b05602 | ||
|
|
a740d82e7f | ||
|
|
43af4d205a | ||
|
|
4d98fa9279 | ||
|
|
67e579be4c | ||
|
|
48d2223edd | ||
|
|
591f9bcc62 | ||
|
|
2284598a24 |
@@ -175,6 +175,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "neilpa",
|
||||||
|
"name": "Neil Pankey",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/42419?v=4",
|
||||||
|
"profile": "https://neilpa.me",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.metrics
|
||||||
|
.DS_store
|
||||||
|
__pycache__
|
||||||
|
.coverage
|
||||||
|
.condaauto
|
||||||
|
t.out
|
||||||
|
.vscode/
|
||||||
|
.tox/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
working/
|
||||||
|
osxphotos.egg-info/
|
||||||
|
.mypy_cache/
|
||||||
|
cli.spec
|
||||||
|
*.pyc
|
||||||
|
docsrc/_build/
|
||||||
108
CHANGELOG.md
@@ -4,6 +4,114 @@ 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.41.0](https://github.com/RhetTbull/osxphotos/compare/v0.40.19...v0.41.0)
|
||||||
|
|
||||||
|
> 22 February 2021
|
||||||
|
|
||||||
|
- Template refactor [`#385`](https://github.com/RhetTbull/osxphotos/pull/385)
|
||||||
|
|
||||||
|
#### [v0.40.19](https://github.com/RhetTbull/osxphotos/compare/v0.40.18...v0.40.19)
|
||||||
|
|
||||||
|
> 20 February 2021
|
||||||
|
|
||||||
|
- Better exception handling for AdjustmentsInfo [`44a1e3e`](https://github.com/RhetTbull/osxphotos/commit/44a1e3e7a7f765bf91c2341e423ec9e5a9e3c1bd)
|
||||||
|
|
||||||
|
#### [v0.40.18](https://github.com/RhetTbull/osxphotos/compare/v0.40.17...v0.40.18)
|
||||||
|
|
||||||
|
> 20 February 2021
|
||||||
|
|
||||||
|
- docs: add neilpa as a contributor [`#383`](https://github.com/RhetTbull/osxphotos/pull/383)
|
||||||
|
- Added AdjustmentsInfo, #150, #379 [`5ee6aff`](https://github.com/RhetTbull/osxphotos/commit/5ee6affc0525db1975cb5095f62494ef10d92f7e)
|
||||||
|
- docs: update .all-contributorsrc [skip ci] [`ebac9d0`](https://github.com/RhetTbull/osxphotos/commit/ebac9d0bfb43f59f046aacdd0290d1fcd29a3b5e)
|
||||||
|
- docs: update README.md [skip ci] [`29716c5`](https://github.com/RhetTbull/osxphotos/commit/29716c52726a4e699c03d43ecc67db57f55b36f8)
|
||||||
|
- Version bump [`fbe8229`](https://github.com/RhetTbull/osxphotos/commit/fbe822910370652975ab83b82344169df4c3027c)
|
||||||
|
|
||||||
|
#### [v0.40.17](https://github.com/RhetTbull/osxphotos/compare/v0.40.16...v0.40.17)
|
||||||
|
|
||||||
|
> 18 February 2021
|
||||||
|
|
||||||
|
- Updated docs for --ignore-signature, #286 [`e5f1c29`](https://github.com/RhetTbull/osxphotos/commit/e5f1c299742fcfa0a855a33df7b266aa2c39e48b)
|
||||||
|
- Added depth_state to _info [`b3a7869`](https://github.com/RhetTbull/osxphotos/commit/b3a7869bd3cc13e40cb3f68ff8caf12edda9a49c)
|
||||||
|
|
||||||
|
#### [v0.40.16](https://github.com/RhetTbull/osxphotos/compare/v0.40.14...v0.40.16)
|
||||||
|
|
||||||
|
> 14 February 2021
|
||||||
|
|
||||||
|
- Write description to ITPC:CaptionAbstract (#380) [`4b7a53f`](https://github.com/RhetTbull/osxphotos/commit/4b7a53faa8d7ff2e941e7653554f61bcbd416fc9)
|
||||||
|
- Removed orientation from XMP, #378 [`70848e1`](https://github.com/RhetTbull/osxphotos/commit/70848e1ff6def928b052271b47c1697c23a8c73f)
|
||||||
|
- Added image orientation bug to Known Bugs [`1316866`](https://github.com/RhetTbull/osxphotos/commit/1316866dc47486ac61db8903d2d7d006f2598a77)
|
||||||
|
|
||||||
|
#### [v0.40.14](https://github.com/RhetTbull/osxphotos/compare/v0.40.13...v0.40.14)
|
||||||
|
|
||||||
|
> 12 February 2021
|
||||||
|
|
||||||
|
- Fix for issue #366, --jpeg-ext, --convert-to-jpeg bug [`3027350`](https://github.com/RhetTbull/osxphotos/commit/30273509d40a270d2610b662ed9238449350064c)
|
||||||
|
- Added test for #374 [`2691902`](https://github.com/RhetTbull/osxphotos/commit/2691902d5c7a4f4f81e3a9b36fd560ff0a07aec1)
|
||||||
|
|
||||||
|
#### [v0.40.13](https://github.com/RhetTbull/osxphotos/compare/v0.40.12...v0.40.13)
|
||||||
|
|
||||||
|
> 10 February 2021
|
||||||
|
|
||||||
|
- Bug fix for --jpeg-ext, #374 [`da47821`](https://github.com/RhetTbull/osxphotos/commit/da47821fae7ee7b2d6d89f5542e729e01d3338df)
|
||||||
|
|
||||||
|
#### [v0.40.12](https://github.com/RhetTbull/osxphotos/compare/v0.40.11...v0.40.12)
|
||||||
|
|
||||||
|
> 9 February 2021
|
||||||
|
|
||||||
|
- Fixed --exiftool-option, #369, for real this time [`857e3db`](https://github.com/RhetTbull/osxphotos/commit/857e3db6ccce810d682cd4632ac9bc8448c4f86b)
|
||||||
|
|
||||||
|
#### [v0.40.11](https://github.com/RhetTbull/osxphotos/compare/v0.40.10...v0.40.11)
|
||||||
|
|
||||||
|
> 9 February 2021
|
||||||
|
|
||||||
|
- Fixed --exiftool-option, #369 [`198adda`](https://github.com/RhetTbull/osxphotos/commit/198addaa07a86ac5b0fd82787fdffff0a0fc19c6)
|
||||||
|
|
||||||
|
#### [v0.40.10](https://github.com/RhetTbull/osxphotos/compare/v0.40.9...v0.40.10)
|
||||||
|
|
||||||
|
> 7 February 2021
|
||||||
|
|
||||||
|
- Fix for issue #366 [`5c3360f`](https://github.com/RhetTbull/osxphotos/commit/5c3360f29d52df2f804c70f37a2ca9a3f102d93c)
|
||||||
|
|
||||||
|
#### [v0.40.9](https://github.com/RhetTbull/osxphotos/compare/v0.40.8...v0.40.9)
|
||||||
|
|
||||||
|
> 7 February 2021
|
||||||
|
|
||||||
|
- Fixed unnecessary warning for long keywords, issue #365 [`f8616ac`](https://github.com/RhetTbull/osxphotos/commit/f8616acf167b5e73ab3e4b68dcfbf578230c330d)
|
||||||
|
|
||||||
|
#### [v0.40.8](https://github.com/RhetTbull/osxphotos/compare/v0.40.7...v0.40.8)
|
||||||
|
|
||||||
|
> 4 February 2021
|
||||||
|
|
||||||
|
- Implemented --in-album, --not-in-album, issue #364 [`addd952`](https://github.com/RhetTbull/osxphotos/commit/addd952aa315007852945a352b2c7c451ba5f21a)
|
||||||
|
- Updated docs [`7fa5fba`](https://github.com/RhetTbull/osxphotos/commit/7fa5fbaa5b7c9aa1412eceef56e068dc044c91e0)
|
||||||
|
- Updated docs Makefile [skip ci] [`683dfe7`](https://github.com/RhetTbull/osxphotos/commit/683dfe7f3ffd235659b58f403562ce2d51123cfb)
|
||||||
|
|
||||||
|
#### [v0.40.7](https://github.com/RhetTbull/osxphotos/compare/v0.40.6...v0.40.7)
|
||||||
|
|
||||||
|
> 3 February 2021
|
||||||
|
|
||||||
|
- Bump bleach from 3.1.4 to 3.3.0 [`#362`](https://github.com/RhetTbull/osxphotos/pull/362)
|
||||||
|
- Fixed XMP template for issue #361 [`43af4d2`](https://github.com/RhetTbull/osxphotos/commit/43af4d205a7264e530bc2b2789d297be633391e1)
|
||||||
|
- Updated sidecar test data [`591f9bc`](https://github.com/RhetTbull/osxphotos/commit/591f9bcc62720f7eddebba3b3dcff265907550dd)
|
||||||
|
- Added tests for --only-new, #358 [`adc4b05`](https://github.com/RhetTbull/osxphotos/commit/adc4b056029794faddd464d22022a2a17298a924)
|
||||||
|
- Updated tests for ExportDB, #358 [`48d2223`](https://github.com/RhetTbull/osxphotos/commit/48d2223edde4850830cc6a3f9776ce08f81a6636)
|
||||||
|
- Added 11.2 to tested versions, #360 [`2284598`](https://github.com/RhetTbull/osxphotos/commit/2284598a24f63232c01dcf27b9982002123834ca)
|
||||||
|
|
||||||
|
#### [v0.40.6](https://github.com/RhetTbull/osxphotos/compare/v0.40.5...v0.40.6)
|
||||||
|
|
||||||
|
> 2 February 2021
|
||||||
|
|
||||||
|
- Add @davidjroos as a contributor [`8dbedef`](https://github.com/RhetTbull/osxphotos/commit/8dbedef1874882815afb4a885184249aae73bf9f)
|
||||||
|
- Fixed documentation, #359 [`77371b6`](https://github.com/RhetTbull/osxphotos/commit/77371b6e5d8a9b8662b7b7d540378beb897f6988)
|
||||||
|
|
||||||
|
#### [v0.40.5](https://github.com/RhetTbull/osxphotos/compare/v0.40.3...v0.40.5)
|
||||||
|
|
||||||
|
> 1 February 2021
|
||||||
|
|
||||||
|
- Restructured docs [`3a4a8bd`](https://github.com/RhetTbull/osxphotos/commit/3a4a8bdb0bdd995c937e0a15f5d8f1685b73407f)
|
||||||
|
- Refactored __main__, added sphinx docs [`51f6958`](https://github.com/RhetTbull/osxphotos/commit/51f69585be60d12f912ba08f138b9c1f74481dbd)
|
||||||
|
- Implemented --only-new, #358 [`5c093c4`](https://github.com/RhetTbull/osxphotos/commit/5c093c43528193ed1704ed4ef1b8d841a95a81cf)
|
||||||
|
|
||||||
#### [v0.40.3](https://github.com/RhetTbull/osxphotos/compare/v0.40.2...v0.40.3)
|
#### [v0.40.3](https://github.com/RhetTbull/osxphotos/compare/v0.40.2...v0.40.3)
|
||||||
|
|
||||||
> 23 January 2021
|
> 23 January 2021
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
include README.md
|
include README.md
|
||||||
|
include README.rst
|
||||||
include osxphotos/templates/*
|
include osxphotos/templates/*
|
||||||
|
include osxphotos/phototemplate.tx
|
||||||
|
include osxphotos/phototemplate.md
|
||||||
481
README.md
@@ -4,7 +4,7 @@
|
|||||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||||

|

|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
[](#contributors)
|
[](#contributors)
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||||
@@ -31,6 +31,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
|||||||
+ [FaceInfo](#faceinfo)
|
+ [FaceInfo](#faceinfo)
|
||||||
+ [CommentInfo](#commentinfo)
|
+ [CommentInfo](#commentinfo)
|
||||||
+ [LikeInfo](#likeinfo)
|
+ [LikeInfo](#likeinfo)
|
||||||
|
+ [AdjustmentsInfo](#adjustmentsinfo)
|
||||||
+ [Raw Photos](#raw-photos)
|
+ [Raw Photos](#raw-photos)
|
||||||
+ [Template Substitutions](#template-substitutions)
|
+ [Template Substitutions](#template-substitutions)
|
||||||
+ [Utility Functions](#utility-functions)
|
+ [Utility Functions](#utility-functions)
|
||||||
@@ -51,7 +52,7 @@ Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS
|
|||||||
|
|
||||||
| macOS Version | macOS name | Photos.app version |
|
| macOS Version | macOS name | Photos.app version |
|
||||||
| ----------------- |------------|:-------------------|
|
| ----------------- |------------|:-------------------|
|
||||||
| 10.16 | Big Sur | 6.0 ✅ |
|
| 10.16, 11.0-11.2 | Big Sur | 6.0 ✅ |
|
||||||
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
||||||
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
||||||
| 10.13.6 | High Sierra| 3.0 ✅ |
|
| 10.13.6 | High Sierra| 3.0 ✅ |
|
||||||
@@ -200,7 +201,7 @@ Options:
|
|||||||
--uuid UUID Search for photos with UUID(s).
|
--uuid UUID Search for photos with UUID(s).
|
||||||
--uuid-from-file FILE Search for photos with UUID(s) loaded from
|
--uuid-from-file FILE Search for photos with UUID(s) loaded from
|
||||||
FILE. Format is a single UUID per line. Lines
|
FILE. Format is a single UUID per line. Lines
|
||||||
preceeded with # are ignored.
|
preceded with # are ignored.
|
||||||
--title TITLE Search for TITLE in title of photo.
|
--title TITLE Search for TITLE in title of photo.
|
||||||
--no-title Search for photos with no title.
|
--no-title Search for photos with no title.
|
||||||
--description DESC Search for DESC in description of photo.
|
--description DESC Search for DESC in description of photo.
|
||||||
@@ -273,6 +274,9 @@ Options:
|
|||||||
--is-reference Search for photos that were imported as
|
--is-reference Search for photos that were imported as
|
||||||
referenced files (not copied into Photos
|
referenced files (not copied into Photos
|
||||||
library).
|
library).
|
||||||
|
--in-album Search for photos that are in one or more
|
||||||
|
albums.
|
||||||
|
--not-in-album Search for photos that are not in any albums.
|
||||||
--missing Export only photos missing from the Photos
|
--missing Export only photos missing from the Photos
|
||||||
library; must be used with --download-missing.
|
library; must be used with --download-missing.
|
||||||
--deleted Include photos from the 'Recently Deleted'
|
--deleted Include photos from the 'Recently Deleted'
|
||||||
@@ -281,15 +285,24 @@ Options:
|
|||||||
Deleted' folder.
|
Deleted' folder.
|
||||||
--update Only export new or updated files. See notes
|
--update Only export new or updated files. See notes
|
||||||
below on export and --update.
|
below on export and --update.
|
||||||
--ignore-signature When used with --update, ignores file
|
--ignore-signature When used with '--update', ignores file
|
||||||
signature when updating files. This is useful
|
signature when updating files. This is useful
|
||||||
if you have processed or edited exported
|
if you have processed or edited exported
|
||||||
photos changing the file signature (size &
|
photos changing the file signature (size &
|
||||||
modification date). In this case, --update
|
modification date). In this case, '--update'
|
||||||
would normally re-export the processed files
|
would normally re-export the processed files
|
||||||
but with --ignore-signature, files which exist
|
but with '--ignore-signature', files which
|
||||||
in the export directory will not be re-
|
exist in the export directory will not be re-
|
||||||
exported.
|
exported. If used with '--sidecar', '--ignore-
|
||||||
|
signature' has the following behavior: 1) if
|
||||||
|
the metadata (in Photos) that went into the
|
||||||
|
sidecar did not change, the sidecar will not
|
||||||
|
be updated; 2) if the metadata (in Photos)
|
||||||
|
that went into the sidecar did change, a new
|
||||||
|
sidecar is written but a new image file is
|
||||||
|
not; 3) if a sidecar does not exist for the
|
||||||
|
photo, a sidecar will be written whether or
|
||||||
|
not the photo file was written or updated.
|
||||||
--only-new If used with --update, ignores any previously
|
--only-new If used with --update, ignores any previously
|
||||||
exported files, even if missing from the
|
exported files, even if missing from the
|
||||||
export folder and only exports new files that
|
export folder and only exports new files that
|
||||||
@@ -374,7 +387,8 @@ Options:
|
|||||||
does not export tag groups. Sidecar filename
|
does not export tag groups. Sidecar filename
|
||||||
is in format photoname.ext.json; For a list of
|
is in format photoname.ext.json; For a list of
|
||||||
tags exported in the JSON and exiftool
|
tags exported in the JSON and exiftool
|
||||||
sidecar, see '--exiftool'.
|
sidecar, see '--exiftool'. See also '--ignore-
|
||||||
|
signature'.
|
||||||
--sidecar-drop-ext Drop the photo's extension when naming sidecar
|
--sidecar-drop-ext Drop the photo's extension when naming sidecar
|
||||||
files. By default, sidecar files are named in
|
files. By default, sidecar files are named in
|
||||||
format 'photo_filename.photo_ext.sidecar_ext',
|
format 'photo_filename.photo_ext.sidecar_ext',
|
||||||
@@ -443,7 +457,16 @@ Options:
|
|||||||
"{folder_album}" You may specify more than one
|
"{folder_album}" You may specify more than one
|
||||||
template, for example --keyword-template
|
template, for example --keyword-template
|
||||||
"{folder_album}" --keyword-template
|
"{folder_album}" --keyword-template
|
||||||
"{created.year}" See Templating System below.
|
"{created.year}". See '--replace-keywords' and
|
||||||
|
Templating System below.
|
||||||
|
--replace-keywords Replace keywords with any values specified
|
||||||
|
with --keyword-template. By default,
|
||||||
|
--keyword-template will add keywords to any
|
||||||
|
keywords already associated with the photo.
|
||||||
|
If --replace-keywords is specified, values
|
||||||
|
from --keyword-template will replace any
|
||||||
|
existing keywords instead of adding additional
|
||||||
|
keywords.
|
||||||
--description-template TEMPLATE
|
--description-template TEMPLATE
|
||||||
For use with --exiftool, --sidecar; specify a
|
For use with --exiftool, --sidecar; specify a
|
||||||
template string to use as description in the
|
template string to use as description in the
|
||||||
@@ -562,6 +585,7 @@ Options:
|
|||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
|
|
||||||
** Export **
|
** Export **
|
||||||
|
|
||||||
When exporting photos, osxphotos creates a database in the top-level export
|
When exporting photos, osxphotos creates a database in the top-level export
|
||||||
folder called '.osxphotos_export.db'. This database preserves state information
|
folder called '.osxphotos_export.db'. This database preserves state information
|
||||||
used for determining which files need to be updated when run with --update. It
|
used for determining which files need to be updated when run with --update. It
|
||||||
@@ -638,119 +662,138 @@ s
|
|||||||
|
|
||||||
** Templating System **
|
** Templating System **
|
||||||
|
|
||||||
Several options, such as --directory, allow you to specify a template which
|
The templating system converts one or template statements, written in osxphotos
|
||||||
will be rendered to substitute template fields with values from the photo. For
|
templating language, to one or more rendered values using information from the
|
||||||
example, '{created.month}' would be replaced with the month name of the photo
|
photo being processed.
|
||||||
creation date. e.g. 'November'.
|
|
||||||
|
|
||||||
Some options supporting templates may be repeated e.g., --keyword-template
|
In its simplest form, a template statement has the form: "{template_field}", for
|
||||||
'{label}' --keyword-template '{media_type}' to add both labels and media types
|
example "{title}" which would resolve to the title of the photo.
|
||||||
to the keywords.
|
|
||||||
|
|
||||||
The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full
|
Template statements may contain one or more modifiers. The full syntax is:
|
||||||
template format is:
|
|
||||||
'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}'
|
|
||||||
|
|
||||||
With a few exceptions (like '{created.strftime}') everything but the
|
"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value
|
||||||
TEMPLATE_FIELD is optional.
|
,default}posttext"
|
||||||
|
|
||||||
- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in
|
Template statements are white-space sensitive meaning that white space (spaces,
|
||||||
place' with an optional delimiter using the template form
|
tabs) changes the meaning of the template statement.
|
||||||
'{DELIM+TEMPLATE_FIELD}'. For example, a photo with keywords 'foo' and 'bar':
|
|
||||||
|
|
||||||
'{keyword}' renders to 'foo' and 'bar'
|
pretext and posttext are free form text. For example, if a photo has title "My
|
||||||
|
Photo Title". the template statement "The title of the photo is {title}",
|
||||||
|
resolves to "The title of the photo is My Photo Title". The pretext in this
|
||||||
|
example is "The title if the photo is " and the template_field is {title}.
|
||||||
|
|
||||||
'{,+keyword}' renders to: 'foo,bar'
|
delim: optional delimiter string to use when expanding multi-valued template
|
||||||
|
values in-place
|
||||||
|
|
||||||
'{; +keyword}' renders to: 'foo; bar'
|
+: If present before template name, expands the template in place. If delim not
|
||||||
|
provided, values are joined with no delimiter.
|
||||||
|
|
||||||
'{+keyword}' renders to 'foobar'
|
e.g. if Photo keywords are ["foo","bar"]:
|
||||||
|
|
||||||
- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword'
|
• "{keyword}" renders to "foo", "bar"
|
||||||
|
• "{,+keyword}" renders to: "foo,bar"
|
||||||
|
• "{; +keyword}" renders to: "foo; bar"
|
||||||
|
• "{+keyword}" renders to "foobar"
|
||||||
|
|
||||||
- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in
|
template_field: The template field to resolve. See Template Substitutions for
|
||||||
that they join multiple elements into a single path-like string. For example,
|
full list of template fields.
|
||||||
if photo is in album Album1 in folder Folder1, '{folder_album}' results in
|
|
||||||
'Folder1/Album1'. This is so these template fields may be used as paths in
|
|
||||||
--directory. If you intend to use such a field as a string, e.g. in the
|
|
||||||
filename, you may specify a different path separator using the form:
|
|
||||||
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
|
|
||||||
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
|
|
||||||
would result in 'Folder1Album1'.
|
|
||||||
|
|
||||||
- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template
|
:subfield: Some templates have sub-fields, For example, {exiftool:IPTC:Make};
|
||||||
value with text "NEW". For example, if you have album names with '/' in the
|
the template_field is exiftool and the sub-field is IPTC:Make.
|
||||||
album name you could replace '/' with "-" using the template '{album[/,-]}'.
|
|
||||||
This would replace any occurence of "/" in the album name with "-"; album
|
|
||||||
"Vacation/2019" would thus become "Vacation-2019". You may specify more than
|
|
||||||
one pair of OLD,NEW values by listing them delimited by '|'. For example:
|
|
||||||
'{album[/,-|:,-]}' to replace both '/' and ':' by '-'. You can also use the
|
|
||||||
[OLD,NEW] syntax to delete a character by omitting the NEW value as in
|
|
||||||
'{album[/,]}'.
|
|
||||||
|
|
||||||
- '?' Some template fields such as 'hdr' are boolean and resolve to True or
|
|filter: You may optionally append one or more filter commands to the end of the
|
||||||
False. These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}',
|
template field using the vertical pipe ('|') symbol. Filters may be combined,
|
||||||
e.g. {hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR
|
separated by '|' as in: {keyword|capitalize|parens}.
|
||||||
image and 'not_hdr' otherwise.
|
|
||||||
|
|
||||||
- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results
|
Valid filters are:
|
||||||
in a null (empty) value, the template will result in default value of '_'. You
|
|
||||||
may specify an alternate default value by appending ',DEFAULT' after
|
|
||||||
template_field. Example: '{title,no_title}' would result in 'no_title' if the
|
|
||||||
photo had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but
|
|
||||||
there was no address associated with the photo, the resulting output would be:
|
|
||||||
'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not contain
|
|
||||||
a brace symbol ('{' or '}').
|
|
||||||
|
|
||||||
Again, if you do not specify a default value and the template substitution has
|
• lower: Convert value to lower case, e.g. 'Value' => 'value'.
|
||||||
no value, '_' (underscore) will be used as the default value. For example, in
|
• upper: Convert value to upper case, e.g. 'Value' => 'VALUE'.
|
||||||
the above example, this would result in '2020/_/photoname.jpg' if address was
|
• strip: Strip whitespace from beginning/end of value, e.g. ' Value ' =>
|
||||||
null.
|
'Value'.
|
||||||
|
• titlecase: Convert value to title case, e.g. 'my value' => 'My Value'.
|
||||||
|
• capitalize: Capitalize first word of value and convert other words to lower
|
||||||
|
case, e.g. 'MY VALUE' => 'My value'.
|
||||||
|
• braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||||
|
• parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||||
|
• brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||||
|
|
||||||
You may specify a null default (e.g. "" or empty string) by omitting the value
|
e.g. if Photo keywords are ["FOO","bar"]:
|
||||||
after the comma, e.g. {title,} which would render to "" if title had no value
|
|
||||||
thus effectively deleting the template from the resulting string.
|
|
||||||
|
|
||||||
You may include other text in the template string outside the {} and use more
|
• "{keyword|lower}" renders to "foo", "bar"
|
||||||
than one template field in a single string, e.g. '{created.year} -
|
• "{keyword|upper}" renders to: "FOO", "BAR"
|
||||||
{created.month}' (e.g. '2020 - November').
|
• "{keyword|capitalize}" renders to: "Foo", "Bar"
|
||||||
|
• "{keyword|lower|parens}" renders to: "(foo)", "(bar)"
|
||||||
|
|
||||||
Some templates may resolve to more than one value. For example, a photo can
|
e.g. if Photo description is "my description":
|
||||||
have multiple keywords so '{keyword}' can result in multiple values. If used in
|
|
||||||
a filename or directory, these templates may result in more than one copy of
|
|
||||||
the photo being exported. For example, if photo has keywords "foo" and "bar",
|
|
||||||
--directory '{keyword}' will result in copies of the photo being exported to
|
|
||||||
'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
|
||||||
|
|
||||||
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow
|
• "{descr|titlecase}" renders to: "My Description"
|
||||||
customization of the output. For example, '{media_type}' resolves to the
|
|
||||||
special media type of the photo such as 'panorama' or 'selfie'. You may use
|
|
||||||
the 'DEFAULT' value to override these in form:
|
|
||||||
'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if
|
|
||||||
photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée'
|
|
||||||
instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an
|
|
||||||
ordinary video.
|
|
||||||
|
|
||||||
With the --directory and --filename options you may specify a template for the
|
(path_sep): optional path separator to use when joining path-like fields, for
|
||||||
export directory or filename, respectively. The directory will be appended to
|
example {folder_album}. Default is "/".
|
||||||
the export path specified in the export DEST argument to export. For example, if
|
|
||||||
template is '{created.year}/{created.month}', and export destination DEST is
|
|
||||||
'/Users/maria/Pictures/export', the actual export directory for a photo would be
|
|
||||||
'/Users/maria/Pictures/export/2020/March' if the photo was created in March
|
|
||||||
2020.
|
|
||||||
|
|
||||||
The templating system may also be used with the --keyword-template option to set
|
e.g. If Photo is in Album1 in Folder1:
|
||||||
keywords on export (with --exiftool or --sidecar), for example, to set a new
|
|
||||||
keyword in format 'folder/subfolder/album' to preserve the folder/album
|
|
||||||
structure, you can use --keyword-template "{folder_album}"
|
|
||||||
|
|
||||||
In the template, valid template substitutions will be replaced by the
|
• "{folder_album}" renders to ["Folder1/Album1"]
|
||||||
corresponding value from the table below. Invalid substitutions will result in
|
• "{folder_album(>)}" renders to ["Folder1>Album1"]
|
||||||
an error.
|
• "{folder_album()}" renders to ["Folder1Album1"]
|
||||||
|
|
||||||
If you want the actual text of the template substition to appear in the rendered
|
[find|replace]: optional text replacement to perform on rendered template value.
|
||||||
name, use double braces, e.g. '{{' or '}}', thus using '{created.year}/{{name}}'
|
For example, to replace "/" in an album name, you could use the template
|
||||||
for --directory would result in output of 2020/{name}/photoname.jpg
|
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding
|
||||||
|
another find|replace pair. e.g. to replace both "/" and ":" in album name:
|
||||||
|
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters.
|
||||||
|
The "|" character cannot be used in a find/replace pair.
|
||||||
|
|
||||||
|
?bool_value: Template fields may be evaluated as boolean by appending "?" after
|
||||||
|
the field name (and following "(path_sep)" or "[find/replace]". If a field is
|
||||||
|
True (e.g. photo is HDR and field is "{hdr}") or has any value, the value
|
||||||
|
following the "?" will be used to render the template instead of the actual
|
||||||
|
field value. If the template field evaluates to False (e.g. in above example,
|
||||||
|
photo is not HDR) or has no value (e.g. photo has no title and field is
|
||||||
|
"{title}") then the default value following a "," will be used.
|
||||||
|
|
||||||
|
e.g. if photo is an HDR image,
|
||||||
|
|
||||||
|
• "{hdr?ISHDR,NOTHDR}" renders to "ISHDR"
|
||||||
|
|
||||||
|
and if it is not an HDR image,
|
||||||
|
|
||||||
|
• "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR"
|
||||||
|
|
||||||
|
,default: optional default value to use if the template name has no value. This
|
||||||
|
modifier is also used for the value if False for boolean-type fields (see above)
|
||||||
|
as well as to hold a sub-template for values like {created.strftime}. If no
|
||||||
|
default value provided, "_" is used.
|
||||||
|
|
||||||
|
e.g., if photo has no title set,
|
||||||
|
|
||||||
|
• "{title}" renders to "_"
|
||||||
|
• "{title,I have no title}" renders to "I have no title"
|
||||||
|
|
||||||
|
Template fields such as created.strftime use the default value to pass the
|
||||||
|
template to use for strftime.
|
||||||
|
|
||||||
|
e.g., if photo date is 4 February 2020, 19:07:38,
|
||||||
|
|
||||||
|
• "{created.strftime,%Y-%m-%d-%H%M%S}" renders to "2020-02-04-190738"
|
||||||
|
|
||||||
|
Some template fields such as "{media_type}" use the default value to allow
|
||||||
|
customization of the output. For example, "{media_type}" resolves to the special
|
||||||
|
media type of the photo such as panorama or selfie. You may use the default
|
||||||
|
value to override these in form:
|
||||||
|
"{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if photo
|
||||||
|
was a time_lapse photo, media_type would resolve to vidéo_accélérée instead of
|
||||||
|
time_lapse.
|
||||||
|
|
||||||
|
Either or both bool_value or default (False value) may be empty which would
|
||||||
|
result in empty string "" when rendered.
|
||||||
|
|
||||||
|
If you want to include "{" or "}" in the output, use "{openbrace}" or
|
||||||
|
"{closebrace}" template substitution.
|
||||||
|
|
||||||
|
e.g. "{created.year}/{openbrace}{title}{closebrace}" would result in
|
||||||
|
"2020/{Photo Title}".
|
||||||
|
|
||||||
With the --directory and --filename options you may specify a template for the
|
With the --directory and --filename options you may specify a template for the
|
||||||
export directory or filename, respectively. The directory will be appended to
|
export directory or filename, respectively. The directory will be appended to
|
||||||
@@ -763,29 +806,15 @@ if template is '{created.year}/{created.month}', and export destination DEST is
|
|||||||
The templating system may also be used with the --keyword-template option to set
|
The templating system may also be used with the --keyword-template option to set
|
||||||
keywords on export (with --exiftool or --sidecar), for example, to set a new
|
keywords on export (with --exiftool or --sidecar), for example, to set a new
|
||||||
keyword in format 'folder/subfolder/album' to preserve the folder/album
|
keyword in format 'folder/subfolder/album' to preserve the folder/album
|
||||||
structure, you can use --keyword-template "{folder_album}"
|
structure, you can use --keyword-template "{folder_album}" or in the
|
||||||
|
'folder>subfolder>album' format used in Lightroom Classic, --keyword-template
|
||||||
|
"{folder_album(>)}".
|
||||||
|
|
||||||
In the template, valid template substitutions will be replaced by the
|
In the template, valid template substitutions will be replaced by the
|
||||||
corresponding value from the table below. Invalid substitutions will result in
|
corresponding value from the table below. Invalid substitutions will result in
|
||||||
a an error and the script will abort.
|
a an error and the script will abort.
|
||||||
|
|
||||||
If you want the actual text of the template substition to appear in the rendered
|
** Template Substitutions **
|
||||||
name, use double braces, e.g. '{{' or '}}', thus using '{created.year}/{{name}}'
|
|
||||||
for --directory would result in output of 2020/{name}/photoname.jpg
|
|
||||||
|
|
||||||
You may specify an optional default value to use if the substitution does not
|
|
||||||
contain a value (e.g. the value is null) by specifying the default value after a
|
|
||||||
',' in the template string: for example, if template is
|
|
||||||
'{created.year}/{place.address,NO_ADDRESS}' but there was no address associated
|
|
||||||
with the photo, the resulting output would be: '2020/NO_ADDRESS/photoname.jpg'.
|
|
||||||
If specified, the default value may not contain a brace symbol ('{' or '}').
|
|
||||||
|
|
||||||
If you do not specify a default value and the template substitution has no
|
|
||||||
value, '_' (underscore) will be used as the default value. For example, in the
|
|
||||||
above example, this would result in '2020/_/photoname.jpg' if address was null.
|
|
||||||
|
|
||||||
You may specify a null default (e.g. "" or empty string) by omitting the value
|
|
||||||
after the comma, e.g. {title,} which would render to "" if title had no value.
|
|
||||||
|
|
||||||
Substitution Description
|
Substitution Description
|
||||||
{name} Current filename of the photo
|
{name} Current filename of the photo
|
||||||
@@ -950,6 +979,15 @@ Substitution Description
|
|||||||
(UUID) for the photo, a 36-character string
|
(UUID) for the photo, a 36-character string
|
||||||
unique to the photo, e.g.
|
unique to the photo, e.g.
|
||||||
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
|
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
|
||||||
|
{comma} A comma: ','
|
||||||
|
{semicolon} A semicolon: ';'
|
||||||
|
{pipe} A vertical pipe: '|'
|
||||||
|
{openbrace} An open brace: '{'
|
||||||
|
{closebrace} A close brace: '}'
|
||||||
|
{openparens} An open parentheses: '('
|
||||||
|
{closeparens} A close parentheses: ')'
|
||||||
|
{openbracket} An open bracket: '['
|
||||||
|
{closebracket} A close bracket: ']'
|
||||||
|
|
||||||
The following substitutions may result in multiple values. Thus if specified for
|
The following substitutions may result in multiple values. Thus if specified for
|
||||||
--directory these could result in multiple copies of a photo being being
|
--directory these could result in multiple copies of a photo being being
|
||||||
@@ -958,39 +996,40 @@ exported, one to each directory. For example: --directory
|
|||||||
of the following directories if the photos were created in 2019 and were in
|
of the following directories if the photos were created in 2019 and were in
|
||||||
albums 'Vacation' and 'Family': 2019/Vacation, 2019/Family
|
albums 'Vacation' and 'Family': 2019/Vacation, 2019/Family
|
||||||
|
|
||||||
Substitution Description
|
Substitution Description
|
||||||
{album} Album(s) photo is contained in
|
{album} Album(s) photo is contained in
|
||||||
{folder_album} Folder path + album photo is contained in. e.g.
|
{folder_album} Folder path + album photo is contained in. e.g.
|
||||||
'Folder/Subfolder/Album' or just 'Album' if no
|
'Folder/Subfolder/Album' or just 'Album' if no
|
||||||
enclosing folder
|
enclosing folder
|
||||||
{keyword} Keyword(s) assigned to photo
|
{keyword} Keyword(s) assigned to photo
|
||||||
{person} Person(s) / face(s) in a photo
|
{person} Person(s) / face(s) in a photo
|
||||||
{label} Image categorization label associated with a photo
|
{label} Image categorization label associated with a photo
|
||||||
(Photos 5+ only)
|
(Photos 5+ only)
|
||||||
{label_normalized} All lower case version of 'label' (Photos 5+ only)
|
{label_normalized} All lower case version of 'label' (Photos 5+ only)
|
||||||
{comment} Comment(s) on shared Photos; format is 'Person name:
|
{comment} Comment(s) on shared Photos; format is 'Person name:
|
||||||
comment text' (Photos 5+ only)
|
comment text' (Photos 5+ only)
|
||||||
{exiftool:GROUP:TAGNAME} Use exiftool (https://exiftool.org) to extract
|
{exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool
|
||||||
metadata, in form GROUP:TAGNAME, from image. E.g.
|
(https://exiftool.org) to extract metadata, in form
|
||||||
'{exiftool:EXIF:Make}' to get camera make, or
|
GROUP:TAGNAME, from image. E.g.
|
||||||
{exiftool:IPTC:Keywords} to extract keywords. See
|
'{exiftool:EXIF:Make}' to get camera make, or
|
||||||
https://exiftool.org/TagNames/ for list of valid tag
|
{exiftool:IPTC:Keywords} to extract keywords. See
|
||||||
names. You must specify group (e.g. EXIF, IPTC,
|
https://exiftool.org/TagNames/ for list of valid tag
|
||||||
etc) as used in `exiftool -G`. exiftool must be
|
names. You must specify group (e.g. EXIF, IPTC, etc)
|
||||||
installed in the path to use this template.
|
as used in `exiftool -G`. exiftool must be installed
|
||||||
{searchinfo.holiday} Holiday names associated with a photo, e.g.
|
in the path to use this template.
|
||||||
'Christmas Day'; (Photos 5+ only, applied
|
{searchinfo.holiday} Holiday names associated with a photo, e.g.
|
||||||
automatically by Photos' image categorization
|
'Christmas Day'; (Photos 5+ only, applied
|
||||||
algorithms).
|
automatically by Photos' image categorization
|
||||||
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
|
algorithms).
|
||||||
Event'; (Photos 5+ only, applied automatically by
|
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
|
||||||
Photos' image categorization algorithms).
|
Event'; (Photos 5+ only, applied automatically by
|
||||||
{searchinfo.venue} Venues associated with a photo, e.g. name of
|
Photos' image categorization algorithms).
|
||||||
restaurant; (Photos 5+ only, applied automatically
|
{searchinfo.venue} Venues associated with a photo, e.g. name of
|
||||||
by Photos' image categorization algorithms).
|
restaurant; (Photos 5+ only, applied automatically by
|
||||||
{searchinfo.venue_type} Venue types associated with a photo, e.g.
|
Photos' image categorization algorithms).
|
||||||
'Restaurant'; (Photos 5+ only, applied automatically
|
{searchinfo.venue_type} Venue types associated with a photo, e.g.
|
||||||
by Photos' image categorization algorithms).
|
'Restaurant'; (Photos 5+ only, applied automatically
|
||||||
|
by Photos' image categorization algorithms).
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -1550,7 +1589,7 @@ Returns height of the photo in pixels. If image has been edited, returns height
|
|||||||
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).
|
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`
|
#### `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).
|
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). If orientation cannot be determined, returns 0 (this happens if osxphotos cannot decode the adjustment info for an edited image).
|
||||||
|
|
||||||
#### `original_height`
|
#### `original_height`
|
||||||
Returns height of the original photo in pixels. See also [height](#height).
|
Returns height of the original photo in pixels. See also [height](#height).
|
||||||
@@ -1570,6 +1609,9 @@ Returns `True` if the original image file is missing on disk, otherwise `False`.
|
|||||||
#### `hasadjustments`
|
#### `hasadjustments`
|
||||||
Returns `True` if the picture has been edited, otherwise `False`
|
Returns `True` if the picture has been edited, otherwise `False`
|
||||||
|
|
||||||
|
#### `adjustments`
|
||||||
|
On Photos 5+, returns an [AdjustmentsInfo](#adjustmentsinfo) object representing the adjustments (edits) to the photo or None if there are no adjustments. On earlier versions of Photos, always returns None.
|
||||||
|
|
||||||
#### `external_edit`
|
#### `external_edit`
|
||||||
Returns `True` if the picture was edited in an external editor (outside Photos.app), otherwise `False`
|
Returns `True` if the picture was edited in an external editor (outside Photos.app), otherwise `False`
|
||||||
|
|
||||||
@@ -1833,76 +1875,117 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
|||||||
|
|
||||||
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 format "{[[DELIM]+]name[(PATH_SEP)][?TRUE_VALUE][,[DEFAULT]]}" where name is one of the values in the [Template Substitutions](#template-substitutions) table. See notes below regarding specific details of the syntax.
|
- `template_str`: str in osxphotos template language (OTL) format. See also [Template Substitutions](#template-substitutions) table. See notes below regarding specific details of the syntax.
|
||||||
- `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 when joining path like fields such as `{folder_album}`; default is `os.path.sep`. May also be provided in the template itself. If provided both in the call to `render_template()` and in the template itself, the value in the template string takes precedence.
|
||||||
- `expand_inplace`: expand multi-valued substitutions in-place as a single string instead of returning individual strings
|
- `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 ','
|
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
|
||||||
- `filename`: if True, template output will be sanitized to produce valid file name
|
- `filename`: if True, template output will be sanitized to produce valid file name
|
||||||
- `dirname`: if True, template output will be sanitized to produce valid directory name
|
- `dirname`: if True, template output will be sanitized to produce valid directory name
|
||||||
- `strip`: if True, leading/trailign whitespace will be stripped from rendered template strings
|
- `strip`: if True, leading/trailign whitespace will be stripped from rendered template strings
|
||||||
|
|
||||||
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"]. If there are unmatched strings, rendered will be []. E.g. a template statement must fully match or will result in error and return all unmatched fields in unmatched.
|
||||||
|
|
||||||
e.g. `render_template("{created.year}/{foo}", photo)` would return `(["2020/{foo}"],["foo"])`
|
e.g. `render_template("{created.year}/{foo}", photo)` would return `([],["foo"])`
|
||||||
|
|
||||||
If you want to include "{" or "}" in the output, use "{{" or "}}"
|
|
||||||
|
|
||||||
e.g. `render_template("{created.year}/{{foo}}", photo)` would return `(["2020/{foo}"],[])`
|
|
||||||
|
|
||||||
Some substitutions, notably `album`, `keyword`, and `person` could return multiple values, hence a new string will be return for each possible substitution (hence why a list of rendered strings is returned). For example, a photo in 2 albums: 'Vacation' and 'Family' would result in the following rendered values if template was "{created.year}/{album}" and created.year == 2020: `["2020/Vacation","2020/Family"]`
|
Some substitutions, notably `album`, `keyword`, and `person` could return multiple values, hence a new string will be return for each possible substitution (hence why a list of rendered strings is returned). For example, a photo in 2 albums: 'Vacation' and 'Family' would result in the following rendered values if template was "{created.year}/{album}" and created.year == 2020: `["2020/Vacation","2020/Family"]`
|
||||||
|
|
||||||
The template field format contains optional modifiers:
|
<!-- OSXPHOTOS-TEMPLATE-HELP:START - Do not remove or modify this section -->
|
||||||
|
The templating system converts one or template statements, written in osxphotos templating language, to one or more rendered values using information from the photo being processed.
|
||||||
|
|
||||||
`"{DELIM+name(PATH_SEP)[OLD,NEW]?TRUE_VALUE,DEFAULT}"`
|
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.
|
||||||
|
|
||||||
`DELIM`: optional delimiter string to use when expanding multi-valued template values in-place
|
Template statements may contain one or more modifiers. The full syntax is:
|
||||||
|
|
||||||
`+`: If present before template `name`, expands the template in place. If `DELIM` not provided, values are joined with no delimiter.
|
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"`
|
||||||
|
|
||||||
|
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||||
|
|
||||||
|
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||||
|
|
||||||
|
|
||||||
|
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||||
|
|
||||||
|
`+`: If present before template `name`, expands the template in place. If `delim` not provided, values are joined with no delimiter.
|
||||||
|
|
||||||
e.g. if Photo keywords are `["foo","bar"]`:
|
e.g. if Photo keywords are `["foo","bar"]`:
|
||||||
|
|
||||||
- `"{keyword}"` renders to `["foo", "bar"]`
|
- `"{keyword}"` renders to `"foo", "bar"`
|
||||||
- `"{,+keyword}"` renders to: `["foo,bar"]`
|
- `"{,+keyword}"` renders to: `"foo,bar"`
|
||||||
- `"{; +keyword}"` renders to: `["foo; bar"]`
|
- `"{; +keyword}"` renders to: `"foo; bar"`
|
||||||
- `"{+keyword}"` renders to `["foobar"]`
|
- `"{+keyword}"` renders to `"foobar"`
|
||||||
|
|
||||||
`PATH_SEP`: optional path separator to use when joining path like fields, for example `{folder_album}`. May also be provided as `path_sep` argument in `render_template()`. If provided both in the call to `render_template()` and in the template itself, the value in the template string takes precedence. If not provided in either the template string or in `path_sep` argument, defaults to `os.path.sep`.
|
`template_field`: The template field to resolve. See [Template Substitutions](#template-substitutions) for full list of template fields.
|
||||||
|
|
||||||
|
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
|
||||||
|
|
||||||
|
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
|
||||||
|
|
||||||
|
Valid filters are:
|
||||||
|
|
||||||
|
<!-- OSXPHOTOS-FILTER-TABLE:START - Do not remove or modify this section -->
|
||||||
|
- lower: Convert value to lower case, e.g. 'Value' => 'value'.
|
||||||
|
- upper: Convert value to upper case, e.g. 'Value' => 'VALUE'.
|
||||||
|
- strip: Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.
|
||||||
|
- titlecase: Convert value to title case, e.g. 'my value' => 'My Value'.
|
||||||
|
- capitalize: Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.
|
||||||
|
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||||
|
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||||
|
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||||
|
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
||||||
|
|
||||||
|
e.g. if Photo keywords are `["FOO","bar"]`:
|
||||||
|
|
||||||
|
- `"{keyword|lower}"` renders to `"foo", "bar"`
|
||||||
|
- `"{keyword|upper}"` renders to: `"FOO", "BAR"`
|
||||||
|
- `"{keyword|capitalize}"` renders to: `"Foo", "Bar"`
|
||||||
|
- `"{keyword|lower|parens}"` renders to: `"(foo)", "(bar)"`
|
||||||
|
|
||||||
|
e.g. if Photo description is "my description":
|
||||||
|
|
||||||
|
- `"{descr|titlecase}"` renders to: `"My Description"`
|
||||||
|
|
||||||
|
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
|
||||||
|
|
||||||
e.g. If Photo is in `Album1` in `Folder1`:
|
e.g. If Photo is in `Album1` in `Folder1`:
|
||||||
|
|
||||||
- `"{folder_album}"` renders to `["Folder1/Album1"]`
|
- `"{folder_album}"` renders to `["Folder1/Album1"]`
|
||||||
- `"{folder_album(:)}"` renders to `["Folder1:Album1"]`
|
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||||
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||||
|
|
||||||
`[OLD,NEW]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`.
|
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||||
|
|
||||||
`?TRUE_VALUE`: optional value to use if name is boolean-type field which evaluates to true. For example `"{hdr}"` evaluates to True if photo is an high dynamic range (HDR) image and False otherwise. In these types of fields, use `?TRUE_VALUE` to provide the value if True and `,DEFAULT` to provide the value of False.
|
`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||||
|
|
||||||
e.g. if photo is an HDR image,
|
e.g. if photo is an HDR image,
|
||||||
|
|
||||||
- `"{hdr?ISHDR,NOTHDR}"` renders to `["ISHDR"]`
|
- `"{hdr?ISHDR,NOTHDR}"` renders to `"ISHDR"`
|
||||||
|
|
||||||
and if it is not an HDR image,
|
and if it is not an HDR image,
|
||||||
|
|
||||||
- `"{hdr?ISHDR,NOTHDR}"` renders to `["NOTHDR"]`
|
- `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
|
||||||
|
|
||||||
Either or both `TRUE_VALUE` or `DEFAULT` (False value) may be empty which would result in empty string `[""]` when rendered.
|
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
|
||||||
|
|
||||||
`,DEFAULT`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used. May also be provided in the `none_str` argument to `render_template()`. If provided both in the template string and in `none_str`, the value in the template string takes precedence.
|
|
||||||
|
|
||||||
e.g., if photo has no title set,
|
e.g., if photo has no title set,
|
||||||
|
|
||||||
- `"{title}"` renders to ["_"]
|
- `"{title}"` renders to "_"
|
||||||
- `"{title,I have no title}"` renders to `["I have no title"]`
|
- `"{title,I have no title}"` renders to `"I have no title"`
|
||||||
|
|
||||||
Template fields such as `created.strftime` use the DEFAULT value to pass the template to use for `strftime`.
|
Template fields such as `created.strftime` use the default value to pass the template to use for `strftime`.
|
||||||
|
|
||||||
e.g., if photo date is 4 February 2020, 19:07:38,
|
e.g., if photo date is 4 February 2020, 19:07:38,
|
||||||
|
|
||||||
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `["2020-02-04-190738"]`
|
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `"2020-02-04-190738"`
|
||||||
|
|
||||||
Some template fields such as `"{media_type}"` use the `DEFAULT` value to allow customization of the output. For example, `"{media_type}"` resolves to the special media type of the photo such as `panorama` or `selfie`. You may use the `DEFAULT` value to override these in form: `"{media_type,video=vidéo;time_lapse=vidéo_accélérée}"`. In this example, if photo was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
|
Some template fields such as `"{media_type}"` use the default value to allow customization of the output. For example, `"{media_type}"` resolves to the special media type of the photo such as `panorama` or `selfie`. You may use the default value to override these in form: `"{media_type,video=vidéo;time_lapse=vidéo_accélérée}"`. In this example, if photo was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
|
||||||
|
|
||||||
|
Either or both bool_value or default (False value) may be empty which would result in empty string `""` when rendered.
|
||||||
|
|
||||||
|
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
|
||||||
|
|
||||||
|
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.
|
||||||
|
<!-- OSXPHOTOS-TEMPLATE-HELP:END -->
|
||||||
|
|
||||||
See [Template Substitutions](#template-substitutions) for additional details.
|
See [Template Substitutions](#template-substitutions) for additional details.
|
||||||
|
|
||||||
@@ -2371,6 +2454,23 @@ Returns a JSON representation of the FaceInfo instance.
|
|||||||
- `user`: `str`, name of user who made the like
|
- `user`: `str`, name of user who made the like
|
||||||
- `ismine`: `bool`, True if like was made by person who owns the Photos library being operated on
|
- `ismine`: `bool`, True if like was made by person who owns the Photos library being operated on
|
||||||
|
|
||||||
|
### AdjustmentsInfo
|
||||||
|
[PhotoInfo.adjustments](#adjustments) returns an AdjustmentsInfo object, if the photo has adjustments, or `None` if the photo does not have adjusments. AdjustmentsInfo has the following properties and methods:
|
||||||
|
|
||||||
|
- `plist`: The adjustments plist file maintained by Photos as a dict.
|
||||||
|
- `data`: The raw, undecoded adjustments info as binary blob.
|
||||||
|
- `editor`: The editor bundle ID of the app which made the edits, e.g. `com.apple.photos`.
|
||||||
|
- `format_id`: The format identifier set by the app which made the edits, e.g. `com.apple.photos`.
|
||||||
|
- `base_version`: Version info set by the app which made the edits.
|
||||||
|
- `format_version`: Version info set by the app which made the edits.
|
||||||
|
- `timestamp`: Time stamp of the adjustment as a timezone-aware datetime.datetime object; None if no timestamp is set.
|
||||||
|
- `adjustments`: a list of dicts containing information about the decoded adjustments to the photo or None if adjustments could not be decoded. AdjustmentsInfo can decode adjustments made by Photos but cannot decode adjustments made by external plugins or apps.
|
||||||
|
- `adj_metadata`: a dict containing additional data about the photo decoded from the adjustment data.
|
||||||
|
- `adj_orientation`: the EXIF orientation of the edited photo decoded from the adjustment metadata.
|
||||||
|
- `adj_format_version`: version for adjustments format decoded from the adjustment data.
|
||||||
|
- `adj_version_info`: version info for the application which made the adjustments to the photo decoded from the adjustments data.
|
||||||
|
- `asdict()`: dict representation of the AdjustmentsInfo object; contains all properties with exception of `plist`.
|
||||||
|
|
||||||
### Raw Photos
|
### Raw Photos
|
||||||
Handling raw photos in `osxphotos` requires a bit of extra work. Raw photos in Photos can be imported in two different ways: 1) a single raw photo with no associated JPEG image is imported 2) a raw+JPEG pair is imported -- two separate images with same file stem (e.g. `IMG_0001.CR2` and `IMG_001.JPG`) are imported.
|
Handling raw photos in `osxphotos` requires a bit of extra work. Raw photos in Photos can be imported in two different ways: 1) a single raw photo with no associated JPEG image is imported 2) a raw+JPEG pair is imported -- two separate images with same file stem (e.g. `IMG_0001.CR2` and `IMG_001.JPG`) are imported.
|
||||||
|
|
||||||
@@ -2474,6 +2574,15 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
|||||||
|{exif.camera_model}|Camera model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s'|
|
|{exif.camera_model}|Camera model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s'|
|
||||||
|{exif.lens_model}|Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'|
|
|{exif.lens_model}|Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'|
|
||||||
|{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'|
|
|{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'|
|
||||||
|
|{comma}|A comma: ','|
|
||||||
|
|{semicolon}|A semicolon: ';'|
|
||||||
|
|{pipe}|A vertical pipe: '|'|
|
||||||
|
|{openbrace}|An open brace: '{'|
|
||||||
|
|{closebrace}|A close brace: '}'|
|
||||||
|
|{openparens}|An open parentheses: '('|
|
||||||
|
|{closeparens}|A close parentheses: ')'|
|
||||||
|
|{openbracket}|An open bracket: '['|
|
||||||
|
|{closebracket}|A close bracket: ']'|
|
||||||
|{album}|Album(s) photo is contained in|
|
|{album}|Album(s) photo is contained in|
|
||||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||||
|{keyword}|Keyword(s) assigned to photo|
|
|{keyword}|Keyword(s) assigned to photo|
|
||||||
@@ -2481,7 +2590,7 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
|||||||
|{label}|Image categorization label associated with a photo (Photos 5+ only)|
|
|{label}|Image categorization label associated with a photo (Photos 5+ only)|
|
||||||
|{label_normalized}|All lower case version of 'label' (Photos 5+ only)|
|
|{label_normalized}|All lower case version of 'label' (Photos 5+ only)|
|
||||||
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)|
|
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)|
|
||||||
|{exiftool:GROUP:TAGNAME}|Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|
|{exiftool}|Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|
||||||
|{searchinfo.holiday}|Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
|{searchinfo.holiday}|Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||||
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||||
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||||
@@ -2618,6 +2727,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt=""/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
<td align="center"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt=""/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
||||||
<td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
<td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
||||||
<td align="center"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt=""/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
<td align="center"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt=""/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
||||||
|
<td align="center"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -2632,6 +2742,7 @@ This project follows the [all-contributors](https://github.com/all-contributors/
|
|||||||
|
|
||||||
My goal is make osxphotos as reliable and comprehensive as possible. The test suite currently has over 800 tests--but there are still some [bugs](https://github.com/RhetTbull/osxphotos/issues?q=is%3Aissue+is%3Aopen+label%3Abug) or incomplete features lurking. If you find bugs please open an [issue](https://github.com/RhetTbull/osxphotos/issues). Please consult the list of open bugs before deciding that you want to use this code on your Photos library. Notable issues include:
|
My goal is make osxphotos as reliable and comprehensive as possible. The test suite currently has over 800 tests--but there are still some [bugs](https://github.com/RhetTbull/osxphotos/issues?q=is%3Aissue+is%3Aopen+label%3Abug) or incomplete features lurking. If you find bugs please open an [issue](https://github.com/RhetTbull/osxphotos/issues). Please consult the list of open bugs before deciding that you want to use this code on your Photos library. Notable issues include:
|
||||||
|
|
||||||
|
- Image orientation is not always correct for photos rotated in Photos.app. See [Issue #379](https://github.com/RhetTbull/osxphotos/issues/379).
|
||||||
- Face coordinates (mouth, left eye, right eye) may not be correct for images where the head is tilted. See [Issue #196](https://github.com/RhetTbull/osxphotos/issues/196).
|
- Face coordinates (mouth, left eye, right eye) may not be correct for images where the head is tilted. See [Issue #196](https://github.com/RhetTbull/osxphotos/issues/196).
|
||||||
- Raw images imported to Photos with an associated jpeg preview are not handled correctly by osxphotos. osxphotos query and export will operate on the jpeg preview instead of the raw image as will `PhotoInfo.path`. If the user selects "Use RAW as original" in Photos, the raw image will be exported or operated on but the jpeg will be ignored. See [Issue #101](https://github.com/RhetTbull/osxphotos/issues/101). Note: Beta version of fix for this bug is implemented in the current version of osxphotos.
|
- Raw images imported to Photos with an associated jpeg preview are not handled correctly by osxphotos. osxphotos query and export will operate on the jpeg preview instead of the raw image as will `PhotoInfo.path`. If the user selects "Use RAW as original" in Photos, the raw image will be exported or operated on but the jpeg will be ignored. See [Issue #101](https://github.com/RhetTbull/osxphotos/issues/101). Note: Beta version of fix for this bug is implemented in the current version of osxphotos.
|
||||||
- The `--download-missing` option for `osxphotos export` does not work correctly with burst images. It will download the primary image but not the other burst images. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75).
|
- The `--download-missing` option for `osxphotos export` does not work correctly with burst images. It will download the primary image but not the other burst images. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75).
|
||||||
@@ -2654,6 +2765,8 @@ For additional details about how osxphotos is implemented or if you would like t
|
|||||||
- [wurlitzer](https://pypi.org/project/wurlitzer/)
|
- [wurlitzer](https://pypi.org/project/wurlitzer/)
|
||||||
- [toml](https://github.com/uiri/toml)
|
- [toml](https://github.com/uiri/toml)
|
||||||
- [PhotoScript](https://github.com/RhetTbull/PhotoScript)
|
- [PhotoScript](https://github.com/RhetTbull/PhotoScript)
|
||||||
|
- [Rich](https://github.com/willmcgugan/rich)
|
||||||
|
- [textx](https://github.com/textX/textX)
|
||||||
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sphinx build info version 1
|
# Sphinx build info version 1
|
||||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
config: 547179dc83846d861e5f79c600fa9301
|
config: d0470550c1fa9feae481cebbbbc126af
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Overview: module code — osxphotos 0.40.4 documentation</title>
|
<title>Overview: module code — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo._photoinfo_exifinfo — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos.photoinfo._photoinfo_exifinfo — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||||
@@ -595,11 +595,11 @@
|
|||||||
<span class="k">if</span> <span class="n">export_db</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">export_db</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||||
<span class="n">export_db</span> <span class="o">=</span> <span class="n">ExportDBNoOp</span><span class="p">()</span>
|
<span class="n">export_db</span> <span class="o">=</span> <span class="n">ExportDBNoOp</span><span class="p">()</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">verbose</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">verbose</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">callable</span><span class="p">(</span><span class="n">verbose</span><span class="p">):</span>
|
||||||
<span class="n">verbose</span> <span class="o">=</span> <span class="n">noop</span>
|
|
||||||
<span class="k">elif</span> <span class="ow">not</span> <span class="n">callable</span><span class="p">(</span><span class="n">verbose</span><span class="p">):</span>
|
|
||||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"verbose must be callable"</span><span class="p">)</span>
|
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"verbose must be callable"</span><span class="p">)</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="n">verbose</span>
|
|
||||||
|
<span class="k">if</span> <span class="n">verbose</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||||
|
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
|
||||||
|
|
||||||
<span class="c1"># suffix to add to edited files</span>
|
<span class="c1"># suffix to add to edited files</span>
|
||||||
<span class="c1"># e.g. name will be filename_edited.jpg</span>
|
<span class="c1"># e.g. name will be filename_edited.jpg</span>
|
||||||
@@ -1534,8 +1534,8 @@
|
|||||||
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">long_str</span><span class="p">)</span> <span class="o">></span> <span class="n">_MAX_IPTC_KEYWORD_LEN</span>
|
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">long_str</span><span class="p">)</span> <span class="o">></span> <span class="n">_MAX_IPTC_KEYWORD_LEN</span>
|
||||||
<span class="p">]</span>
|
<span class="p">]</span>
|
||||||
<span class="k">if</span> <span class="n">long_keywords</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">long_keywords</span><span class="p">:</span>
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span><span class="p">(</span>
|
||||||
<span class="sa">f</span><span class="s2">"Some keywords exceed max IPTC Keyword length of </span><span class="si">{</span><span class="n">_MAX_IPTC_KEYWORD_LEN</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">long_keywords</span><span class="si">}</span><span class="s2">"</span>
|
<span class="sa">f</span><span class="s2">"Warning: some keywords exceed max IPTC Keyword length of </span><span class="si">{</span><span class="n">_MAX_IPTC_KEYWORD_LEN</span><span class="si">}</span><span class="s2"> (exiftool will truncate these): </span><span class="si">{</span><span class="n">long_keywords</span><span class="si">}</span><span class="s2">"</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
|
|
||||||
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span>
|
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span>
|
||||||
@@ -1814,17 +1814,6 @@
|
|||||||
<span class="k">if</span> <span class="n">_OSXPHOTOS_NONE_SENTINEL</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">keyword</span>
|
<span class="k">if</span> <span class="n">_OSXPHOTOS_NONE_SENTINEL</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">keyword</span>
|
||||||
<span class="p">]</span>
|
<span class="p">]</span>
|
||||||
|
|
||||||
<span class="c1"># check to see if any keywords too long</span>
|
|
||||||
<span class="n">long_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
|
||||||
<span class="n">long_str</span>
|
|
||||||
<span class="k">for</span> <span class="n">long_str</span> <span class="ow">in</span> <span class="n">rendered_keywords</span>
|
|
||||||
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">long_str</span><span class="p">)</span> <span class="o">></span> <span class="n">_MAX_IPTC_KEYWORD_LEN</span>
|
|
||||||
<span class="p">]</span>
|
|
||||||
<span class="k">if</span> <span class="n">long_keywords</span><span class="p">:</span>
|
|
||||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
|
|
||||||
<span class="sa">f</span><span class="s2">"Some keywords exceed max IPTC Keyword length of </span><span class="si">{</span><span class="n">_MAX_IPTC_KEYWORD_LEN</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">long_keywords</span><span class="si">}</span><span class="s2">"</span>
|
|
||||||
<span class="p">)</span>
|
|
||||||
|
|
||||||
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span>
|
<span class="n">keyword_list</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered_keywords</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># remove duplicates</span>
|
<span class="c1"># remove duplicates</span>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo._photoinfo_scoreinfo — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos.photoinfo._photoinfo_scoreinfo — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo._photoinfo_searchinfo — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos.photoinfo._photoinfo_searchinfo — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||||
@@ -103,6 +103,7 @@
|
|||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">info</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">info</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_verbose</span>
|
||||||
|
|
||||||
<span class="nd">@property</span>
|
<span class="nd">@property</span>
|
||||||
<span class="k">def</span> <span class="nf">filename</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
<span class="k">def</span> <span class="nf">filename</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos.photosdb.photosdb — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
|
||||||
|
|||||||
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
var DOCUMENTATION_OPTIONS = {
|
var DOCUMENTATION_OPTIONS = {
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||||
VERSION: '0.40.4',
|
VERSION: '0.41.0',
|
||||||
LANGUAGE: 'None',
|
LANGUAGE: 'None',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos command line interface (CLI) — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos command line interface (CLI) — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
@@ -465,6 +465,18 @@ to modify this behavior.</p>
|
|||||||
<dd><p>Search for photos that were imported as referenced files (not copied into Photos library).</p>
|
<dd><p>Search for photos that were imported as referenced files (not copied into Photos library).</p>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
|
<dl class="std option">
|
||||||
|
<dt id="cmdoption-osxphotos-export-in-album">
|
||||||
|
<code class="sig-name descname">--in-album</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-in-album" title="Permalink to this definition">¶</a></dt>
|
||||||
|
<dd><p>Search for photos that are in one or more albums.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
|
<dl class="std option">
|
||||||
|
<dt id="cmdoption-osxphotos-export-not-in-album">
|
||||||
|
<code class="sig-name descname">--not-in-album</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-not-in-album" title="Permalink to this definition">¶</a></dt>
|
||||||
|
<dd><p>Search for photos that are not in any albums.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
<dl class="std option">
|
<dl class="std option">
|
||||||
<dt id="cmdoption-osxphotos-export-missing">
|
<dt id="cmdoption-osxphotos-export-missing">
|
||||||
<code class="sig-name descname">--missing</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-missing" title="Permalink to this definition">¶</a></dt>
|
<code class="sig-name descname">--missing</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-missing" title="Permalink to this definition">¶</a></dt>
|
||||||
@@ -495,6 +507,12 @@ to modify this behavior.</p>
|
|||||||
<dd><p>When used with –update, ignores file signature when updating files. This is useful if you have processed or edited exported photos changing the file signature (size & modification date). In this case, –update would normally re-export the processed files but with –ignore-signature, files which exist in the export directory will not be re-exported.</p>
|
<dd><p>When used with –update, ignores file signature when updating files. This is useful if you have processed or edited exported photos changing the file signature (size & modification date). In this case, –update would normally re-export the processed files but with –ignore-signature, files which exist in the export directory will not be re-exported.</p>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
|
<dl class="std option">
|
||||||
|
<dt id="cmdoption-osxphotos-export-only-new">
|
||||||
|
<code class="sig-name descname">--only-new</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-only-new" title="Permalink to this definition">¶</a></dt>
|
||||||
|
<dd><p>If used with –update, ignores any previously exported files, even if missing from the export folder and only exports new files that haven’t previously been exported.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
<dl class="std option">
|
<dl class="std option">
|
||||||
<dt id="cmdoption-osxphotos-export-dry-run">
|
<dt id="cmdoption-osxphotos-export-dry-run">
|
||||||
<code class="sig-name descname">--dry-run</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-dry-run" title="Permalink to this definition">¶</a></dt>
|
<code class="sig-name descname">--dry-run</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-export-dry-run" title="Permalink to this definition">¶</a></dt>
|
||||||
@@ -691,7 +709,7 @@ to modify this behavior.</p>
|
|||||||
<dl class="std option">
|
<dl class="std option">
|
||||||
<dt id="cmdoption-osxphotos-export-jpeg-ext">
|
<dt id="cmdoption-osxphotos-export-jpeg-ext">
|
||||||
<code class="sig-name descname">--jpeg-ext</code><code class="sig-prename descclassname"> <EXTENSION></code><a class="headerlink" href="#cmdoption-osxphotos-export-jpeg-ext" title="Permalink to this definition">¶</a></dt>
|
<code class="sig-name descname">--jpeg-ext</code><code class="sig-prename descclassname"> <EXTENSION></code><a class="headerlink" href="#cmdoption-osxphotos-export-jpeg-ext" title="Permalink to this definition">¶</a></dt>
|
||||||
<dd><p>Specify file extension for JPEG files. Photos uses .jpeg for edited images but many images are imported with .jpg or .JPG which can result in multiple different extensions used for JPEG files upon export. Use –jpg-ext to specify a single extension to use for all exported JPEG images. Valid values are jpeg, jpg, JPEG, JPG; e.g. ‘–jpg-ext jpg’ to use ‘.jpg’ for all JPEGs.</p>
|
<dd><p>Specify file extension for JPEG files. Photos uses .jpeg for edited images but many images are imported with .jpg or .JPG which can result in multiple different extensions used for JPEG files upon export. Use –jpeg-ext to specify a single extension to use for all exported JPEG images. Valid values are jpeg, jpg, JPEG, JPG; e.g. ‘–jpeg-ext jpg’ to use ‘.jpg’ for all JPEGs.</p>
|
||||||
<dl class="field-list simple">
|
<dl class="field-list simple">
|
||||||
<dt class="field-odd">Options</dt>
|
<dt class="field-odd">Options</dt>
|
||||||
<dd class="field-odd"><p>jpeg|jpg|JPEG|JPG</p>
|
<dd class="field-odd"><p>jpeg|jpg|JPEG|JPG</p>
|
||||||
@@ -1263,6 +1281,18 @@ if more than one option is provided, they are treated as “AND”
|
|||||||
<dd><p>Search for photos that were imported as referenced files (not copied into Photos library).</p>
|
<dd><p>Search for photos that were imported as referenced files (not copied into Photos library).</p>
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
|
<dl class="std option">
|
||||||
|
<dt id="cmdoption-osxphotos-query-in-album">
|
||||||
|
<code class="sig-name descname">--in-album</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-query-in-album" title="Permalink to this definition">¶</a></dt>
|
||||||
|
<dd><p>Search for photos that are in one or more albums.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
|
<dl class="std option">
|
||||||
|
<dt id="cmdoption-osxphotos-query-not-in-album">
|
||||||
|
<code class="sig-name descname">--not-in-album</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-query-not-in-album" title="Permalink to this definition">¶</a></dt>
|
||||||
|
<dd><p>Search for photos that are not in any albums.</p>
|
||||||
|
</dd></dl>
|
||||||
|
|
||||||
<dl class="std option">
|
<dl class="std option">
|
||||||
<dt id="cmdoption-osxphotos-query-deleted">
|
<dt id="cmdoption-osxphotos-query-deleted">
|
||||||
<code class="sig-name descname">--deleted</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-query-deleted" title="Permalink to this definition">¶</a></dt>
|
<code class="sig-name descname">--deleted</code><code class="sig-prename descclassname"></code><a class="headerlink" href="#cmdoption-osxphotos-query-deleted" title="Permalink to this definition">¶</a></dt>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Index — osxphotos 0.40.4 documentation</title>
|
<title>Index — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
@@ -395,6 +395,15 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">osxphotos-export command line option</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">osxphotos-export command line option</a>
|
||||||
|
</li>
|
||||||
|
</ul></li>
|
||||||
|
<li>
|
||||||
|
--in-album
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-export-in-album">osxphotos-export command line option</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-query-in-album">osxphotos-query command line option</a>
|
||||||
</li>
|
</li>
|
||||||
</ul></li>
|
</ul></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -590,6 +599,15 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">osxphotos-export command line option</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">osxphotos-export command line option</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">osxphotos-query command line option</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">osxphotos-query command line option</a>
|
||||||
|
</li>
|
||||||
|
</ul></li>
|
||||||
|
<li>
|
||||||
|
--not-in-album
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-in-album">osxphotos-export command line option</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-query-not-in-album">osxphotos-query command line option</a>
|
||||||
</li>
|
</li>
|
||||||
</ul></li>
|
</ul></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -685,6 +703,13 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-export-only-movies">osxphotos-export command line option</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-only-movies">osxphotos-export command line option</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-query-only-movies">osxphotos-query command line option</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-only-movies">osxphotos-query command line option</a>
|
||||||
|
</li>
|
||||||
|
</ul></li>
|
||||||
|
<li>
|
||||||
|
--only-new
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-export-only-new">osxphotos-export command line option</a>
|
||||||
</li>
|
</li>
|
||||||
</ul></li>
|
</ul></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -1452,6 +1477,8 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-date-modified">--ignore-date-modified</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-date-modified">--ignore-date-modified</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">--ignore-signature</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-ignore-signature">--ignore-signature</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-export-in-album">--in-album</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-is-reference">--is-reference</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-is-reference">--is-reference</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -1488,6 +1515,8 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-export-not-hdr">--not-hdr</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-hdr">--not-hdr</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">--not-hidden</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-hidden">--not-hidden</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-in-album">--not-in-album</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-not-live">--not-live</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-live">--not-live</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -1506,6 +1535,8 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-export-not-time-lapse">--not-time-lapse</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-not-time-lapse">--not-time-lapse</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-only-movies">--only-movies</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-only-movies">--only-movies</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-export-only-new">--only-new</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-export-only-photos">--only-photos</a>
|
<li><a href="cli.html#cmdoption-osxphotos-export-only-photos">--only-photos</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -1694,6 +1725,8 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-query-hidden">--hidden</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-hidden">--hidden</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-query-i">--ignore-case</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-i">--ignore-case</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-query-in-album">--in-album</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-query-incloud">--incloud</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-incloud">--incloud</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -1728,6 +1761,8 @@
|
|||||||
<li><a href="cli.html#cmdoption-osxphotos-query-not-hdr">--not-hdr</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-not-hdr">--not-hdr</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">--not-hidden</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-not-hidden">--not-hidden</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="cli.html#cmdoption-osxphotos-query-not-in-album">--not-in-album</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="cli.html#cmdoption-osxphotos-query-not-incloud">--not-incloud</a>
|
<li><a href="cli.html#cmdoption-osxphotos-query-not-incloud">--not-incloud</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.40.4 documentation</title>
|
<title>Welcome to osxphotos’s documentation! — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
BIN
docs/objects.inv
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos package — osxphotos 0.40.4 documentation</title>
|
<title>osxphotos package — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — osxphotos 0.40.4 documentation</title>
|
<title>Search — osxphotos 0.41.0 documentation</title>
|
||||||
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ github:
|
|||||||
@make html
|
@make html
|
||||||
@cp -a _build/html/. ../docs
|
@cp -a _build/html/. ../docs
|
||||||
|
|
||||||
|
pdf:
|
||||||
|
@make latexpdf
|
||||||
|
@cp -a _build/latex/osxphotos.pdf ../docs
|
||||||
|
|
||||||
.PHONY: help Makefile
|
.PHONY: help Makefile
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import importlib
|
|||||||
pathex = os.getcwd()
|
pathex = os.getcwd()
|
||||||
|
|
||||||
# include necessary data files
|
# include necessary data files
|
||||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')]
|
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates'), ('osxphotos/phototemplate.tx', 'osxphotos'), ('osxphotos/phototemplate.md', 'osxphotos')]
|
||||||
package_imports = [['photoscript', ['photoscript.applescript']]]
|
package_imports = [['photoscript', ['photoscript.applescript']]]
|
||||||
for package, files in package_imports:
|
for package, files in package_imports:
|
||||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ _TESTED_OS_VERSIONS = [
|
|||||||
("10", "16"),
|
("10", "16"),
|
||||||
("11", "0"),
|
("11", "0"),
|
||||||
("11", "1"),
|
("11", "1"),
|
||||||
|
("11", "2"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Photos 5 has persons who are empty string if unidentified face
|
# Photos 5 has persons who are empty string if unidentified face
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.40.6"
|
__version__ = "0.41.2"
|
||||||
|
|||||||
174
osxphotos/adjustmentsinfo.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
""" AdjustmentsInfo class to read adjustments data for photos edited in Apple's Photos.app
|
||||||
|
In Catalina and Big Sur, the adjustments data (data about edits done to the photo)
|
||||||
|
is stored in a plist file in
|
||||||
|
~/Pictures/Photos Library.photoslibrary/resources/renders/X/UUID.plist
|
||||||
|
where X is first character of the photo's UUID string and UUID is the full UUID,
|
||||||
|
e.g.: ~/Pictures/Photos Library.photoslibrary/resources/renders/3/30362C1D-192F-4CCD-9A2A-968F436DC0DE.plist
|
||||||
|
|
||||||
|
Thanks to @neilpa who figured out how to decode this information:
|
||||||
|
Reference: https://github.com/neilpa/photohack/issues/4
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import plistlib
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from .datetime_utils import datetime_naive_to_utc
|
||||||
|
|
||||||
|
|
||||||
|
class AdjustmentsDecodeError(Exception):
|
||||||
|
"""Could not decode adjustments plist file"""
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class AdjustmentsInfo:
|
||||||
|
def __init__(self, plist_file):
|
||||||
|
self._plist_file = plist_file
|
||||||
|
self._plist = self._load_plist_file(plist_file)
|
||||||
|
|
||||||
|
self._base_version = self._plist.get("adjustmentBaseVersion", None)
|
||||||
|
self._data = self._plist.get("adjustmentData", None)
|
||||||
|
self._editor_bundle_id = self._plist.get("adjustmentEditorBundleID", None)
|
||||||
|
self._format_identifier = self._plist.get("adjustmentFormatIdentifier", None)
|
||||||
|
self._format_version = self._plist.get("adjustmentFormatVersion")
|
||||||
|
self._timestamp = self._plist.get("adjustmentTimestamp", None)
|
||||||
|
if self._timestamp and type(self._timestamp) == datetime.datetime:
|
||||||
|
self._timestamp = datetime_naive_to_utc(self._timestamp)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._adjustments = self._decode_adjustments_from_plist(self._plist)
|
||||||
|
except Exception as e:
|
||||||
|
self._adjustments = None
|
||||||
|
|
||||||
|
def _decode_adjustments_from_plist(self, plist):
|
||||||
|
"""decode adjustmentData from Apple Photos adjustments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plist: a plist dict as loaded by plistlib
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
decoded adjustmentsData as dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
return json.loads(
|
||||||
|
zlib.decompress(plist["adjustmentData"], -zlib.MAX_WBITS).decode()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _load_plist_file(self, plist_file):
|
||||||
|
"""Load plist file from disk
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plist_file: full path to plist file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
plist as dict
|
||||||
|
"""
|
||||||
|
with open(str(plist_file), "rb") as fd:
|
||||||
|
plist_dict = plistlib.load(fd)
|
||||||
|
return plist_dict
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plist(self):
|
||||||
|
"""The actual adjustments plist content as a dict """
|
||||||
|
return self._plist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
"""The raw adjustments data as a binary blob """
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def editor(self):
|
||||||
|
"""The editor bundle ID for app/plug-in which made the adjustments """
|
||||||
|
return self._editor_bundle_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format_id(self):
|
||||||
|
"""The value of the adjustmentFormatIdentifier field in the plist """
|
||||||
|
return self._format_identifier
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_version(self):
|
||||||
|
"""Value of adjustmentBaseVersion field """
|
||||||
|
return self._base_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format_version(self):
|
||||||
|
"""The value of the adjustmentFormatVersion in the plist """
|
||||||
|
return self._format_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self):
|
||||||
|
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp """
|
||||||
|
return self._timestamp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adjustments(self):
|
||||||
|
"""List of adjustment dictionaries (or empty list if none or could not be decoded)"""
|
||||||
|
try:
|
||||||
|
return self._adjustments["adjustments"] if self._adjustments else []
|
||||||
|
except KeyError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adj_metadata(self):
|
||||||
|
"""Metadata dictionary or None if adjustment data could not be decoded"""
|
||||||
|
try:
|
||||||
|
return self._adjustments["metadata"] if self._adjustments else None
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adj_orientation(self):
|
||||||
|
"""EXIF orientation of image or 0 if none specified or None if adjustments could not be decoded"""
|
||||||
|
try:
|
||||||
|
return self._adjustments["metadata"]["orientation"]
|
||||||
|
except KeyError:
|
||||||
|
# no orientation field
|
||||||
|
return 0
|
||||||
|
except TypeError:
|
||||||
|
# adjustments is None
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adj_format_version(self):
|
||||||
|
"""Format version for adjustments data (formatVersion field from adjustmentData) or None if adjustments could not be decoded"""
|
||||||
|
try:
|
||||||
|
return self._adjustments["formatVersion"] if self._adjustments else None
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adj_version_info(self):
|
||||||
|
"""version info for adjustments data or None if adjustments data could not be decoded"""
|
||||||
|
try:
|
||||||
|
return self._adjustments["versionInfo"] if self._adjustments else None
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
"""Returns all adjustments info as dictionary"""
|
||||||
|
timestamp = self.timestamp
|
||||||
|
if type(timestamp) == datetime.datetime:
|
||||||
|
timestamp = timestamp.isoformat()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": self.data,
|
||||||
|
"editor": self.editor,
|
||||||
|
"format_id": self.format_id,
|
||||||
|
"base_version": self.base_version,
|
||||||
|
"format_version": self.format_version,
|
||||||
|
"adjustments": self.adjustments,
|
||||||
|
"metadata": self.adj_metadata,
|
||||||
|
"orientation": self.adj_orientation,
|
||||||
|
"adjustment_format_version": self.adj_format_version,
|
||||||
|
"version_info": self.adj_version_info,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"AdjustmentsInfo(plist_file='{self._plist_file}')"
|
||||||
144
osxphotos/cli.py
@@ -236,7 +236,7 @@ def query_options(f):
|
|||||||
default=None,
|
default=None,
|
||||||
multiple=False,
|
multiple=False,
|
||||||
help="Search for photos with UUID(s) loaded from FILE. "
|
help="Search for photos with UUID(s) loaded from FILE. "
|
||||||
"Format is a single UUID per line. Lines preceeded with # are ignored.",
|
"Format is a single UUID per line. Lines preceded with # are ignored.",
|
||||||
type=click.Path(exists=True),
|
type=click.Path(exists=True),
|
||||||
),
|
),
|
||||||
o(
|
o(
|
||||||
@@ -403,6 +403,16 @@ def query_options(f):
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
||||||
),
|
),
|
||||||
|
o(
|
||||||
|
"--in-album",
|
||||||
|
is_flag=True,
|
||||||
|
help="Search for photos that are in one or more albums.",
|
||||||
|
),
|
||||||
|
o(
|
||||||
|
"--not-in-album",
|
||||||
|
is_flag=True,
|
||||||
|
help="Search for photos that are not in any albums.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
for o in options[::-1]:
|
for o in options[::-1]:
|
||||||
f = o(f)
|
f = o(f)
|
||||||
@@ -437,11 +447,18 @@ def cli(ctx, db, json_, debug):
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--ignore-signature",
|
"--ignore-signature",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="When used with --update, ignores file signature when updating files. "
|
help="When used with '--update', ignores file signature when updating files. "
|
||||||
"This is useful if you have processed or edited exported photos changing the "
|
"This is useful if you have processed or edited exported photos changing the "
|
||||||
"file signature (size & modification date). In this case, --update would normally "
|
"file signature (size & modification date). In this case, '--update' would normally "
|
||||||
"re-export the processed files but with --ignore-signature, files which exist "
|
"re-export the processed files but with '--ignore-signature', files which exist "
|
||||||
"in the export directory will not be re-exported.",
|
"in the export directory will not be re-exported. "
|
||||||
|
"If used with '--sidecar', '--ignore-signature' has the following behavior: "
|
||||||
|
"1) if the metadata (in Photos) that went into the sidecar did not change, "
|
||||||
|
"the sidecar will not be updated; "
|
||||||
|
"2) if the metadata (in Photos) that went into the sidecar did change, "
|
||||||
|
"a new sidecar is written but a new image file is not; "
|
||||||
|
"3) if a sidecar does not exist for the photo, a sidecar will be written "
|
||||||
|
"whether or not the photo file was written or updated.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--only-new",
|
"--only-new",
|
||||||
@@ -559,7 +576,8 @@ def cli(ctx, db, json_, debug):
|
|||||||
"\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. "
|
"\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. "
|
||||||
"Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. "
|
"Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. "
|
||||||
"Sidecar filename is in format photoname.ext.json; "
|
"Sidecar filename is in format photoname.ext.json; "
|
||||||
"For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'.",
|
"For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'. "
|
||||||
|
"See also '--ignore-signature'.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--sidecar-drop-ext",
|
"--sidecar-drop-ext",
|
||||||
@@ -642,8 +660,16 @@ def cli(ctx, db, json_, debug):
|
|||||||
"the full path to the folder and album photo is contained in as a keyword when exporting "
|
"the full path to the folder and album photo is contained in as a keyword when exporting "
|
||||||
'you could specify --keyword-template "{folder_album}" '
|
'you could specify --keyword-template "{folder_album}" '
|
||||||
'You may specify more than one template, for example --keyword-template "{folder_album}" '
|
'You may specify more than one template, for example --keyword-template "{folder_album}" '
|
||||||
'--keyword-template "{created.year}" '
|
'--keyword-template "{created.year}". '
|
||||||
"See Templating System below.",
|
"See '--replace-keywords' and Templating System below.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--replace-keywords",
|
||||||
|
is_flag=True,
|
||||||
|
help="Replace keywords with any values specified with --keyword-template. "
|
||||||
|
"By default, --keyword-template will add keywords to any keywords already associated "
|
||||||
|
"with the photo. If --replace-keywords is specified, values from --keyword-template "
|
||||||
|
"will replace any existing keywords instead of adding additional keywords.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--description-template",
|
"--description-template",
|
||||||
@@ -845,6 +871,7 @@ def export(
|
|||||||
person_keyword,
|
person_keyword,
|
||||||
album_keyword,
|
album_keyword,
|
||||||
keyword_template,
|
keyword_template,
|
||||||
|
replace_keywords,
|
||||||
description_template,
|
description_template,
|
||||||
finder_tag_template,
|
finder_tag_template,
|
||||||
finder_tag_keywords,
|
finder_tag_keywords,
|
||||||
@@ -907,6 +934,8 @@ def export(
|
|||||||
save_config,
|
save_config,
|
||||||
is_reference,
|
is_reference,
|
||||||
beta,
|
beta,
|
||||||
|
in_album,
|
||||||
|
not_in_album,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -988,6 +1017,7 @@ def export(
|
|||||||
person_keyword = cfg.person_keyword
|
person_keyword = cfg.person_keyword
|
||||||
album_keyword = cfg.album_keyword
|
album_keyword = cfg.album_keyword
|
||||||
keyword_template = cfg.keyword_template
|
keyword_template = cfg.keyword_template
|
||||||
|
replace_keywords = cfg.replace_keywords
|
||||||
description_template = cfg.description_template
|
description_template = cfg.description_template
|
||||||
finder_tag_template = cfg.finder_tag_template
|
finder_tag_template = cfg.finder_tag_template
|
||||||
finder_tag_keywords = cfg.finder_tag_keywords
|
finder_tag_keywords = cfg.finder_tag_keywords
|
||||||
@@ -1047,6 +1077,8 @@ def export(
|
|||||||
exportdb = cfg.exportdb
|
exportdb = cfg.exportdb
|
||||||
beta = cfg.beta
|
beta = cfg.beta
|
||||||
only_new = cfg.only_new
|
only_new = cfg.only_new
|
||||||
|
in_album = cfg.in_album
|
||||||
|
not_in_album = cfg.not_in_album
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
VERBOSE = bool(verbose)
|
VERBOSE = bool(verbose)
|
||||||
@@ -1080,6 +1112,7 @@ def export(
|
|||||||
("shared", "not_shared"),
|
("shared", "not_shared"),
|
||||||
("has_comment", "no_comment"),
|
("has_comment", "no_comment"),
|
||||||
("has_likes", "no_likes"),
|
("has_likes", "no_likes"),
|
||||||
|
("in_album", "not_in_album"),
|
||||||
]
|
]
|
||||||
dependent_options = [
|
dependent_options = [
|
||||||
("missing", ("download_missing", "use_photos_export")),
|
("missing", ("download_missing", "use_photos_export")),
|
||||||
@@ -1339,6 +1372,8 @@ def export(
|
|||||||
has_likes=has_likes,
|
has_likes=has_likes,
|
||||||
no_likes=no_likes,
|
no_likes=no_likes,
|
||||||
is_reference=is_reference,
|
is_reference=is_reference,
|
||||||
|
in_album=in_album,
|
||||||
|
not_in_album=not_in_album,
|
||||||
)
|
)
|
||||||
|
|
||||||
if photos:
|
if photos:
|
||||||
@@ -1409,6 +1444,7 @@ def export(
|
|||||||
exiftool_option=exiftool_option,
|
exiftool_option=exiftool_option,
|
||||||
strip=strip,
|
strip=strip,
|
||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
results += export_results
|
results += export_results
|
||||||
|
|
||||||
@@ -1620,6 +1656,8 @@ def query(
|
|||||||
has_likes,
|
has_likes,
|
||||||
no_likes,
|
no_likes,
|
||||||
is_reference,
|
is_reference,
|
||||||
|
in_album,
|
||||||
|
not_in_album,
|
||||||
):
|
):
|
||||||
"""Query the Photos database using 1 or more search options;
|
"""Query the Photos database using 1 or more search options;
|
||||||
if more than one option is provided, they are treated as "AND"
|
if more than one option is provided, they are treated as "AND"
|
||||||
@@ -1667,6 +1705,7 @@ def query(
|
|||||||
(shared, not_shared),
|
(shared, not_shared),
|
||||||
(has_comment, no_comment),
|
(has_comment, no_comment),
|
||||||
(has_likes, no_likes),
|
(has_likes, no_likes),
|
||||||
|
(in_album, not_in_album),
|
||||||
]
|
]
|
||||||
# print help if no non-exclusive term or a double exclusive term is given
|
# print help if no non-exclusive term or a double exclusive term is given
|
||||||
if any(all(bb) for bb in exclusive) or not any(
|
if any(all(bb) for bb in exclusive) or not any(
|
||||||
@@ -1759,6 +1798,8 @@ def query(
|
|||||||
has_likes=has_likes,
|
has_likes=has_likes,
|
||||||
no_likes=no_likes,
|
no_likes=no_likes,
|
||||||
is_reference=is_reference,
|
is_reference=is_reference,
|
||||||
|
in_album=in_album,
|
||||||
|
not_in_album=not_in_album,
|
||||||
)
|
)
|
||||||
|
|
||||||
# below needed for to make CliRunner work for testing
|
# below needed for to make CliRunner work for testing
|
||||||
@@ -1930,6 +1971,8 @@ def _query(
|
|||||||
has_likes=False,
|
has_likes=False,
|
||||||
no_likes=False,
|
no_likes=False,
|
||||||
is_reference=False,
|
is_reference=False,
|
||||||
|
in_album=False,
|
||||||
|
not_in_album=False,
|
||||||
):
|
):
|
||||||
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
|
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
|
||||||
|
|
||||||
@@ -2162,6 +2205,11 @@ def _query(
|
|||||||
if is_reference:
|
if is_reference:
|
||||||
photos = [p for p in photos if p.isreference]
|
photos = [p for p in photos if p.isreference]
|
||||||
|
|
||||||
|
if in_album:
|
||||||
|
photos = [p for p in photos if p.albums]
|
||||||
|
elif not_in_album:
|
||||||
|
photos = [p for p in photos if not p.albums]
|
||||||
|
|
||||||
return photos
|
return photos
|
||||||
|
|
||||||
|
|
||||||
@@ -2233,6 +2281,7 @@ def export_photo(
|
|||||||
exiftool_option=None,
|
exiftool_option=None,
|
||||||
strip=False,
|
strip=False,
|
||||||
jpeg_ext=None,
|
jpeg_ext=None,
|
||||||
|
replace_keywords=False,
|
||||||
):
|
):
|
||||||
"""Helper function for export that does the actual export
|
"""Helper function for export that does the actual export
|
||||||
|
|
||||||
@@ -2271,6 +2320,7 @@ def export_photo(
|
|||||||
exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
jpeg_ext: if not None, specify the extension to use for all JPEG images on export
|
jpeg_ext: if not None, specify the extension to use for all JPEG images on export
|
||||||
|
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of path(s) of exported photo or None if photo was missing
|
list of path(s) of exported photo or None if photo was missing
|
||||||
@@ -2334,15 +2384,15 @@ def export_photo(
|
|||||||
rendered_suffix, unmatched = photo.render_template(
|
rendered_suffix, unmatched = photo.render_template(
|
||||||
original_suffix, filename=True, strip=strip
|
original_suffix, filename=True, strip=strip
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"original_suffix",
|
"original_suffix",
|
||||||
f"Invalid template for --original-suffix '{original_suffix}'",
|
f"Invalid template for --original-suffix '{original_suffix}': {e}",
|
||||||
)
|
)
|
||||||
if not rendered_suffix or unmatched:
|
if not rendered_suffix or unmatched:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"original_suffix",
|
"original_suffix",
|
||||||
f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unmatched={unmatched}",
|
f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unknown field={unmatched}",
|
||||||
)
|
)
|
||||||
if len(rendered_suffix) > 1:
|
if len(rendered_suffix) > 1:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
@@ -2352,13 +2402,16 @@ def export_photo(
|
|||||||
rendered_suffix = rendered_suffix[0]
|
rendered_suffix = rendered_suffix[0]
|
||||||
|
|
||||||
original_filename = pathlib.Path(filename)
|
original_filename = pathlib.Path(filename)
|
||||||
file_ext = (
|
file_ext = original_filename.suffix
|
||||||
"." + jpeg_ext
|
if photo.isphoto and (jpeg_ext or convert_to_jpeg):
|
||||||
if jpeg_ext and (photo.uti == "public.jpeg" or convert_to_jpeg)
|
# change the file extension to correct jpeg extension if needed
|
||||||
else ".jpeg"
|
file_ext = (
|
||||||
if convert_to_jpeg and photo.uti != "public.jpeg"
|
"." + jpeg_ext
|
||||||
else original_filename.suffix
|
if jpeg_ext and (photo.uti_original == "public.jpeg" or convert_to_jpeg)
|
||||||
)
|
else ".jpeg"
|
||||||
|
if convert_to_jpeg and photo.uti_original != "public.jpeg"
|
||||||
|
else original_filename.suffix
|
||||||
|
)
|
||||||
original_filename = (
|
original_filename = (
|
||||||
original_filename.parent
|
original_filename.parent
|
||||||
/ f"{original_filename.stem}{rendered_suffix}{file_ext}"
|
/ f"{original_filename.stem}{rendered_suffix}{file_ext}"
|
||||||
@@ -2448,6 +2501,7 @@ def export_photo(
|
|||||||
verbose=verbose_,
|
verbose=verbose_,
|
||||||
exiftool_flags=exiftool_option,
|
exiftool_flags=exiftool_option,
|
||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
results += export_results
|
results += export_results
|
||||||
for warning_ in export_results.exiftool_warning:
|
for warning_ in export_results.exiftool_warning:
|
||||||
@@ -2490,17 +2544,28 @@ def export_photo(
|
|||||||
if export_edited and photo.hasadjustments:
|
if export_edited and photo.hasadjustments:
|
||||||
edited_filename = pathlib.Path(filename)
|
edited_filename = pathlib.Path(filename)
|
||||||
edited_ext = (
|
edited_ext = (
|
||||||
"." + jpeg_ext
|
# rare cases on Photos <= 4 that uti_edited is None
|
||||||
if jpeg_ext and photo.uti_edited == "public.jpeg"
|
"." + get_preferred_uti_extension(photo.uti_edited)
|
||||||
else "." + get_preferred_uti_extension(photo.uti_edited)
|
|
||||||
if photo.uti_edited
|
if photo.uti_edited
|
||||||
else pathlib.Path(photo.path_edited).suffix
|
else pathlib.Path(photo.path_edited).suffix
|
||||||
if photo.path_edited
|
if photo.path_edited
|
||||||
else pathlib.Path(photo.filename).suffix
|
else pathlib.Path(photo.filename).suffix
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
photo.isphoto
|
||||||
|
and jpeg_ext
|
||||||
|
and edited_ext.lower() in [".jpg", ".jpeg"]
|
||||||
|
):
|
||||||
|
edited_ext = "." + jpeg_ext
|
||||||
|
|
||||||
# Big Sur uses .heic for some edited photos so need to check
|
# Big Sur uses .heic for some edited photos so need to check
|
||||||
# if extension isn't jpeg/jpg and using --convert-to-jpeg
|
# if extension isn't jpeg/jpg and using --convert-to-jpeg
|
||||||
if convert_to_jpeg and edited_ext.lower() not in [".jpg", ".jpeg"]:
|
if (
|
||||||
|
photo.isphoto
|
||||||
|
and convert_to_jpeg
|
||||||
|
and edited_ext.lower() not in [".jpg", ".jpeg"]
|
||||||
|
):
|
||||||
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
|
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
|
||||||
|
|
||||||
if edited_suffix:
|
if edited_suffix:
|
||||||
@@ -2508,15 +2573,15 @@ def export_photo(
|
|||||||
rendered_suffix, unmatched = photo.render_template(
|
rendered_suffix, unmatched = photo.render_template(
|
||||||
edited_suffix, filename=True, strip=strip
|
edited_suffix, filename=True, strip=strip
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"edited_suffix",
|
"edited_suffix",
|
||||||
f"Invalid template for --edited-suffix '{edited_suffix}'",
|
f"Invalid template for --edited-suffix '{edited_suffix}': {e}",
|
||||||
)
|
)
|
||||||
if not rendered_suffix or unmatched:
|
if not rendered_suffix or unmatched:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"edited_suffix",
|
"edited_suffix",
|
||||||
f"Invalid template for --edited-suffix '{edited_suffix}': results={rendered_suffix} unmatched={unmatched}",
|
f"Invalid template for --edited-suffix '{edited_suffix}': unknown field={unmatched}",
|
||||||
)
|
)
|
||||||
if len(rendered_suffix) > 1:
|
if len(rendered_suffix) > 1:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
@@ -2584,6 +2649,7 @@ def export_photo(
|
|||||||
verbose=verbose_,
|
verbose=verbose_,
|
||||||
exiftool_flags=exiftool_option,
|
exiftool_flags=exiftool_option,
|
||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
results += export_results_edited
|
results += export_results_edited
|
||||||
for warning_ in export_results_edited.exiftool_warning:
|
for warning_ in export_results_edited.exiftool_warning:
|
||||||
@@ -2655,14 +2721,14 @@ def get_filenames_from_template(photo, filename_template, original_name, strip=F
|
|||||||
filenames, unmatched = photo.render_template(
|
filenames, unmatched = photo.render_template(
|
||||||
filename_template, path_sep="_", filename=True, strip=strip
|
filename_template, path_sep="_", filename=True, strip=strip
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"filename_template", f"Invalid template '{filename_template}'"
|
"filename_template", f"Invalid template '{filename_template}': {e}"
|
||||||
)
|
)
|
||||||
if not filenames or unmatched:
|
if not filenames or unmatched:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"filename_template",
|
"filename_template",
|
||||||
f"Invalid template '{filename_template}': results={filenames} unmatched={unmatched}",
|
f"Invalid template '{filename_template}': unknown field={unmatched}",
|
||||||
)
|
)
|
||||||
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
||||||
else:
|
else:
|
||||||
@@ -2709,12 +2775,14 @@ def get_dirnames_from_template(
|
|||||||
dirnames, unmatched = photo.render_template(
|
dirnames, unmatched = photo.render_template(
|
||||||
directory, dirname=True, strip=strip
|
directory, dirname=True, strip=strip
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise click.BadOptionUsage("directory", f"Invalid template '{directory}'")
|
raise click.BadOptionUsage(
|
||||||
|
"directory", f"Invalid template '{directory}': {e}"
|
||||||
|
)
|
||||||
if not dirnames or unmatched:
|
if not dirnames or unmatched:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"directory",
|
"directory",
|
||||||
f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}",
|
f"Invalid template '{directory}': unknown field={unmatched}",
|
||||||
)
|
)
|
||||||
|
|
||||||
dest_paths = []
|
dest_paths = []
|
||||||
@@ -2938,7 +3006,7 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple of (number of files deleted, number of directories deleted)
|
tuple of (number of files deleted, number of directories deleted)
|
||||||
"""
|
"""
|
||||||
keepers = {filename.lower(): 1 for filename in files_to_keep}
|
keepers = {str(filename).lower(): 1 for filename in files_to_keep}
|
||||||
|
|
||||||
deleted_files = 0
|
deleted_files = 0
|
||||||
for p in pathlib.Path(dest_path).rglob("*"):
|
for p in pathlib.Path(dest_path).rglob("*"):
|
||||||
@@ -3015,16 +3083,16 @@ def write_finder_tags(
|
|||||||
path_sep="/",
|
path_sep="/",
|
||||||
strip=strip,
|
strip=strip,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"finder_tag_template",
|
"finder_tag_template",
|
||||||
f"Invalid template for --finder-tag-template': {template_str}",
|
f"Invalid template for --finder-tag-template '{template_str}': {e}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if unmatched:
|
if unmatched:
|
||||||
click.echo(
|
click.echo(
|
||||||
click.style(
|
click.style(
|
||||||
f"Warning: unmatched template substitution for template: {template_str} {unmatched}",
|
f"Warning: unknown field for template: {template_str} unknown field = {unmatched}",
|
||||||
fg=CLI_COLOR_WARNING,
|
fg=CLI_COLOR_WARNING,
|
||||||
),
|
),
|
||||||
err=True,
|
err=True,
|
||||||
@@ -3071,15 +3139,15 @@ def write_extended_attributes(photo, files, xattr_template, strip=False):
|
|||||||
path_sep="/",
|
path_sep="/",
|
||||||
strip=strip,
|
strip=strip,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise click.BadOptionUsage(
|
raise click.BadOptionUsage(
|
||||||
"xattr_template",
|
"xattr_template",
|
||||||
f"Invalid template for --xattr-template': {template_str}",
|
f"Invalid template for --xattr-template '{template_str}': {e}",
|
||||||
)
|
)
|
||||||
if unmatched:
|
if unmatched:
|
||||||
click.echo(
|
click.echo(
|
||||||
click.style(
|
click.style(
|
||||||
f"Warning: unmatched template substitution for template: {template_str} {unmatched}",
|
f"Warning: unmatched template substitution for template: {template_str} unknown field={unmatched}",
|
||||||
fg=CLI_COLOR_WARNING,
|
fg=CLI_COLOR_WARNING,
|
||||||
),
|
),
|
||||||
err=True,
|
err=True,
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
"""Help text helper class for osxphotos CLI """
|
"""Help text helper class for osxphotos CLI """
|
||||||
|
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import osxmetadata
|
import osxmetadata
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
from ._constants import (
|
from ._constants import (
|
||||||
EXTENDED_ATTRIBUTE_NAMES,
|
EXTENDED_ATTRIBUTE_NAMES,
|
||||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||||
OSXPHOTOS_EXPORT_DB,
|
OSXPHOTOS_EXPORT_DB,
|
||||||
)
|
)
|
||||||
from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
from .phototemplate import (
|
||||||
|
TEMPLATE_SUBSTITUTIONS,
|
||||||
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||||
|
get_template_help,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportCommand(click.Command):
|
class ExportCommand(click.Command):
|
||||||
@@ -17,11 +26,11 @@ class ExportCommand(click.Command):
|
|||||||
def get_help(self, ctx):
|
def get_help(self, ctx):
|
||||||
help_text = super().get_help(ctx)
|
help_text = super().get_help(ctx)
|
||||||
formatter = click.HelpFormatter()
|
formatter = click.HelpFormatter()
|
||||||
|
|
||||||
# passed to click.HelpFormatter.write_dl for formatting
|
# passed to click.HelpFormatter.write_dl for formatting
|
||||||
|
|
||||||
formatter.write("\n\n")
|
formatter.write("\n\n")
|
||||||
formatter.write_text("** Export **")
|
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
|
||||||
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"When exporting photos, osxphotos creates a database in the top-level "
|
"When exporting photos, osxphotos creates a database in the top-level "
|
||||||
+ f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information "
|
+ f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information "
|
||||||
@@ -56,7 +65,7 @@ class ExportCommand(click.Command):
|
|||||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||||
)
|
)
|
||||||
formatter.write("\n\n")
|
formatter.write("\n\n")
|
||||||
formatter.write_text("** Extended Attributes **")
|
formatter.write(rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width))
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"""
|
"""
|
||||||
@@ -90,124 +99,9 @@ The following attributes may be used with '--xattr-template':
|
|||||||
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
||||||
)
|
)
|
||||||
formatter.write("\n\n")
|
formatter.write("\n\n")
|
||||||
formatter.write_text("** Templating System **")
|
formatter.write(rich_text("[bold]** Templating System **[/bold]", width=formatter.width))
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write(template_help(width=formatter.width))
|
||||||
"""
|
|
||||||
Several options, such as --directory, allow you to specify a template which
|
|
||||||
will be rendered to substitute template fields with values from the photo.
|
|
||||||
For example, '{created.month}' would be replaced with the month name of the
|
|
||||||
photo creation date. e.g. 'November'.
|
|
||||||
|
|
||||||
Some options supporting templates may be repeated e.g., --keyword-template
|
|
||||||
'{label}' --keyword-template '{media_type}' to add both labels and media
|
|
||||||
types to the keywords.
|
|
||||||
|
|
||||||
The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full template format is:
|
|
||||||
'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}'
|
|
||||||
|
|
||||||
With a few exceptions (like '{created.strftime}') everything but the TEMPLATE_FIELD
|
|
||||||
is optional.
|
|
||||||
|
|
||||||
- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in place'
|
|
||||||
with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'.
|
|
||||||
For example, a photo with keywords 'foo' and 'bar':
|
|
||||||
|
|
||||||
'{keyword}' renders to 'foo' and 'bar'
|
|
||||||
|
|
||||||
'{,+keyword}' renders to: 'foo,bar'
|
|
||||||
|
|
||||||
'{; +keyword}' renders to: 'foo; bar'
|
|
||||||
|
|
||||||
'{+keyword}' renders to 'foobar'
|
|
||||||
|
|
||||||
- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword'
|
|
||||||
|
|
||||||
- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in
|
|
||||||
that they join multiple elements into a single path-like string. For example,
|
|
||||||
if photo is in album Album1 in folder Folder1, '{folder_album}' results in
|
|
||||||
'Folder1/Album1'. This is so these template fields may be used as paths in
|
|
||||||
--directory. If you intend to use such a field as a string, e.g. in the
|
|
||||||
filename, you may specify a different path separator using the form:
|
|
||||||
'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above,
|
|
||||||
'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}'
|
|
||||||
would result in 'Folder1Album1'.
|
|
||||||
|
|
||||||
- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template value
|
|
||||||
with text "NEW". For example, if you have album names with '/' in the album name you
|
|
||||||
could replace '/' with "-" using the template '{album[/,-]}'. This would replace
|
|
||||||
any occurence of "/" in the album name with "-"; album "Vacation/2019" would thus
|
|
||||||
become "Vacation-2019". You may specify more than one pair of OLD,NEW values by
|
|
||||||
listing them delimited by '|'. For example: '{album[/,-|:,-]}' to replace both
|
|
||||||
'/' and ':' by '-'. You can also use the [OLD,NEW] syntax to delete a character by
|
|
||||||
omitting the NEW value as in '{album[/,]}'.
|
|
||||||
|
|
||||||
- '?' Some template fields such as 'hdr' are boolean and resolve to True or False.
|
|
||||||
These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g.
|
|
||||||
{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image
|
|
||||||
and 'not_hdr' otherwise.
|
|
||||||
|
|
||||||
- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results
|
|
||||||
in a null (empty) value, the template will result in default value of '_'.
|
|
||||||
You may specify an alternate default value by appending ',DEFAULT' after
|
|
||||||
template_field. Example: '{title,no_title}' would result in 'no_title' if the photo
|
|
||||||
had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but there was
|
|
||||||
no address associated with the photo, the resulting output would be:
|
|
||||||
'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not
|
|
||||||
contain a brace symbol ('{' or '}').
|
|
||||||
|
|
||||||
Again, if you do not specify a default value and the template substitution has no
|
|
||||||
value, '_' (underscore) will be used as the default value. For example, in the
|
|
||||||
above example, this would result in '2020/_/photoname.jpg' if address was
|
|
||||||
null.
|
|
||||||
|
|
||||||
You may specify a null default (e.g. "" or empty string) by omitting the value
|
|
||||||
after the comma, e.g. {title,} which would render to "" if title had no value thus
|
|
||||||
effectively deleting the template from the resulting string.
|
|
||||||
|
|
||||||
You may include other text in the template string outside the {}
|
|
||||||
and use more than one template field in a single string,
|
|
||||||
e.g. '{created.year} - {created.month}' (e.g. '2020 - November').
|
|
||||||
|
|
||||||
Some templates may resolve to more than one value. For example, a photo can
|
|
||||||
have multiple keywords so '{keyword}' can result in multiple values. If used
|
|
||||||
in a filename or directory, these templates may result in more than one copy
|
|
||||||
of the photo being exported. For example, if photo has keywords "foo" and
|
|
||||||
"bar", --directory '{keyword}' will result in copies of the photo being
|
|
||||||
exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'.
|
|
||||||
|
|
||||||
Some template fields such as '{media_type}' use the 'DEFAULT' value to allow
|
|
||||||
customization of the output. For example, '{media_type}' resolves to the
|
|
||||||
special media type of the photo such as 'panorama' or 'selfie'. You may use
|
|
||||||
the 'DEFAULT' value to override these in form:
|
|
||||||
'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if
|
|
||||||
photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée'
|
|
||||||
instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an
|
|
||||||
ordinary video.
|
|
||||||
|
|
||||||
With the --directory and --filename options you may specify a template for the
|
|
||||||
export directory or filename, respectively. The directory will be appended to
|
|
||||||
the export path specified in the export DEST argument to export. For example,
|
|
||||||
if template is '{created.year}/{created.month}', and export destination DEST
|
|
||||||
is '/Users/maria/Pictures/export', the actual export directory for a photo
|
|
||||||
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
|
|
||||||
March 2020.
|
|
||||||
|
|
||||||
The templating system may also be used with the --keyword-template option to
|
|
||||||
set keywords on export (with --exiftool or --sidecar), for example, to set a
|
|
||||||
new keyword in format 'folder/subfolder/album' to preserve the folder/album
|
|
||||||
structure, you can use --keyword-template "{folder_album}"
|
|
||||||
|
|
||||||
In the template, valid template substitutions will be replaced by the
|
|
||||||
corresponding value from the table below. Invalid substitutions will result
|
|
||||||
in an error.
|
|
||||||
|
|
||||||
If you want the actual text of the template substition to appear in the
|
|
||||||
rendered name, use double braces, e.g. '{{' or '}}', thus using
|
|
||||||
'{created.year}/{{name}}' for --directory would result in output of
|
|
||||||
2020/{name}/photoname.jpg
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"With the --directory and --filename options you may specify a template for the "
|
"With the --directory and --filename options you may specify a template for the "
|
||||||
@@ -224,7 +118,8 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
|
|||||||
"The templating system may also be used with the --keyword-template option "
|
"The templating system may also be used with the --keyword-template option "
|
||||||
+ "to set keywords on export (with --exiftool or --sidecar), "
|
+ "to set keywords on export (with --exiftool or --sidecar), "
|
||||||
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
|
+ "for example, to set a new keyword in format 'folder/subfolder/album' to "
|
||||||
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"'
|
+ 'preserve the folder/album structure, you can use --keyword-template "{folder_album}" '
|
||||||
|
+ "or in the 'folder>subfolder>album' format used in Lightroom Classic, --keyword-template \"{folder_album(>)}\"."
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
@@ -233,33 +128,7 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
|
|||||||
+ "an error and the script will abort."
|
+ "an error and the script will abort."
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write(rich_text("[bold]** Template Substitutions **[/bold]", width=formatter.width))
|
||||||
"If you want the actual text of the template substition to appear "
|
|
||||||
+ "in the rendered name, use double braces, e.g. '{{' or '}}', thus "
|
|
||||||
+ "using '{created.year}/{{name}}' for --directory "
|
|
||||||
+ "would result in output of 2020/{name}/photoname.jpg"
|
|
||||||
)
|
|
||||||
formatter.write("\n")
|
|
||||||
formatter.write_text(
|
|
||||||
"You may specify an optional default value to use if the substitution does not contain a value "
|
|
||||||
+ "(e.g. the value is null) "
|
|
||||||
+ "by specifying the default value after a ',' in the template string: "
|
|
||||||
+ "for example, if template is '{created.year}/{place.address,NO_ADDRESS}' "
|
|
||||||
+ "but there was no address associated with the photo, the resulting output would be: "
|
|
||||||
+ "'2020/NO_ADDRESS/photoname.jpg'. "
|
|
||||||
+ "If specified, the default value may not contain a brace symbol ('{' or '}')."
|
|
||||||
)
|
|
||||||
formatter.write("\n")
|
|
||||||
formatter.write_text(
|
|
||||||
"If you do not specify a default value and the template substitution "
|
|
||||||
+ "has no value, '_' (underscore) will be used as the default value. For example, in the "
|
|
||||||
+ "above example, this would result in '2020/_/photoname.jpg' if address was null."
|
|
||||||
)
|
|
||||||
formatter.write("\n")
|
|
||||||
formatter.write_text(
|
|
||||||
'You may specify a null default (e.g. "" or empty string) by omitting the value after '
|
|
||||||
+ 'the comma, e.g. {title,} which would render to "" if title had no value.'
|
|
||||||
)
|
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
templ_tuples = [("Substitution", "Description")]
|
templ_tuples = [("Substitution", "Description")]
|
||||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
||||||
@@ -284,3 +153,45 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
|
|||||||
formatter.write_dl(templ_tuples)
|
formatter.write_dl(templ_tuples)
|
||||||
help_text += formatter.getvalue()
|
help_text += formatter.getvalue()
|
||||||
return help_text
|
return help_text
|
||||||
|
|
||||||
|
|
||||||
|
def template_help(width=78):
|
||||||
|
"""Return formatted string for template system """
|
||||||
|
sio = io.StringIO()
|
||||||
|
console = Console(file=sio, force_terminal=True, width=width)
|
||||||
|
template_help_md = strip_md_links(get_template_help())
|
||||||
|
console.print(Markdown(template_help_md))
|
||||||
|
help_str = sio.getvalue()
|
||||||
|
sio.close()
|
||||||
|
return help_str
|
||||||
|
|
||||||
|
|
||||||
|
def rich_text(text, width=78):
|
||||||
|
"""Return rich formatted text"""
|
||||||
|
sio = io.StringIO()
|
||||||
|
console = Console(file=sio, force_terminal=True, width=width)
|
||||||
|
console.print(text)
|
||||||
|
rich_text = sio.getvalue()
|
||||||
|
sio.close()
|
||||||
|
return rich_text
|
||||||
|
|
||||||
|
|
||||||
|
def strip_md_links(md):
|
||||||
|
"""strip markdown links from markdown text md
|
||||||
|
|
||||||
|
Args:
|
||||||
|
md: str, markdown text
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str with markdown links removed
|
||||||
|
|
||||||
|
Note: This uses a very basic regex that likely fails on all sorts of edge cases
|
||||||
|
but works for the links in the osxphotos docs
|
||||||
|
"""
|
||||||
|
links = r"(?:[*#])|\[(.*?)\]\(.+?\)"
|
||||||
|
|
||||||
|
def subfn(match):
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
return re.sub(links, subfn, md)
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ class ExifTool:
|
|||||||
command = [f"-{tag}={value}"]
|
command = [f"-{tag}={value}"]
|
||||||
if self.overwrite and not self._context_mgr:
|
if self.overwrite and not self._context_mgr:
|
||||||
command.append("-overwrite_original")
|
command.append("-overwrite_original")
|
||||||
|
|
||||||
|
# avoid "Warning: Some character(s) could not be encoded in Latin" warning
|
||||||
|
command.append("-iptc:codedcharacterset=utf8")
|
||||||
|
|
||||||
if self._context_mgr:
|
if self._context_mgr:
|
||||||
self._commands.extend(command)
|
self._commands.extend(command)
|
||||||
return True
|
return True
|
||||||
@@ -254,7 +258,11 @@ class ExifTool:
|
|||||||
filename = os.fsencode(self.file) if not no_file else b""
|
filename = os.fsencode(self.file) if not no_file else b""
|
||||||
|
|
||||||
if self.flags:
|
if self.flags:
|
||||||
command_str = b"\n".join([f.encode("utf-8") for f in self.flags])
|
# need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]
|
||||||
|
flags = []
|
||||||
|
for f in self.flags:
|
||||||
|
flags.extend(f.split())
|
||||||
|
command_str = b"\n".join([f.encode("utf-8") for f in flags])
|
||||||
command_str += b"\n"
|
command_str += b"\n"
|
||||||
else:
|
else:
|
||||||
command_str = b""
|
command_str = b""
|
||||||
@@ -311,7 +319,13 @@ class ExifTool:
|
|||||||
if not json_str:
|
if not json_str:
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
exifdict = json.loads(json_str)
|
try:
|
||||||
|
exifdict = json.loads(json_str)
|
||||||
|
except Exception as e:
|
||||||
|
# will fail with some commands, e.g --ext AVI which produces
|
||||||
|
# 'No file with specified extension' instead of json
|
||||||
|
return dict()
|
||||||
|
|
||||||
exifdict = exifdict[0]
|
exifdict = exifdict[0]
|
||||||
if not tag_groups:
|
if not tag_groups:
|
||||||
# strip tag groups
|
# strip tag groups
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
|||||||
|
|
||||||
|
|
||||||
class PersonInfo:
|
class PersonInfo:
|
||||||
""" Info about a person in the Photos library
|
"""Info about a person in the Photos library"""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, db=None, pk=None):
|
def __init__(self, db=None, pk=None):
|
||||||
""" Creates a new PersonInfo instance
|
"""Creates a new PersonInfo instance
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
db: instance of PhotosDB object
|
db: instance of PhotosDB object
|
||||||
@@ -57,8 +56,8 @@ class PersonInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def face_info(self):
|
def face_info(self):
|
||||||
""" Returns a list of FaceInfo objects associated with this person sorted by quality score
|
"""Returns a list of FaceInfo objects associated with this person sorted by quality score
|
||||||
Highest quality face is result[0] and lowest quality face is result[n]
|
Highest quality face is result[0] and lowest quality face is result[n]
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
faces = self._db._db_faceinfo_person[self._pk]
|
faces = self._db._db_faceinfo_person[self._pk]
|
||||||
@@ -103,11 +102,10 @@ class PersonInfo:
|
|||||||
|
|
||||||
|
|
||||||
class FaceInfo:
|
class FaceInfo:
|
||||||
""" Info about a face in the Photos library
|
"""Info about a face in the Photos library"""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, db=None, pk=None):
|
def __init__(self, db=None, pk=None):
|
||||||
""" Creates a new FaceInfo instance
|
"""Creates a new FaceInfo instance
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
db: instance of PhotosDB object
|
db: instance of PhotosDB object
|
||||||
@@ -156,7 +154,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def center(self):
|
def center(self):
|
||||||
""" Coordinates, in PIL format, for center of face
|
"""Coordinates, in PIL format, for center of face
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple of coordinates in form (x, y)
|
tuple of coordinates in form (x, y)
|
||||||
@@ -165,7 +163,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def size_pixels(self):
|
def size_pixels(self):
|
||||||
""" Size of face in pixels (centered around center_x, center_y)
|
"""Size of face in pixels (centered around center_x, center_y)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
size, in int pixels, of a circle drawn around the center of the face
|
size, in int pixels, of a circle drawn around the center of the face
|
||||||
@@ -176,7 +174,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mouth(self):
|
def mouth(self):
|
||||||
""" Coordinates, in PIL format, for mouth position
|
"""Coordinates, in PIL format, for mouth position
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple of coordinates in form (x, y)
|
tuple of coordinates in form (x, y)
|
||||||
@@ -185,7 +183,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def left_eye(self):
|
def left_eye(self):
|
||||||
""" Coordinates, in PIL format, for left eye position
|
"""Coordinates, in PIL format, for left eye position
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple of coordinates in form (x, y)
|
tuple of coordinates in form (x, y)
|
||||||
@@ -194,7 +192,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def right_eye(self):
|
def right_eye(self):
|
||||||
""" Coordinates, in PIL format, for right eye position
|
"""Coordinates, in PIL format, for right eye position
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple of coordinates in form (x, y)
|
tuple of coordinates in form (x, y)
|
||||||
@@ -223,7 +221,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mwg_rs_area(self):
|
def mwg_rs_area(self):
|
||||||
""" Get coordinates for Metadata Working Group Region Area.
|
"""Get coordinates for Metadata Working Group Region Area.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MWG_RS_Area named tuple with x, y, h, w where:
|
MWG_RS_Area named tuple with x, y, h, w where:
|
||||||
@@ -249,7 +247,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mpri_reg_rect(self):
|
def mpri_reg_rect(self):
|
||||||
""" Get coordinates for Microsoft Photo Region Rectangle.
|
"""Get coordinates for Microsoft Photo Region Rectangle.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MPRI_Reg_Rect named tuple with x, y, h, w where:
|
MPRI_Reg_Rect named tuple with x, y, h, w where:
|
||||||
@@ -278,7 +276,7 @@ class FaceInfo:
|
|||||||
return MPRI_Reg_Rect(x, y, h, w)
|
return MPRI_Reg_Rect(x, y, h, w)
|
||||||
|
|
||||||
def face_rect(self):
|
def face_rect(self):
|
||||||
""" Get face rectangle coordinates for current version of the associated image
|
"""Get face rectangle coordinates for current version of the associated image
|
||||||
If image has been edited, rectangle applies to edited version, otherwise original version
|
If image has been edited, rectangle applies to edited version, otherwise original version
|
||||||
Coordinates in format and reference frame used by PIL
|
Coordinates in format and reference frame used by PIL
|
||||||
|
|
||||||
@@ -321,7 +319,7 @@ class FaceInfo:
|
|||||||
return yaw
|
return yaw
|
||||||
|
|
||||||
def _fix_orientation(self, xy):
|
def _fix_orientation(self, xy):
|
||||||
""" Translate an (x, y) tuple based on image orientation
|
"""Translate an (x, y) tuple based on image orientation
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
xy: tuple of (x, y) coordinates for point to translate
|
xy: tuple of (x, y) coordinates for point to translate
|
||||||
@@ -350,15 +348,18 @@ class FaceInfo:
|
|||||||
elif orientation == 7:
|
elif orientation == 7:
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
y = 1.0 - y
|
y = 1.0 - y
|
||||||
elif orientation ==8:
|
elif orientation == 8:
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
|
elif orientation == 0:
|
||||||
|
# set by osxphotos if adjusted orientation cannot be read, assume it's 1
|
||||||
|
y = 1.0 - y
|
||||||
else:
|
else:
|
||||||
logging.warning(f"Unhandled orientation: {orientation}")
|
logging.warning(f"Unhandled orientation: {orientation}")
|
||||||
|
|
||||||
return (x, y)
|
return (x, y)
|
||||||
|
|
||||||
def _make_point(self, xy):
|
def _make_point(self, xy):
|
||||||
""" Translate an (x, y) tuple based on image orientation
|
"""Translate an (x, y) tuple based on image orientation
|
||||||
and convert to image coordinates
|
and convert to image coordinates
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -379,7 +380,7 @@ class FaceInfo:
|
|||||||
return (int(x * dx), int(y * dy))
|
return (int(x * dx), int(y * dy))
|
||||||
|
|
||||||
def _make_point_with_rotation(self, xy):
|
def _make_point_with_rotation(self, xy):
|
||||||
""" Translate an (x, y) tuple based on image orientation and rotation
|
"""Translate an (x, y) tuple based on image orientation and rotation
|
||||||
and convert to image coordinates
|
and convert to image coordinates
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -472,7 +473,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
|
|
||||||
def rotate_image_point(x, y, xmid, ymid, angle):
|
def rotate_image_point(x, y, xmid, ymid, angle):
|
||||||
""" rotate image point about xm, ym by angle in radians
|
"""rotate image point about xm, ym by angle in radians
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
x: x coordinate of point to rotate
|
x: x coordinate of point to rotate
|
||||||
|
|||||||
@@ -475,6 +475,9 @@ def export2(
|
|||||||
merge_exif_keywords=False,
|
merge_exif_keywords=False,
|
||||||
merge_exif_persons=False,
|
merge_exif_persons=False,
|
||||||
jpeg_ext=None,
|
jpeg_ext=None,
|
||||||
|
persons=True,
|
||||||
|
location=True,
|
||||||
|
replace_keywords=False,
|
||||||
):
|
):
|
||||||
"""export photo, like export but with update and dry_run options
|
"""export photo, like export but with update and dry_run options
|
||||||
dest: must be valid destination path or exception raised
|
dest: must be valid destination path or exception raised
|
||||||
@@ -527,6 +530,9 @@ def export2(
|
|||||||
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading "."
|
jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading "."
|
||||||
|
persons: if True, include persons in exported metadata
|
||||||
|
location: if True, include location in exported metadata
|
||||||
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
|
||||||
Returns: ExportResults class
|
Returns: ExportResults class
|
||||||
ExportResults has attributes:
|
ExportResults has attributes:
|
||||||
@@ -562,11 +568,11 @@ def export2(
|
|||||||
if export_db is None:
|
if export_db is None:
|
||||||
export_db = ExportDBNoOp()
|
export_db = ExportDBNoOp()
|
||||||
|
|
||||||
if verbose is None:
|
if verbose and not callable(verbose):
|
||||||
verbose = noop
|
|
||||||
elif not callable(verbose):
|
|
||||||
raise TypeError("verbose must be callable")
|
raise TypeError("verbose must be callable")
|
||||||
self._verbose = verbose
|
|
||||||
|
if verbose is None:
|
||||||
|
verbose = self._verbose
|
||||||
|
|
||||||
# suffix to add to edited files
|
# suffix to add to edited files
|
||||||
# e.g. name will be filename_edited.jpg
|
# e.g. name will be filename_edited.jpg
|
||||||
@@ -941,6 +947,9 @@ def export2(
|
|||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
filename=dest.name,
|
filename=dest.name,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
sidecars.append(
|
sidecars.append(
|
||||||
(
|
(
|
||||||
@@ -964,6 +973,9 @@ def export2(
|
|||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
filename=dest.name,
|
filename=dest.name,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
sidecars.append(
|
sidecars.append(
|
||||||
(
|
(
|
||||||
@@ -983,6 +995,9 @@ def export2(
|
|||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
extension=dest.suffix[1:] if dest.suffix else None,
|
extension=dest.suffix[1:] if dest.suffix else None,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
sidecars.append(
|
sidecars.append(
|
||||||
(
|
(
|
||||||
@@ -1050,6 +1065,9 @@ def export2(
|
|||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
)[0]
|
)[0]
|
||||||
if old_data != current_data:
|
if old_data != current_data:
|
||||||
@@ -1070,6 +1088,9 @@ def export2(
|
|||||||
flags=exiftool_flags,
|
flags=exiftool_flags,
|
||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
if warning_:
|
if warning_:
|
||||||
all_results.exiftool_warning.append((exported_file, warning_))
|
all_results.exiftool_warning.append((exported_file, warning_))
|
||||||
@@ -1087,6 +1108,9 @@ def export2(
|
|||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
export_db.set_stat_exif_for_file(
|
export_db.set_stat_exif_for_file(
|
||||||
@@ -1109,6 +1133,9 @@ def export2(
|
|||||||
flags=exiftool_flags,
|
flags=exiftool_flags,
|
||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
if warning_:
|
if warning_:
|
||||||
all_results.exiftool_warning.append((exported_file, warning_))
|
all_results.exiftool_warning.append((exported_file, warning_))
|
||||||
@@ -1126,6 +1153,9 @@ def export2(
|
|||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
export_db.set_stat_exif_for_file(
|
export_db.set_stat_exif_for_file(
|
||||||
@@ -1345,6 +1375,9 @@ def _write_exif_data(
|
|||||||
flags=None,
|
flags=None,
|
||||||
merge_exif_keywords=False,
|
merge_exif_keywords=False,
|
||||||
merge_exif_persons=False,
|
merge_exif_persons=False,
|
||||||
|
persons=True,
|
||||||
|
location=True,
|
||||||
|
replace_keywords=False,
|
||||||
):
|
):
|
||||||
"""write exif data to image file at filepath
|
"""write exif data to image file at filepath
|
||||||
|
|
||||||
@@ -1355,6 +1388,9 @@ def _write_exif_data(
|
|||||||
keyword_template: (list of strings); list of template strings to render as keywords
|
keyword_template: (list of strings); list of template strings to render as keywords
|
||||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||||
flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)
|
flags: optional list of exiftool flags to prepend to exiftool command when writing metadata (e.g. -m or -F)
|
||||||
|
persons: if True, write person data to metadata
|
||||||
|
location: if True, write location data to metadata
|
||||||
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(warning, error) of warning and error strings if exiftool produces warnings or errors
|
(warning, error) of warning and error strings if exiftool produces warnings or errors
|
||||||
@@ -1369,6 +1405,9 @@ def _write_exif_data(
|
|||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
|
|
||||||
with ExifTool(filepath, flags=flags, exiftool=self._db._exiftool_path) as exiftool:
|
with ExifTool(filepath, flags=flags, exiftool=self._db._exiftool_path) as exiftool:
|
||||||
@@ -1391,6 +1430,9 @@ def _exiftool_dict(
|
|||||||
merge_exif_keywords=False,
|
merge_exif_keywords=False,
|
||||||
merge_exif_persons=False,
|
merge_exif_persons=False,
|
||||||
filename=None,
|
filename=None,
|
||||||
|
persons=True,
|
||||||
|
location=True,
|
||||||
|
replace_keywords=False,
|
||||||
):
|
):
|
||||||
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
||||||
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.
|
||||||
@@ -1404,6 +1446,9 @@ def _exiftool_dict(
|
|||||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||||
merge_exif_keywords: merge keywords in the file's exif metadata (requires exiftool)
|
merge_exif_keywords: merge keywords in the file's exif metadata (requires exiftool)
|
||||||
merge_exif_persons: merge persons in the file's exif metadata (requires exiftool)
|
merge_exif_persons: merge persons in the file's exif metadata (requires exiftool)
|
||||||
|
persons: if True, include person data
|
||||||
|
location: if True, include location data
|
||||||
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
|
||||||
Returns: dict with exiftool tags / values
|
Returns: dict with exiftool tags / values
|
||||||
|
|
||||||
@@ -1411,8 +1456,10 @@ def _exiftool_dict(
|
|||||||
EXIF:ImageDescription (may include template)
|
EXIF:ImageDescription (may include template)
|
||||||
XMP:Description (may include template)
|
XMP:Description (may include template)
|
||||||
XMP:Title
|
XMP:Title
|
||||||
|
IPTC:ObjectName
|
||||||
XMP:TagsList (may include album name, person name, or template)
|
XMP:TagsList (may include album name, person name, or template)
|
||||||
IPTC:Keywords (may include album name, person name, or template)
|
IPTC:Keywords (may include album name, person name, or template)
|
||||||
|
IPTC:Caption-Abstract
|
||||||
XMP:Subject (set to keywords + persons)
|
XMP:Subject (set to keywords + persons)
|
||||||
XMP:PersonInImage
|
XMP:PersonInImage
|
||||||
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
||||||
@@ -1428,6 +1475,9 @@ def _exiftool_dict(
|
|||||||
QuickTime:ModifyDate (UTC)
|
QuickTime:ModifyDate (UTC)
|
||||||
QuickTime:GPSCoordinates
|
QuickTime:GPSCoordinates
|
||||||
UserData:GPSCoordinates
|
UserData:GPSCoordinates
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
https://iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201610_1.pdf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
exif = (
|
exif = (
|
||||||
@@ -1447,30 +1497,34 @@ def _exiftool_dict(
|
|||||||
description = " ".join(rendered) if rendered else ""
|
description = " ".join(rendered) if rendered else ""
|
||||||
exif["EXIF:ImageDescription"] = description
|
exif["EXIF:ImageDescription"] = description
|
||||||
exif["XMP:Description"] = description
|
exif["XMP:Description"] = description
|
||||||
|
exif["IPTC:Caption-Abstract"] = description
|
||||||
elif self.description:
|
elif self.description:
|
||||||
exif["EXIF:ImageDescription"] = self.description
|
exif["EXIF:ImageDescription"] = self.description
|
||||||
exif["XMP:Description"] = self.description
|
exif["XMP:Description"] = self.description
|
||||||
|
exif["IPTC:Caption-Abstract"] = self.description
|
||||||
|
|
||||||
if self.title:
|
if self.title:
|
||||||
exif["XMP:Title"] = self.title
|
exif["XMP:Title"] = self.title
|
||||||
|
exif["IPTC:ObjectName"] = self.title
|
||||||
|
|
||||||
keyword_list = []
|
keyword_list = []
|
||||||
if merge_exif_keywords:
|
if merge_exif_keywords:
|
||||||
keyword_list.extend(self._get_exif_keywords())
|
keyword_list.extend(self._get_exif_keywords())
|
||||||
|
|
||||||
if self.keywords:
|
if self.keywords and not replace_keywords:
|
||||||
keyword_list.extend(self.keywords)
|
keyword_list.extend(self.keywords)
|
||||||
|
|
||||||
person_list = []
|
person_list = []
|
||||||
if merge_exif_persons:
|
if persons:
|
||||||
person_list.extend(self._get_exif_persons())
|
if merge_exif_persons:
|
||||||
|
person_list.extend(self._get_exif_persons())
|
||||||
|
|
||||||
if self.persons:
|
if self.persons:
|
||||||
# filter out _UNKNOWN_PERSON
|
# filter out _UNKNOWN_PERSON
|
||||||
person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON])
|
person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON])
|
||||||
|
|
||||||
if use_persons_as_keywords and person_list:
|
if use_persons_as_keywords and person_list:
|
||||||
keyword_list.extend(person_list)
|
keyword_list.extend(person_list)
|
||||||
|
|
||||||
if use_albums_as_keywords and self.albums:
|
if use_albums_as_keywords and self.albums:
|
||||||
keyword_list.extend(self.albums)
|
keyword_list.extend(self.albums)
|
||||||
@@ -1501,8 +1555,8 @@ def _exiftool_dict(
|
|||||||
if len(long_str) > _MAX_IPTC_KEYWORD_LEN
|
if len(long_str) > _MAX_IPTC_KEYWORD_LEN
|
||||||
]
|
]
|
||||||
if long_keywords:
|
if long_keywords:
|
||||||
logging.warning(
|
self._verbose(
|
||||||
f"Some keywords exceed max IPTC Keyword length of {_MAX_IPTC_KEYWORD_LEN}: {long_keywords}"
|
f"Warning: some keywords exceed max IPTC Keyword length of {_MAX_IPTC_KEYWORD_LEN} (exiftool will truncate these): {long_keywords}"
|
||||||
)
|
)
|
||||||
|
|
||||||
keyword_list.extend(rendered_keywords)
|
keyword_list.extend(rendered_keywords)
|
||||||
@@ -1514,25 +1568,26 @@ def _exiftool_dict(
|
|||||||
exif["XMP:Subject"] = keyword_list.copy()
|
exif["XMP:Subject"] = keyword_list.copy()
|
||||||
exif["XMP:TagsList"] = keyword_list.copy()
|
exif["XMP:TagsList"] = keyword_list.copy()
|
||||||
|
|
||||||
if person_list:
|
if persons and person_list:
|
||||||
person_list = sorted(list(set(person_list)))
|
person_list = sorted(list(set(person_list)))
|
||||||
exif["XMP:PersonInImage"] = person_list.copy()
|
exif["XMP:PersonInImage"] = person_list.copy()
|
||||||
|
|
||||||
# if self.favorite():
|
# if self.favorite():
|
||||||
# exif["Rating"] = 5
|
# exif["Rating"] = 5
|
||||||
|
|
||||||
(lat, lon) = self.location
|
if location:
|
||||||
if lat is not None and lon is not None:
|
(lat, lon) = self.location
|
||||||
if self.isphoto:
|
if lat is not None and lon is not None:
|
||||||
exif["EXIF:GPSLatitude"] = lat
|
if self.isphoto:
|
||||||
exif["EXIF:GPSLongitude"] = lon
|
exif["EXIF:GPSLatitude"] = lat
|
||||||
lat_ref = "N" if lat >= 0 else "S"
|
exif["EXIF:GPSLongitude"] = lon
|
||||||
lon_ref = "E" if lon >= 0 else "W"
|
lat_ref = "N" if lat >= 0 else "S"
|
||||||
exif["EXIF:GPSLatitudeRef"] = lat_ref
|
lon_ref = "E" if lon >= 0 else "W"
|
||||||
exif["EXIF:GPSLongitudeRef"] = lon_ref
|
exif["EXIF:GPSLatitudeRef"] = lat_ref
|
||||||
elif self.ismovie:
|
exif["EXIF:GPSLongitudeRef"] = lon_ref
|
||||||
exif["Keys:GPSCoordinates"] = f"{lat} {lon}"
|
elif self.ismovie:
|
||||||
exif["UserData:GPSCoordinates"] = f"{lat} {lon}"
|
exif["Keys:GPSCoordinates"] = f"{lat} {lon}"
|
||||||
|
exif["UserData:GPSCoordinates"] = f"{lat} {lon}"
|
||||||
|
|
||||||
# process date/time and timezone offset
|
# process date/time and timezone offset
|
||||||
# Photos exports the following fields and sets modify date to creation date
|
# Photos exports the following fields and sets modify date to creation date
|
||||||
@@ -1591,6 +1646,13 @@ def _exiftool_dict(
|
|||||||
exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
|
exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
|
||||||
self.date_modified
|
self.date_modified
|
||||||
).strftime("%Y:%m:%d %H:%M:%S")
|
).strftime("%Y:%m:%d %H:%M:%S")
|
||||||
|
|
||||||
|
# remove any new lines in any fields
|
||||||
|
for field, val in exif.items():
|
||||||
|
if type(val) == str:
|
||||||
|
exif[field] = val.replace("\n", " ")
|
||||||
|
elif type(val) == list:
|
||||||
|
exif[field] = [v.replace("\n", " ") for v in val]
|
||||||
return exif
|
return exif
|
||||||
|
|
||||||
|
|
||||||
@@ -1640,6 +1702,9 @@ def _exiftool_json_sidecar(
|
|||||||
merge_exif_keywords=False,
|
merge_exif_keywords=False,
|
||||||
merge_exif_persons=False,
|
merge_exif_persons=False,
|
||||||
filename=None,
|
filename=None,
|
||||||
|
persons=True,
|
||||||
|
location=True,
|
||||||
|
replace_keywords=False,
|
||||||
):
|
):
|
||||||
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
|
||||||
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.
|
||||||
@@ -1654,13 +1719,18 @@ def _exiftool_json_sidecar(
|
|||||||
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
filename: filename of the destination image file for including in exiftool signature in JSON sidecar
|
filename: filename of the destination image file for including in exiftool signature in JSON sidecar
|
||||||
|
persons: if True, include person data
|
||||||
|
location: if True, include location data
|
||||||
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
|
||||||
Returns: dict with exiftool tags / values
|
Returns: dict with exiftool tags / values
|
||||||
|
|
||||||
Exports the following:
|
Exports the following:
|
||||||
EXIF:ImageDescription
|
EXIF:ImageDescription
|
||||||
XMP:Description (may include template)
|
XMP:Description (may include template)
|
||||||
|
IPTC:CaptionAbstract
|
||||||
XMP:Title
|
XMP:Title
|
||||||
|
IPTC:ObjectName
|
||||||
XMP:TagsList
|
XMP:TagsList
|
||||||
IPTC:Keywords (may include album name, person name, or template)
|
IPTC:Keywords (may include album name, person name, or template)
|
||||||
XMP:Subject (set to keywords + person)
|
XMP:Subject (set to keywords + person)
|
||||||
@@ -1688,6 +1758,9 @@ def _exiftool_json_sidecar(
|
|||||||
merge_exif_keywords=merge_exif_keywords,
|
merge_exif_keywords=merge_exif_keywords,
|
||||||
merge_exif_persons=merge_exif_persons,
|
merge_exif_persons=merge_exif_persons,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
|
persons=persons,
|
||||||
|
location=location,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not tag_groups:
|
if not tag_groups:
|
||||||
@@ -1710,6 +1783,9 @@ def _xmp_sidecar(
|
|||||||
extension=None,
|
extension=None,
|
||||||
merge_exif_keywords=False,
|
merge_exif_keywords=False,
|
||||||
merge_exif_persons=False,
|
merge_exif_persons=False,
|
||||||
|
persons=True,
|
||||||
|
location=True,
|
||||||
|
replace_keywords=False,
|
||||||
):
|
):
|
||||||
"""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
|
||||||
@@ -1719,6 +1795,9 @@ def _xmp_sidecar(
|
|||||||
extension: which extension to use for SidecarForExtension property
|
extension: which extension to use for SidecarForExtension property
|
||||||
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
|
persons: if True, include person data
|
||||||
|
location: if True, include location data
|
||||||
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
"""
|
"""
|
||||||
|
|
||||||
xmp_template_file = (
|
xmp_template_file = (
|
||||||
@@ -1742,22 +1821,23 @@ def _xmp_sidecar(
|
|||||||
if merge_exif_keywords:
|
if merge_exif_keywords:
|
||||||
keyword_list.extend(self._get_exif_keywords())
|
keyword_list.extend(self._get_exif_keywords())
|
||||||
|
|
||||||
if self.keywords:
|
if self.keywords and not replace_keywords:
|
||||||
keyword_list.extend(self.keywords)
|
keyword_list.extend(self.keywords)
|
||||||
|
|
||||||
# TODO: keyword handling in this and _exiftool_json_sidecar is
|
# TODO: keyword handling in this and _exiftool_json_sidecar is
|
||||||
# good candidate for pulling out in a function
|
# good candidate for pulling out in a function
|
||||||
|
|
||||||
person_list = []
|
person_list = []
|
||||||
if merge_exif_persons:
|
if persons:
|
||||||
person_list.extend(self._get_exif_persons())
|
if merge_exif_persons:
|
||||||
|
person_list.extend(self._get_exif_persons())
|
||||||
|
|
||||||
if self.persons:
|
if self.persons:
|
||||||
# filter out _UNKNOWN_PERSON
|
# filter out _UNKNOWN_PERSON
|
||||||
person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON])
|
person_list.extend([p for p in self.persons if p != _UNKNOWN_PERSON])
|
||||||
|
|
||||||
if use_persons_as_keywords and person_list:
|
if use_persons_as_keywords and person_list:
|
||||||
keyword_list.extend(person_list)
|
keyword_list.extend(person_list)
|
||||||
|
|
||||||
if use_albums_as_keywords and self.albums:
|
if use_albums_as_keywords and self.albums:
|
||||||
keyword_list.extend(self.albums)
|
keyword_list.extend(self.albums)
|
||||||
@@ -1781,28 +1861,20 @@ def _xmp_sidecar(
|
|||||||
if _OSXPHOTOS_NONE_SENTINEL not in keyword
|
if _OSXPHOTOS_NONE_SENTINEL not in keyword
|
||||||
]
|
]
|
||||||
|
|
||||||
# check to see if any keywords too long
|
|
||||||
long_keywords = [
|
|
||||||
long_str
|
|
||||||
for long_str in rendered_keywords
|
|
||||||
if len(long_str) > _MAX_IPTC_KEYWORD_LEN
|
|
||||||
]
|
|
||||||
if long_keywords:
|
|
||||||
logging.warning(
|
|
||||||
f"Some keywords exceed max IPTC Keyword length of {_MAX_IPTC_KEYWORD_LEN}: {long_keywords}"
|
|
||||||
)
|
|
||||||
|
|
||||||
keyword_list.extend(rendered_keywords)
|
keyword_list.extend(rendered_keywords)
|
||||||
|
|
||||||
# remove duplicates
|
# remove duplicates
|
||||||
# sorted mainly to make testing the XMP file easier
|
# sorted mainly to make testing the XMP file easier
|
||||||
if keyword_list:
|
if keyword_list:
|
||||||
keyword_list = sorted(list(set(keyword_list)))
|
keyword_list = sorted(list(set(keyword_list)))
|
||||||
if person_list:
|
if persons and person_list:
|
||||||
person_list = sorted(list(set(person_list)))
|
person_list = sorted(list(set(person_list)))
|
||||||
|
|
||||||
subject_list = keyword_list
|
subject_list = keyword_list
|
||||||
|
|
||||||
|
if location:
|
||||||
|
latlon = self.location
|
||||||
|
|
||||||
xmp_str = xmp_template.render(
|
xmp_str = xmp_template.render(
|
||||||
photo=self,
|
photo=self,
|
||||||
description=description,
|
description=description,
|
||||||
@@ -1810,6 +1882,7 @@ def _xmp_sidecar(
|
|||||||
persons=person_list,
|
persons=person_list,
|
||||||
subjects=subject_list,
|
subjects=subject_list,
|
||||||
extension=extension,
|
extension=extension,
|
||||||
|
location=latlon,
|
||||||
version=__version__,
|
version=__version__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from .._constants import (
|
|||||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||||
_PHOTOS_5_VERSION,
|
_PHOTOS_5_VERSION,
|
||||||
)
|
)
|
||||||
|
from ..adjustmentsinfo import AdjustmentsInfo
|
||||||
from ..albuminfo import AlbumInfo, ImportInfo
|
from ..albuminfo import AlbumInfo, ImportInfo
|
||||||
from ..personinfo import FaceInfo, PersonInfo
|
from ..personinfo import FaceInfo, PersonInfo
|
||||||
from ..phototemplate import PhotoTemplate
|
from ..phototemplate import PhotoTemplate
|
||||||
@@ -70,6 +71,7 @@ class PhotoInfo:
|
|||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
self._info = info
|
self._info = info
|
||||||
self._db = db
|
self._db = db
|
||||||
|
self._verbose = self._db._verbose
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
@@ -509,6 +511,30 @@ class PhotoInfo:
|
|||||||
""" True if picture has adjustments / edits """
|
""" True if picture has adjustments / edits """
|
||||||
return self._info["hasAdjustments"] == 1
|
return self._info["hasAdjustments"] == 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adjustments(self):
|
||||||
|
""" Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only """
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.hasadjustments:
|
||||||
|
try:
|
||||||
|
return self._adjustmentinfo
|
||||||
|
except AttributeError:
|
||||||
|
library = self._db._library_path
|
||||||
|
directory = self._uuid[0] # first char of uuid
|
||||||
|
plist_file = (
|
||||||
|
pathlib.Path(library)
|
||||||
|
/ "resources"
|
||||||
|
/ "renders"
|
||||||
|
/ directory
|
||||||
|
/ f"{self._uuid}.plist"
|
||||||
|
)
|
||||||
|
if not plist_file.is_file():
|
||||||
|
return None
|
||||||
|
self._adjustmentinfo = AdjustmentsInfo(plist_file)
|
||||||
|
return self._adjustmentinfo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def external_edit(self):
|
def external_edit(self):
|
||||||
""" Returns True if picture was edited outside of Photos using external editor """
|
""" Returns True if picture was edited outside of Photos using external editor """
|
||||||
@@ -822,8 +848,19 @@ class PhotoInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def orientation(self):
|
def orientation(self):
|
||||||
""" returns EXIF orientation of the current photo version as int """
|
""" returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined """
|
||||||
return self._info["orientation"]
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return self._info["orientation"]
|
||||||
|
|
||||||
|
# For Photos 5+, try to get the adjusted orientation
|
||||||
|
if self.hasadjustments:
|
||||||
|
if self.adjustments:
|
||||||
|
return self.adjustments.adj_orientation
|
||||||
|
else:
|
||||||
|
# can't reliably determine orientation for edited photo if adjustmentinfo not available
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return self._info["orientation"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def original_height(self):
|
def original_height(self):
|
||||||
|
|||||||
@@ -2041,6 +2041,7 @@ class PhotosDB:
|
|||||||
# > 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)
|
# > 6 = portrait (sometimes, see ZDEPTHSTATE/ZDEPTHTYPE)
|
||||||
info["customRenderedValue"] = row[22]
|
info["customRenderedValue"] = row[22]
|
||||||
info["hdr"] = True if row[22] == 3 else False
|
info["hdr"] = True if row[22] == 3 else False
|
||||||
|
info["depth_state"] = row[36]
|
||||||
info["portrait"] = True if row[36] != 0 else False
|
info["portrait"] = True if row[36] != 0 else False
|
||||||
|
|
||||||
# Set panorama from either KindSubType or RenderedValue
|
# Set panorama from either KindSubType or RenderedValue
|
||||||
|
|||||||
94
osxphotos/phototemplate.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
The templating system converts one or template statements, written in osxphotos templating language, to one or more rendered values using information from the photo being processed.
|
||||||
|
|
||||||
|
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.
|
||||||
|
|
||||||
|
Template statements may contain one or more modifiers. The full syntax is:
|
||||||
|
|
||||||
|
`"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}posttext"`
|
||||||
|
|
||||||
|
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||||
|
|
||||||
|
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||||
|
|
||||||
|
|
||||||
|
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||||
|
|
||||||
|
`+`: If present before template `name`, expands the template in place. If `delim` not provided, values are joined with no delimiter.
|
||||||
|
|
||||||
|
e.g. if Photo keywords are `["foo","bar"]`:
|
||||||
|
|
||||||
|
- `"{keyword}"` renders to `"foo", "bar"`
|
||||||
|
- `"{,+keyword}"` renders to: `"foo,bar"`
|
||||||
|
- `"{; +keyword}"` renders to: `"foo; bar"`
|
||||||
|
- `"{+keyword}"` renders to `"foobar"`
|
||||||
|
|
||||||
|
`template_field`: The template field to resolve. See [Template Substitutions](#template-substitutions) for full list of template fields.
|
||||||
|
|
||||||
|
`:subfield`: Some templates have sub-fields, For example, `{exiftool:IPTC:Make}`; the template_field is `exiftool` and the sub-field is `IPTC:Make`.
|
||||||
|
|
||||||
|
`|filter`: You may optionally append one or more filter commands to the end of the template field using the vertical pipe ('|') symbol. Filters may be combined, separated by '|' as in: `{keyword|capitalize|parens}`.
|
||||||
|
|
||||||
|
Valid filters are:
|
||||||
|
|
||||||
|
<!-- OSXPHOTOS-FILTER-TABLE:START - Do not remove or modify this section -->
|
||||||
|
- lower: Convert value to lower case, e.g. 'Value' => 'value'.
|
||||||
|
- upper: Convert value to upper case, e.g. 'Value' => 'VALUE'.
|
||||||
|
- strip: Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.
|
||||||
|
- titlecase: Convert value to title case, e.g. 'my value' => 'My Value'.
|
||||||
|
- capitalize: Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.
|
||||||
|
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||||
|
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||||
|
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||||
|
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
||||||
|
|
||||||
|
e.g. if Photo keywords are `["FOO","bar"]`:
|
||||||
|
|
||||||
|
- `"{keyword|lower}"` renders to `"foo", "bar"`
|
||||||
|
- `"{keyword|upper}"` renders to: `"FOO", "BAR"`
|
||||||
|
- `"{keyword|capitalize}"` renders to: `"Foo", "Bar"`
|
||||||
|
- `"{keyword|lower|parens}"` renders to: `"(foo)", "(bar)"`
|
||||||
|
|
||||||
|
e.g. if Photo description is "my description":
|
||||||
|
|
||||||
|
- `"{descr|titlecase}"` renders to: `"My Description"`
|
||||||
|
|
||||||
|
`(path_sep)`: optional path separator to use when joining path-like fields, for example `{folder_album}`. Default is "/".
|
||||||
|
|
||||||
|
e.g. If Photo is in `Album1` in `Folder1`:
|
||||||
|
|
||||||
|
- `"{folder_album}"` renders to `["Folder1/Album1"]`
|
||||||
|
- `"{folder_album(>)}"` renders to `["Folder1>Album1"]`
|
||||||
|
- `"{folder_album()}"` renders to `["Folder1Album1"]`
|
||||||
|
|
||||||
|
`[find|replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||||
|
|
||||||
|
`?bool_value`: Template fields may be evaluated as boolean by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||||
|
|
||||||
|
e.g. if photo is an HDR image,
|
||||||
|
|
||||||
|
- `"{hdr?ISHDR,NOTHDR}"` renders to `"ISHDR"`
|
||||||
|
|
||||||
|
and if it is not an HDR image,
|
||||||
|
|
||||||
|
- `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
|
||||||
|
|
||||||
|
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
|
||||||
|
|
||||||
|
e.g., if photo has no title set,
|
||||||
|
|
||||||
|
- `"{title}"` renders to "_"
|
||||||
|
- `"{title,I have no title}"` renders to `"I have no title"`
|
||||||
|
|
||||||
|
Template fields such as `created.strftime` use the default value to pass the template to use for `strftime`.
|
||||||
|
|
||||||
|
e.g., if photo date is 4 February 2020, 19:07:38,
|
||||||
|
|
||||||
|
- `"{created.strftime,%Y-%m-%d-%H%M%S}"` renders to `"2020-02-04-190738"`
|
||||||
|
|
||||||
|
Some template fields such as `"{media_type}"` use the default value to allow customization of the output. For example, `"{media_type}"` resolves to the special media type of the photo such as `panorama` or `selfie`. You may use the default value to override these in form: `"{media_type,video=vidéo;time_lapse=vidéo_accélérée}"`. In this example, if photo was a time_lapse photo, `media_type` would resolve to `vidéo_accélérée` instead of `time_lapse`.
|
||||||
|
|
||||||
|
Either or both bool_value or default (False value) may be empty which would result in empty string `""` when rendered.
|
||||||
|
|
||||||
|
If you want to include "{" or "}" in the output, use "{openbrace}" or "{closebrace}" template substitution.
|
||||||
|
|
||||||
|
e.g. `"{created.year}/{openbrace}{title}{closebrace}"` would result in `"2020/{Photo Title}"`.
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
""" Custom template system for osxphotos (implemented in PhotoInfo.render_template) """
|
""" Custom template system for osxphotos, implements osxphotos template language (OTL) """
|
||||||
|
|
||||||
|
|
||||||
# Rolled my own template system because:
|
|
||||||
# 1. Needed to handle multiple values (e.g. album, keyword)
|
|
||||||
# 2. Needed to handle default values if template not found
|
|
||||||
# 3. Didn't want user to need to know python (e.g. by using Mako which is
|
|
||||||
# already used elsewhere in this project)
|
|
||||||
#
|
|
||||||
# This code isn't elegant and is prime for refactoring but it seems to work well. PRs gladly accepted.
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
|
||||||
from functools import partial
|
from textx import TextXSyntaxError, metamodel_from_file
|
||||||
|
|
||||||
from ._constants import _UNKNOWN_PERSON
|
from ._constants import _UNKNOWN_PERSON
|
||||||
from .datetime_formatter import DateTimeFormatter
|
from .datetime_formatter import DateTimeFormatter
|
||||||
@@ -24,6 +15,10 @@ from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
|||||||
# ensure locale set to user's locale
|
# ensure locale set to user's locale
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
locale.setlocale(locale.LC_ALL, "")
|
||||||
|
|
||||||
|
OTL_GRAMMAR_MODEL = str(pathlib.Path(__file__).parent / "phototemplate.tx")
|
||||||
|
|
||||||
|
"""TextX metamodel for osxphotos template language """
|
||||||
|
|
||||||
PHOTO_VIDEO_TYPE_DEFAULTS = {"photo": "photo", "video": "video"}
|
PHOTO_VIDEO_TYPE_DEFAULTS = {"photo": "photo", "video": "video"}
|
||||||
|
|
||||||
MEDIA_TYPE_DEFAULTS = {
|
MEDIA_TYPE_DEFAULTS = {
|
||||||
@@ -122,6 +117,15 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
"{exif.camera_model}": "Camera model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s'",
|
"{exif.camera_model}": "Camera model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s'",
|
||||||
"{exif.lens_model}": "Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
|
"{exif.lens_model}": "Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
|
||||||
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
|
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
|
||||||
|
"{comma}": "A comma: ','",
|
||||||
|
"{semicolon}": "A semicolon: ';'",
|
||||||
|
"{pipe}": "A vertical pipe: '|'",
|
||||||
|
"{openbrace}": "An open brace: '{'",
|
||||||
|
"{closebrace}": "A close brace: '}'",
|
||||||
|
"{openparens}": "An open parentheses: '('",
|
||||||
|
"{closeparens}": "A close parentheses: ')'",
|
||||||
|
"{openbracket}": "An open bracket: '['",
|
||||||
|
"{closebracket}": "A close bracket: ']'",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||||
@@ -133,7 +137,7 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
|
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
|
||||||
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
|
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
|
||||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
|
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
|
||||||
"{exiftool:GROUP:TAGNAME}": "Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
"{exiftool}": "Format: '{exiftool:GROUP:TAGNAME}'; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
||||||
"E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. "
|
"E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. "
|
||||||
"See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) "
|
"See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) "
|
||||||
"as used in `exiftool -G`. exiftool must be installed in the path to use this template.",
|
"as used in `exiftool -G`. exiftool must be installed in the path to use this template.",
|
||||||
@@ -143,35 +147,71 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FILTER_VALUES = {
|
||||||
|
"lower": "Convert value to lower case, e.g. 'Value' => 'value'.",
|
||||||
|
"upper": "Convert value to upper case, e.g. 'Value' => 'VALUE'.",
|
||||||
|
"strip": "Strip whitespace from beginning/end of value, e.g. ' Value ' => 'Value'.",
|
||||||
|
"titlecase": "Convert value to title case, e.g. 'my value' => 'My Value'.",
|
||||||
|
"capitalize": "Capitalize first word of value and convert other words to lower case, e.g. 'MY VALUE' => 'My value'.",
|
||||||
|
"braces": "Enclose value in curly braces, e.g. 'value => '{value}'.",
|
||||||
|
"parens": "Enclose value in parentheses, e.g. 'value' => '(value')",
|
||||||
|
"brackets": "Enclose value in brackets, e.g. 'value' => '[value]'",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just the substitutions without the braces
|
||||||
|
SINGLE_VALUE_SUBSTITUTIONS = [
|
||||||
|
field.replace("{", "").replace("}", "") for field in TEMPLATE_SUBSTITUTIONS
|
||||||
|
]
|
||||||
|
|
||||||
# Just the multi-valued substitution names without the braces
|
# Just the multi-valued substitution names without the braces
|
||||||
MULTI_VALUE_SUBSTITUTIONS = [
|
MULTI_VALUE_SUBSTITUTIONS = [
|
||||||
field.replace("{", "").replace("}", "")
|
field.replace("{", "").replace("}", "")
|
||||||
for field in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
for field in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||||
]
|
]
|
||||||
|
|
||||||
# regular expressions for matching template syntax
|
FIELD_NAMES = SINGLE_VALUE_SUBSTITUTIONS + MULTI_VALUE_SUBSTITUTIONS
|
||||||
RE_OPENING_BRACE = r"(?<!\{)\{" # match { but not {{
|
|
||||||
RE_DELIM = r"([^}]*\+)?" # group 1: optional DELIM+
|
|
||||||
RE_FIELD_NAME = r"([^\\,}+\?]+)" # group 2: field name
|
|
||||||
RE_PATH_SEP = r"(\([^{}\)]*\))?" # group 3: optional (PATH_SEP)
|
|
||||||
# + r"(\[[^{}\)]*\])?" # group 4: optional [REPLACE]
|
|
||||||
RE_REPLACE = r"(\[[^{}]*\])?" # group 4: optional [REPLACE]
|
|
||||||
RE_BOOL_VAL = r"(\?[^\\,}]*)?" # group 5: optional ?TRUE_VALUE for boolean fields
|
|
||||||
RE_DEFAULT_VAL = r"(,[\w\=\;\-\%. ]*)?" # group 6: optional ,DEFAULT
|
|
||||||
RE_CLOSING_BRACE = r"(?=\}(?!\}))\}" # match } but not }}
|
|
||||||
|
|
||||||
MATCH_GROUPS_TOTAL = 6
|
|
||||||
MATCH_GROUPS_DELIM = 1
|
|
||||||
MATCH_GROUPS_FIELD = 2
|
|
||||||
MATCH_GROUPS_PATH_SEP = 3
|
|
||||||
MATCH_GROUPS_REPLACE = 4
|
|
||||||
MATCH_GROUPS_BOOL_VAL = 5
|
|
||||||
MATCH_GROUPS_DEFAULT = 6
|
|
||||||
|
|
||||||
# default values for string manipulation template options
|
# default values for string manipulation template options
|
||||||
INPLACE_DEFAULT = ","
|
INPLACE_DEFAULT = ","
|
||||||
PATH_SEP_DEFAULT = os.path.sep
|
PATH_SEP_DEFAULT = os.path.sep
|
||||||
|
|
||||||
|
PUNCTUATION = {
|
||||||
|
"comma": ",",
|
||||||
|
"semicolon": ";",
|
||||||
|
"pipe": "|",
|
||||||
|
"openbrace": "{",
|
||||||
|
"closebrace": "}",
|
||||||
|
"openparens": "(",
|
||||||
|
"closeparens": ")",
|
||||||
|
"openbracket": "[",
|
||||||
|
"closebracket": "]",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoTemplateParser:
|
||||||
|
"""Parser for PhotoTemplate """
|
||||||
|
|
||||||
|
# implemented as Singleton
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
""" create new object or return instance of already created singleton """
|
||||||
|
if not hasattr(cls, "instance") or not cls.instance:
|
||||||
|
cls.instance = super().__new__(cls)
|
||||||
|
|
||||||
|
return cls.instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
""" return existing singleton or create a new one """
|
||||||
|
|
||||||
|
if hasattr(self, "metamodel"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.metamodel = metamodel_from_file(OTL_GRAMMAR_MODEL, skipws=False)
|
||||||
|
|
||||||
|
def parse(self, template_statement):
|
||||||
|
"""Parse a template_statement string """
|
||||||
|
return self.metamodel.model_from_str(template_statement)
|
||||||
|
|
||||||
|
|
||||||
class PhotoTemplate:
|
class PhotoTemplate:
|
||||||
""" PhotoTemplate class to render a template string from a PhotoInfo object """
|
""" PhotoTemplate class to render a template string from a PhotoInfo object """
|
||||||
@@ -190,61 +230,8 @@ class PhotoTemplate:
|
|||||||
# gets initialized in get_template_value
|
# gets initialized in get_template_value
|
||||||
self.today = None
|
self.today = None
|
||||||
|
|
||||||
def make_subst_function(self, none_str, filename, dirname, get_func=None):
|
# get parser singleton
|
||||||
""" returns: substitution function for use in re.sub
|
self.parser = PhotoTemplateParser()
|
||||||
none_str: value to use if substitution lookup is None and no default provided
|
|
||||||
get_func: function that gets the substitution value for a given template field
|
|
||||||
default is get_template_value which handles the single-value fields """
|
|
||||||
|
|
||||||
if get_func is None:
|
|
||||||
# used by make_subst_function to get the value for a template substitution
|
|
||||||
get_func = partial(
|
|
||||||
self.get_template_value, filename=filename, dirname=dirname
|
|
||||||
)
|
|
||||||
|
|
||||||
# closure to capture photo, none_str, filename, dirname in subst
|
|
||||||
def subst(matchobj):
|
|
||||||
groups = len(matchobj.groups())
|
|
||||||
if groups != MATCH_GROUPS_TOTAL:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unexpected number of groups: expected {MATCH_GROUPS_TOTAL}, got {groups}"
|
|
||||||
)
|
|
||||||
|
|
||||||
delim = matchobj.group(MATCH_GROUPS_DELIM)
|
|
||||||
field = matchobj.group(MATCH_GROUPS_FIELD)
|
|
||||||
path_sep = matchobj.group(MATCH_GROUPS_PATH_SEP)
|
|
||||||
replace = matchobj.group(MATCH_GROUPS_REPLACE)
|
|
||||||
bool_val = matchobj.group(MATCH_GROUPS_BOOL_VAL)
|
|
||||||
default = matchobj.group(MATCH_GROUPS_DEFAULT)
|
|
||||||
|
|
||||||
# drop the '+' on delim
|
|
||||||
delim = delim[:-1] if delim is not None else None
|
|
||||||
# drop () from path_sep
|
|
||||||
path_sep = path_sep.strip("()") if path_sep is not None else None
|
|
||||||
# drop [] from replace
|
|
||||||
replace = replace[1:-1] if replace is not None else None
|
|
||||||
# drop the ? on bool_val
|
|
||||||
bool_val = bool_val[1:] if bool_val is not None else None
|
|
||||||
# drop the comma on default
|
|
||||||
default_val = default[1:] if default is not None else None
|
|
||||||
|
|
||||||
try:
|
|
||||||
val = get_func(
|
|
||||||
field, default_val, bool_val, delim, path_sep, replacement=replace
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
return matchobj.group(0)
|
|
||||||
|
|
||||||
if val is None:
|
|
||||||
# field valid but didn't match a value
|
|
||||||
if default == ",":
|
|
||||||
val = ""
|
|
||||||
else:
|
|
||||||
val = default_val if default_val is not None else none_str
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
return subst
|
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
@@ -281,89 +268,67 @@ class PhotoTemplate:
|
|||||||
if inplace_sep is None:
|
if inplace_sep is None:
|
||||||
inplace_sep = INPLACE_DEFAULT
|
inplace_sep = INPLACE_DEFAULT
|
||||||
|
|
||||||
# the rendering happens in two phases:
|
|
||||||
# phase 1: handle all the single-value template substitutions
|
|
||||||
# results in a single string with all the template fields replaced
|
|
||||||
# phase 2: loop through all the multi-value template substitutions
|
|
||||||
# could result in multiple strings
|
|
||||||
# e.g. if template is "{album}/{person}" and there are 2 albums and 3 persons in the photo
|
|
||||||
# there would be 6 possible renderings (2 albums x 3 persons)
|
|
||||||
|
|
||||||
# regex to find {template_field,optional_default} in strings
|
|
||||||
# pylint: disable=anomalous-backslash-in-string
|
|
||||||
regex = (
|
|
||||||
RE_OPENING_BRACE
|
|
||||||
+ RE_DELIM
|
|
||||||
+ RE_FIELD_NAME
|
|
||||||
+ RE_PATH_SEP
|
|
||||||
+ RE_REPLACE
|
|
||||||
+ RE_BOOL_VAL
|
|
||||||
+ RE_DEFAULT_VAL
|
|
||||||
+ RE_CLOSING_BRACE
|
|
||||||
)
|
|
||||||
|
|
||||||
if type(template) is not str:
|
if type(template) is not str:
|
||||||
raise TypeError(f"template must be type str, not {type(template)}")
|
raise TypeError(f"template must be type str, not {type(template)}")
|
||||||
|
|
||||||
subst_func = self.make_subst_function(none_str, filename, dirname)
|
try:
|
||||||
|
model = self.parser.parse(template)
|
||||||
|
except TextXSyntaxError as e:
|
||||||
|
raise ValueError(f"SyntaxError: {e}")
|
||||||
|
|
||||||
# do the replacements
|
if not model:
|
||||||
rendered = re.sub(regex, subst_func, template)
|
# empty string
|
||||||
|
return [], []
|
||||||
|
|
||||||
# do multi-valued placements
|
return self._render_statement(
|
||||||
# start with the single string from phase 1 above then loop through all
|
model,
|
||||||
# multi-valued fields and all values for each of those fields
|
none_str=none_str,
|
||||||
# rendered_strings will be updated as each field is processed
|
path_sep=path_sep,
|
||||||
# for example: if two albums, two keywords, and one person and template is:
|
expand_inplace=expand_inplace,
|
||||||
# "{created.year}/{album}/{keyword}/{person}"
|
inplace_sep=inplace_sep,
|
||||||
# rendered strings would do the following:
|
filename=filename,
|
||||||
# start (created.year filled in phase 1)
|
dirname=dirname,
|
||||||
# ['2011/{album}/{keyword}/{person}']
|
strip=strip,
|
||||||
# after processing albums:
|
|
||||||
# ['2011/Album1/{keyword}/{person}',
|
|
||||||
# '2011/Album2/{keyword}/{person}',]
|
|
||||||
# after processing keywords:
|
|
||||||
# ['2011/Album1/keyword1/{person}',
|
|
||||||
# '2011/Album1/keyword2/{person}',
|
|
||||||
# '2011/Album2/keyword1/{person}',
|
|
||||||
# '2011/Album2/keyword2/{person}',]
|
|
||||||
# after processing person:
|
|
||||||
# ['2011/Album1/keyword1/person1',
|
|
||||||
# '2011/Album1/keyword2/person1',
|
|
||||||
# '2011/Album2/keyword1/person1',
|
|
||||||
# '2011/Album2/keyword2/person1',]
|
|
||||||
|
|
||||||
rendered_strings = self._render_multi_valued_templates(
|
|
||||||
rendered, none_str, path_sep, expand_inplace, inplace_sep, filename, dirname
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# process exiftool: templates
|
def _render_statement(
|
||||||
rendered_strings = self._render_exiftool_template(
|
self,
|
||||||
rendered_strings,
|
statement,
|
||||||
none_str,
|
none_str="_",
|
||||||
path_sep,
|
path_sep=None,
|
||||||
expand_inplace,
|
expand_inplace=False,
|
||||||
inplace_sep,
|
inplace_sep=None,
|
||||||
filename,
|
filename=False,
|
||||||
dirname,
|
dirname=False,
|
||||||
)
|
strip=False,
|
||||||
|
):
|
||||||
# find any {fields} that weren't replaced
|
results = []
|
||||||
unmatched = []
|
unmatched = []
|
||||||
for rendered_str in rendered_strings:
|
for ts in statement.template_strings:
|
||||||
unmatched.extend(
|
results, unmatched = self._render_template_string(
|
||||||
[
|
ts,
|
||||||
no_match[1]
|
none_str=none_str,
|
||||||
for no_match in re.findall(regex, rendered_str)
|
path_sep=path_sep,
|
||||||
if no_match[1] not in unmatched
|
expand_inplace=expand_inplace,
|
||||||
]
|
inplace_sep=inplace_sep,
|
||||||
|
filename=filename,
|
||||||
|
dirname=dirname,
|
||||||
|
results=results,
|
||||||
|
unmatched=unmatched,
|
||||||
)
|
)
|
||||||
|
|
||||||
# fix any escaped curly braces
|
# process find/replace
|
||||||
rendered_strings = [
|
if ts.template and ts.template.findreplace:
|
||||||
rendered_str.replace("{{", "{").replace("}}", "}")
|
new_results = []
|
||||||
for rendered_str in rendered_strings
|
for result in results:
|
||||||
]
|
for pair in ts.template.findreplace.pairs:
|
||||||
|
find = pair.find or ""
|
||||||
|
repl = pair.replace or ""
|
||||||
|
result = result.replace(find, repl)
|
||||||
|
new_results.append(result)
|
||||||
|
results = new_results
|
||||||
|
|
||||||
|
rendered_strings = results
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
rendered_strings = [
|
rendered_strings = [
|
||||||
@@ -377,268 +342,149 @@ class PhotoTemplate:
|
|||||||
|
|
||||||
return rendered_strings, unmatched
|
return rendered_strings, unmatched
|
||||||
|
|
||||||
def _render_multi_valued_templates(
|
def _render_template_string(
|
||||||
self,
|
self,
|
||||||
rendered,
|
ts,
|
||||||
none_str,
|
none_str="_",
|
||||||
path_sep,
|
path_sep=None,
|
||||||
expand_inplace,
|
expand_inplace=False,
|
||||||
inplace_sep,
|
inplace_sep=None,
|
||||||
filename,
|
filename=False,
|
||||||
dirname,
|
dirname=False,
|
||||||
|
results=None,
|
||||||
|
unmatched=None,
|
||||||
):
|
):
|
||||||
rendered_strings = [rendered]
|
"""Render a TemplateString object """
|
||||||
new_rendered_strings = []
|
|
||||||
while new_rendered_strings != rendered_strings:
|
results = results or [""]
|
||||||
new_rendered_strings = rendered_strings
|
unmatched = unmatched or []
|
||||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
|
||||||
# Build a regex that matches only the field being processed
|
if ts.template:
|
||||||
re_str = (
|
# have a template field to process
|
||||||
RE_OPENING_BRACE
|
field = ts.template.field
|
||||||
+ RE_DELIM
|
if field not in FIELD_NAMES:
|
||||||
+ r"("
|
unmatched.append(field)
|
||||||
+ field # group 2: field name
|
return [], unmatched
|
||||||
+ r")"
|
|
||||||
+ RE_PATH_SEP
|
subfield = ts.template.subfield
|
||||||
+ RE_REPLACE
|
|
||||||
+ RE_BOOL_VAL
|
# process filters
|
||||||
+ RE_DEFAULT_VAL
|
filters = []
|
||||||
+ RE_CLOSING_BRACE
|
if ts.template.filter is not None:
|
||||||
|
filters = ts.template.filter.value
|
||||||
|
|
||||||
|
# process path_sep
|
||||||
|
if ts.template.pathsep is not None:
|
||||||
|
path_sep = ts.template.pathsep.value
|
||||||
|
|
||||||
|
# process delim
|
||||||
|
if ts.template.delim is not None:
|
||||||
|
# if value is None, means format was {+field}
|
||||||
|
delim = ts.template.delim.value or ""
|
||||||
|
else:
|
||||||
|
delim = None
|
||||||
|
|
||||||
|
if ts.template.bool is not None:
|
||||||
|
is_bool = True
|
||||||
|
if ts.template.bool.value is not None:
|
||||||
|
bool_val, u = self._render_statement(
|
||||||
|
ts.template.bool.value,
|
||||||
|
none_str=none_str,
|
||||||
|
path_sep=path_sep,
|
||||||
|
expand_inplace=expand_inplace,
|
||||||
|
inplace_sep=inplace_sep,
|
||||||
|
filename=filename,
|
||||||
|
dirname=dirname,
|
||||||
|
)
|
||||||
|
unmatched.extend(u)
|
||||||
|
else:
|
||||||
|
# blank bool value
|
||||||
|
bool_val = [""]
|
||||||
|
else:
|
||||||
|
is_bool = False
|
||||||
|
bool_val = None
|
||||||
|
|
||||||
|
# process default
|
||||||
|
if ts.template.default is not None:
|
||||||
|
# default is also a TemplateString
|
||||||
|
if ts.template.default.value is not None:
|
||||||
|
default, u = self._render_statement(
|
||||||
|
ts.template.default.value,
|
||||||
|
none_str=none_str,
|
||||||
|
path_sep=path_sep,
|
||||||
|
expand_inplace=expand_inplace,
|
||||||
|
inplace_sep=inplace_sep,
|
||||||
|
filename=filename,
|
||||||
|
dirname=dirname,
|
||||||
|
)
|
||||||
|
unmatched.extend(u)
|
||||||
|
else:
|
||||||
|
# blank default value
|
||||||
|
default = [""]
|
||||||
|
else:
|
||||||
|
default = []
|
||||||
|
|
||||||
|
vals = []
|
||||||
|
if field in SINGLE_VALUE_SUBSTITUTIONS:
|
||||||
|
vals = self.get_template_value(
|
||||||
|
field,
|
||||||
|
default=default,
|
||||||
|
delim=delim or inplace_sep,
|
||||||
|
path_sep=path_sep,
|
||||||
|
filename=filename,
|
||||||
|
dirname=dirname,
|
||||||
)
|
)
|
||||||
regex_multi = re.compile(re_str)
|
elif field == "exiftool":
|
||||||
|
if subfield is None:
|
||||||
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
|
raise ValueError(
|
||||||
new_strings = {}
|
"SyntaxError: GROUP:NAME subfield must not be null with {exiftool:GROUP:NAME}'"
|
||||||
|
|
||||||
for str_template in rendered_strings:
|
|
||||||
matches = regex_multi.search(str_template)
|
|
||||||
if matches:
|
|
||||||
path_sep = (
|
|
||||||
matches.group(MATCH_GROUPS_PATH_SEP).strip("()")
|
|
||||||
if matches.group(MATCH_GROUPS_PATH_SEP) is not None
|
|
||||||
else path_sep
|
|
||||||
)
|
|
||||||
replace = (
|
|
||||||
matches.group(MATCH_GROUPS_REPLACE)[1:-1]
|
|
||||||
if matches.group(MATCH_GROUPS_REPLACE) is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
values = self.get_template_value_multi(
|
|
||||||
field,
|
|
||||||
path_sep,
|
|
||||||
filename=filename,
|
|
||||||
dirname=dirname,
|
|
||||||
replacement=replace,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
expand_inplace
|
|
||||||
or matches.group(MATCH_GROUPS_DELIM) is not None
|
|
||||||
):
|
|
||||||
delim = (
|
|
||||||
matches.group(MATCH_GROUPS_DELIM)[:-1]
|
|
||||||
if matches.group(MATCH_GROUPS_DELIM) is not None
|
|
||||||
else inplace_sep
|
|
||||||
)
|
|
||||||
# instead of returning multiple strings, join values into a single string
|
|
||||||
val = (
|
|
||||||
delim.join(sorted(values))
|
|
||||||
if values and values[0]
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
def lookup_template_value_multi(
|
|
||||||
lookup_value, *args, **kwargs
|
|
||||||
):
|
|
||||||
""" 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
|
|
||||||
_ 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 = self.make_subst_function(
|
|
||||||
none_str,
|
|
||||||
filename,
|
|
||||||
dirname,
|
|
||||||
get_func=lookup_template_value_multi,
|
|
||||||
)
|
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
|
||||||
|
|
||||||
# update rendered_strings for the next field to process
|
|
||||||
rendered_strings = list({new_string})
|
|
||||||
else:
|
|
||||||
# create a new template string for each value
|
|
||||||
for val in values:
|
|
||||||
|
|
||||||
def lookup_template_value_multi(
|
|
||||||
lookup_value, *args, **kwargs
|
|
||||||
):
|
|
||||||
""" 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
|
|
||||||
_ 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 = self.make_subst_function(
|
|
||||||
none_str,
|
|
||||||
filename,
|
|
||||||
dirname,
|
|
||||||
get_func=lookup_template_value_multi,
|
|
||||||
)
|
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
|
||||||
new_strings[new_string] = 1
|
|
||||||
|
|
||||||
# update rendered_strings for the next field to process
|
|
||||||
rendered_strings = sorted(list(new_strings.keys()))
|
|
||||||
return rendered_strings
|
|
||||||
|
|
||||||
def _render_exiftool_template(
|
|
||||||
self,
|
|
||||||
rendered_strings,
|
|
||||||
none_str,
|
|
||||||
path_sep,
|
|
||||||
expand_inplace,
|
|
||||||
inplace_sep,
|
|
||||||
filename,
|
|
||||||
dirname,
|
|
||||||
):
|
|
||||||
# TODO: lots of code commonality with render_multi_valued_templates -- combine or pull out
|
|
||||||
# TODO: put these in globals
|
|
||||||
if path_sep is None:
|
|
||||||
path_sep = os.path.sep
|
|
||||||
|
|
||||||
if inplace_sep is None:
|
|
||||||
inplace_sep = ","
|
|
||||||
|
|
||||||
# Build a regex that matches only the field being processed
|
|
||||||
re_str = (
|
|
||||||
RE_OPENING_BRACE
|
|
||||||
+ RE_DELIM
|
|
||||||
+ r"(exiftool:[^\\,}+\?\[\]]+)" # group 3 field name
|
|
||||||
+ RE_PATH_SEP
|
|
||||||
+ RE_REPLACE
|
|
||||||
+ RE_BOOL_VAL
|
|
||||||
+ RE_DEFAULT_VAL
|
|
||||||
+ RE_CLOSING_BRACE
|
|
||||||
)
|
|
||||||
regex_multi = re.compile(re_str)
|
|
||||||
|
|
||||||
# holds each of the new rendered_strings, dict to avoid repeats (dict.keys())
|
|
||||||
new_rendered_strings = []
|
|
||||||
while new_rendered_strings != rendered_strings:
|
|
||||||
new_rendered_strings = rendered_strings
|
|
||||||
new_strings = {}
|
|
||||||
for str_template in rendered_strings:
|
|
||||||
matches = regex_multi.search(str_template)
|
|
||||||
if matches:
|
|
||||||
# allmatches = regex_multi.finditer(str_template)
|
|
||||||
# for matches in allmatches:
|
|
||||||
path_sep = (
|
|
||||||
matches.group(MATCH_GROUPS_PATH_SEP).strip("()")
|
|
||||||
if matches.group(MATCH_GROUPS_PATH_SEP) is not None
|
|
||||||
else path_sep
|
|
||||||
)
|
)
|
||||||
replace = (
|
vals = self.get_template_value_exiftool(
|
||||||
matches.group(MATCH_GROUPS_REPLACE)[1:-1]
|
subfield, filename=filename, dirname=dirname
|
||||||
if matches.group(MATCH_GROUPS_REPLACE) is not None
|
)
|
||||||
else None
|
elif field in MULTI_VALUE_SUBSTITUTIONS:
|
||||||
)
|
vals = self.get_template_value_multi(
|
||||||
field = matches.group(MATCH_GROUPS_FIELD)
|
field, path_sep=path_sep, filename=filename, dirname=dirname
|
||||||
subfield = field[9:]
|
)
|
||||||
if not self.photo.path:
|
else:
|
||||||
values = [None]
|
unmatched.append(field)
|
||||||
else:
|
return [], unmatched
|
||||||
exif = ExifTool(self.photo.path, exiftool=self.exiftool_path)
|
|
||||||
exifdict = exif.asdict()
|
|
||||||
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
|
||||||
subfield = subfield.lower()
|
|
||||||
if subfield in exifdict:
|
|
||||||
values = exifdict[subfield]
|
|
||||||
values = (
|
|
||||||
[values] if not isinstance(values, list) else values
|
|
||||||
)
|
|
||||||
if replace and values:
|
|
||||||
new_values = []
|
|
||||||
for value in values:
|
|
||||||
new_values.append(self.replace(value, replace))
|
|
||||||
values = new_values
|
|
||||||
|
|
||||||
# sanitize directory names if needed
|
vals = [val for val in vals if val is not None]
|
||||||
if filename:
|
|
||||||
values = [sanitize_pathpart(value) for value in values]
|
|
||||||
elif dirname:
|
|
||||||
values = [sanitize_dirname(value) for value in values]
|
|
||||||
|
|
||||||
else:
|
if is_bool:
|
||||||
values = [None]
|
if not vals:
|
||||||
if expand_inplace or matches.group(MATCH_GROUPS_DELIM) is not None:
|
vals = default
|
||||||
delim = (
|
else:
|
||||||
matches.group(MATCH_GROUPS_DELIM)[:-1]
|
vals = bool_val
|
||||||
if matches.group(MATCH_GROUPS_DELIM) is not None
|
elif not vals:
|
||||||
else inplace_sep
|
vals = default or [none_str]
|
||||||
)
|
|
||||||
# instead of returning multiple strings, join values into a single string
|
|
||||||
val = (
|
|
||||||
delim.join(sorted(values)) if values and values[0] else None
|
|
||||||
)
|
|
||||||
|
|
||||||
def lookup_template_value_exif(lookup_value, *args, **kwargs):
|
if expand_inplace or delim is not None:
|
||||||
""" Closure passed to make_subst_function get_func
|
sep = delim if delim is not None else inplace_sep
|
||||||
Capture val and field in the closure
|
vals = [sep.join(sorted(vals))]
|
||||||
Allows make_subst_function to be re-used w/o modification
|
|
||||||
_ 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 = self.make_subst_function(
|
for filter_ in filters:
|
||||||
none_str,
|
vals = self.get_template_value_filter(filter_, vals)
|
||||||
filename,
|
|
||||||
dirname,
|
|
||||||
get_func=lookup_template_value_exif,
|
|
||||||
)
|
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
|
||||||
# update rendered_strings for the next field to process
|
|
||||||
rendered_strings = list({new_string})
|
|
||||||
else:
|
|
||||||
# create a new template string for each value
|
|
||||||
for val in values:
|
|
||||||
|
|
||||||
def lookup_template_value_exif(
|
pre = ts.pre or ""
|
||||||
lookup_value, *args, **kwargs
|
post = ts.post or ""
|
||||||
):
|
|
||||||
""" 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
|
|
||||||
_ 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 = self.make_subst_function(
|
rendered = [pre + val + post for val in vals]
|
||||||
none_str,
|
results_new = []
|
||||||
filename,
|
for ren in rendered:
|
||||||
dirname,
|
for res in results:
|
||||||
get_func=lookup_template_value_exif,
|
res_new = res + ren
|
||||||
)
|
results_new.append(res_new)
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
results = results_new
|
||||||
new_strings[new_string] = 1
|
|
||||||
# update rendered_strings for the next field to process
|
else:
|
||||||
rendered_strings = sorted(list(new_strings.keys()))
|
# no template
|
||||||
return rendered_strings
|
pre = ts.pre or ""
|
||||||
|
post = ts.post or ""
|
||||||
|
results = [r + pre + post for r in results]
|
||||||
|
|
||||||
|
return results, unmatched
|
||||||
|
|
||||||
def get_template_value(
|
def get_template_value(
|
||||||
self,
|
self,
|
||||||
@@ -649,7 +495,6 @@ class PhotoTemplate:
|
|||||||
path_sep=None,
|
path_sep=None,
|
||||||
filename=False,
|
filename=False,
|
||||||
dirname=False,
|
dirname=False,
|
||||||
replacement=None,
|
|
||||||
):
|
):
|
||||||
"""lookup value for template field (single-value template substitutions)
|
"""lookup value for template field (single-value template substitutions)
|
||||||
|
|
||||||
@@ -661,7 +506,6 @@ class PhotoTemplate:
|
|||||||
path_sep: path separator for fields that are path-like
|
path_sep: path separator for fields that are path-like
|
||||||
filename: if True, template output will be sanitized to produce valid file name
|
filename: if True, template output will be sanitized to produce valid file name
|
||||||
dirname: if True, template output will be sanitized to produce valid directory name
|
dirname: if True, template output will be sanitized to produce valid directory name
|
||||||
replacement: str, value to replace any illegal file path characters with; default = ":"
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The matching template value (which may be None).
|
The matching template value (which may be None).
|
||||||
@@ -669,6 +513,8 @@ class PhotoTemplate:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError if no rule exists for field.
|
ValueError if no rule exists for field.
|
||||||
"""
|
"""
|
||||||
|
if field not in FIELD_NAMES:
|
||||||
|
raise ValueError(f"SyntaxError: Unknown field: {field}")
|
||||||
|
|
||||||
# initialize today with current date/time if needed
|
# initialize today with current date/time if needed
|
||||||
if self.today is None:
|
if self.today is None:
|
||||||
@@ -690,9 +536,9 @@ class PhotoTemplate:
|
|||||||
elif field == "photo_or_video":
|
elif field == "photo_or_video":
|
||||||
value = self.get_photo_video_type(default)
|
value = self.get_photo_video_type(default)
|
||||||
elif field == "hdr":
|
elif field == "hdr":
|
||||||
value = self.get_photo_bool_attribute("hdr", default, bool_val)
|
value = "hdr" if self.photo.hdr else None
|
||||||
elif field == "edited":
|
elif field == "edited":
|
||||||
value = self.get_photo_bool_attribute("hasadjustments", default, bool_val)
|
value = "edited" if self.photo.hasadjustments else None
|
||||||
elif field == "created.date":
|
elif field == "created.date":
|
||||||
value = DateTimeFormatter(self.photo.date).date
|
value = DateTimeFormatter(self.photo.date).date
|
||||||
elif field == "created.year":
|
elif field == "created.year":
|
||||||
@@ -720,7 +566,7 @@ class PhotoTemplate:
|
|||||||
elif field == "created.strftime":
|
elif field == "created.strftime":
|
||||||
if default:
|
if default:
|
||||||
try:
|
try:
|
||||||
value = self.photo.date.strftime(default)
|
value = self.photo.date.strftime(default[0])
|
||||||
except:
|
except:
|
||||||
raise ValueError(f"Invalid strftime template: '{default}'")
|
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
else:
|
else:
|
||||||
@@ -801,7 +647,7 @@ class PhotoTemplate:
|
|||||||
if default:
|
if default:
|
||||||
try:
|
try:
|
||||||
date = self.photo.date_modified or self.photo.date
|
date = self.photo.date_modified or self.photo.date
|
||||||
value = date.strftime(default)
|
value = date.strftime(default[0])
|
||||||
except:
|
except:
|
||||||
raise ValueError(f"Invalid strftime template: '{default}'")
|
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
else:
|
else:
|
||||||
@@ -833,7 +679,7 @@ class PhotoTemplate:
|
|||||||
elif field == "today.strftime":
|
elif field == "today.strftime":
|
||||||
if default:
|
if default:
|
||||||
try:
|
try:
|
||||||
value = self.today.strftime(default)
|
value = self.today.strftime(default[0])
|
||||||
except:
|
except:
|
||||||
raise ValueError(f"Invalid strftime template: '{default}'")
|
raise ValueError(f"Invalid strftime template: '{default}'")
|
||||||
else:
|
else:
|
||||||
@@ -918,49 +764,65 @@ class PhotoTemplate:
|
|||||||
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
|
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
|
||||||
elif field == "uuid":
|
elif field == "uuid":
|
||||||
value = self.photo.uuid
|
value = self.photo.uuid
|
||||||
|
elif field in PUNCTUATION:
|
||||||
|
value = PUNCTUATION[field]
|
||||||
else:
|
else:
|
||||||
# if here, didn't get a match
|
# if here, didn't get a match
|
||||||
raise ValueError(f"Unhandled template value: {field}")
|
raise ValueError(f"Unhandled template value: {field}")
|
||||||
|
|
||||||
if value and replacement:
|
|
||||||
value = self.replace(value, replacement)
|
|
||||||
# process character replacements
|
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
value = sanitize_pathpart(value)
|
value = sanitize_pathpart(value)
|
||||||
elif dirname:
|
elif dirname:
|
||||||
value = sanitize_dirname(value)
|
value = sanitize_dirname(value)
|
||||||
|
|
||||||
|
return [value]
|
||||||
|
|
||||||
|
def get_template_value_filter(self, filter_, values):
|
||||||
|
if filter_ == "lower":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = [v.lower() for v in values]
|
||||||
|
else:
|
||||||
|
value = [values.lower()]
|
||||||
|
elif filter_ == "upper":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = [v.upper() for v in values]
|
||||||
|
else:
|
||||||
|
value = [values.upper()]
|
||||||
|
elif filter_ == "strip":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = [v.strip() for v in values]
|
||||||
|
else:
|
||||||
|
value = [values.strip()]
|
||||||
|
elif filter_ == "capitalize":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = [v.capitalize() for v in values]
|
||||||
|
else:
|
||||||
|
value = [values.capitalize()]
|
||||||
|
elif filter_ == "titlecase":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = [v.title() for v in values]
|
||||||
|
else:
|
||||||
|
value = [values.title()]
|
||||||
|
elif filter_ == "braces":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = ["{" + v + "}" for v in values]
|
||||||
|
else:
|
||||||
|
value = ["{" + values + "}"]
|
||||||
|
elif filter_ == "parens":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = ["(" + v + ")" for v in values]
|
||||||
|
else:
|
||||||
|
value = ["(" + values + ")"]
|
||||||
|
elif filter_ == "brackets":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = ["[" + v + "]" for v in values]
|
||||||
|
else:
|
||||||
|
value = ["[" + values + "]"]
|
||||||
|
else:
|
||||||
|
value = []
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def replace(self, value, replacement):
|
def get_template_value_multi(self, field, path_sep, filename=False, dirname=False):
|
||||||
""" process REPLACE template option
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: str value to process
|
|
||||||
replacement: str in form OLD,NEW|OLD,NEW... with old and new values for replacement
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
value with all replacements done
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError if replacement string is in wrong format
|
|
||||||
"""
|
|
||||||
if not value:
|
|
||||||
return value
|
|
||||||
|
|
||||||
replacements = replacement.split("|")
|
|
||||||
for r in replacements:
|
|
||||||
try:
|
|
||||||
old, new = r.split(",")
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f"Invalid template REPLACE value: {replacement}")
|
|
||||||
value = value.replace(old, new)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def get_template_value_multi(
|
|
||||||
self, field, path_sep, filename=False, dirname=False, replacement=None
|
|
||||||
):
|
|
||||||
"""lookup value for template field (multi-value template substitutions)
|
"""lookup value for template field (multi-value template substitutions)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -969,7 +831,7 @@ class PhotoTemplate:
|
|||||||
dirname: if True, values will be sanitized to be valid directory names; default = False
|
dirname: if True, values will be sanitized to be valid directory names; default = False
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of the matching template values or [None].
|
List of the matching template values or [].
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError if no rule exists for field.
|
ValueError if no rule exists for field.
|
||||||
@@ -1025,18 +887,9 @@ class PhotoTemplate:
|
|||||||
values = (
|
values = (
|
||||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||||
)
|
)
|
||||||
elif not field.startswith("exiftool:"):
|
else:
|
||||||
# exiftool: templates handled by _render_exiftool_template
|
|
||||||
raise ValueError(f"Unhandled template value: {field}")
|
raise ValueError(f"Unhandled template value: {field}")
|
||||||
|
|
||||||
# do any replacements needs
|
|
||||||
if replacement:
|
|
||||||
new_values = []
|
|
||||||
for value in values:
|
|
||||||
# process replacements
|
|
||||||
new_values.append(self.replace(value, replacement))
|
|
||||||
values = new_values
|
|
||||||
|
|
||||||
# sanitize directory names if needed, folder_album handled differently above
|
# sanitize directory names if needed, folder_album handled differently above
|
||||||
if filename:
|
if filename:
|
||||||
values = [sanitize_pathpart(value) for value in values]
|
values = [sanitize_pathpart(value) for value in values]
|
||||||
@@ -1044,8 +897,32 @@ class PhotoTemplate:
|
|||||||
# skip folder_album because it would have been handled above
|
# skip folder_album because it would have been handled above
|
||||||
values = [sanitize_dirname(value) for value in values]
|
values = [sanitize_dirname(value) for value in values]
|
||||||
|
|
||||||
# If no values, insert None so code below will substite none_str for None
|
# If no values, insert None so code below will substitute none_str for None
|
||||||
values = values or [None]
|
values = values or []
|
||||||
|
return values
|
||||||
|
|
||||||
|
def get_template_value_exiftool(self, subfield, filename=None, dirname=None):
|
||||||
|
"""Get template value for format "{exiftool:EXIF:Model}" """
|
||||||
|
|
||||||
|
if not self.photo.path:
|
||||||
|
return []
|
||||||
|
|
||||||
|
exif = ExifTool(self.photo.path, exiftool=self.exiftool_path)
|
||||||
|
exifdict = exif.asdict()
|
||||||
|
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
|
||||||
|
subfield = subfield.lower()
|
||||||
|
if subfield in exifdict:
|
||||||
|
values = exifdict[subfield]
|
||||||
|
values = [values] if not isinstance(values, list) else values
|
||||||
|
|
||||||
|
# sanitize directory names if needed
|
||||||
|
if filename:
|
||||||
|
values = [sanitize_pathpart(value) for value in values]
|
||||||
|
elif dirname:
|
||||||
|
values = [sanitize_dirname(value) for value in values]
|
||||||
|
else:
|
||||||
|
values = []
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def get_photo_video_type(self, default):
|
def get_photo_video_type(self, default):
|
||||||
@@ -1103,7 +980,7 @@ def parse_default_kv(default, default_dict):
|
|||||||
|
|
||||||
default_dict_ = default_dict.copy()
|
default_dict_ = default_dict.copy()
|
||||||
if default:
|
if default:
|
||||||
defaults = default.split(";")
|
defaults = default[0].split(";")
|
||||||
for kv in defaults:
|
for kv in defaults:
|
||||||
try:
|
try:
|
||||||
k, v = kv.split("=")
|
k, v = kv.split("=")
|
||||||
@@ -1113,3 +990,12 @@ def parse_default_kv(default, default_dict):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return default_dict_
|
return default_dict_
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_help():
|
||||||
|
"""Return help for template system as markdown string """
|
||||||
|
# TODO: would be better to use importlib.abc.ResourceReader but I can't find a single example of how to do this
|
||||||
|
help_file = pathlib.Path(__file__).parent / "phototemplate.md"
|
||||||
|
with open(help_file, "r") as fd:
|
||||||
|
md = fd.read()
|
||||||
|
return md
|
||||||
|
|||||||
122
osxphotos/phototemplate.tx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// OSXPhotos Template Language (OTL)
|
||||||
|
// a TemplateString has format:
|
||||||
|
// pre{delim+template_field:subfield|filter(path_sep)[find,replace]?bool_value,default}post
|
||||||
|
// a TemplateStatement may contain zero or more TemplateStrings
|
||||||
|
// The pre and post are optional strings
|
||||||
|
// The template itself (inside the {}) is also optional but if present
|
||||||
|
// everything but template_field is also optional
|
||||||
|
|
||||||
|
Statement:
|
||||||
|
(template_strings+=TemplateString)?
|
||||||
|
;
|
||||||
|
|
||||||
|
TemplateString:
|
||||||
|
pre=NON_TEMPLATE_STRING?
|
||||||
|
template=Template?
|
||||||
|
post=NON_TEMPLATE_STRING?
|
||||||
|
;
|
||||||
|
|
||||||
|
Template:
|
||||||
|
(
|
||||||
|
"{"
|
||||||
|
delim=Delim
|
||||||
|
field=Field
|
||||||
|
subfield=SubField
|
||||||
|
filter=Filter
|
||||||
|
pathsep=PathSep
|
||||||
|
findreplace=FindReplace
|
||||||
|
bool=Boolean
|
||||||
|
default=Default
|
||||||
|
"}"
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
NON_TEMPLATE_STRING:
|
||||||
|
/[^\{\},]*/
|
||||||
|
;
|
||||||
|
|
||||||
|
Delim:
|
||||||
|
(
|
||||||
|
(value=DELIM_WORD)?
|
||||||
|
'+'
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
DELIM_WORD:
|
||||||
|
/[^\{\}]*(?=\+\w)/
|
||||||
|
;
|
||||||
|
|
||||||
|
Field:
|
||||||
|
FIELD_WORD+
|
||||||
|
;
|
||||||
|
|
||||||
|
SubField:
|
||||||
|
(
|
||||||
|
":"-
|
||||||
|
SUBFIELD_WORD+
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
FIELD_WORD:
|
||||||
|
/[\.\w]+/
|
||||||
|
;
|
||||||
|
|
||||||
|
SUBFIELD_WORD:
|
||||||
|
/[\.\w:]+/
|
||||||
|
;
|
||||||
|
|
||||||
|
Filter:
|
||||||
|
(
|
||||||
|
"|"-
|
||||||
|
(value+=FILTER_WORD['|'])?
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
FILTER_WORD:
|
||||||
|
/[\.\w]+/
|
||||||
|
;
|
||||||
|
|
||||||
|
PathSep:
|
||||||
|
(
|
||||||
|
"("
|
||||||
|
(value=/[^\(\)\{\}]{0,1}/)?
|
||||||
|
")"
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
FindReplace:
|
||||||
|
(
|
||||||
|
"["
|
||||||
|
(pairs+=FindReplacePair['|'])?
|
||||||
|
"]"
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
FindReplacePair:
|
||||||
|
find=FIND_WORD
|
||||||
|
","
|
||||||
|
(replace=REPLACE_WORD)?
|
||||||
|
;
|
||||||
|
|
||||||
|
FIND_WORD:
|
||||||
|
/[^\[\]\|]*(?=\,)/
|
||||||
|
;
|
||||||
|
|
||||||
|
REPLACE_WORD:
|
||||||
|
/[^\[\]\|]*/
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
Boolean:
|
||||||
|
(
|
||||||
|
"?"
|
||||||
|
(value=Statement)?
|
||||||
|
)?
|
||||||
|
;
|
||||||
|
|
||||||
|
Default:
|
||||||
|
(
|
||||||
|
","
|
||||||
|
(value=Statement)?
|
||||||
|
)?
|
||||||
|
;
|
||||||
@@ -99,12 +99,6 @@
|
|||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="orientation(orientation)">
|
|
||||||
% if orientation is not None:
|
|
||||||
<tiff:Orientation>${orientation}</tiff:Orientation>
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="mwg_face_regions(photo)">
|
<%def name="mwg_face_regions(photo)">
|
||||||
% if photo.face_info:
|
% if photo.face_info:
|
||||||
<mwg-rs:Regions rdf:parseType="Resource">
|
<mwg-rs:Regions rdf:parseType="Resource">
|
||||||
@@ -152,7 +146,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos ${version}">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -182,12 +176,7 @@
|
|||||||
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
${gps_info(*photo.location)}
|
${gps_info(*location)}
|
||||||
</rdf:Description>
|
|
||||||
|
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
${orientation(photo.orientation)}
|
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
|
|||||||
@@ -99,12 +99,6 @@
|
|||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="orientation(orientation)">
|
|
||||||
% if orientation is not None:
|
|
||||||
<tiff:Orientation>${orientation}</tiff:Orientation>
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="mwg_face_regions(photo)">
|
<%def name="mwg_face_regions(photo)">
|
||||||
% if photo.face_info:
|
% if photo.face_info:
|
||||||
<mwg-rs:Regions rdf:parseType="Resource">
|
<mwg-rs:Regions rdf:parseType="Resource">
|
||||||
@@ -152,7 +146,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos ${version}">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -182,12 +176,7 @@
|
|||||||
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
${gps_info(*photo.location)}
|
${gps_info(*location)}
|
||||||
</rdf:Description>
|
|
||||||
|
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
${orientation(photo.orientation)}
|
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ atomicwrites==1.3.0
|
|||||||
attrs==19.1.0
|
attrs==19.1.0
|
||||||
backcall==0.1.0
|
backcall==0.1.0
|
||||||
better-exceptions-fork==0.2.1.post6
|
better-exceptions-fork==0.2.1.post6
|
||||||
black==19.10b0
|
bleach==3.3.0
|
||||||
bleach==3.1.4
|
|
||||||
bpylist2==3.0.2
|
bpylist2==3.0.2
|
||||||
certifi==2020.4.5.1
|
certifi==2020.4.5.1
|
||||||
cffi==1.14.0
|
cffi==1.14.0
|
||||||
@@ -188,8 +187,10 @@ readme-renderer==25.0
|
|||||||
regex==2020.2.20
|
regex==2020.2.20
|
||||||
requests==2.23.0
|
requests==2.23.0
|
||||||
requests-toolbelt==0.9.1
|
requests-toolbelt==0.9.1
|
||||||
|
rich==9.11.1
|
||||||
six==1.14.0
|
six==1.14.0
|
||||||
termcolor==1.1.0
|
termcolor==1.1.0
|
||||||
|
textx==2.3.0
|
||||||
toml==0.10.0
|
toml==0.10.0
|
||||||
tornado==6.0.4
|
tornado==6.0.4
|
||||||
tox==3.19.0
|
tox==3.19.0
|
||||||
|
|||||||
2
setup.py
@@ -84,6 +84,8 @@ setup(
|
|||||||
"photoscript>=0.1.0",
|
"photoscript>=0.1.0",
|
||||||
"toml>=0.10.0",
|
"toml>=0.10.0",
|
||||||
"osxmetadata>=0.99.13",
|
"osxmetadata>=0.99.13",
|
||||||
|
"textx==2.3.0",
|
||||||
|
"rich>=9.11.1",
|
||||||
],
|
],
|
||||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 528 KiB |
@@ -7,7 +7,7 @@
|
|||||||
<key>hostuuid</key>
|
<key>hostuuid</key>
|
||||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||||
<key>pid</key>
|
<key>pid</key>
|
||||||
<integer>55247</integer>
|
<integer>86501</integer>
|
||||||
<key>processname</key>
|
<key>processname</key>
|
||||||
<string>photolibraryd</string>
|
<string>photolibraryd</string>
|
||||||
<key>uid</key>
|
<key>uid</key>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 577 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
@@ -3,24 +3,24 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>BackgroundHighlightCollection</key>
|
<key>BackgroundHighlightCollection</key>
|
||||||
<date>2020-12-16T05:41:43Z</date>
|
<date>2021-03-13T16:38:25Z</date>
|
||||||
<key>BackgroundHighlightEnrichment</key>
|
<key>BackgroundHighlightEnrichment</key>
|
||||||
<date>2020-12-16T05:41:42Z</date>
|
<date>2021-03-13T16:38:24Z</date>
|
||||||
<key>BackgroundJobAssetRevGeocode</key>
|
<key>BackgroundJobAssetRevGeocode</key>
|
||||||
<date>2020-12-16T05:41:43Z</date>
|
<date>2021-03-13T16:38:25Z</date>
|
||||||
<key>BackgroundJobSearch</key>
|
<key>BackgroundJobSearch</key>
|
||||||
<date>2020-12-16T05:41:43Z</date>
|
<date>2021-03-13T16:38:25Z</date>
|
||||||
<key>BackgroundPeopleSuggestion</key>
|
<key>BackgroundPeopleSuggestion</key>
|
||||||
<date>2020-12-16T05:41:41Z</date>
|
<date>2021-03-13T16:38:23Z</date>
|
||||||
<key>BackgroundUserBehaviorProcessor</key>
|
<key>BackgroundUserBehaviorProcessor</key>
|
||||||
<date>2020-12-16T05:41:43Z</date>
|
<date>2021-03-13T16:38:25Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||||
<date>2020-10-17T23:45:33Z</date>
|
<date>2020-10-17T23:45:33Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||||
<date>2020-10-17T23:45:24Z</date>
|
<date>2020-10-17T23:45:24Z</date>
|
||||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||||
<date>2020-12-16T05:41:44Z</date>
|
<date>2021-03-13T16:38:25Z</date>
|
||||||
<key>SiriPortraitDonation</key>
|
<key>SiriPortraitDonation</key>
|
||||||
<date>2020-12-16T05:41:43Z</date>
|
<date>2021-03-13T16:38:25Z</date>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 46 KiB |
48
tests/photoinfo_mock.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Selectively mock a PhotoInfo object"""
|
||||||
|
|
||||||
|
from osxphotos import PhotoInfo
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoInfoMock(PhotoInfo):
|
||||||
|
def __init__(self, photo, **kwargs):
|
||||||
|
self._photo = photo
|
||||||
|
self._db = photo._db
|
||||||
|
self._info = photo._info
|
||||||
|
|
||||||
|
for kw in kwargs:
|
||||||
|
if hasattr(photo, kw):
|
||||||
|
setattr(self, f"_mock_{kw}", kwargs[kw])
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Not a PhotoInfo attribute: {kw}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hdr(self):
|
||||||
|
return (
|
||||||
|
self._mock_hdr
|
||||||
|
if getattr(self, "_mock_hdr", None) is not None
|
||||||
|
else self._photo.hdr
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hasadjustments(self):
|
||||||
|
return (
|
||||||
|
self._mock_hasadjustments
|
||||||
|
if getattr(self, "_mock_hasadjustments", None) is not None
|
||||||
|
else self._photo.hasadjustments
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def keywords(self):
|
||||||
|
return (
|
||||||
|
self._mock_keywords
|
||||||
|
if getattr(self, "_mock_keywords", None) is not None
|
||||||
|
else self._photo.keywords
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def title(self):
|
||||||
|
return (
|
||||||
|
self._mock_title
|
||||||
|
if getattr(self, "_mock_title", None) is not None
|
||||||
|
else self._photo.title
|
||||||
|
)
|
||||||
@@ -1 +1 @@
|
|||||||
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -46,10 +46,6 @@
|
|||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -52,10 +52,6 @@
|
|||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -46,10 +46,6 @@
|
|||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Kids"], "XMP:Subject": ["Kids"], "XMP:TagsList": ["Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
||||||
@@ -1 +1 @@
|
|||||||
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:Subject": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:TagsList": ["Folder1/SubFolder2/AlbumInFolder", "Kids", "Pumpkin Farm", "Test Album (1)"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -54,10 +54,6 @@
|
|||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"ImageDescription": "Girl holding pumpkin", "Description": "Girl holding pumpkin", "Title": "I found one!", "Keywords": ["Kids"], "Subject": ["Kids"], "TagsList": ["Kids"], "PersonInImage": ["Katie"], "DateTimeOriginal": "2018:09:28 16:07:07", "CreateDate": "2018:09:28 16:07:07", "OffsetTimeOriginal": "-04:00", "DateCreated": "2018:09:28", "TimeCreated": "16:07:07-04:00", "ModifyDate": "2018:09:28 16:07:07"}]
|
[{"ImageDescription": "Girl holding pumpkin", "Description": "Girl holding pumpkin", "Caption-Abstract": "Girl holding pumpkin", "Title": "I found one!", "ObjectName": "I found one!", "Keywords": ["Kids"], "Subject": ["Kids"], "TagsList": ["Kids"], "PersonInImage": ["Katie"], "DateTimeOriginal": "2018:09:28 16:07:07", "CreateDate": "2018:09:28 16:07:07", "OffsetTimeOriginal": "-04:00", "DateCreated": "2018:09:28", "TimeCreated": "16:07:07-04:00", "ModifyDate": "2018:09:28 16:07:07"}]
|
||||||
@@ -1 +1 @@
|
|||||||
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:Keywords": ["Katie", "Kids"], "XMP:Subject": ["Katie", "Kids"], "XMP:TagsList": ["Katie", "Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
[{"EXIF:ImageDescription": "Girl holding pumpkin", "XMP:Description": "Girl holding pumpkin", "IPTC:Caption-Abstract": "Girl holding pumpkin", "XMP:Title": "I found one!", "IPTC:ObjectName": "I found one!", "IPTC:Keywords": ["Katie", "Kids"], "XMP:Subject": ["Katie", "Kids"], "XMP:TagsList": ["Katie", "Kids"], "XMP:PersonInImage": ["Katie"], "EXIF:DateTimeOriginal": "2018:09:28 16:07:07", "EXIF:CreateDate": "2018:09:28 16:07:07", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "16:07:07-04:00", "EXIF:ModifyDate": "2018:09:28 16:07:07"}]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -48,10 +48,6 @@
|
|||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"XMP:Title": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}]
|
[{"XMP:Title": "St. James's Park", "IPTC:ObjectName": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -53,10 +53,6 @@
|
|||||||
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
||||||
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"XMP:Title": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}]
|
[{"XMP:Title": "St. James's Park", "IPTC:ObjectName": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2019:12:01 11:43:45"}]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -53,10 +53,6 @@
|
|||||||
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
||||||
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos 0.40.5">
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos">
|
||||||
<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/"
|
||||||
@@ -53,10 +53,6 @@
|
|||||||
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
||||||
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=''
|
|
||||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
|
||||||
<tiff:Orientation>1</tiff:Orientation>
|
|
||||||
</rdf:Description>
|
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
[{"XMP:Title": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2018:10:13 09:18:12"}]
|
[{"XMP:Title": "St. James's Park", "IPTC:ObjectName": "St. James's Park", "IPTC:Keywords": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:Subject": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "XMP:TagsList": ["England", "London", "London 2018", "St. James's Park", "UK", "United Kingdom"], "EXIF:GPSLatitude": 51.50357167, "EXIF:GPSLongitude": -0.1318055, "EXIF:GPSLatitudeRef": "N", "EXIF:GPSLongitudeRef": "W", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:CreateDate": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:10:13", "IPTC:TimeCreated": "09:18:12-04:00", "EXIF:ModifyDate": "2018:10:13 09:18:12"}]
|
||||||