Search code examples
ioscameraphotoexif

Modified EXIF data doesn't save properly


After countless attempts and sieving through every SO answer + google result, it baffles me that working with EXIF on iOS is so frustrating.

Below is working code with its results.

[[self stillImageOutput] captureStillImageAsynchronouslyFromConnection:videoConnection
    completionHandler:^(CMSampleBufferRef imageSampleBuffer, NSError *error)
    {
        NSData *imageNSData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer];

        CGImageSourceRef imgSource = CGImageSourceCreateWithData((__bridge_retained CFDataRef)imageNSData, NULL);

        //get all the metadata in the image
        NSDictionary *metadata = (__bridge NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imgSource, 0, NULL);

        NSLog(@"original metadata Info: %@",metadata);

        //make the metadata dictionary mutable so we can add properties to it
        NSMutableDictionary *metadataAsMutable = [metadata mutableCopy];

        NSMutableDictionary *EXIFDictionary = [[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyExifDictionary]mutableCopy];
        NSMutableDictionary *GPSDictionary = [[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyGPSDictionary]mutableCopy];
        NSMutableDictionary *RAWDictionary = [[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyRawDictionary]mutableCopy];

        if(!EXIFDictionary)
            EXIFDictionary = [[NSMutableDictionary dictionary] init];

        if(!GPSDictionary)
            GPSDictionary = [[NSMutableDictionary dictionary] init];

        if(!RAWDictionary)
            RAWDictionary = [[NSMutableDictionary dictionary] init];


        [GPSDictionary setObject:@"camera coord Latitude"
                         forKey:(NSString*)kCGImagePropertyGPSLatitude];
        [GPSDictionary setObject:@"camera coord Longitude"
                         forKey:(NSString*)kCGImagePropertyGPSLongitude];
        [GPSDictionary setObject:@"camera GPS Date Stamp"
                         forKey:(NSString*)kCGImagePropertyGPSDateStamp];
        [GPSDictionary setObject:@"camera direction (heading) in degrees"
                         forKey:(NSString*)kCGImagePropertyGPSImgDirection];

        [GPSDictionary setObject:@"subject coord Latitude"
                         forKey:(NSString*)kCGImagePropertyGPSDestLatitude];
        [GPSDictionary setObject:@"subject coord Longitude"
                         forKey:(NSString*)kCGImagePropertyGPSDestLongitude];

        [EXIFDictionary setObject:@"[S.D.] kCGImagePropertyExifUserComment"
                           forKey:(NSString *)kCGImagePropertyExifUserComment];

        [EXIFDictionary setValue:@"69 m" forKey:(NSString *)kCGImagePropertyExifSubjectDistance];


        //Add the modified Data back into the image’s metadata
        [metadataAsMutable setObject:EXIFDictionary forKey:(NSString *)kCGImagePropertyExifDictionary];
        [metadataAsMutable setObject:GPSDictionary forKey:(NSString *)kCGImagePropertyGPSDictionary];
        [metadataAsMutable setObject:RAWDictionary forKey:(NSString *)kCGImagePropertyRawDictionary];


        NSLog(@"metadataAsMutable Info: %@",metadataAsMutable);

        CFStringRef UTI = CGImageSourceGetType(imgSource); //this is the type of image (e.g., public.jpeg)

        //this will be the data CGImageDestinationRef will write into
        NSMutableData *newImageData = [NSMutableData data];

        CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)newImageData, 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, imgSource, 0, (__bridge 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 ***");

        CIImage *testImage = [CIImage imageWithData:newImageData];
        NSDictionary *propDict = [testImage properties];
        NSLog(@"Properties %@", propDict);

    }];

Which outputs this:

