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.
Related
I am uploading an array of images to firebase, which was previously filled by up to three photos taken by camera.
After each upload, I save the downloadURL.
But I see, that the order of the images is random.
I suspect that it depends on the photosize, which photo is uploaded first.
How can I ensure, that the first image in imageArray will be also the first image uploaded and therefore the first downloadURL I get?
func storeInFirestore(var1:String, var2:String, var3:String, var4:String, var5: String) {
guard let user = Auth.auth().currentUser?.uid else {return}
var data = NSData()
var i = 0
for items in imageArray {
i += 1
if i <= imageArray.count {
data = items.images.jpegData(compressionQuality: 0.8)! as NSData
let filePath = "\(user)/images"
let metaData = StorageMetadata()
let ref = Storage.storage().reference().child("\(user)_\(var1)_\(i)")
metaData.contentType = "image/jpg"
ref.putData(data as Data, metadata: metaData){(metaData,error) in
if let error = error {
print(error.localizedDescription)
return
}else{
[get the downloadURL and store in array...]
Just enumerate the loop and use n, the index value, to construct the array. You can also use a dictionary instead of an array and simply use n as the key (and the file name as the value).
for (n, img) in imageArray.enumerated() {
let data = img.images.jpegData(compressionQuality: 0.8)
let filePath = "\(user)/images"
let storageRef = Storage.storage().reference().child("\(user)_\(var1)_\(n)")
let metaData = StorageMetadata()
metaData.contentType = "image/jpg"
storageRef.putData(data, metadata: metaData) { (metaData, error) in
if let _ = metaData {
// image successfully saved to storage
// Remember, `n` is still in scope which is the array index
// (i.e. 0 is the first image, 1 is the second, etc.) so
// simply construct your array using these indices. To simplify
// things, you can use a dictionary here instead of an array,
// which could look something like `remoteImagePaths[n] = remotePath`.
} else {
if let error = error {
print(error.localizedDescription)
}
}
}
}
Solution
Well kind of... knowing if an uploaded image is the first in the list when the callback is triggered can be tricky as the first image could be massive, so it takes a while to upload, then the second image is small, so it doesn't take as long, therefore the second image is uploaded first. I gather you already know this from your post though, just wanted to clarify this for others who visit this issue.
Now as for fixing it, there's a few ways, but I think the cleanest one is this.
func storeInFirestore(var1:String, var2:String, var3:String, var4:String, var5: String, completion: #escaping([URL]) -> Void) {
guard let user = Auth.auth().currentUser?.uid else {return}
var data = NSData()
var imageToUrlsPair = [UIImage: URL]()
var imagesDownloadedCount: Int = 0
for items in imageArray {
let data = items.images.jpegData(compressionQuality: 0.8)
let filePath = "\(user)/images"
let metaData = StorageMetadata()
let ref = Storage.storage().reference().child("\(user)_\(var1)_\(i)")
metaData.contentType = "image/jpg"
ref.putData(data, metadata: metaData){(metaData,error) in
if let error = error {
imagesDownloadedCount += 1 // Handle this error however you please, you could fail this entire request.
print(error.localizedDescription)
return
} else {
imagesDownloadedCount += 1
imageToUrlsPair[items.images] = [get the downloadURL and store in array...]
if imagesDownloadedCount == imageArray.count {
completion(imageToUrlsPair.map { $0.value })
}
}
}
}
}
So to sum up what I've done:
I've made a dictionary to contain a pair of images to urls (the imageToUrlsPair variables)
Once an image is downloaded, it adds the url to the associated image and increments the downloaded image counter (imagesDownloadedCount)
Once the final image is downloaded, the imagesDownloadedCount will equal the imageArray.count so it will trigger the completion callback.
I have added a completion callback so that this function performs its network requests asynchronously and returns the urls once all requests have been completed in the background.
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
}
}
Firebase Storage Image Not Downloading in Tableview. If I replace the line let tempImageRef = storage.child("myFiles/sample.jpg"), it's showing the single image. But if try to grab all the images inside 'myFiles' , it doesn't work. Please help
func fetchPhotos()
{
//let database = FIRDatabase.database().reference()
let storage = FIRStorage.storage().reference(forURL: "gs://fir-test-bafca.appspot.com")
let tempImageRef = storage.child("myFiles/")
tempImageRef.data(withMaxSize: (1*1024*1024)) { (data, error) in
if error != nil{
print(error!)
}else{
print(data!)
let pic = UIImage(data: data!)
self.picArray.append(pic!)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
You are referencing an entire folder (myFiles/). That wont work. You need to reference each image individually.
One way to do this is to write the image metadata to your realtime database when you upload an image. Then read the metadata (more specifically the path) and then reference the image that way.
If you have 3 images you want to upload and store, you could write the metadata as follows:
/images
/image1key
/metdata
/path
/image2key
/metdata
/path
/image3key
/metdata
/path
Then you can query your database for your images path's like so
let ref = Firebase.database().ref("images")
ref.observe(.value, withCompletion: { snapshot in
if let values = snapshot.value as? [String : Any] {
if let metadata = values["metadata"] as? [String: Any] {
let imagePath = metadata["path"]
//Now download your image from storage.
}
}
})
This isn't the cleanest code and you can definitely improve, but it will work :)
I want to list all photos from "My Photo Stream", here is my code:
private func fetchAssetCollection(){
let result = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumMyPhotoStream, options: nil)
result.enumerateObjects({ (collection, index, stop) in
if let albumName = collection.localizedTitle {
print("Album => \(collection.localIdentifier), \(collection.estimatedAssetCount), \(albumName) ")
}
let assResult = PHAsset.fetchAssets(in: collection, options: nil)
let options = PHImageRequestOptions()
options.resizeMode = .exact
let scale = UIScreen.main.scale
let dimension = CGFloat(78.0)
let size = CGSize(width: dimension * scale, height: dimension * scale)
assResult.enumerateObjects({ (asset, index, stop) in
print("index \(index)")
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: options) { (image, info) in
if let name = asset.originalFilename {
print("photo \(name) \(index) \(asset.localIdentifier)")
}
}
})
})
}
extension PHAsset {
var originalFilename: String? {
var fname:String?
if #available(iOS 9.0, *) {
let resources = PHAssetResource.assetResources(for: self)
if let resource = resources.first {
fname = resource.originalFilename
}
}
if fname == nil {
// this is an undocumented workaround that works as of iOS 9.1
fname = self.value(forKey: "filename") as? String
}
return fname
}
}
it works, but the problem is that it print duplicated record.
It prints 329*2 records but actually I have 329 photos in my "My Photo stream".
photo IMG_0035.JPG 10 0671E1F3-CB7C-459E-8111-FCB381175F29/L0/001
photo IMG_0035.JPG 10 0671E1F3-CB7C-459E-8111-FCB381175F29/L0/001
......
From the documentation for PHImageManager requestImage:
By default, this method executes asynchronously. If you call it from a background thread you may change the isSynchronous property of the options parameter to true to block the calling thread until either the requested image is ready or an error occurs, at which time Photos calls your result handler.
For an asynchronous request, Photos may call your result handler block more than once. Photos first calls the block to provide a low-quality image suitable for displaying temporarily while it prepares a high-quality image. (If low-quality image data is immediately available, the first call may occur before the method returns.) When the high-quality image is ready, Photos calls your result handler again to provide it. If the image manager has already cached the requested image at full quality, Photos calls your result handler only once. The PHImageResultIsDegradedKey key in the result handler’s info parameter indicates when Photos is providing a temporary low-quality image.
So either make the request synchronous or check the PHImageResultIsDegradedKey value from the info dictionary to see if this instance of the image is the one you actually wish to keep or ignore.
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.