Problem setting exif data for an image

IphoneMacosCore GraphicsExifjavax.imageio

Iphone Problem Overview


I'm using the new ImageIO framework in iOS 4.1. I successfully retrieve the exif metadata using the following:

CFDictionaryRef metadataDict = CMGetAttachment(sampleBuffer, kCGImagePropertyExifDictionary , NULL);

Reading it out, it appears valid. Saving an image out works, but there is never any exif data in the image.

    CGImageDestinationRef myImageDest = CGImageDestinationCreateWithURL((CFURLRef) docurl, kUTTypeJPEG, 1, NULL);
    
    // Add the image to the destination using previously saved options. 
    CGImageDestinationAddImage(myImageDest, iref, NULL);
    
    //add back exif
    NSDictionary *props = [NSDictionary dictionaryWithObjectsAndKeys:
                            [NSNumber numberWithFloat:.1], kCGImageDestinationLossyCompressionQuality,
                           metadataDict, kCGImagePropertyExifDictionary, //the exif metadata
                                                        nil];
                           
                          //kCGImagePropertyExifAuxDictionary
    
    CGImageDestinationSetProperties(myImageDest, (CFDictionaryRef) props);
    
    // Finalize the image destination. 
    bool status = CGImageDestinationFinalize(myImageDest);

Iphone Solutions


Solution 1 - Iphone

The following blog post is where I got my answer when I had issues with modifying and saving Exif data Caffeinated Cocoa. This works on iOS.

Here is my test code for writing Exif and GPS data. It pretty much a copy and paste of the code from the above blog. I am using this to write exif data to a captured image.

NSData *jpeg = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer] ;

CGImageSourceRef  source ;
    source = CGImageSourceCreateWithData((CFDataRef)jpeg, NULL);

    //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 = [[[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyExifDictionary]mutableCopy]autorelease];
    NSMutableDictionary *GPSDictionary = [[[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyGPSDictionary]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];
    }
    if(!GPSDictionary) {
        GPSDictionary = [NSMutableDictionary dictionary];
    }
    
    //Setup GPS dict
    
    
    [GPSDictionary setValue:[NSNumber numberWithFloat:_lat] forKey:(NSString*)kCGImagePropertyGPSLatitude];
    [GPSDictionary setValue:[NSNumber numberWithFloat:_lon] forKey:(NSString*)kCGImagePropertyGPSLongitude];
    [GPSDictionary setValue:lat_ref forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
    [GPSDictionary setValue:lon_ref forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
    [GPSDictionary setValue:[NSNumber numberWithFloat:_alt] forKey:(NSString*)kCGImagePropertyGPSAltitude];
    [GPSDictionary setValue:[NSNumber numberWithShort:alt_ref] forKey:(NSString*)kCGImagePropertyGPSAltitudeRef]; 
    [GPSDictionary setValue:[NSNumber numberWithFloat:_heading] forKey:(NSString*)kCGImagePropertyGPSImgDirection];
    [GPSDictionary setValue:[NSString stringWithFormat:@"%c",_headingRef] forKey:(NSString*)kCGImagePropertyGPSImgDirectionRef];
    
    [EXIFDictionary setValue:xml forKey:(NSString *)kCGImagePropertyExifUserComment];
    //add our modified EXIF data back into the image’s metadata
    [metadataAsMutable setObject:EXIFDictionary forKey:(NSString *)kCGImagePropertyExifDictionary];
    [metadataAsMutable setObject:GPSDictionary forKey:(NSString *)kCGImagePropertyGPSDictionary];
    
    CFStringRef UTI = CGImageSourceGetType(source); //this is the type of image (e.g., public.jpeg)
    
    //this will be the data CGImageDestinationRef will write into
    NSMutableData *dest_data = [NSMutableData data];
    
    CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)dest_data,UTI,1,NULL);
    
    if(!destination) {
        NSLog(@"***Could not create image destination ***");
    }
    
    //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 ***");
    }
    
    //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
    [dest_data writeToFile:file atomically:YES];
    
    //cleanup
    
    CFRelease(destination);
    CFRelease(source);

Solution 2 - Iphone

I tried Steve's answer and it works, but I think it's slow for large images because it's duplicating the entire image.

