Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to preserve original photo metadata when editing PHAssets?

I am editing photos via PhotoKit but I discovered this does not preserve the original photo's metadata. This occurs even with the SamplePhotosApp provided by Apple when they apply Sepia or Chrome filters. My question is, how do you ensure all the original photo metadata is preserved?

I've discovered how you can obtain the original image's metadata, and I was able to save that metadata to the final CIImage I create, but it still is stripped out when the edit is committed. There must be an issue in the way I convert the CIImage to a CGImage to a UIImage to NSData, or how I'm writing it to disk.

asset.requestContentEditingInputWithOptions(options) { (input: PHContentEditingInput!, _) -> Void in
    //Get full image
    let url = input.fullSizeImageURL
    let orientation = self.input.fullSizeImageOrientation
    var inputImage = CIImage(contentsOfURL: url)
    inputImage = inputImage.imageByApplyingOrientation(orientation)

    //do some processing on original photo here and create a CGImage...

    //save the original photo's metadata to a new CIImage:
    let originalMetadata = inputImage.properties()
    let newImage = CIImage(CGImage: editedCGImage, options: [kCIImageProperties: originalMetadata])

    println(newImage.properties()) //correctly prints all metadata!

    //commit changes to disk - somewhere after this line the metadata is lost
    let eaglContext = EAGLContext(API: .OpenGLES2)
    let ciContext = CIContext(EAGLContext: eaglContext)
    let outputImageRef = ciContext.createCGImage(newImage, fromRect: newImage.extent())
    let uiImage = UIImage(CGImage: outputImageRef, scale: 1.0, orientation: UIImageOrientation.Up)
    let jpegNSData = UIImageJPEGRepresentation(uiImage, 0.75)

    let contentEditingOutput = PHContentEditingOutput(contentEditingInput: input)
    let success = jpegData.writeToURL(contentEditingOutput.renderedContentURL, options: NSDataWritingOptions.AtomicWrite, error: _)

    PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
        let request = PHAssetChangeRequest(forAsset: asset)
        request.contentEditingOutput = contentEditingOutput
    }, completionHandler: { (success: Bool, error: NSError!) -> Void in
        if success == false { println('failed to commit image edit: \(error)') }
    })
})

Original - note the GPS tab:
Original photo's metadata

After editing the photo:
enter image description here

like image 285
Jordan H Avatar asked Sep 07 '14 22:09

Jordan H


2 Answers

According to the Apple document for the CIImage.properties:

If the CIImage object is the output of a filter (or filter chain), this method returns the metadata from the filter’s original input image.

With that said, this is how I would do it if the documentation is not correct.

You need to save the properties on your own. To do this you'll need to read the content of the URL as NSData

NSURL *url = @"http://somewebsite.com/path/to/some/image.jpg";
NSData *imageData = [NSData dataWithContentsOfURL:url];
CIImage *image = [CIImage imageWithData:imageData];

// Save off the properties
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef) imageData);
NSDictionary* metadata = (__bridge NSDictionary *) CGImageSourceCopyProperties(imageSource, NULL);

// process your image
// ...
NSData *outputData = // create data from altered image.

// Save the image
CGImageSourceRef outputImageSource = CGImageSourceCreateWithData((__bridge CFDataRef) outputData);
CFMutableDataRef jpegData = CFDataCreateMutable(NULL, 0);
CGImageDestinationRef outputDestination = CGImageDestinationCreateWithData(jpegData, CGImageSourceGetType(outputImageSource), 1, NULL);

// add the image data to the destination
CGImageDestinationAddImageFromSource(jpegData, outputImageSource, 0, (__bridge CFDictionaryRef) metadata);

if (CGImageDestinationFinalize(outputDestination)) {
    NSLog(@"Successful image creation.");
    // process the image rendering, adjustment data creation and finalize the asset edit.
} else {
    NSLog(@"Image creation failed.");
}

If you don't need to modify the metadata, there is no need to cast the CFDataRef to an NSDictionary*, but I did here just for completeness.

like image 193
Bradley M Handy Avatar answered Sep 24 '22 09:09

Bradley M Handy


While this was possible to do in Objective-C as Bradley showed, it did not work correctly in Swift and would cause the performChanges block to fail (see my other question). This has been addressed in the latest releases - Swift 1.2 in Xcode 6.3. To preserve the metadata, it's this simple:

let metadata = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as NSDictionary
CGImageDestinationAddImage(destination, cgImage, metadata)

Or for Objective-C:

NSMutableDictionary *imageMetadata = [(NSDictionary *) CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL)) mutableCopy];
CGImageDestinationAddImageFromSource(destination, imageSource, 0, (__bridge CFDictionaryRef)(imageMetadata));

EDIT: While this works in the iOS Simulator, it did not work on a real device running the Swift code.

like image 21
Jordan H Avatar answered Sep 24 '22 09:09

Jordan H