Updated photokit code to work with raw+jpeg
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.48"
|
__version__ = "0.42.49"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user