z2k
z2k

Reputation: 10380

iOS 8 Photos framework. Access photo metadata

I'm looking at replacing ALAssetsLibrary with Photos framework in my app.

I can retrieve photos, collections, and asset sources just fine (even write them back out), but don't see anywhere to access the metadata of the photos (the dictionaries such as {Exif}, {TIFF}, {GPS}, etc...).

ALAssetsLibrary has a way. UIImagePickerController has a way. Photos must have a way too.

I see that PHAsset has a location property which will do for the GPS dictionary, but I'm looking to access all of the metadata which include faces, orientation, exposure, ISO, and tons more.

Currently apple is at beta 2. Perhaps there are more APIs to come ?

UPDATE

There is no official way to do this using only Photos APIs.

However you can read the metadata after you download the image data. There are a couple of methods to do this using either PHImageManager or PHContentEditingInput.

The PHContentEditingInput method required less code and doesn't require you to import ImageIO. I've wrapped it up in a PHAsset category.

Upvotes: 27

Views: 31989

Answers (6)

andrewchan2022
andrewchan2022

Reputation: 5290

I prefer not to CIImage solution, but to ImageIO solution:

func imageAndMetadataFromImageData(data: NSData)-> (UIImage?,[String: Any]?) {
    let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse]
    if let imgSrc = CGImageSourceCreateWithData(data, options as CFDictionary) {
        let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options as CFDictionary) as! [String: Any]
        //print(metadata)
        // let image = UIImage(cgImage: imgSrc as! CGImage)
        let image = UIImage(data: data as Data)
        return (image, metadata)
    }
    return (nil, nil)
}

below is code to get data from PHAseet

func getImageAndMeta(asset: PHAsset){
    let options = PHImageRequestOptions()
    options.isSynchronous = true
    options.resizeMode = .none
    options.isNetworkAccessAllowed = false
    options.version = .current
    var image: UIImage? = nil
    var meta:[String:Any]? = nil
    _ = PHCachingImageManager().requestImageData(for: asset, options: options) { (imageData, dataUTI, orientation, info) in
        if let data = imageData {
            (image, meta) = imageAndMetadataFromImageData(data: data as NSData)
            //image = UIImage(data: data)
        }
    }
    // here to return image and meta
}

Upvotes: 7

Jordan H
Jordan H

Reputation: 55665

If you request a content editing input, you can get the full image as a CIImage, and CIImage has a property titled properties which is a dictionary containing the image metadata.

Sample Swift Code:

let options = PHContentEditingInputRequestOptions()
options.networkAccessAllowed = true //download asset metadata from iCloud if needed

asset.requestContentEditingInputWithOptions(options) { (contentEditingInput: PHContentEditingInput?, _) -> Void in
    let fullImage = CIImage(contentsOfURL: contentEditingInput!.fullSizeImageURL)

    print(fullImage.properties)
}

Sample Objective-C Code:

PHContentEditingInputRequestOptions *options = [[PHContentEditingInputRequestOptions alloc] init];
options.networkAccessAllowed = YES; //download asset metadata from iCloud if needed

[asset requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
    CIImage *fullImage = [CIImage imageWithContentsOfURL:contentEditingInput.fullSizeImageURL];

    NSLog(@"%@", fullImage.properties.description);
}];

You'll get the desired {Exif}, {TIFF}, {GPS}, etc dictionaries.

Upvotes: 38

kev8484
kev8484

Reputation: 648

You can modify the PHAsset (e.g. adding location metadata) using Photos Framework and the UIImagePickerControllerDelegate method. No overhead from third party libraries, no duplicate photos created. Works for iOS 8.0+

In the didFinishPickingMediaWithInfo delegate method, call UIImageWriteToSavedPhotosAlbum to first save the image. This will also create the PHAsset whose EXIF GPS data we will modify:

func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {

    if let myImage = info[UIImagePickerControllerOriginalImage] as? UIImage  {

        UIImageWriteToSavedPhotosAlbum(myImage, self, Selector("image:didFinishSavingWithError:contextInfo:"), nil)
    }    
}

The completion selector function will run after the save completes or fails with error. In the callback, fetch the newly created PHAsset. Then, create a PHAssetChangeRequest to modify the location metadata.

