10

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

I need to:

  • Rotate the image to the orientation from the metadata
  • Remove orientation from the metadata
  • Save the date taken to the metadata
  • Save that image with the metadata to the documents directory

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)
}
SeanRobinson159
  • 894
  • 10
  • 19

2 Answers2

13

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])
}
SeanRobinson159
  • 894
  • 10
  • 19
  • I'm assuming that I can get `filePath` from `NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first! + "name.png"`. But, what is `fileService`? Is `fileService` a custom Class? – Carlos.V Sep 26 '16 at 20:24
  • 1
    @Carlos.V `fileService` is a custom class I made to wrap the `NSFileManager` and make my life easier. – SeanRobinson159 Sep 26 '16 at 21:07
  • I am able to read the actual metadata and modify it, but cannot write the new meta to the file. Can you please explain a bit how works the `.save()` function?, I tried using `fileMan.createFileAtPath(filePath, contents: finalData, attributes: metadata as! [String : AnyObject] )` but doesn't save the metadata, it is for other attributes. Maybe, are you using `PHPhotoLibrary`? I'm thinking do try that way but I am not sure it would work 100%. – Carlos.V Sep 27 '16 at 01:25
  • @Carlos.V I have updated my answer to include the code I used for saving. – SeanRobinson159 Sep 27 '16 at 19:49
  • Thanks @SeanRobinson159 for your help. Can I send you a PM?, I can't believe I am not able to do it, I'm doing exactly the same, I can read the actual metadata captured before modifying it, I modify it, I create the path, I generate the new data from `UIImage` and `Metadata`, I send it to save, I read the Image using `UIImage(contentsOfFile: self.filePath)!`, but if I print the new file metadata, it shows exactly the same as before, like if nothing happened. – Carlos.V Sep 28 '16 at 00:05
  • @Carlos.V Yeah, you can private message me. – SeanRobinson159 Sep 28 '16 at 14:52
  • 2
    Just for those of you who want to use this piece of code, remember to `import ImageIO` so you can access all the CGImageSourcemethods. – Fernando Mata Oct 07 '16 at 09:21
  • @Carlos.V, any luck? I am having the same problem - I can get the original metadata and can see those values when I load the file, but when I save my own data using the method above it does not exist when I load it back in. – Maury Markowitz Mar 14 '17 at 16:05
  • @MauryMarkowitz I couldn't do it this way, I tried a lot of different ways, neither creating a new image file was storing metadata info =/, I opt to change a lot of things and code, and finally ended up using `Photos` library, and at the time I save a new image file, I did it in a new PhotoAlbum (so this can be apart from users photos) and set the `CLLocation` because that was the info I wanted to store in every image. Any doubt? – Carlos.V Mar 15 '17 at 16:35
  • This should be easier! I think I may have erased your codeshare somehow, can you check? – Maury Markowitz Mar 15 '17 at 17:45
  • @Carlos.V - they should make this easier! So I think what you did was make a new PhotoAlbum and put the CLLocation where you wanted and then used the PA stuff to save? If so, would you codeshare that? – Maury Markowitz Mar 15 '17 at 17:54
-1

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)
}
Maury Markowitz
  • 9,082
  • 11
  • 46
  • 98
  • It works for me. I am taking the produced file, uploading it to a server, and it has the new metadata. So it definitely IS possible. – SeanRobinson159 Aug 02 '17 at 14:44
  • Hello, how do i add metadata to this function? Also, if i want to save the photos to the photos library how would i go about doing that? – Karthik Kannan Feb 15 '18 at 13:32
  • Make the dictionary and pass it in. Just be sure that the keys in the dictionary are ones that Apple supports, otherwise they will look like they are being saved, but will not end up in the actual file when you save it. – Maury Markowitz Feb 16 '18 at 15:37
  • I've got this updating the `data` but when I convert the data back into an `image source` it looses the data. I think it may be because a `UIImage` is an _impoverished_ reading of the `image source`. How can I ensure a `UIImage` has all the Exif data. Or at least the key/value I've added (UserComment). @SeanRobinson159 – Mudlabs Jul 19 '18 at 21:25
  • 1
    @SeanRobinson159 - in my experiments I was able to retrieve any key/value as long as it was one that Apple's own EXIF code "understood". So if you put in custom keys they disappear, but if you use one that Apple uses (for some other purpose) it appears to work. This is limiting, but perhaps still useful. – Maury Markowitz May 20 '20 at 13:31