CFDictionary updates to PHPhotoLibrary kCGImagePropertyExifDictionary not Taking

1.4k views Asked by At

I am trying to add/modify the image metadata when I add an image asset to an album in an iOS 8.4 Photos collection. The following code successfully adds the GPS dictionary but does not add or change the EXIF data (I have examined the image header and used EXIF tools to confirm). I have tried two different methods (see altSetMyImageInfo) with the same non-result. What am I missing?

 @property (nonatomic) CLLocation *location; 
. . .
[self.stillImageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:^( CMSampleBufferRef imageDataSampleBuffer, NSError *error ) {
    if ( imageDataSampleBuffer ) {
        NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        CFDictionaryRef tempDict = CMCopyDictionaryOfAttachments(NULL, imageDataSampleBuffer, kCMAttachmentMode_ShouldPropagate);
        CFMutableDictionaryRef theDict = CFDictionaryCreateMutableCopy(NULL, 0, tempDict);

        [self setMyImageInfo:theDict];
        [self setMyLocation:theDict];

        CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
        CFStringRef UTI = CGImageSourceGetType(source);                 
        NSMutableData *newImageData = [NSMutableData data];
        CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)newImageData,UTI,1,NULL);
        BOOL success = CGImageDestinationFinalize(destination);
        CFRelease(destination);
        CFRelease(source);
    }
}
. . .
if ( [PHAssetResourceCreationOptions class] ) {
    [[PHAssetCreationRequest creationRequestForAsset] addResourceWithType:PHAssetResourceTypeFullSizePhoto data:newImageData options:nil]; 
}
else {
     NSError *error = nil;
     [newImageData writeToURL:temporaryFileURL options:NSDataWritingAtomic error:&error];
     if ( error ) {
        NSLog( @"Error: %@", error );
     }
     else {
          PHAssetChangeRequest *newImageRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:temporaryFileURL];
          PHAssetCollectionChangeRequest *albumChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:theAlbum];
          [albumChangeRequest addAssets:@[newImageRequest.placeholderForCreatedAsset]];
}
. . .             
- (void) setMyImageInfo:(CFMutableDictionaryRef)theDict
{
NSMutableDictionary *exif = [NSMutableDictionary dictionary];
[exif setObject:@"2.2.2" forKey:(NSString *)kCGImagePropertyExifVersion];
[exif setObject:@“MyIdentity” forKey:(NSString *)kCGImagePropertyExifImageUniqueID];
[exif setObject:@"I hope this works!" forKey:(NSString *)kCGImagePropertyExifUserComment];

CFDictionarySetValue(theDict, kCGImagePropertyExifDictionary, (__bridge void *)exif);    
}
- (void) setMyLocation:(CFMutableDictionaryRef)theDict
{
NSMutableDictionary *gps = [NSMutableDictionary dictionary];
[gps setObject:@"2.2.0.0" forKey:(NSString *)kCGImagePropertyGPSVersion];
NSDateFormatter *formatter  = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"HH:mm:ss.SSSSSS"];
[formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
[gps setObject:[formatter stringFromDate:self.location.timestamp] forKey:(NSString *)kCGImagePropertyGPSTimeStamp];
[formatter setDateFormat:@"yyyy:MM:dd"];
[gps setObject:[formatter stringFromDate:self.location.timestamp] forKey:(NSString *)kCGImagePropertyGPSDateStamp];
CGFloat latitude = self.location.coordinate.latitude;
if (latitude < 0) {
    latitude = -latitude;
    [gps setObject:@"S" forKey:(NSString *)kCGImagePropertyGPSLatitudeRef];
} else {
    [gps setObject:@"N" forKey:(NSString *)kCGImagePropertyGPSLatitudeRef];
}
[gps setObject:[NSNumber numberWithFloat:latitude] forKey:(NSString *)kCGImagePropertyGPSLatitude];
CGFloat longitude = self.location.coordinate.longitude;
if (longitude < 0) {
    longitude = -longitude;
    [gps setObject:@"W" forKey:(NSString *)kCGImagePropertyGPSLongitudeRef];
} else {
    [gps setObject:@"E" forKey:(NSString *)kCGImagePropertyGPSLongitudeRef];
}
[gps setObject:[NSNumber numberWithFloat:longitude] forKey:(NSString *)kCGImagePropertyGPSLongitude]; 
CGFloat altitude = self.location.altitude;
if (!isnan(altitude)){
    if (altitude < 0) {
        altitude = -altitude;
        [gps setObject:@"1" forKey:(NSString *)kCGImagePropertyGPSAltitudeRef];
    } else {
        [gps setObject:@"0" forKey:(NSString *)kCGImagePropertyGPSAltitudeRef];
    }
    [gps setObject:[NSNumber numberWithFloat:altitude] forKey:(NSString *)kCGImagePropertyGPSAltitude];
}    
if (self.location.speed >= 0){
    [gps setObject:@"K" forKey:(NSString *)kCGImagePropertyGPSSpeedRef];
    [gps setObject:[NSNumber numberWithFloat:self.location.speed*3.6] forKey:(NSString *)kCGImagePropertyGPSSpeed];
}
if (self.location.course >= 0){
    [gps setObject:@"T" forKey:(NSString *)kCGImagePropertyGPSTrackRef];
    [gps setObject:[NSNumber numberWithFloat:self.location.course] forKey:(NSString *)kCGImagePropertyGPSTrack];
}
CFDictionarySetValue(theDict, kCGImagePropertyGPSDictionary, (__bridge void *)gps);
}

