Skip to content

Instantly share code, notes, and snippets.

@rob-secondstage
Created July 6, 2022 21:19
Show Gist options
  • Save rob-secondstage/99d30f4e9804c5f250dcb063f779fd87 to your computer and use it in GitHub Desktop.
Save rob-secondstage/99d30f4e9804c5f250dcb063f779fd87 to your computer and use it in GitHub Desktop.
Adds GPS metadata to an existing image file without re-encoding or re-compressing the image. Swift, iOS.
import AVFoundation
import CoreLocation
import OSLog
private let logger = Logger(subsystem: "utilities", category: "images")
/// Adds the GPS metadata from `location` to the image file at `url` without re-encoding the image.
/// - Note: The image at `url` must be `JPG` or `HEIC` format. `HEIF` will not work.
/// - Warning: The file at `url` will be overwritten with a new file containing the updated metadata.
/// - Parameters:
/// - url: The location of the image file on disk.
/// - location: A `CLLocation` object whose information is to be added to the image at `url`.
func addLocationMetadata(to url: URL, location: CLLocation) {
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
logger.error("\(#fileID) \(#function): Error creating source")
return
}
guard let uti: CFString = CGImageSourceGetType(source), let fileType = UTType(uti as String) else {
logger.error("\(#fileID) \(#function): Error getting file type")
return
}
let tempURL = URL.temporary.appending(path: UUID().uuidString).appendingPathExtension(for: fileType)
guard let destination = CGImageDestinationCreateWithURL(tempURL as CFURL, uti as CFString, 1, nil) else {
logger.error("\(#fileID) \(#function): Error creating destination")
return
}
var newMetadata: CGMutableImageMetadata
if let imageMetadata = CGImageSourceCopyMetadataAtIndex(source, 0, nil) {
newMetadata = CGImageMetadataCreateMutableCopy(imageMetadata) ?? CGImageMetadataCreateMutable()
} else {
newMetadata = CGImageMetadataCreateMutable()
}
let gpsMetadata = location.exifMetadata()
for (key, value) in gpsMetadata {
let success = CGImageMetadataSetValueMatchingImageProperty(newMetadata, kCGImagePropertyGPSDictionary, key, value as! NSObject)
logger.log("\(#fileID) \(#function) set: \(key): \(success)")
}
let options = [
kCGImageDestinationMetadata as String : newMetadata
] as [String : Any]
var err: Unmanaged<CFError>?
guard CGImageDestinationCopyImageSource(destination, source, options as CFDictionary, &err) else {
logger.error("\(#fileID) \(#function): Failed to write metadata")
print(err ?? "none")
return
}
var fileError: NSError?
NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &fileError,
byAccessor: { (newURL: URL) -> Void in
do {
try FileManager.default.removeItem(at: url)
try FileManager.default.moveItem(at: tempURL, to: url)
} catch {
logger.error("\(#function): Failed to save attachment data to \(url)")
}
})
if let nsError = fileError {
print("###\(#function): \(nsError.localizedDescription)")
}
try? FileManager.default.removeItem(at: tempURL)
}
extension CLLocation {
/// Returns a dictionary of GPS metadata values suitable for writing into an image file.
func exifMetadata() -> [CFString:Any] {
var gpsMetadata: [CFString:Any] = [:]
let altitudeRef = Int(altitude < 0.0 ? 1 : 0)
let latitudeRef = coordinate.latitude < 0.0 ? "S" : "N"
let longitudeRef = coordinate.longitude < 0.0 ? "W" : "E"
gpsMetadata[kCGImagePropertyGPSLatitude] = abs(coordinate.latitude)
gpsMetadata[kCGImagePropertyGPSLongitude] = abs(coordinate.longitude)
gpsMetadata[kCGImagePropertyGPSLatitudeRef] = latitudeRef
gpsMetadata[kCGImagePropertyGPSLongitudeRef] = longitudeRef
gpsMetadata[kCGImagePropertyGPSAltitude] = Int(abs(altitude))
gpsMetadata[kCGImagePropertyGPSAltitudeRef] = altitudeRef
gpsMetadata[kCGImagePropertyGPSHPositioningError] = horizontalAccuracy
gpsMetadata[kCGImagePropertyGPSImgDirection] = course
gpsMetadata[kCGImagePropertyGPSImgDirectionRef] = "T"
gpsMetadata[kCGImagePropertyGPSSpeed] = speed
gpsMetadata[kCGImagePropertyGPSSpeedRef] = "K"
return gpsMetadata
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment