EXIF data read and write - swift

I searched for getting the EXIF data from picture files and write them back for Swift. But I only could find predefied libs for different languages.
I also found references to "CFDictionaryGetValue", but which keys do I need to get the data? And how can I write it back?

I'm using this to get EXIF infos from an image file:
import ImageIO
let fileURL = theURLToTheImageFile
if let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) {
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
if let dict = imageProperties as? [String: Any] {
print(dict)
}
}
It gives you a dictionary containing various informations like the color profile - the EXIF info specifically is in dict["{Exif}"].

Swift 4
extension UIImage {
func getExifData() -> CFDictionary? {
var exifData: CFDictionary? = nil
if let data = self.jpegData(compressionQuality: 1.0) {
data.withUnsafeBytes {(bytes: UnsafePointer<UInt8>)->Void in
if let cfData = CFDataCreate(kCFAllocatorDefault, bytes, data.count) {
let source = CGImageSourceCreateWithData(cfData, nil)
exifData = CGImageSourceCopyPropertiesAtIndex(source!, 0, nil)
}
}
}
return exifData
}
}
Swift 5
extension UIImage {
func getExifData() -> CFDictionary? {
var exifData: CFDictionary? = nil
if let data = self.jpegData(compressionQuality: 1.0) {
data.withUnsafeBytes {
let bytes = $0.baseAddress?.assumingMemoryBound(to: UInt8.self)
if let cfData = CFDataCreate(kCFAllocatorDefault, bytes, data.count),
let source = CGImageSourceCreateWithData(cfData, nil) {
exifData = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
}
}
}
return exifData
}
}

You can use AVAssetExportSession to write metadata.
let asset = AVAsset(url: existingUrl)
let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality)
exportSession?.outputURL = newURL
exportSession?.metadata = [
// whatever [AVMetadataItem] you want to write
]
exportSession?.exportAsynchronously {
// respond to file writing completion
}

Related

Swift. How to extract the individual sub images from an HEIC image

I need to be able to load an heic image and extract and output all of the sub images as pngs similar to how preview does it. For example, if you open a dynamic heic wallpaper in preview, it shows all the images in the sidebar with their names:
How do you do this? I've tried to use NSImage like below. But that only outputs a single image:
let image = NSImage(byReferencing: url)
image.writePNG(toURL: newUrl)
You need to load the HEIC data, get its CGImageSource and its count. Then create a loop from 0 to count-1 and get each image at the corresponding index. You can create an array with those CGImages in memory or write them to disk (preferred). Note that this will take a while to be executed because of the size of the HEIC file 186MB. Each image extracted will be from 19MB to 28MB.
func extractHeicImages(from url: URL) throws {
let data = try Data(contentsOf: url)
let location = url.deletingLastPathComponent()
let pathExtension = url.pathExtension
let fileName = url.deletingPathExtension().lastPathComponent
let destinationFolder = location.appendingPathComponent(fileName)
guard pathExtension == "heic", let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { return }
let count = CGImageSourceGetCount(imageSource)
try FileManager.default.createDirectory(at: destinationFolder, withIntermediateDirectories: false, attributes: nil)
for index in 0..<count {
try autoreleasepool {
if let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, nil) {
let number = String(format: "#%05d", index)
let destinationURL = destinationFolder
.appendingPathComponent(fileName+number)
.appendingPathExtension(pathExtension)
try NSImage(cgImage: cgImage, size: .init(width: cgImage.width, height: cgImage.height))
.heic?
.write(to: destinationURL)
print("saved image " + number)
}
}
}
}
You will need these helpers as well to extract the cgimate from your image and also to get a HEIC data representation from them:
extension NSImage {
var heic: Data? { heic() }
var cgImage: CGImage? {
var rect = NSRect(origin: .zero, size: size)
return cgImage(forProposedRect: &rect, context: .current, hints: nil)
}
func heic(compressionQuality: CGFloat = 1) -> Data? {
guard
let mutableData = CFDataCreateMutable(nil, 0),
let destination = CGImageDestinationCreateWithData(mutableData, "public.heic" as CFString, 1, nil),
let cgImage = cgImage
else { return nil }
CGImageDestinationAddImage(destination, cgImage, [kCGImageDestinationLossyCompressionQuality: compressionQuality] as CFDictionary)
guard CGImageDestinationFinalize(destination) else { return nil }
return mutableData as Data
}
}
Playground testing. This assumes the "Catalina.heic" is located at your desktop.
let catalinaURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent("Catalina.heic")
do {
try extractHeicImages(from: catalinaURL)
} catch {
print(error)
}
Each subimage is represented by a NSBitmapImageRep. Loop the image reps, convert to png and save:
let imageReps = image.representations
for imageIndex in 0..<imageReps.count {
if let imageRep = imageReps[imageIndex] as? NSBitmapImageRep {
if let data = imageRep.representation(using: .png, properties: [:]) {
do {
let url = folderURL.appendingPathComponent("image \(imageIndex).png", isDirectory: false)
try data.write(to: url, options:[])
} catch {
print("Unexpected error: \(error).")
}
}
}
}
The conversion to png takes some time. Running the conversions in parallel is faster but I'm not sure if it's save:
DispatchQueue.concurrentPerform(iterations: imageReps.count) { iteration in
if let imageRep = imageReps[iteration] as? NSBitmapImageRep {
if let data = imageRep.representation(using: .png, properties: [:]) {
do {
let url = folderURL.appendingPathComponent("image \(iteration).png", isDirectory: false)
try data.write(to: url, options:[])
} catch {
print("Unexpected error: \(error).")
}
}
}
}

