Cannot convert UIImage to NSData when fetched with PhotoKit - ios

My scenario is that I select one of the photos fetched from system album with PhotoKit and presented in my UICollectionView and pass the selected photo (UIImage) to my next UIView and send it to remote server in form of NSData.
But when I put breakpoint before sending to track the NSData I found that the UIImage had data with allocated memory while NSData did not.
Here is the code to fetch the UIImage (Please notice that I didn't specify any PHImageRequestOptions object):
NSMutableArray *images = #[].mutableCopy;
PHImageManager *manager = [PHImageManager defaultManager];
PHAsset *asset = _photoPickerCollectionView.pickedAsset;
CGSize targetSize = CGSizeMake(asset.pixelWidth*0.5, asset.pixelHeight*0.5);
[manager requestImageForAsset:asset targetSize:targetSize contentMode:PHImageContentModeAspectFill options:nil resultHandler:^(UIImage *result, NSDictionary *info) {
_nextView.pickedOne = result;
}];
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
I convert the UIImage to NSData like this:
UIImage *image = _pickedOne;
NSData *imgData = UIImageJPEGRepresentation(image, imageQuality);
When I tracked the variables, the UIImage contains data as below:
But the NSData is nil as below:
And what was ODD is that if I specified an PHImageRequestOptions object for the PHImageManager to request images, the NSData wouldn't be nil. I'm not sure what was changed with or without the PHImageRequestOptions and why it would make such difference.
UPDATE:
What I found is that if you specify PHImageRequestOptions to nil then the default options will force it to fetch photos asynchronously which, in my opinion, can be unstable for NSData, so when I specify options.synchronous = YES; it would work.
But in this case, would it cause any retain cycle or some PhotoKit objects won't get released?

The method by default is asynchronous. You need to set options to explicitly make the image fetching synchronous. My strong suggestion for such kind of conversion is to use this API:
- (PHImageRequestID)requestImageDataForAsset:(PHAsset *)asset options:(nullable PHImageRequestOptions *)options resultHandler:(void(^)(NSData *__nullable imageData, NSString *__nullable dataUTI, UIImageOrientation orientation, NSDictionary *__nullable info))resultHandler;
This returns you the data directly but you will have to make this synchronous as well. This WON'T cause any retain cycles for any objects.
Hope this helps. :)

Related

PhotoKit - didFinishPickingMediaWithInfo vs creationRequestForAssetFromImage (data not equal??)

Wanted to get some feedback to see what I might be missing here. Basically I use a UIImagePickerViewController to take a photo. When I am done i retrieve this image like this:
UIImage *photoTaken = [info objectForKey:#"UIImagePickerControllerOriginalImage"];
After I have taken the photo, at a later time, I need to be able load all the images in my camera roll and highlight the photo(I display all the images from the camera roll) that I just took. Because these photos are different objects in memory (different view controllers), the only way to compare them is by comparing the actual data that represents the images. i..e..
NSData *alreadySelectedPhotoData = UIImageJPEGRepresentation(alreadySelectedPhoto.photoImage, 0.0);
NSData *cameralRollPhotoData = UIImageJPEGRepresentation(cameraRollPhoto.photoImage, 0.0);
if([cameralRollPhotoData isEqualToData:alreadySelectedPhotoData]){
//do something here if they are equal(draw a border, etc)
}
However, the photos never actually were equal based on this comparison, despite the fact that the images displayed are identical.
So I went back to the original code, did some digging and did a data and visual test:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
__block UIImage *photoTaken = [info objectForKey:#"UIImagePickerControllerOriginalImage"];
__block PHObjectPlaceholder *placeholderAsset = nil;
//save our new photo to the camera roll album(successfully)
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:photoTaken];
changeRequest.creationDate = creationTimeStamp = [NSDate date];
placeholderAsset = changeRequest.placeholderForCreatedAsset;
}
completionHandler:^(BOOL success, NSError *error){
PHImageManager *manager = [PHImageManager defaultManager];
PHImageRequestOptions *requestOptions = [PHImageRequestOptions new];
requestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
CGFloat dimension = [UIScreen mainScreen].bounds.size.width / 3 * [UIScreen mainScreen].scale;
CGSize targetSize = CGSizeMake(dimension, dimension);
PHFetchResult *savedAssets = [PHAsset fetchAssetsWithLocalIdentifiers:#[placeholderAsset.localIdentifier] options:nil];
[manager requestImageForAsset:savedAssets.firstObject targetSize:targetSize contentMode:PHImageContentModeAspectFill options:requestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
//images are the 'same' but their NSData representations appear to not be. NSLog statement never executes.
NSData *alreadySelectedPhotoData = UIImageJPEGRepresentation(photoTaken, 0.0);
NSData *cameralRollPhotoData = UIImageJPEGRepresentation(result, 0.0);
if([cameralRollPhotoData isEqualToData:alreadySelectedPhotoData]){
NSLog(#"images are equal");
}
}];
}];
So to summarize:
store the image that comes back from the info object in the picker delegate method
use this image and store it in the camera roll by using 'creationRequestForAssetFromImage'
retrieve back the image that we just stored by getting Asset ('fetchAssetsWithLocalIdentifiers')
convert that asset back into an image (PHManager - requestImageForAsset)
convert both the original UIImage that was returned via the picker delegate and the image back from the camera roll that was created to NSData objects.
Result: they do not equal, even though the images on the screen are exactly the same.
Conclusion: It seems to me that this below:
PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:photoTaken];
saves to the camera roll successfully, can verify that the image it displays is exactly the image that I got from the UIPickerImage delegate method(visually looks the same), yet when converting both images to NSData objects the comparison fails.
Does anyone have any idea whats going on here? did I miss something or is this a bug?

