From a80dee401c7eb959f6ad6d93a3272657ed28f521 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Thu, 14 May 2020 12:55:17 -0700 Subject: [PATCH] Implemented PhotoInfo.exiftool --- README.md | 44 +++++++++++++++++ osxphotos/_version.py | 2 +- osxphotos/photoinfo/_photoinfo_exiftool.py | 33 +++++++++++++ osxphotos/photoinfo/photoinfo.py | 1 + tests/test_exiftool.py | 56 ++++++++++++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 osxphotos/photoinfo/_photoinfo_exiftool.py diff --git a/README.md b/README.md index ccaf86fe..b8068431 100644 --- a/README.md +++ b/README.md @@ -1028,6 +1028,50 @@ Returns an [ExifInfo](#exifinfo) object with EXIF details from the Photos databa **Note**: Only valid on Photos 5; on earlier versions, returns `None`. The EXIF details returned are a subset of the actual EXIF data in a typical image. At import Photos stores this subset in the database and it's this stored data that `exif_info` returns. +See also `exiftool`. + +#### `exiftool` +Returns an ExifTool object for the photo which provides an interface to [exiftool](https://exiftool.org/) allowing you to read or write the actual EXIF data in the image file inside the Photos library. If [exif_info](#exif-info) doesn't give you all the data you need, you can use `exiftool` to read the entire EXIF contents of the image. + +If the file is missing from the library (e.g. not downloaded from iCloud), returns None. + +exiftool must be installed in the path for this to work. If exiftool cannot be found in the path, calling `exiftool` will log a warning and return `None`. You can check the exiftool path using `osxphotos.exiftool.get_exiftool_path` which will raise FileNotFoundError if exiftool cannot be found. + +```python +>>> import osxphotos +>>> osxphotos.exiftool.get_exiftool_path() +'/usr/local/bin/exiftool' +>>> +``` + +`ExifTool` provides the following methods: + +- `as_dict()`: returns all EXIF metadata found in the file as a dictionary in following form (Note: this shows just a subset of available metadata). See [exiftool](https://exiftool.org/) documentation to understand which metadata keys are available. +```python +{'Composite:Aperture': 2.2, + 'Composite:GPSPosition': '-34.9188916666667 138.596861111111', + 'Composite:ImageSize': '2754 2754', + 'EXIF:CreateDate': '2017:06:20 17:18:56', + 'EXIF:LensMake': 'Apple', + 'EXIF:LensModel': 'iPhone 6s back camera 4.15mm f/2.2', + 'EXIF:Make': 'Apple', + 'XMP:Title': 'Elder Park', +} +``` + +- `json()`: returns same information as `as_dict()` but as a serialized JSON string. + +- `setvalue(tag, value)`: write to the EXIF data in the photo file. To delete a tag, use setvalue with value = `None`. For example: +```python +photo.exiftool.setvalue("XMP:Title", "Title of photo") +``` +- `addvalues(tag, *values)`: Add one or more value(s) to tag. For a tag that accepts multiple values, like "IPTC:Keywords", this will add the values as additional list values. However, for tags which are not usually lists, such as "EXIF:ISO" this will literally add the new value to the old value which is probably not the desired effect. Be sure you understand the behavior of the individual tag before using this. For example: +```python +photo.exiftool.addvalues("IPTC:Keywords", "vacation", "beach") +``` + +**Caution**: I caution against writing new EXIF data to photos in the Photos library because this will overwrite the original copy of the photo and could adversely affect how Photos behaves. `exiftool.as_dict()` is useful for getting access to all the photos information but if you want to write new EXIF data, I recommend you export the photo first then write the data. [PhotoInfo.export()](#export) does this if called with `exiftool=True`. + #### `json()` Returns a JSON representation of all photo info diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 7d43e1ba..26a677a7 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.28.17" +__version__ = "0.28.18" diff --git a/osxphotos/photoinfo/_photoinfo_exiftool.py b/osxphotos/photoinfo/_photoinfo_exiftool.py new file mode 100644 index 00000000..689c210d --- /dev/null +++ b/osxphotos/photoinfo/_photoinfo_exiftool.py @@ -0,0 +1,33 @@ +""" Implementation for PhotoInfo.exiftool property which returns ExifTool object for a photo """ + +import logging +import os + +from ..exiftool import ExifTool, get_exiftool_path + +@property +def exiftool(self): + """ Returns an ExifTool object for the photo + requires that exiftool (https://exiftool.org/) be installed + If exiftool not installed, logs warning and returns None + If photo path is missing, returns None + """ + try: + # return the memoized instance if it exists + return self._exiftool + except AttributeError: + try: + exiftool_path = get_exiftool_path() + if self.path is not None and os.path.isfile(self.path): + exiftool = ExifTool(self.path) + else: + exiftool = None + logging.debug(f"exiftool: missing path {self.uuid}") + except FileNotFoundError: + # get_exiftool_path raises FileNotFoundError if exiftool not found + exiftool = None + logging.warning(f"exiftool not in path; download and install from https://exiftool.org/") + + self._exiftool = exiftool + return self._exiftool + diff --git a/osxphotos/photoinfo/photoinfo.py b/osxphotos/photoinfo/photoinfo.py index 12ed9301..a2b84343 100644 --- a/osxphotos/photoinfo/photoinfo.py +++ b/osxphotos/photoinfo/photoinfo.py @@ -65,6 +65,7 @@ class PhotoInfo: SearchInfo, ) from ._photoinfo_exifinfo import exif_info, ExifInfo + from ._photoinfo_exiftool import exiftool def __init__(self, db=None, uuid=None, info=None): self._uuid = uuid diff --git a/tests/test_exiftool.py b/tests/test_exiftool.py index 878e7db0..9ad01815 100644 --- a/tests/test_exiftool.py +++ b/tests/test_exiftool.py @@ -24,6 +24,39 @@ TEST_MULTI_KEYWORDS = [ "photography", ] +PHOTOS_DB = "tests/Test-10.15.4.photoslibrary" +EXIF_UUID = { + "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4": { + "EXIF:DateTimeOriginal": "2019:07:04 16:24:01", + "EXIF:LensModel": "XF18-55mmF2.8-4 R LM OIS", + "IPTC:Keywords": [ + "Digital Nomad", + "Indoor", + "Reiseblogger", + "Stock Photography", + "Top Shot", + "close up", + "colorful", + "design", + "display", + "fake", + "flower", + "outdoor", + "photography", + "plastic", + "stock photo", + "vibrant", + ], + "IPTC:DocumentNotes": "https://flickr.com/e/l7FkSm4f2lQkSV3CG6xlv8Sde5uF3gVu4Hf0Qk11AnU%3D", + }, + "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": { + "EXIF:Make": "NIKON CORPORATION", + "EXIF:Model": "NIKON D810", + "IPTC:DateCreated": "2019:04:15", + }, +} +EXIF_UUID_NONE = ["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"] + try: exiftool = get_exiftool_path() except: @@ -184,3 +217,26 @@ def test_str(): exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD) assert "file: " in str(exif1) assert "exiftool: " in str(exif1) + + +def test_photoinfo_exiftool(): + """ test PhotoInfo.exiftool which returns ExifTool object for photo """ + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + for uuid in EXIF_UUID: + photo = photosdb.photos(uuid=[uuid])[0] + exiftool = photo.exiftool + exif_dict = exiftool.as_dict() + for key, val in EXIF_UUID[uuid].items(): + assert exif_dict[key] == val + + +def test_photoinfo_exiftool_none(): + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + for uuid in EXIF_UUID_NONE: + photo = photosdb.photos(uuid=[uuid])[0] + exiftool = photo.exiftool + assert exiftool is None