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