Fixed leaky memory in PhotoKit, issue #276
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.39.12"
|
__version__ = "0.39.13"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
# add original=False to export instead of version= (and maybe others like path())
|
# add original=False to export instead of version= (and maybe others like path())
|
||||||
# make burst/live methods get uuid from self instead of passing as arg
|
# make burst/live methods get uuid from self instead of passing as arg
|
||||||
|
|
||||||
|
import copy
|
||||||
import pathlib
|
import pathlib
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -169,12 +170,14 @@ class ImageData:
|
|||||||
requestImageDataAndOrientationForAsset_options_resultHandler_
|
requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(
|
||||||
self.metadata = None
|
self, metadata=None, uti=None, image_data=None, info=None, orientation=None
|
||||||
self.uti = None
|
):
|
||||||
self.image_data = None
|
self.metadata = metadata
|
||||||
self.info = None
|
self.uti = uti
|
||||||
self.orientation = None
|
self.image_data = image_data
|
||||||
|
self.info = info
|
||||||
|
self.orientation = orientation
|
||||||
|
|
||||||
|
|
||||||
class AVAssetData:
|
class AVAssetData:
|
||||||
@@ -475,44 +478,48 @@ class PhotoAsset:
|
|||||||
# if self.live:
|
# if self.live:
|
||||||
# raise NotImplementedError("Live photos not implemented yet")
|
# raise NotImplementedError("Live photos not implemented yet")
|
||||||
|
|
||||||
filename = (
|
with objc.autorelease_pool():
|
||||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
filename = (
|
||||||
)
|
pathlib.Path(filename)
|
||||||
|
if filename
|
||||||
|
else pathlib.Path(self.original_filename)
|
||||||
|
)
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if not dest.is_dir():
|
if not dest.is_dir():
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
|
|
||||||
output_file = None
|
output_file = None
|
||||||
if self.isphoto:
|
if self.isphoto:
|
||||||
imagedata = self._request_image_data(version=version)
|
imagedata = self._request_image_data(version=version)
|
||||||
ext = get_preferred_uti_extension(imagedata.uti)
|
ext = get_preferred_uti_extension(imagedata.uti)
|
||||||
|
|
||||||
output_file = dest / f"{filename.stem}.{ext}"
|
output_file = dest / f"{filename.stem}.{ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
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(imagedata.image_data)
|
||||||
elif self.ismovie:
|
del imagedata
|
||||||
videodata = self._request_video_data(version=version)
|
elif self.ismovie:
|
||||||
if videodata.asset is None:
|
videodata = self._request_video_data(version=version)
|
||||||
raise PhotoKitExportError("Could not get video for asset")
|
if videodata.asset is None:
|
||||||
|
raise PhotoKitExportError("Could not get video for asset")
|
||||||
|
|
||||||
url = videodata.asset.URL()
|
url = videodata.asset.URL()
|
||||||
path = pathlib.Path(NSURL_to_path(url))
|
path = pathlib.Path(NSURL_to_path(url))
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise FileNotFoundError("Could not get path to video file")
|
raise FileNotFoundError("Could not get path to video file")
|
||||||
ext = path.suffix
|
ext = path.suffix
|
||||||
output_file = dest / f"{filename.stem}{ext}"
|
output_file = dest / f"{filename.stem}{ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
output_file = pathlib.Path(increment_filename(output_file))
|
output_file = pathlib.Path(increment_filename(output_file))
|
||||||
|
|
||||||
FileUtil.copy(path, output_file)
|
FileUtil.copy(path, output_file)
|
||||||
|
|
||||||
return [str(output_file)]
|
return [str(output_file)]
|
||||||
|
|
||||||
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||||
""" Request image data and metadata for self._phasset
|
""" Request image data and metadata for self._phasset
|
||||||
@@ -529,50 +536,56 @@ class PhotoAsset:
|
|||||||
|
|
||||||
# reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc
|
# reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc
|
||||||
|
|
||||||
if version not in [
|
with objc.autorelease_pool():
|
||||||
PHOTOS_VERSION_CURRENT,
|
if version not in [
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
PHOTOS_VERSION_CURRENT,
|
||||||
PHOTOS_VERSION_UNADJUSTED,
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
]:
|
PHOTOS_VERSION_UNADJUSTED,
|
||||||
raise ValueError("Invalid value for version")
|
]:
|
||||||
|
raise ValueError("Invalid value for version")
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
options_request = Photos.PHImageRequestOptions.alloc().init()
|
|
||||||
options_request.setNetworkAccessAllowed_(True)
|
|
||||||
options_request.setSynchronous_(True)
|
|
||||||
options_request.setVersion_(version)
|
|
||||||
options_request.setDeliveryMode_(
|
|
||||||
Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
|
||||||
)
|
|
||||||
requestdata = ImageData()
|
|
||||||
event = threading.Event()
|
|
||||||
|
|
||||||
def handler(imageData, dataUTI, orientation, info):
|
|
||||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
|
||||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
|
||||||
|
|
||||||
nonlocal requestdata
|
|
||||||
|
|
||||||
options = {}
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
options_request = Photos.PHImageRequestOptions.alloc().init()
|
||||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
options_request.setNetworkAccessAllowed_(True)
|
||||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
options_request.setSynchronous_(True)
|
||||||
imgSrc, 0, options
|
options_request.setVersion_(version)
|
||||||
|
options_request.setDeliveryMode_(
|
||||||
|
Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
|
||||||
)
|
)
|
||||||
requestdata.uti = dataUTI
|
requestdata = ImageData()
|
||||||
requestdata.orientation = orientation
|
event = threading.Event()
|
||||||
requestdata.info = info
|
|
||||||
requestdata.image_data = imageData
|
|
||||||
|
|
||||||
event.set()
|
def handler(imageData, dataUTI, orientation, info):
|
||||||
|
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||||
|
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||||
|
|
||||||
self._manager.requestImageDataAndOrientationForAsset_options_resultHandler_(
|
nonlocal requestdata
|
||||||
self.phasset, options_request, handler
|
|
||||||
)
|
options = {}
|
||||||
event.wait()
|
# pylint: disable=no-member
|
||||||
self._imagedata = requestdata
|
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
||||||
return requestdata
|
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||||
|
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||||
|
imgSrc, 0, options
|
||||||
|
)
|
||||||
|
requestdata.uti = dataUTI
|
||||||
|
requestdata.orientation = orientation
|
||||||
|
requestdata.info = info
|
||||||
|
requestdata.image_data = imageData
|
||||||
|
|
||||||
|
event.set()
|
||||||
|
|
||||||
|
self._manager.requestImageDataAndOrientationForAsset_options_resultHandler_(
|
||||||
|
self.phasset, options_request, handler
|
||||||
|
)
|
||||||
|
event.wait()
|
||||||
|
# options_request.dealloc()
|
||||||
|
|
||||||
|
# not sure why this is needed -- some weird ref count thing maybe
|
||||||
|
# if I don't do this, memory leaks
|
||||||
|
data = copy.copy(requestdata)
|
||||||
|
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
|
||||||
@@ -634,37 +647,41 @@ class SlowMoVideoExporter(NSObject):
|
|||||||
Returns:
|
Returns:
|
||||||
path to exported file
|
path to exported file
|
||||||
"""
|
"""
|
||||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
|
||||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
|
||||||
)
|
|
||||||
exporter.setOutputURL_(self.url)
|
|
||||||
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
|
||||||
exporter.setShouldOptimizeForNetworkUse_(True)
|
|
||||||
|
|
||||||
self.done = False
|
with objc.autorelease_pool():
|
||||||
|
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||||
|
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||||
|
)
|
||||||
|
exporter.setOutputURL_(self.url)
|
||||||
|
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
|
||||||
|
exporter.setShouldOptimizeForNetworkUse_(True)
|
||||||
|
|
||||||
def handler():
|
self.done = False
|
||||||
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
|
||||||
self.done = True
|
|
||||||
|
|
||||||
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
def handler():
|
||||||
# wait for export to complete
|
""" result handler for exportAsynchronouslyWithCompletionHandler """
|
||||||
# would be more elegant to use a dispatch queue, notification, or thread event to wait
|
self.done = True
|
||||||
# but I can't figure out how to make that work and this does work
|
|
||||||
while True:
|
|
||||||
status = exporter.status()
|
|
||||||
if status == AVFoundation.AVAssetExportSessionStatusCompleted:
|
|
||||||
break
|
|
||||||
elif status not in (
|
|
||||||
AVFoundation.AVAssetExportSessionStatusWaiting,
|
|
||||||
AVFoundation.AVAssetExportSessionStatusExporting,
|
|
||||||
):
|
|
||||||
raise PhotoKitExportError(
|
|
||||||
f"Error encountered during exportAsynchronouslyWithCompletionHandler: status = {status}"
|
|
||||||
)
|
|
||||||
time.sleep(MIN_SLEEP)
|
|
||||||
|
|
||||||
return NSURL_to_path(exporter.outputURL())
|
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
|
||||||
|
# wait for export to complete
|
||||||
|
# would be more elegant to use a dispatch queue, notification, or thread event to wait
|
||||||
|
# but I can't figure out how to make that work and this does work
|
||||||
|
while True:
|
||||||
|
status = exporter.status()
|
||||||
|
if status == AVFoundation.AVAssetExportSessionStatusCompleted:
|
||||||
|
break
|
||||||
|
elif status not in (
|
||||||
|
AVFoundation.AVAssetExportSessionStatusWaiting,
|
||||||
|
AVFoundation.AVAssetExportSessionStatusExporting,
|
||||||
|
):
|
||||||
|
raise PhotoKitExportError(
|
||||||
|
f"Error encountered during exportAsynchronouslyWithCompletionHandler: status = {status}"
|
||||||
|
)
|
||||||
|
time.sleep(MIN_SLEEP)
|
||||||
|
|
||||||
|
exported_path = NSURL_to_path(exporter.outputURL())
|
||||||
|
# exporter.dealloc()
|
||||||
|
return exported_path
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.avasset = None
|
self.avasset = None
|
||||||
@@ -701,39 +718,43 @@ class VideoAsset(PhotoAsset):
|
|||||||
ValueError if dest is not a valid directory
|
ValueError if dest is not a valid directory
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
with objc.autorelease_pool():
|
||||||
return [
|
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||||
self._export_slow_mo(
|
return [
|
||||||
dest, filename=filename, version=version, overwrite=overwrite
|
self._export_slow_mo(
|
||||||
)
|
dest, filename=filename, version=version, overwrite=overwrite
|
||||||
]
|
)
|
||||||
|
]
|
||||||
|
|
||||||
filename = (
|
filename = (
|
||||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
pathlib.Path(filename)
|
||||||
)
|
if filename
|
||||||
|
else pathlib.Path(self.original_filename)
|
||||||
|
)
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if not dest.is_dir():
|
if not dest.is_dir():
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
|
|
||||||
output_file = None
|
output_file = None
|
||||||
videodata = self._request_video_data(version=version)
|
videodata = self._request_video_data(version=version)
|
||||||
if videodata.asset is None:
|
if videodata.asset is None:
|
||||||
raise PhotoKitExportError("Could not get video for asset")
|
raise PhotoKitExportError("Could not get video for asset")
|
||||||
|
|
||||||
url = videodata.asset.URL()
|
url = videodata.asset.URL()
|
||||||
path = pathlib.Path(NSURL_to_path(url))
|
path = pathlib.Path(NSURL_to_path(url))
|
||||||
if not path.is_file():
|
del videodata
|
||||||
raise FileNotFoundError("Could not get path to video file")
|
if not path.is_file():
|
||||||
ext = path.suffix
|
raise FileNotFoundError("Could not get path to video file")
|
||||||
output_file = dest / f"{filename.stem}{ext}"
|
ext = path.suffix
|
||||||
|
output_file = dest / f"{filename.stem}{ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
output_file = pathlib.Path(increment_filename(output_file))
|
output_file = pathlib.Path(increment_filename(output_file))
|
||||||
|
|
||||||
FileUtil.copy(path, output_file)
|
FileUtil.copy(path, output_file)
|
||||||
|
|
||||||
return [str(output_file)]
|
return [str(output_file)]
|
||||||
|
|
||||||
def _export_slow_mo(
|
def _export_slow_mo(
|
||||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||||
@@ -752,33 +773,38 @@ class VideoAsset(PhotoAsset):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError if dest is not a valid directory
|
ValueError if dest is not a valid directory
|
||||||
"""
|
"""
|
||||||
if not self.slow_mo:
|
with objc.autorelease_pool():
|
||||||
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
if not self.slow_mo:
|
||||||
|
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
||||||
|
|
||||||
videodata = self._request_video_data(version=version)
|
videodata = self._request_video_data(version=version)
|
||||||
if (
|
if (
|
||||||
not isinstance(videodata.asset, AVFoundation.AVComposition)
|
not isinstance(videodata.asset, AVFoundation.AVComposition)
|
||||||
or len(videodata.asset.tracks()) != 2
|
or len(videodata.asset.tracks()) != 2
|
||||||
):
|
):
|
||||||
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
|
raise PhotoKitMediaTypeError("Does not appear to be slow-mo video")
|
||||||
|
|
||||||
filename = (
|
filename = (
|
||||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
pathlib.Path(filename)
|
||||||
)
|
if filename
|
||||||
|
else pathlib.Path(self.original_filename)
|
||||||
|
)
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if not dest.is_dir():
|
if not dest.is_dir():
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
|
|
||||||
output_file = dest / f"{filename.stem}.mov"
|
output_file = dest / f"{filename.stem}.mov"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
output_file = pathlib.Path(increment_filename(output_file))
|
output_file = pathlib.Path(increment_filename(output_file))
|
||||||
|
|
||||||
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
|
exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_(
|
||||||
videodata.asset, output_file
|
videodata.asset, output_file
|
||||||
)
|
)
|
||||||
return exporter.exportSlowMoVideo()
|
video = exporter.exportSlowMoVideo()
|
||||||
|
# exporter.dealloc()
|
||||||
|
return video
|
||||||
|
|
||||||
# todo: rewrite this with NotificationCenter and App event loop?
|
# todo: rewrite this with NotificationCenter and App event loop?
|
||||||
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||||
@@ -793,38 +819,43 @@ class VideoAsset(PhotoAsset):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError if passed invalid value for version
|
ValueError if passed invalid value for version
|
||||||
"""
|
"""
|
||||||
|
with objc.autorelease_pool():
|
||||||
|
if version not in [
|
||||||
|
PHOTOS_VERSION_CURRENT,
|
||||||
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
|
PHOTOS_VERSION_UNADJUSTED,
|
||||||
|
]:
|
||||||
|
raise ValueError("Invalid value for version")
|
||||||
|
|
||||||
if version not in [
|
options_request = Photos.PHVideoRequestOptions.alloc().init()
|
||||||
PHOTOS_VERSION_CURRENT,
|
options_request.setNetworkAccessAllowed_(True)
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
options_request.setVersion_(version)
|
||||||
PHOTOS_VERSION_UNADJUSTED,
|
options_request.setDeliveryMode_(
|
||||||
]:
|
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||||
raise ValueError("Invalid value for version")
|
)
|
||||||
|
requestdata = AVAssetData()
|
||||||
|
event = threading.Event()
|
||||||
|
|
||||||
options_request = Photos.PHVideoRequestOptions.alloc().init()
|
def handler(asset, audiomix, info):
|
||||||
options_request.setNetworkAccessAllowed_(True)
|
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
||||||
options_request.setVersion_(version)
|
nonlocal requestdata
|
||||||
options_request.setDeliveryMode_(
|
|
||||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
|
||||||
)
|
|
||||||
requestdata = AVAssetData()
|
|
||||||
event = threading.Event()
|
|
||||||
|
|
||||||
def handler(asset, audiomix, info):
|
requestdata.asset = asset
|
||||||
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
|
requestdata.audiomix = audiomix
|
||||||
nonlocal requestdata
|
requestdata.info = info
|
||||||
|
|
||||||
requestdata.asset = asset
|
event.set()
|
||||||
requestdata.audiomix = audiomix
|
|
||||||
requestdata.info = info
|
|
||||||
|
|
||||||
event.set()
|
self._manager.requestAVAssetForVideo_options_resultHandler_(
|
||||||
|
self.phasset, options_request, handler
|
||||||
|
)
|
||||||
|
event.wait()
|
||||||
|
|
||||||
self._manager.requestAVAssetForVideo_options_resultHandler_(
|
# not sure why this is needed -- some weird ref count thing maybe
|
||||||
self.phasset, options_request, handler
|
# if I don't do this, memory leaks
|
||||||
)
|
data = copy.copy(requestdata)
|
||||||
event.wait()
|
del requestdata
|
||||||
return requestdata
|
return data
|
||||||
|
|
||||||
|
|
||||||
class LivePhotoRequest(NSObject):
|
class LivePhotoRequest(NSObject):
|
||||||
@@ -843,47 +874,54 @@ class LivePhotoRequest(NSObject):
|
|||||||
|
|
||||||
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
|
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
|
||||||
""" return the photos and video components of a live video as [PHAssetResource] """
|
""" return the photos and video components of a live video as [PHAssetResource] """
|
||||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
|
||||||
options.setNetworkAccessAllowed_(True)
|
|
||||||
options.setVersion_(version)
|
|
||||||
options.setDeliveryMode_(
|
|
||||||
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
|
||||||
)
|
|
||||||
delegate = PhotoKitNotificationDelegate.alloc().init()
|
|
||||||
|
|
||||||
self.nc.addObserver_selector_name_object_(
|
with objc.autorelease_pool():
|
||||||
delegate, "liveNotification:", None, None
|
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||||
)
|
options.setNetworkAccessAllowed_(True)
|
||||||
|
options.setVersion_(version)
|
||||||
self.live_photo = None
|
options.setDeliveryMode_(
|
||||||
|
Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat
|
||||||
def handler(result, info):
|
|
||||||
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
|
||||||
if not info["PHImageResultIsDegradedKey"]:
|
|
||||||
self.live_photo = result
|
|
||||||
self.info = info
|
|
||||||
self.nc.postNotificationName_object_(
|
|
||||||
PHOTOKIT_NOTIFICATION_FINISHED_REQUEST, self
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.manager.requestLivePhotoForAsset_targetSize_contentMode_options_resultHandler_(
|
|
||||||
self.asset,
|
|
||||||
Photos.PHImageManagerMaximumSize,
|
|
||||||
Photos.PHImageContentModeDefault,
|
|
||||||
options,
|
|
||||||
handler,
|
|
||||||
)
|
)
|
||||||
AppHelper.runConsoleEventLoop(installInterrupt=True)
|
delegate = PhotoKitNotificationDelegate.alloc().init()
|
||||||
except KeyboardInterrupt:
|
|
||||||
AppHelper.stopEventLoop()
|
|
||||||
finally:
|
|
||||||
pass
|
|
||||||
|
|
||||||
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
self.nc.addObserver_selector_name_object_(
|
||||||
self.live_photo
|
delegate, "liveNotification:", None, None
|
||||||
)
|
)
|
||||||
return asset_resources
|
|
||||||
|
self.live_photo = None
|
||||||
|
|
||||||
|
def handler(result, info):
|
||||||
|
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
|
||||||
|
if not info["PHImageResultIsDegradedKey"]:
|
||||||
|
self.live_photo = result
|
||||||
|
self.info = info
|
||||||
|
self.nc.postNotificationName_object_(
|
||||||
|
PHOTOKIT_NOTIFICATION_FINISHED_REQUEST, self
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.manager.requestLivePhotoForAsset_targetSize_contentMode_options_resultHandler_(
|
||||||
|
self.asset,
|
||||||
|
Photos.PHImageManagerMaximumSize,
|
||||||
|
Photos.PHImageContentModeDefault,
|
||||||
|
options,
|
||||||
|
handler,
|
||||||
|
)
|
||||||
|
AppHelper.runConsoleEventLoop(installInterrupt=True)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
AppHelper.stopEventLoop()
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
||||||
|
self.live_photo
|
||||||
|
)
|
||||||
|
|
||||||
|
# not sure why this is needed -- some weird ref count thing maybe
|
||||||
|
# if I don't do this, memory leaks
|
||||||
|
data = copy.copy(asset_resources)
|
||||||
|
del asset_resources
|
||||||
|
return data
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.manager = None
|
self.manager = None
|
||||||
@@ -923,88 +961,99 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
ValueError if dest is not a valid directory
|
ValueError if dest is not a valid directory
|
||||||
PhotoKitExportError if error during export
|
PhotoKitExportError if error during export
|
||||||
"""
|
"""
|
||||||
filename = (
|
|
||||||
pathlib.Path(filename) if filename else pathlib.Path(self.original_filename)
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
with objc.autorelease_pool():
|
||||||
if not dest.is_dir():
|
filename = (
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
pathlib.Path(filename)
|
||||||
|
if filename
|
||||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
else pathlib.Path(self.original_filename)
|
||||||
self._manager, self.phasset
|
|
||||||
)
|
|
||||||
resources = request.requestLivePhotoResources(version=version)
|
|
||||||
|
|
||||||
video_resource = None
|
|
||||||
photo_resource = None
|
|
||||||
for resource in resources:
|
|
||||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
|
||||||
video_resource = resource
|
|
||||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
|
||||||
photo_resource = resource
|
|
||||||
|
|
||||||
if not video_resource or not photo_resource:
|
|
||||||
raise PhotoKitExportError(
|
|
||||||
"Did not find photo/video resources for live photo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
photo_ext = get_preferred_uti_extension(photo_resource.uniformTypeIdentifier())
|
dest = pathlib.Path(dest)
|
||||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
if not dest.is_dir():
|
||||||
video_ext = get_preferred_uti_extension(video_resource.uniformTypeIdentifier())
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
|
||||||
|
|
||||||
if not overwrite:
|
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
self._manager, self.phasset
|
||||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
)
|
||||||
|
resources = request.requestLivePhotoResources(version=version)
|
||||||
|
|
||||||
# def handler(error):
|
video_resource = None
|
||||||
# if error:
|
photo_resource = None
|
||||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
for resource in resources:
|
||||||
|
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||||
|
video_resource = resource
|
||||||
|
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||||
|
photo_resource = resource
|
||||||
|
|
||||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
if not video_resource or not photo_resource:
|
||||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
raise PhotoKitExportError(
|
||||||
# options.setNetworkAccessAllowed_(True)
|
"Did not find photo/video resources for live photo"
|
||||||
# exported = []
|
)
|
||||||
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
|
||||||
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
|
||||||
# causes pathlib.Path().is_file() to fail in tests
|
|
||||||
|
|
||||||
# if photo:
|
photo_ext = get_preferred_uti_extension(
|
||||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
photo_resource.uniformTypeIdentifier()
|
||||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
)
|
||||||
# photo_resource, photo_output_url, options, handler
|
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||||
# )
|
video_ext = get_preferred_uti_extension(
|
||||||
# exported.append(str(photo_output_file))
|
video_resource.uniformTypeIdentifier()
|
||||||
|
)
|
||||||
|
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||||
|
|
||||||
# if video:
|
if not overwrite:
|
||||||
# video_output_url = path_to_NSURL(video_output_file)
|
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
||||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
||||||
# video_resource, video_output_url, options, handler
|
|
||||||
# )
|
|
||||||
# exported.append(str(video_output_file))
|
|
||||||
|
|
||||||
# def completion_handler(error):
|
# def handler(error):
|
||||||
# if error:
|
# if error:
|
||||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||||
|
|
||||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||||
|
# options.setNetworkAccessAllowed_(True)
|
||||||
|
# exported = []
|
||||||
|
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
|
||||||
|
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
|
||||||
|
# causes pathlib.Path().is_file() to fail in tests
|
||||||
|
|
||||||
exported = []
|
# if photo:
|
||||||
if photo:
|
# photo_output_url = path_to_NSURL(photo_output_file)
|
||||||
data = self._request_resource_data(photo_resource)
|
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||||
# image_data = self.request_image_data(version=version)
|
# photo_resource, photo_output_url, options, handler
|
||||||
with open(photo_output_file, "wb") as fd:
|
# )
|
||||||
fd.write(data)
|
# exported.append(str(photo_output_file))
|
||||||
exported.append(str(photo_output_file))
|
|
||||||
if video:
|
|
||||||
data = self._request_resource_data(video_resource)
|
|
||||||
with open(video_output_file, "wb") as fd:
|
|
||||||
fd.write(data)
|
|
||||||
exported.append(str(video_output_file))
|
|
||||||
|
|
||||||
return exported
|
# if video:
|
||||||
|
# video_output_url = path_to_NSURL(video_output_file)
|
||||||
|
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
||||||
|
# video_resource, video_output_url, options, handler
|
||||||
|
# )
|
||||||
|
# exported.append(str(video_output_file))
|
||||||
|
|
||||||
|
# def completion_handler(error):
|
||||||
|
# if error:
|
||||||
|
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
||||||
|
|
||||||
|
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
||||||
|
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
||||||
|
|
||||||
|
exported = []
|
||||||
|
if photo:
|
||||||
|
data = self._request_resource_data(photo_resource)
|
||||||
|
# image_data = self.request_image_data(version=version)
|
||||||
|
with open(photo_output_file, "wb") as fd:
|
||||||
|
fd.write(data)
|
||||||
|
exported.append(str(photo_output_file))
|
||||||
|
del data
|
||||||
|
if video:
|
||||||
|
data = self._request_resource_data(video_resource)
|
||||||
|
with open(video_output_file, "wb") as fd:
|
||||||
|
fd.write(data)
|
||||||
|
exported.append(str(video_output_file))
|
||||||
|
del data
|
||||||
|
|
||||||
|
request.dealloc()
|
||||||
|
return exported
|
||||||
|
|
||||||
def _request_resource_data(self, resource):
|
def _request_resource_data(self, resource):
|
||||||
""" Request asset resource data (either photo or video component)
|
""" Request asset resource data (either photo or video component)
|
||||||
@@ -1015,33 +1064,40 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
Raises:
|
Raises:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
with objc.autorelease_pool():
|
||||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||||
options.setNetworkAccessAllowed_(True)
|
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||||
|
options.setNetworkAccessAllowed_(True)
|
||||||
|
|
||||||
requestdata = PHAssetResourceData()
|
requestdata = PHAssetResourceData()
|
||||||
event = threading.Event()
|
event = threading.Event()
|
||||||
|
|
||||||
def handler(data):
|
def handler(data):
|
||||||
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
|
||||||
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
|
||||||
|
|
||||||
nonlocal requestdata
|
nonlocal requestdata
|
||||||
|
|
||||||
requestdata.data += data
|
requestdata.data += data
|
||||||
|
|
||||||
def completion_handler(error):
|
def completion_handler(error):
|
||||||
if error:
|
if error:
|
||||||
raise PhotoKitExportError("Error requesting data for asset resource")
|
raise PhotoKitExportError(
|
||||||
event.set()
|
"Error requesting data for asset resource"
|
||||||
|
)
|
||||||
|
event.set()
|
||||||
|
|
||||||
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
||||||
resource, options, handler, completion_handler
|
resource, options, handler, completion_handler
|
||||||
)
|
)
|
||||||
|
|
||||||
event.wait()
|
event.wait()
|
||||||
options.dealloc()
|
|
||||||
return requestdata.data
|
# 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
|
||||||
@@ -1127,19 +1183,20 @@ class PhotoLibrary:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
fetch_options = Photos.PHFetchOptions.alloc().init()
|
with objc.autorelease_pool():
|
||||||
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
fetch_options = Photos.PHFetchOptions.alloc().init()
|
||||||
uuid_list, fetch_options
|
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
||||||
)
|
uuid_list, fetch_options
|
||||||
if fetch_result and fetch_result.count() >= 1:
|
|
||||||
return [
|
|
||||||
self._asset_factory(fetch_result.objectAtIndex_(idx))
|
|
||||||
for idx in range(fetch_result.count())
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
raise PhotoKitFetchFailed(
|
|
||||||
f"Fetch did not return result for uuid_list {uuid_list}"
|
|
||||||
)
|
)
|
||||||
|
if fetch_result and fetch_result.count() >= 1:
|
||||||
|
return [
|
||||||
|
self._asset_factory(fetch_result.objectAtIndex_(idx))
|
||||||
|
for idx in range(fetch_result.count())
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
raise PhotoKitFetchFailed(
|
||||||
|
f"Fetch did not return result for uuid_list {uuid_list}"
|
||||||
|
)
|
||||||
|
|
||||||
def fetch_uuid(self, uuid):
|
def fetch_uuid(self, uuid):
|
||||||
""" fetch PHAsset with uuid = uuid
|
""" fetch PHAsset with uuid = uuid
|
||||||
|
|||||||
@@ -260,10 +260,10 @@ def get_preferred_uti_extension(uti):
|
|||||||
returns: preferred extension as str """
|
returns: preferred extension as str """
|
||||||
|
|
||||||
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
||||||
|
with objc.autorelease_pool():
|
||||||
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
return CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||||
uti, CoreServices.kUTTagClassFilenameExtension
|
uti, CoreServices.kUTTagClassFilenameExtension
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def findfiles(pattern, path_):
|
def findfiles(pattern, path_):
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user