Problem: Loading an UIImage with an url obtained from PHAsset returns nil.
Context: I have a UI component which works with image URLs and is populated in different parts of the app with different providers. In a specific case I receive PHAsset.
Code for requesting PHAsset url:
let inputRequestOptions = PHContentEditingInputRequestOptions()
inputRequestOptions.isNetworkAccessAllowed = true
asset.requestContentEditingInput(with: inputRequestOptions, completionHandler: { (edittingInput, _) in
guard let url = edittingInput?.fullSizeImageURL else {
return
}
switch asset.mediaType {
case .image: editMediaContentContainerPresenter.assets = [.image(url)]
case .video: editMediaContentContainerPresenter.assets = [.movie(url)]
default:
debugPrint("unsupported asset")
}
})
Code for loading the URL into an UIImage inside my UI component:
func image(for url: URL, maximumPixelSize: CGFloat) -> UIImage? {
let options = [kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maximumPixelSize] as CFDictionary
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
return nil
}
guard let imageReference = CGImageSourceCreateThumbnailAtIndex(source, 0, options) else {
return nil
}
return UIImage(cgImage: imageReference, scale: UIScreen.main.scale, orientation: .up)
}
Special considerations: I need to transform the PHAsset into an URL because the current UI component is working with many other providers and parts of the app and all of them resolve to URLs. Sometimes the loading from that URL works, but usually maximum once. More often it doesn't manage to load an UIImage from the URL provided.
Question: How can I obtain a valid URL.
Solutions taken into consideration already, but to be avoided:
loading the image with phasset requests and then saving it separately to a local url
passing the UIImage instead of URL's
Related
In one of my project there is a functionality to pick image from Photos. So for that I've used one third party lib named "YangMingShan". The functionality works perfect as per my requirement.
But the problem is that, I want to get the image name picked from the Photos. Whenever I picked single image from photos, it called below method. And it gives me image.
func photoPickerViewController(_ picker: YMSPhotoPickerViewController!, didFinishPicking image: UIImage!) {
}
Can anyone please help me to get the image name from image ?
func photoPickerViewController(_ picker: YMSPhotoPickerViewController!, didFinishPickingImages photoAssets: [PHAsset]!) {
// Remember images you get here is PHAsset array, you need to implement PHImageManager to get UIImage data by yourself
picker.dismiss(animated: true) {
let options = PHImageRequestOptions.init()
options.deliveryMode = .highQualityFormat
options.resizeMode = .exact
options.isSynchronous = true
var imagesWithName:[ImageModel] = []
for asset: PHAsset in photoAssets {
print(asset.originalFilename)
let image = asset.getImage()
let fileName = asset.originalFilename
imagesWithName.append(ImageModel(name: fileName, image: image))
}
print(imagesWithName)
// Assign to Array with images
}
}
the extension of PHAsset to get file name as follows
extension PHAsset {
var primaryResource: PHAssetResource? {
let types: Set<PHAssetResourceType>
switch mediaType {
case .video:
types = [.video, .fullSizeVideo]
case .image:
types = [.photo, .fullSizePhoto]
case .audio:
types = [.audio]
case .unknown:
types = []
#unknown default:
types = []
}
let resources = PHAssetResource.assetResources(for: self)
let resource = resources.first { types.contains($0.type)}
return resource ?? resources.first
}
var originalFilename: String {
guard let result = primaryResource else {
return "file"
}
return result.originalFilename
}
func getImage() -> UIImage {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: self,
targetSize: CGSize(width: self.pixelWidth, height: self.pixelHeight),
contentMode: .aspectFit,
options: option,
resultHandler: {(result, info) -> Void in
thumbnail = result!
})
return thumbnail
}
}
and the model
struct ImageModel {
let name:String
let image:UIImage
}
replace the line 77 in Pod -> YangMingSha -> YMSPhotoPicker -> YMSPhotoPickerViewController.h with
- (void)photoPickerViewController:(YMSPhotoPickerViewController *)picker didFinishPickingImage:(UIImage *)image didFinishPickingAssets:(PHAsset *)photoAssets;
then after build it will raise two error for that photoAsset parameter missing in delete function
in YMSPhotoPickerViewController.m replace the error part line 256 with
[self.delegate photoPickerViewController:self
didFinishPickingImage:[self yms_orientationNormalizedImage:image] didFinishPickingAssets:asset];
and in other error also replace the code with
PHAsset *asset = self.currentCollectionItem[#"assets"];
[self.delegate photoPickerViewController:self
didFinishPickingImage:[self yms_orientationNormalizedImage:image] didFinishPickingAssets:asset];
and the singleImage delegate method now should be like this
func photoPickerViewController(_ picker: YMSPhotoPickerViewController!, didFinishPicking image: UIImage!, didFinishPickingAssets photoAssets: PHAsset!) {
let asset = photoAssets.originalFilename
let image = photoAssets.getImage()
print(asset)
picker.dismiss(animated: true)
}
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.
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 have an iOS app in which there are 2 ways the user can get a picture:
Select it from photos library (UIImagePickerController)
Click it from a custom made camera
Here is my code for clicking the image from a custom camera (this is within a custom class called Camera, which is a subclass of UIView)
func clickPicture(completion:#escaping (UIImage) -> Void) {
guard let videoConnection = stillImageOutput?.connection(withMediaType: AVMediaTypeVideo) else { return }
videoConnection.videoOrientation = .portrait
stillImageOutput?.captureStillImageAsynchronously(from: videoConnection, completionHandler: { (sampleBuffer, error) -> Void in
guard let buffer = sampleBuffer else { return }
let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer)
let dataProvider = CGDataProvider(data: imageData! as CFData)
let cgImageRef = CGImage(jpegDataProviderSource: dataProvider!, decode: nil, shouldInterpolate: true, intent: .defaultIntent)
let image = UIImage(cgImage: cgImageRef!, scale: 1, orientation: .right)
completion(image)
})
}
Here is how I click the image within the ViewController:
#IBAction func clickImage(_ sender: AnyObject) {
cameraView.clickPicture { (image) in
//use "image" variable
}
}
Later, I attempt to upload this picture to the user's iCloud account using CloudKit. However I receive an error saying the record is too large. I then came across this SO post, which says to use a CKAsset. However, the only constructor for a CKAsset requires a URL.
Is there a generic way I can get a URL from any UIImage? Otherwise, how can get a URL from the image I clicked using my custom camera (I have seen other posts about getting a url from a UIImagePickerController)? Thanks!
CKAsset represents some external file (image, video, binary data and etc). This is why it requires URL as init parameter.
In your case I would recommend to use following steps to upload large image to CloudKit:
Save UIImage to local storage (e.g. documents directory).
Initialize CKAsset with path to image in local storage.
Upload asset to Cloud.
Delete image from local storage when uploading completed.
Here is some code:
// Save image.
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let filePath = "\(path)/MyImageName.jpg"
UIImageJPEGRepresentation(image, 1)!.writeToFile(filePath, atomically: true)
let asset = CKAsset(fileURL: NSURL(fileURLWithPath: filePath)!)
// Upload asset here.
// Delete image.
do {
try FileManager.default.removeItem(atPath: filePath)
} catch {
print(error)
}
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.