Swift: Custom camera save modified metadata with image

i0S Swift Issue

Question or problem in the Swift programming language:

I am trying to save SOME of the metadata from an image sample buffer along with the image.

I need to:

I have tried creating a UIImage from the data, but that strips out the metadata. I have tried using a CIImage from the data, which keeps the metadata, but I can’t rotate it then save it to a file.

private func snapPhoto(success: (UIImage, CFMutableDictionary) -> Void, errorMessage: String -> Void) {
    guard !self.stillImageOutput.capturingStillImage,
        let videoConnection = stillImageOutput.connectionWithMediaType(AVMediaTypeVideo) else { return }

    videoConnection.fixVideoOrientation()

    stillImageOutput.captureStillImageAsynchronouslyFromConnection(videoConnection) {
        (imageDataSampleBuffer, error) -> Void in
        guard imageDataSampleBuffer != nil && error == nil else {
            errorMessage("Couldn't snap photo")
            return
        }

        let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer)

        let metadata = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate))
        let metadataMutable = CFDictionaryCreateMutableCopy(nil, 0, metadata)

        let utcDate = "\(NSDate())"
        let cfUTCDate = CFStringCreateCopy(nil, utcDate)
        CFDictionarySetValue(metadataMutable!, unsafeAddressOf(kCGImagePropertyGPSDateStamp), unsafeAddressOf(cfUTCDate))

        guard let image = UIImage(data: data)?.fixOrientation() else { return }
        CFDictionarySetValue(metadataMutable, unsafeAddressOf(kCGImagePropertyOrientation), unsafeAddressOf(1))

        success(image, metadataMutable)
    }
}

Here is my code for saving the image.

func saveImageAsJpg(image: UIImage, metadata: CFMutableDictionary) {
    // Add metadata to image
    guard let jpgData = UIImageJPEGRepresentation(image, 1) else { return }
    jpgData.writeToFile("\(self.documentsDirectory)/image1.jpg", atomically: true)
}

How to solve the problem:

Solution 1:

I ended up figuring out how to get everything to work the way I needed it to. The thing that helped me the most was finding out that a CFDictionary can be cast as a NSMutableDictionary.

Here is my final code:

As you can see I add a property to the EXIF dictionary for the date digitized, and changed the orientation value.

private func snapPhoto(success: (UIImage, NSMutableDictionary) -> Void, errorMessage: String -> Void) {
    guard !self.stillImageOutput.capturingStillImage,
        let videoConnection = stillImageOutput.connectionWithMediaType(AVMediaTypeVideo) else { return }

    videoConnection.fixVideoOrientation()

    stillImageOutput.captureStillImageAsynchronouslyFromConnection(videoConnection) {
        (imageDataSampleBuffer, error) -> Void in
        guard imageDataSampleBuffer != nil && error == nil else {
            errorMessage("Couldn't snap photo")
            return
        }

        let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(imageDataSampleBuffer)

        let rawMetadata = CMCopyDictionaryOfAttachments(nil, imageDataSampleBuffer, CMAttachmentMode(kCMAttachmentMode_ShouldPropagate))
        let metadata = CFDictionaryCreateMutableCopy(nil, 0, rawMetadata) as NSMutableDictionary

        let exifData = metadata.valueForKey(kCGImagePropertyExifDictionary as String) as? NSMutableDictionary
        exifData?.setValue(NSDate().toString("yyyy:MM:dd HH:mm:ss"), forKey: kCGImagePropertyExifDateTimeDigitized as String)

        metadata.setValue(exifData, forKey: kCGImagePropertyExifDictionary as String)
        metadata.setValue(1, forKey: kCGImagePropertyOrientation as String)

        guard let image = UIImage(data: data)?.fixOrientation() else {
            errorMessage("Couldn't create image")
            return
        }

        success(image, metadata)
    }
}

And my final code for saving the image with the metadata:

Lots of guard statements, which I hate, but it is better than force unwrapping.

func saveImage(withMetadata image: UIImage, metadata: NSMutableDictionary) {
    let filePath = "\(self.documentsPath)/image1.jpg"

    guard let jpgData = UIImageJPEGRepresentation(image, 1) else { return }

    // Add metadata to jpgData
    guard let source = CGImageSourceCreateWithData(jpgData, nil),
        let uniformTypeIdentifier = CGImageSourceGetType(source) else { return }
    let finalData = NSMutableData(data: jpgData)
    guard let destination = CGImageDestinationCreateWithData(finalData, uniformTypeIdentifier, 1, nil) else { return }
    CGImageDestinationAddImageFromSource(destination, source, 0, metadata)
    guard CGImageDestinationFinalize(destination) else { return }

    // Save image that now has metadata
    self.fileService.save(filePath, data: finalData)
}

Here is my updated save method (Not the exact same that I was using when I wrote this question, since I have updated to Swift 2.3, but the concept is the same):

public func save(fileAt path: NSURL, with data: NSData) throws -> Bool {
    guard let pathString = path.absoluteString else { return false }
    let directory = (pathString as NSString).stringByDeletingLastPathComponent

    if !self.fileManager.fileExistsAtPath(directory) {
        try self.makeDirectory(at: NSURL(string: directory)!)
    }

    if self.fileManager.fileExistsAtPath(pathString) {
        try self.delete(fileAt: path)
    }

    return self.fileManager.createFileAtPath(pathString, contents: data, attributes: [NSFileProtectionKey: NSFileProtectionComplete])
}

Solution 2:

I made a greatly simplified version of the code above. It does make an image file, but as Carlos has noted, no custom metadata is in the file when you load it up again. According to other threads, this simply may not be possible.

func saveImage(_ image: UIImage, withMetadata metadata: NSMutableDictionary, atPath path: URL) -> Bool {
    guard let jpgData = UIImageJPEGRepresentation(image, 1) else {
        return false
    }
    // make an image source
    guard let source = CGImageSourceCreateWithData(jpgData as CFData, nil), let uniformTypeIdentifier = CGImageSourceGetType(source) else {
        return false
    }
    // make an image destination pointing to the file we want to write
    guard let destination = CGImageDestinationCreateWithURL(path as CFURL, uniformTypeIdentifier, 1, nil) else {
        return false
    }

    // add the source image to the destination, along with the metadata
    CGImageDestinationAddImageFromSource(destination, source, 0, metadata)

    // and write it out
    return CGImageDestinationFinalize(destination)
}

Hope this helps!