1// Copyright 2015-present 650 Industries. All rights reserved.
2
3// Prior to React Native 0.61, it contained `RCTAssetsLibraryRequestHandler` that handles loading data from URLs
4// with `assets-library://` or `ph://` schemes. Due to lean core project, it's been moved to `@react-native-community/cameraroll`
5// and that made it impossible to render assets using URLs returned by MediaLibrary without installing CameraRoll.
6// Because it's still a unimodule and we need to export bare React Native module, we should make sure React Native is installed.
7#if __has_include(<React/RCTImageURLLoader.h>)
8
9#import <Photos/Photos.h>
10#import <React/RCTDefines.h>
11#import <React/RCTUtils.h>
12#import <React/RCTBridgeModule.h>
13#import <EXMediaLibrary/EXMediaLibraryImageLoader.h>
14
15@implementation EXMediaLibraryImageLoader
16
17RCT_EXPORT_MODULE()
18
19#pragma mark - RCTImageURLLoader
20
21- (BOOL)canLoadImageURL:(NSURL *)requestURL
22{
23  if (![PHAsset class]) {
24    return NO;
25  }
26  return [requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame ||
27    [requestURL.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame;
28}
29
30- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
31                                              size:(CGSize)size
32                                             scale:(CGFloat)scale
33                                        resizeMode:(RCTResizeMode)resizeMode
34                                   progressHandler:(RCTImageLoaderProgressBlock)progressHandler
35                                partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
36                                 completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
37{
38  // Using PhotoKit for iOS 8+
39  // The 'ph://' prefix is used by FBMediaKit to differentiate between
40  // assets-library. It is prepended to the local ID so that it is in the
41  // form of an NSURL which is what assets-library uses.
42  NSString *assetID = @"";
43  PHFetchResult *results;
44  if (!imageURL) {
45    completionHandler(RCTErrorWithMessage(@"Cannot load a photo library asset with no URL"), nil);
46    return ^{};
47  } else if ([imageURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
48    assetID = [imageURL absoluteString];
49    results = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil];
50  } else {
51    assetID = [imageURL.absoluteString substringFromIndex:@"ph://".length];
52    results = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetID] options:nil];
53  }
54  if (results.count == 0) {
55    NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", assetID];
56    completionHandler(RCTErrorWithMessage(errorText), nil);
57    return ^{};
58  }
59
60  PHAsset *asset = [results firstObject];
61  PHImageRequestOptions *imageOptions = [PHImageRequestOptions new];
62
63  // Allow PhotoKit to fetch images from iCloud
64  imageOptions.networkAccessAllowed = YES;
65
66  if (progressHandler) {
67    imageOptions.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary<NSString *, id> *info) {
68      static const double multiplier = 1e6;
69      progressHandler(progress * multiplier, multiplier);
70    };
71  }
72
73  // Note: PhotoKit defaults to a deliveryMode of PHImageRequestOptionsDeliveryModeOpportunistic
74  // which means it may call back multiple times - we probably don't want that
75  imageOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
76
77  BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero);
78  CGSize targetSize;
79  if (useMaximumSize) {
80    targetSize = PHImageManagerMaximumSize;
81    imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone;
82  } else {
83    targetSize = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(scale, scale));
84    imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast;
85  }
86
87  PHImageContentMode contentMode = PHImageContentModeAspectFill;
88  if (resizeMode == RCTResizeModeContain) {
89    contentMode = PHImageContentModeAspectFit;
90  }
91
92  PHImageRequestID requestID =
93  [[PHImageManager defaultManager] requestImageForAsset:asset
94                                             targetSize:targetSize
95                                            contentMode:contentMode
96                                                options:imageOptions
97                                          resultHandler:^(UIImage *result, NSDictionary<NSString *, id> *info) {
98    if (result) {
99      completionHandler(nil, result);
100    } else {
101      completionHandler(info[PHImageErrorKey], nil);
102    }
103  }];
104
105  return ^{
106    [[PHImageManager defaultManager] cancelImageRequest:requestID];
107  };
108}
109
110@end
111
112#endif
113