Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stripping All Exif Data in Objective-C

How do you strip all exif data in a UIImage using objective-c? I have been able to get the exif data using the following :

NSData* pngData =  UIImagePNGRepresentation(image);
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)pngData, NULL);

NSDictionary* dic   =   nil;
if ( NULL == imageSource )
{
    #ifdef _DEBUG
    CGImageSourceStatus status = CGImageSourceGetStatus ( source );
    NSLog ( @"Error: file name : %@ - Status: %d", file, status );
    #endif
}
else
{
    CFDictionaryRef propertyRef = CGImageSourceCopyPropertiesAtIndex ( imageSource, 0, NULL );
    CGImageMetadataRef metadataRef = CGImageSourceCopyMetadataAtIndex ( imageSource, 0, NULL );
    // CFDictionaryRef metadataRef = CFDictionaryGetValue(imageProperties, kCGImagePropertyExifDictionary);
    if (metadataRef) {
        NSDictionary* immutableMetadata = (NSDictionary *)metadataRef;
        if ( immutableMetadata ) {
            dic = [ NSDictionary dictionaryWithDictionary : (NSDictionary *)metadataRef ];
        }   
        CFRelease ( metadataRef );
    }
    CFRelease(imageSource);
    imageSource = nil;
}


return dic;
like image 928
Jason Foster Avatar asked Aug 31 '14 05:08

Jason Foster


2 Answers

A couple of thoughts:

  1. Generally the process of loading an image from the NSData or from the contents of a file into a UIImage, re-extracting the data with UIImagePNGRepresentation and saving that back to a NSData will strip the metadata from the image.

    This simple technique has its disadvantages, though. Notably, the process of forcing it to a PNG representation may affect the size of the output NSData dramatically, though. For example, if the original image was a compressed JPEG (such as captured by the camera), the resulting PNG file can actually be larger than the original JPEG.

    Generally I would prefer to get the original data (if a file in Documents or the bundle, load that directly to a NSData; if it's something retrieved from the ALAssetsLibrary, I would retrieve the ALAsset, and from that, the ALAssetRepresentation, and from that I'd use getBytes to get the original binary representation of that original asset). This avoids the round-tripping it through a UIImage (particularly if subsequently using UIImagePNGRepresentation or UIImageJEPGRepresentation).

  2. If you want to strip exif data, you can:

    • Create an image source from the original NSData;

    • Create an image destination, using the same "number of images" (almost always 1) and the "type";

    • When copying the images from the source to the destination, tell it that the appropriate keys (kCGImagePropertyExifDictionary and kCGImagePropertyGPSDictionary should be set to kCFNull), which according to the CGImageDestinationAddImageFromSource documentation is how you specify that those should be removed in the destination if they happened to appear in the source).

    Thus, it might look like:

    - (NSData *)dataByRemovingExif:(NSData *)data
    {
        CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)data, NULL);
        NSMutableData *mutableData = nil;
    
        if (source) {
            CFStringRef type = CGImageSourceGetType(source);
            size_t count = CGImageSourceGetCount(source);
            mutableData = [NSMutableData data];
    
            CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)mutableData, type, count, NULL);
    
            NSDictionary *removeExifProperties = @{(id)kCGImagePropertyExifDictionary: (id)kCFNull,
                                                   (id)kCGImagePropertyGPSDictionary : (id)kCFNull};
    
            if (destination) {
                for (size_t index = 0; index < count; index++) {
                    CGImageDestinationAddImageFromSource(destination, source, index, (__bridge CFDictionaryRef)removeExifProperties);
                }
    
                if (!CGImageDestinationFinalize(destination)) {
                    NSLog(@"CGImageDestinationFinalize failed");
                }
    
                CFRelease(destination);
            }
    
            CFRelease(source);
        }
    
        return mutableData;
    }
    

    Note, GPS information is not technically exif data, but I assume you wanted to remove that, too. If you wanted to keep the GPS data, remove the kCGImagePropertyGPSDictionary entry from my removeExifProperties dictionary.

  3. BTW, in your code to extract the meta data, you seem to be casting the CGImageMetadataRef as a NSDictionary. If your technique works, that's fine, but I think that CGImageMetadataRef is considered to be an opaque data type, and that one really should be using CGImageMetadataCopyTags to extract the array of tags:

    - (NSArray *)metadataForData:(NSData *)data
    {
        NSArray *metadataArray = nil;
        CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)data, NULL);
    
        if (source) {
            CGImageMetadataRef metadata = CGImageSourceCopyMetadataAtIndex(source, 0, NULL);
            if (metadata) {
                metadataArray = CFBridgingRelease(CGImageMetadataCopyTags(metadata));
                CFRelease(metadata);
            }
            CFRelease(source);
        }
    
        return metadataArray;
    }
    

    For the sake of completeness, in iOS versions prior to 7.0, you can extract the data from the properties:

    - (NSDictionary *)metadataForData:(NSData *)data
    {
        NSDictionary *properties = nil;
        CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)data, NULL);
    
        if (source) {
            properties = CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(source, 0, NULL));
            CFRelease(source);
        }
    
        return properties;
    }
    
like image 95
Rob Avatar answered Oct 30 '22 02:10

Rob


According to the Image I/O programming guide, you can use CGImageDestinationSetProperties to add a CFDictionaryRef of the properties.

Their sample code is:

float compression = 1.0; // Lossless compression if available.
int orientation = 4; // Origin is at bottom, left.
CFStringRef myKeys[3];
CFTypeRef   myValues[3];
CFDictionaryRef myOptions = NULL;
myKeys[0] = kCGImagePropertyOrientation;
myValues[0] = CFNumberCreate(NULL, kCFNumberIntType, &orientation);
myKeys[1] = kCGImagePropertyHasAlpha;
myValues[1] = kCFBooleanTrue;
myKeys[2] = kCGImageDestinationLossyCompressionQuality;
myValues[2] = CFNumberCreate(NULL, kCFNumberFloatType, &compression);
myOptions = CFDictionaryCreate( NULL, (const void **)myKeys, (const void **)myValues, 3,
                      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// Release the CFNumber and CFDictionary objects when you no longer need them.

And I don't understand why they didn't take it all the way. I'm guessing what comes next would be:

  • create a CFImageDestinationRef
  • copy your image to it
  • set your desired metadata on it

How to do the first step isn't very clear from the docs. So I looked at the CGImageDestination reference, and it looks like it can be done this way (NOT TESTED):

NSMutableData* mutableData = [[NSMutableData alloc] initWithCapacity:[pngData length]];
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge_transfer CFMutableDataRef)mutableData, <# Some UTI Type #>, 1, NULL);
CGImageDestinationAddImage(imageDestination, image, yourProperties);

CGImageDestinationFinalize(imageDestination);

So create your properties dictionary the way Apple shows in the docs, then create an image destination and write everything to it, including your properties. Afterward you can access the NSMutableData object and read your image data.

like image 34
Shinigami Avatar answered Oct 30 '22 01:10

Shinigami