4

I'm trying to write an iOS app in Swift that will let the user take a photo, and then I'm going to overlay some additional data onto the image. I would like the images to include location data. I'm using AVCapturePhoto, and I can see in the documentation that it has some metadata variables, but I can't find any info on how to use them. When I take a photo now with my app, it has no location data in the EXIF info.

How can I set the capture sessions to embed the location data?

Alec.
  • 5,371
  • 5
  • 34
  • 69
DJFriar
  • 340
  • 7
  • 21

2 Answers2

8

I haven't dealt with the new IOS 11 AVCapturePhoto object so this answer has to make a couple of assumptions about how to access data but in theory all of this should work.

Before adding location data you need to ask the user if you can use their location. Add the "Privacy - Location When In Use" tag to your Info.plist. Then add the following code somewhere in your initialisation.

// Request authorization for location manager 
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
    break

case .notDetermined:
    locationManager.requestWhenInUseAuthorization()
    locationManagerStatus = CLLocationManager.authorizationStatus()

default:
    locationManagerStatus = .denied
}

Now the following will create a dictionary with location information in it.

// create GPS metadata properties
func createLocationMetadata() -> NSMutableDictionary? {

    guard CLLocationManager.authorizationStatus() == .authorizedWhenInUse else {return nil}

    if let location = locationManager.location {
        let gpsDictionary = NSMutableDictionary()
        var latitude = location.coordinate.latitude
        var longitude = location.coordinate.longitude
        var altitude = location.altitude
        var latitudeRef = "N"
        var longitudeRef = "E"
        var altitudeRef = 0

        if latitude < 0.0 {
            latitude = -latitude
            latitudeRef = "S"
        }

        if longitude < 0.0 {
            longitude = -longitude
            longitudeRef = "W"
        }

        if altitude < 0.0 {
            altitude = -altitude
            altitudeRef = 1
        }

        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy:MM:dd"
        gpsDictionary[kCGImagePropertyGPSDateStamp] = formatter.string(from:location.timestamp)
        formatter.dateFormat = "HH:mm:ss"
        gpsDictionary[kCGImagePropertyGPSTimeStamp] = formatter.string(from:location.timestamp)
        gpsDictionary[kCGImagePropertyGPSLatitudeRef] = latitudeRef
        gpsDictionary[kCGImagePropertyGPSLatitude] = latitude
        gpsDictionary[kCGImagePropertyGPSLongitudeRef] = longitudeRef
        gpsDictionary[kCGImagePropertyGPSLongitude] = longitude
        gpsDictionary[kCGImagePropertyGPSDOP] = location.horizontalAccuracy
        gpsDictionary[kCGImagePropertyGPSAltitudeRef] = altitudeRef
        gpsDictionary[kCGImagePropertyGPSAltitude] = altitude

        if let heading = locationManager.heading {
            gpsDictionary[kCGImagePropertyGPSImgDirectionRef] = "T"
            gpsDictionary[kCGImagePropertyGPSImgDirection] = heading.trueHeading
        }

        return gpsDictionary;
    }
    return nil
}

This is where I have to do some guessing as I haven't dealt with IOS 11 AVPhotoCapture. You will need a file data representation of your image data. I assume

AVCapturePhoto.fileDataRepresentation() 

returns this. Also you will need the original file metadata. I'll take a guess that

AVCapturePhoto.metadata

contains this. With those assumptions the following function will give you a file data representation with additional location data. There may be newer IOS 11 methods to do this in a cleaner way.

func getFileRepresentationWithLocationData(photo : AVCapturePhoto) -> Data {
    // get image metadata
    var properties = photo.metadata

    // add gps data to metadata
    if let gpsDictionary = createLocationMetadata() {
        properties[kCGImagePropertyGPSDictionary as String] = gpsDictionary
    }

    // create new file representation with edited metadata
    return photo.fileDataRepresentation(withReplacementMetadata:properties,
        replacementEmbeddedThumbnailPhotoFormat:photo.embeddedThumbnailPhotoFormat,
        replacementEmbeddedThumbnailPixelBuffer:photo.previewPixelBuffer,
        replacementDepthData:photo.depthData)
}
adamfowlerphoto
  • 2,708
  • 1
  • 11
  • 24
  • 1
    Had a quick look at AVCapturePhoto docs and it looks like there is an easier way to replace meta data. Instead of using the cgImage functions you can use. AVCapturePhoto.fileDataRepresentation(withReplacementMetadata:replacementEmbeddedThumbnailPhotoFormat:replacementEmbeddedThumbnailPixelBuffer:replacementDepthData:) I will edit my answer – adamfowlerphoto Jan 23 '18 at 11:11