- (void) altSetMyImageInfo:(CFMutableDictionaryRef)theDict
{
CFStringRef imageUniqueID = CFStringCreateWithCString(NULL, [@“MyImageID” cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
CFDictionarySetValue(theDict, kCGImagePropertyExifImageUniqueID, imageUniqueID);
CFRelease (imageUniqueID);    
CFStringRef userComment = CFStringCreateWithCString(NULL, [@"I hope this works!" cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
CFDictionarySetValue(theDict, kCGImagePropertyExifUserComment, userComment);
CFRelease (userComment);
}
1

There are 1 answers

0
user2912894 On

The answer to this question involves a large number of things. A minor example, "CGImageSourceRef source" was missing. The most important thing was that I could not put the problem into the context of the project due to NDA restrictions from "you all know who" (you better know because I cannot say). Instead of parsing the question, I have posted the answer in code. If you insert this code into a certain example program of "you know who" (I cannot say the example name, but maCVA might offer you a clue to search their developer library). I have made it so that most of the code will sit at the end of a certain large source file, and the other lines have reference line numbers (start inserting at the top). The only other change is that the header file gets CLLocationManagerDelegate added to the @interface. You might want to add location-services to the plist, too.

I have included debug code to see the metadata in a log file. The only question remaining is whether to use CFDictionarySetValue or "setObject: forKey:" to assign the dictionary values. I used the former for the EXIF/TIFF data and the latter for the GPS data but included the "setObject: forKey:" code in the EXIF/TIFF method as comments. Both work. The "setObject: forKey:" is more terse. Any opinions?

@import CoreLocation; // MAS: for GPS (line 11)
@property (strong, nonatomic) CLLocationManager *locationManager;  // MAS: for image metadata (line 42)
@property (nonatomic) CLLocation *location; //  MAS: get it early and save it here (line 43)
@property (assign, nonatomic) id< CLLocationManagerDelegate > delegate; // MAS: do we really need this? (line 44)
/* MAS: Instantiate location manager (in viewDidLoad, about line 70)
 */
 self.locationManager = [[CLLocationManager alloc] init];
/* MAS: end
 */
    [self startStandardUpdates];  // MAS: get our location (in viewDidLoad about line 201, not quite at the end)
/* MAS: (still in veiwDidLoad about line 206 after dispatch)
 */
if ([CLLocationManager locationServicesEnabled]) {
    NSLog(@"location services enabled");
}
else {
    NSLog( @"Could not add location services to the session" );
    // self.setupResult = PikBitSetupResultSessionConfigurationFailed;
    // Display alert to the user.
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Location services"
                                                                   message:@"Location services are not enabled on this device. Please enable location services in settings."
                                                            preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction * action) {}]; // NULL action to dismiss the alert.
    [alert addAction:defaultAction];
    [self presentViewController:alert animated:YES completion:nil];
}

if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined)
{
    [self.locationManager requestWhenInUseAuthorization];
    NSLog(@"location authorization status not determined");

} else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
    NSLog(@"location services authorization was previously denied by the user.");
    // Display alert to the user.
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Location services"
                                                                   message:@"Location services were previously denied by the user. Please enable location services for this app in settings."
                                                            preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction * action) {}]; // Do nothing action to dismiss the alert.
    [alert addAction:defaultAction];
    [self presentViewController:alert animated:YES completion:nil];
}

