Updated photokit code to work with raw+jpeg

This commit is contained in:
Rhet Turnbull
2021-06-29 17:46:40 -07:00
parent 08147e91d9
commit d2dcbaaec4
3 changed files with 246 additions and 160 deletions

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.42.48" __version__ = "0.42.49"

View File

@@ -182,8 +182,7 @@ class ImageData:
class AVAssetData: class AVAssetData:
""" Simple class to hold the data passed to the handler for """Simple class to hold the data passed to the handler for"""
"""
def __init__(self): def __init__(self):
self.asset = None self.asset = None
@@ -279,6 +278,18 @@ class PhotoAsset:
return resource.originalFilename() return resource.originalFilename()
return None return None
@property
def raw_filename(self):
"""Return RAW filename for RAW+JPEG photos or None if no RAW asset"""
resources = self._resources()
for resource in resources:
if (
self.isphoto
and resource.type() == Photos.PHAssetResourceTypeAlternatePhoto
):
return resource.originalFilename()
return None
@property @property
def hasadjustments(self): def hasadjustments(self):
"""Check to see if a PHAsset has adjustment data associated with it """Check to see if a PHAsset has adjustment data associated with it
@@ -420,6 +431,17 @@ class PhotoAsset:
imagedata = self._request_image_data(version=version) imagedata = self._request_image_data(version=version)
return imagedata.uti return imagedata.uti
def uti_raw(self):
"""Return UTI of RAW component of RAW+JPEG pair"""
resources = self._resources()
for resource in resources:
if (
self.isphoto
and resource.type() == Photos.PHAssetResourceTypeAlternatePhoto
):
return resource.uniformTypeIdentifier()
return None
def url(self, version=PHOTOS_VERSION_CURRENT): def url(self, version=PHOTOS_VERSION_CURRENT):
"""Return URL of asset """Return URL of asset
@@ -459,7 +481,12 @@ class PhotoAsset:
return imagedata.info["PHImageResultIsDegradedKey"] return imagedata.info["PHImageResultIsDegradedKey"]
def export( def export(
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False self,
dest,
filename=None,
version=PHOTOS_VERSION_CURRENT,
overwrite=False,
raw=False,
): ):
"""Export image to path """Export image to path
@@ -468,6 +495,7 @@ class PhotoAsset:
filename: str, optional name of exported file; if not provided, defaults to asset's original filename filename: str, optional name of exported file; if not provided, defaults to asset's original filename
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT) version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
overwrite: bool, if True, overwrites destination file if it already exists; default is False overwrite: bool, if True, overwrites destination file if it already exists; default is False
raw: bool, if True, export RAW component of RAW+JPEG pair, default is False
Returns: Returns:
List of path to exported image(s) List of path to exported image(s)
@@ -492,11 +520,28 @@ class PhotoAsset:
output_file = None output_file = None
if self.isphoto: if self.isphoto:
# will hold exported image data and needs to be cleaned up at end
imagedata = None
if raw:
# export the raw component
resources = self._resources()
for resource in resources:
if resource.type() == Photos.PHAssetResourceTypeAlternatePhoto:
data = self._request_resource_data(resource)
ext = pathlib.Path(self.raw_filename).suffix[1:]
break
else:
raise PhotoKitExportError(
"Could not get image data for RAW photo"
)
else:
# TODO: if user has selected use RAW as original, this returns the RAW
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
imagedata = self._request_image_data(version=version) imagedata = self._request_image_data(version=version)
if not imagedata.image_data: if not imagedata.image_data:
raise PhotoKitExportError("Could not get image data") raise PhotoKitExportError("Could not get image data")
ext = get_preferred_uti_extension(imagedata.uti) ext = get_preferred_uti_extension(imagedata.uti)
data = imagedata.image_data
output_file = dest / f"{filename.stem}.{ext}" output_file = dest / f"{filename.stem}.{ext}"
@@ -504,7 +549,9 @@ class PhotoAsset:
output_file = pathlib.Path(increment_filename(output_file)) output_file = pathlib.Path(increment_filename(output_file))
with open(output_file, "wb") as fd: with open(output_file, "wb") as fd:
fd.write(imagedata.image_data) fd.write(data)
if imagedata:
del imagedata del imagedata
elif self.ismovie: elif self.ismovie:
videodata = self._request_video_data(version=version) videodata = self._request_video_data(version=version)
@@ -594,6 +641,50 @@ class PhotoAsset:
del requestdata del requestdata
return data return data
def _request_resource_data(self, resource):
"""Request asset resource data (either photo or video component)
Args:
resource: PHAssetResource to request
Raises:
"""
with objc.autorelease_pool():
resource_manager = Photos.PHAssetResourceManager.defaultManager()
options = Photos.PHAssetResourceRequestOptions.alloc().init()
options.setNetworkAccessAllowed_(True)
requestdata = PHAssetResourceData()
event = threading.Event()
def handler(data):
"""result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object)"""
nonlocal requestdata
requestdata.data += data
def completion_handler(error):
if error:
raise PhotoKitExportError(
"Error requesting data for asset resource"
)
event.set()
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
resource, options, handler, completion_handler
)
event.wait()
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata.data)
del requestdata
return data
def _make_result_handle_(self, data): def _make_result_handle_(self, data):
"""Make handler function and threading event to use with """Make handler function and threading event to use with
requestImageDataAndOrientationForAsset_options_resultHandler_ requestImageDataAndOrientationForAsset_options_resultHandler_
@@ -656,9 +747,11 @@ class SlowMoVideoExporter(NSObject):
""" """
with objc.autorelease_pool(): with objc.autorelease_pool():
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_( exporter = (
AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
) )
)
exporter.setOutputURL_(self.url) exporter.setOutputURL_(self.url)
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie) exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
exporter.setShouldOptimizeForNetworkUse_(True) exporter.setShouldOptimizeForNetworkUse_(True)
@@ -1062,50 +1155,6 @@ class LivePhotoAsset(PhotoAsset):
request.dealloc() request.dealloc()
return exported return exported
def _request_resource_data(self, resource):
""" Request asset resource data (either photo or video component)
Args:
resource: PHAssetResource to request
Raises:
"""
with objc.autorelease_pool():
resource_manager = Photos.PHAssetResourceManager.defaultManager()
options = Photos.PHAssetResourceRequestOptions.alloc().init()
options.setNetworkAccessAllowed_(True)
requestdata = PHAssetResourceData()
event = threading.Event()
def handler(data):
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
nonlocal requestdata
requestdata.data += data
def completion_handler(error):
if error:
raise PhotoKitExportError(
"Error requesting data for asset resource"
)
event.set()
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
resource, options, handler, completion_handler
)
event.wait()
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata.data)
del requestdata
return data
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT): # def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
# # Returns an NSImage which isn't overly useful # # Returns an NSImage which isn't overly useful
# # https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimageforasset?language=objc # # https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimageforasset?language=objc

View File

@@ -57,6 +57,14 @@ UUID_DICT = {
"burst_selected": 4, "burst_selected": 4,
"burst_all": 5, "burst_all": 5,
}, },
"raw+jpeg": {
"uuid": "E3DD04AF-CB65-4D9B-BB79-FF4C955533DB",
"filename": "IMG_1994.JPG",
"raw_filename": "IMG_1994.CR2",
"unadjusted_size": 16128420,
"uti_raw": "com.canon.cr2-raw-image",
"uti": "public.jpeg",
},
} }
@@ -78,10 +86,23 @@ def test_plain_photo():
lib = PhotoLibrary() lib = PhotoLibrary()
photo = lib.fetch_uuid(uuid) photo = lib.fetch_uuid(uuid)
assert photo.original_filename == filename assert photo.original_filename == filename
assert photo.raw_filename is None
assert photo.isphoto assert photo.isphoto
assert not photo.ismovie assert not photo.ismovie
def test_raw_plus_jpeg():
"""test RAW+JPEG"""
uuid = UUID_DICT["raw+jpeg"]["uuid"]
lib = PhotoLibrary()
photo = lib.fetch_uuid(uuid)
assert photo.original_filename == UUID_DICT["raw+jpeg"]["filename"]
assert photo.raw_filename == UUID_DICT["raw+jpeg"]["raw_filename"]
assert photo.uti_raw() == UUID_DICT["raw+jpeg"]["uti_raw"]
assert photo.uti() == UUID_DICT["raw+jpeg"]["uti"]
def test_hdr(): def test_hdr():
"""test hdr""" """test hdr"""
uuid = UUID_DICT["hdr"]["uuid"] uuid = UUID_DICT["hdr"]["uuid"]
@@ -196,6 +217,22 @@ def test_export_photo_current():
assert export_path.stat().st_size == test_dict["adjusted_size"] assert export_path.stat().st_size == test_dict["adjusted_size"]
def test_export_photo_raw():
"""test PhotoAsset.export for raw component"""
test_dict = UUID_DICT["raw+jpeg"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
photo = lib.fetch_uuid(uuid)
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
export_path = photo.export(tempdir, raw=True)
export_path = pathlib.Path(export_path[0])
assert export_path.is_file()
filename = test_dict["raw_filename"]
assert export_path.stem == pathlib.Path(filename).stem
assert export_path.stat().st_size == test_dict["unadjusted_size"]
### VideoAsset ### VideoAsset