3

@adamfowlerphoto gave by far the best answer I could find on this subject!

AVCapturePhoto.fileDataRepresentation(withReplacementMetadata:replacementEmbeddedThumbnailPhotoFormat:replacementEmbeddedThumbnailPixelBuffer:replacementDepthData:) was deprecated in iOS 12. For the new API you must implement the AVCapturePhotoFileDataRepresentationCustomizerProtocol with the method replacementMetadata(). Here is the updated code:

extension CameraViewController: AVCapturePhotoFileDataRepresentationCustomizer {

    // create GPS metadata properties
    func createLocationMetadata() -> NSMutableDictionary? {
        if let location = locationManager.location {
            let gpsDictionary = NSMutableDictionary()
            var latitude = location.coordinate.latitude
            var longitude = location.coordinate.longitude
            var altitude = location.altitude
            var latitudeRef = "N"
            var longitudeRef = "E"
            var altitudeRef = 0

            if latitude < 0.0 {
                latitude = -latitude
                latitudeRef = "S"
            }

            if longitude < 0.0 {
                longitude = -longitude
                longitudeRef = "W"
            }

            if altitude < 0.0 {
                altitude = -altitude
                altitudeRef = 1
            }

            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy:MM:dd"
            gpsDictionary[kCGImagePropertyGPSDateStamp] = formatter.string(from:location.timestamp)
            formatter.dateFormat = "HH:mm:ss"
            gpsDictionary[kCGImagePropertyGPSTimeStamp] = formatter.string(from:location.timestamp)
            gpsDictionary[kCGImagePropertyGPSLatitudeRef] = latitudeRef
            gpsDictionary[kCGImagePropertyGPSLatitude] = latitude
            gpsDictionary[kCGImagePropertyGPSLongitudeRef] = longitudeRef
            gpsDictionary[kCGImagePropertyGPSLongitude] = longitude
            gpsDictionary[kCGImagePropertyGPSDOP] = location.horizontalAccuracy
            gpsDictionary[kCGImagePropertyGPSAltitudeRef] = altitudeRef
            gpsDictionary[kCGImagePropertyGPSAltitude] = altitude

            if let heading = locationManager.heading {
                gpsDictionary[kCGImagePropertyGPSImgDirectionRef] = "T"
                gpsDictionary[kCGImagePropertyGPSImgDirection] = heading.trueHeading
            }

            return gpsDictionary;
        }
        return nil
    }
    
    func getFileRepresentationWithLocationData(photo : AVCapturePhoto) -> Data {
        // get image metadata
        var properties = photo.metadata

        // add gps data to metadata
        if let gpsDictionary = createLocationMetadata() {
            properties[kCGImagePropertyGPSDictionary as String] = gpsDictionary
        }

        // create new file representation with edited metadata
        
        return photo.fileDataRepresentation(with: self) ?? Data()
    }
    
    func replacementMetadata(for photo: AVCapturePhoto) -> [String : Any]? {
        var properties = photo.metadata

        // add gps data to metadata
        if let gpsDictionary = createLocationMetadata() {
            properties[kCGImagePropertyGPSDictionary as String] = gpsDictionary
        }
        return properties
    }
}

To print the result you could use this:

    static func printEXIFData(imageData: Data) {
        var exifData: CFDictionary? = nil
        imageData.withUnsafeBytes {
            let bytes = $0.baseAddress?.assumingMemoryBound(to: UInt8.self)
            if let cfData = CFDataCreate(kCFAllocatorDefault, bytes, imageData.count),
               let source = CGImageSourceCreateWithData(cfData, nil) {
                exifData = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
                print(exifData)
            }
        }
    }

Don't try to convert your imageData into JPEG representation because it will discard the GPS metadata. (Source: https://stackoverflow.com/a/10339278/647644)

let data = jpegData(compressionQuality: 1.0) // NOOOOO
Lindemann
  • 3,336
  • 3
  • 29
  • 27