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,8 +478,11 @@ class PhotoAsset:
|
|||||||
# if self.live:
|
# if self.live:
|
||||||
# raise NotImplementedError("Live photos not implemented yet")
|
# raise NotImplementedError("Live photos not implemented yet")
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
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)
|
||||||
@@ -495,6 +501,7 @@ class PhotoAsset:
|
|||||||
|
|
||||||
with open(output_file, "wb") as fd:
|
with open(output_file, "wb") as fd:
|
||||||
fd.write(imagedata.image_data)
|
fd.write(imagedata.image_data)
|
||||||
|
del imagedata
|
||||||
elif self.ismovie:
|
elif self.ismovie:
|
||||||
videodata = self._request_video_data(version=version)
|
videodata = self._request_video_data(version=version)
|
||||||
if videodata.asset is None:
|
if videodata.asset is None:
|
||||||
@@ -529,6 +536,7 @@ 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
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
if version not in [
|
if version not in [
|
||||||
PHOTOS_VERSION_CURRENT,
|
PHOTOS_VERSION_CURRENT,
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
@@ -571,8 +579,13 @@ class PhotoAsset:
|
|||||||
self.phasset, options_request, handler
|
self.phasset, options_request, handler
|
||||||
)
|
)
|
||||||
event.wait()
|
event.wait()
|
||||||
self._imagedata = requestdata
|
# options_request.dealloc()
|
||||||
return requestdata
|
|
||||||
|
# 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,6 +647,8 @@ class SlowMoVideoExporter(NSObject):
|
|||||||
Returns:
|
Returns:
|
||||||
path to exported file
|
path to exported file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
|
||||||
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
|
||||||
)
|
)
|
||||||
@@ -664,7 +679,9 @@ class SlowMoVideoExporter(NSObject):
|
|||||||
)
|
)
|
||||||
time.sleep(MIN_SLEEP)
|
time.sleep(MIN_SLEEP)
|
||||||
|
|
||||||
return NSURL_to_path(exporter.outputURL())
|
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,6 +718,7 @@ class VideoAsset(PhotoAsset):
|
|||||||
ValueError if dest is not a valid directory
|
ValueError if dest is not a valid directory
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||||
return [
|
return [
|
||||||
self._export_slow_mo(
|
self._export_slow_mo(
|
||||||
@@ -709,7 +727,9 @@ class VideoAsset(PhotoAsset):
|
|||||||
]
|
]
|
||||||
|
|
||||||
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)
|
||||||
@@ -723,6 +743,7 @@ class VideoAsset(PhotoAsset):
|
|||||||
|
|
||||||
url = videodata.asset.URL()
|
url = videodata.asset.URL()
|
||||||
path = pathlib.Path(NSURL_to_path(url))
|
path = pathlib.Path(NSURL_to_path(url))
|
||||||
|
del videodata
|
||||||
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
|
||||||
@@ -752,6 +773,7 @@ class VideoAsset(PhotoAsset):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError if dest is not a valid directory
|
ValueError if dest is not a valid directory
|
||||||
"""
|
"""
|
||||||
|
with objc.autorelease_pool():
|
||||||
if not self.slow_mo:
|
if not self.slow_mo:
|
||||||
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
raise PhotoKitMediaTypeError("Not a slow-mo video")
|
||||||
|
|
||||||
@@ -763,7 +785,9 @@ class VideoAsset(PhotoAsset):
|
|||||||
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)
|
||||||
@@ -778,7 +802,9 @@ class VideoAsset(PhotoAsset):
|
|||||||
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,7 +819,7 @@ 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 [
|
if version not in [
|
||||||
PHOTOS_VERSION_CURRENT,
|
PHOTOS_VERSION_CURRENT,
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
@@ -824,7 +850,12 @@ class VideoAsset(PhotoAsset):
|
|||||||
self.phasset, options_request, handler
|
self.phasset, options_request, handler
|
||||||
)
|
)
|
||||||
event.wait()
|
event.wait()
|
||||||
return requestdata
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
class LivePhotoRequest(NSObject):
|
class LivePhotoRequest(NSObject):
|
||||||
@@ -843,6 +874,8 @@ 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] """
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
options = Photos.PHLivePhotoRequestOptions.alloc().init()
|
||||||
options.setNetworkAccessAllowed_(True)
|
options.setNetworkAccessAllowed_(True)
|
||||||
options.setVersion_(version)
|
options.setVersion_(version)
|
||||||
@@ -883,7 +916,12 @@ class LivePhotoRequest(NSObject):
|
|||||||
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
asset_resources = Photos.PHAssetResource.assetResourcesForLivePhoto_(
|
||||||
self.live_photo
|
self.live_photo
|
||||||
)
|
)
|
||||||
return asset_resources
|
|
||||||
|
# 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,8 +961,12 @@ 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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
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)
|
||||||
@@ -949,9 +991,13 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
"Did not find photo/video resources for live photo"
|
"Did not find photo/video resources for live photo"
|
||||||
)
|
)
|
||||||
|
|
||||||
photo_ext = get_preferred_uti_extension(photo_resource.uniformTypeIdentifier())
|
photo_ext = get_preferred_uti_extension(
|
||||||
|
photo_resource.uniformTypeIdentifier()
|
||||||
|
)
|
||||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||||
video_ext = get_preferred_uti_extension(video_resource.uniformTypeIdentifier())
|
video_ext = get_preferred_uti_extension(
|
||||||
|
video_resource.uniformTypeIdentifier()
|
||||||
|
)
|
||||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
@@ -998,12 +1044,15 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
with open(photo_output_file, "wb") as fd:
|
with open(photo_output_file, "wb") as fd:
|
||||||
fd.write(data)
|
fd.write(data)
|
||||||
exported.append(str(photo_output_file))
|
exported.append(str(photo_output_file))
|
||||||
|
del data
|
||||||
if video:
|
if video:
|
||||||
data = self._request_resource_data(video_resource)
|
data = self._request_resource_data(video_resource)
|
||||||
with open(video_output_file, "wb") as fd:
|
with open(video_output_file, "wb") as fd:
|
||||||
fd.write(data)
|
fd.write(data)
|
||||||
exported.append(str(video_output_file))
|
exported.append(str(video_output_file))
|
||||||
|
del data
|
||||||
|
|
||||||
|
request.dealloc()
|
||||||
return exported
|
return exported
|
||||||
|
|
||||||
def _request_resource_data(self, resource):
|
def _request_resource_data(self, resource):
|
||||||
@@ -1015,6 +1064,7 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
Raises:
|
Raises:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
with objc.autorelease_pool():
|
||||||
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
||||||
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
||||||
options.setNetworkAccessAllowed_(True)
|
options.setNetworkAccessAllowed_(True)
|
||||||
@@ -1032,7 +1082,9 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
|
|
||||||
def completion_handler(error):
|
def completion_handler(error):
|
||||||
if error:
|
if error:
|
||||||
raise PhotoKitExportError("Error requesting data for asset resource")
|
raise PhotoKitExportError(
|
||||||
|
"Error requesting data for asset resource"
|
||||||
|
)
|
||||||
event.set()
|
event.set()
|
||||||
|
|
||||||
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
|
||||||
@@ -1040,8 +1092,12 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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,6 +1183,7 @@ class PhotoLibrary:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
with objc.autorelease_pool():
|
||||||
fetch_options = Photos.PHFetchOptions.alloc().init()
|
fetch_options = Photos.PHFetchOptions.alloc().init()
|
||||||
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
fetch_result = Photos.PHAsset.fetchAssetsWithLocalIdentifiers_options_(
|
||||||
uuid_list, fetch_options
|
uuid_list, fetch_options
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ 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
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user