I am adding exif data to images on iPhone device using the following code:
func testPhoto(identifier: String) {
let result =
PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil)
PHCachingImageManager
.default()
.requestImageData(for: result.firstObject!, options: nil) {
(data, uti, orientation, info) in
_ = self.addExif(data: data!)
}
}
func addExif(data: Data) -> NSData? {
let selectedImageSourceRef =
CGImageSourceCreateWithData(data as CFData, nil)!
let imagePropertiesDictionaryRef =
CGImageSourceCopyPropertiesAtIndex(selectedImageSourceRef, 0, nil)
var imagePropsDictionary: [String: Any] =
imagePropertiesDictionaryRef as! [String : Any]
var exifData = [String: Any]()
let newImageData = NSMutableData()
let imageDestination =
CGImageDestinationCreateWithData(newImageData, kUTTypeJPEG, 1, nil)!
exifData[kCGImagePropertyExifDateTimeOriginal as String] = NSDate()
imagePropsDictionary[kCGImagePropertyExifDictionary as String] = exifData
CGImageDestinationAddImageFromSource(
imageDestination,
selectedImageSourceRef,
0,
imagePropsDictionary as CFDictionary)
if !CGImageDestinationFinalize(imageDestination) {
NSLog("Could not finalize")
}
return newImageData
}
The method crashes when trying to finalize the destination data source (CGImageDestinationFinalize).
The app terminates due to memory pressure. This happens for huge images (20,000x20,000 pixels). Regular images pass through this method just fine.
Is there any way to add EXIF info to image without causing memory pressure?
Thanks!
CGImageSource is designed to serve as a data source without having to load all the data into memory at once. Rather than using CGImageSourceCreateWithData, use CGImageSourceCreateWithURL(_:_:) to create an image source from a file on disk. The system will then manage streaming the data as needed without having to load it all into memory at once.
Related
I have a UIImage object and some Exif data I need to preserve on a separate dictionary. I'm combining both of those to a Data object using this method:
static func convert(image: UIImage, imageMetadata: [String: Any]) -> Data {
var imageMetadata = imageMetadata
imageMetadata[kCGImagePropertyTIFFOrientation as String] = image.imageOrientation.rawValue
if var tiff = imageMetadata[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
tiff[kCGImagePropertyOrientation as String] = image.imageOrientation.rawValue
imageMetadata[kCGImagePropertyTIFFDictionary as String] = tiff
}
let destData = NSMutableData() as CFMutableData
if let data = image.jpegData(compressionQuality: 0.2) as NSData?,
let imageSource = CGImageSourceCreateWithData(data, nil),
let cgImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil),
let destination = CGImageDestinationCreateWithData(
destData as CFMutableData,
kUTTypeJPEG,
1,
nil) {
CGImageDestinationAddImage(destination, cgImage, imageMetadata as CFDictionary)
CGImageDestinationFinalize(destination)
} else {
assertionFailure("can't attach exif")
}
return destData as Data
}
The method itself works and the output is the expected output, however, for some reason, there's a memory leak after performing this operation and I can't seem to find the root cause (Instruments didn't really help - it just helped me to find that this method was causing the leak).
Any idea what's causing the memory leak and how can I prevent it? (tried forcing an autoreleasepool as well but that didn't change anything).
I am removing exif and location metadata from images using Photos and image I/O frameworks:
First I get Data from PHAssets:
let manager = PHImageManager()
manager.requestImageData(for: currentAsset, options: options) { (data, dataUTI, orientation, info) in
if let data = data {
dataArray.append(data)
}
}
Then I use this function to remove metadata:
fileprivate func removeMetadataFromData(data: Data) -> NSMutableData? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {return nil}
guard let type = CGImageSourceGetType(source) else {return nil}
let count = CGImageSourceGetCount(source)
let mutableData = NSMutableData(data: data)
guard let destination = CGImageDestinationCreateWithData(mutableData, type, count, nil) else {return nil}
let removeExifProperties: CFDictionary = [String(kCGImagePropertyExifDictionary) : kCFNull, String(kCGImagePropertyGPSDictionary): kCFNull] as CFDictionary
for i in 0..<count {
CGImageDestinationAddImageFromSource(destination, source, i, removeExifProperties)
}
guard CGImageDestinationFinalize(destination) else {return nil}
return mutableData
}
Then I use this to create UIImage from NSMutableData objects that I get from previous function:
let image = UIImage(data: mutableData as Data)
and I save the image to user's library like so:
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
let placeholder = request.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(for: collection)
if let placeholder = placeholder, let albumChangeRequest = albumChangeRequest {
albumChangeRequest.addAssets([placeholder] as NSArray)
}
return mutableData
}
The problem I have is that using this method, the output file is compressed, and also the name and DPI of the resulting image is different from the original image. I want to keep everything the same as the original image and just remove the metadata. Is there a way to do that?
The problem is the round-trip through UIImage. Just save the Data obtained from requestImageDataAndOrientation.
func saveCopyWithoutLocation(for asset: PHAsset) {
let options = PHImageRequestOptions()
manager.requestImageDataAndOrientation(for: asset, options: options) { data, dataUTI, orientation, info in
guard let data = data else { return }
self.library.performChanges {
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: data, options: nil)
request.location = nil
} completionHandler: { success, error in
if success {
print("successful")
} else {
print(error?.localizedDescription ?? "no error?")
}
}
}
}
Now, that only removes location. If you really want to remove more EXIF data obtained through CGImageSourceCreateWithData, you can do that. But just avoid an unnecessary round-trip through a UIImage. It is the whole purpose to use CGImageSource functions, namely that you can change metadata without changing the underlying image payload. (Round-tripping through UIImage is another way to strip meta data, but as you have discovered, it changes the image payload, too, though often not observable to the naked eye.)
So, if you want, just take the data from CGImageDestination functions directly, and pass that to PHAssetCreationRequest. But I might advise being a little more discriminating about which EXIF metadata you choose to remove, because some of it is important, non-confidential image data (e.g., likely the DPI is in there).
Regarding the filename, I'm not entirely sure you can control that. E.g., I've had images using the above location-stripping routine, and some preserve the file name in the copy, and others do not (and the logic of which applies is not immediately obvious to me; could be the sourceType). Obviously, you can use PHAssetChangeRequest rather than PHAssetCreationRequest, and you can just update the original PHAsset, and that would preserve the file name, but you might not have intended to edit the original asset and may have preferred to make a new copy.
I am currently adding the ability for users to drag and drop in my app in iOS 11. The app supports dropping images into a UIImageView.
I would like to find the GPS data from the image that is being dropped in from the files app. I am using the code below to find the other data but cannot access the GPS data.
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: UIImage.self) { imageItems in
let images = imageItems as! [UIImage]
self.imageView.image = images.first
let data = UIImageJPEGRepresentation(images.first!, 1.0)
let imageData: NSData = data! as NSData
let imageDataOptional: NSData? = data as? NSData
if let imageSource = CGImageSourceCreateWithData(imageData, nil) {
let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)! as NSDictionary
print("Metadata: \(imageProperties)")
print("DPI: \(imageProperties["DPIHeight"])")
print("Width: \(imageProperties["PixelWidth"])")
print("Height: \(imageProperties["PixelHeight"])")
}
}
I am able to find the data by creating a CIImage from the file URL when choosing from the Document picker (see below).
let ciimg = CIImage(contentsOf: url)
if ciimg != nil {
let metadata = ciimg!.properties
print("Meta: \(metadata)")
}
Is it possible to access the GPS Data from the UIImage or find out the URL to do it the CIImage way and if so, how?
I have a UIImage, and I wish to save it to Photos with the DPI metadata set. I understand that a UIImage is immutable, so I have to create a new UIImage. I've used this answer as a reference to create an swift extension function on UIImage that produces a new UIImage with the DPI set. I'm successfully saving this to Photos with UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil). I email this to myself (set as 'Actual Size') and open it in Preview but the DPI is always left at 72.
Here is the function that I converted:
func imageWithDPI(dpi :Int) -> UIImage? {
guard let sourceImageData = UIImageJPEGRepresentation(self, 0.8) else {
print("Couldn't make PNG Respresentation of UIImage")
return nil
}
guard let source = CGImageSourceCreateWithData(sourceImageData, nil) else {
print("Couldn't create source with sourceImageData")
return nil
}
var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String:AnyObject] ?? [String:AnyObject]()
metadata[kCGImagePropertyDPIWidth as String] = dpi
metadata[kCGImagePropertyDPIHeight as String] = dpi
var exifDictionary = metadata[kCGImagePropertyTIFFDictionary as String] as? [String:AnyObject] ?? [String:AnyObject]()
exifDictionary[kCGImagePropertyTIFFXResolution as String] = dpi
exifDictionary[kCGImagePropertyTIFFYResolution as String] = dpi
metadata[kCGImagePropertyTIFFDictionary as String] = exifDictionary
var jfifDictionary = metadata[kCGImagePropertyJFIFDictionary as String] as? [String:AnyObject] ?? [String:AnyObject]()
jfifDictionary[kCGImagePropertyJFIFXDensity as String] = dpi
jfifDictionary[kCGImagePropertyJFIFYDensity as String] = dpi
jfifDictionary[kCGImagePropertyJFIFVersion as String] = 1
metadata[kCGImagePropertyJFIFDictionary as String] = jfifDictionary
guard let uti = CGImageSourceGetType(source) else {
print("Couldn't get type from source")
return nil
}
let destinationImageData = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(destinationImageData, uti, 1, nil) else {
print("Couldn't create destination with destinationImageData")
return nil
}
CGImageDestinationAddImageFromSource(destination, source,0, metadata)
CGImageDestinationFinalize(destination)
return UIImage(data: destinationImageData)
}
I'm kind of out of my depth in Core Graphics, so any advice would be much appreciated.
Many thanks in advance.
UIImage may be stripping all of the metadata from the image when you create it from the destinationImageData, including the DPI. I recently ran into this issue when sharing images via iOS’s UIActivityItemProvider API. If I returned a UIImage * from the - (id)item method then all metadata was lost. The fix was to return either an NSData * or an NSURL *.
Try returning the destinationImageData directly, or saving it to a file & emailing that to yourself.
I’m curious as to why you want to set the DPI for an image? DPI is mostly meaningless for nearly all digital images these days (unless an image is meant to exactly represent the dimensions of a physical object). Actual pixel resolution is far more important than DPI.
Have a reference to an image only having its NSURL.
How to get a GPS metadata from it?
Of course, I can load a UIImage from NSURL, but then what?
Majority of answers I've found here is regarding UIImagePicker, and using ALAssets then, but I have no such option.
Answering my own question. The memory-effective and fast way to get a GPS metadata is
let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse]
if let data = NSData(contentsOfURL: url), imgSrc = CGImageSourceCreateWithData(data, options) {
let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options) as Dictionary
let gpsData = metadata[kCGImagePropertyGPSDictionary] as? [String : AnyObject]
}
The second option is
if let img = CIImage(contentsOfURL: url), metadata = img.properties(),
gpsData = metadata[kCGImagePropertyGPSDictionary] as? [String : AnyObject] { … }
it looks nicer in Swift but uses more memory (tested via Profiler).
Updated version for Swift 3:
let options = [kCGImageSourceShouldCache as String: kCFBooleanFalse]
if let data = NSData(contentsOfURL: url), let imgSrc = CGImageSourceCreateWithData(data, options as CFDictionary) {
let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options as CFDictionary) as? [String : AnyObject]
if let gpsData = metadata?[kCGImagePropertyGPSDictionary as String] {
//do interesting stuff here
}
}