/* MAS: end
 */
            /* MAS: about line 616, once you have image NSData
             */
            CFDictionaryRef tempDict = CMCopyDictionaryOfAttachments(NULL, imageDataSampleBuffer, kCMAttachmentMode_ShouldPropagate);
            CFMutableDictionaryRef theDict = CFDictionaryCreateMutableCopy(NULL, 0, tempDict);

            [self setMyImageInfo:theDict];
            [self setMyLocation:theDict];

            CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
            CFStringRef UTI = CGImageSourceGetType(source);
            NSMutableData *newImageData = [NSMutableData data];
            CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)newImageData,UTI,1,NULL);
            if(!destination) {
                NSLog(@"***Could not create image destination ***");
            }
            CGImageDestinationAddImageFromSource(destination,source,0, (CFDictionaryRef) theDict);

            BOOL success = CGImageDestinationFinalize(destination);
            CFRelease(destination);
            CFRelease(source);
            if( success ) {
                NSLog(@"it worked, let's see the metadata");
                [self checkEXIF:newImageData];
            }
            else {
                NSLog(@"***Could not create data from image destination ***");
            }
            /* MAS: end
             */
[[PHAssetCreationRequest creationRequestForAsset] addResourceWithType:PHAssetResourceTypePhoto data:newImageData options:nil]; // MAS: change data to 'NewImageData' (about line 654)
                            [newImageData writeToURL:temporaryFileURL options:NSDataWritingAtomic error:&error]; // MAS: change source to newImageData (about line 668)
/* MAS: start (immediately before @end)
 */
#pragma mark - location delegate methods

// Start the standard location service

- (void)startStandardUpdates
{
// set the delegate and the parameters
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;

// Set a movement threshold for new events.
self.locationManager.distanceFilter = 500; // meters

[self.locationManager startUpdatingLocation];
NSLog(@"location manager started");
}

// Delegate methods

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// If it's a relatively recent event, turn off updates to save power.
self.location = [locations lastObject];
NSLog(@"location manager delegate used");
NSDate* eventDate = self.location.timestamp;
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
if (fabs(howRecent) < 15.0) {
    // If the event is recent, do something with it.
    NSLog(@"latitude %+.6f, longitude %+.6f\n",
          self.location.coordinate.latitude,
          self.location.coordinate.longitude);
}
else {
    NSLog(@"latitude %+.6f, longitude %+.6f\n",
          self.location.coordinate.latitude,
          self.location.coordinate.longitude);

}
/*  MAS: for optimizing usage (later)

 [manager stopUpdatingLocation]; // If we only want one update.

 manager.delegate = nil;         // We might be called again here, even though we
 // called "stopUpdatingLocation", so remove us as the delegate to be sure.
 */
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
// report any errors returned back from Location Services
NSLog(@"location manager error - %@", error.description);
}

