Skip to content

Commit 36c8271

Browse files
authored
Merge pull request #42 from gre/dirs
drop filename option, add path option
2 parents ef1a8d0 + 9d59033 commit 36c8271

File tree

6 files changed

+128
-41
lines changed

6 files changed

+128
-41
lines changed

README.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ Snapshot a React Native view and save it to an image.
1010
## Usage
1111

1212
```js
13-
import RNViewShot from "react-native-view-shot";
13+
import { takeSnapshot } from "react-native-view-shot";
1414

15-
RNViewShot.takeSnapshot(viewRef, {
15+
takeSnapshot(viewRef, {
1616
format: "jpeg",
1717
quality: 0.8
1818
})
@@ -28,7 +28,7 @@ RNViewShot.takeSnapshot(viewRef, {
2828

2929
## Full API
3030

31-
### `RNViewShot.takeSnapshot(view, options)`
31+
### `takeSnapshot(view, options)`
3232

3333
Returns a Promise of the image URI.
3434

@@ -41,9 +41,27 @@ Returns a Promise of the image URI.
4141
- `"file"` (default): save to a temporary file *(that will only exist for as long as the app is running)*.
4242
- `"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*.
4343
- `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
44-
- **`filename`** *(string)*: the name of the generated file if any (Android only). Defaults to `ReactNative_snapshot_image_${timestamp}`.
44+
- **`path`** *(string)*: The absolute path where the file get generated. See *`dirs` constants* for more information.
4545
- **`snapshotContentContainer`** *(bool)*: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height. (Android only)
4646

47+
### `dirs` constants
48+
49+
By default, takeSnapshot will export in a temporary folder and the snapshot file will be deleted as soon as the app leaves. If you use the `path` option, you make the snapshot file more permanent and at a specific file location. To make file location more 'universal', the library exports some classic directory constants:
50+
51+
```js
52+
import { takeSnapshot, dirs } from "react-native-view-shot";
53+
// cross platform dirs:
54+
const { CacheDir, DocumentDir, MainBundleDir, MovieDir, MusicDir, PictureDir } = dirs;
55+
// only available Android:
56+
const { DCIMDir, DownloadDir, RingtoneDir, SDCardDir } = dirs;
57+
58+
takeSnapshot(viewRef, { path: PictureDir+"/foo.png" })
59+
.then(
60+
uri => console.log("Image saved to", uri),
61+
error => console.error("Oops, snapshot failed", error)
62+
);
63+
```
64+
4765
## Caveats
4866

4967
Snapshots are not guaranteed to be pixel perfect. It also depends on the platform. Here is some difference we have noticed and how to workaround.
@@ -54,8 +72,7 @@ Snapshots are not guaranteed to be pixel perfect. It also depends on the platfor
5472
### specific to Android implementation
5573

5674
- you need to make sure `collapsable` is set to `false` if you want to snapshot a **View**. Otherwise that view won't reflect any UI View. ([found by @gaguirre](https://github.com/gre/react-native-view-shot/issues/7#issuecomment-245302844))
57-
- if you want to share out the screenshoted file, you will have to copy it somewhere first so it's accessible to an Intent, see comment: https://github.com/gre/react-native-view-shot/issues/11#issuecomment-251080804 .
58-
- if you implement a third party library and want to get back a File, you must first resolve the `Uri`. the `file` result returns an `Uri` so it's consistent with iOS and you can give it to `Image.getSize` for instance.
75+
- if you implement a third party library and want to get back a File, you must first resolve the `Uri`. (the `file` result returns an `Uri` so it's consistent with iOS and can be given to APIs like `Image.getSize`)
5976

6077
## Getting started
6178

android/build.gradle

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ android {
2525
}
2626
}
2727

28-
repositories {
29-
mavenCentral()
30-
maven {
31-
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
32-
url "$projectDir/../../../node_modules/react-native/android"
28+
allprojects {
29+
repositories {
30+
mavenLocal()
31+
jcenter()
32+
maven {
33+
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
34+
url "$rootDir/../node_modules/react-native/android"
35+
}
3336
}
3437
}
3538

3639
dependencies {
37-
compile "com.facebook.react:react-native:+" // From node_modules
40+
compile 'com.facebook.react:react-native:+'
3841
}

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.content.Context;
55
import android.graphics.Bitmap;
66
import android.os.AsyncTask;
7+
import android.os.Environment;
78
import android.util.DisplayMetrics;
89
import android.view.View;
910

@@ -22,6 +23,8 @@
2223
import java.io.File;
2324
import java.io.FilenameFilter;
2425
import java.io.IOException;
26+
import java.util.HashMap;
27+
import java.util.Map;
2528

2629
public class RNViewShotModule extends ReactContextBaseJavaModule {
2730

@@ -37,6 +40,11 @@ public String getName() {
3740
return "RNViewShot";
3841
}
3942

43+
@Override
44+
public Map<String, Object> getConstants() {
45+
return getSystemFolders(this.getReactApplicationContext());
46+
}
47+
4048
@Override
4149
public void onCatalystInstanceDestroy() {
4250
super.onCatalystInstanceDestroy();
@@ -66,17 +74,25 @@ public void takeSnapshot(int tag, ReadableMap options, Promise promise) {
6674
String result = options.hasKey("result") ? options.getString("result") : "file";
6775
Boolean snapshotContentContainer = options.hasKey("snapshotContentContainer") ? options.getBoolean("snapshotContentContainer") : false;
6876
try {
69-
String name = options.hasKey("filename") ? options.getString("filename") : null;
70-
File tmpFile = "file".equals(result) ? createTempFile(getReactApplicationContext(), format, name) : null;
77+
File file = null;
78+
if ("file".equals(result)) {
79+
if (options.hasKey("path")) {
80+
file = new File(options.getString("path"));
81+
file.createNewFile();
82+
}
83+
else {
84+
file = createTempFile(getReactApplicationContext(), format);
85+
}
86+
}
7187
UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
72-
uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, tmpFile, result, snapshotContentContainer, promise));
88+
uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, file, result, snapshotContentContainer, promise));
7389
}
7490
catch (Exception e) {
7591
promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
7692
}
7793
}
7894

79-
private static final String TEMP_FILE_PREFIX = "ReactNative_snapshot_image_";
95+
private static final String TEMP_FILE_PREFIX = "ReactNative-snapshot-image";
8096

8197
/**
8298
* Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped
@@ -116,11 +132,30 @@ public boolean accept(File dir, String filename) {
116132
}
117133
}
118134

135+
static private Map<String, Object> getSystemFolders(ReactApplicationContext ctx) {
136+
Map<String, Object> res = new HashMap<>();
137+
res.put("CacheDir", ctx.getCacheDir().getAbsolutePath());
138+
res.put("DCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());
139+
res.put("DocumentDir", ctx.getFilesDir().getAbsolutePath());
140+
res.put("DownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath());
141+
res.put("MainBundleDir", ctx.getApplicationInfo().dataDir);
142+
res.put("MovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath());
143+
res.put("MusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
144+
res.put("PictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath());
145+
res.put("RingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath());
146+
String state;
147+
state = Environment.getExternalStorageState();
148+
if (state.equals(Environment.MEDIA_MOUNTED)) {
149+
res.put("SDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath());
150+
}
151+
return res;
152+
}
153+
119154
/**
120155
* Create a temporary file in the cache directory on either internal or external storage,
121156
* whichever is available and has more free space.
122157
*/
123-
private File createTempFile(Context context, String ext, String name)
158+
private File createTempFile(Context context, String ext)
124159
throws IOException {
125160
File externalCacheDir = context.getExternalCacheDir();
126161
File internalCacheDir = context.getCacheDir();
@@ -139,12 +174,6 @@ else if (internalCacheDir == null) {
139174
}
140175
String suffix = "." + ext;
141176
File tmpFile = File.createTempFile(TEMP_FILE_PREFIX, suffix, cacheDir);
142-
if (name != null) {
143-
File renamed = new File(cacheDir, name + suffix);
144-
tmpFile.renameTo(renamed);
145-
return renamed;
146-
}
147-
148177
return tmpFile;
149178
}
150179

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ private void captureView (View view, OutputStream os) {
124124
}
125125

126126
//evaluate real height
127-
if (this.snapshotContentContainer){
127+
if (snapshotContentContainer) {
128128
h=0;
129129
ScrollView scrollView = (ScrollView)view;
130130
for (int i = 0; i < scrollView.getChildCount(); i++) {

index.js

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
//@flow
2-
32
import { NativeModules, findNodeHandle } from "react-native";
4-
53
const { RNViewShot } = NativeModules;
64

5+
export const dirs = {
6+
// cross platform
7+
CacheDir: RNViewShot.CacheDir,
8+
DocumentDir: RNViewShot.DocumentDir,
9+
MainBundleDir: RNViewShot.MainBundleDir,
10+
MovieDir: RNViewShot.MovieDir,
11+
MusicDir: RNViewShot.MusicDir,
12+
PictureDir: RNViewShot.PictureDir,
13+
// only Android
14+
DCIMDir: RNViewShot.DCIMDir,
15+
DownloadDir: RNViewShot.DownloadDir,
16+
RingtoneDir: RNViewShot.RingtoneDir,
17+
SDCardDir: RNViewShot.SDCardDir,
18+
};
19+
720
export function takeSnapshot(
821
view: number | ReactElement<any>,
9-
options ?: {
10-
width ?: number;
11-
height ?: number;
12-
filename ?: string;
13-
format ?: "png" | "jpg" | "jpeg" | "webm";
14-
quality ?: number;
15-
result ?: "file" | "base64" | "data-uri";
16-
}
22+
options?: {
23+
width?: number,
24+
height?: number,
25+
path?: string,
26+
format?: "png" | "jpg" | "jpeg" | "webm",
27+
quality?: number,
28+
result?: "file" | "base64" | "data-uri",
29+
snapshotContentContainer?: bool
30+
} = {}
1731
): Promise<string> {
1832
if (typeof view !== "number") {
1933
const node = findNodeHandle(view);
@@ -23,4 +37,4 @@ export function takeSnapshot(
2337
return RNViewShot.takeSnapshot(view, options);
2438
}
2539

26-
export default { takeSnapshot };
40+
export default { takeSnapshot, dirs };

ios/RNViewShot.m

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ - (dispatch_queue_t)methodQueue
2020
return self.bridge.uiManager.methodQueue;
2121
}
2222

23+
- (NSDictionary *)constantsToExport
24+
{
25+
return @{
26+
@"CacheDir" : [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject],
27+
@"DocumentDir": [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject],
28+
@"MainBundleDir" : [[NSBundle mainBundle] bundlePath],
29+
@"MovieDir": [NSSearchPathForDirectoriesInDomains(NSMoviesDirectory, NSUserDomainMask, YES) firstObject],
30+
@"MusicDir": [NSSearchPathForDirectoriesInDomains(NSMusicDirectory, NSUserDomainMask, YES) firstObject],
31+
@"PictureDir": [NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES) firstObject],
32+
};
33+
}
34+
2335
// forked from RN implementation
2436
// https://github.com/facebook/react-native/blob/f35b372883a76b5666b016131d59268b42f3c40d/React/Modules/RCTUIManager.m#L1367
2537

@@ -75,10 +87,22 @@ - (dispatch_queue_t)methodQueue
7587
NSString *res = nil;
7688
if ([result isEqualToString:@"file"]) {
7789
// 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;
90+
NSString *path;
91+
if (options[@"path"]) {
92+
path = options[@"path"];
93+
NSString * folder = [path stringByDeletingLastPathComponent];
94+
NSFileManager * fm = [NSFileManager defaultManager];
95+
if(![fm fileExistsAtPath:folder]) {
96+
[fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:&error];
97+
[fm createFileAtPath:path contents:nil attributes:nil];
98+
}
99+
}
100+
else {
101+
path = RCTTempFilePath(format, &error);
102+
}
103+
if (path && !error) {
104+
if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) {
105+
res = path;
82106
}
83107
}
84108
}
@@ -95,13 +119,13 @@ - (dispatch_queue_t)methodQueue
95119
reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported result: %@. Try one of: file | base64 | data-uri", result], nil);
96120
return;
97121
}
98-
if (res != nil) {
122+
if (res && !error) {
99123
resolve(res);
100124
return;
101125
}
102126

103127
// If we reached here, something went wrong
104-
if (error != nil) reject(RCTErrorUnspecified, error.localizedDescription, error);
128+
if (error) reject(RCTErrorUnspecified, error.localizedDescription, error);
105129
else reject(RCTErrorUnspecified, @"viewshot unknown error", nil);
106130
});
107131
}];

0 commit comments

Comments
 (0)