2012-10-12 23:17:45.415 Waypointer[3120:907] original metadata Info: {
ColorModel = RGB;
DPIHeight = 72;
DPIWidth = 72;
Depth = 8;
Orientation = 1;
PixelHeight = 2448;
PixelWidth = 3264;
"{Exif}" =     {
    ApertureValue = "2.526069";
    BrightnessValue = "-4.410617";
    ColorSpace = 1;
    ComponentsConfiguration =         (
        1,
        2,
        3,
        0
    );
    ExifVersion =         (
        2,
        2,
        1
    );
    ExposureMode = 0;
    ExposureProgram = 2;
    ExposureTime = "0.06666667";
    FNumber = "2.4";
    Flash = 16;
    FlashPixVersion =         (
        1,
        0
    );
    FocalLenIn35mmFilm = 35;
    FocalLength = "4.28";
    ISOSpeedRatings =         (
        800
    );
    MeteringMode = 5;
    PixelXDimension = 3264;
    PixelYDimension = 2448;
    SceneCaptureType = 0;
    SensingMethod = 2;
    ShutterSpeedValue = "3.906905";
    SubjectArea =         (
        1631,
        1223,
        881,
        881
    );
    WhiteBalance = 0;
};
"{TIFF}" =     {
    Orientation = 1;
    ResolutionUnit = 2;
    XResolution = 72;
    YResolution = 72;
    "_YCbCrPositioning" = 1;
};
}

And this:

2012-10-12 23:17:45.421 Waypointer[3120:907] metadataAsMutable Info: {
ColorModel = RGB;
DPIHeight = 72;
DPIWidth = 72;
Depth = 8;
Orientation = 1;
PixelHeight = 2448;
PixelWidth = 3264;
"{Exif}" =     {
    ApertureValue = "2.526069";
    BrightnessValue = "-4.410617";
    ColorSpace = 1;
    ComponentsConfiguration =         (
        1,
        2,
        3,
        0
    );
    ExifVersion =         (
        2,
        2,
        1
    );
    ExposureMode = 0;
    ExposureProgram = 2;
    ExposureTime = "0.06666667";
    FNumber = "2.4";
    Flash = 16;
    FlashPixVersion =         (
        1,
        0
    );
    FocalLenIn35mmFilm = 35;
    FocalLength = "4.28";
    ISOSpeedRatings =         (
        800
    );
    MeteringMode = 5;
    PixelXDimension = 3264;
    PixelYDimension = 2448;
    SceneCaptureType = 0;
    SensingMethod = 2;
    ShutterSpeedValue = "3.906905";
    SubjectArea =         (
        1631,
        1223,
        881,
        881
    );
    SubjectDistance = "69 m";
    UserComment = "[S.D.] kCGImagePropertyExifUserComment";
    WhiteBalance = 0;
};
"{GPS}" =     {
    DateStamp = "camera GPS Date Stamp";
    DestLatitude = "subject coord Latitude";
    DestLongitude = "subject coord Longitude";
    ImgDirection = "camera direction (heading) in degrees";
    Latitude = "camera coord Latitude";
    Longitude = "camera coord Longitude";
};
"{Raw}" =     {
};
"{TIFF}" =     {
    Orientation = 1;
    ResolutionUnit = 2;
    XResolution = 72;
    YResolution = 72;
    "_YCbCrPositioning" = 1;
};
}

And, after it's all done, this:

2012-10-12 23:17:47.131 Waypointer[3120:907] Properties {
ColorModel = RGB;
DPIHeight = 72;
DPIWidth = 72;
Depth = 8;
Orientation = 1;
PixelHeight = 2448;
PixelWidth = 3264;
"{Exif}" =     {
    ApertureValue = "2.526069";
    BrightnessValue = "-4.410617";
    ColorSpace = 1;
    ComponentsConfiguration =         (
        0,
        0,
        0,
        1
    );
    ExifVersion =         (
        2,
        2,
        1
    );
    ExposureMode = 0;
    ExposureProgram = 2;
    ExposureTime = "0.06666667";
    FNumber = "2.4";
    Flash = 16;
    FlashPixVersion =         (
        1,
        0
    );
    FocalLenIn35mmFilm = 35;
    FocalLength = "4.28";
    ISOSpeedRatings =         (
        800
    );
    MeteringMode = 5;
    PixelXDimension = 3264;
    PixelYDimension = 2448;
    SceneCaptureType = 0;
    SensingMethod = 2;
    ShutterSpeedValue = "3.906905";
    SubjectArea =         (
        1631,
        1223,
        881,
        881
    );
    UserComment = "[S.D.] kCGImagePropertyExifUserComment";
    WhiteBalance = 0;
};
"{JFIF}" =     {
    DensityUnit = 1;
    JFIFVersion =         (
        1,
        1
    );
    XDensity = 72;
    YDensity = 72;
};
"{TIFF}" =     {
    Orientation = 1;
    ResolutionUnit = 2;
    XResolution = 72;
    YResolution = 72;
    "_YCbCrPositioning" = 1;
};
}

As the example illustrates, you can see the image's original metaData, its modification, and then its final output.

The final output is what bothers me because no matter what I do, I cannot get my modified values to stick!

Is there some very specific format I'm missing? Why is iOS stripping my modifications? What do I need to do to add these extra values? They are listed in the .header and figured it should be easily accepted.


