8

How do I save custom metadata in an image when i get the image using the AVFoundation framework?

I know I can access the properties weather it I have my image as UIImage or CIImage but the properties that these seem to have differ from each other (even if it is the same image).

So far I access the dictionary like this: (Code taken from Caffeinated Cocoa blog)

NSLog(@"about to request a capture from: %@", [self stillImageOutput]);
    [[self stillImageOutput] captureStillImageAsynchronouslyFromConnection:videoConnection completionHandler:^(CMSampleBufferRef imageSampleBuffer, NSError *error) 
    {

NSData *jpeg = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageSampleBuffer] ;

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

        //get all the metadata in the image
        NSDictionary *metadata = (__bridge NSDictionary *) CGImageSourceCopyPropertiesAtIndex(source,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];
        }
        if(!GPSDictionary) {
            GPSDictionary = [NSMutableDictionary dictionary];
        }
        if(!RAWDictionary) {
            RAWDictionary = [NSMutableDictionary dictionary];
        }

        float _lat = gpsInfo.coordinate.latitude;
        float _lon = gpsInfo.coordinate.longitude;
        float _alt = gpsInfo.altitude;
        NSDate *_date = gpsInfo.timestamp;
        float _heading = gpsInfo.course;;

        [GPSDictionary setValue:[NSNumber numberWithFloat:_lat] 
                         forKey:(NSString*)kCGImagePropertyGPSLatitude];
        [GPSDictionary setValue:[NSNumber numberWithFloat:_lon] 
                         forKey:(NSString*)kCGImagePropertyGPSLongitude];
        [GPSDictionary setValue:[NSNumber numberWithFloat:_alt] 
                         forKey:(NSString*)kCGImagePropertyGPSAltitude];
        [GPSDictionary setValue:_date 
                         forKey:(NSString*)kCGImagePropertyGPSDateStamp];
        [GPSDictionary setValue:[NSNumber numberWithFloat:_heading] 
                         forKey:(NSString*)kCGImagePropertyGPSImgDirection];


        [EXIFDictionary setValue:@"Wasup" forKey:(NSString *)kCGImagePropertyExifUserComment];

        [RAWDictionary setValue:attitude forKey:@"Attitude"];

        //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(@"Info: %@",metadataAsMutable);

        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((__bridge CFMutableDataRef)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, (__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 ***");
        }

And i can either save my image like this:

CIImage *testImage = [CIImage imageWithData:data];

        NSDictionary *propDict = [testImage properties];


        NSLog(@"Properties %@",propDict);

Or like this

UIImage *imageMod = [[UIImage alloc] initWithData:data];


CGImageSourceRef source2 = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

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

NSLog(@"WASD: %@",metadata2);

BUt neither of the options return my Custom Raw Data Dictionary.

In summary what i want to do is save my own object in a field created by myself (in this case a cmattitude object)

PD: When i NSLOG the dictionary before merging it with the image it does have all the files like i want them.

Info: {
    ColorModel = RGB;
    DPIHeight = 72;
    DPIWidth = 72;
    Depth = 8;
    Orientation = 6;
    PixelHeight = 1080;
    PixelWidth = 1920;
    "{Exif}" =     {
        ApertureValue = "2.526069";
        BrightnessValue = "4.013361";
        ColorSpace = 1;
        ComponentsConfiguration =         (
            1,
            2,
            3,
            0
        );
        ExifVersion =         (
            2,
            2,
            1
        );
        ExposureMode = 0;
        ExposureProgram = 2;
        ExposureTime = "0.03333334";
        FNumber = "2.4";
        Flash = 16;
        FlashPixVersion =         (
            1,
            0
        );
        FocalLenIn35mmFilm = 35;
        FocalLength = "4.28";
        ISOSpeedRatings =         (
            80
        );
        MeteringMode = 5;
        PixelXDimension = 1920;
        PixelYDimension = 1080;
        SceneCaptureType = 0;
        SensingMethod = 2;
        Sharpness = 0;
        ShutterSpeedValue = "4.906905";
        SubjectArea =         (
            959,
            539,
            518,
            388
        );
        UserComment = "Wazup";
        WhiteBalance = 0;
    };
    "{GPS}" =     {
        Altitude = 102;
        DateStamp = "2012-01-20 06:55:48 +0000";
        ImgDirection = "-1";
        Latitude = "35.02496";
        Longitude = "135.754";
    };
    "{Raw}" =     {
        Attitude = "Pitch: 50.913760, Roll: 36.342350, Yaw: -164.272361 @ 0.048291\n";
    };
    "{TIFF}" =     {
        Orientation = 6;
        ResolutionUnit = 2;
        XResolution = 72;
        YResolution = 72;
        "_YCbCrPositioning" = 1;
    };

But when i NSLOG the dictionary from the new image it gives me this for the CIIMage:

Properties {
    ColorModel = RGB;
    DPIHeight = 72;
    DPIWidth = 72;
    Depth = 8;
    Orientation = 6;
    PixelHeight = 1080;
    PixelWidth = 1920;
    "{Exif}" =     {
        ApertureValue = "2.526069";
        BrightnessValue = "4.013361";
        ColorSpace = 1;
        ComponentsConfiguration =         (
            0,
            0,
            0,
            1
        );
        ExifVersion =         (
            2,
            2,
            1
        );
        ExposureMode = 0;
        ExposureProgram = 2;
        ExposureTime = "0.03333334";
        FNumber = "2.4";
        Flash = 16;
        FlashPixVersion =         (
            1,
            0
        );
        FocalLenIn35mmFilm = 35;
        FocalLength = "4.28";
        ISOSpeedRatings =         (
            80
        );
        MeteringMode = 5;
        PixelXDimension = 1920;
        PixelYDimension = 1080;
        SceneCaptureType = 0;
        SensingMethod = 2;
        Sharpness = 0;
        ShutterSpeedValue = "4.906905";
        SubjectArea =         (
            959,
            539,
            518,
            388
        );
        UserComment = "Wazup";
        WhiteBalance = 0;
    };
    "{GPS}" =     {
        Altitude = 102;
        ImgDirection = 0;
        Latitude = "35.025";
        Longitude = "135.754";
    };
    "{JFIF}" =     {
        DensityUnit = 1;
        JFIFVersion =         (
            1,
            1
        );
        XDensity = 72;
        YDensity = 72;
    };
    "{TIFF}" =     {
        Orientation = 6;
        ResolutionUnit = 2;
        XResolution = 72;
        YResolution = 72;
        "_YCbCrPositioning" = 1;
    };

And if I NSLog the UIImage it gives me even less info.

Thanks!

Daxesh Nagar
  • 1,405
  • 14
  • 22
Pochi
  • 13,391
  • 3
  • 64
  • 104
  • Well for starters, your code is producing metadata-enriched image data at a location held by the variable "destination", but then you try to save it using the variable "data". No wonder it doesn't work! – Ben Wheeler Jul 02 '13 at 15:56
  • Lately I am working on images, spent lots of time trying to get good result with Apple's frameworks. However even if you manage to change metadata you need to reprocesses image itself, I mean you have to create a copy of the image you are working on it. Even if it looks same file size change etc. After little bit search alternative to native frameworks, I found Adobe's XMP Toolkit. Not just image files you can alter metadata inside almost all media files with XMP toolkit. It can read/write/alter all metadata name spaces. – modusCell Jul 19 '14 at 21:29
  • @BenjaminWheeler no... CGImageDestinationCreateWithData "Creates an image destination that writes to a Core Foundation mutable data object" so by passing to it a NSMutableData reference I am writing to "data" directly. – Pochi Aug 25 '15 at 06:26

1 Answers1

7

UIImage and CIImage don't hold all the metadata. In fact, converting it from NSData to either of those two formats will strip a lot of that metadata out, so IMHO, UIImage and CIImage should only be used if you are planning on displaying it in the UI or passing it to CoreImage.

You can read the image properties like this:

- (NSDictionary *)getImageProperties:(NSData *)imageData {
    // get the original image metadata
    CGImageSourceRef imageSourceRef = CGImageSourceCreateWithData((__bridge CFDataRef) imageData, NULL);
    CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSourceRef, 0, NULL);
    NSDictionary *props = (__bridge_transfer NSDictionary *) properties;
    CFRelease(imageSourceRef);

    return props;
}

You can then convert your NSDictionary to a NSMutableDictionary:

NSDictionary *props = [self getImageProperties:imageData];
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:props];

After that, add whatever fields you would like. Note that EXIF data is found by grabbing the kCGImagePropertyExifDictionary, GPS data by the kCGImagePropertyGPSDictionary, and so on:

NSMutableDictionary *exifDictionary = properties[(__bridge NSString *) kCGImagePropertyExifDictionary];

Look at the CGImageProperties header file for all the keys available.

Okay, so now that you have made whatever changes you needed to the metadata, adding it back takes a couple of more steps:

// incoming image data
NSData *imageData;

// create the image ref
CGDataProviderRef imgDataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef) imageData);
CGImageRef imageRef = CGImageCreateWithJPEGDataProvider(imgDataProvider, NULL, true, kCGRenderingIntentDefault);

