Created
July 6, 2022 21:19
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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