Saturday, June 21, 2008

Geo Tagging images on the iPhone using EXIF (PART II)

In the first part we briefly examined the iphone-exif library API. In this part we will address the wider API, covering the GPS tags and the use of custom handlers to read/write nonstandard data or deal with specific bespoke formats.

GPS Tags
The GPS tags in the EXIF spec are represented as follows:
EXIF_GPSLatitudeRef     
EXIF_GPSLatitude        
EXIF_GPSLongitudeRef      
EXIF_GPSLongitude         
EXIF_GPSAltitudeRef      
EXIF_GPSAltitude           
EXIF_GPSTimeStamp      
EXIF_GPSSatellites    
EXIF_GPSStatus        
EXIF_GPSMeasureMode          
EXIF_GPSDOP              
EXIF_GPSSpeedRef      
EXIF_GPSSpeed            
EXIF_GPSTrackRef         
EXIF_GPSTrack                       
EXIF_GPSImgDirectionRef     
EXIF_GPSImgDirection       
EXIF_GPSMapDatum           
EXIF_GPSDestLatitudeRef       
EXIF_GPSDestLatitude            
EXIF_GPSDestLongitudeRef     
EXIF_GPSDestLongitude     
EXIF_GPSDestBearingRef    
EXIF_GPSDestBearing          
EXIF_GPSDestDistanceRef       
EXIF_GPSDestDistance     



The actual location data is generally represented as a pair of tags with the xxxRef tag representing a modifier for the corresponding value tag. For instance the GPSLongitudeRef and GPSLongitude tag together represent the longitude value.

The longitude and latitude tag data (and indeed all rational number types) in the Exif spec is defined as a block of rational numbers. In this case, one for degrees, minutes and seconds. While this sounds reasonable the spec goes on to say that each rational number is actually stored as a fraction comprising two longs rather than a floating point number. So, the iPhone representation as a single decimal must be converted to and from this block of longs.

The library models this exif format as a GPS location class, which contains three member variables of a Fraction type.
@interface EXFGPSLoc : NSObject {
 EXFraction* degrees;
 EXFraction* minutes;
 EXFfraction* seconds
}
The class provides a method to output the normal string representation in our expected form (122° 1' 50.6316") to the GUI but the fraction structure was adopted internally, rather than say doubles, so that we can round trip the values in any existing EXIF GPS Tags without losing any precision.

While this is fine in principle it can be a little awkward in practice. In order to set the GPS data the main the programming task is to construct an instance of a GPSLoc class and use this as the data type to pass into the exif Library e.g.

// 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]   
}
// end of helper methods

// adding GPS data to the Exif object
 NSMutableArray* locArray = [self createLocArray:location.coordinate.latitude];
 EXFGPSLoc* gpsLoc = [[EXFGPSLoc alloc] init];
 [self populateGPS: gpsLoc :locArray];
 [exifMetaData addTagValue:gpsLoc forKey:[NSNumber numberWithInt:EXIF_GPSLatitude] ];
 [gpsLoc release];
 [locArray release];

In the above code we are converting the  iPhone representation of the location.coordinate into an array representation of degrees, minutes and seconds, then populating the GPSLoc object and setting this into the exifMetaData object. 

The createLocArray method first converts the decimal representation of the latitude from the iphone, which looks something like -122.030731, to an array of NSNumbers. These are then passed to the populateGPS method which converts each NSNumber to a fraction and sets this into the GPSLoc instance. This is a simple process but a little tedious. Note: the conversion of each of these numbers to a fraction in the EXFUtils class uses a standard Euclidean GCD method.


The ref part of the tag pair is dealt with as a String e.g 

 location.coordinate.latitude;
 if (location.coordinate.latitude <0.0){
 ref = @"S";
 }else{
 ref =@"N";
 }
[exifMetaData addTagValue: ref forKey:[NSNumber numberWithInt:EXIF_GPSLatitudeRef] ];

Of course, this means that you have to have some knowledge of the Exif GPS tags, but the library shields you from the nitty-gritty of the underlying byte formats. Generally this is not too much of a problem, once you get the conversion routines written.
Custom Tag Handlers
Mostly, You do not have to deal with custom handlers, as the tags are either Numeric (Single Byte, Short, Long), a String of ASCII or a Rational number (as a fraction). Some tags are specified as arrays of each of these types and the library takes this into account.

The type to pass to the Library can easily be determined by examining the Tag definition for the TagId we are interested in e.g

@interface EXFTag : NSObject {     
    EXFTagId tagId;
    EXFDataType dataType;
    int parentTagId;
    NSString* name;
    BOOL editable;
    int components;  
}

The data type will be one of:

enum EXFDataType {
    FMT_BYTE =       1,
    FMT_STRING  =    2,
    FMT_USHORT  =    3,
    FMT_ULONG   =    4,
    FMT_URATIONAL  = 5,
    FMT_SBYTE      = 6,
    FMT_UNDEFINED  = 7,
    FMT_SSHORT     = 8,
    FMT_SLONG      = 9,
    FMT_SRATIONAL  =10,
    FMT_SINGLE     =11,
    FMT_DOUBLE     =12
};

For the numeric types you must pass a numeric instance that is coercible to that type, the S prefix is for signed values and the single and double are not really present in the spec, but are representable as rational numbers. In order to work out if the data type is an array we examine the components entry in the tag definition, if the value is greater than one then that is how many slots in the array we must pass.

The only real problem is the undefined type. This basically means that the data can be of any type nad is usually used for data that is related to manufacturer data or where the numeric types don't provide the correct formats. In this case we can use an NSData instance and process it ourselves or we can register a handler with the library to do it for us.

The handler protocol is defined as:

