Browse Source

Introduce a result option, one of file|base64|data-uri & Fixes #1

Gaëtan Renaudeau 8 years ago
parent
commit
cadb795ac5

+ 6
- 3
README.md View File

@@ -3,8 +3,6 @@
3 3
 
4 4
 Snapshot a React Native view and save it to an image.
5 5
 
6
-The image will be stored in a temporary file that will only exist for as long as the app is running.
7
-
8 6
 <img src="https://github.com/gre/react-native-view-shot-example/raw/master/docs/recursive.gif" width=300 />
9 7
 
10 8
 ## Usage
@@ -37,7 +35,12 @@ Returns a Promise of the image URI.
37 35
  - **`width`** / **`height`** *(number)*: the width and height of the image to capture.
38 36
  - **`format`** *(string)*: either `png` or `jpg`/`jpeg` or `webm` (Android). Defaults to `png`.
39 37
  - **`quality`** *(number)*: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpeg)
40
- - **`base64`** *(bool)*: if true, the promise returns the base64 encoded data instead of the uri. Defaults to `false`.
38
+ - **`result`** *(string)*, the method you want to use to save the snapshot, one of:
39
+   - `"file"` (default): save to a temporary file *(that will only exist for as long as the app is running)*.
40
+   - `"base64"`: encode as base64 and returns the raw string. Use only with small images as this may result of lags (the string is sent over the bridge). *N.B. This is not a data uri, use `data-uri` instead*.
41
+   - `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
42
+
43
+   . if true, the promise returns the base64 encoded data instead of the uri. Defaults to `false`.
41 44
 
42 45
 
43 46
 ## Getting started

+ 6
- 4
android/src/main/java/fr/greweb/reactnativeviewshot/RNViewShotModule.java View File

@@ -5,6 +5,7 @@ import android.content.Context;
5 5
 import android.graphics.Bitmap;
6 6
 import android.os.AsyncTask;
7 7
 import android.util.DisplayMetrics;
8
+import android.view.View;
8 9
 
9 10
 import com.facebook.react.bridge.ReactApplicationContext;
10 11
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -55,17 +56,18 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
55 56
                         ? Bitmap.CompressFormat.WEBP
56 57
                         : null;
57 58
         if (compressFormat == null) {
58
-            throw new JSApplicationIllegalArgumentException("Unsupported image format: " + format);
59
+            promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Unsupported image format: "+format+". Try one of: png | jpg | jpeg");
60
+            return;
59 61
         }
60 62
         double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0;
61 63
         DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
62 64
         Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
63 65
         Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : null;
64
-        boolean base64 = options.hasKey("base64") ? options.getBoolean("base64") : false;
66
+        String result = options.hasKey("result") ? options.getString("result") : "file";
65 67
         try {
66
-            File tmpFile = createTempFile(getReactApplicationContext(), format);
68
+            File tmpFile = "file".equals(result) ? createTempFile(getReactApplicationContext(), format) : null;
67 69
             UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
68
-            uiManager.addUIBlock(new ViewShot(tag, compressFormat, quality, width, height, tmpFile, base64, promise));
70
+            uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, tmpFile, result, promise));
69 71
         }
70 72
         catch (Exception e) {
71 73
             promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);

+ 28
- 9
android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java View File

@@ -24,30 +24,33 @@ public class ViewShot implements UIBlock {
24 24
     static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT";
25 25
 
26 26
     private int tag;
27
+    private String extension;
27 28
     private Bitmap.CompressFormat format;
28 29
     private double quality;
29 30
     private Integer width;
30 31
     private Integer height;
31 32
     private File output;
32
-    private boolean base64;
33
+    private String result;
33 34
     private Promise promise;
34 35
 
35 36
     public ViewShot(
36 37
             int tag,
38
+            String extension,
37 39
             Bitmap.CompressFormat format,
38 40
             double quality,
39 41
             @Nullable Integer width,
40 42
             @Nullable Integer height,
41 43
             File output,
42
-            boolean base64,
44
+            String result,
43 45
             Promise promise) {
44 46
         this.tag = tag;
47
+        this.extension = extension;
45 48
         this.format = format;
46 49
         this.quality = quality;
47 50
         this.width = width;
48 51
         this.height = height;
49 52
         this.output = output;
50
-        this.base64 = base64;
53
+        this.result = result;
51 54
         this.promise = promise;
52 55
     }
53 56
 
@@ -55,22 +58,38 @@ public class ViewShot implements UIBlock {
55 58
     public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
56 59
         OutputStream os = null;
57 60
         View view = nativeViewHierarchyManager.resolveView(tag);
61
+        if (view == null) {
62
+            promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "No view found with reactTag: "+tag);
63
+            return;
64
+        }
58 65
         try {
59
-            if (base64) {
66
+            if ("file".equals(result)) {
67
+                os = new FileOutputStream(output);
68
+                captureView(view, os);
69
+                String uri = Uri.fromFile(output).toString();
70
+                promise.resolve(uri);
71
+            }
72
+            else if ("base64".equals(result)) {
60 73
                 os = new ByteArrayOutputStream();
61 74
                 captureView(view, os);
62 75
                 byte[] bytes = ((ByteArrayOutputStream) os).toByteArray();
63 76
                 String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
64 77
                 promise.resolve(data);
65
-            } else {
66
-                os = new FileOutputStream(output);
78
+            }
79
+            else if ("data-uri".equals(result)) {
80
+                os = new ByteArrayOutputStream();
67 81
                 captureView(view, os);
68
-                String uri = Uri.fromFile(output).toString();
69
-                promise.resolve(uri);
82
+                byte[] bytes = ((ByteArrayOutputStream) os).toByteArray();
83
+                String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
84
+                data = "data:image/"+extension+";base64," + data;
85
+                promise.resolve(data);
86
+            }
87
+            else {
88
+                promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Unsupported result: "+result+". Try one of: file | base64 | data-uri");
70 89
             }
71 90
         }
72 91
         catch (Exception e) {
73
-            promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
92
+            promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Failed to capture view snapshot");
74 93
         }
75 94
         finally {
76 95
             if (os != null) {

+ 76
- 62
ios/RNViewShot.m View File

@@ -28,69 +28,83 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
28 28
                   resolve:(RCTPromiseResolveBlock)resolve
29 29
                   reject:(RCTPromiseRejectBlock)reject)
30 30
 {
31
-    [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
32
-
33
-        // Get view
34
-        UIView *view;
35
-        view = viewRegistry[target];
36
-        if (!view) {
37
-            reject(RCTErrorUnspecified, [NSString stringWithFormat:@"No view found with reactTag: %@", target], nil);
38
-            return;
39
-        }
40
-
41
-        // Get options
42
-        CGSize size = [RCTConvert CGSize:options];
43
-        NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
44
-
45
-        // Capture image
46
-        if (size.width < 0.1 || size.height < 0.1) {
47
-            size = view.bounds.size;
48
-        }
49
-        UIGraphicsBeginImageContextWithOptions(size, NO, 0);
50
-        BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
51
-        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
52
-        UIGraphicsEndImageContext();
53
-
54
-        if (!success || !image) {
55
-            reject(RCTErrorUnspecified, @"Failed to capture view snapshot.", nil);
56
-            return;
31
+  [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
32
+    
33
+    // Get view
34
+    UIView *view;
35
+    view = viewRegistry[target];
36
+    if (!view) {
37
+      reject(RCTErrorUnspecified, [NSString stringWithFormat:@"No view found with reactTag: %@", target], nil);
38
+      return;
39
+    }
40
+    
41
+    // Get options
42
+    CGSize size = [RCTConvert CGSize:options];
43
+    NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
44
+    NSString *result = [RCTConvert NSString:options[@"result"] ?: @"file"];
45
+    
46
+    // Capture image
47
+    if (size.width < 0.1 || size.height < 0.1) {
48
+      size = view.bounds.size;
49
+    }
50
+    UIGraphicsBeginImageContextWithOptions(size, NO, 0);
51
+    BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
52
+    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
53
+    UIGraphicsEndImageContext();
54
+    
55
+    if (!success || !image) {
56
+      reject(RCTErrorUnspecified, @"Failed to capture view snapshot", nil);
57
+      return;
58
+    }
59
+    
60
+    // Convert image to data (on a background thread)
61
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
62
+      
63
+      NSData *data;
64
+      if ([format isEqualToString:@"png"]) {
65
+        data = UIImagePNGRepresentation(image);
66
+      } else if ([format isEqualToString:@"jpeg"] || [format isEqualToString:@"jpg"]) {
67
+        CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1];
68
+        data = UIImageJPEGRepresentation(image, quality);
69
+      } else {
70
+        reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported image format: %@. Try one of: png | jpg | jpeg", format], nil);
71
+        return;
72
+      }
73
+      
74
+      NSError *error = nil;
75
+      NSString *res = nil;
76
+      if ([result isEqualToString:@"file"]) {
77
+        // Save to a temp file
78
+        NSString *tempFilePath = RCTTempFilePath(format, &error);
79
+        if (tempFilePath) {
80
+          if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) {
81
+            res = tempFilePath;
82
+          }
57 83
         }
58
-
59
-        // Convert image to data (on a background thread)
60
-        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
61
-
62
-            NSData *data;
63
-            if ([format isEqualToString:@"png"]) {
64
-                data = UIImagePNGRepresentation(image);
65
-            } else if ([format isEqualToString:@"jpeg"] || [format isEqualToString:@"jpg"]) {
66
-                CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1];
67
-                data = UIImageJPEGRepresentation(image, quality);
68
-            } else {
69
-                reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported image format: %@", format], nil);
70
-                return;
71
-            }
72
-
73
-            NSError *error = nil;
74
-            if ([[options objectForKey:@"base64"] boolValue]) {
75
-                // Return as a base64'd string
76
-                NSString *dataString = [data base64EncodedStringWithOptions:0];
77
-                resolve(dataString);
78
-                return;
79
-            } else {
80
-                // Save to a temp file
81
-                NSString *tempFilePath = RCTTempFilePath(format, &error);
82
-                if (tempFilePath) {
83
-                    if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) {
84
-                        resolve(tempFilePath);
85
-                        return;
86
-                    }
87
-                }
88
-            }
89
-
90
-            // If we reached here, something went wrong
91
-            reject(RCTErrorUnspecified, error.localizedDescription, error);
92
-        });
93
-    }];
84
+      }
85
+      else if ([result isEqualToString:@"base64"]) {
86
+        // Return as a base64 raw string
87
+        res = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
88
+      }
89
+      else if ([result isEqualToString:@"data-uri"]) {
90
+        // Return as a base64 data uri string
91
+        NSString *base64 = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
92
+        res = [NSString stringWithFormat:@"data:image/%@;base64,%@", format, base64];
93
+      }
94
+      else {
95
+        reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported result: %@. Try one of: file | base64 | data-uri", result], nil);
96
+        return;
97
+      }
98
+      if (res != nil) {
99
+        resolve(res);
100
+        return;
101
+      }
102
+      
103
+      // If we reached here, something went wrong
104
+      if (error != nil) reject(RCTErrorUnspecified, error.localizedDescription, error);
105
+      else reject(RCTErrorUnspecified, @"viewshot unknown error", nil);
106
+    });
107
+  }];
94 108
 }
95 109
 
96 110
 

+ 5
- 3
ios/RNViewShot.xcodeproj/project.pbxproj View File

@@ -24,8 +24,8 @@
24 24
 
25 25
 /* Begin PBXFileReference section */
26 26
 		134814201AA4EA6300B7C361 /* libRNViewShot.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNViewShot.a; sourceTree = BUILT_PRODUCTS_DIR; };
27
-		B3E7B5881CC2AC0600A0062D /* RNViewShot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNViewShot.h; sourceTree = "<group>"; };
28
-		B3E7B5891CC2AC0600A0062D /* RNViewShot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNViewShot.m; sourceTree = "<group>"; };
27
+		B3E7B5881CC2AC0600A0062D /* RNViewShot.h */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.h; path = RNViewShot.h; sourceTree = "<group>"; tabWidth = 2; };
28
+		B3E7B5891CC2AC0600A0062D /* RNViewShot.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNViewShot.m; sourceTree = "<group>"; tabWidth = 2; };
29 29
 /* End PBXFileReference section */
30 30
 
31 31
 /* Begin PBXFrameworksBuildPhase section */
@@ -54,7 +54,9 @@
54 54
 				B3E7B5891CC2AC0600A0062D /* RNViewShot.m */,
55 55
 				134814211AA4EA7D00B7C361 /* Products */,
56 56
 			);
57
+			indentWidth = 2;
57 58
 			sourceTree = "<group>";
59
+			tabWidth = 2;
58 60
 		};
59 61
 /* End PBXGroup section */
60 62
 
@@ -197,7 +199,7 @@
197 199
 			isa = XCBuildConfiguration;
198 200
 			buildSettings = {
199 201
 				HEADER_SEARCH_PATHS = (
200
-				"$(inherited)",
202
+					"$(inherited)",
201 203
 					/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
202 204
 					"$(SRCROOT)/../../../React/**",
203 205
 					"$(SRCROOT)/../../react-native/React/**",