Skip to content

Instantly share code, notes, and snippets.

@ochafik
Last active September 21, 2022 22:31
Show Gist options
  • Select an option

  • Save ochafik/59cfc9e4450e137ffe40e4e8d9f5309e to your computer and use it in GitHub Desktop.

Select an option

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…)
# 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