@protocol EXFTagHandler
-(void)decodeTag:(NSMutableDictionary*) keyedValues: (NSNumber*) tagId: (CFDataRef*) tagData: (BOOL) bigEndianOrder;

-(int) getSizeOfValue:(id)value;

-(BOOL)supportsValueType:(id) value;

-(void)encodeTag: (NSMutableData*) targetBuffer: (id) tagData:(BOOL) bigEndianOrder;

@optional
-(int) tagFormat;
-(int) parentTagId;
-(BOOL) isEditable;

@end

The decode and encode methods are called by the library when it is constructing an instance from the JPEG bytes or adding the byte representation of the data into the JPEG byte format. The sizeof and supports are used to enable the library to test if an actual value can be supported by the handler and what the encoded byte size of the data would be.

For instance, the internal custom handler that deals with ASCII tags looks like:

 
@implementation EXFASCIIHandler

-(void)decodeTag:(NSMutableDictionary*) keyedValues: (NSNumber*) tagId: (CFDataRef*) tagData: (BOOL) bigEndianOrder{
    
    UInt8* ptr = (UInt8*) CFDataGetBytePtr(*tagData);
    CFIndex byteLength = CFDataGetLength(*tagData);
    NSString* value = [EXFUtils createStringFromBuffer:&ptr: byteLength: NSASCIIStringEncoding];
    // Debug(@"Assigned string %@",value);
    
    [keyedValues setObject: value forKey: tagId];
    
    [value release];
    
}

-(void)encodeTag: (NSMutableData*) targetBuffer: (id) tagData:(BOOL) bigEndianOrder{
    // tag data is an array of NSNumber
    int length = [((NSString*)tagData) lengthOfBytesUsingEncoding:NSASCIIStringEncoding];
    const char* cString = [((NSString*)tagData) cStringUsingEncoding:NSASCIIStringEncoding];
    [targetBuffer appendBytes: cString length:length];
    
    
}

-(BOOL)supportsValueType:(id) value{
    
    if ([value isMemberOfClass:[NSString class]]){
        return TRUE;
    }else{
        return FALSE;
    }
}

-(int) getSizeOfValue:(id)value{
    // value should be a GPS Loc
    if ([value isKindOfClass:[NSString class]]){
        
        return[((NSString*)value) lengthOfBytesUsingEncoding:NSASCIIStringEncoding];
    }else{
        return -1;
    }
}

-(NSString*) description{
    return @"EXF ASCII Handler";
}
@end

The tag handler is registered using the following call prior to any tag values being set.

 
 [exifMetadata addHandler:asciiHandler :EXIF_ExifVersion];
 
Once we have set the data into the exif Object we must obviously be able to retrieve the JPEG data including the tags we have altered. This is achieved in a single call using the following method:

 
-(void) populateImageData: (NSMutableData*) newImageData;

The programmer must provide an NSMutableData instance to the library which will be filled with the bytes of the resulting JPEG with the amended EXIF data. This can then be saved or uploaded to the desired site.







Thursday, June 19, 2008

Geo Tagging images using iphone-exif

The GeoTagging of photos can be really useful. Flickr and Picasa can both use Exif data embedded into an image to generate the Map locations displayed on the site. The only downside is that it is quite tricky to do this directly on the phone.

There are a number of solutions to do this on the PC or by uploading to the site and then tag each image or image folder with  a location, but it is another step when you really want to do it all at once.

I ended up creating a static C/Objective-C library that allows reading/editing and deleting standard Exif tags directly on images on your iPhone. 

This is a quick guide to getting it set up in your project. The Exif spec is a bit tedious and can be found at www.exif.org/Exif2-2.PDF.

Add Into Xcode
Download the library from code.google.com/p/iphone-exif  and unzip into a directory. The zip file contains a static library which you must add into the project as a dependency. then place the header files into your source directory and you are good to go. This is not a dynamic library as the application must ship with the code embedded, it will not be on the iPhone to link with like the framework files you use.

Getting Started
NOTE: The 0.8 version of the library expects a logging variable to be declared - just add it into the AppDelegate .m class like so:

BOOL gLogging = FALSE;


this controls whether internal log statements are echoed to standard out.


The initial task is to parse the Jpeg file data to extrac the Exif data.
This we achieve in the following lines
NSData* theImageData = UIImageRepresentation(anImage,1.0);
EXFJpeg* jpegScanner = [[EXFJpeg alloc] init];
[jpegScanner scanImageData: theImageData];

This will scan the image and extract any Exif data. 

The ExifJpeg class gives us an interface that provides access to the Exif data and the JFIF tags using the properties:

EXFMetaData* exifData = jpegScanner.exifMetaData;
EXFJFIF* jfif = jpegScanner.jfif;

The EXFMetaData class is where most of the work is done. It allows us to 
  • retrieve a tag definition
  • get a list of child tags
  • get a value for a tag
  • remove a tag value
  • add a remove a custom handler
  • get a list of all the tag values.

Retrieving a tag definition
The tag definition details the id, data type, short string name, parent tag, and the number of components (see the Exif Spec for details) it can occupy.

EXFTag* tagDefinition = [exifData tagDefinition: aTagId];

Getting a list of child tags for a parent
NSMutableArray* tagDefinitions = [exifData tagDefinitionsForParent:aTagId];
Getting a value for a tag
The value is an id as it could be a Numeric value, a String, a binary type, or a type provided by a custom tag handler.
id tagValue = [exifData tagValue: aTagId];
Add a tag value
[exifData addTagValue: aValue forKey: aTagId];

Remove a tag value
[exifData removeTagValue:aTagId];
There is obviously a requirement to know what the tagIds are, and these are available in the spec. The EXFConstants.h file also has a defined set of constants to make it easy to see what these are.

Part II
The second post will deal with adding custom handlers and a discussion of the data types, especially around the GPS tag set.