--- ImagePickerManager.m 1985-10-26 04:15:00.000000000 -0400 +++ ImagePickerManager.m 2020-11-15 21:01:40.000000000 -0500 @@ -1,9 +1,11 @@ #import "ImagePickerManager.h" #import -#import #import #import #import +#if !TARGET_OS_MACCATALYST +#import +#endif @import MobileCoreServices; @@ -42,25 +44,25 @@ { self.callback = callback; // Save the callback so we can use it from the delegate methods self.options = options; - + dispatch_async(dispatch_get_main_queue(), ^{ - + NSString *title = [self.options valueForKey:@"title"]; if ([title isEqual:[NSNull null]] || title.length == 0) { title = nil; // A more visually appealing UIAlertControl is displayed with a nil title rather than title = @"" } NSString *cancelTitle = [self.options valueForKey:@"cancelButtonTitle"]; NSString *takePhotoButtonTitle = [self.options valueForKey:@"takePhotoButtonTitle"]; - NSString *chooseFromLibraryButtonTitle = [self.options valueForKey:@"chooseFromLibraryButtonTitle"]; - + NSString *chooseFromLibraryButtonTitle = [self.options valueForKey:@"chooseFromLibraryButtonTitle"]; + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleActionSheet]; alertController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]]; - + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { self.callback(@[@{@"didCancel": @YES}]); // Return callback for 'cancel' action (if is required) }]; [alertController addAction:cancelAction]; - + if (![takePhotoButtonTitle isEqual:[NSNull null]] && takePhotoButtonTitle.length > 0) { UIAlertAction *takePhotoAction = [UIAlertAction actionWithTitle:takePhotoButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { [self actionHandler:action]; @@ -73,7 +75,7 @@ }]; [alertController addAction:chooseFromLibraryAction]; } - + // Add custom buttons to action sheet if ([self.options objectForKey:@"customButtons"] && [[self.options objectForKey:@"customButtons"] isKindOfClass:[NSArray class]]) { self.customButtons = [self.options objectForKey:@"customButtons"]; @@ -85,16 +87,16 @@ [alertController addAction:customAction]; } } - + UIViewController *root = RCTPresentedViewController(); - + /* On iPad, UIAlertController presents a popover view rather than an action sheet like on iPhone. We must provide the location - of the location to show the popover in this case. For simplicity, we'll just display it on the bottom center of the screen - to mimic an action sheet */ + of the location to show the popover in this case. For simplicity, we'll just display it on the bottom center of the screen + to mimic an action sheet */ alertController.popoverPresentationController.sourceView = root.view; alertController.popoverPresentationController.sourceRect = CGRectMake(root.view.bounds.size.width / 2.0, root.view.bounds.size.height, 1.0, 1.0); - - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { alertController.popoverPresentationController.permittedArrowDirections = 0; for (id subview in alertController.view.subviews) { if ([subview isMemberOfClass:[UIView class]]) { @@ -102,7 +104,7 @@ } } } - + [root presentViewController:alertController animated:YES completion:nil]; }); } @@ -119,7 +121,7 @@ return; } } - + if ([action.title isEqualToString:[self.options valueForKey:@"takePhotoButtonTitle"]]) { // Take photo [self launchImagePicker:RNImagePickerTargetCamera]; } @@ -137,7 +139,7 @@ - (void)launchImagePicker:(RNImagePickerTarget)target { self.picker = [[UIImagePickerController alloc] init]; - + if (target == RNImagePickerTargetCamera) { #if TARGET_IPHONE_SIMULATOR self.callback(@[@{@"error": @"Camera not available on simulator"}]); @@ -155,10 +157,10 @@ else { // RNImagePickerTargetLibrarySingleImage self.picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; } - + if ([[self.options objectForKey:@"mediaType"] isEqualToString:@"video"] || [[self.options objectForKey:@"mediaType"] isEqualToString:@"mixed"]) { - + if ([[self.options objectForKey:@"videoQuality"] isEqualToString:@"high"]) { self.picker.videoQuality = UIImagePickerControllerQualityTypeHigh; } @@ -168,7 +170,7 @@ else { self.picker.videoQuality = UIImagePickerControllerQualityTypeMedium; } - + id durationLimit = [self.options objectForKey:@"durationLimit"]; if (durationLimit) { self.picker.videoMaximumDuration = [durationLimit doubleValue]; @@ -182,13 +184,13 @@ } else { self.picker.mediaTypes = @[(NSString *)kUTTypeImage]; } - + if ([[self.options objectForKey:@"allowsEditing"] boolValue]) { self.picker.allowsEditing = true; } self.picker.modalPresentationStyle = UIModalPresentationCurrentContext; self.picker.delegate = self; - + // Check permissions void (^showPickerViewController)() = ^void() { dispatch_async(dispatch_get_main_queue(), ^{ @@ -196,57 +198,57 @@ [root presentViewController:self.picker animated:YES completion:nil]; }); }; - + if (target == RNImagePickerTargetCamera) { [self checkCameraPermissions:^(BOOL granted) { if (!granted) { self.callback(@[@{@"error": @"Camera permissions not granted"}]); return; } - + showPickerViewController(); }]; } else { // RNImagePickerTargetLibrarySingleImage - if (@available(iOS 11.0, *)) { - showPickerViewController(); - } else { - [self checkPhotosPermissions:^(BOOL granted) { - if (!granted) { - self.callback(@[@{@"error": @"Photo library permissions not granted"}]); - return; - } - - showPickerViewController(); - }]; - } + if (@available(iOS 11.0, *)) { + showPickerViewController(); + } else { + [self checkPhotosPermissions:^(BOOL granted) { + if (!granted) { + self.callback(@[@{@"error": @"Photo library permissions not granted"}]); + return; + } + + showPickerViewController(); + }]; + } } } - (NSString * _Nullable)originalFilenameForAsset:(PHAsset * _Nullable)asset assetType:(PHAssetResourceType)type { if (!asset) { return nil; } - + PHAssetResource *originalResource; // Get the underlying resources for the PHAsset (PhotoKit) NSArray *pickedAssetResources = [PHAssetResource assetResourcesForAsset:asset]; - + // Find the original resource (underlying image) for the asset, which has the desired filename for (PHAssetResource *resource in pickedAssetResources) { if (resource.type == type) { originalResource = resource; } } - + return originalResource.originalFilename; } - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { dispatch_block_t dismissCompletionBlock = ^{ - - NSURL *imageURL = [info valueForKey:UIImagePickerControllerReferenceURL]; + + NSURL *imageURL = [info valueForKey:UIImagePickerControllerPHAsset]; NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType]; - + NSString *fileName; if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { NSString *tempFileName = [[NSUUID UUID] UUIDString]; @@ -264,18 +266,18 @@ NSURL *videoURL = info[UIImagePickerControllerMediaURL]; fileName = videoURL.lastPathComponent; } - + // We default to path to the temporary directory NSString *path = [[NSTemporaryDirectory()stringByStandardizingPath] stringByAppendingPathComponent:fileName]; - + // If storage options are provided, we use the documents directory which is persisted if ([self.options objectForKey:@"storageOptions"] && [[self.options objectForKey:@"storageOptions"] isKindOfClass:[NSDictionary class]]) { NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"]; - + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; path = [documentsDirectory stringByAppendingPathComponent:fileName]; - + // Creates documents subdirectory, if provided if ([storageOptions objectForKey:@"path"]) { NSString *newPath = [documentsDirectory stringByAppendingPathComponent:[storageOptions objectForKey:@"path"]]; @@ -291,10 +293,10 @@ } } } - + // Create the response object self.response = [[NSMutableDictionary alloc] init]; - + if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) { // PHOTOS UIImage *originalImage; if ([[self.options objectForKey:@"allowsEditing"] boolValue]) { @@ -303,13 +305,15 @@ else { originalImage = [info objectForKey:UIImagePickerControllerOriginalImage]; } - + if (imageURL) { PHAsset *pickedAsset; if (@available(iOS 11.0, *)) { - pickedAsset = [info objectForKey: UIImagePickerControllerPHAsset]; + pickedAsset = [info objectForKey: UIImagePickerControllerPHAsset]; } else { - pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil].lastObject; +#if !TARGET_OS_MACCATALYST + pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil].lastObject; +#endif } NSString *originalFilename = [self originalFilenameForAsset:pickedAsset assetType:PHAssetResourceTypePhoto]; @@ -322,9 +326,10 @@ self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:pickedAsset.creationDate]; } } - + // GIFs break when resized, so we handle them differently if (imageURL && [[imageURL absoluteString] rangeOfString:@"ext=GIF"].location != NSNotFound) { +#if !TARGET_OS_MACCATALYST ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; [assetsLibrary assetForURL:imageURL resultBlock:^(ALAsset *asset) { ALAssetRepresentation *rep = [asset defaultRepresentation]; @@ -333,38 +338,39 @@ NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:repSize error:nil]; NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES]; [data writeToFile:path atomically:YES]; - + NSMutableDictionary *gifResponse = [[NSMutableDictionary alloc] init]; [gifResponse setObject:@(originalImage.size.width) forKey:@"width"]; [gifResponse setObject:@(originalImage.size.height) forKey:@"height"]; - + BOOL vertical = (originalImage.size.width < originalImage.size.height) ? YES : NO; [gifResponse setObject:@(vertical) forKey:@"isVertical"]; - + if (![[self.options objectForKey:@"noData"] boolValue]) { NSString *dataString = [data base64EncodedStringWithOptions:0]; [gifResponse setObject:dataString forKey:@"data"]; } - + NSURL *fileURL = [NSURL fileURLWithPath:path]; [gifResponse setObject:[fileURL absoluteString] forKey:@"uri"]; - + NSNumber *fileSizeValue = nil; NSError *fileSizeError = nil; [fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError]; if (fileSizeValue){ [gifResponse setObject:fileSizeValue forKey:@"fileSize"]; } - + self.callback(@[gifResponse]); } failureBlock:^(NSError *error) { self.callback(@[@{@"error": error.localizedFailureReason}]); }]; +#endif return; } - + UIImage *editedImage = [self fixOrientation:originalImage]; // Rotate the image for upload to web - + // If needed, downscale image float maxWidth = editedImage.size.width; float maxHeight = editedImage.size.height; @@ -375,7 +381,7 @@ maxHeight = [[self.options valueForKey:@"maxHeight"] floatValue]; } editedImage = [self downscaleImageIfNecessary:editedImage maxWidth:maxWidth maxHeight:maxHeight]; - + NSData *data; NSString *mimeType; if ([[self.options objectForKey:@"imageFileType"] isEqualToString:@"png"]) { @@ -388,36 +394,37 @@ } [self.response setObject:mimeType forKey:@"type"]; [data writeToFile:path atomically:YES]; - + if (![[self.options objectForKey:@"noData"] boolValue]) { NSString *dataString = [data base64EncodedStringWithOptions:0]; // base64 encoded image string [self.response setObject:dataString forKey:@"data"]; } - + BOOL vertical = (editedImage.size.width < editedImage.size.height) ? YES : NO; [self.response setObject:@(vertical) forKey:@"isVertical"]; NSURL *fileURL = [NSURL fileURLWithPath:path]; NSString *filePath = [fileURL absoluteString]; [self.response setObject:filePath forKey:@"uri"]; - + // add ref to the original image NSString *origURL = [imageURL absoluteString]; if (origURL) { - [self.response setObject:origURL forKey:@"origURL"]; + [self.response setObject:origURL forKey:@"origURL"]; } - + NSNumber *fileSizeValue = nil; NSError *fileSizeError = nil; [fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError]; if (fileSizeValue){ [self.response setObject:fileSizeValue forKey:@"fileSize"]; } - + [self.response setObject:@(editedImage.size.width) forKey:@"width"]; [self.response setObject:@(editedImage.size.height) forKey:@"height"]; - + NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"]; if (storageOptions && [[storageOptions objectForKey:@"cameraRoll"] boolValue] == YES && self.picker.sourceType == UIImagePickerControllerSourceTypeCamera) { +#if !TARGET_OS_MACCATALYST ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; if ([[storageOptions objectForKey:@"waitUntilSaved"] boolValue]) { [library writeImageToSavedPhotosAlbum:originalImage.CGImage metadata:[info valueForKey:UIImagePickerControllerMediaMetadata] completionBlock:^(NSURL *assetURL, NSError *error) { @@ -440,13 +447,15 @@ } else { [library writeImageToSavedPhotosAlbum:originalImage.CGImage metadata:[info valueForKey:UIImagePickerControllerMediaMetadata] completionBlock:nil]; } +#endif } } else { // VIDEO - NSURL *videoRefURL = info[UIImagePickerControllerReferenceURL]; + NSURL *videoRefURL = info[UIImagePickerControllerPHAsset]; NSURL *videoURL = info[UIImagePickerControllerMediaURL]; NSURL *videoDestinationURL = [NSURL fileURLWithPath:path]; - + +#if !TARGET_OS_MACCATALYST if (videoRefURL) { PHAsset *pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[videoRefURL] options:nil].lastObject; NSString *originalFilename = [self originalFilenameForAsset:pickedAsset assetType:PHAssetResourceTypeVideo]; @@ -459,39 +468,41 @@ self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:pickedAsset.creationDate]; } } - +#endif + if ([videoURL.URLByResolvingSymlinksInPath.path isEqualToString:videoDestinationURL.URLByResolvingSymlinksInPath.path] == NO) { NSFileManager *fileManager = [NSFileManager defaultManager]; - + // Delete file if it already exists if ([fileManager fileExistsAtPath:videoDestinationURL.path]) { [fileManager removeItemAtURL:videoDestinationURL error:nil]; } - + if (videoURL) { // Protect against reported crash - NSError *error = nil; - - // If we have write access to the source file, move it. Otherwise use copy. - if ([fileManager isWritableFileAtPath:[videoURL path]]) { - [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error]; - } else { - [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error]; - } - - if (error) { - self.callback(@[@{@"error": error.localizedFailureReason}]); - return; - } + NSError *error = nil; + + // If we have write access to the source file, move it. Otherwise use copy. + if ([fileManager isWritableFileAtPath:[videoURL path]]) { + [fileManager moveItemAtURL:videoURL toURL:videoDestinationURL error:&error]; + } else { + [fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error]; + } + + if (error) { + self.callback(@[@{@"error": error.localizedFailureReason}]); + return; + } } } - + [self.response setObject:videoDestinationURL.absoluteString forKey:@"uri"]; if (videoRefURL.absoluteString) { [self.response setObject:videoRefURL.absoluteString forKey:@"origURL"]; } - + NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"]; if (storageOptions && [[storageOptions objectForKey:@"cameraRoll"] boolValue] == YES && self.picker.sourceType == UIImagePickerControllerSourceTypeCamera) { +#if !TARGET_OS_MACCATALYST ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; [library writeVideoAtPathToSavedPhotosAlbum:videoDestinationURL completionBlock:^(NSURL *assetURL, NSError *error) { if (error) { @@ -509,22 +520,23 @@ self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:capturedAsset.creationDate]; } } - + self.callback(@[self.response]); } } }]; +#endif } } - + // If storage options are provided, check the skipBackup flag if ([self.options objectForKey:@"storageOptions"] && [[self.options objectForKey:@"storageOptions"] isKindOfClass:[NSDictionary class]]) { NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"]; - + if ([[storageOptions objectForKey:@"skipBackup"] boolValue]) { [self addSkipBackupAttributeToItemAtPath:path]; // Don't back up the file to iCloud } - + if ([[storageOptions objectForKey:@"waitUntilSaved"] boolValue] == NO || [[storageOptions objectForKey:@"cameraRoll"] boolValue] == NO || self.picker.sourceType != UIImagePickerControllerSourceTypeCamera) @@ -536,7 +548,7 @@ self.callback(@[self.response]); } }; - + dispatch_async(dispatch_get_main_queue(), ^{ [picker dismissViewControllerAnimated:YES completion:dismissCompletionBlock]; }); @@ -595,12 +607,12 @@ - (UIImage*)downscaleImageIfNecessary:(UIImage*)image maxWidth:(float)maxWidth maxHeight:(float)maxHeight { UIImage* newImage = image; - + // Nothing to do here if (image.size.width <= maxWidth && image.size.height <= maxHeight) { return newImage; } - + CGSize scaledSize = CGSizeMake(image.size.width, image.size.height); if (maxWidth < scaledSize.width) { scaledSize = CGSizeMake(maxWidth, (maxWidth / scaledSize.width) * scaledSize.height); @@ -608,11 +620,11 @@ if (maxHeight < scaledSize.height) { scaledSize = CGSizeMake((maxHeight / scaledSize.height) * scaledSize.width, maxHeight); } - + // If the pixels are floats, it causes a white line in iOS8 and probably other versions too scaledSize.width = (int)scaledSize.width; scaledSize.height = (int)scaledSize.height; - + UIGraphicsBeginImageContext(scaledSize); // this will resize [image drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)]; newImage = UIGraphicsGetImageFromCurrentImageContext(); @@ -620,7 +632,7 @@ NSLog(@"could not scale image"); } UIGraphicsEndImageContext(); - + return newImage; } @@ -628,7 +640,7 @@ if (srcImg.imageOrientation == UIImageOrientationUp) { return srcImg; } - + CGAffineTransform transform = CGAffineTransformIdentity; switch (srcImg.imageOrientation) { case UIImageOrientationDown: @@ -636,13 +648,13 @@ transform = CGAffineTransformTranslate(transform, srcImg.size.width, srcImg.size.height); transform = CGAffineTransformRotate(transform, M_PI); break; - + case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: transform = CGAffineTransformTranslate(transform, srcImg.size.width, 0); transform = CGAffineTransformRotate(transform, M_PI_2); break; - + case UIImageOrientationRight: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, 0, srcImg.size.height); @@ -652,14 +664,14 @@ case UIImageOrientationUpMirrored: break; } - + switch (srcImg.imageOrientation) { case UIImageOrientationUpMirrored: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, srcImg.size.width, 0); transform = CGAffineTransformScale(transform, -1, 1); break; - + case UIImageOrientationLeftMirrored: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, srcImg.size.height, 0); @@ -671,7 +683,7 @@ case UIImageOrientationRight: break; } - + CGContextRef ctx = CGBitmapContextCreate(NULL, srcImg.size.width, srcImg.size.height, CGImageGetBitsPerComponent(srcImg.CGImage), 0, CGImageGetColorSpace(srcImg.CGImage), CGImageGetBitmapInfo(srcImg.CGImage)); CGContextConcatCTM(ctx, transform); switch (srcImg.imageOrientation) { @@ -681,12 +693,12 @@ case UIImageOrientationRightMirrored: CGContextDrawImage(ctx, CGRectMake(0,0,srcImg.size.height,srcImg.size.width), srcImg.CGImage); break; - + default: CGContextDrawImage(ctx, CGRectMake(0,0,srcImg.size.width,srcImg.size.height), srcImg.CGImage); break; } - + CGImageRef cgimg = CGBitmapContextCreateImage(ctx); UIImage *img = [UIImage imageWithCGImage:cgimg]; CGContextRelease(ctx); @@ -701,7 +713,7 @@ NSError *error = nil; BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES] forKey: NSURLIsExcludedFromBackupKey error: &error]; - + if(!success){ NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error); }