// the updated properties from above
NSMutableDictionary *mutableDictionary;

// create the new output data
CFMutableDataRef newImageData = CFDataCreateMutable(NULL, 0);
// my code assumes JPEG type since the input is from the iOS device camera
CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef) @"image/jpg", kUTTypeImage);
// create the destination
CGImageDestinationRef destination = CGImageDestinationCreateWithData(newImageData, type, 1, NULL);
// add the image to the destination
CGImageDestinationAddImage(destination, imageRef, (__bridge CFDictionaryRef) mutableDictionary);
// finalize the write
CGImageDestinationFinalize(destination);

// memory cleanup
CGDataProviderRelease(imgDataProvider);
CGImageRelease(imageRef);
CFRelease(type);
CFRelease(destination);

// your new image data
NSData *newImage = (__bridge_transfer NSData *)newImageData;

And there you have it.

mikeho
  • 6,790
  • 3
  • 34
  • 46
  • Hey! Nice code here! But, @mikeho , I have a question: Where is the newImage stored? I believe I must store it myself with `[newImage writeToFile:@"/Users/nikola/Desktop/newImage.jpg" atomically:YES];`, but if I check it by logging the properties of that file it has no new keys I've written... – Nikola Markovic Jun 20 '16 at 16:40
  • @NikolaMarkovic Hey, sorry. Haven't had the chance to respond. Yes, you have to write it the way that you did. But which properties are you looking at? You need an EXIF viewer to see the data (or load the data into Photoshop and it should show you). – mikeho Jul 13 '16 at 00:30
  • Sorry for being late with this answer.. I save the new image on desktop, but it's EXIF remains the same.. I can't understand why.. It behaves like it's protected / immutable, but I don't why. – Nikola Markovic Jul 15 '16 at 19:11
  • @NikolaMarkovic honestly, I'm not sure :( I tried this on the phone and downloading the image or viewing it in Image Capture shows the data correctly. It may be that OSX behaves differently. – mikeho Jul 15 '16 at 23:15
  • must be.. I'm currently not working on that project, but as soon as I come back to it and find an answer, I'll let you know. thanks anyway! – Nikola Markovic Jul 16 '16 at 10:48
  • Can someone suggest me the way to add geotag for video taken from UIImagePicker? – Abilash Balasubramanian Aug 07 '18 at 05:13