I am trying to convert UIImage into PHAsset but could not find any solution. All I found is the vice versa (i.e. PHAsset to UIImage).
The scenario is, I am fetching images from a custom directory into PHAssetCollection and showing in UICollectionView. I also want to include few more images here from previous screen which are coming from remote source. I don't want to persist remote images into my directory but want to include them in my UICollectionView.
Kindly suggest me solution or some good alternate so that the source of UICollectionView will be same (i.e. PHAssetCollection)
A bit late to the party, but I'll post my answer for people of the future.
What I did in my app was to take a screenshot and save to a dedicated album in the Photos app.
getAlbum() in the below example is just a helper method that retrieves the photo album.
ImageStoreItem is just a wrapper with two fields: image: UIImage and id: String for the identifier of the original PHAsset.
/**
Saves the image to the album in the Photos app.
- Throws:
- `ImageStore.TypeError.accessDenied`
if the user has denied access to the photo library.
- An error thrown by `PHPhotoLibrary`.
*/
func saveImage(_ image: UIImage) throws {
let album = try getAlbum()
try PHPhotoLibrary.shared().performChangesAndWait {
let imgReq = PHAssetChangeRequest.creationRequestForAsset(from: image),
albReq = PHAssetCollectionChangeRequest(for: album)!
albReq.addAssets([imgReq.placeholderForCreatedAsset] as NSFastEnumeration)
}
}
/**
Fetches images in the app album.
- Throws:
- `ImageStore.TypeError.accessDenied`
if the user has denied access to the photo library.
- An error thrown by `PHPhotoLibrary`.
- `NSError` thrown by `PHImageManager`
if fetching images from the `PHAsset` list failed.
- `Globalerror.unknown` if no image was fetched,
but there is no corresponding error object.
- Returns:
An array of `ImageStoreItem`s with the album items.
*/
func getImages() throws -> [ImageStoreItem] {
let album = try ImageStore.shared.getAlbum(),
assets = PHAsset.fetchAssets(in: album, options: nil)
let size = UIScreen.main.bounds.size,
opt = PHImageRequestOptions()
opt.isSynchronous = true
var res = [ImageStoreItem]()
var err: Error? = nil
let handler: (PHAsset) -> (UIImage?, [AnyHashable : Any]?) -> Void =
{ (asset) in
{ (image, info) in
if let image = image {
let item = ImageStoreItem(image: image,
id: asset.localIdentifier)
res.append(item)
} else if let info = info {
if let error = info[PHImageErrorKey] {
err = error as! NSError
} else {
var userInfo = [String : Any]()
for (key, value) in info {
userInfo[String(describing: key)] = value
}
err = GlobalError.unknown(info: userInfo)
}
}
}
}
assets.enumerateObjects { (asset, _, _) in
PHImageManager.default()
.requestImage(for: asset,
targetSize: size,
contentMode: .default,
options: opt,
resultHandler: handler(asset))
}
if let error = err { throw error }
return res
}
The documentation on the PhotoKit isn't too impressive, but take a look here and there and you'll get the hang of it.
You do not want to convert a PHAsset to UIImage. I do not know if it is possible; it shouldn't be, since PHAsset and UIImage have different properties, behaviours, etc. The vice versa is possible because all the properties of UIImage can be derived from PHAsset but not vice-versa.
Instead, change the data source of the collection view to [UIImage] while converting PHAsset to UIImage instead. This approach is clean and solves your other problem of having to convert UIImage to PHImage.
Related
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 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 successfully saved an image to Photo Library using Photo Framework API's after user selected that image from library using UIImagePickerController.
As I am capturing image inside my app, I have to take care of the metadata. I could save location & creationDate to the newly captured image and saved it into library.
But the problem is that I want to attach a ton of other properties, I mean I want to attach EXIF & TIFF properties.
There are a lot of properties in EXIF like this,
kCGImagePropertyExifDateTimeOriginal
kCGImagePropertyExifMaxApertureValue
kCGImagePropertyExifSpatialFrequencyResponse
kCGImagePropertyExifLensModel
How am I supposed to get these values from the current device and attach it to image and save it to Photo Library?
Here is the complete code which I am using to save an Image to Photo Library after taking it from UIImagePickerController. I would like to put attaching metadata lines somewhere in this function before saving it to Photo Library.
//MARK: Saving an Image to PhotoLibrary and taking back the PHAsset
class func savingThis(image : UIImage, completion : (asset : PHAsset?) -> ())
{
var localIdentifier : String?
let imageManager = PHPhotoLibrary.sharedPhotoLibrary()
imageManager.performChanges({ () -> Void in
let request = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
request.location = LocationManager.sharedInstance.locationManager.location
request.creationDate = NSDate()
// Add more properties here - somewhere EXIF, TIFF....
if let properAsset = request.placeholderForCreatedAsset {
localIdentifier = properAsset.localIdentifier
}
else {
completion(asset: nil)
}
}, completionHandler: { (success, error) -> Void in
if let properLocalIdentifier = localIdentifier {
let result = PHAsset.fetchAssetsWithLocalIdentifiers([properLocalIdentifier], options: nil)
if result.count > 0 {
completion(asset: result[0] as? PHAsset)
}
else {
completion(asset: nil)
}
}
else {
completion(asset: nil)
}
})
}
Is this a dumb question? I don't have to use Photos Framework if there is another way to attach metadata, whatever way, I just wanna attach it. I don't know. Let me know your suggestions?
I am working on an app that can show image metadata and share the image without the metadata. My app also has action extension that can view metadata.
When I try to share an image within my app using the share sheet, and select the action extension of my own app, I noticed that loadItemForTypeIdentifier doesn't return the NSURL, instead it is giving me an UIImage. Is there anyway that I can force the return type as NSURL?
if let itemProvider = extensionItem.attachments?[0] as? NSItemProvider {
//println("\(itemProvider.description)")
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
// This is an image. We'll load it, then place it in our image view.
itemProvider.loadItemForTypeIdentifier(kUTTypeImage as String, options: nil, completionHandler: { (item, error) -> Void in
if let photoURL = item as? NSURL {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.fetchPhotoMetadata(photoURL)
let imageData = NSData(contentsOfURL: photoURL)
self.imageView.image = UIImage(data: imageData!)
}
}
})
}
In Apple's documentation, it says for "item"
The item to be loaded. When specifying your block, set the type of
this parameter to the specific data type you want. For example, when
requesting text data, you might set the type to NSString or
NSAttributedString. The item provider attempts to coerce the data to
the class you specify.
When I try to specify the return type in the closure as (item: NSURL!, error: NSError!) -> Void ..., I got the following error
Cannot invoke 'loadItemForTypeIdentifier' with an argument list of
type '(String, options: nil, completionHandler: (NSURL!, NSError!) ->
Void)'
if let itemProvider = extensionItem.attachments?[0] as? NSItemProvider {
//println("\(itemProvider.description)")
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
// This is an image. We'll load it, then place it in our image view.
itemProvider.loadItemForTypeIdentifier(kUTTypeImage as String, options: nil, completionHandler: { (item: NSURL!, error: NSError!) -> Void in
if let photoURL = item as? NSURL {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.fetchPhotoMetadata(photoURL)
let imageData = NSData(contentsOfURL: photoURL)
self.imageView.image = UIImage(data: imageData!)
}
}
})
}
Has anyone here encounter the similar issue?
It appears that the shared attachment can take the form of an NSURL or a UIImage depending on the context it is launched from. I am inferring this from a Monotouch port that has been modified to handle this possibility. Sadly this change is absent from the original Apple sample code.
Once you have registered a share extension, it appears that this code is called in preference to the application.openURL method that was previously the entry point for registered document types. This is invoked in the context of your own application with richer access to the shared object data (hence the UIImage), whereas share extensions appear to be based around a common NSURL written by the host app to some sort of sandboxed DMZ.
I'm sure this is not the whole story, but the Apple documentation is frustratingly opaque.
Use kUTTypeURL instead of kUTTypeImage in these both statements
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
// This is an image. We'll load it, then place it in our image view.
itemProvider.loadItemForTypeIdentifier(kUTTypeURL as String, options:
I'm trying to do the equivalent of writeImageToSavedPhotosAlbum with the new Photo framework.
To save the image, I only do this:
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
let image = info[UIImagePickerControllerOriginalImage] as UIImage
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
let changeRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
}, completionHandler: { (success, error) -> Void in
//
})
}
Of course, there's no magic and since I don't do anything of
info[UIImagePickerControllerMediaMetadata]
, the above code doesn't save any metadata to the "Camera Roll", as you can see using the screenshot of the Preview.app when I connect my iPhone to my Mac.
You get that view by opening Preview.app, selecting File > Import from "you device name"; then you can browse your pictures and see that those taken with the Camera app show exif data such as focal length, while those saved with the above code show empty values.
Now the documentation for creationRequestForAssetFromImage says:
To set metadata properties of the newly created asset, use the
corresponding properties of the change request (listed in Modifying
Assets).
Which links to "+changeRequestForAsset:" and 4 properties (creationDate, location, favorite, hidden), that's a little light. What about the other properties one might want to save (like aperture, focal length, shutter speed, …)?
How are you supposed to save your meta data along the image with the Photo framework?
Here's what I ended up doing:
extension UIImage {
/**
Gets the metadata from the photo album
:param: info The picker dictionary
:param: completionHandler A block to call when the metadata is available
*/
class func requestMetadata(info: [NSObject : AnyObject], completionHandler: ([NSObject : AnyObject]) -> Void) {
let assetUrl = info[UIImagePickerControllerReferenceURL] as! NSURL
let result = PHAsset.fetchAssetsWithALAssetURLs([assetUrl], options: nil)
if let asset = result.firstObject as? PHAsset {
let editOptions = PHContentEditingInputRequestOptions()
editOptions.networkAccessAllowed = true
asset.requestContentEditingInputWithOptions(editOptions, completionHandler: { (contentEditingInput, _) -> Void in
let url = contentEditingInput.fullSizeImageURL
let orientation = contentEditingInput.fullSizeImageOrientation
var inputImage = CoreImage.CIImage(contentsOfURL: url)
completionHandler(inputImage.properties())
})
} else {
completionHandler([:])
}
}
}