Solution

  • Scott, a developer at Apple, returned with my incident report and fixed the issue:

    The previous code above is writing string values for the GPS values - this won't work, they must be NS/CFNumbers (we extract a float value for EXIF).

    I'll be filing a bug report to Apple against their documentation.

    Although it took a week to get this response, I really do appreciate the support Apple provides to their developers. (thanks Scott!) ;-)

    Below is the improved code and its correct output:

    [[self stillImageOutput] captureStillImageAsynchronouslyFromConnection:videoConnection
        completionHandler:^(CMSampleBufferRef imageSampleBuffer, NSError *error)
        {
            NSData *imageNSData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer];
    
            CGImageSourceRef imgSource = CGImageSourceCreateWithData((__bridge_retained CFDataRef)imageNSData, NULL);
    
            //get all the metadata in the image
            NSDictionary *metadata = (__bridge NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imgSource, 0, NULL);
    
            //make the metadata dictionary mutable so we can add properties to it
            NSMutableDictionary *metadataAsMutable = [metadata mutableCopy];
    
            NSMutableDictionary *EXIFDictionary = [[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyExifDictionary]mutableCopy];
            NSMutableDictionary *GPSDictionary = [[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyGPSDictionary]mutableCopy];
            NSMutableDictionary *RAWDictionary = [[metadataAsMutable objectForKey:(NSString *)kCGImagePropertyRawDictionary]mutableCopy];
    
            if(!EXIFDictionary)
                EXIFDictionary = [[NSMutableDictionary dictionary] init];
    
            if(!GPSDictionary)
                GPSDictionary = [[NSMutableDictionary dictionary] init];
    
            if(!RAWDictionary)
                RAWDictionary = [[NSMutableDictionary dictionary] init];
    
    
            [GPSDictionary setObject:[NSNumber numberWithFloat:37.795]
                              forKey:(NSString*)kCGImagePropertyGPSLatitude];
    
            [GPSDictionary setObject:@"N" forKey:(NSString*)kCGImagePropertyGPSLatitudeRef];
    
            [GPSDictionary setObject:[NSNumber numberWithFloat:122.410]
                              forKey:(NSString*)kCGImagePropertyGPSLongitude];
    
            [GPSDictionary setObject:@"W" forKey:(NSString*)kCGImagePropertyGPSLongitudeRef];
    
            [GPSDictionary setObject:@"2012:10:18"
                              forKey:(NSString*)kCGImagePropertyGPSDateStamp];
    
            [GPSDictionary setObject:[NSNumber numberWithFloat:300]
                              forKey:(NSString*)kCGImagePropertyGPSImgDirection];
    
            [GPSDictionary setObject:[NSNumber numberWithFloat:37.795]
                              forKey:(NSString*)kCGImagePropertyGPSDestLatitude];
    
            [GPSDictionary setObject:@"N" forKey:(NSString*)kCGImagePropertyGPSDestLatitudeRef];
    
            [GPSDictionary setObject:[NSNumber numberWithFloat:122.410]
                              forKey:(NSString*)kCGImagePropertyGPSDestLongitude];
    
            [GPSDictionary setObject:@"W" forKey:(NSString*)kCGImagePropertyGPSDestLongitudeRef];
    
            [EXIFDictionary setObject:@"[S.D.] kCGImagePropertyExifUserComment"
                               forKey:(NSString *)kCGImagePropertyExifUserComment];
    
            [EXIFDictionary setObject:[NSNumber numberWithFloat:69.999]
                               forKey:(NSString*)kCGImagePropertyExifSubjectDistance];
    
    
            //Add the modified Data back into the image’s metadata
            [metadataAsMutable setObject:EXIFDictionary forKey:(NSString *)kCGImagePropertyExifDictionary];
            [metadataAsMutable setObject:GPSDictionary forKey:(NSString *)kCGImagePropertyGPSDictionary];
            [metadataAsMutable setObject:RAWDictionary forKey:(NSString *)kCGImagePropertyRawDictionary];
    
    
            CFStringRef UTI = CGImageSourceGetType(imgSource); //this is the type of image (e.g., public.jpeg)
    
            //this will be the data CGImageDestinationRef will write into
            NSMutableData *newImageData = [NSMutableData data];
    
            CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)newImageData, 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, imgSource, 0, (__bridge 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 ***");
    
            CIImage *testImage = [CIImage imageWithData:newImageData];
            NSDictionary *propDict = [testImage properties];
            NSLog(@"Final properties %@", propDict);
    
        }];
    

    Which outputs this:

    Final properties info {
        ColorModel = RGB;
        DPIHeight = 72;
        DPIWidth = 72;
        Depth = 8;
        Orientation = 6;
        PixelHeight = 2448;
        PixelWidth = 3264;
        "{Exif}" =     {
            ApertureValue = "2.526069";
            BrightnessValue = "0.547474";
            ColorSpace = 1;
            ComponentsConfiguration =         (
                0,
                0,
                0,
                1
            );
            ExifVersion =         (
                2,
                2,
                1
            );
            ExposureMode = 0;
            ExposureProgram = 2;
            ExposureTime = "0.05";
            FNumber = "2.4";
            Flash = 16;
            FlashPixVersion =         (
                1,
                0
            );
            FocalLenIn35mmFilm = 35;
            FocalLength = "4.28";
            ISOSpeedRatings =         (
                320
            );
            MeteringMode = 5;
            PixelXDimension = 3264;
            PixelYDimension = 2448;
            SceneCaptureType = 0;
            SensingMethod = 2;
            ShutterSpeedValue = "4.321929";
            SubjectArea =         (
                1631,
                1223,
                881,
                881
            );
            SubjectDistance = "69.999";
            UserComment = "[S.D.] kCGImagePropertyExifUserComment";
            WhiteBalance = 0;
        };
        "{GPS}" =     {
            DateStamp = "2012:10:18";
            DestLatitude = "37.795";
            DestLatitudeRef = N;
            DestLongitude = "122.41";
            DestLongitudeRef = W;
            ImgDirection = 300;
            Latitude = "37.795";
            LatitudeRef = N;
            Longitude = "122.41";
            LongitudeRef = W;
        };
        "{JFIF}" =     {
            DensityUnit = 1;
            JFIFVersion =         (
                1,
                1
            );
            XDensity = 72;
            YDensity = 72;
        };
        "{TIFF}" =     {
            Orientation = 6;
            ResolutionUnit = 2;
            XResolution = 72;
            YResolution = 72;
            "_YCbCrPositioning" = 1;
        };
    }
    

    As you can see, all values are now properly embedded into the EXIF header and I've tested that this is correctly written with the JPG to the camera roll.

    Enjoy! :)