# Experimental photo picker using the Photos framework via objc_util. Compared to the photos module, this has the advantage of showing all photos, including iCloud photo library. Not very well tested! from objc_util import * import threading from io import BytesIO from PIL import Image import sys import ui import ctypes NSBundle.bundleWithPath_('/System/Library/Frameworks/Photos.framework').load() PHAsset = ObjCClass('PHAsset') PHImageManager = ObjCClass('PHImageManager') PHImageRequestOptions = ObjCClass('PHImageRequestOptions') mgr = PHImageManager.defaultManager() def nsdata2str(data): return ctypes.string_at(data.bytes(), data.length()) class Asset (object): def __init__(self, asset_obj): self._asset = asset_obj def __repr__(self): return repr(self._asset) def fetch_data(self): '''Return a tuple of (UTI, data) for the asset. Both are strings, the UTI indicates the file type (e.g. 'public.jpeg').''' e = threading.Event() result = {} def handler(_cmd, _data, _uti, orientation, _info): if _data: result['data'] = nsdata2str(ObjCInstance(_data)) result['uti'] = str(ObjCInstance(_uti)) result['orientation'] = orientation result['info'] = ObjCInstance(_info) e.set() handler_block = ObjCBlock(handler, restype=None, argtypes=[c_void_p, c_void_p, c_void_p, NSInteger, c_void_p]) options = PHImageRequestOptions.new().autorelease() options.networkAccessAllowed = True options.synchronous = True mgr.requestImageDataForAsset_options_resultHandler_(self._asset, options, handler_block) e.wait() return result def fetch_ui_thumbnail(self): a = self._asset options = PHImageRequestOptions.new().autorelease() options.networkAccessAllowed = True options.synchronous = True target_size = CGSize(80, 80) result = {} e = threading.Event() def handler(_cmd, _result, _info): result['image'] = ObjCInstance(_result) e.set() handler_block = ObjCBlock(handler, restype=None, argtypes=[c_void_p, c_void_p, c_void_p]) mgr.requestImageForAsset_targetSize_contentMode_options_resultHandler_( a, target_size, 0, options, handler_block) e.wait() return result['image'] def fetch_image(self): '''Return the asset as a decoded PIL Image object.''' info = self.fetch_data() img_data = info['data'] orientation = info['orientation'] b = BytesIO(img_data) img = Image.open(b) # NOTE: Mirrored orientations are not supported here. orientations = {1: 180, 2: 90, 3: -90} rotation = orientations.get(orientation, 0) if rotation != 0: img = img.rotate(rotation) return img UICollectionView = ObjCClass('UICollectionView') UICollectionViewFlowLayout = ObjCClass('UICollectionViewFlowLayout') UICollectionViewCell = ObjCClass('UICollectionViewCell') UIImageView = ObjCClass('UIImageView') UIColor = ObjCClass('UIColor') def collectionView_numberOfItemsInSection_(_self, _cmd, _cv, _sec): ds = ObjCInstance(_self) return len(ds.assets) collectionView_numberOfItemsInSection_.encoding = 'q32@0:8@16q24' def collectionView_cellForItemAtIndexPath_(_self, _cmd, _cv, _ip): ds = ObjCInstance(_self) ip = ObjCInstance(_ip) cv = ObjCInstance(_cv) asset = ds.assets[ip.item()] thumb = asset.fetch_ui_thumbnail() cell = cv.dequeueReusableCellWithReuseIdentifier_forIndexPath_('Cell', ip) iv = cell.viewWithTag_(123) if not iv: iv_frame = cell.bounds() iv = UIImageView.alloc().initWithFrame_(iv_frame).autorelease() iv.setTag_(123) iv.setContentMode_(2) iv.setClipsToBounds_(True) iv.setAutoresizingMask_(18) cell.addSubview_(iv) iv.setImage_(thumb) return cell.ptr collectionView_cellForItemAtIndexPath_.encoding = '@32@0:8@16@24' def collectionView_didSelectItemAtIndexPath_(_self, _cmd, _cv, _ip): ds = ObjCInstance(_self) ds.selected_asset_index = ObjCInstance(_ip).item() ds.asset_collection_view.close() collectionView_didSelectItemAtIndexPath_.encoding = 'v32@0:8@16@24' methods = [collectionView_numberOfItemsInSection_, collectionView_cellForItemAtIndexPath_, collectionView_didSelectItemAtIndexPath_] DataSource = create_objc_class('DataSource', methods=methods, protocols=['UICollectionViewDelegateFlowLayout', 'UICollectionViewDataSource', 'UICollectionViewDelegate']) class AssetCollectionView (ui.View): def __init__(self, *args, **kwargs): ui.View.__init__(self, *args, **kwargs) layout = UICollectionViewFlowLayout.alloc().init().autorelease() layout.itemSize = CGSize(80, 80) layout.sectionInset = UIEdgeInsets(8, 8, 8, 8) frame = ((0, 0), (self.bounds.width, self.bounds.height)) cv = UICollectionView.alloc().initWithFrame_collectionViewLayout_(frame, layout) cv.backgroundColor = UIColor.whiteColor() cv.registerClass_forCellWithReuseIdentifier_(UICollectionViewCell, 'Cell') ds = DataSource.alloc().init().autorelease() res = PHAsset.fetchAssetsWithMediaType_options_(1, None) ds.assets = [Asset(res.objectAtIndex_(i)) for i in xrange(res.count())] ds.asset_collection_view = self self.data_source = ds cv.dataSource = ds cv.delegate = ds cv.setAlwaysBounceVertical_(True) cv.setAutoresizingMask_(18) ObjCInstance(self._objc_ptr).addSubview_(cv) def pick_asset(): av = AssetCollectionView(frame=(0, 0, 540, 576)) av.name = 'All Photos' av.present('sheet') av.wait_modal() if hasattr(av.data_source, 'selected_asset_index'): asset = av.data_source.assets[av.data_source.selected_asset_index] return asset else: return None # Demo: def main(): asset = pick_asset() if asset: img = asset.fetch_image() img.show() else: print 'No image picked' if __name__ == '__main__': main()