1#import "RNViewShot.h" 2#import <AVFoundation/AVFoundation.h> 3#import <React/RCTLog.h> 4#import <React/UIView+React.h> 5#import <React/RCTUtils.h> 6#import <React/RCTConvert.h> 7#import <React/RCTScrollView.h> 8#import <React/RCTUIManager.h> 9#if __has_include(<React/RCTUIManagerUtils.h>) 10#import <React/RCTUIManagerUtils.h> 11#endif 12#import <React/RCTBridge.h> 13 14@implementation RNViewShot 15 16RCT_EXPORT_MODULE() 17 18@synthesize bridge = _bridge; 19 20- (dispatch_queue_t)methodQueue 21{ 22 return RCTGetUIManagerQueue(); 23} 24 25RCT_EXPORT_METHOD(captureScreen: (NSDictionary *)options 26 resolve:(RCTPromiseResolveBlock)resolve 27 reject:(RCTPromiseRejectBlock)reject) 28{ 29 [self captureRef: [NSNumber numberWithInt:-1] withOptions:options resolve:resolve reject:reject]; 30} 31 32RCT_EXPORT_METHOD(releaseCapture:(nonnull NSString *)uri) 33{ 34 NSString *directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ReactNative"]; 35 // Ensure it's a valid file in the tmp directory 36 if ([uri hasPrefix:directory] && ![uri isEqualToString:directory]) { 37 NSFileManager *fileManager = [NSFileManager new]; 38 if ([fileManager fileExistsAtPath:uri]) { 39 [fileManager removeItemAtPath:uri error:NULL]; 40 } 41 } 42} 43 44RCT_EXPORT_METHOD(captureRef:(nonnull NSNumber *)target 45 withOptions:(NSDictionary *)options 46 resolve:(RCTPromiseResolveBlock)resolve 47 reject:(RCTPromiseRejectBlock)reject) 48{ 49 [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 50 51 // Get view 52 UIView *view; 53 54 if ([target intValue] == -1) { 55 UIWindow *window = [[UIApplication sharedApplication] keyWindow]; 56 view = window; 57 } else { 58 view = viewRegistry[target]; 59 } 60 61 if (!view) { 62 reject(RCTErrorUnspecified, [NSString stringWithFormat:@"No view found with reactTag: %@", target], nil); 63 return; 64 } 65 66 // Get options 67 CGSize size = [RCTConvert CGSize:options]; 68 NSString *format = [RCTConvert NSString:options[@"format"]]; 69 NSString *result = [RCTConvert NSString:options[@"result"]]; 70 BOOL renderInContext = [RCTConvert BOOL:options[@"useRenderInContext"]]; 71 BOOL snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"]]; 72 73 // Capture image 74 BOOL success; 75 76 UIView* rendered; 77 UIScrollView* scrollView; 78 if (snapshotContentContainer) { 79 if (![view isKindOfClass:[RCTScrollView class]]) { 80 reject(RCTErrorUnspecified, [NSString stringWithFormat:@"snapshotContentContainer can only be used on a RCTScrollView. instead got: %@", view], nil); 81 return; 82 } 83 RCTScrollView* rctScrollView = (RCTScrollView *)view; 84 scrollView = rctScrollView.scrollView; 85 rendered = scrollView; 86 } 87 else { 88 rendered = view; 89 } 90 91 if (size.width < 0.1 || size.height < 0.1) { 92 size = snapshotContentContainer ? scrollView.contentSize : view.bounds.size; 93 } 94 if (size.width < 0.1 || size.height < 0.1) { 95 reject(RCTErrorUnspecified, [NSString stringWithFormat:@"The content size must not be zero or negative. Got: (%g, %g)", size.width, size.height], nil); 96 return; 97 } 98 99 CGPoint savedContentOffset; 100 CGRect savedFrame; 101 if (snapshotContentContainer) { 102 // Save scroll & frame and set it temporarily to the full content size 103 savedContentOffset = scrollView.contentOffset; 104 savedFrame = scrollView.frame; 105 scrollView.contentOffset = CGPointZero; 106 scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); 107 } 108 109 UIGraphicsBeginImageContextWithOptions(size, NO, 0); 110 111 if (renderInContext) { 112 // this comes with some trade-offs such as inability to capture gradients or scrollview's content in full but it works for large views 113 [rendered.layer renderInContext: UIGraphicsGetCurrentContext()]; 114 success = YES; 115 } 116 else { 117 // this doesn't work for large views and reports incorrect success even though the image is blank 118 success = [rendered drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES]; 119 } 120 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 121 UIGraphicsEndImageContext(); 122 123 if (snapshotContentContainer) { 124 // Restore scroll & frame 125 scrollView.contentOffset = savedContentOffset; 126 scrollView.frame = savedFrame; 127 } 128 129 if (!success) { 130 reject(RCTErrorUnspecified, @"The view cannot be captured. drawViewHierarchyInRect was not successful. This is a potential technical or security limitation.", nil); 131 return; 132 } 133 134 if (!image) { 135 reject(RCTErrorUnspecified, @"Failed to capture view snapshot. UIGraphicsGetImageFromCurrentImageContext() returned nil!", nil); 136 return; 137 } 138 139 // Convert image to data (on a background thread) 140 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 141 142 NSData *data; 143 if ([format isEqualToString:@"jpg"]) { 144 CGFloat quality = [RCTConvert CGFloat:options[@"quality"]]; 145 data = UIImageJPEGRepresentation(image, quality); 146 } 147 else { 148 data = UIImagePNGRepresentation(image); 149 } 150 151 NSError *error = nil; 152 NSString *res = nil; 153 if ([result isEqualToString:@"base64"]) { 154 // Return as a base64 raw string 155 res = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength]; 156 } 157 else if ([result isEqualToString:@"data-uri"]) { 158 // Return as a base64 data uri string 159 NSString *base64 = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength]; 160 NSString *imageFormat = ([format isEqualToString:@"jpg"]) ? @"jpeg" : format; 161 res = [NSString stringWithFormat:@"data:image/%@;base64,%@", imageFormat, base64]; 162 } 163 else { 164 // Save to a temp file 165 NSString *path = RCTTempFilePath(format, &error); 166 if (path && !error) { 167 if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) { 168 res = path; 169 } 170 } 171 } 172 173 if (res && !error) { 174 resolve(res); 175 return; 176 } 177 178 // If we reached here, something went wrong 179 if (error) reject(RCTErrorUnspecified, error.localizedDescription, error); 180 else reject(RCTErrorUnspecified, @"viewshot unknown error", nil); 181 }); 182 }]; 183} 184 185 186@end 187