Save (override) my jpg file with editing EXIF data

Im open and edit EXIF (or TIFF...) data in my jpg file.
Please help me save (override) my jpg file with editing data.
Working with this simple code:
let fileURL = URL(fileURLWithPath: "/Users/test/Pictures/IMG_2808.jpg")
if let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) {
var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary?
let exifDict = imageProperties?[kCGImagePropertyExifDictionary]
if let UserComment = exifDict?[kCGImagePropertyExifUserComment] ?? nil {
print("kCGImagePropertyExifUserComment: \(UserComment)")
}
else {
exifDict!.setValue("My comment...", forKey: kCGImagePropertyExifUserComment as String)
imageProperties![kCGImagePropertyExifDictionary] = exifDict
// What I should do in the next step for update and override my jpg file on disk?
// Maybe this is not nice way for this realisation? Please help
//
}
}
After some test Im found a solution:
let fileURL = URL(fileURLWithPath: "/Users/test/Pictures/IMG_2808.jpg")
if let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) {
var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary?
let exifDict = imageProperties?[kCGImagePropertyExifDictionary]
if let UserComment = exifDict?[kCGImagePropertyExifUserComment] ?? nil {
print("kCGImagePropertyExifUserComment: \(UserComment)")
}
else {
exifDict!.setValue("My comment...", forKey: kCGImagePropertyExifUserComment as String)
imageProperties![kCGImagePropertyExifDictionary] = exifDict
if let imageDestination = CGImageDestinationCreateWithURL(fileURL as CFURL, kUTTypeJPEG, 1, nil) {
CGImageDestinationAddImageFromSource(imageDestination, imageSource, 0, imageProperties as CFDictionary?)
CGImageDestinationFinalize(imageDestination)
}
}
}
I don't know if this solution is true but it works!
Please comment if you know more perfected way

write GPS metadata to EXIF in Swift

