UIImagePickerController and extracting EXIF data from existing photos

Viewed 47686

It's well known that UIImagePickerController doesn't return the metadata of the photo after selection. However, a couple of apps in the app store (Mobile Fotos, PixelPipe) seem to be able to read the original files and the EXIF data stored within them, enabling the app to extract the geodata from the selected photo.

They seem to do this by reading the original file from the /private/var/mobile/Media/DCIM/100APPLE/ folder and running it through an EXIF library.

However, I can't work out a way of matching a photo returned from the UIImagePickerController to a file on disk. I've explored file sizes, but the original file is a JPEG, whilst the returned image is a raw UIImage, making it impossible to know the file size of the image that was selected.

I'm considering making a table of hashes and matching against the first x pixels of each image. This seems a bit over the top though, and probably quite slow.

Any suggestions?

18 Answers

Have you took a look at this exif iPhone library?

http://code.google.com/p/iphone-exif/

Gonna try it on my side. I'd like to get the GPS (geotags) coordinates from the picture that has been taken with the UIImagePickerController :/

After a deeper look, this library seems to take NSData info as an input and the UIImagePickerController returns a UIImage after taking a snapshot. In theory, if we use the selected from the UIkit category for UIImage

NSData * UIImageJPEGRepresentation (
   UIImage *image,
   CGFloat compressionQuality
);

Then we can convert the UIImage into a NSData instance and then use it with the iPhone exif library.

UPDATE:
I gave a test to the library mentioned above and it seems to work. However because of my limited knwoledge about the EXIF format and the lack of high level API in the library, I don't manage to get the values for the EXIF tags. Here's my code in case any of you can go further :


#import "EXFJpeg.h"

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo {
    NSLog(@"image picked %@ with info %@", image, editingInfo);
    NSData* jpegData = UIImageJPEGRepresentation (image,0.5);
    EXFJpeg* jpegScanner = [[EXFJpeg alloc] init];
    [jpegScanner scanImageData: jpegData];
    EXFMetaData* exifData = jpegScanner.exifMetaData;
    EXFJFIF* jfif = jpegScanner.jfif;
    EXFTag* tagDefinition = [exifData tagDefinition: [NSNumber numberWithInt:EXIF_DateTime]];
    //EXFTag* latitudeDef = [exifData tagDefinition: [NSNumber numberWithInt:EXIF_GPSLatitude]];
    //EXFTag* longitudeDef = [exifData tagDefinition: [NSNumber numberWithInt:EXIF_GPSLongitude]];
    id latitudeValue = [exifData tagValue:[NSNumber numberWithInt:EXIF_GPSLatitude]];
    id longitudeValue = [exifData tagValue:[NSNumber numberWithInt:EXIF_GPSLongitude]];
    id datetime = [exifData tagValue:[NSNumber numberWithInt:EXIF_DateTime]];
    id t = [exifData tagValue:[NSNumber numberWithInt:EXIF_Model]];
....
....

The retrieving of tags definition is OK, but all tag values returns nil :(

In case you want to give a try to the library, you need to define a global variable to get it running (as explained in the doc but hum.. :/)

BOOL gLogging = FALSE;

UPDATE 2
Answer here : iPhone - access location information from a photo A UIImage does not encapsulate the meta information, so we're stuck : for sure, no EXIF info will be given through this interface.

FINAL UPDATE
Ok I managed to get it working, at least to geotag properly pictures returned by the picker.
Before triggering the UIImagePickerController, it's up to you to use the CLLocationManager to retrieve the current CLocation
Once you have it, you can use this method that uses exif-iPhone library to geotag the UIImage from the CLLocation :


-(NSData*) geotagImage:(UIImage*)image withLocation:(CLLocation*)imageLocation {
    NSData* jpegData =  UIImageJPEGRepresentation(image, 0.8);
    EXFJpeg* jpegScanner = [[EXFJpeg alloc] init];
    [jpegScanner scanImageData: jpegData];
    EXFMetaData* exifMetaData = jpegScanner.exifMetaData;
    // end of helper methods 
    // adding GPS data to the Exif object 
    NSMutableArray* locArray = [self createLocArray:imageLocation.coordinate.latitude]; 
    EXFGPSLoc* gpsLoc = [[EXFGPSLoc alloc] init]; 
    [self populateGPS: gpsLoc :locArray]; 
    [exifMetaData addTagValue:gpsLoc forKey:[NSNumber numberWithInt:EXIF_GPSLatitude] ]; 
    [gpsLoc release]; 
    [locArray release]; 
    locArray = [self createLocArray:imageLocation.coordinate.longitude]; 
    gpsLoc = [[EXFGPSLoc alloc] init]; 
    [self populateGPS: gpsLoc :locArray]; 
    [exifMetaData addTagValue:gpsLoc forKey:[NSNumber numberWithInt:EXIF_GPSLongitude] ]; 
    [gpsLoc release]; 
    [locArray release];
    NSString* ref;
    if (imageLocation.coordinate.latitude <0.0)
        ref = @"S"; 
    else
        ref =@"N"; 
    [exifMetaData addTagValue: ref forKey:[NSNumber numberWithInt:EXIF_GPSLatitudeRef] ]; 
    if (imageLocation.coordinate.longitude <0.0)
        ref = @"W"; 
    else
        ref =@"E"; 
    [exifMetaData addTagValue: ref forKey:[NSNumber numberWithInt:EXIF_GPSLongitudeRef] ]; 
    NSMutableData* taggedJpegData = [[NSMutableData alloc] init];
    [jpegScanner populateImageData:taggedJpegData];
    [jpegScanner release];
    return [taggedJpegData autorelease];
}

// Helper methods for location conversion -(NSMutableArray*) createLocArray:(double) val{ val = fabs(val); NSMutableArray* array = [[NSMutableArray alloc] init]; double deg = (int)val; [array addObject:[NSNumber numberWithDouble:deg]]; val = val - deg; val = val*60; double minutes = (int) val; [array addObject:[NSNumber numberWithDouble:minutes]]; val = val - minutes; val = val*60; double seconds = val; [array addObject:[NSNumber numberWithDouble:seconds]]; return array; } -(void) populateGPS:(EXFGPSLoc* ) gpsLoc :(NSArray*) locArray{ long numDenumArray[2]; long* arrPtr = numDenumArray; [EXFUtils convertRationalToFraction:&arrPtr :[locArray objectAtIndex:0]]; EXFraction* fract = [[EXFraction alloc] initWith:numDenumArray[0]:numDenumArray[1]]; gpsLoc.degrees = fract; [fract release]; [EXFUtils convertRationalToFraction:&arrPtr :[locArray objectAtIndex:1]]; fract = [[EXFraction alloc] initWith:numDenumArray[0] :numDenumArray[1]]; gpsLoc.minutes = fract; [fract release]; [EXFUtils convertRationalToFraction:&arrPtr :[locArray objectAtIndex:2]]; fract = [[EXFraction alloc] initWith:numDenumArray[0] :numDenumArray[1]]; gpsLoc.seconds = fract; [fract release]; }

I spend a while working on this as well for an application I was contracted to build. Basically as the API currently stands it is not possible. The basic problem is the UIImage class STRIPS all EXIF data except for the orientation out. Also the function to save to the camera roll strips this data out. So basically the only way to grab and maintain any extra EXIF data is to save it in a private "camera roll" in your application. I have filed this bug with apple as well and emphasized the need to the app reviewer reps we've been in contact with. Hopefully someday they'll add it in.. Otherwise it makes having GEO tagging completely useless as it only works in the "stock" camera application.

NOTE Some applications on the app store hack around this. By, what I have found, directly accessing the camera roll and SAVING photos straight to it to save GEO data. However this only works with the camera roll/saved photos and NOT the rest of the photo library. The photos "synced" to your phone from your computer have all EXIF data except for orientation stripped.

I still can't understand why those applications were approved (heck they even DELETE from the camera roll) and our application which does none of that is still being held back.

For iOS 10 - Swift 3

The picker's callback has an info dict where there is a key with metadata: UIImagePickerControllerMediaMetadata

Image picker metadata example

The naughty way to do this is to traverse the UIImagePickerViewController's views and pick out the selected image in the delegate callback.

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {

  id thumbnailView = [[[[[[[[[[picker.view subviews] 
                  objectAtIndex:0] subviews]
                  objectAtIndex:0] subviews]
                objectAtIndex:0] subviews]
                objectAtIndex:0] subviews]
              objectAtIndex:0];

  NSString *fullSizePath = [[[thumbnailView selectedPhoto] fileGroup] pathForFullSizeImage];
  NSString *thumbnailPath = [[[thumbnailView selectedPhoto] fileGroup] pathForThumbnailFile];

  NSLog(@"%@ and %@", fullSizePath, thumbnailPath);

}

That will give you the path to the full size image, which you can then open with an EXIF library of your choice.

But, this calls a Private API and these method names will be detected by Apple if you submit this app. So don't do this, OK?

Related