Editing Photo metadata and PHAdjustmentData - ios

The following function loads a photo, edits the exif metadata attached to it and saves it back out. The function only seems to work for photos that already have a PHAdjustmentData attached ie photos that have been edited with another application previously. If a photo hasn't been edited it fails in the performChanges() block and prints
Failed to save. Error: Optional(Error Domain=NSCocoaErrorDomain Code=-1 "(null)").
Why does it fail in this situation? Looking through Stack Overflow I have seen a number of other versions of this question but none of them seemed to get resolved. I know this fails if the image saved is a PNG but my original image is a JPEG so the saved image is also a JPEG.
func editPhotoProperties(_ asset: PHAsset) {
let options = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = { data in
return false
}
asset.requestContentEditingInput(with: options) { input, info in
if let input = input {
let adjustmentData = PHAdjustmentData(formatIdentifier:"viewfinder", formatVersion:"1.0", data:"viewfinder".data(using:.utf8)!)
let output = PHContentEditingOutput(contentEditingInput:input)
output.adjustmentData = adjustmentData
do {
let imageData = try Data(contentsOf:input.fullSizeImageURL!)
} catch {
print("Failed to load data")
return
}
let properties = getImageDataProperties(imageData)!
let properties2 = properties.mutableCopy() as! NSMutableDictionary
// edit properties2
...
let newImageData = addImageProperties(imageData: imageData, properties: properties2)
do {
try newImageData!.write(to: output.renderedContentURL, options: .atomic)
} catch {
print("Failed to write to disk")
return
}
PHPhotoLibrary.shared().performChanges({
let changeRequest = PHAssetChangeRequest(for:asset)
changeRequest.contentEditingOutput = output
}) { success, error in
if !success {
print("Failed to save. Error: \(String(describing:error))")
}
}
}
}
}
func getImageDataProperties(_ data: Data) -> NSDictionary? {
if let imageSource = CGImageSourceCreateWithData(data as CFData, nil) {
if let dictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) {
return dictionary
}
}
return nil
}
// add image properties (exif, gps etc) to image
func addImageProperties(imageData: Data, properties: NSDictionary?) -> Data? {
// create an imagesourceref
if let source = CGImageSourceCreateWithData(imageData as CFData, nil) {
// this is of type image
if let uti = CGImageSourceGetType(source) {
// create a new data object and write the new image into it
let destinationData = NSMutableData()
if let destination = CGImageDestinationCreateWithData(destinationData, uti, 1, nil) {
// add the image contained in the image source to the destination, overidding the old metadata with our modified metadata
CGImageDestinationAddImageFromSource(destination, source, 0, properties)
if CGImageDestinationFinalize(destination) == false {
return nil
}
return destinationData as Data
}
}
}
return nil
}

Related

Loading images from external storage using Core Graphics not working iOS 13