I am trying to write gps coordinates to an image that is taken within my app. So i looked around for code to use, and i wrote a function that takes Data and also returns Data (since i need to send it like this to Firebase.
But no matter how i seem to write this code the metadata won't "stick" to the picture. The picture looks correct when it uploads, but upon download it doesn't contain any more metadata than the size of the photo.
Have I totally missed out on something here?
func setMetaData(imageData: Data) -> Data? {
var source: CGImageSource? = nil
source = CGImageSourceCreateWithData((imageData as CFData?)!, nil)
let metadata = CGImageSourceCopyPropertiesAtIndex(source!, 0, nil) as? [AnyHashable: Any]
var metadataAsMutable = metadata
var EXIFDictionary = (metadataAsMutable?[(kCGImagePropertyExifDictionary as String)]) as? [AnyHashable: Any]
var GPSDictionary = (metadataAsMutable?[(kCGImagePropertyGPSDictionary as String)]) as? [AnyHashable: Any]
if !(EXIFDictionary != nil) {
EXIFDictionary = [AnyHashable: Any]()
}
if !(GPSDictionary != nil) {
GPSDictionary = [AnyHashable: Any]()
}
GPSDictionary![(kCGImagePropertyGPSLatitude as String)] = 30.21313
GPSDictionary![(kCGImagePropertyGPSLongitude as String)] = 76.22346
let dest_data = NSMutableData()
guard let imageDest = CGImageDestinationCreateWithData(dest_data as CFMutableData, kUTTypeJPEG, 1, nil) else { return nil }
CGImageDestinationAddImageFromSource(imageDest, source!, 0, (metadataAsMutable as CFDictionary?))
if CGImageDestinationFinalize(imageDest) {
return dest_data as Data
} else {
print("FAILED")
}
return nil
}
I found a solution that's not super pretty, but it totally does the job
func addMetaData(data: Data) -> NSData? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {return nil}
guard let type = CGImageSourceGetType(source) else {return nil}
let mutableData = NSMutableData(data: data)
guard let destination = CGImageDestinationCreateWithData(mutableData, type, 1, nil) else {
return nil}
guard let path = Bundle.main.url(forResource: "predict_pic", withExtension: "jpg") else {
return nil }
let imageSource = CGImageSourceCreateWithURL(path as CFURL, nil)
let imageProperties = CGImageSourceCopyMetadataAtIndex(imageSource!, 0, nil)
let mutableMetadata = CGImageMetadataCreateMutableCopy(imageProperties!)
CGImageMetadataSetValueMatchingImageProperty(mutableMetadata!, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLatitudeRef, "N" as CFTypeRef)
CGImageMetadataSetValueMatchingImageProperty(mutableMetadata!, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLatitude, location.coordinate.latitude as CFTypeRef)
CGImageMetadataSetValueMatchingImageProperty(mutableMetadata!, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLongitudeRef, "E" as CFTypeRef)
CGImageMetadataSetValueMatchingImageProperty(mutableMetadata!, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLongitude, location.coordinate.longitude as CFTypeRef)
let finalMetadata:CGImageMetadata = mutableMetadata!
CGImageDestinationAddImageAndMetadata(destination, UIImage(data: data)!.cgImage! , finalMetadata, nil)
guard CGImageDestinationFinalize(destination) else { return nil }
return mutableData;
}

OSX - Loading an image and saving it as a smaller png file using Swift

