def testAutomaticObserving(self): with objc.autorelease_pool(): observer = PyObjCTestObserver.alloc().init() o = PyObjCTestObserved2.alloc().init() with objc.autorelease_pool(): self.assertEqual(o.foo, None) self.assertEqual(o.bar, None) o.foo = "foo" self.assertEqual(o.foo, "foo") o.bar = "bar" self.assertEqual(o.bar, "bar") o.addObserver_forKeyPath_options_context_( observer, "bar", (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), 0, ) o.addObserver_forKeyPath_options_context_( observer, "foo", (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), 0, ) try: o.bar = "world" self.assertEqual(o.bar, "world") o.foo = "xxx" self.assertEqual(o.foo, "xxx") finally: o.removeObserver_forKeyPath_(observer, "bar") o.removeObserver_forKeyPath_(observer, "foo") self.assertEqual(len(observer.observed), 2) self.assertEqual( observer.observed[0], ("bar", o, {"kind": 1, "new": "world", "old": "bar"}, 0), ) self.assertEqual( observer.observed[1], ("foo", o, {"kind": 1, "new": "xxx", "old": "foo"}, 0), ) del observer before = DEALLOCS del o self.assertEqual(DEALLOCS, before + 1, "Leaking an observed object")
def testAutomaticObserving(self): with objc.autorelease_pool(): observer = PyObjCTestObserver.alloc().init() o = PyObjCTestObserved2.alloc().init() with objc.autorelease_pool(): self.assertEqual(o.foo, None) self.assertEqual(o.bar, None) o.foo = 'foo' self.assertEqual(o.foo, 'foo') o.bar = 'bar' self.assertEqual(o.bar, 'bar') o.addObserver_forKeyPath_options_context_( observer, 'bar', (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), 0) o.addObserver_forKeyPath_options_context_( observer, 'foo', (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), 0) try: o.bar = "world" self.assertEqual(o.bar, "world") o.foo = "xxx" self.assertEqual(o.foo, "xxx") finally: o.removeObserver_forKeyPath_(observer, "bar") o.removeObserver_forKeyPath_(observer, "foo") self.assertEqual(len(observer.observed), 2) self.assertEqual(observer.observed[0], ('bar', o, { 'kind': 1, 'new': 'world', 'old': 'bar' }, 0)) self.assertEqual(observer.observed[1], ('foo', o, { 'kind': 1, 'new': 'xxx', 'old': 'foo' }, 0)) del observer before = DEALLOCS del o self.assertEqual(DEALLOCS, before + 1, "Leaking an observed object")
def fetch_uuid_list(self, uuid_list): """ fetch PHAssets with uuids in uuid_list Args: uuid_list: list of str (UUID of image assets to fetch) Returns: list of PhotoAsset objects Raises: PhotoKitFetchFailed if fetch failed """ # pylint: disable=no-member with objc.autorelease_pool(): fetch_options = Photos.PHFetchOptions.alloc().init() 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}" )
def del_value_cache_free(self, name): with objc.autorelease_pool(): try: self._user_defaults.removeObjectForKey_(self._copy_str(name)) self._user_defaults.synchronize() except: self.LOG.exception( "Unable to delete '%s' from the user defaults:", name)
def get_value(self, name): with objc.autorelease_pool(): try: v = self._user_defaults.stringForKey_(self._copy_str(name)) return str(v) if v is not None else None except: pass return None
def get_array_value(self, name): with objc.autorelease_pool(): try: v = self._user_defaults.arrayForKey_(self._copy_str(name)) return [str(i) for i in v] if v is not None else None except: pass return None
def get_dict_value(self, name): with objc.autorelease_pool(): try: v = self._user_defaults.dictionaryForKey_(self._copy_str(name)) return {str(k): str(i) for k, i in v.items()} if v is not None else None except: pass return None
def set_dict_value(self, name, value): with objc.autorelease_pool(): try: if value is not None: self._user_defaults.setObject_forKey_(self._copy_dict(value), self._copy_str(name)) else: self.del_value(self._copy_str(name)) except: self.LOG.exception("Unable to set dict '%s' in the user defaults:", name)
def get_value_cache_free(self, name): with objc.autorelease_pool(): try: v = self._user_defaults.stringForKey_(self._copy_str(name)) return str(v) if v is not None else None except: pass return None
def get_array_value_cache_free(self, name, allow_cache=False): with objc.autorelease_pool(): try: v = self._user_defaults.arrayForKey_(self._copy_str(name)) return [str(i) for i in v] if v is not None else None except: pass return None
def get_preferred_uti_extension(uti): """ get preferred extension for a UTI type uti: UTI str, e.g. 'public.jpeg' returns: preferred extension as str """ # reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc with objc.autorelease_pool(): return CoreServices.UTTypeCopyPreferredTagWithClass( uti, CoreServices.kUTTagClassFilenameExtension)
def get_dict_value_cache_free(self, name, allow_cache=False): with objc.autorelease_pool(): try: v = self._user_defaults.dictionaryForKey_(self._copy_str(name)) return {str(k): str(i) for k, i in v.items()} if v is not None else None except: pass return None
def export( self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False ): """ Export video to path Args: dest: str, path to destination directory 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) overwrite: bool, if True, overwrites destination file if it already exists; default is False Returns: List of path to exported image(s) Raises: ValueError if dest is not a valid directory """ with objc.autorelease_pool(): if self.slow_mo and version == PHOTOS_VERSION_CURRENT: return [ self._export_slow_mo( dest, filename=filename, version=version, overwrite=overwrite ) ] filename = ( pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) ) dest = pathlib.Path(dest) if not dest.is_dir(): raise ValueError("dest must be a valid directory: {dest}") output_file = None videodata = self._request_video_data(version=version) if videodata.asset is None: raise PhotoKitExportError("Could not get video for asset") url = videodata.asset.URL() path = pathlib.Path(NSURL_to_path(url)) del videodata if not path.is_file(): raise FileNotFoundError("Could not get path to video file") ext = path.suffix output_file = dest / f"{filename.stem}{ext}" if not overwrite: output_file = pathlib.Path(increment_filename(output_file)) FileUtil.copy(path, output_file) return [str(output_file)]
def set_array_value_cache_free(self, name, value): with objc.autorelease_pool(): try: if value is not None: self._user_defaults.setObject_forKey_( self._copy_array(value), self._copy_str(name)) self._user_defaults.synchronize() else: self.del_value_cache_free(self._copy_str(name)) except: self.LOG.exception( "Unable to set array '%s' in the user defaults:", name)
def testAutomaticObserving(self): with objc.autorelease_pool(): observer = PyObjCTestObserver.alloc().init() o = PyObjCTestObserved2.alloc().init() with objc.autorelease_pool(): self.assertEqual(o.foo, None) self.assertEqual(o.bar, None) o.foo = "foo" self.assertEqual(o.foo, "foo") o.bar = "bar" self.assertEqual(o.bar, "bar") o.addObserver_forKeyPath_options_context_( observer, "bar", (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), 0 ) o.addObserver_forKeyPath_options_context_( observer, "foo", (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld), 0 ) try: o.bar = "world" self.assertEqual(o.bar, "world") o.foo = "xxx" self.assertEqual(o.foo, "xxx") finally: o.removeObserver_forKeyPath_(observer, "bar") o.removeObserver_forKeyPath_(observer, "foo") self.assertEqual(len(observer.observed), 2) self.assertEqual(observer.observed[0], ("bar", o, {"kind": 1, "new": "world", "old": "bar"}, 0)) self.assertEqual(observer.observed[1], ("foo", o, {"kind": 1, "new": "xxx", "old": "foo"}, 0)) del observer before = DEALLOCS del o self.assertEqual(DEALLOCS, before + 1, "Leaking an observed object")
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT): """ return the photos and video components of a live video as [PHAssetResource] """ with objc.autorelease_pool(): 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_( delegate, "liveNotification:", None, None ) 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 _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL): """ Request video data for self._phasset Args: version: which version to request PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version PHOTOS_VERSION_CURRENT, request current version with all edits PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version Raises: 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") options_request = Photos.PHVideoRequestOptions.alloc().init() options_request.setNetworkAccessAllowed_(True) options_request.setVersion_(version) options_request.setDeliveryMode_( Photos.PHVideoRequestOptionsDeliveryModeHighQualityFormat ) requestdata = AVAssetData() event = threading.Event() def handler(asset, audiomix, info): """ result handler for requestAVAssetForVideo:asset options:options resultHandler """ nonlocal requestdata requestdata.asset = asset requestdata.audiomix = audiomix requestdata.info = info event.set() self._manager.requestAVAssetForVideo_options_resultHandler_( self.phasset, options_request, 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) del requestdata return data
def _export_slow_mo( self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False ): """ Export slow-motion video to path Args: dest: str, path to destination directory 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) overwrite: bool, if True, overwrites destination file if it already exists; default is False Returns: Path to exported image Raises: ValueError if dest is not a valid directory """ with objc.autorelease_pool(): if not self.slow_mo: raise PhotoKitMediaTypeError("Not a slow-mo video") videodata = self._request_video_data(version=version) if ( not isinstance(videodata.asset, AVFoundation.AVComposition) or len(videodata.asset.tracks()) != 2 ): raise PhotoKitMediaTypeError("Does not appear to be slow-mo video") filename = ( pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) ) dest = pathlib.Path(dest) if not dest.is_dir(): raise ValueError("dest must be a valid directory: {dest}") output_file = dest / f"{filename.stem}.mov" if not overwrite: output_file = pathlib.Path(increment_filename(output_file)) exporter = SlowMoVideoExporter.alloc().initWithAVAsset_path_( videodata.asset, output_file ) video = exporter.exportSlowMoVideo() # exporter.dealloc() return video
def _runloop_thread(self): try: with objc.autorelease_pool(): queue_ptr = objc.objc_object(c_void_p=self._dispatch_queue) self._manager = CoreBluetooth.CBCentralManager.alloc() self._manager.initWithDelegate_queue_options_( self, queue_ptr, None) #self._peripheral_manager = CoreBluetooth.CBPeripheralManager.alloc() #self._peripheral_manager.initWithDelegate_queue_options_(self, queue_ptr, None) self._runloop_started_lock.set() AppHelper.runConsoleEventLoop(installInterrupt=True) except Exception as e: log.exception(e) log.info("Exiting runloop")
def detect_text(img_path: str, orientation: Optional[int] = None) -> List: """process image at img_path with VNRecognizeTextRequest and return list of results Args: img_path: path to the image file orientation: optional EXIF orientation (if known, passing orientation may improve quality of results) """ if not vision: logging.warning( f"detect_text not implemented for this version of macOS") return [] with objc.autorelease_pool(): input_url = NSURL.fileURLWithPath_(img_path) with pipes() as (out, err): # capture stdout and stderr from system calls # otherwise, Quartz.CIImage.imageWithContentsOfURL_ # prints to stderr something like: # 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774 # 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0 input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url) vision_options = NSDictionary.dictionaryWithDictionary_({}) if orientation is not None: if not 1 <= orientation <= 8: raise ValueError("orientation must be between 1 and 8") vision_handler = Vision.VNImageRequestHandler.alloc( ).initWithCIImage_orientation_options_(input_image, orientation, vision_options) else: vision_handler = ( Vision.VNImageRequestHandler.alloc().initWithCIImage_options_( input_image, vision_options)) results = [] handler = make_request_handler(results) vision_request = (Vision.VNRecognizeTextRequest.alloc(). initWithCompletionHandler_(handler)) error = vision_handler.performRequests_error_([vision_request], None) vision_request.dealloc() vision_handler.dealloc() for result in results: result[0] = str(result[0]) return results
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 exportSlowMoVideo(self): """export slow-mo video with AVAssetExportSession Returns: path to exported file """ 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) self.done = False def handler(): """result handler for exportAsynchronouslyWithCompletionHandler""" self.done = True 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 get_preferred_uti_extension(uti): """get preferred extension for a UTI type uti: UTI str, e.g. 'public.jpeg' returns: preferred extension as str or None if cannot be determined""" if (OS_VER, OS_MAJOR) <= (10, 16): # reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc # deprecated in Catalina+, likely won't work at all on macOS 12 with objc.autorelease_pool(): extension = CoreServices.UTTypeCopyPreferredTagWithClass( uti, CoreServices.kUTTagClassFilenameExtension ) if extension: return extension # on MacOS 10.12, HEIC files are not supported and UTTypeCopyPreferredTagWithClass will return None for HEIC if uti == "public.heic": return "HEIC" return None return _get_ext_from_uti_dict(uti)
def get_uti_for_extension(extension): """get UTI for a given file extension""" if not extension: return None # accepts extension with or without leading 0 if extension[0] == ".": extension = extension[1:] if (OS_VER, OS_MAJOR) <= (10, 16): # https://developer.apple.com/documentation/coreservices/1448939-uttypecreatepreferredidentifierf with objc.autorelease_pool(): uti = CoreServices.UTTypeCreatePreferredIdentifierForTag( CoreServices.kUTTagClassFilenameExtension, extension, None ) if uti: return uti # on MacOS 10.12, HEIC files are not supported and UTTypeCopyPreferredTagWithClass will return None for HEIC if extension.lower() == "heic": return "public.heic" return None uti = _get_uti_from_ext_dict(extension) if uti: return uti uti = _get_uti_from_mdls(extension) if uti: # cache the UTI EXT_UTI_DICT[extension.lower()] = uti UTI_EXT_DICT[uti] = extension.lower() return uti return None
def export( self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False, photo=True, video=True, **kwargs, ): """Export image to path Args: dest: str, path to destination directory 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) overwrite: bool, if True, overwrites destination file if it already exists; default is False photo: bool, if True, export photo component of live photo video: bool, if True, export live video component of live photo **kwargs: used only to avoid issues with each asset type having slightly different export arguments Returns: list of [path to exported image and/or video] Raises: ValueError if dest is not a valid directory PhotoKitExportError if error during export """ with objc.autorelease_pool(): with pipes() as (out, err): filename = ( pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) ) dest = pathlib.Path(dest) if not dest.is_dir(): raise ValueError("dest must be a valid directory: {dest}") request = LivePhotoRequest.alloc().initWithManager_Asset_( 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() ) photo_output_file = dest / f"{filename.stem}.{photo_ext}" video_ext = get_preferred_uti_extension( video_resource.uniformTypeIdentifier() ) video_output_file = dest / f"{filename.stem}.{video_ext}" if not overwrite: photo_output_file = pathlib.Path( increment_filename(photo_output_file) ) video_output_file = pathlib.Path( increment_filename(video_output_file) ) 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 export( self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False, raw=False, **kwargs, ): """Export image to path Args: dest: str, path to destination directory 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) 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 **kwargs: used only to avoid issues with each asset type having slightly different export arguments Returns: List of path to exported image(s) Raises: ValueError if dest is not a valid directory """ with objc.autorelease_pool(): with pipes() as (out, err): filename = ( pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) ) dest = pathlib.Path(dest) if not dest.is_dir(): raise ValueError("dest must be a valid directory: {dest}") output_file = None 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) suffix = pathlib.Path(self.raw_filename).suffix ext = suffix[1:] if suffix else "" 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) if not imagedata.image_data: raise PhotoKitExportError("Could not get image data") ext = get_preferred_uti_extension(imagedata.uti) data = imagedata.image_data output_file = dest / f"{filename.stem}.{ext}" if not overwrite: output_file = pathlib.Path(increment_filename(output_file)) with open(output_file, "wb") as fd: fd.write(data) if imagedata: del imagedata elif self.ismovie: videodata = self._request_video_data(version=version) if videodata.asset is None: raise PhotoKitExportError("Could not get video for asset") url = videodata.asset.URL() path = pathlib.Path(NSURL_to_path(url)) if not path.is_file(): raise FileNotFoundError("Could not get path to video file") ext = path.suffix output_file = dest / f"{filename.stem}{ext}" if not overwrite: output_file = pathlib.Path(increment_filename(output_file)) FileUtil.copy(path, output_file) return [str(output_file)]
def theme(): with objc.autorelease_pool(): user_defaults = Foundation.NSUserDefaults.standardUserDefaults() system_theme = user_defaults.stringForKey_("AppleInterfaceStyle") return "dark" if system_theme == "Dark" else "light"
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL): """ Request image data and metadata for self._phasset Args: version: which version to request PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version PHOTOS_VERSION_CURRENT, request current version with all edits PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version Returns: ImageData instance Raises: ValueError if passed invalid value for version """ # reference: https://developer.apple.com/documentation/photokit/phimagemanager/3237282-requestimagedataandorientationfo?language=objc with objc.autorelease_pool(): if version not in [ PHOTOS_VERSION_CURRENT, PHOTOS_VERSION_ORIGINAL, PHOTOS_VERSION_UNADJUSTED, ]: 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 options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse 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 export( self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False, photo=True, video=True, ): """ Export image to path Args: dest: str, path to destination directory 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) overwrite: bool, if True, overwrites destination file if it already exists; default is False photo: bool, if True, export photo component of live photo video: bool, if True, export live video component of live photo Returns: list of [path to exported image and/or video] Raises: ValueError if dest is not a valid directory PhotoKitExportError if error during export """ with objc.autorelease_pool(): filename = ( pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) ) dest = pathlib.Path(dest) if not dest.is_dir(): raise ValueError("dest must be a valid directory: {dest}") request = LivePhotoRequest.alloc().initWithManager_Asset_( 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() ) photo_output_file = dest / f"{filename.stem}.{photo_ext}" video_ext = get_preferred_uti_extension( video_resource.uniformTypeIdentifier() ) video_output_file = dest / f"{filename.stem}.{video_ext}" if not overwrite: photo_output_file = pathlib.Path(increment_filename(photo_output_file)) video_output_file = pathlib.Path(increment_filename(video_output_file)) # def handler(error): # if error: # raise PhotoKitExportError(f"writeDataForAssetResource error: {error}") # resource_manager = Photos.PHAssetResourceManager.defaultManager() # 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 # if photo: # photo_output_url = path_to_NSURL(photo_output_file) # resource_manager.writeDataForAssetResource_toFile_options_completionHandler_( # photo_resource, photo_output_url, options, handler # ) # exported.append(str(photo_output_file)) # 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 del_value(self, name): with objc.autorelease_pool(): try: self._user_defaults.removeObjectForKey_(self._copy_str(name)) except: self.LOG.exception("Unable to delete '%s' from the user defaults:", name)
def export( self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False ): """ Export image to path Args: dest: str, path to destination directory 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) overwrite: bool, if True, overwrites destination file if it already exists; default is False Returns: List of path to exported image(s) Raises: ValueError if dest is not a valid directory """ # if self.live: # raise NotImplementedError("Live photos not implemented yet") with objc.autorelease_pool(): filename = ( pathlib.Path(filename) if filename else pathlib.Path(self.original_filename) ) dest = pathlib.Path(dest) if not dest.is_dir(): raise ValueError("dest must be a valid directory: {dest}") output_file = None if self.isphoto: imagedata = self._request_image_data(version=version) if not imagedata.image_data: raise PhotoKitExportError("Could not get image data") ext = get_preferred_uti_extension(imagedata.uti) output_file = dest / f"{filename.stem}.{ext}" if not overwrite: output_file = pathlib.Path(increment_filename(output_file)) with open(output_file, "wb") as fd: fd.write(imagedata.image_data) del imagedata elif self.ismovie: videodata = self._request_video_data(version=version) if videodata.asset is None: raise PhotoKitExportError("Could not get video for asset") url = videodata.asset.URL() path = pathlib.Path(NSURL_to_path(url)) if not path.is_file(): raise FileNotFoundError("Could not get path to video file") ext = path.suffix output_file = dest / f"{filename.stem}{ext}" if not overwrite: output_file = pathlib.Path(increment_filename(output_file)) FileUtil.copy(path, output_file) return [str(output_file)]
def write_jpeg(self, input_path, output_path, compression_quality=1.0): """convert image to jpeg and write image to output_path Args: input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path output_path: path to exported jpeg (e.g. '/path/to/export/file.jpeg') as str or pathlib.Path compression_quality: JPEG compression quality, float in range 0.0 to 1.0; default is 1.0 (best quality) Return: True if conversion successful, else False Raises: ValueError if compression quality not in range 0.0 to 1.0 FileNotFoundError if input_path doesn't exist ImageConversionError if error during conversion """ # Set up a dedicated objc autorelease pool for this function call. # This is to ensure that all the NSObjects are cleaned up after each # call to prevent memory leaks. # https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html # https://pyobjc.readthedocs.io/en/latest/api/module-objc.html#memory-management with objc.autorelease_pool(): # accept input_path or output_path as pathlib.Path if not isinstance(input_path, str): input_path = str(input_path) if not isinstance(output_path, str): output_path = str(output_path) if not pathlib.Path(input_path).is_file(): raise FileNotFoundError(f"could not find {input_path}") if not (0.0 <= compression_quality <= 1.0): raise ValueError( "illegal value for compression_quality: {compression_quality}" ) input_url = NSURL.fileURLWithPath_(input_path) output_url = NSURL.fileURLWithPath_(output_path) with pipes() as (out, err): # capture stdout and stderr from system calls # otherwise, Quartz.CIImage.imageWithContentsOfURL_ # prints to stderr something like: # 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774 # 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0 input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url) if input_image is None: raise ImageConversionError( f"Could not create CIImage for {input_path}") output_colorspace = (input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName( Quartz.CoreGraphics.kCGColorSpaceSRGB)) output_options = NSDictionary.dictionaryWithDictionary_({ "kCGImageDestinationLossyCompressionQuality": compression_quality }) ( _, error, ) = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_( input_image, output_url, output_colorspace, output_options, None) if not error: return True else: raise ImageConversionError( f"Error converting file {input_path} to jpeg at {output_path}: {error}" )