10

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

Jordan H
  • 45,794
  • 29
  • 162
  • 306

2 Answers2

5

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.

Bradley M Handy
  • 563
  • 4
  • 13
  • This is likely a bug, as you pointed out, `properties` should always return the metadata from the **original input image**. I'm working on converting your code to Swift though, looks promising. – Jordan H Sep 22 '14 at 06:00
  • Were you able to get it ported to Swift? – Bradley M Handy Sep 25 '14 at 00:47
  • Hm, I'm finding the only object in `metadata` is Filesize = #. – Jordan H Jan 28 '15 at 04:55
  • If you are using the Obj-C code in your original post, try using the `[CIImage imageWithContentsOfURL: ...]` method without the additional options parameter. According to the documentation for the _Image Dictionary Keys_ providing a null value for the `kCIImageProperties` dictionary key will remove metadata properties. Based on this documentation, the version of the method without the _options_ parameter may preserve the properties. As I've never used `CIImage`, this is just a hunch. – Bradley M Handy Jan 28 '15 at 14:30
  • Also depending on the image type, there may be multiple images with in your image file. You may need to use the `CGImageSourceCopyPropertiesAtIndex` method to get the metadata if you're adapting the code from my suggestion. – Bradley M Handy Jan 28 '15 at 14:34
  • 1
    I was able to get the metadata using `CGImageSourceCopyPropertiesAtIndex`, thanks! It is able to finalize successfully but when I call `performChanges` for that `asset` and `contentEditingOutput` it fails. If I remove the metadata from the `options` parameter it succeeds. Do you know why? – Jordan H Feb 01 '15 at 19:28
  • I don't know for sure, but what seems off to me is the lack of `adjustmentData` being set on `PHContentEditingOutput`. With the `adjustmentData`, the framework will not save the edit. – Bradley M Handy Feb 06 '15 at 19:43
  • I do set `adjustmentData` for the output, just left it out in the code above for brevity. – Jordan H Feb 06 '15 at 19:48
4

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.

Community
  • 1
  • 1
Jordan H
  • 45,794
  • 29
  • 162
  • 306