I am attempting to load photos located on external storage (SD card) using core graphics in iOS 13 (beta). The code below works fine when the files are on the device. When the files are on external storage however it fails returning nil and I don't know why.
I believe I am using the correct security scoping.
I loaded the file URLs from a security scoped folder url as per Providing Access to Directories
guard folderUrl.startAccessingSecurityScopedResource() else {
return nil
}
defer { folderUrl.stopAccessingSecurityScopedResource() }
guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, options) else {
throw Error.failedToOpenImage(message: "Failed to open image at \(imageURL)")
}
So... for my own project, where I ran into the same issue, I now have the following function to give me a thumbnail, going from elegant and quick to brute force.
static func thumbnailForImage(at url: URL, completion: (Result<UIImage, Error>) -> Void) {
let shouldStopAccessing = url.startAccessingSecurityScopedResource()
defer { if shouldStopAccessing { url.stopAccessingSecurityScopedResource() } }
let coordinator = NSFileCoordinator()
var error: NSError?
coordinator.coordinate(readingItemAt: url, options: .withoutChanges, error: &error) { url in
var thumbnailImage: UIImage?
var storedError: NSError?
var imageSource: CGImageSource?
print("Strategy 1: Via URL resource key")
do {
let resourceKeys = Set([URLResourceKey.thumbnailDictionaryKey])
let resources = try url.resourceValues(forKeys: resourceKeys)
if let dict = resources.thumbnailDictionary, let resource = dict[URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey] {
thumbnailImage = resource
} else {
throw "No thumbnail dictionary"
}
} catch let error {
storedError = error as NSError
}
let options = [kCGImageSourceCreateThumbnailFromImageIfAbsent: true, kCGImageSourceShouldAllowFloat: true, kCGImageSourceCreateThumbnailWithTransform: true]
if thumbnailImage == nil {
print("Strategy 2: Via CGImageSourceCreateWithURL")
imageSource = CGImageSourceCreateWithURL(url as CFURL, options as CFDictionary)
}
if thumbnailImage == nil && imageSource == nil {
print("Strategy 3: Via CGImageSourceCreateWithData")
let data = try? Data.init(contentsOf: url)
if let data = data {
imageSource = CGImageSourceCreateWithData(data as CFData, options as CFDictionary)
}
}
if let imageSource = imageSource, thumbnailImage == nil {
print("Attempting thumbnail creation from source created in strategy 2 or 3")
if let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) {
thumbnailImage = UIImage(cgImage: image)
}
}
if let thumbnailImage = thumbnailImage {
print("Success")
completion(.success(thumbnailImage))
} else {
print("Failure")
if let error = storedError { completion(.failure(error)) }
else { completion(.failure("Everything just fails...")) }
}
}
if let error = error { completion(.failure(error)) }
}
Basically it works by trying to get a thumbnail via the URL resources first. This is the quickest and nicest way, of it works. If that fails, I try CGImageSourceCreateWithURL. That works most of the time, except on remote storage. I suspect that's still a bug and submitted a feedback ticket to apple for this. I suggest you do the same. Last attempt, just try to read the entire file using NSData and creating an image source via CGImageSourceCreateWithData...
So far, if it's an image file I, this seems to produce a thumbnail most of the time. It can be quite slow though, having to read the entire file.

How to save a group (array) of images into firebase cloud storage?

I am attempting to save an array of images the user took into cloud storage and then save a URL to that image under that persons profile. How can I do this? Currently I wrote the code bellow which save one images at a time taken by the user but I was told this was a bad way of doing it. Additionally images are not being added but replacing the one before it.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let userPickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
// let imageToUse = PhotoArray()
// let data = UIImagePNGRepresentation(userPickedImage) //here convert to data
PhotoArray.sharedInstance.photosArray.append(userPickedImage) //append converted data in array
// do {
// try realm.write {
// realm.add(imageToUse)
// }
// } catch {
// print(“error adding image to array\(error)“)
// }
imageView.image = userPickedImage
//-----------------------------//
//Create a reference to the image
let imageRef = Storage.storage().reference().child("image.jpg")
// Get image data
if let uploadData = UIImagePNGRepresentation(userPickedImage) {
// Upload image to Firebase Cloud Storage
imageRef.putData(uploadData, metadata: nil) { (metadata, error) in
guard error == nil else {
// Handle error
return
}
// Get full image url
imageRef.downloadURL { (url, error) in
guard let downloadURL = url else {
// Handle error
return
}
// Save url to database
Firestore.firestore().collection("images").document("myImage").setData(["imageUrl" : downloadURL.absoluteString])
}
}
}
//-----------------------------//
}
// print(PhotoArray().photosArray.count)
imagePicker.dismiss(animated: true, completion: nil)
}
I found this in the firebase docs:
// Add a new document with a generated id.
var ref: DocumentReference? = nil
ref = db.collection("cities").addDocument(data: [
"name": "Tokyo",
"country": "Japan"
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(ref!.documentID)")
}
}
Hope this will help you.
You can read about here: Click me

