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.rootViewController.view; 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 snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"]]; 71 72 // Capture image 73 BOOL success; 74 75 UIView* rendered; 76 UIScrollView* scrollView; 77 if (snapshotContentContainer) { 78 if (![view isKindOfClass:[RCTScrollView class]]) { 79 reject(RCTErrorUnspecified, [NSString stringWithFormat:@"snapshotContentContainer can only be used on a RCTScrollView. instead got: %@", view], nil); 80 return; 81 } 82 RCTScrollView* rctScrollView = (RCTScrollView *)view; 83 scrollView = rctScrollView.scrollView; 84 rendered = scrollView; 85 } 86 else { 87 rendered = view; 88 } 89 90 if (size.width < 0.1 || size.height < 0.1) { 91 size = snapshotContentContainer ? scrollView.contentSize : view.bounds.size; 92 } 93 if (size.width < 0.1 || size.height < 0.1) { 94 reject(RCTErrorUnspecified, [NSString stringWithFormat:@"The content size must not be zero or negative. Got: (%g, %g)", size.width, size.height], nil); 95 return; 96 } 97 98 CGPoint savedContentOffset; 99 CGRect savedFrame; 100 if (snapshotContentContainer) { 101 // Save scroll & frame and set it temporarily to the full content size 102 savedContentOffset = scrollView.contentOffset; 103 savedFrame = scrollView.frame; 104 scrollView.contentOffset = CGPointZero; 105 scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); 106 } 107 108 UIGraphicsBeginImageContextWithOptions(size, NO, 0); 109 110 success = [rendered drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES]; 111 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 112 UIGraphicsEndImageContext(); 113 114 if (snapshotContentContainer) { 115 // Restore scroll & frame 116 scrollView.contentOffset = savedContentOffset; 117 scrollView.frame = savedFrame; 118 } 119 120 if (!success) { 121 reject(RCTErrorUnspecified, @"The view cannot be captured. drawViewHierarchyInRect was not successful. This is a potential technical or security limitation.", nil); 122 return; 123 } 124 125 if (!image) { 126 reject(RCTErrorUnspecified, @"Failed to capture view snapshot. UIGraphicsGetImageFromCurrentImageContext() returned nil!", nil); 127 return; 128 } 129 130 // Convert image to data (on a background thread) 131 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 132 133 NSData *data; 134 if ([format isEqualToString:@"jpg"]) { 135 CGFloat quality = [RCTConvert CGFloat:options[@"quality"]]; 136 data = UIImageJPEGRepresentation(image, quality); 137 } 138 else { 139 data = UIImagePNGRepresentation(image); 140 } 141 142 NSError *error = nil; 143 NSString *res = nil; 144 if ([result isEqualToString:@"base64"]) { 145 // Return as a base64 raw string 146 res = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength]; 147 } 148 else if ([result isEqualToString:@"data-uri"]) { 149 // Return as a base64 data uri string 150 NSString *base64 = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength]; 151 NSString *imageFormat = ([format isEqualToString:@"jpg"]) ? @"jpeg" : format; 152 res = [NSString stringWithFormat:@"data:image/%@;base64,%@", imageFormat, base64]; 153 } 154 else { 155 // Save to a temp file 156 NSString *path = RCTTempFilePath(format, &error); 157 if (path && !error) { 158 if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) { 159 res = path; 160 } 161 } 162 } 163 164 if (res && !error) { 165 resolve(res); 166 return; 167 } 168 169 // If we reached here, something went wrong 170 if (error) reject(RCTErrorUnspecified, error.localizedDescription, error); 171 else reject(RCTErrorUnspecified, @"viewshot unknown error", nil); 172 }); 173 }]; 174} 175 176 177@end 178