How do I create a UIImage with the DPI set? - ios

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.

Related

Memory Leak when using ImageIO to convert a UIImage and it's metadata to a Data object

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).

How to enrich UIImage data with metadata

In my app I'm trying to have image data have metadata (location, timestamp etc.). I'm using UIImagePickerController to do image capture, and the delegate function of which has:
info[UIImagePickerController.InfoKey.originalImage]
info[UIImagePickerController.InfoKey.phAsset]
info[UIImagePickerController.InfoKey.mediaMetadata]
So for the images picked from the library .phAsset has everything I need. I just use .getDataFromPHAsset function to get the enriched data. However, for the images that were just taken .phAsset is nil. I thought about trying to somehow combine .originalImage and .mediaMetadata together into single Data object, but can't get the result desired. I tried to use this approach: https://gist.github.com/kwylez/a4b6ec261e52970e1fa5dd4ccfe8898f
I know I can also make custom imageCapture controller, using AVCaptureSession, and use AVCapturePhoto function .fileDataRepresentation() on didFinishProcessingPhoto delegate call, but that's not my first choice.
Any kind of help is highly appreciated.
I'm pretty sure mediaMetadata will not have location info. Use this CLLocation extension.
Then use the function below to add the metadata to the image data:
func addImageProperties(imageData: Data, properties: NSMutableDictionary) -> Data? {
let dict = NSMutableDictionary()
dict[(kCGImagePropertyGPSDictionary as String)] = properties
if let source = CGImageSourceCreateWithData(imageData as CFData, nil) {
if let uti = CGImageSourceGetType(source) {
let destinationData = NSMutableData()
if let destination = CGImageDestinationCreateWithData(destinationData, uti, 1, nil) {
CGImageDestinationAddImageFromSource(destination, source, 0, dict as CFDictionary)
if CGImageDestinationFinalize(destination) == false {
return nil
}
return destinationData as Data
}
}
}
return nil
}
Usage:
if let imageData = image.jpegData(compressionQuality: 1.0), let metadata = locationManager.location?.exifMetadata() {
if let newImageData = addImageProperties(imageData: imageData, properties: metadata) {
// newImageData now contains exif metadata
}
}

Saving Data as UIImage while preserving the original image characteristics

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.

cannot assign value of type 'UIImage?' to type 'NSData?' in swift 3

I am using CoreData for an app. I have set image as BinaryData in data model. But I have fetched image from server as UIImage and it throws error as:
cannot assign value of type 'UIImage?' to type 'NSData?
I searched but couldn't find any resemblance solution for it. Can anyone help me in swift 3?
My code is:
let url1:URL = URL(string:self.appDictionary.value(forKey: "image") as! String)!
let picture = "http://54.243.11.100/storage/images/news/f/"
let strInterval = String(format:"%#%#",picture as CVarArg,url1 as CVarArg) as String as String
let url = URL(string: strInterval as String)
SDWebImageManager.shared().downloadImage(with: url, options: [],progress: nil, completed: {[weak self] (image, error, cached, finished, url) in
if self != nil {
task.imagenews = image //Error:cannot assign value of type 'UIImage?' to type 'NSData?'
}
})
The error message is pretty clear - you cannot assign UIImage object to a variable of NSData type.
To convert UIImage to Swift's Data type, use UIImagePNGRepresentation
var data : Data = UIImagePNGRepresentation(image)
Note that if you're using Swift, you should be using Swift's type Data instead of NSData
You must convert, image into Data (or NSData) to support imagenews data type.
Try this
let url1:URL = URL(string:self.appDictionary.value(forKey: "image") as! String)!
let picture = "http://54.243.11.100/storage/images/news/f/"
let strInterval = String(format:"%#%#",picture as CVarArg,url1 as CVarArg) as String as String
let url = URL(string: strInterval as String)
SDWebImageManager.shared().downloadImage(with: url, options: [],progress: nil, completed: {[weak self] (image, error, cached, finished, url) in
if self != nil {
if let data = img.pngRepresentationData { // If image type is PNG
task.imagenews = data
} else if let data = img.jpegRepresentationData { // If image type is JPG/JPEG
task.imagenews = data
}
}
})
// UIImage extension, helps to convert Image into data
extension UIImage {
var pngRepresentationData: Data? {
return UIImagePNGRepresentation(img)
}
var jpegRepresentationData: Data? {
return UIImageJPEGRepresentation(self, 1.0)
}
}

Adding EXIF data to huge image on iOS

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.

Resources