Modifing metadata from existing phAsset seems not working

In my App I want to make it possible, that the user sets an StarRating from 0 to 5 for any Image he has in his PhotoLibrary. My research shows, that there are a couple of ways to get this done:
Save the exif metadata using the new PHPhotoLibrary
Swift: Custom camera save modified metadata with image
Writing a Photo with Metadata using Photokit
Most of these Answers were creating a new Photo. My snippet now looks like this:
let options = PHContentEditingInputRequestOptions()
options.isNetworkAccessAllowed = true
self.requestContentEditingInput(with: options, completionHandler: {
(contentEditingInput, _) -> Void in
if contentEditingInput != nil {
if let url = contentEditingInput!.fullSizeImageURL {
if let nsurl = url as? NSURL {
if let imageSource = CGImageSourceCreateWithURL(nsurl, nil) {
var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary?
if imageProperties != nil {
imageProperties![kCGImagePropertyIPTCStarRating] = rating as AnyObject
let imageData = NSMutableData(contentsOf: url)
let image = UIImage(contentsOfFile: url.path)
let destination = CGImageDestinationCreateWithData(imageData!, CGImageSourceGetType(imageSource)!, 1, nil)
CGImageDestinationAddImage(destination!, image!.cgImage!, imageProperties! as CFDictionary)
var contentEditingOutput : PHContentEditingOutput? = nil
if CGImageDestinationFinalize(destination!) {
let archievedData = NSKeyedArchiver.archivedData(withRootObject: rating)
let identifier = "com.example.starrating"
let adjustmentData = PHAdjustmentData(formatIdentifier: identifier, formatVersion: "1.0", data: archievedData)
contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput!)
contentEditingOutput!.adjustmentData = adjustmentData
if imageData!.write(to: contentEditingOutput!.renderedContentURL, atomically: true) {
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest(for: self)
request.contentEditingOutput = contentEditingOutput
}, completionHandler: {
success, error in
if success && error == nil {
completion(true)
} else {
completion(false)
}
})
}
} else {
completion(false)
}
}
}
}
}
}
})
Now when I want to read the metadata from the PHAsset I request the ContentEditingInput again and do the following:
if let url = contentEditingInput!.fullSizeImageURL {
if let nsurl = url as? NSURL {
if let imageSource = CGImageSourceCreateWithURL(nsurl, nil) {
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
if let starRating = imageProperties[kCGImagePropertyIPTCStarRating] as? Int {
rating = starRating
}
}
}
}
}
But I never get my rating because it says that the value of imageProperties[kCGImagePropertyIPTCStarRating] is nil.
I also tried the examples from the Answers I posted above, but I always get the same result.
I hope anybody knows, what I can do to change the Metadata.
Also, how can I change the Metadata from an PHAsset with the MediaType .video? I tried to achieve that through the AVAssetWriter and AVExportSession Objects, but in both cases it does not work. Here what I tried for Videos:
var exportSession = AVAssetExportSession(asset: asset!, presetName: AVAssetExportPresetPassthrough)
exportSession!.outputURL = outputURL
exportSession!.outputFileType = AVFileTypeQuickTimeMovie
exportSession!.timeRange = CMTimeRange(start: start, duration: duration)
var modifiedMetadata = asset!.metadata
let metadataItem = AVMutableMetadataItem()
metadataItem.keySpace = AVMetadataKeySpaceQuickTimeMetadata
metadataItem.key = AVMetadataQuickTimeMetadataKeyRatingUser as NSCopying & NSObjectProtocol
metadataItem.value = rating as NSCopying & NSObjectProtocol
modifiedMetadata.append(metadataItem)
exportSession!.metadata = modifiedMetadata
exportSession!.exportAsynchronously(completionHandler: {
let status = exportSession?.status
let success = status == AVAssetExportSessionStatus.completed
if success {
do {
let sourceURL = urlAsset.url
let manager = FileManager.default
_ = try manager.removeItem(at: sourceURL)
_ = try manager.moveItem(at: outputURL, to: sourceURL)
} catch {
LogError("\(error)")
completion(false)
}
} else {
LogError("\(exportSession!.error!)")
completion(false)
}
})
Sorry this isn't a full answer but it covers one part of your question. I noticed you are placing the StarRating in the wrong place. You need to place it in a IPTC dictionary. Also the properties data is stored as strings. Given you have the imageProperties you can add the star rating as follows and read it back using the following two functions
func setIPTCStarRating(imageProperties : NSMutableDictionary, rating : Int) {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSMutableDictionary {
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
} else {
let iptc = NSMutableDictionary()
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
imageProperties[kCGImagePropertyIPTCDictionary] = iptc
}
}
func getIPTCStarRating(imageProperties : NSMutableDictionary) -> Int? {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSDictionary {
if let starRating = iptc[kCGImagePropertyIPTCStarRating] as? String {
return Int(starRating)
}
}
return nil
}
As the imageProperties you get from the image are not mutable you need to create a mutable copy of these properties first before you can call the functions above. When you create your image to save use the mutable properties in your call to CGImageDestinationAddImage()
if let mutableProperties = imageProperties.mutableCopy() as? NSMutableDictionary {
setIPTCStarRating(imageProperties:mutableProperties, rating:rating)
}
One other point you are creating an unnecessary UIImage. If you use CGImageDestinationAddImageFromSource() instead of CGImageDestinationAddImage() you can use the imageSource you created earlier instead of loading the image data into a UIImage.

