Digital cameras are really cool in that whenever you take a picture, it saves a lot more than just the image data. Right in the image file, it adds the date it was taken, the length of the flash, the exposure time, the model of the camera, and much more. If you have an expensive camera with GPS, it will even save the location at which the photo was taken.
Most of this information is stored with EXIF (Exchangeable image file format). This is a pretty much universally adopted standard, so most cameras and photo programs use and save the data. iPhoto and Preview, for example, allow you to view this information easily by opening the Info window.
The problem is that Cocoa does not currently have great support for EXIF. Also, the support it does have is horribly documented. If you are working with photos in a cocoa app, however, it is important to understand and support it. Even if you have no intention to work with the EXIF data at all, if you mess with the image data, you need to make sure you preserve all the metadata when you save the image. Depending on what you do, this may not happen automatically.
To work with EXIF in a cocoa app, you will need to dive into Carbon a bit, but it is really not that bad. Also, note that the code in this article requires Tiger or later. You will need to use another tool to get metadata out of images if you need to support Panther.
Finally, I hear Leopard will have a new framework for working with images, ImageKit. This might make some of this stuff easier to do. I don’t know, as I am not lucky enough to have the beta
Getting Metadata
So, most commonly, you just want to read the metadata from a photo and use in some manner. Magrathea, for example, would see if any photo you tried to import contained GPS EXIF properties, and if so, place the photo on the map automatically for you.
You will need to use the ImageIO framework, so first add ApplicationServices.framework to your project. Then, import it into your file.
Next, you need to create a CGImageSourceIf you are reading the image from disk (or the internet) use:
CGImageSourceRef source = CGImageSourceCreateWithURL( (CFURLRef) aUrl, NULL);
Note that, thanks to Toll-Free Bridging, aUrl can be a NSURL, even though the function takes a CFURLRef. There is no need to convert the URL first. The cast just calms the compiler down so it doesn’t throw a warning.
If you already have the image as a NSImage, you will need to get the data out of the image first. The easiest way to do this is just to use -TIFFRepresentation, and that will give you a NSData object you can use. Then, create a CGImageSourceRef with:
CGImageSourceRef source = CGImageSourceCreateWithData( (CFDataRef) theData, NULL);
Again, Toll-Free Bridging allows you to pass the NSData object right in.Now, you can get a NSDictionary with all the image’s metadata:
NSDictionary* metadata = (NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source,0,NULL);
Toll-Free Bridging works both ways. Yay!
Now, you are pretty much done with Carbon and you can start getting the metadata you want.
All the keys in the metadata dictionary are listed here.
So, say you want to get the date the photo was taken. You would just use the key kCGImagePropertyExifDateTimeOriginal
(Note that all the keys are CFStringRef’s, so you should cast them to an NSString * before using them with NSDictionary)
You might wonder what the format of the date is. Well, just look what the documentation says:kCGImagePropertyExifDateTimeOriginalSpecifies the original date and time.
Thanks! In fact, none of the keys have any documentation on what the hell they actually contain.
Most of it should be pretty self explanatory, but if you are unsure you can check the spec (the good stuff starts on page 17) Core Graphics uses the same terminology for the names of the keys, so it is not that hard to find.Also, something important to note, is that unless the documentation says otherwise, all the properties are returned as CFStringRef’s/NSString’s. That means you will need to do a little extra work if you want to use a property that contains a date or a number.
So, with dates for example, the spec says dates are returned in the format YYYY:MM:DD HH:MM:SSThis is slightly different than the format NSDate expects (YYYY-MM-DD HH:MM:SS ±HHMM). Here is some quick code to convert it to that format (unformattedDateAsString is the NSString returned from the metadata dictionary):
NSDate date =
nil;
*
NSMutableString *unformattedDateAsMutableString = [[unformattedDateAsString
mutableCopy]
autorelease];
//make sure the date stored in the metadata is not nil, and contains a meaningful date
if(unformattedDateAsMutableString && ![unformattedDateAsMutableString
isEqualToString:@""] && ![unformattedDateAsMutableString
isEqualToString:@"0000:00:00 00:00:00"])
{
//the date (not the time) part of the string needs to contain dashes, not colons, for NSDate to read it correctly
[unformattedDateAsMutableString replaceOccurrencesOfString:@":" withString:@"-" options:0 range:NSMakeRange(0, 10]; //the first 10 characters are the date part
//the EXIF spec does not allow the time zone to be saved with the date,
// so we must assume the camera’s clock is set to the same time zone as the computer’s.
[unformattedDateAsMutableString appendString:@" +0000"];
date = [NSDate dateWithString: unformattedDateAsMutableString];
}
And finally, don’t forget to release the metadata dictionary and the image source when you are done.
Setting Metadata
You may also want to add metadata to an image, for instance the program that created the image. Or, in the case of PhotoBook, I add the date a photo was uploaded to facebook in the metadata before adding it to iPhoto.
It is also really important that you preserve any metadata that has already been set for an image when you add new properties.
To start, you go through the same process to get the metadata. Create a CGImageSource using one of the methods above and get the metadata as a dictionary with CGImageSourceCopyPropertiesAtIndex
Now it gets a bit more complicated.First create a mutable copy of the metadata dictionary and make any changes you’d like (add properties, override properties, etc.) You can use all the same keys listed here (just make sure you cast them to a NSString* first).
Note that in order to be useful, all the values you set must be NSStrings (unless otherwise documented) and conform to the format of the spec
Next, you create a CGImageDestination with some path or data where you want the modified image to be written. Use CGImageDestinationAddImageFromSource(destination,source,0, (CFDictionaryRef) metadata); to add the image you are working with to the destination, and override the old metadata with the modified metadata. Finally, you call CGImageDestinationFinalize(destination); to either write to data or write to file (depending on what you initialized the destination with).
Instead of showing little code samples of all of these steps, I think it is better to give one big example that demonstrates everything.
This method takes in a NSDate and a path to an image file, and sets the date as the date digitized in the metadata. It then writes the modified image back to its original path. It also demonstrates how to convert a NSDate into a string that is the right EXIF format. Enjoy!
Available for download here.
- (BOOL)setDateDigitized:(NSDate *)date forPhotoWithURL:(NSURL *)URL;
{
CGImageSourceRef source = CGImageSourceCreateWithURL( (CFURLRef) URL,NULL);
if (!source)
{
NSLog(@"***Could not create image source ***");
return NO;
}
//get all the metadata in the image
NSDictionary *metadata = (NSDictionary *) CGImageSourceCopyPropertiesAtIndex(source,0,NULL);
//make the metadata dictionary mutable so we can add properties to it
NSMutableDictionary *metadataAsMutable = [[metadata mutableCopy]autorelease];
[metadata release];
NSMutableDictionary *EXIFDictionary = [[[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary]mutableCopy]autorelease];
if(!EXIFDictionary)
{
//if the image does not have an EXIF dictionary (not all images do), then create one for us to use
EXIFDictionary = [NSMutableDictionary dictionary];
}
//we need to format the date so it conforms to the EXIF spec and can be read by other apps
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[dateFormatter setDateFormat:@"yyyy:MM:dd HH:mm:ss"]; //the date format for EXIF dates as from http://www.abmt.unibas.ch/dokumente/ExIF.pdf
NSString *EXIFFormattedCreatedDate = [dateFormatter stringFromDate:date]; //use the date formatter to get a string from the date we were passed in the EXIF format
[dateFormatter release];
[EXIFDictionary setObject:EXIFFormattedCreatedDate forKey:(NSString *)kCGImagePropertyExifDateTimeDigitized];
//add our modified EXIF data back into the image’s metadata
[metadataAsMutable setObject:EXIFDictionary forKey:(NSString *)kCGImagePropertyExifDictionary];
CFStringRef UTI = CGImageSourceGetType(source); //this is the type of image (e.g., public.jpeg)
//this will be the data CGImageDestinationRef will write into
NSMutableData *data = [NSMutableData data];
CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)data,UTI,1,NULL);
if(!destination)
{
NSLog(@"***Could not create image destination ***");
return NO;
}
//add the image contained in the image source to the destination, overidding the old metadata with our modified metadata
CGImageDestinationAddImageFromSource(destination,source,0, (CFDictionaryRef) metadataAsMutable);
//tell the destination to write the image data and metadata into our data object.
//It will return false if something goes wrong
BOOL success = NO;
success = CGImageDestinationFinalize(destination);
if(!success)
{
NSLog(@"***Could not create data from image destination ***");
return NO;
}
//now we have the data ready to go, so do whatever you want with it
//here we just write it to disk at the same path we were passed
[data writeToURL:URL atomically:YES];
//cleanup
CFRelease(destination);
CFRelease(source);
return YES;
}