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 objectNSMutableArray* 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 NSNumberint 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 Locif ([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.