Memory issue when requesting for Gallery image DATA using Photos Framework in iOS

I am working on an iOS app in which I have to fetch all the images from iPhone gallery and then fetch its exif Value.To get exif value I need to get image using image data.
I am using following code for this:
-(void)getExifDataFromImage:(NSInteger)index{
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous=YES;
options.resizeMode=PHImageRequestOptionsResizeModeFast;
[[PHImageManager defaultManager] requestImageDataForAsset:asset
options:options
resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info)
{
CIImage* ciImage = [CIImage imageWithData:imageData];
NSLog(#"Metadata : %#", ciImage.properties);
exifCount++;
NSDictionary *pro=ciImage.properties;
NSDictionary *exifDic=[pro objectForKey:#"{Exif}"];
NSDictionary *tiffDic=[pro objectForKey:#"{TIFF}"];
}];}
All works fine for the images around 1200 but if we go beyond that we get memory issue and the application crashes.
I also tried getting image using the below code to get the image and then tried to find the exif value of the resized image.
[self.imageManager requestImageForAsset:asset targetSize:CGSizeMake(360, 360) contentMode:PHImageContentModeAspectFill options:nil resultHandler:^(UIImage *result, NSDictionary *info)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self doSomething]
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image=result;
});//
});
}];
But I am unable to get the exif value for the resized images.
Error : libBacktraceRecording.dylib: allocate_free_list_pages() -- virtual memory exhausted!
Please suggest me how to handle this Memory crash issue. Also please suggest if there is another way to get the exif value.
You're loading autoreleased images with CIImage* ciImage = [CIImage imageWithData:imageData];. That will use up a lot of data really quickly because the Autorealeasepool is not drained.
You can wrap the image creation and dictionary access in #autoreleasepool{/* your code*/}. Or just use CIImage* ciImage = [[CIImage alloc] initWithData:imageData]; to let ARC take care of of freeing the memory.

How to convert PHAsset to PHLivePhoto

I have PHAsset and I want to get PHLivePhoto.
PHAsset is PHLivePhoto's asset.
I know this Function.
/// Requests a Live Photo from the given resource URLs. The result handler will be called multiple times to deliver new PHLivePhoto instances with increasingly more content. If a placeholder image is provided, the result handler will first be invoked synchronously to deliver a live photo containing only the placeholder image. Subsequent invocations of the result handler will occur on the main queue.
// The targetSize and contentMode parameters are used to resize the live photo content if needed. If targetSize is equal to CGRectZero, content will not be resized.
// When using this method to provide content for a PHLivePhotoView, each live photo instance delivered via the result handler should be passed to - [PHLivePhotoView setLivePhoto:].
+ (PHLivePhotoRequestID)requestLivePhotoWithResourceFileURLs:(NSArray<NSURL *> *)fileURLs placeholderImage:(UIImage *__nullable)image targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode resultHandler:(void(^)(PHLivePhoto *__nullable livePhoto, NSDictionary *info))resultHandler;
But I don't know use.
How to convert?
You can use this method from PHImageManager
- (PHImageRequestID)requestLivePhotoForAsset:(PHAsset *)asset targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(PHLivePhotoRequestOptions *)options resultHandler:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))resultHandler
Inside the resultHandler you obtain the PHLivePhoto ready to show in a PHLivePhotoView.
EXAMPLE:
[[PHImageManager defaultManager] requestLivePhotoForAsset:asset targetSize:self.contentView.frame.size contentMode:PHImageContentModeDefault options:nil resultHandler:^(PHLivePhoto * _Nullable livePhoto, NSDictionary * _Nullable info) { self.contentView.livePhoto = livePhoto }];
[asset requestContentEditingInputWithOptions:nil completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
PHLivePhoto *livePhoto = [contentEditingInput.livePhoto];
}];