func image(image: UIImage, didFinishSavingWithError: NSErrorPointer, contextInfo:UnsafePointer<Void>)       {

    if (didFinishSavingWithError != nil) {
        print("Error saving photo: \(didFinishSavingWithError)")
    } else {
        print("Successfully saved photo, will make request to update asset metadata")

        // fetch the most recent image asset:
        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
        let fetchResult = PHAsset.fetchAssetsWithMediaType(PHAssetMediaType.Image, options: fetchOptions)

        // get the asset we want to modify from results:
        let lastImageAsset = fetchResult.lastObject as! PHAsset

        // create CLLocation from lat/long coords:
        // (could fetch from LocationManager if needed)
        let coordinate = CLLocationCoordinate2DMake(myLatitude, myLongitude)
        let nowDate = NSDate()
        // I add some defaults for time/altitude/accuracies:
        let myLocation = CLLocation(coordinate: coordinate, altitude: 0.0, horizontalAccuracy: 1.0, verticalAccuracy: 1.0, timestamp: nowDate)

        // make change request:
        PHPhotoLibrary.sharedPhotoLibrary().performChanges({

            // modify existing asset:
            let assetChangeRequest = PHAssetChangeRequest(forAsset: lastImageAsset)
            assetChangeRequest.location = myLocation

            }, completionHandler: {
                (success:Bool, error:NSError?) -> Void in

                if (success) {
                    print("Succesfully saved metadata to asset")
                    print("location metadata = \(myLocation)")
                } else {
                    print("Failed to save metadata to asset with error: \(error!)")
}

Upvotes: 0

Karthik
Karthik

Reputation: 1396

Better solution i found and worked well for me is:

[[PHImageManager defaultManager] requestImageDataForAsset:photoAsset
                                                  options:reqOptions
                                            resultHandler:
         ^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
             CIImage* ciImage = [CIImage imageWithData:imageData];
             DLog(@"Metadata : %@", ciImage.properties);
         }];

Upvotes: 6

z2k
z2k

Reputation: 10380

I thought I'd share some code to read the metadata using the ImageIO framework in conjunction with Photos framework. You must request the image data using a PHCachingImageManager.

@property (strong) PHCachingImageManager *imageManager;

Request the image and use it's data to create a metadata dictionary

-(void)metadataReader{
    PHFetchResult *result = [PHAsset fetchAssetsInAssetCollection:self.myAssetCollection options:nil];
    [result enumerateObjectsAtIndexes:[NSIndexSet indexSetWithIndex:myIndex] options:NSEnumerationConcurrent usingBlock:^(PHAsset *asset, NSUInteger idx, BOOL *stop) {
        [self.imageManager requestImageDataForAsset:asset options:nil resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
            NSDictionary *metadata = [self metadataFromImageData:imageData];
                           NSLog(@"Metadata: %@", metadata.description);
            NSDictionary *gpsDictionary = metadata[(NSString*)kCGImagePropertyGPSDictionary];
            if(gpsDictionary){
                NSLog(@"GPS: %@", gpsDictionary.description);
            }
            NSDictionary *exifDictionary = metadata[(NSString*)kCGImagePropertyExifDictionary];
            if(exifDictionary){
                NSLog(@"EXIF: %@", exifDictionary.description);
            }

            UIImage *image = [UIImage imageWithData:imageData scale:[UIScreen mainScreen].scale];
            // assign image where ever you need...
        }];

    }];
}

Convert NSData to metadata

-(NSDictionary*)metadataFromImageData:(NSData*)imageData{
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)(imageData), NULL);
    if (imageSource) {
        NSDictionary *options = @{(NSString *)kCGImageSourceShouldCache : [NSNumber numberWithBool:NO]};
        CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
        if (imageProperties) {
            NSDictionary *metadata = (__bridge NSDictionary *)imageProperties;
            CFRelease(imageProperties);
            CFRelease(imageSource);
            return metadata;
        }
        CFRelease(imageSource);
    }

    NSLog(@"Can't read metadata");
    return nil;
}

This has the overhead of grabbing the image, so it's not nearly as fast as enumerating your assets or collections, but it's something at least.

Upvotes: 8

holtmann
holtmann

Reputation: 6293

PhotoKit limits the access to metadata to the properties of PHAsset (location, creationDate, favorite, hidden, modificatonDate, pixelWidth, pixelHeight...). The reason (I suspect) is that due to the introduction of iCloud PhotoLibrary the images may not be on the device. Therefore the whole metadata is not available. The only way to get full EXIF/IPTC metadata is to first download the original image (if not available) from iCloud and then use ImageIO to extract its metadata.

Upvotes: 5

Related Questions