You can set the properties directly on the CMSampleBuffer using CMSetAttachments. Just do this before calling jpegStillImageNSDataRepresentation

CFDictionaryRef metaDict = CMCopyDictionaryOfAttachments(NULL, imageSampleBuffer, kCMAttachmentMode_ShouldPropagate);
CFMutableDictionaryRef mutable = CFDictionaryCreateMutableCopy(NULL, 0, metaDict);

NSMutableDictionary * mutableGPS = [self getGPSDictionaryForLocation:self.myLocation];
CFDictionarySetValue(mutable, kCGImagePropertyGPSDictionary, mutableGPS);

// set the dictionary back to the buffer
CMSetAttachments(imageSampleBuffer, mutable, kCMAttachmentMode_ShouldPropagate);

And the method getGPSDictionaryForLocation: can be found here:

<https://stackoverflow.com/questions/3884060/saving-geotag-info-with-photo-on-ios4-1/5314634#5314634>

Solution 3 - Iphone

SWIFT 5:

It took me a week of scoping around to gather all the pieces into working code.

This will save an UIImage to JPEG temp file with GPS metadata:

let image:UIImage = mImageView.image! // your UIImage

// create filename
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy.MM.dd-HH.mm.ss"
let now = Date()
let date_time = dateFormatter.string(from: now)
let fileName:String = "your_image_"+date_time+".jpg" // name your file the way you want
let temporaryFolder:URL = FileManager.default.temporaryDirectory
let temporaryFileURL:URL = temporaryFolder.appendingPathComponent(fileName)

// save the image to chosen path
let jpeg = image.jpegData(compressionQuality: 0.85)! // set JPG quality here (1.0 is best)
let src = CGImageSourceCreateWithData(jpeg as CFData, nil)!
let uti = CGImageSourceGetType(src)!
let cfPath = CFURLCreateWithFileSystemPath(nil, temporaryFileURL.path as CFString, CFURLPathStyle.cfurlposixPathStyle, false)
let dest = CGImageDestinationCreateWithURL(cfPath!, uti, 1, nil)

// create GPS metadata from current location
let gpsMeta = gCurrentLocation?.exifMetadata() // gCurrentLocation is your CLLocation (exifMetadata is an extension)
let tiffProperties = [
    kCGImagePropertyTIFFMake as String: "Camera vendor",
    kCGImagePropertyTIFFModel as String: "Camera model"
    // --(insert other properties here if required)--
] as CFDictionary
            
let properties = [
    kCGImagePropertyTIFFDictionary as String: tiffProperties,
    kCGImagePropertyGPSDictionary: gpsMeta as Any
    // --(insert other dictionaries here if required)--
] as CFDictionary  

CGImageDestinationAddImageFromSource(dest!, src, 0, properties)
if (CGImageDestinationFinalize(dest!)) {
    print("Saved image with metadata!")
} else {
    print("Error saving image with metadata")
}