Photos Framework requestImageDataForAsset occasionally fails

I'm using the photos framework on iOS8.1 and requesting the image data for the asset using requestImageDataForAsset... Most of the time it works and I get the image data and a dictionary containing what you see below. But sometimes the call completes, but the data is nil and the dictionary contains three generic looking entries.
The calls are performed sequentially and on the same thread. It is not specific to any particular image. The error will happen on images I've successfully opened in the past. Has anyone encountered this?
+ (NSData *)retrieveAssetDataPhotosFramework:(NSURL *)urlMedia resolution:(CGFloat)resolution imageOrientation:(ALAssetOrientation*)imageOrientation {
__block NSData *iData = nil;
PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:#[urlMedia] options:nil];
PHAsset *asset = [result firstObject];
PHImageManager *imageManager = [PHImageManager defaultManager];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
options.synchronous = YES;
options.version = PHImageRequestOptionsVersionCurrent;
#autoreleasepool {
[imageManager requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
iData = [imageData copy];
NSLog(#"requestImageDataForAsset returned info(%#)", info);
*imageOrientation = (ALAssetOrientation)orientation;
}];
}
assert(iData.length != 0);
return iData;
}
This is the desired result where I get image data and the dictionary of meta data:
requestImageDataForAsset returned info({
PHImageFileDataKey = <PLXPCShMemData: 0x1702214a0> bufferLength=1753088 dataLength=1749524;
PHImageFileOrientationKey = 1;
PHImageFileSandboxExtensionTokenKey = "6e14948c4d0019fbb4d14cc5e021199f724f0323;00000000;00000000;000000000000001a;com.apple.app-sandbox.read;00000001;01000003;000000000009da80;/private/var/mobile/Media/DCIM/107APPLE/IMG_7258.JPG";
PHImageFileURLKey = "file:///var/mobile/Media/DCIM/107APPLE/IMG_7258.JPG";
PHImageFileUTIKey = "public.jpeg";
PHImageResultDeliveredImageFormatKey = 9999;
PHImageResultIsDegradedKey = 0;
PHImageResultIsInCloudKey = 0;
PHImageResultIsPlaceholderKey = 0;
PHImageResultWantedImageFormatKey = 9999;
})
Here's what I get occasionally. image data is nil. Dictionary contains not so much.
requestImageDataForAsset returned info({
PHImageResultDeliveredImageFormatKey = 9999;
PHImageResultIsDegradedKey = 0;
PHImageResultWantedImageFormatKey = 9999;
})
I had a problem with similar symptoms where requestImageDataForAsset returned nil image data but was also accompanied by a console error message like this:
[Generic] Failed to load image data for asset <PHAsset: 0x13d041940> 87CCAFDC-A0E3-4AC9-AD1C-3F57B897A52E/L0/001 mediaType=1/0, sourceType=2, (113x124), creationDate=2015-06-29 04:56:34 +0000, location=0, hidden=0, favorite=0 with format 9999
In my case, the problem suddenly started happening on a specific device only with assets in iCloud shared albums after upgrading from iOS 10.x to 11.0.3, and since then through to 11.2.5. Thinking that maybe requestImageDataForAsset was trying to use files locally cached in /var/mobile/Media/PhotoData/PhotoCloudSharingData/ (from the info dictionary's PHImageFileURLKey key) and that the cache may be corrupt I thought about how to clear that cache.
Toggling the 'iCloud Photo Sharing' switch in iOS' Settings -> Accounts & Passwords -> iCloud -> Photos seems to have done the trick. requestImageDataForAsset is now working for those previously failing assets.
Update 9th March 2018
I can reproduce this problem now. It seems to occur after restoring a backup from iTunes:
Use the iOS app and retrieve photos from an iCloud shared album.
Backup the iOS device using iTunes.
Restore the backup using iTunes.
Using the app again to retrieve the same photos from the iCloud shared album now fails with the above console message.
Toggling the 'iCloud Photo Sharing' switch fixes it still. Presumably the restore process somehow corrupts some cache. I've reported it as Bug 38290463 to Apple.
You are likely iterating through an array, and memory is not freed timely, you can try the below code. Make sure theData is marked by __block.
#autoreleasepool {
[imageManager requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
NSLog(#"requestImageDataForAsset returned info(%#)", info);
theData = [imageData copy];
}];
}
Getting back to this after a long while, I have solved a big part of my problem. No mystery, just bad code:
PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:#[urlMedia] options:nil];
PHAsset *asset = [result firstObject];
if (asset != nil) { // the fix
PHImageManager *imageManager = [PHImageManager defaultManager];
PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
...
}
The most common cause for me was a problem with the media URL passed to fetchAssetsWithALAssetURLs causing asset to be nil and requestImageDataForAsset return a default info object.
The following code maybe help. I think the class PHImageRequestOptions has a bug, so I pass nil , and then fix the bug.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
assetModel.size = imageData.length;
NSString *filename = [asset valueForKey:#"filename"];
assetModel.fileName = filename;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

How to know if a PHAsset has been modified?

More specifically, how can you know whether a PHAsset has current version of the underlying asset different than the original?
My user should only need to choose between the current or original asset when necessary. And then I need their answer for PHImageRequestOptions.version.
As of iOS 16, PHAsset has a hasAdjustments property which indicates if the asset has been edited.
For previous releases, you can get an array of data resources for a given asset via PHAssetResource API - it will have an adjustment data resource if that asset has been edited.
let isEdited = PHAssetResource.assetResources(for: asset).contains(where: { $0.type == .adjustmentData })
Note that if you want to actually work with a resource file, you have to fetch its data using a PHAssetResourceManager API. Also note that this method returns right away - there's no waiting for an async network request, unlike other answers here.
I have found two ways of checking PHAsset for modifications.
- (void)tb_checkForModificationsWithEditingInputMethodCompletion:(void (^)(BOOL))completion {
PHContentEditingInputRequestOptions *options = [PHContentEditingInputRequestOptions new];
options.canHandleAdjustmentData = ^BOOL(PHAdjustmentData *adjustmentData) { return YES; };
[self requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
if (completion) completion(contentEditingInput.adjustmentData != nil);
}];
}
- (void)tb_checkForModificationsWithAssetPathMethodCompletion:(void (^)(BOOL))completion {
PHVideoRequestOptions *options = [PHVideoRequestOptions new];
options.deliveryMode = PHVideoRequestOptionsDeliveryModeFastFormat;
[[PHImageManager defaultManager] requestAVAssetForVideo:self options:options resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if (completion) completion([[asset description] containsString:#"/Mutations/"]);
}];
}
EDIT: I was at the point where I needed the same functionality for PHAsset with an image. I used this:
- (void)tb_checkForModificationsWithAssetPathMethodCompletion:(void (^)(BOOL))completion {
[self requestContentEditingInputWithOptions:nil completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
NSString *path = (contentEditingInput.avAsset) ? [contentEditingInput.avAsset description] : contentEditingInput.fullSizeImageURL.path;
completion([path containsString:#"/Mutations/"]);
}];
}
Take a look at PHImageRequestOptionsVersion
PHImageRequestOptionsVersionCurrent
Request the most recent version of the image asset (the one that reflects all edits).
The resulting image is the rendered output from all previously made adjustments.
PHImageRequestOptionsVersionUnadjusted
Request a version of the image asset without adjustments.
If the asset has been edited, the resulting image reflects the state of the asset before any edits were performed.
PHImageRequestOptionsVersionOriginal
Request the original, highest-fidelity version of the image asset. The
resulting image is originally captured or imported version of the
asset, regardless of any edits made.
If you ask user before retrieving assets, you know which version user specified. If you get a phasset from elsewhere, you can do a revertAssetContentToOriginal to get the original asset. And PHAsset has modificationDate and creationDate properties, you can use this to tell if a PHAsset is modified.
I found this code the only one working for now, and it handles most of the edge cases. It may not be the fastest one but works well for most images types. It takes the smallest possible original and modified image and compare their data content.
#implementation PHAsset (Utilities)
- (void)checkEditingHistoryCompletion:(void (^)(BOOL edited))completion
{
PHImageManager *manager = [PHImageManager defaultManager];
CGSize compareSize = CGSizeMake(64, 48);
PHImageRequestOptions *requestOptions = [PHImageRequestOptions new];
requestOptions.synchronous = YES;
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat;
requestOptions.version = PHImageRequestOptionsVersionOriginal;
[manager requestImageForAsset:self
targetSize:compareSize
contentMode:PHImageContentModeAspectFit
options:requestOptions
resultHandler:^(UIImage *originalResult, NSDictionary *info) {
UIImage *currentImage = originalResult;
requestOptions.version = PHImageRequestOptionsVersionCurrent;
[manager requestImageForAsset:self
targetSize:currentImage.size
contentMode:PHImageContentModeAspectFit
options:requestOptions
resultHandler:^(UIImage *currentResult, NSDictionary *info) {
NSData *currData = UIImageJPEGRepresentation(currentResult, 0.1);
NSData *orgData = UIImageJPEGRepresentation(currentImage, 0.1);
if (completion) {
//handle case when both images cannot be retrived it also mean no edition
if ((currData == nil) && (orgData == nil)) {
completion(NO);
return;
}
completion(([currData isEqualToData:orgData] == NO));
}
}];
}];
}
#end

Resources