Skip to content

Commit cadb795

Browse files
committed
Introduce a result option, one of file|base64|data-uri & Fixes #1
1 parent ee38431 commit cadb795

File tree

5 files changed

+121
-81
lines changed

5 files changed

+121
-81
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
Snapshot a React Native view and save it to an image.
55

6-
The image will be stored in a temporary file that will only exist for as long as the app is running.
7-
86
<img src="https://github.com/gre/react-native-view-shot-example/raw/master/docs/recursive.gif" width=300 />
97

108
## Usage
@@ -37,7 +35,12 @@ Returns a Promise of the image URI.
3735
- **`width`** / **`height`** *(number)*: the width and height of the image to capture.
3836
- **`format`** *(string)*: either `png` or `jpg`/`jpeg` or `webm` (Android). Defaults to `png`.
3937
- **`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`.
4144

4245

4346
## Getting started

android/src/main/java/fr/greweb/reactnativeviewshot/RNViewShotModule.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.graphics.Bitmap;
66
import android.os.AsyncTask;
77
import android.util.DisplayMetrics;
8+
import android.view.View;
89

910
import com.facebook.react.bridge.ReactApplicationContext;
1011
import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -55,17 +56,18 @@ public void takeSnapshot(int tag, ReadableMap options, Promise promise) {
5556
? Bitmap.CompressFormat.WEBP
5657
: null;
5758
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;
5961
}
6062
double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0;
6163
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
6264
Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
6365
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";
6567
try {
66-
File tmpFile = createTempFile(getReactApplicationContext(), format);
68+
File tmpFile = "file".equals(result) ? createTempFile(getReactApplicationContext(), format) : null;
6769
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));
6971
}
7072
catch (Exception e) {
7173
promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);

android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,53 +24,72 @@ public class ViewShot implements UIBlock {
2424
static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT";
2525

2626
private int tag;
27+
private String extension;
2728
private Bitmap.CompressFormat format;
2829
private double quality;
2930
private Integer width;
3031
private Integer height;
3132
private File output;
32-
private boolean base64;
33+
private String result;
3334
private Promise promise;
3435

3536
public ViewShot(
3637
int tag,
38+
String extension,
3739
Bitmap.CompressFormat format,
3840
double quality,
3941
@Nullable Integer width,
4042
@Nullable Integer height,
4143
File output,
42-
boolean base64,
44+
String result,
4345
Promise promise) {
4446
this.tag = tag;
47+
this.extension = extension;
4548
this.format = format;
4649
this.quality = quality;
4750
this.width = width;
4851
this.height = height;
4952
this.output = output;
50-
this.base64 = base64;
53+
this.result = result;
5154
this.promise = promise;
5255
}
5356

5457
@Override
5558
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
5659
OutputStream os = null;
5760
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+
}
5865
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)) {
6073
os = new ByteArrayOutputStream();
6174
captureView(view, os);
6275
byte[] bytes = ((ByteArrayOutputStream) os).toByteArray();
6376
String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
6477
promise.resolve(data);
65-
} else {
66-
os = new FileOutputStream(output);
78+
}
79+
else if ("data-uri".equals(result)) {
80+
os = new ByteArrayOutputStream();
6781
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");
7089
}
7190
}
7291
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");
7493
}
7594
finally {
7695
if (os != null) {

ios/RNViewShot.m

Lines changed: 76 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -28,69 +28,83 @@ - (dispatch_queue_t)methodQueue
2828
resolve:(RCTPromiseResolveBlock)resolve
2929
reject:(RCTPromiseRejectBlock)reject)
3030
{
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+
}
5783
}
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+
}];
94108
}
95109

96110

ios/RNViewShot.xcodeproj/project.pbxproj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424

2525
/* Begin PBXFileReference section */
2626
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; };
2929
/* End PBXFileReference section */
3030

3131
/* Begin PBXFrameworksBuildPhase section */
@@ -54,7 +54,9 @@
5454
B3E7B5891CC2AC0600A0062D /* RNViewShot.m */,
5555
134814211AA4EA7D00B7C361 /* Products */,
5656
);
57+
indentWidth = 2;
5758
sourceTree = "<group>";
59+
tabWidth = 2;
5860
};
5961
/* End PBXGroup section */
6062

@@ -197,7 +199,7 @@
197199
isa = XCBuildConfiguration;
198200
buildSettings = {
199201
HEADER_SEARCH_PATHS = (
200-
"$(inherited)",
202+
"$(inherited)",
201203
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
202204
"$(SRCROOT)/../../../React/**",
203205
"$(SRCROOT)/../../react-native/React/**",

0 commit comments

Comments
 (0)