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