├── README.md └── filters.py /README.md: -------------------------------------------------------------------------------- 1 | # filters 2 | Restore filters on photos imported from iOS 8. 3 | 4 | ### Problem 5 | 6 | iOS 8 lets you apply filters on photos like "Chrome", "Transfer" or "Instant". 7 | 8 | But when you import the photos on a Mac, the filters are lost. 9 | 10 | ### Solution 11 | 12 | 1. import the photos with Image Capture 13 | 2. run `$ python filters.py` 14 | 15 | ### Algorithm 16 | 17 | for each AAE file: 18 | if there is a paired JPG file: 19 | read the filter name in the AAE file 20 | apply the filter to the JPG file 21 | save the new JPG file 22 | 23 | ### Disclaimer 24 | 25 | Quick and dirty PyObjC script that you can read and hack. 26 | 27 | Works for me on OS X 10.10.3. 28 | -------------------------------------------------------------------------------- /filters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Nicolas Seriot 4 | # 2015-04-11 5 | 6 | import argparse 7 | import os 8 | import time 9 | 10 | from Foundation import NSBundle, NSClassFromString, NSDictionary, NSURL 11 | 12 | success = NSBundle.bundleWithPath_("/System/Library/PrivateFrameworks/PhotoLibraryPrivate.framework/Versions/A/Frameworks/PAImaging.framework").load() 13 | assert success == True 14 | 15 | CIImage = NSClassFromString("CIImage") 16 | assert CIImage 17 | 18 | CIFilter = NSClassFromString("CIFilter") 19 | assert CIFilter 20 | 21 | NSBitmapImageRep = NSClassFromString("NSBitmapImageRep") 22 | assert NSBitmapImageRep 23 | 24 | IPAPhotoAdjustmentStackSerializer_v10 = NSClassFromString("IPAPhotoAdjustmentStackSerializer_v10") 25 | assert IPAPhotoAdjustmentStackSerializer_v10 26 | 27 | ipaPASS = IPAPhotoAdjustmentStackSerializer_v10.alloc().init() 28 | 29 | def apply_cifilter_with_name(filter_name, orientation, in_path, out_path, dry_run=False): 30 | 31 | print "-- in: ", in_path 32 | print "-- out:", out_path 33 | 34 | assert in_path 35 | assert out_path 36 | 37 | assert filter_name in ["CIPhotoEffectTonal", "CIPhotoEffectMono", "CIPhotoEffectInstant", "CIPhotoEffectTransfer", 38 | "CIPhotoEffectProcess", "CIPhotoEffectChrome", "CIPhotoEffectNoir", "CIPhotoEffectFade", 39 | "CIPhotoEffect3DDramatic", "CIPhotoEffect3DVivid", "CIPhotoEffect3DDramaticCool", "CIPhotoEffect3DNoir"] 40 | 41 | url = NSURL.alloc().initFileURLWithPath_(in_path) 42 | ci_image = CIImage.imageWithContentsOfURL_(url) 43 | assert ci_image 44 | 45 | in_creation_timestamp = os.path.getmtime(in_path) 46 | print time.ctime(in_creation_timestamp) 47 | 48 | if orientation != None and orientation != 1: 49 | print "-- orientation:", orientation 50 | ci_image = ci_image.imageByApplyingOrientation_(orientation) 51 | 52 | ci_filter = CIFilter.filterWithName_(filter_name) 53 | assert ci_filter 54 | 55 | ci_filter.setValue_forKey_(ci_image, "inputImage") 56 | ci_filter.setDefaults() 57 | 58 | ci_image_result = ci_filter.outputImage() 59 | assert ci_image_result 60 | 61 | bitmap_rep = NSBitmapImageRep.alloc().initWithCIImage_(ci_image_result) 62 | assert bitmap_rep 63 | 64 | properties = { "NSImageCompressionFactor" : 0.9 } 65 | data = bitmap_rep.representationUsingType_properties_(3, properties) # 3 for JPEG 66 | 67 | if dry_run: 68 | print "-- dryrun, don't write", out_path 69 | return 70 | 71 | assert data.writeToFile_atomically_(out_path, True) 72 | 73 | os.utime(out_path, (time.time(), in_creation_timestamp)) 74 | 75 | def read_aae_file(path): 76 | 77 | plist = NSDictionary.dictionaryWithContentsOfFile_(path) 78 | 79 | if plist["adjustmentFormatIdentifier"] != "com.apple.photo": 80 | print "-- bad format identifier:", plist["adjustmentFormatIdentifier"] 81 | return None, None 82 | 83 | data = plist["adjustmentData"] 84 | 85 | d = ipaPASS.archiveFromData_error_(data, None) 86 | 87 | adjustments = d["adjustments"] 88 | orientation = d["metadata"]["orientation"] 89 | 90 | effect_names = [ d_["settings"]["effectName"] for d_ in d["adjustments"] if d_["identifier"] == "Effect"] 91 | 92 | if len(effect_names) == 0: 93 | print "-- no effect name" 94 | return None, None 95 | 96 | filter_name = "CIPhotoEffect" + effect_names[0] 97 | print "-- filter:", filter_name 98 | 99 | return filter_name, orientation 100 | 101 | def main(): 102 | 103 | parser = argparse.ArgumentParser(description='Restore filters on photos imported from iOS 8 with Image Capture.') 104 | parser.add_argument("-o", "--overwrite", action='store_true', default=False, help="overwrite original photos with filtered photos, remove AAE files") 105 | parser.add_argument("-d", "--dryrun", action='store_true', default=False, help="don't write anything on disk") 106 | parser.add_argument("path", help="path to folder with JPG and AAC files") 107 | args = parser.parse_args() 108 | 109 | aae_files = [ os.path.join(args.path, f) for f in os.listdir(args.path) if f.endswith('.AAE') ] 110 | 111 | for aae in aae_files: 112 | 113 | print "-- reading", aae 114 | 115 | filter_name, orientation = read_aae_file(aae) 116 | if not filter_name: 117 | continue 118 | 119 | name, ext = os.path.splitext(aae) 120 | jpg_in = name + ".JPG" 121 | 122 | if args.overwrite and not args.dryrun: 123 | print "-- removing", aae 124 | os.remove(aae) 125 | 126 | if not os.path.exists(jpg_in): 127 | print "-- missing file:", jpg_in 128 | continue 129 | 130 | jpg_out = jpg_in if args.overwrite else (name + "_" + filter_name + ".JPG") 131 | 132 | apply_cifilter_with_name(filter_name, orientation, jpg_in, jpg_out, args.dryrun) 133 | 134 | del(jpg_in) 135 | del(jpg_out) 136 | 137 | if __name__=='__main__': 138 | main() 139 | --------------------------------------------------------------------------------