How do you set kCGImagePropertyExifUserComment on a CGImageMetadataRef?

How do you set kCGImagePropertyExifUserComment on a CGImageMetadataRef in iOS? These are the only functions I have found and they are only for MacOS
https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.9.sdk/System/Library/Frameworks/ImageIO.framework/Versions/A/Headers/CGImageMetadata.h
Given image data you can get it's properties with the following
func getImageDataProperties(_ data: Data) -> NSDictionary? {
if let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil)
{
if let dictionary = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) {
return dictionary
}
}
return nil
}
The exif comments can be accessed as follows with the properties
if let properties = getImageDataProperties(data) {
if let exif = properties[kCGImagePropertyExifDictionary] as? NSDictionary {
if let comment = exif[kCGImagePropertyExifUserComment] as? String {
...
}
}
}
Once you have edited the comments you can save your image with the following given you original image data and your altered properties dictionary
// add image properties (exif, gps etc) to image
func addImageProperties(imageData: Data, properties: NSMutableDictionary) -> Data? {
// create an imagesourceref
if let source = CGImageSourceCreateWithData(imageData as CFData, nil) {
// this is of type image
if let uti = CGImageSourceGetType(source) {
// create a new data object and write the new image into it
let destinationData = NSMutableData()
if let destination = CGImageDestinationCreateWithData(destinationData, uti, 1, nil) {
// add the image contained in the image source to the destination, overidding the old metadata with our modified metadata
CGImageDestinationAddImageFromSource(destination, source, 0, properties)
if CGImageDestinationFinalize(destination) == false {
return nil
}
return destinationData as Data
}
}
}
return nil
}
Create a mutable copy of the CGImageMetadataRef with CGImageMetadataCreateMutableCopy. Then set the property with
CGImageMetadataSetValueMatchingImageProperty(
mutableCopy,
kCGImagePropertyExifDictionary,
kCGImagePropertyExifUserComment,
value)
where value is your comment as a CFTypeRef.

Swift: How to Delete EXIF data from picture taken with AVFoundation?

