RNViewShot.m 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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. #import <React/RCTBridge.h>
  10. @implementation RNViewShot
  11. RCT_EXPORT_MODULE()
  12. @synthesize bridge = _bridge;
  13. - (dispatch_queue_t)methodQueue
  14. {
  15. return RCTGetUIManagerQueue();
  16. }
  17. RCT_EXPORT_METHOD(captureScreenshot: (NSDictionary *)options
  18. resolve:(RCTPromiseResolveBlock)resolve
  19. reject:(RCTPromiseRejectBlock)reject)
  20. {
  21. [captureRef -1, options, resolve, reject];
  22. }
  23. RCT_EXPORT_METHOD(releaseCapture:(nonnull NSString *)uri)
  24. {
  25. NSString *directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ReactNative"];
  26. // Ensure it's a valid file in the tmp directory
  27. if ([uri hasPrefix:directory] && ![uri isEqualToString:directory]) {
  28. NSFileManager *fileManager = [NSFileManager new];
  29. if ([fileManager fileExistsAtPath:uri]) {
  30. [fileManager removeItemAtPath:uri error:NULL];
  31. }
  32. }
  33. }
  34. RCT_EXPORT_METHOD(captureRef:(nonnull NSNumber *)target
  35. withOptions:(NSDictionary *)options
  36. resolve:(RCTPromiseResolveBlock)resolve
  37. reject:(RCTPromiseRejectBlock)reject)
  38. {
  39. [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
  40. // Get view
  41. UIView *view;
  42. if (target == -1) {
  43. UIWindow *window = [[UIApplication sharedApplication] keyWindow];
  44. view = window.rootViewController.view;
  45. } else {
  46. view = viewRegistry[target];
  47. }
  48. if (!view) {
  49. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"No view found with reactTag: %@", target], nil);
  50. return;
  51. }
  52. // Get options
  53. CGSize size = [RCTConvert CGSize:options];
  54. NSString *format = [RCTConvert NSString:options[@"format"]];
  55. NSString *result = [RCTConvert NSString:options[@"result"]];
  56. BOOL snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"]];
  57. // Capture image
  58. BOOL success;
  59. UIView* rendered;
  60. UIScrollView* scrollView;
  61. if (snapshotContentContainer) {
  62. if (![view isKindOfClass:[RCTScrollView class]]) {
  63. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"snapshotContentContainer can only be used on a RCTScrollView. instead got: %@", view], nil);
  64. return;
  65. }
  66. RCTScrollView* rctScrollView = view;
  67. scrollView = rctScrollView.scrollView;
  68. rendered = scrollView;
  69. }
  70. else {
  71. rendered = view;
  72. }
  73. if (size.width < 0.1 || size.height < 0.1) {
  74. size = snapshotContentContainer ? scrollView.contentSize : view.bounds.size;
  75. }
  76. if (size.width < 0.1 || size.height < 0.1) {
  77. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"The content size must not be zero or negative. Got: (%g, %g)", size.width, size.height], nil);
  78. return;
  79. }
  80. CGPoint savedContentOffset;
  81. CGRect savedFrame;
  82. if (snapshotContentContainer) {
  83. // Save scroll & frame and set it temporarily to the full content size
  84. savedContentOffset = scrollView.contentOffset;
  85. savedFrame = scrollView.frame;
  86. scrollView.contentOffset = CGPointZero;
  87. scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
  88. }
  89. if (target == -1) {
  90. if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
  91. UIGraphicsBeginImageContextWithOptions(currentView.window.bounds.size, NO, [UIScreen mainScreen].scale);
  92. } else {
  93. UIGraphicsBeginImageContext(currentView.window.bounds.size);
  94. }
  95. } else {
  96. UIGraphicsBeginImageContextWithOptions(size, NO, 0);
  97. }
  98. success = [rendered drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
  99. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  100. UIGraphicsEndImageContext();
  101. if (snapshotContentContainer) {
  102. // Restore scroll & frame
  103. scrollView.contentOffset = savedContentOffset;
  104. scrollView.frame = savedFrame;
  105. }
  106. if (!success) {
  107. reject(RCTErrorUnspecified, @"The view cannot be captured. drawViewHierarchyInRect was not successful. This is a potential technical or security limitation.", nil);
  108. return;
  109. }
  110. if (!image) {
  111. reject(RCTErrorUnspecified, @"Failed to capture view snapshot. UIGraphicsGetImageFromCurrentImageContext() returned nil!", nil);
  112. return;
  113. }
  114. // Convert image to data (on a background thread)
  115. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  116. NSData *data;
  117. if ([format isEqualToString:@"jpg"]) {
  118. CGFloat quality = [RCTConvert CGFloat:options[@"quality"]];
  119. data = UIImageJPEGRepresentation(image, quality);
  120. }
  121. else {
  122. data = UIImagePNGRepresentation(image);
  123. }
  124. NSError *error = nil;
  125. NSString *res = nil;
  126. if ([result isEqualToString:@"base64"]) {
  127. // Return as a base64 raw string
  128. res = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
  129. }
  130. else if ([result isEqualToString:@"data-uri"]) {
  131. // Return as a base64 data uri string
  132. NSString *base64 = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
  133. res = [NSString stringWithFormat:@"data:image/%@;base64,%@", format, base64];
  134. }
  135. else {
  136. // Save to a temp file
  137. NSString *path = RCTTempFilePath(format, &error);
  138. if (path && !error) {
  139. if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) {
  140. res = path;
  141. }
  142. }
  143. }
  144. if (res && !error) {
  145. resolve(res);
  146. return;
  147. }
  148. // If we reached here, something went wrong
  149. if (error) reject(RCTErrorUnspecified, error.localizedDescription, error);
  150. else reject(RCTErrorUnspecified, @"viewshot unknown error", nil);
  151. });
  152. }];
  153. }
  154. @end