├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── stale.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── rumax │ └── reactnative │ └── saveview │ ├── SaveView.java │ ├── SaveViewModule.java │ └── SaveViewPackage.java ├── ios └── RNSaveView │ ├── RNSaveView.xcodeproj │ └── project.pbxproj │ └── RNSaveView │ ├── RNSaveViewConstants.h │ ├── RNSaveViewConstants.m │ ├── RNSaveViewManager.h │ └── RNSaveViewManager.m ├── package.json ├── react-native-save-view.podspec └── src └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [**/{.js,package.json}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.java] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** 10 | 11 | - [ ] I checked the demo project and cannot reproduce the issue 12 | - [ ] I checked the demo project and the issue exists 13 | 14 | Steps to reproduce the behavior: 15 | 1. Render with '...' elements (Code example) 16 | 2. Other actions '...' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Smartphone (please complete the following information):** 26 | - Device: [e.g. iPhone6] 27 | - OS: [e.g. iOS8.1] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 5 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | react-native-save-view-*.tgz 12 | package/ 13 | 14 | # Xcode 15 | # 16 | build/ 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | xcuserdata 26 | *.xccheckout 27 | *.moved-aside 28 | DerivedData 29 | *.hmap 30 | *.ipa 31 | *.xcuserstate 32 | project.xcworkspace 33 | 34 | 35 | # Android/IntelliJ 36 | # 37 | build/ 38 | .idea 39 | .gradle 40 | local.properties 41 | *.iml 42 | 43 | # BUCK 44 | buck-out/ 45 | \.buckd/ 46 | *.keystore 47 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .DS_Store 3 | .editorconfig 4 | .github 5 | CONTRIBUTING.md 6 | node_modules 7 | package-lock.json 8 | PULL_REQUEST_TEMPLATE.md 9 | react-native-save-view-*.tgz 10 | android/*.iml 11 | android/build 12 | ios/RNSaveView/RNSaveView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 13 | ios/RNSaveView/RNSaveView.xcodeproj/project.xcworkspace/xcuserdata/*.xcuserdatad/UserInterfaceState.xcuserstate 14 | ios/RNSaveView/RNSaveView.xcodeproj/xcuserdata/*.xcuserdatad/xcschemes/xcschememanagement.plist 15 | ios/RNSaveView/RNSaveView.xcodeproj/project.xcworkspace/contents.xcworkspacedata 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to react-native-PDFView 2 | 3 | :+1::tada: Thanks for taking the time to contribute! :tada::+1: 4 | 5 | ## Styleguides 6 | 7 | Please, follow the current style of the code. 8 | 9 | ### Git Commit Messages 10 | 11 | * Use the present tense ("Add feature" not "Added feature") 12 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 13 | * Limit the first line to 72 characters or less 14 | * Reference issues and pull requests liberally after the first line 15 | * Consider starting the commit message with an applicable emoji: 16 | * :art: `:art:` when improving the format/structure of the code 17 | * :racehorse: `:racehorse:` when improving performance 18 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks 19 | * :memo: `:memo:` when writing docs 20 | * :lollipop: `:lollipop:` when fixing Android 21 | * :apple: `:apple:` when fixing iOS 22 | * :bug: `:bug:` when fixing a bug 23 | * :fire: `:fire:` when removing code or files 24 | * :green_heart: `:green_heart:` when fixing the CI build 25 | * :white_check_mark: `:white_check_mark:` when adding tests 26 | * :lock: `:lock:` when dealing with security 27 | * :arrow_up: `:arrow_up:` when upgrading dependencies 28 | * :arrow_down: `:arrow_down:` when downgrading dependencies 29 | * :shirt: `:shirt:` when removing linter warnings 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Maksym Rusynyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description of changes 2 | 3 | ### I did Exploratory testing 4 | - [ ] android 5 | - [ ] ios 6 | 7 | ### Can it be published to NPM? 8 | - [ ] Breaking change 9 | - [ ] Documentation is updated 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-save-view 3 | 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/87b93c7986514ee2829370b17423a6e2)](https://www.codacy.com/app/rumax/react-native-SaveView?utm_source=github.com&utm_medium=referral&utm_content=rumax/react-native-SaveView&utm_campaign=Badge_Grade) 5 | [![npm](https://img.shields.io/npm/l/express.svg)](https://github.com/rumax/react-native-SaveView) 6 | [![npm version](https://badge.fury.io/js/react-native-save-view.svg)](https://badge.fury.io/js/react-native-save-view) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 8 | 9 | Library for saving React Native View/ScrollView elements 10 | 11 | ## Example 12 | 13 | ```js 14 | export default class App extends Component { 15 | componentDidMount() { 16 | this._saveView(); 17 | } 18 | 19 | render() { 20 | return ( 21 | this._viewRef = ref} collapsable={false}> 22 | Some content 23 | 24 | ); 25 | } 26 | 27 | async _saveView() { 28 | await this._makeSnapshotPNG(); 29 | await this._makeSnapshotPNGBase64(); 30 | } 31 | 32 | async _makeSnapshotPNG() { 33 | await RNSaveView.saveToPNG(ref, '/sdcard/Download/view.png'); 34 | // Check /sdcard/Download/view.png using Device File Explorer 35 | } 36 | 37 | async _makeSnapshotPNGBase64() { 38 | const base64 = await RNSaveView.saveToPNGBase64(ref); 39 | console.log('base64:', base64); 40 | } 41 | } 42 | ``` 43 | 44 | ## Methods 45 | Name|Android|iOS|Description| 46 | ----|-------|---|-----------| 47 | saveToPNG|✓|✓|Save View to PNG file. Before the function is called, check that android has [write to file permissions](https://developer.android.com/training/data-storage/files)| 48 | saveToPNGBase64|✓|✓|Save View to PNG base64| 49 | 50 | ## License 51 | 52 | [MIT](https://opensource.org/licenses/MIT) 53 | 54 | ## Author 55 | 56 | - [rumax](https://github.com/rumax) 57 | 58 | ### Other information 59 | 60 | - Generated with [react-native-create-library](https://github.com/frostney/react-native-create-library) 61 | - Zero JavaScript dependency. Which means that you do not bring other dependencies to your project 62 | - If you think that something is missing or would like to propose new feature, please, discuss it with author 63 | - Please, feel free to ⭐️ the project. This gives the confidence that you like it and a great job was done by publishing and supporting it 🤩 64 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | def safeExtGet(prop, fallback) { 2 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 3 | } 4 | 5 | apply plugin: 'com.android.library' 6 | 7 | android { 8 | compileSdkVersion safeExtGet('compileSdkVersion', 26) 9 | buildToolsVersion safeExtGet('buildToolsVersion', '25.0.3') 10 | 11 | defaultConfig { 12 | minSdkVersion safeExtGet('minSdkVersion', 19) 13 | targetSdkVersion safeExtGet('targetSdkVersion', 25) 14 | } 15 | lintOptions { 16 | abortOnError false 17 | } 18 | } 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | //noinspection GradleDynamicVersion 26 | implementation 'com.facebook.react:react-native:+' 27 | } 28 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/rumax/reactnative/saveview/SaveView.java: -------------------------------------------------------------------------------- 1 | package com.rumax.reactnative.saveview; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.drawable.Drawable; 6 | import android.util.Base64; 7 | import android.view.View; 8 | import android.widget.ScrollView; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import com.facebook.react.bridge.ReactApplicationContext; 13 | import com.facebook.react.uimanager.NativeViewHierarchyManager; 14 | import com.facebook.react.uimanager.UIBlock; 15 | import com.facebook.react.uimanager.UIManagerModule; 16 | 17 | import java.io.ByteArrayOutputStream; 18 | import java.io.File; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | 22 | public class SaveView { 23 | private ReactApplicationContext context; 24 | 25 | public SaveView(@NonNull final ReactApplicationContext context) { 26 | this.context = context; 27 | } 28 | 29 | public void saveToPNG(final int reactTag, final File file, @NonNull final OnComplete callback) { 30 | context.getNativeModule(UIManagerModule.class).addUIBlock(new UIBlock() { 31 | @Override 32 | public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { 33 | try { 34 | View view = nativeViewHierarchyManager.resolveView(reactTag); 35 | Bitmap bitmap = getBitmap(view); 36 | saveBitmap(bitmap, file); 37 | callback.onSuccess(); 38 | } catch (Exception e) { 39 | callback.onFail(e); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | public void saveToPNGBase64(final int reactTag, @NonNull final OnCompleteBase64 callback) { 46 | context.getNativeModule(UIManagerModule.class).addUIBlock(new UIBlock() { 47 | @Override 48 | public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) { 49 | try { 50 | View view = nativeViewHierarchyManager.resolveView(reactTag); 51 | Bitmap bitmap = getBitmap(view); 52 | String base64 = bitmapToBase64(bitmap); 53 | callback.onSuccess(base64); 54 | } catch (Exception e) { 55 | callback.onFail(e); 56 | } 57 | } 58 | }); 59 | } 60 | 61 | private Bitmap getBitmap(View view) { 62 | View viewToSave = view; 63 | 64 | if (view instanceof ScrollView) { 65 | ScrollView scrollView = (ScrollView) view; 66 | viewToSave = scrollView.getChildAt(0); 67 | } 68 | 69 | return getBitmapFromView(viewToSave, viewToSave.getHeight(), viewToSave.getWidth()); 70 | } 71 | 72 | private Bitmap getBitmapFromView(View view, int height, int width) { 73 | Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 74 | Canvas canvas = new Canvas(bitmap); 75 | Drawable bgDrawable = view.getBackground(); 76 | 77 | if (bgDrawable != null) { 78 | bgDrawable.draw(canvas); 79 | } 80 | 81 | view.draw(canvas); 82 | 83 | return bitmap; 84 | } 85 | 86 | private void saveBitmap(Bitmap bitmap, File file) throws IOException { 87 | if (file.exists()) { 88 | throw new IOException("File [" + file.getAbsolutePath() + "] already exits"); 89 | } 90 | 91 | try (FileOutputStream output = new FileOutputStream(file)) { 92 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, output); 93 | } 94 | } 95 | 96 | private String bitmapToBase64(Bitmap bitmap) { 97 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 98 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); 99 | byte[] byteArray = byteArrayOutputStream .toByteArray(); 100 | return Base64.encodeToString(byteArray, Base64.DEFAULT); 101 | } 102 | 103 | public interface OnComplete { 104 | void onSuccess(); 105 | void onFail(Exception exception); 106 | } 107 | 108 | public interface OnCompleteBase64 { 109 | void onSuccess(String base64); 110 | void onFail(Exception exception); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /android/src/main/java/com/rumax/reactnative/saveview/SaveViewModule.java: -------------------------------------------------------------------------------- 1 | package com.rumax.reactnative.saveview; 2 | 3 | import com.facebook.react.bridge.Promise; 4 | import com.facebook.react.bridge.ReactApplicationContext; 5 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 6 | import com.facebook.react.bridge.ReactMethod; 7 | 8 | import java.io.File; 9 | 10 | public class SaveViewModule extends ReactContextBaseJavaModule { 11 | private static final String NAME = "RNSaveView"; 12 | private ReactApplicationContext context; 13 | 14 | SaveViewModule(ReactApplicationContext reactContext) { 15 | super(reactContext); 16 | context = reactContext; 17 | } 18 | 19 | @Override 20 | public String getName() { 21 | return NAME; 22 | } 23 | 24 | @ReactMethod 25 | public void saveToPNG(final int reactTag, final String filePath, final Promise promise) { 26 | SaveView saveView = new SaveView(context); 27 | 28 | File file = new File(filePath); 29 | if (file.exists()) { 30 | file.delete(); 31 | file = new File(filePath); 32 | } 33 | 34 | saveView.saveToPNG(reactTag, file, new SaveView.OnComplete() { 35 | @Override 36 | public void onSuccess() { 37 | promise.resolve(true); 38 | } 39 | 40 | @Override 41 | public void onFail(Exception exception) { 42 | promise.reject(exception); 43 | } 44 | }); 45 | } 46 | 47 | @ReactMethod 48 | public void saveToPNGBase64(final int reactTag, final Promise promise) { 49 | SaveView saveView = new SaveView(context); 50 | 51 | saveView.saveToPNGBase64(reactTag, new SaveView.OnCompleteBase64() { 52 | @Override 53 | public void onSuccess(String base64) { 54 | promise.resolve(base64); 55 | } 56 | 57 | @Override 58 | public void onFail(Exception exception) { 59 | promise.reject(exception); 60 | } 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /android/src/main/java/com/rumax/reactnative/saveview/SaveViewPackage.java: -------------------------------------------------------------------------------- 1 | package com.rumax.reactnative.saveview; 2 | 3 | import com.facebook.react.ReactPackage; 4 | import com.facebook.react.bridge.NativeModule; 5 | import com.facebook.react.bridge.ReactApplicationContext; 6 | import com.facebook.react.uimanager.ViewManager; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public class SaveViewPackage implements ReactPackage { 13 | @Override 14 | public List createNativeModules(ReactApplicationContext reactContext) { 15 | List modules = new ArrayList<>(); 16 | modules.add(new SaveViewModule(reactContext)); 17 | return modules; 18 | } 19 | 20 | @Override 21 | @SuppressWarnings("rawtypes") 22 | public List createViewManagers(ReactApplicationContext reactContext) { 23 | return Collections.emptyList(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ios/RNSaveView/RNSaveView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 538815A22200AA4C00F84E15 /* RNSaveViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5388159E2200AA4C00F84E15 /* RNSaveViewManager.m */; }; 11 | 538815A32200AA4C00F84E15 /* RNSaveViewConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 5388159F2200AA4C00F84E15 /* RNSaveViewConstants.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | 538815902200AA1900F84E15 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = "include/$(PRODUCT_NAME)"; 19 | dstSubfolderSpec = 16; 20 | files = ( 21 | ); 22 | runOnlyForDeploymentPostprocessing = 0; 23 | }; 24 | /* End PBXCopyFilesBuildPhase section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 538815922200AA1900F84E15 /* libRNSaveView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNSaveView.a; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 5388159E2200AA4C00F84E15 /* RNSaveViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSaveViewManager.m; sourceTree = ""; }; 29 | 5388159F2200AA4C00F84E15 /* RNSaveViewConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSaveViewConstants.m; sourceTree = ""; }; 30 | 538815A02200AA4C00F84E15 /* RNSaveViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSaveViewManager.h; sourceTree = ""; }; 31 | 538815A12200AA4C00F84E15 /* RNSaveViewConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSaveViewConstants.h; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | 5388158F2200AA1900F84E15 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 538815892200AA1900F84E15 = { 46 | isa = PBXGroup; 47 | children = ( 48 | 538815942200AA1900F84E15 /* RNSaveView */, 49 | 538815932200AA1900F84E15 /* Products */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | 538815932200AA1900F84E15 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 538815922200AA1900F84E15 /* libRNSaveView.a */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | 538815942200AA1900F84E15 /* RNSaveView */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 538815A12200AA4C00F84E15 /* RNSaveViewConstants.h */, 65 | 5388159F2200AA4C00F84E15 /* RNSaveViewConstants.m */, 66 | 538815A02200AA4C00F84E15 /* RNSaveViewManager.h */, 67 | 5388159E2200AA4C00F84E15 /* RNSaveViewManager.m */, 68 | ); 69 | path = RNSaveView; 70 | sourceTree = ""; 71 | }; 72 | /* End PBXGroup section */ 73 | 74 | /* Begin PBXNativeTarget section */ 75 | 538815912200AA1900F84E15 /* RNSaveView */ = { 76 | isa = PBXNativeTarget; 77 | buildConfigurationList = 5388159B2200AA1900F84E15 /* Build configuration list for PBXNativeTarget "RNSaveView" */; 78 | buildPhases = ( 79 | 5388158E2200AA1900F84E15 /* Sources */, 80 | 5388158F2200AA1900F84E15 /* Frameworks */, 81 | 538815902200AA1900F84E15 /* CopyFiles */, 82 | ); 83 | buildRules = ( 84 | ); 85 | dependencies = ( 86 | ); 87 | name = RNSaveView; 88 | productName = RNSaveView; 89 | productReference = 538815922200AA1900F84E15 /* libRNSaveView.a */; 90 | productType = "com.apple.product-type.library.static"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | 5388158A2200AA1900F84E15 /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | LastUpgradeCheck = 1010; 99 | ORGANIZATIONNAME = "sander frenken"; 100 | TargetAttributes = { 101 | 538815912200AA1900F84E15 = { 102 | CreatedOnToolsVersion = 10.1; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = 5388158D2200AA1900F84E15 /* Build configuration list for PBXProject "RNSaveView" */; 107 | compatibilityVersion = "Xcode 9.3"; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | ); 113 | mainGroup = 538815892200AA1900F84E15; 114 | productRefGroup = 538815932200AA1900F84E15 /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | 538815912200AA1900F84E15 /* RNSaveView */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXSourcesBuildPhase section */ 124 | 5388158E2200AA1900F84E15 /* Sources */ = { 125 | isa = PBXSourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | 538815A22200AA4C00F84E15 /* RNSaveViewManager.m in Sources */, 129 | 538815A32200AA4C00F84E15 /* RNSaveViewConstants.m in Sources */, 130 | ); 131 | runOnlyForDeploymentPostprocessing = 0; 132 | }; 133 | /* End PBXSourcesBuildPhase section */ 134 | 135 | /* Begin XCBuildConfiguration section */ 136 | 538815992200AA1900F84E15 /* Debug */ = { 137 | isa = XCBuildConfiguration; 138 | buildSettings = { 139 | ALWAYS_SEARCH_USER_PATHS = NO; 140 | CLANG_ANALYZER_NONNULL = YES; 141 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 142 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 143 | CLANG_CXX_LIBRARY = "libc++"; 144 | CLANG_ENABLE_MODULES = YES; 145 | CLANG_ENABLE_OBJC_ARC = YES; 146 | CLANG_ENABLE_OBJC_WEAK = YES; 147 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 148 | CLANG_WARN_BOOL_CONVERSION = YES; 149 | CLANG_WARN_COMMA = YES; 150 | CLANG_WARN_CONSTANT_CONVERSION = YES; 151 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 152 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 153 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 154 | CLANG_WARN_EMPTY_BODY = YES; 155 | CLANG_WARN_ENUM_CONVERSION = YES; 156 | CLANG_WARN_INFINITE_RECURSION = YES; 157 | CLANG_WARN_INT_CONVERSION = YES; 158 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 159 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 160 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 161 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 162 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 163 | CLANG_WARN_STRICT_PROTOTYPES = YES; 164 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 165 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 166 | CLANG_WARN_UNREACHABLE_CODE = YES; 167 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 168 | CODE_SIGN_IDENTITY = "iPhone Developer"; 169 | COPY_PHASE_STRIP = NO; 170 | DEBUG_INFORMATION_FORMAT = dwarf; 171 | ENABLE_STRICT_OBJC_MSGSEND = YES; 172 | ENABLE_TESTABILITY = YES; 173 | GCC_C_LANGUAGE_STANDARD = gnu11; 174 | GCC_DYNAMIC_NO_PIC = NO; 175 | GCC_NO_COMMON_BLOCKS = YES; 176 | GCC_OPTIMIZATION_LEVEL = 0; 177 | GCC_PREPROCESSOR_DEFINITIONS = ( 178 | "DEBUG=1", 179 | "$(inherited)", 180 | ); 181 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 182 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 183 | GCC_WARN_UNDECLARED_SELECTOR = YES; 184 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 185 | GCC_WARN_UNUSED_FUNCTION = YES; 186 | GCC_WARN_UNUSED_VARIABLE = YES; 187 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 188 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 189 | MTL_FAST_MATH = YES; 190 | ONLY_ACTIVE_ARCH = YES; 191 | SDKROOT = iphoneos; 192 | }; 193 | name = Debug; 194 | }; 195 | 5388159A2200AA1900F84E15 /* Release */ = { 196 | isa = XCBuildConfiguration; 197 | buildSettings = { 198 | ALWAYS_SEARCH_USER_PATHS = NO; 199 | CLANG_ANALYZER_NONNULL = YES; 200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_ENABLE_OBJC_WEAK = YES; 206 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 207 | CLANG_WARN_BOOL_CONVERSION = YES; 208 | CLANG_WARN_COMMA = YES; 209 | CLANG_WARN_CONSTANT_CONVERSION = YES; 210 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 211 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 212 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 213 | CLANG_WARN_EMPTY_BODY = YES; 214 | CLANG_WARN_ENUM_CONVERSION = YES; 215 | CLANG_WARN_INFINITE_RECURSION = YES; 216 | CLANG_WARN_INT_CONVERSION = YES; 217 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 219 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 220 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 221 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 222 | CLANG_WARN_STRICT_PROTOTYPES = YES; 223 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 224 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 225 | CLANG_WARN_UNREACHABLE_CODE = YES; 226 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 227 | CODE_SIGN_IDENTITY = "iPhone Developer"; 228 | COPY_PHASE_STRIP = NO; 229 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 230 | ENABLE_NS_ASSERTIONS = NO; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | GCC_C_LANGUAGE_STANDARD = gnu11; 233 | GCC_NO_COMMON_BLOCKS = YES; 234 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 235 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 236 | GCC_WARN_UNDECLARED_SELECTOR = YES; 237 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 238 | GCC_WARN_UNUSED_FUNCTION = YES; 239 | GCC_WARN_UNUSED_VARIABLE = YES; 240 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 241 | MTL_ENABLE_DEBUG_INFO = NO; 242 | MTL_FAST_MATH = YES; 243 | SDKROOT = iphoneos; 244 | VALIDATE_PRODUCT = YES; 245 | }; 246 | name = Release; 247 | }; 248 | 5388159C2200AA1900F84E15 /* Debug */ = { 249 | isa = XCBuildConfiguration; 250 | buildSettings = { 251 | CODE_SIGN_STYLE = Automatic; 252 | OTHER_LDFLAGS = "-ObjC"; 253 | PRODUCT_NAME = "$(TARGET_NAME)"; 254 | SKIP_INSTALL = YES; 255 | TARGETED_DEVICE_FAMILY = "1,2"; 256 | }; 257 | name = Debug; 258 | }; 259 | 5388159D2200AA1900F84E15 /* Release */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | CODE_SIGN_STYLE = Automatic; 263 | OTHER_LDFLAGS = "-ObjC"; 264 | PRODUCT_NAME = "$(TARGET_NAME)"; 265 | SKIP_INSTALL = YES; 266 | TARGETED_DEVICE_FAMILY = "1,2"; 267 | }; 268 | name = Release; 269 | }; 270 | /* End XCBuildConfiguration section */ 271 | 272 | /* Begin XCConfigurationList section */ 273 | 5388158D2200AA1900F84E15 /* Build configuration list for PBXProject "RNSaveView" */ = { 274 | isa = XCConfigurationList; 275 | buildConfigurations = ( 276 | 538815992200AA1900F84E15 /* Debug */, 277 | 5388159A2200AA1900F84E15 /* Release */, 278 | ); 279 | defaultConfigurationIsVisible = 0; 280 | defaultConfigurationName = Release; 281 | }; 282 | 5388159B2200AA1900F84E15 /* Build configuration list for PBXNativeTarget "RNSaveView" */ = { 283 | isa = XCConfigurationList; 284 | buildConfigurations = ( 285 | 5388159C2200AA1900F84E15 /* Debug */, 286 | 5388159D2200AA1900F84E15 /* Release */, 287 | ); 288 | defaultConfigurationIsVisible = 0; 289 | defaultConfigurationName = Release; 290 | }; 291 | /* End XCConfigurationList section */ 292 | }; 293 | rootObject = 5388158A2200AA1900F84E15 /* Project object */; 294 | } 295 | -------------------------------------------------------------------------------- /ios/RNSaveView/RNSaveView/RNSaveViewConstants.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | extern NSString * const RNSV_ERROR_INVALID_REACT_TAG; 4 | extern NSString * const RNSV_ERROR_INVALID_BASE_64; 5 | extern NSString * const RNSV_ERROR_MESSAGE_INVALID_BASE_64; 6 | -------------------------------------------------------------------------------- /ios/RNSaveView/RNSaveView/RNSaveViewConstants.m: -------------------------------------------------------------------------------- 1 | #import "RNSaveViewConstants.h" 2 | 3 | NSString *const RNSV_ERROR_INVALID_REACT_TAG = @"Invalid react tag"; 4 | NSString *const RNSV_ERROR_INVALID_BASE_64 = @"Invalid base64"; 5 | NSString *const RNSV_ERROR_MESSAGE_INVALID_BASE_64 = @"Image could not be converted to base64 representation"; 6 | -------------------------------------------------------------------------------- /ios/RNSaveView/RNSaveView/RNSaveViewManager.h: -------------------------------------------------------------------------------- 1 | #import "RNSaveViewConstants.h" 2 | #import 3 | #import 4 | #import 5 | 6 | @interface RNSaveViewManager : RCTViewManager 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /ios/RNSaveView/RNSaveView/RNSaveViewManager.m: -------------------------------------------------------------------------------- 1 | #import "RNSaveViewManager.h" 2 | 3 | @implementation RNSaveViewManager 4 | 5 | RCT_EXPORT_MODULE(RNSaveView) 6 | 7 | RCT_EXPORT_METHOD(saveToPNGBase64: (nonnull NSNumber *)reactTag resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) { 8 | dispatch_async(dispatch_get_main_queue(), ^{ 9 | UIImage *snapshot = [self snapshotReactView: reactTag]; 10 | if (snapshot != nil) { 11 | NSString* base64Image = [self imageToBase64: snapshot]; 12 | if (base64Image != nil) { 13 | resolve(base64Image); 14 | } else { 15 | reject(RNSV_ERROR_INVALID_BASE_64, RNSV_ERROR_MESSAGE_INVALID_BASE_64, nil); 16 | } 17 | } else { 18 | reject(RNSV_ERROR_INVALID_REACT_TAG, [NSString stringWithFormat: @"ReactTag passed: %@", reactTag], nil); 19 | } 20 | }); 21 | } 22 | 23 | RCT_EXPORT_METHOD(saveToPNG: (nonnull NSNumber *)reactTag filePath: (nonnull NSString *)filePath resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) { 24 | dispatch_async(dispatch_get_main_queue(), ^{ 25 | UIImage *snapshot = [self snapshotReactView: reactTag]; 26 | if (snapshot != nil) { 27 | [self saveImage: snapshot atFilepath: filePath]; 28 | resolve(nil); 29 | } else { 30 | reject(RNSV_ERROR_INVALID_REACT_TAG, [NSString stringWithFormat: @"ReactTag passed: %@", reactTag], nil); 31 | } 32 | }); 33 | } 34 | 35 | - (instancetype)init { 36 | self = [super init]; 37 | return self; 38 | } 39 | 40 | + (BOOL)requiresMainQueueSetup { 41 | return YES; 42 | } 43 | 44 | - (UIImage *)snapshotReactView: (nonnull NSNumber *)reactTag { 45 | UIView *view = (UIView *)[self.bridge.uiManager viewForReactTag: reactTag]; 46 | 47 | UIImage *snapshot; 48 | if ([view isKindOfClass: [RCTScrollView class]]) { 49 | RCTScrollView* rctScrollView = (RCTScrollView *)view; 50 | snapshot = [self snapshotScrollView: rctScrollView.scrollView]; 51 | } else { 52 | snapshot = [self snapshotView: view withSize: view.frame.size]; 53 | } 54 | 55 | return snapshot; 56 | } 57 | 58 | - (UIImage *)snapshotScrollView: (UIScrollView *)scrollView { 59 | // Store the original frame of the scrollview to restore later 60 | CGRect originalFrame = scrollView.frame; 61 | 62 | // Set the frame to it's contentSize 63 | scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); 64 | 65 | UIImage *image = [self snapshotView: scrollView withSize: scrollView.contentSize]; 66 | 67 | // Return to the original frame of the scrollview 68 | scrollView.frame = originalFrame; 69 | 70 | return image; 71 | } 72 | 73 | - (UIImage *)snapshotView: (UIView *)view withSize: (CGSize)size { 74 | UIGraphicsBeginImageContextWithOptions(size, NO, 0); 75 | [view drawViewHierarchyInRect: CGRectMake(0, 0, size.width, size.height) afterScreenUpdates: YES]; 76 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 77 | UIGraphicsEndImageContext(); 78 | 79 | return image; 80 | } 81 | 82 | - (void) saveImage: (UIImage *)image atFilepath: (NSString *)filePath { 83 | NSFileManager *fileManager = [NSFileManager defaultManager]; 84 | NSData *imageData = UIImageJPEGRepresentation(image, 1.0); 85 | [fileManager createFileAtPath: filePath contents: imageData attributes: nil]; 86 | } 87 | 88 | - (NSString *)imageToBase64: (UIImage *)image { 89 | NSData *imageData = UIImagePNGRepresentation(image); 90 | return [imageData base64EncodedStringWithOptions: 0]; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-save-view", 3 | "version": "0.2.3", 4 | "description": "React native save View implementation", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "react-native react-component view android ios screenshot viewshot" 11 | ], 12 | "author": "Maksym Rusynyk ", 13 | "license": "MIT", 14 | "peerDependencies": { 15 | "react-native": "0.*" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+ssh://git@github.com/rumax/react-native-SaveView.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/rumax/react-native-SaveView/issues" 23 | }, 24 | "homepage": "https://github.com/rumax/react-native-SaveView#readme" 25 | } 26 | -------------------------------------------------------------------------------- /react-native-save-view.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = package['name'] 7 | s.version = package['version'] 8 | s.summary = package['description'] 9 | s.license = package['license'] 10 | 11 | s.authors = package['author'] 12 | s.homepage = package['homepage'] 13 | s.platform = :ios, "9.0" 14 | 15 | s.source = { :git => "https://github.com/rumax/react-native-SaveView.git", :tag => "v#{s.version}" } 16 | s.source_files = "ios/**/*.{h,m}" 17 | 18 | s.dependency 'React' 19 | end 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { NativeModules, findNodeHandle } from 'react-native'; 3 | 4 | const { RNSaveView } = NativeModules; 5 | 6 | const getReactTag = (view: ?React$ElementRef<*>) => { 7 | if (!view) { 8 | throw new Error('View has to be defined'); 9 | } 10 | 11 | const reactTag = findNodeHandle(view); 12 | 13 | if (!reactTag) { 14 | throw new Error('Invalid view provided, cannot get reactTag'); 15 | } 16 | 17 | return reactTag; 18 | } 19 | 20 | export default { 21 | saveToPNG: async (view: ?React$ElementRef<*>, path: string) => { 22 | await RNSaveView.saveToPNG(getReactTag(view), path); 23 | }, 24 | saveToPNGBase64: async (view: ?React$ElementRef<*>) => { 25 | const base64 = await RNSaveView.saveToPNGBase64(getReactTag(view)); 26 | return base64; 27 | }, 28 | }; 29 | --------------------------------------------------------------------------------