- (void) setMyImageInfo:(CFMutableDictionaryRef)theDict
{
NSString *temp = @"This is a very long string, used to test the loading of the image description field";
NSString *tempID = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ012345"; // used to test imageUniqueID field, should the 33rd character be a nul?

//
if (theDict) {
    // save a dictionary of the TIFF image properties
    CFDictionaryRef tiffData = CFDictionaryGetValue(theDict, kCGImagePropertyTIFFDictionary);

    CFMutableDictionaryRef tiffDataMut;
    if (tiffData) {
        tiffDataMut = CFDictionaryCreateMutableCopy(nil, 0, tiffData);
    }
    else {
        tiffDataMut = CFDictionaryCreateMutable(nil, 0,
                                                &kCFTypeDictionaryKeyCallBacks,  &kCFTypeDictionaryValueCallBacks);
    }
    // The best place to store a long string --
    CFStringRef imageDescription = CFStringCreateWithCString(NULL, [temp cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
    CFDictionarySetValue(tiffDataMut, kCGImagePropertyTIFFImageDescription, imageDescription);
    CFRelease (imageDescription);
    // Carry the brand into the image --
    CFStringRef software = CFStringCreateWithCString(NULL, [@"MyApp" cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
    CFDictionarySetValue(tiffDataMut, kCGImagePropertyTIFFSoftware, software);
    CFRelease (software);
    // Carry the manufacturer into the image --
    CFStringRef make = CFStringCreateWithCString(NULL, [@"Apple" cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
    CFDictionarySetValue(tiffDataMut, kCGImagePropertyTIFFMake, make);
    CFRelease (make);
    // Carry the device name into the image --
    CFStringRef model = CFStringCreateWithCString(NULL, [@"iPhone" cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
    CFDictionarySetValue(tiffDataMut, kCGImagePropertyTIFFModel, model);
    CFRelease (model);

    CFDictionarySetValue(theDict, kCGImagePropertyTIFFDictionary, tiffDataMut);

    CFRelease(tiffDataMut);

    // Now do the same for EXIF
    // save a dictionary of the EXIF image properties
    CFDictionaryRef exifData = CFDictionaryGetValue(theDict, kCGImagePropertyExifDictionary);

    CFMutableDictionaryRef exifDataMut;
    if (exifData) {
        exifDataMut = CFDictionaryCreateMutableCopy(nil, 0, exifData);
    }
    else {
        exifDataMut = CFDictionaryCreateMutable(nil, 0,
                                                &kCFTypeDictionaryKeyCallBacks,  &kCFTypeDictionaryValueCallBacks);
    }
    // This is a good place to store an ID, but it only holds 33 characters --
    CFStringRef imageUniqueID = CFStringCreateWithCString(NULL, [tempID cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
    CFDictionarySetValue(exifDataMut, kCGImagePropertyExifImageUniqueID, imageUniqueID);
    CFRelease (imageUniqueID);
    // Just to see that it works --
    CFStringRef userComment = CFStringCreateWithCString(NULL, [@"I hope this works!  And it does!!" cStringUsingEncoding:NSASCIIStringEncoding], kCFStringEncodingASCII);
    CFDictionarySetValue(exifDataMut, kCGImagePropertyExifUserComment, userComment);
    CFRelease (userComment);

    CFDictionarySetValue(theDict, kCGImagePropertyExifDictionary, exifDataMut);

    CFRelease(exifDataMut);

}
else {
    NSLog(@"Metadata Dictionairy for image empty");
}

/*  alternative version (this has been tested and works) --
 // Create dictionaries -
 NSMutableDictionary *exif = [NSMutableDictionary dictionary];
 NSMutableDictionary *tiff = [NSMutableDictionary dictionary];
 // load some TIFF and EXIF data --
 [tiff setObject:temp forKey:(NSString *)kCGImagePropertyTIFFImageDescription];
 [tiff setObject:@"Apple" forKey:(NSString *)kCGImagePropertyTIFFMake];
 [tiff setObject:@"iPhone" forKey:(NSString *)kCGImagePropertyTIFFModel];
 [tiff setObject:@"PikLips" forKey:(NSString *)kCGImagePropertyTIFFSoftware];
 [exif setObject:@"I hope this works!  And it does, too!!" forKey:(NSString *)kCGImagePropertyExifUserComment];
 [exif setObject:tempID forKey:(NSString *)kCGImagePropertyExifImageUniqueID];

 // load the info into the passed CFMutableDictionaryRef --
 CFDictionarySetValue(theDict, kCGImagePropertyExifDictionary, (__bridge void *)exif);
 CFDictionarySetValue(theDict, kCGImagePropertyTIFFDictionary, (__bridge void *)tiff);

 * end
 */
}
- (void) setMyLocation:(CFMutableDictionaryRef)theDict
{
NSMutableDictionary *gps = [NSMutableDictionary dictionary];
[gps setObject:@"2.2.0.0" forKey:(NSString *)kCGImagePropertyGPSVersion];
NSDateFormatter *formatter  = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"HH:mm:ss.SSSSSS"];
[formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]];
[gps setObject:[formatter stringFromDate:self.location.timestamp] forKey:(NSString *)kCGImagePropertyGPSTimeStamp];
[formatter setDateFormat:@"yyyy:MM:dd"];
[gps setObject:[formatter stringFromDate:self.location.timestamp] forKey:(NSString *)kCGImagePropertyGPSDateStamp];
CGFloat latitude = self.location.coordinate.latitude;
if (latitude < 0) {
    latitude = -latitude;
    [gps setObject:@"S" forKey:(NSString *)kCGImagePropertyGPSLatitudeRef];
} else {
    [gps setObject:@"N" forKey:(NSString *)kCGImagePropertyGPSLatitudeRef];
}
[gps setObject:[NSNumber numberWithFloat:latitude] forKey:(NSString *)kCGImagePropertyGPSLatitude];
CGFloat longitude = self.location.coordinate.longitude;
if (longitude < 0) {
    longitude = -longitude;
    [gps setObject:@"W" forKey:(NSString *)kCGImagePropertyGPSLongitudeRef];
} else {
    [gps setObject:@"E" forKey:(NSString *)kCGImagePropertyGPSLongitudeRef];
}
[gps setObject:[NSNumber numberWithFloat:longitude] forKey:(NSString *)kCGImagePropertyGPSLongitude];
CGFloat altitude = self.location.altitude;
if (!isnan(altitude)){
    if (altitude < 0) {
        altitude = -altitude;
        [gps setObject:@"1" forKey:(NSString *)kCGImagePropertyGPSAltitudeRef];
    } else {
        [gps setObject:@"0" forKey:(NSString *)kCGImagePropertyGPSAltitudeRef];
    }
    [gps setObject:[NSNumber numberWithFloat:altitude] forKey:(NSString *)kCGImagePropertyGPSAltitude];
}
if (self.location.speed >= 0){
    [gps setObject:@"K" forKey:(NSString *)kCGImagePropertyGPSSpeedRef];
    [gps setObject:[NSNumber numberWithFloat:self.location.speed*3.6] forKey:(NSString *)kCGImagePropertyGPSSpeed];
}
if (self.location.course >= 0){
    [gps setObject:@"T" forKey:(NSString *)kCGImagePropertyGPSTrackRef];
    [gps setObject:[NSNumber numberWithFloat:self.location.course] forKey:(NSString *)kCGImagePropertyGPSTrack];
}
CFDictionarySetValue(theDict, kCGImagePropertyGPSDictionary, (__bridge void *)gps);
}
#pragma mark debug

// This is used to verify the metadata loading

- (void)checkEXIF:(NSData *) imageData {
CGImageSourceRef myImageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);

CFDictionaryRef imagePropertiesDictionary;
CFDictionaryRef tiffPropertiesDictionary;
CFDictionaryRef exifPropertiesDictionary;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                         [NSNumber numberWithBool:NO], (NSString *)kCGImageSourceShouldCache,
                         nil];

imagePropertiesDictionary = CGImageSourceCopyPropertiesAtIndex(myImageSource,0, (CFDictionaryRef)options);

/*  these value are produced by the system
 */
CFNumberRef imageWidth = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelWidth);
CFNumberRef imageHeight = (CFNumberRef)CFDictionaryGetValue(imagePropertiesDictionary, kCGImagePropertyPixelHeight);

int w = 0;
int h = 0;

CFNumberGetValue(imageWidth, kCFNumberIntType, &w);
CFNumberGetValue(imageHeight, kCFNumberIntType, &h);
// NSDictionary *propertiesDict = (NSDictionary *)CFBridgingRelease(imagePropertiesDictionary); // this assignment throws an exception later in the code

NSLog(@"Image Width: %d",w);
NSLog(@"Image Height: %d",h);
// NSLog(@"Image Properties: %@", propertiesDict);

/* These values are what we added --
 */
tiffPropertiesDictionary = CFDictionaryGetValue(imagePropertiesDictionary,kCGImagePropertyTIFFDictionary);

if ( tiffPropertiesDictionary ) {
    NSString *make = (NSString *)CFDictionaryGetValue(tiffPropertiesDictionary, kCGImagePropertyTIFFMake);
    NSString *model = (NSString *)CFDictionaryGetValue(tiffPropertiesDictionary, kCGImagePropertyTIFFModel);
    NSString *imageDescription = (NSString *)CFDictionaryGetValue(tiffPropertiesDictionary, kCGImagePropertyTIFFImageDescription);
    NSString *software = (NSString *)CFDictionaryGetValue(tiffPropertiesDictionary, kCGImagePropertyTIFFSoftware);

    NSLog(@"Image Description: %@", imageDescription);
    NSLog(@"Make: %@", make);
    NSLog(@"Model: %@", model);
    NSLog(@"Software: %@", software);

}
else {
    NSLog(@"No TIFF dictionary");
}

exifPropertiesDictionary = CFDictionaryGetValue(imagePropertiesDictionary,kCGImagePropertyExifDictionary);

if ( exifPropertiesDictionary ) {
    NSString *uniqueID = (__bridge NSString *)CFDictionaryGetValue(exifPropertiesDictionary, kCGImagePropertyExifImageUniqueID);
    NSString *userComment = (__bridge NSString *)CFDictionaryGetValue(exifPropertiesDictionary, kCGImagePropertyExifUserComment);

    NSLog(@"Unique ID: %@", uniqueID);
    NSLog(@"User comment: %@", userComment);
}
else {
    NSLog(@"No EXIF dictionary");
}

CFRelease(imagePropertiesDictionary);

}

/* MAS: end
 */