RNViewShot.m 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. - (NSDictionary *)constantsToExport
  18. {
  19. return @{
  20. @"CacheDir" : [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject],
  21. @"DocumentDir": [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject],
  22. @"MainBundleDir" : [[NSBundle mainBundle] bundlePath],
  23. @"MovieDir": [NSSearchPathForDirectoriesInDomains(NSMoviesDirectory, NSUserDomainMask, YES) firstObject],
  24. @"MusicDir": [NSSearchPathForDirectoriesInDomains(NSMusicDirectory, NSUserDomainMask, YES) firstObject],
  25. @"PictureDir": [NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES) firstObject],
  26. };
  27. }
  28. // forked from RN implementation
  29. // https://github.com/facebook/react-native/blob/f35b372883a76b5666b016131d59268b42f3c40d/React/Modules/RCTUIManager.m#L1367
  30. RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
  31. withOptions:(NSDictionary *)options
  32. resolve:(RCTPromiseResolveBlock)resolve
  33. reject:(RCTPromiseRejectBlock)reject)
  34. {
  35. [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
  36. // Get view
  37. UIView *view;
  38. view = viewRegistry[target];
  39. if (!view) {
  40. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"No view found with reactTag: %@", target], nil);
  41. return;
  42. }
  43. // Get options
  44. CGSize size = [RCTConvert CGSize:options];
  45. NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
  46. NSString *result = [RCTConvert NSString:options[@"result"] ?: @"file"];
  47. BOOL snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"] ?: @"false"];
  48. // Capture image
  49. BOOL success;
  50. UIView* rendered;
  51. UIScrollView* scrollView;
  52. if (snapshotContentContainer) {
  53. if (![view isKindOfClass:[RCTScrollView class]]) {
  54. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"snapshotContentContainer can only be used on a RCTScrollView. instead got: %@", view], nil);
  55. return;
  56. }
  57. RCTScrollView* rctScrollView = view;
  58. scrollView = rctScrollView.scrollView;
  59. rendered = scrollView;
  60. }
  61. else {
  62. rendered = view;
  63. }
  64. if (size.width < 0.1 || size.height < 0.1) {
  65. size = snapshotContentContainer ? scrollView.contentSize : view.bounds.size;
  66. }
  67. if (size.width < 0.1 || size.height < 0.1) {
  68. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"The content size must not be zero or negative. Got: (%g, %g)", size.width, size.height], nil);
  69. return;
  70. }
  71. CGPoint savedContentOffset;
  72. CGRect savedFrame;
  73. if (snapshotContentContainer) {
  74. // Save scroll & frame and set it temporarily to the full content size
  75. savedContentOffset = scrollView.contentOffset;
  76. savedFrame = scrollView.frame;
  77. scrollView.contentOffset = CGPointZero;
  78. scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
  79. }
  80. UIGraphicsBeginImageContextWithOptions(size, NO, 0);
  81. success = [rendered drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
  82. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  83. UIGraphicsEndImageContext();
  84. if (snapshotContentContainer) {
  85. // Restore scroll & frame
  86. scrollView.contentOffset = savedContentOffset;
  87. scrollView.frame = savedFrame;
  88. }
  89. if (!success) {
  90. reject(RCTErrorUnspecified, @"The view cannot be captured. drawViewHierarchyInRect was not successful. This is a potential technical or security limitation.", nil);
  91. return;
  92. }
  93. if (!image) {
  94. reject(RCTErrorUnspecified, @"Failed to capture view snapshot. UIGraphicsGetImageFromCurrentImageContext() returned nil!", nil);
  95. return;
  96. }
  97. // Convert image to data (on a background thread)
  98. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  99. NSData *data;
  100. if ([format isEqualToString:@"png"]) {
  101. data = UIImagePNGRepresentation(image);
  102. } else if ([format isEqualToString:@"jpeg"] || [format isEqualToString:@"jpg"]) {
  103. CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1];
  104. data = UIImageJPEGRepresentation(image, quality);
  105. } else {
  106. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported image format: %@. Try one of: png | jpg | jpeg", format], nil);
  107. return;
  108. }
  109. NSError *error = nil;
  110. NSString *res = nil;
  111. if ([result isEqualToString:@"file"]) {
  112. // Save to a temp file
  113. NSString *path;
  114. if (options[@"path"]) {
  115. path = options[@"path"];
  116. NSString * folder = [path stringByDeletingLastPathComponent];
  117. NSFileManager * fm = [NSFileManager defaultManager];
  118. if(![fm fileExistsAtPath:folder]) {
  119. [fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:&error];
  120. [fm createFileAtPath:path contents:nil attributes:nil];
  121. }
  122. }
  123. else {
  124. path = RCTTempFilePath(format, &error);
  125. }
  126. if (path && !error) {
  127. if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) {
  128. res = path;
  129. }
  130. }
  131. }
  132. else if ([result isEqualToString:@"base64"]) {
  133. // Return as a base64 raw string
  134. res = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
  135. }
  136. else if ([result isEqualToString:@"data-uri"]) {
  137. // Return as a base64 data uri string
  138. NSString *base64 = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
  139. res = [NSString stringWithFormat:@"data:image/%@;base64,%@", format, base64];
  140. }
  141. else {
  142. reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported result: %@. Try one of: file | base64 | data-uri", result], nil);
  143. return;
  144. }
  145. if (res && !error) {
  146. resolve(res);
  147. return;
  148. }
  149. // If we reached here, something went wrong
  150. if (error) reject(RCTErrorUnspecified, error.localizedDescription, error);
  151. else reject(RCTErrorUnspecified, @"viewshot unknown error", nil);
  152. });
  153. }];
  154. }
  155. @end