I'm trying to get rid of the EXIF data from a picture taken with AVFoundation, How can I do this in swift (2) preferred, Objective-C is okay too, I know how to convert the code to swift.
Why?
I have done my research and I see a lot of famous Social Media (Reddit Source and many more) do remove EXIF data for identity purposes and other purposes.
If you think this is duplicate post, please read what I'm asking and provide link. Thank you.
My answer is based a lot on this previous question. I adapted the code to work on Swift 2.0.
class ImageHelper {
static func removeExifData(data: NSData) -> NSData? {
guard let source = CGImageSourceCreateWithData(data, 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
}
// Check the keys for what you need to remove
// As per documentation, if you need a key removed, assign it kCFNull
let removeExifProperties: CFDictionary = [String(kCGImagePropertyExifDictionary) : kCFNull, String(kCGImagePropertyOrientation): kCFNull]
for i in 0..<count {
CGImageDestinationAddImageFromSource(destination, source, i, removeExifProperties)
}
guard CGImageDestinationFinalize(destination) else {
return nil
}
return mutableData;
}
}
Then you can simply do something like this:
let imageData = ImageHelper.removeExifData(UIImagePNGRepresentation(image))
In my example, I remove the rotation and the EXIF data. You can easily search the keys if you need anything else removed. Just make extra checks on the data generated as it is an optional.
Here is a solution that removes the exif from raw data. Exif in jpeg is inside APP1 frame. Frame start is indicated with FF_E1. Frame end at next FF byte that does not follow 00 value.
var data: Data = ... // read jpeg one way or another
var app1_start = 0
var app1_end = Int.max
for i in 0 ..< data.count {
if data[i] == 0xFF {
if data[i + 1] == 0xE1 {
print("found start \(i)")
app1_start = i
} else if app1_start > 0, data [i] != 0x00 {
app1_end = i - 1
print("found end \(i-1)")
break
}
}
}
data.removeSubrange(Range(app1_start...app1_end))
Data in this example is assumed to be jpeg. Code loops through byte array and stores APP1 start and end. Then removes the data from original mutable data. More about jpeg structure here https://en.wikipedia.org/wiki/JPEG
You have UIImage right?
Then you can convert UIImage to Data and save it to image again new image will not have any EXIF data
Swift 3
let imageData:Data = UIImagePNGRepresentation(image!)!
func saveToPhotoLibrary_iOS9(data:NSData, completionHandler: #escaping (PHAsset?)->()) {
var assetIdentifier: String?
PHPhotoLibrary.requestAuthorization { (status:PHAuthorizationStatus) in
if(status == PHAuthorizationStatus.authorized){
PHPhotoLibrary.shared().performChanges({
let creationRequest = PHAssetCreationRequest.forAsset()
let placeholder = creationRequest.placeholderForCreatedAsset
creationRequest.addResource(with: PHAssetResourceType.photo, data: data as Data, options: nil)
assetIdentifier = placeholder?.localIdentifier
}, completionHandler: { (success, error) in
if let error = error {
print("There was an error saving to the photo library: \(error)")
}
var asset: PHAsset? = nil
if let assetIdentifier = assetIdentifier{
asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil).firstObject//fetchAssetsWithLocalIdentifiers([assetIdentifier], options: nil).firstObject as? PHAsset
}
completionHandler(asset)
})
}else {
print("Need authorisation to write to the photo library")
completionHandler(nil)
}
}
}
Swift 5 version of the accepted answer:
extension Data {
func byRemovingEXIF() -> Data? {
guard let source = CGImageSourceCreateWithData(self as NSData, nil),
let type = CGImageSourceGetType(source) else
{
return nil
}
let count = CGImageSourceGetCount(source)
let mutableData = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(mutableData, type, count, nil) else {
return nil
}
let exifToRemove: CFDictionary = [
kCGImagePropertyExifDictionary: kCFNull,
kCGImagePropertyGPSDictionary: kCFNull,
] as CFDictionary
for index in 0 ..< count {
CGImageDestinationAddImageFromSource(destination, source, index, exifToRemove)
if !CGImageDestinationFinalize(destination) {
print("Failed to finalize")
}
}
return mutableData as Data
}
}

Resources