I am looking for a way to to upload an image file, resize it (in order to reduce the total file size), and then save it as a png. I think there must be a fairly straightforward way of doing this, but hours of searching around have yielded no effective results. I have been able to achieve the desired file size by exporting the image as a compressed JPEG, but I need to preserve transparency. Here is the code I used to get the JPEG:
func chooseImage () {
var image = NSImage()
//choose image from hard disk
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.runModal()
panel.allowedFileTypes = ["png", "jpeg", "jpg"]
let chosenFile = panel.URL
//convert to NSData and send to Jpeg function
if chosenFile != nil {
image = NSImage(contentsOfURL: chosenFile!)!
let imageData = image.TIFFRepresentation
self.saveAsJpeg(imageData!, compression: 0.5)
}
}
func saveAsJpeg (image:NSData, compression:NSNumber) {
// make imagerep and define properties
let imgRep = NSBitmapImageRep(data: image)
let props = NSDictionary.init(object: compression, forKey: NSImageCompressionFactor)
let pingy = imgRep?.representationUsingType(NSBitmapImageFileType.NSJPEGFileType, properties: props as! [String : AnyObject])
//save to disk
let documentURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
let folderURL = documentURL.URLByAppendingPathComponent("KKNightlife Data")
let g = GetUniqueID()
let fileName = g.getUniqueID() + ".jpeg"
do {
try NSFileManager.defaultManager().createDirectoryAtURL(folderURL, withIntermediateDirectories: false, attributes: nil)
} catch {
print("cannot create directory - folder Exists?")
}
let url = folderURL.URLByAppendingPathComponent(fileName)
if let pid = pingy {
pid.writeToURL(url, atomically: false)
} else {
print("error saving image")
}
}
I attempted to use the following code to scale the image in order to create a smaller .png file, but no matter what values I enter for the size, the resulting file is the same size (both in terms of height/width and overall file size):
func chooseImage (size:String) {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.runModal()
panel.allowedFileTypes = ["png", "jpeg", "jpg"]
let chosenFile = panel.URL
if chosenFile != nil {
let image = NSImage(contentsOfURL: chosenFile!)
self.scaleImage(image!)
}
}
func scaleImage (image:NSImage) {
//create resized image
let newSize = NSSize(width: 10, height: 10)
var imageRect:CGRect = CGRectMake(0, 0, image.size.width, image.size.height)
let imageRef = image.CGImageForProposedRect(&imageRect, context: nil, hints: nil)
let resizedImage = NSImage(CGImage: imageRef!, size: newSize)
let imageData = resizedImage.TIFFRepresentation
//make imagerep
let imgRep = NSBitmapImageRep(data: imageData!)
let pingy = imgRep?.representationUsingType(NSBitmapImageFileType.NSPNGFileType, properties: [:])
//save to disk
let documentURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
let g = GetUniqueID()
let fileName = g.getUniqueID() + ".png"
let folderURL = documentURL.URLByAppendingPathComponent("KKNightlife Data")
do {
try NSFileManager.defaultManager().createDirectoryAtURL(folderURL, withIntermediateDirectories: false, attributes: nil)
} catch {
print("cannot create directory - folder Exists?")
}
let url = folderURL.URLByAppendingPathComponent(fileName)
if let pid = pingy {
pid.writeToURL(url, atomically: false)
print("image is at \(documentURL)")
} else {
print("error saving image")
}
}
Any suggestions would be greatly appreciated.
I was finally able to do this using the extensions found here:
https://gist.github.com/raphaelhanneken/cb924aa280f4b9dbb480
This is how I ended up calling them in case anyone encounters similar problems:
func chooseImage (size:String) {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.runModal()
panel.allowedFileTypes = ["png", "jpeg", "jpg"]
let chosenFile = panel.URL
if chosenFile != nil {
let image = NSImage(contentsOfURL: chosenFile!)
self.scaleImageUsingExtensions(image!)
}
}
func scaleImageUsingExtensions (image:NSImage){
let size: NSSize = NSMakeSize(10, 10)
let resizedImage = image.resizeWhileMaintainingAspectRatioToSize(size)
let documentURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
let g = GetUniqueID()
let fileName = g.getUniqueID() + ".png"
let folderURL = documentURL.URLByAppendingPathComponent("KKNightlife Data")
do {
try NSFileManager.defaultManager().createDirectoryAtURL(folderURL, withIntermediateDirectories: false, attributes: nil)
} catch {
print("cannot create directory - folder Exists?")
}
let url = folderURL.URLByAppendingPathComponent(fileName)
do {
try resizedImage?.savePNGRepresentationToURL(url)
}
catch {
print("error saving file")
}
}

Saving Video to Parse & Playback

So i'm using this custom class to record my video -- https://github.com/piemonte/PBJVision. I am attempting to record video in my iOS app and I can't seem to get the code correct to upload the file to my parse server. A few things:
In the PBJVision class it allows you to use NSURL(fileWithPath:videoPath) to access the asset after the video has been recorded.
To access the Data in the asset and save to Parse, I use the following function:
func vision(vision: PBJVision, capturedVideo videoDict: [NSObject : AnyObject]?, error: NSError?) {
if error != nil {
print("Encountered error with video")
isVideo = false
} else {
let currentVideo = videoDict
let videoPath = currentVideo![PBJVisionVideoPathKey] as! String
print("The video path is: \(videoPath)")
self.player = Player()
self.player.delegate = self
self.player.view.frame = CGRect(x: cameraView.frame.origin.x, y: cameraView.frame.origin.y, width: cameraView.frame.width, height: cameraView.frame.height)
self.player.playbackLoops = true
videoUrl = NSURL(fileURLWithPath: videoPath)
self.player.setUrl(videoUrl)
self.cameraView.addSubview(self.player.view)
self.player.playFromBeginning()
nextButton.hidden = false
isVideo = true
let contents: NSData?
do {
contents = try NSData(contentsOfFile: videoPath, options: NSDataReadingOptions.DataReadingMappedAlways)
} catch _ {
contents = nil
}
print(contents)
let videoObject = PFObject(className: "EventChatroomMessages")
videoObject.setValue(user, forKey: "user")
videoObject.setValue("uG7v2KWBQm", forKey: "eventId")
videoObject.setValue(NSDate(), forKey: "timestamp")
let videoFile: PFFile?
do {
videoFile = try PFFile(name: randomAlphaNumericString(26) + ".mp4", data: contents!, contentType: "video/mp4")
print("VideoFile: \(videoFile)")
} catch _ {
print("error")
}
print(videoFile)
videoObject.setValue(videoFile, forKey: "image")
videoObject.saveInBackgroundWithBlock {
(success: Bool, error: NSError?) -> Void in
if success == true {
ProgressHUD.showSuccess("Video Saved.", interaction: false)
dispatch_async(dispatch_get_main_queue()) {
ProgressHUD.dismiss()
}
} else {
ProgressHUD.showError("Error Saving Video.", interaction: false)
dispatch_async(dispatch_get_main_queue()) {
ProgressHUD.dismiss()
}
}
}
}
}
I am then using a UITableView to display my data from Parse. Here is how I retrieve my asset back from Parse and into my AVPlayer():
// Create Player for Reaction
let player = Player()
player.delegate = self
player.view.frame = CGRectMake(0.0, nameLabel.frame.origin.y + nameLabel.frame.size.height + 0.0, self.view.frame.width, 150)
player.view.backgroundColor = UIColor.whiteColor()
let video = message.objectForKey("image") as! PFFile
let urlFromParse = video.url!
print(urlFromParse)
let url = NSURL(fileURLWithPath: video.url!)
print(url)
let playerNew = AVPlayer(URL: url!)
let playerLayer = AVPlayerLayer(player: playerNew)
playerLayer.frame = CGRectMake(0.0, nameLabel.frame.origin.y + nameLabel.frame.size.height + 0.0, self.view.frame.width, 150)
cell.layer.addSublayer(playerLayer)
playerLayer.backgroundColor = UIColor.whiteColor().CGColor
playerNew.play()
I copy the value that is returned from urlFromParse which is (http://parlayapp.herokuapp.com/parse/files/smTrXDGZhlYQGh4BZcVvmZ2rYB9kA5EhPkGbj2R2/58c0648ae4ca9900f2d835feb77f165e_file.mp4) and paste it into my browser and the video plays in browser. Am I correct to assume the file has been saved correctly?
When I go to run my app, the video does not play.Any suggestion on what i'm doing wrong?
I have found that playing video using the pfFile.url does not work. You have to write the NSData from the PFFIle to a local file using the right extension (mov) and then play the video using the local file as the source.