And here is the GPS metadata extension (from https://gist.github.com/chefren/8b50652d67c397a825619f83c8dba6d3):

import Foundation
import CoreLocation

extension CLLocation {
    
    func exifMetadata(heading:CLHeading? = nil) -> NSMutableDictionary {
        
        let GPSMetadata = NSMutableDictionary()
        let altitudeRef = Int(self.altitude < 0.0 ? 1 : 0)
        let latitudeRef = self.coordinate.latitude < 0.0 ? "S" : "N"
        let longitudeRef = self.coordinate.longitude < 0.0 ? "W" : "E"
        
        // GPS metadata
        GPSMetadata[(kCGImagePropertyGPSLatitude as String)] = abs(self.coordinate.latitude)
        GPSMetadata[(kCGImagePropertyGPSLongitude as String)] = abs(self.coordinate.longitude)
        GPSMetadata[(kCGImagePropertyGPSLatitudeRef as String)] = latitudeRef
        GPSMetadata[(kCGImagePropertyGPSLongitudeRef as String)] = longitudeRef
        GPSMetadata[(kCGImagePropertyGPSAltitude as String)] = Int(abs(self.altitude))
        GPSMetadata[(kCGImagePropertyGPSAltitudeRef as String)] = altitudeRef
        GPSMetadata[(kCGImagePropertyGPSTimeStamp as String)] = self.timestamp.isoTime()
        GPSMetadata[(kCGImagePropertyGPSDateStamp as String)] = self.timestamp.isoDate()
        GPSMetadata[(kCGImagePropertyGPSVersion as String)] = "2.2.0.0"
        
        if let heading = heading {
            GPSMetadata[(kCGImagePropertyGPSImgDirection as String)] = heading.trueHeading
            GPSMetadata[(kCGImagePropertyGPSImgDirectionRef as String)] = "T"
        }
        
        return GPSMetadata
    }
}

extension Date {
    
    func isoDate() -> String {
        let f = DateFormatter()
        f.timeZone = TimeZone(abbreviation: "UTC")
        f.dateFormat = "yyyy:MM:dd"
        return f.string(from: self)
    }
    
    func isoTime() -> String {
        let f = DateFormatter()
        f.timeZone = TimeZone(abbreviation: "UTC")
        f.dateFormat = "HH:mm:ss.SSSSSS"
        return f.string(from: self)
    }
}

That's it!

You can now use activityViewController to save the temp image (using temporaryFileURL) to photos album, or save it as a file, or share it to other apps, or whatever you want.

Solution 4 - Iphone

I created a MSMutableDictionary category to help save geotag and other metadata to an image. Check out my blog post here:

http://blog.codecropper.com/2011/05/adding-metadata-to-ios-images-the-easy-way/

Solution 5 - Iphone

Since I'm still learning, it took me a while to piece together both Steve and Marty's answers to add metadata to the Data for an image and subsequently save it. Below, I've made a Swift implementation of their answers that does not use ImageIO methods.

Given a CMSampleBuffer sample buffer buffer, some CLLocation location, and using Morty’s suggestion to use CMSetAttachments in order to avoid duplicating the image, we can do the following. The gpsMetadata method extending CLLocation can be found here (also Swift).

if let location = location {
    // Get the existing metadata dictionary (if there is one)
    var metaDict = CMCopyDictionaryOfAttachments(nil, buffer, kCMAttachmentMode_ShouldPropagate) as? Dictionary<String, Any> ?? [:]

    // Append the GPS metadata to the existing metadata
    metaDict[kCGImagePropertyGPSDictionary as String] = location.gpsMetadata()
      
    // Save the new metadata back to the buffer without duplicating any data
    CMSetAttachments(buffer, metaDict as CFDictionary, kCMAttachmentMode_ShouldPropagate)
}
    
// Get JPG image Data from the buffer
guard let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer) else {
    // There was a problem; handle it here
}
    

