-
-
Save ochafik/59cfc9e4450e137ffe40e4e8d9f5309e to your computer and use it in GitHub Desktop.
Python: list iOS RAW Photos that can be deleted or downgraded to JPEG (PHAsset, objc_util…)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # RAWs 5408 | |
| #Adjusted RAWs 144 | |
| #Non-adjusted RAWs 5264 | |
| #Full savings: 231999038981 bytes | |
| #Deleted images: 4113 | |
| #Modified images: 766 | |
| #Unmodified images: 529 | |
| import ui | |
| import photos | |
| from enum import IntEnum | |
| from objc_util import * | |
| from ctypes import * | |
| LP64 = (sizeof(c_void_p) == 8) | |
| c = cdll.LoadLibrary(None) | |
| NSInteger = c_long if LP64 else c_int | |
| NSUInteger = c_ulong if LP64 else c_uint | |
| def bind(fptr, restype, argtypes): | |
| fptr.restype = restype | |
| fptr.argtypes = argtypes | |
| return fptr | |
| class_copyMethodList = bind( | |
| c.class_copyMethodList, | |
| POINTER(c_void_p), | |
| [c_void_p, POINTER(c_uint)]) | |
| object_getClass = bind( | |
| c.object_getClass, | |
| c_void_p, | |
| [c_void_p]) | |
| method_getName = bind( | |
| c.method_getName, | |
| c_void_p, | |
| [c_void_p]) | |
| sel_getName = bind( | |
| c.sel_getName, | |
| c_char_p, | |
| [c_void_p]) | |
| def get_method_names(cls): | |
| mc = c_uint(0) | |
| count = class_copyMethodList(object_getClass(cls), byref(mc)) | |
| mlist = class_copyMethodList(object_getClass(cls), byref(mc)) | |
| return [ | |
| sel_getName(method_getName(mlist[i])).decode() | |
| for i in range(mc.value) | |
| ] | |
| def print_methods(o): | |
| print("\n".join(sorted(get_method_names(o)))) | |
| PHAsset = ObjCClass("PHAsset") | |
| PHAssetResource = ObjCClass("PHAssetResource") | |
| PHAssetCollection = ObjCClass("PHAssetCollection") | |
| PHFetchOptions = ObjCClass("PHFetchOptions") | |
| PHAssetCreationRequest = ObjCClass("PHAssetCreationRequest") | |
| PHAssetCollectionChangeRequest = ObjCClass("PHAssetCollectionChangeRequest") | |
| PHCollectionListChangeRequest = ObjCClass("PHCollectionListChangeRequest") | |
| PHPhotoLibrary = ObjCClass("PHPhotoLibrary") | |
| #IndexSet = ObjCClass("IndexSet") | |
| def PHResultToIterator(result): | |
| #print("\n".join(sorted(get_method_names(result)))) | |
| for i in range(result.count()): | |
| yield result.objectAtIndex_(NSUInteger(i)) | |
| def PHResultToList(result): | |
| return [x for x in PHResultToIterator(result)] | |
| def PHAssetResourceSize(resource): | |
| return resource.valueForKey_("fileSize").integerValue() | |
| #def clone_asset(asset, resources): | |
| #req = PHAssetCreationRequest.forAsset() | |
| #req = PHAssetCreationRequest.creationRequestForAsset() | |
| # req = PHAssetChangeRequest.forAsset() | |
| #print(get_method_names(PHAsset)) | |
| #print(get_method_names(PHAssetResource)) | |
| #print("\n".join(sorted(get_method_names(PHAssetCollection)))) | |
| class PHAssetCollectionType(IntEnum): | |
| album = 1 | |
| smartAlbum = 2 | |
| moment = 3 | |
| #PHAssetCollectionType_album = 1 | |
| #PHAssetCollectionType_smartAlbum = 2 | |
| #PHAssetCollectionType_moment = 3 | |
| class PHAssetCollectionSubtype(IntEnum): | |
| albumRegular = 2 | |
| albumSyncedFaces = 4 | |
| albumSyncedAlbum = 5 | |
| albumImported = 6 | |
| albumSyncedEvent = 3 | |
| smartAlbumRAW = 217 | |
| smartAlbumScreenshots = 211 | |
| smartAlbumFavorites = 203 | |
| smartAlbumAllHidden = 205 | |
| smartAlbumLivePhotos = 213 | |
| smartAlbumAnimated = 214 | |
| smartAlbumBursts = 207 | |
| #PHAssetCollectionSubtype_albumRegular = 2 | |
| #PHAssetCollectionSubtype_albumSyncedFaces = 4 | |
| #PHAssetCollectionSubtype_albumSyncedAlbum = 5 | |
| #PHAssetCollectionSubtype_albumImported = 6 | |
| #PHAssetCollectionSubtype_albumSyncedEvent = 3 | |
| #PHAssetCollectionSubtype_smartAlbumRAW = 217 | |
| #PHAssetCollectionSubtype_smartAlbumScreenshots = 211 | |
| #PHAssetCollectionSubtype_smartAlbumFavorites = 203 | |
| #PHAssetCollectionSubtype_smartAlbumAllHidden = 205 | |
| #PHAssetCollectionSubtype_smartAlbumLivePhotos = 213 | |
| #PHAssetCollectionSubtype_smartAlbumAnimated = 214 | |
| #PHAssetCollectionSubtype_smartAlbumBursts = 207 | |
| class PHAssetResourceType(IntEnum): | |
| photo = 1; | |
| video = 2; | |
| audio = 3; | |
| alternatePhoto = 4; | |
| fullSizePhoto = 5; | |
| fullSizeVideo = 6; | |
| adjustmentData = 7; | |
| adjustmentBasePhoto = 8; | |
| pairedVideo = 9; | |
| fullSizePairedVideo = 10; | |
| adjustmentBaseVide = 12; | |
| adjustmentBasePairedVideo = 11; | |
| #PHAssetResourceType_photo = 1; | |
| #PHAssetResourceType_video = 2; | |
| #PHAssetResourceType_audio = 3; | |
| #PHAssetResourceType_alternatePhoto = 4; | |
| #PHAssetResourceType_fullSizePhoto = 5; | |
| #PHAssetResourceType_fullSizeVideo = 6; | |
| #PHAssetResourceType_adjustmentData = 7; | |
| #PHAssetResourceType_adjustmentBasePhoto = 8; | |
| #PHAssetResourceType_pairedVideo = 9; | |
| #PHAssetResourceType_fullSizePairedVideo = 10; | |
| #PHAssetResourceType_adjustmentBaseVide = 12; | |
| #PHAssetResourceType_adjustmentBasePairedVideo = 11; | |
| #library = PHPhotoLibrary.shared() | |
| res = PHResultToList( | |
| PHAssetCollection.fetchAssetCollectionsWithType_subtype_options_( | |
| PHAssetCollectionType.smartAlbum, | |
| PHAssetCollectionSubtype.smartAlbumRAW, | |
| None | |
| ) | |
| ) | |
| print(res) | |
| #assert(len(res) == 1) | |
| rawCol = res[0] | |
| print(rawCol) | |
| #print(rawCol.startDate()) | |
| #print(rawCol.endDate()) | |
| #print(rawCol.estimatedAssetCount()) | |
| #print_methods(PHAsset) | |
| #res = | |
| #print(res.count()) | |
| #print(PHResultToList(res)) | |
| raw_assets = PHAsset.fetchAssetsInAssetCollection_options_(rawCol, None) | |
| print("RAWs", len([None for asset in PHResultToIterator(raw_assets)])) | |
| print("Adjusted RAWs", len([None for asset in PHResultToIterator(raw_assets) if asset.hasAdjustments()])) | |
| print("Non-adjusted RAWs", len([None for asset in PHResultToIterator(raw_assets) if not asset.hasAdjustments()])) | |
| #def replace_asset(asset_mapping_by_asset_id, collections_by_asset_id): | |
| # collection_by_id = {} | |
| # asset_ids_to_replace_by_collection_id = {} | |
| # for asset_id, collections in collections_by_asset_id.items(): | |
| # for collection in collections: | |
| # collection_by_id[collection.local_id()] = collection | |
| # asset_ids_to_replace_by_collection_id[collection.local_id()] = | |
| # asset_ids_to_replace_by_collection_id.get(collection.local_id(), []) | |
| # list = asset_ids_to_replace_by_collection_id[collection.local_id()] | |
| # list.append(asset_id) | |
| # for collection_id, asset_ids in asset_ids_by_collection_id.items(): | |
| # assets = PHAsset.fetchAssetsInAssetCollection_options_(collection_by_id[collection_id], None) | |
| # indices = IndexSet.alloc().init() | |
| # replacements = NSArray.alloc().init() | |
| # for i, asset in enumerate(PHResultToIterator(assets)): | |
| # if asset_mapping_by_asset_id.has(asset.local_id()): | |
| # indices.insert(i) | |
| # replacements.append(asset_mapping_by_asset_id.get(asset.local_id())) | |
| # change.replaceAssetsAt_withAssets_(indices, replacements) | |
| # | |
| # new_assets = [ | |
| # asset_mapping_by_asset_id.get(a.local_id(), a) | |
| # for a in PHResultToIterator(assets) | |
| # ] | |
| # | |
| # assets_result = PHFetchResult. | |
| # PHAssetCollectionChangeRequest.initForAssetCollection_assets_(collection_by_id[collection_id], assets_result) | |
| # Case 1: Isolated RAW only | |
| # {type: 1 (photo), uti: com.sony.arw-raw-image, filename: DSC05346.ARW} | |
| # | |
| # Case 2: Separate JPEG+RAW pictures paired up | |
| # {type: 1 (photo), uti: public.jpeg, filename: DSC05342.JPG}, | |
| # {type: 4 (photo_alt), uti: com.sony.arw-raw-image, filename: DSC05342.ARW} | |
| # | |
| # Case 3: Assuming photo_full is the "developed" RAW w/ the adjustments | |
| # {type: 1 (photo), uti: com.sony.arw-raw-image, filename: DSC06411.ARW}, | |
| # {type: 7 (adjustment), uti: com.apple.property-list, filename: Adjustments.plist}, | |
| # {type: 5 (photo_full), uti: public.jpeg, filename: DSC06411.JPG} | |
| # | |
| # Case 4: | |
| # {type: 1 (photo), uti: com.sony.arw-raw-image, filename: DSC06411.ARW}, | |
| # {type: 7 (adjustment), uti: com.apple.property-list, filename: Adjustments.plist}, | |
| # {type: 8 (photo_base), uti: public.jpeg, filename: PenultimateFullSizeRender.jpg} | |
| # {type: 5 (photo_full), uti: public.jpeg, filename: FullSizeRender.JPG} | |
| # | |
| #for expected_length in [1, 2, 3, 4, 5]: | |
| savings = 0 | |
| deleted_images = 0 | |
| modified_images = 0 | |
| unmodified_images = 0 | |
| if True: | |
| for asset in PHResultToIterator(raw_assets): | |
| if asset.hasAdjustments(): | |
| unmodified_images += 1 | |
| continue | |
| should_keep_raw = False | |
| should_keep_jpeg = False | |
| albums = PHResultToList(PHAssetCollection.fetchAssetCollectionsContainingAsset_withType_options_(asset, PHAssetCollectionType.album, None)) | |
| resources = PHAssetResource.assetResourcesForAsset_(asset) | |
| full_size = sum([PHAssetResourceSize(r) for r in resources]) | |
| if not asset.isFavorite() and not asset.isHidden() and len(albums) == 0: | |
| # and not asset.modificationDate(): | |
| #print("Dropping asset", asset) | |
| deleted_images += 1 | |
| savings += full_size | |
| continue | |
| photo_full = [r for r in resources if r.type() == PHAssetResourceType.fullSizePhoto] | |
| photo = [r for r in resources if r.type() == PHAssetResourceType.photo] | |
| if len(photo_full) == 1 and len(photo) == 1: | |
| savings += full_size - PHAssetResourceSize(photo_full[0]) | |
| modified_images += 1 | |
| elif len(photo) == 1 and len(resources) > 1: | |
| savings += full_size - PHAssetResourceSize(photo[0]) | |
| modified_images += 1 | |
| elif len(photo) == 1 and len(resources) == 1: | |
| # TODO: Develop the RAW into JPEG if needed | |
| unmodified_images += 1 | |
| else: | |
| unmodified_images += 1 | |
| #for resource in resources: | |
| # if resource.type() == | |
| #if len(resources) == expected_length: | |
| # print(asset) | |
| # print(resources) | |
| # print([r.type() for r in resources]) | |
| # print([r.valueForKey_("fileSize").integerValue() for r in resources]) | |
| # break | |
| print("Full savings:", savings, "bytes") | |
| print("Deleted images:", deleted_images) | |
| print("Modified images:", modified_images) | |
| print("Unmodified images:", unmodified_images) | |
| # print(asset) | |
| #for col in photos.get_smart_albums(): | |
| # #print(col.title) | |
| # #print(col.subtype) | |
| # if col.title == "RAW": | |
| # #all_assets = photos.get_assets() | |
| # #last_asset = all_assets[-1] | |
| # #for asset in all_assets: | |
| # for asset in col.assets: | |
| # with asset.get_image_data(original=True) as data: | |
| # #if data.uti == "public.jpeg": | |
| # # continue | |
| # | |
| # print(asset.local_id) | |
| # print("\t{:d} bytes".format(len(data.getbuffer()))) | |
| # #data.close() | |
| # print("\t{:s}".format(data.uti)) | |
| # if asset.hidden: | |
| # print("\tHidden") | |
| # if asset.favorite: | |
| # print("\tFavorite") | |
| # if asset.duration != 0: | |
| # print("\tVideo") | |
| #img = last_asset.get_image() | |
| #img.show() | |
| #v = ui.load_view() | |
| #v.present('sheet'#) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment