Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Missing image metadata when saving updated image into PhotoKit

I am having an issue updating an image's metadata and saving that back to the Photos library. Everything works except that the image metadata after it's altered has missing entries that were there before and I am not getting any errors manipulating the image or executing the photos library change block. Also, the dictionary before it's written back into the image looks like the original plus my dictionary in the debugger.

My questions are:

  1. What am I doing wrong that would cause rewriting the existing properties back with additional data to wipe out what is there?
  2. Is there better, more canonical way to do this? It seems like a lot of mechanics to update some meta data in an image. It just seems like What Everyone Else Is Doing.

EDIT:

Before the save all the Exif and Tiff values are present. This is the entirety of the metadata after the save to photos using the code below:

["PixelHeight": 2448, "PixelWidth": 3264, "{Exif}": {
ColorSpace = 1;
PixelXDimension = 3264;
PixelYDimension = 2448;}, "Depth": 8, "ProfileName": sRGB IEC61966-2.1, "Orientation": 1, "{TIFF}": {
Orientation = 1;}, "ColorModel": RGB, "{JFIF}": {
DensityUnit = 0;
JFIFVersion =     (
    1,
    0,
    1
);
XDensity = 72;
YDensity = 72;}]

The code, all in Swift 3, testing on iOS 10.1

The basic workflow is:

    // Get a mutable copy of the existing Exif meta
    let mutableMetaData = getMutableMetadataFrom(imageData: data)

    // Check to see if it has the {GPS} entry, if it does just exit.
    if let _ = mutableMetaData[kCGImagePropertyGPSDictionary as String] {
       callback(imageAsset, true, nil)
       return
    }

    // Add the {GPS} tag to the existing metadata
    let clLocation = media.location!.asCLLocation()
    mutableMetaData[kCGImagePropertyGPSDictionary as String] =
       clLocation.asGPSMetaData()

    // Attach the new metadata to the existing image
    guard let newImageData = attach(metadata: mutableMetaData, toImageData: data) else {
            callback(imageAsset, false, nil)
            return
    }

    let editingOptions = PHContentEditingInputRequestOptions()
    imageAsset.requestContentEditingInput(with: editingOptions) { editingInput, info in
        guard let editingInput = editingInput else { return }
        let library = PHPhotoLibrary.shared()
        let output = PHContentEditingOutput(contentEditingInput: editingInput)
        output.adjustmentData = PHAdjustmentData(formatIdentifier: "Project", formatVersion: "0.1",
                                                 data: "Location Adjustment".data(using: .utf8)!)
        do {
            try newImageData.write(to: output.renderedContentURL, options: [.atomic])
        } catch {
            callback(imageAsset, false, error)
            return
        }

        library.performChanges({
            let changeRequest = PHAssetChangeRequest(for: imageAsset)
            changeRequest.location = clLocation
            changeRequest.contentEditingOutput = output

        }, completionHandler: { success, error in ... ...

The helper methods to the workflow are:

func attach(metadata: NSDictionary, toImageData imageData:Data) -> Data? {

    guard
        let imageDataProvider = CGDataProvider(data: imageData as CFData),
        let cgImage = CGImage(jpegDataProviderSource: imageDataProvider, decode: nil,
                              shouldInterpolate: true, intent: .defaultIntent),
        let newImageData = CFDataCreateMutable(nil, 0),
        let type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType,
                                                         "image/jpg" as CFString, kUTTypeImage),
        let destination = CGImageDestinationCreateWithData(newImageData,
                                                           (type.takeRetainedValue()), 1, nil) else {

            return nil
        }

    CGImageDestinationAddImage(destination, cgImage, metadata as CFDictionary)
    CGImageDestinationFinalize(destination)

    guard
        let newProvider = CGDataProvider(data: newImageData),
        let newCGImage = CGImage(jpegDataProviderSource: newProvider, decode: nil,
                                 shouldInterpolate: false, intent: .defaultIntent) else {

            return nil
    }

    return UIImageJPEGRepresentation(UIImage(cgImage: newCGImage), 1.0)
}

func getMutableMetadataFrom(imageData data : Data) -> NSMutableDictionary {

    let imageSourceRef = CGImageSourceCreateWithData(data as CFData, nil)
    let currentProperties = CGImageSourceCopyPropertiesAtIndex(imageSourceRef!, 0, nil)
    let mutableDict = NSMutableDictionary(dictionary: currentProperties!)

    return mutableDict
}

Also the asGPSMetaData is an extension on CLLocation than looks a Swift 3 version of this Gist

like image 735
Ray Pendergraph Avatar asked Dec 15 '16 16:12

Ray Pendergraph


People also ask

How do I add metadata to an image?

Adding Metadata in Adobe Photoshop In Photoshop, you can update metadata for any open image. Go to File > File Info. Update the Document Title, Description and Copyright Notice on the Description tab. Save the image.

Do downloaded images have metadata?

File format forensics: metadataWhen an image is saved, the file typically contains data about the image, known as metadata.


1 Answers

It turns out that it was not the manipulation of the image or metadata with CoreGraphics that was the issue at all, it was a couple of things I overlooked:

  1. Constructing a UIImage from data that contains EXIF or GPS data removes that data... in fact, it removes most of the meta data except a core set of JFIF and size data (when using JPEG). It makes sense in retrospect as their internal representation would be just raw data. However, I did not find any explicit statement about metadata in the docs.
  2. Given the prior point, the main two ways to get a Data (image with metadata) object into the Photos library was either to write it to a temp file and read it in with PHAssetChangeRequest::creationRequestForAssetFromImage(atFileURL:) or use PHAssetCreationRequest::forAsset to create a creation request and then use PHAssetCreationRequest::addResource(with:data:options:) to add the data as a photo. I chose the latter as it had less moving parts.

So I guess all that replaces the nice, succinct ALAssetsLibrary::writeImage(toSavedPhotosAlbum:metadata:completionBlock:).

The final change block for the Photos library ended up being this:

    var assetID: String?
    PHPhotoLibrary.shared().performChanges({ 
        let creationRequest = PHAssetCreationRequest.forAsset()
        creationRequest.addResource(with: .photo, data: imageWithMetaData as Data, options: nil)
        creationRequest.location = clLocation
        assetID = creationRequest.placeholderForCreatedAsset?.localIdentifier
    }) { success, error  in ...
like image 104
Ray Pendergraph Avatar answered Sep 23 '22 09:09

Ray Pendergraph