At this point, you can write the image data to a file or use the Photos API to save the image to the Camera Roll (using PHAssetCreationRequest's addResource:with:data:options for iOS 9+, or for older iOS versions by writing the imageData to a temporary file and then calling PHAssetChangeRequest.creationRequestForAssetFromImage:atFileURL). Note that ALAssertLibrary is deprecated for iOS 9. I provide more implementation details in an answer here.

Solution 6 - Iphone

Translated Steve McFarlin's answer to Swift & Wrapped it inside a class.

class GeoTagImage {
  
  /// Writes GPS data into the meta data.
  /// - Parameters:
  ///   - data: Coordinate meta data will be written to the copy of this data.
  ///   - coordinate: Cooordinates to write to meta data.
  static func mark(_ data: Data, with coordinate: Coordinate) -> Data {
    var source: CGImageSource? = nil
    source = CGImageSourceCreateWithData((data as CFData?)!, nil)
    // Get all the metadata in the image
    let metadata = CGImageSourceCopyPropertiesAtIndex(source!, 0, nil) as? [AnyHashable: Any]
    // Make the metadata dictionary mutable so we can add properties to it
    var metadataAsMutable = metadata
    var EXIFDictionary = (metadataAsMutable?[(kCGImagePropertyExifDictionary as String)]) as? [AnyHashable: Any]
    var GPSDictionary = (metadataAsMutable?[(kCGImagePropertyGPSDictionary as String)]) as? [AnyHashable: Any]
    
    if !(EXIFDictionary != nil) {
      // If the image does not have an EXIF dictionary (not all images do), then create one.
      EXIFDictionary = [:]
    }
    if !(GPSDictionary != nil) {
      GPSDictionary = [:]
    }
    
    // add coordinates in the GPS Dictionary
    GPSDictionary![(kCGImagePropertyGPSLatitude as String)] = coordinate.latitude
    GPSDictionary![(kCGImagePropertyGPSLongitude as String)] = coordinate.longitude
    EXIFDictionary![(kCGImagePropertyExifUserComment as String)] = "Raw Image"
    
    // Add our modified EXIF data back into the image’s metadata
    metadataAsMutable!.updateValue(GPSDictionary!, forKey: kCGImagePropertyGPSDictionary)
    metadataAsMutable!.updateValue(EXIFDictionary!, forKey: kCGImagePropertyExifDictionary)
    
    // This is the type of image (e.g., public.jpeg)
    let UTI: CFString = CGImageSourceGetType(source!)!
    
    // This will be the data CGImageDestinationRef will write into
    let dest_data = NSMutableData()
    let destination: CGImageDestination = CGImageDestinationCreateWithData(dest_data as CFMutableData, UTI, 1, nil)!
    // Add the image contained in the image source to the destination, overidding the old metadata with our modified metadata
    CGImageDestinationAddImageFromSource(destination, source!, 0, (metadataAsMutable as CFDictionary?))
    
    // Tells the destination to write the image data and metadata into our data object.
    // It will return false if something goes wrong
    _ = CGImageDestinationFinalize(destination)
    
    return (dest_data as Data)
  }
  
  /// Prints the Meta Data from the Data.
  /// - Parameter data: Meta data will be printed of this object.
  static func logMetaData(from data: Data) {
    if let imageSource = CGImageSourceCreateWithData(data as CFData, nil) {
      let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
      if let dict = imageProperties as? [String: Any] {
        print(dict)
      }
    }
  }
}

Solution 7 - Iphone

A piece involves creating the GPS metadata dictionary for the EXIF data. Here's a CLLocation category to do just that:

https://gist.github.com/phildow/6043486

Solution 8 - Iphone

[Revisiting this answer because of downvotes without explanations.]

Apple has updated their article addressing this issue (Technical Q&A QA1622). If you're using an older version of Xcode, you may still have the article that says, more or less, tough luck, you can't do this without low-level parsing of the image data.

The updated version is here:

https://developer.apple.com/library/ios/#qa/qa1622/_index.html

I adapted the code there as follows:

- (void) saveImage:(UIImage *)imageToSave withInfo:(NSDictionary *)info
{
    // Get the image metadata (EXIF & TIFF)
    NSMutableDictionary * imageMetadata = [[info objectForKey:UIImagePickerControllerMediaMetadata] mutableCopy];

    // add (fake) GPS data
    CLLocationCoordinate2D coordSF = CLLocationCoordinate2DMake(37.732711,-122.45224);

    // arbitrary altitude and accuracy
    double altitudeSF = 15.0;
    double accuracyHorizontal = 1.0;
    double accuracyVertical = 1.0;
    NSDate * nowDate = [NSDate date];
    // create CLLocation for image
    CLLocation * loc = [[CLLocation alloc] initWithCoordinate:coordSF altitude:altitudeSF horizontalAccuracy:accuracyHorizontal verticalAccuracy:accuracyVertical timestamp:nowDate];

    // this is in case we try to acquire actual location instead of faking it with the code right above
    if ( loc ) {
        [imageMetadata setObject:[self gpsDictionaryForLocation:loc] forKey:(NSString*)kCGImagePropertyGPSDictionary];
    }

    // Get the assets library
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];

    // create a completion block for when we process the image
   ALAssetsLibraryWriteImageCompletionBlock imageWriteCompletionBlock =
    ^(NSURL *newURL, NSError *error) {
        if (error) {
            NSLog( @"Error writing image with metadata to Photo Library: %@", error );
        } else {
            NSLog( @"Wrote image %@ with metadata %@ to Photo Library",newURL,imageMetadata);
        }
    };

    // Save the new image to the Camera Roll, using the completion block defined just above
    [library writeImageToSavedPhotosAlbum:[imageToSave CGImage]
                                 metadata:imageMetadata
                          completionBlock:imageWriteCompletionBlock];
}

and I call this from

imagePickerController:didFinishPickingMediaWithInfo:

which is the delegate method for the image picker. (That's where I put the logic to see if there is an image to save, etc.)

For completeness, here's the helper method to get the GPS data as a dictionary:

- (NSDictionary *) gpsDictionaryForLocation:(CLLocation *)location
{
    CLLocationDegrees exifLatitude  = location.coordinate.latitude;
    CLLocationDegrees exifLongitude = location.coordinate.longitude;

    NSString * latRef;
    NSString * longRef;
    if (exifLatitude < 0.0) {
        exifLatitude = exifLatitude * -1.0f;
        latRef = @"S";
    } else {
        latRef = @"N";
    }

    if (exifLongitude < 0.0) {
        exifLongitude = exifLongitude * -1.0f;
        longRef = @"W";
    } else {
        longRef = @"E";
    }

    NSMutableDictionary *locDict = [[NSMutableDictionary alloc] init];

    // requires ImageIO
    [locDict setObject:location.timestamp forKey:(NSString*)kCGImagePropertyGPSTimeStamp];
    [locDict setObject:latRef forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
    [locDict setObject:[NSNumber numberWithFloat:exifLatitude] forKey:(NSString *)kCGImagePropertyGPSLatitude];
    [locDict setObject:longRef forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
    [locDict setObject:[NSNumber numberWithFloat:exifLongitude] forKey:(NSString *)kCGImagePropertyGPSLongitude];
    [locDict setObject:[NSNumber numberWithFloat:location.horizontalAccuracy] forKey:(NSString*)kCGImagePropertyGPSDOP];
    [locDict setObject:[NSNumber numberWithFloat:location.altitude] forKey:(NSString*)kCGImagePropertyGPSAltitude];

    return locDict;

}

See also https://stackoverflow.com/questions/7965299/write-uiimage-along-with-metadata-exif-gps-tiff-in-iphones-photo-library/11038316#11038316

Solution 9 - Iphone

In this way I set EXIF data, also you can compress photo if need it, this solved the issue for me: Hope it helps

// Get your image.
NSURL *url = @"http://somewebsite.com/path/to/some/image.jpg";
UIImage *loImgPhoto = [NSData dataWithContentsOfURL:url];

// Get your metadata (includes the EXIF data).
CGImageSourceRef loImageOriginalSource = CGImageSourceCreateWithData(( CFDataRef) loDataFotoOriginal, NULL);
NSDictionary *loDicMetadata = (__bridge NSDictionary *) CGImageSourceCopyPropertiesAtIndex(loImageOriginalSource, 0, NULL);
                          
// Set your compression quality (0.0 to 1.0).
NSMutableDictionary *loDicMutableMetadata = [loDicMetadata mutableCopy];
[loDicMutableMetadata setObject:@(lfCompressionQualityValue) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
                          
// Create an image destination.
NSMutableData *loNewImageDataWithExif = [NSMutableData data];
CGImageDestinationRef loImgDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)loNewImageDataWithExif, CGImageSourceGetType(loImageOriginalSource), 1, NULL);
                          
                         
// Add your image to the destination.
CGImageDestinationAddImage(loImgDestination, loImgPhoto.CGImage, (__bridge CFDictionaryRef) loDicMutableMetadata);
                          
// Finalize the destination.
if (CGImageDestinationFinalize(loImgDestination))
   {
       NSLog(@"Successful image creation.");                   
       // process the image rendering, adjustment data creation and finalize the asset edit.
    
                          
       //Upload photo with EXIF metadata
       [self myUploadMethod:loNewImageDataWithExif];
                          
    }
    else
    {
          NSLog(@"Error -> failed to finalize the image.");                         
    }
                                              
CFRelease(loImageOriginalSource);
CFRelease(loImgDestination);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionakaruView Question on Stackoverflow
Solution 1 - IphoneSteve McFarlinView Answer on Stackoverflow
Solution 2 - IphoneMortyView Answer on Stackoverflow
Solution 3 - IphonelenoohView Answer on Stackoverflow
Solution 4 - IphoneGustavo AmbrozioView Answer on Stackoverflow
Solution 5 - IphoneUndreaView Answer on Stackoverflow
Solution 6 - IphoneAnirudha MahaleView Answer on Stackoverflow
Solution 7 - IphonePhilipView Answer on Stackoverflow
Solution 8 - IphoneCode RoadieView Answer on Stackoverflow
Solution 9 - IphoneCGRView Answer on Stackoverflow