diff --git a/README.md b/README.md index f58de457..af3d8ff4 100644 --- a/README.md +++ b/README.md @@ -66,17 +66,18 @@ takeSnapshot(viewRef, { path: PictureDir+"/foo.png" }) Model tested: iPhone 6 (iOS), Nexus 5 (Android). -| System | iOS | Android | -|--------------------|--------------------|-------------------| -| View,Text,Image,.. | YES | YES | -| WebView | YES | YES1 | -| gl-react v2 | YES | NO2 | -| react-native-video | NO | NO | -| react-native-maps | YES | [NO](https://github.com/gre/react-native-view-shot/issues/36) | +| System | iOS | Android | Windows | +|--------------------|--------------------|-------------------|-------------------| +| View,Text,Image,.. | YES | YES | YES | +| WebView | YES | YES1 | YES | +| gl-react v2 | YES | NO2 | NO3 | +| react-native-video | NO | NO | NO +| react-native-maps | YES | [NO](https://github.com/gre/react-native-view-shot/issues/36) | NO3 > 1. Only supported by wrapping a `` parent and snapshotting it. 2. It returns an empty image (not a failure Promise). +3. Component itself lacks platform support. ## Caveats @@ -128,7 +129,10 @@ react-native link react-native-view-shot #### Windows -Stay tuned, https://github.com/gre/react-native-view-shot/pull/45 will be merged soon! +1. In Visual Studio, in the solution explorer, right click on your solution then select `Add` ➜ `ExisitingProject` +2. Go to `node_modules` ➜ `react-native-view-shot` and add `RNViewShot.csproj` (UWP) or optionally `RNViewShot.Net46.csproj` (WPF) +3. In Visual Studio, in the solution explorer, right click on your Application project then select `Add` ➜ `Reference` +4. Under the projects tab select `RNViewShot` (UWP) or `RNViewShot.Net46` (WPF) ## Thanks diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..c98b6e08 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 00000000..ffcdc432 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,281 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Telerik's JustMock configuration file +*.jmconfig \ No newline at end of file diff --git a/windows/RNViewShot.Net46/Properties/AssemblyInfo.cs b/windows/RNViewShot.Net46/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..95962a00 --- /dev/null +++ b/windows/RNViewShot.Net46/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RNViewShot.Net46")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RNViewShot.Net46")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1dd45d00-ddf4-43b4-ab28-f98e6dc325b5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/windows/RNViewShot.Net46/RNViewShot.Net46.csproj b/windows/RNViewShot.Net46/RNViewShot.Net46.csproj new file mode 100644 index 00000000..71326c8d --- /dev/null +++ b/windows/RNViewShot.Net46/RNViewShot.Net46.csproj @@ -0,0 +1,97 @@ + + + + + Debug + AnyCPU + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5} + Library + Properties + RNViewShot.Net46 + RNViewShot.Net46 + v4.6 + 512 + + + true + bin\x86\Debug\ + DEBUG;TRACE + full + x86 + prompt + MinimumRecommendedRules.ruleset + + + bin\x86\Release\ + TRACE + true + pdbonly + x86 + prompt + MinimumRecommendedRules.ruleset + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + ..\..\..\..\..\..\..\..\..\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.UniversalApiContract\2.0.0.0\Windows.Foundation.UniversalApiContract.winmd + + + + + + + + + + + + {22cbff9c-fe36-43e8-a246-266c7635e662} + ReactNative.Net46 + + + + + + + + + \ No newline at end of file diff --git a/windows/RNViewShot.Net46/RNViewShotModule.cs b/windows/RNViewShot.Net46/RNViewShotModule.cs new file mode 100644 index 00000000..f55ce82c --- /dev/null +++ b/windows/RNViewShot.Net46/RNViewShotModule.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.UIManager; +using System; +using System.IO; +using System.Collections.Generic; + +namespace RNViewShot +{ + /// + /// A module that allows JS to share data. + /// + class RNViewShotModule : ReactContextNativeModuleBase + { + private const string ErrorUnableToSnapshot = "E_UNABLE_TO_SNAPSHOT"; + private readonly ReactContext _reactContext; + + /// + /// Instantiates the . + /// + public RNViewShotModule(ReactContext reactContext) : base(reactContext) + { + this._reactContext = reactContext; + } + + /// + /// The name of the native module. + /// + public override string Name + { + get + { + return "RNViewShot"; + } + } + + [ReactMethod] + public void takeSnapshot(int tag, JObject options, IPromise promise) + { + string format = options["format"] != null ? options.Value("format") : "png"; + double quality = options["quality"] != null ? options.Value("quality") : 1.0; + int? width = options["width"] != null ? options.Value("width") : null; + int? height = options["height"] != null ? options.Value("height") : null; + string result = options["result"] != null ? options.Value("result") : "file"; + string path = options["path"] != null ? options.Value("path") : null; + + if (format != "png" && format != "jpg" && format != "jpeg") + { + promise.Reject(ViewShot.ErrorUnableToSnapshot, "Unsupported image format: " + format + ". Try one of: png | jpg | jpeg"); + return; + } + + UIManagerModule uiManager = this._reactContext.GetNativeModule(); + var viewShot = new ViewShot(tag, format, quality, width, height, path, result, promise); + uiManager.AddUIBlock(viewShot); + } + } +} diff --git a/windows/RNViewShot.Net46/RNViewShotPackage.cs b/windows/RNViewShot.Net46/RNViewShotPackage.cs new file mode 100644 index 00000000..97d676a0 --- /dev/null +++ b/windows/RNViewShot.Net46/RNViewShotPackage.cs @@ -0,0 +1,53 @@ +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using System; +using System.Collections.Generic; + +namespace RNViewShot +{ + /// + /// Package defining core framework modules (e.g., ). + /// It should be used for modules that require special integration with + /// other framework parts (e.g., with the list of packages to load view + /// managers from). + /// + public class RNViewShotPackage : IReactPackage + { + /// + /// Creates the list of native modules to register with the react + /// instance. + /// + /// The react application context. + /// The list of native modules. + public IReadOnlyList CreateNativeModules(ReactContext reactContext) + { + return new List + { + new RNViewShotModule(reactContext), + }; + } + + /// + /// Creates the list of JavaScript modules to register with the + /// react instance. + /// + /// The list of JavaScript modules. + public IReadOnlyList CreateJavaScriptModulesConfig() + { + return new List(0); + } + + /// + /// Creates the list of view managers that should be registered with + /// the . + /// + /// The react application context. + /// The list of view managers. + public IReadOnlyList CreateViewManagers( + ReactContext reactContext) + { + return new List(0); + } + } +} diff --git a/windows/RNViewShot.Net46/ViewShot.cs b/windows/RNViewShot.Net46/ViewShot.cs new file mode 100644 index 00000000..05a59ecb --- /dev/null +++ b/windows/RNViewShot.Net46/ViewShot.cs @@ -0,0 +1,156 @@ +using ReactNative.Bridge; +using ReactNative.UIManager; +using System; +using System.Diagnostics; +using System.IO; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace RNViewShot +{ + public class ViewShot : IUIBlock + { + public const string ErrorUnableToSnapshot = "E_UNABLE_TO_SNAPSHOT"; + private int tag; + private string extension; + private double quality; + private int? width; + private int? height; + private string path; + private string result; + private IPromise promise; + + public ViewShot( + int tag, + string extension, + double quality, + int? width, + int? height, + string path, + string result, + IPromise promise) + { + this.tag = tag; + this.extension = extension; + this.quality = quality; + this.width = width; + this.height = height; + this.path = path; + this.result = result; + this.promise = promise; + } + + public void Execute(NativeViewHierarchyManager nvhm) + { + var view = nvhm.ResolveView(tag) as FrameworkElement; + if (view == null) + { + promise.Reject(ErrorUnableToSnapshot, "No view found with reactTag: " + tag); + return; + } + + try + { + BitmapEncoder image = CaptureView(view); + + if ("file" == result) + { + string filePath = GetFilePath(); + Stream stream = File.Create(filePath); + image.Save(stream); + promise.Resolve(filePath); + stream.Close(); + } + else if ("base64" == result) + { + MemoryStream stream = new MemoryStream(); + image.Save(stream); + byte[] imageBytes = stream.ToArray(); + string data = Convert.ToBase64String(imageBytes); + promise.Resolve(data); + stream.Close(); + } + else if ("data-uri" == result) + { + MemoryStream stream = new MemoryStream(); + image.Save(stream); + byte[] imageBytes = stream.ToArray(); + string data = Convert.ToBase64String(imageBytes); + data = "data:image/" + extension + ";base64," + data; + promise.Resolve(data); + stream.Close(); + } + else + { + promise.Reject(ErrorUnableToSnapshot, "Unsupported result: " + result + ". Try one of: file | base64 | data-uri"); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + promise.Reject(ErrorUnableToSnapshot, "Failed to capture view snapshot"); + } + } + + private BitmapEncoder CaptureView(FrameworkElement view) + { + int w = (int)view.ActualWidth; + int h = (int)view.ActualHeight; + + if (w <= 0 || h <= 0) + { + throw new InvalidOperationException("Impossible to snapshot the view: view is invalid"); + } + + RenderTargetBitmap targetBitmap = new RenderTargetBitmap(w, h, 96, 96, PixelFormats.Default); + targetBitmap.Render(view); + + BitmapSource bitmap; + if (width != null && height != null && (width != w || height != h)) + { + double scaleX = (double)width / targetBitmap.PixelWidth; + double scaleY = (double)height / targetBitmap.PixelHeight; + bitmap = new TransformedBitmap(targetBitmap, new ScaleTransform(scaleX, scaleY)); + } + else + { + bitmap = targetBitmap; + } + + if (bitmap == null) + { + throw new InvalidOperationException("Impossible to snapshot the view"); + } + + if (extension == "png") + { + PngBitmapEncoder image = new PngBitmapEncoder(); + image.Frames.Add(BitmapFrame.Create(bitmap)); + return image; + } + else + { + JpegBitmapEncoder image = new JpegBitmapEncoder(); + image.QualityLevel = (int)(100.0 * quality); + image.Frames.Add(BitmapFrame.Create(bitmap)); + return image; + } + } + + private string GetFilePath() + { + if (string.IsNullOrEmpty(path)) + { + string tmpFilePath = Path.GetTempPath(); + string fileName = Guid.NewGuid().ToString(); + fileName = Path.ChangeExtension(fileName, extension); + return Path.Combine(tmpFilePath, fileName); + } + else + { + return path; + } + } + } +} diff --git a/windows/RNViewShot.Net46/app.config b/windows/RNViewShot.Net46/app.config new file mode 100644 index 00000000..cacd4cd7 --- /dev/null +++ b/windows/RNViewShot.Net46/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/windows/RNViewShot.Net46/packages.config b/windows/RNViewShot.Net46/packages.config new file mode 100644 index 00000000..90aba300 --- /dev/null +++ b/windows/RNViewShot.Net46/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/windows/RNViewShot.sln b/windows/RNViewShot.sln new file mode 100644 index 00000000..9e835dd4 --- /dev/null +++ b/windows/RNViewShot.sln @@ -0,0 +1,126 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RNViewShot", "RNViewShot\RNViewShot.csproj", "{391A35D0-FEBA-11E6-9171-BD5177E581B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNative", "..\node_modules\react-native-windows\ReactWindows\ReactNative\ReactNative.csproj", "{C7673AD5-E3AA-468C-A5FD-FA38154E205C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ChakraBridge", "..\node_modules\react-native-windows\ReactWindows\ChakraBridge\ChakraBridge.vcxproj", "{4B72C796-16D5-4E3A-81C0-3E36F531E578}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactNative.Net46", "..\node_modules\react-native-windows\ReactWindows\ReactNative.Net46\ReactNative.Net46.csproj", "{22CBFF9C-FE36-43E8-A246-266C7635E662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RNViewShot.Net46", "RNViewShot.Net46\RNViewShot.Net46.csproj", "{1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}" +EndProject +Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.projitems*{22cbff9c-fe36-43e8-a246-266c7635e662}*SharedItemsImports = 4 + ..\node_modules\react-native-windows\ReactWindows\ReactNative.Shared\ReactNative.Shared.projitems*{c7673ad5-e3aa-468c-a5fd-fa38154e205c}*SharedItemsImports = 4 + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Development|ARM = Development|ARM + Development|x64 = Development|x64 + Development|x86 = Development|x86 + Release|ARM = Release|ARM + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Debug|ARM.ActiveCfg = Debug|ARM + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Debug|ARM.Build.0 = Debug|ARM + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Debug|x64.ActiveCfg = Debug|x64 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Debug|x64.Build.0 = Debug|x64 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Debug|x86.ActiveCfg = Debug|x86 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Debug|x86.Build.0 = Debug|x86 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Development|ARM.ActiveCfg = Development|ARM + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Development|ARM.Build.0 = Development|ARM + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Development|x64.ActiveCfg = Development|x64 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Development|x64.Build.0 = Development|x64 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Development|x86.ActiveCfg = Development|x86 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Development|x86.Build.0 = Development|x86 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Release|ARM.ActiveCfg = Release|ARM + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Release|ARM.Build.0 = Release|ARM + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Release|x64.ActiveCfg = Release|x64 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Release|x64.Build.0 = Release|x64 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Release|x86.ActiveCfg = Release|x86 + {391A35D0-FEBA-11E6-9171-BD5177E581B7}.Release|x86.Build.0 = Release|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|ARM.ActiveCfg = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|ARM.Build.0 = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x64.ActiveCfg = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x64.Build.0 = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x86.ActiveCfg = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Debug|x86.Build.0 = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|ARM.ActiveCfg = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|ARM.Build.0 = Debug|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x64.ActiveCfg = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x64.Build.0 = Debug|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x86.ActiveCfg = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Development|x86.Build.0 = Debug|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|ARM.ActiveCfg = Release|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|ARM.Build.0 = Release|ARM + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x64.ActiveCfg = Release|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x64.Build.0 = Release|x64 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x86.ActiveCfg = Release|x86 + {C7673AD5-E3AA-468C-A5FD-FA38154E205C}.Release|x86.Build.0 = Release|x86 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|ARM.ActiveCfg = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|ARM.Build.0 = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x64.ActiveCfg = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x64.Build.0 = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x86.ActiveCfg = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Debug|x86.Build.0 = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|ARM.ActiveCfg = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|ARM.Build.0 = Debug|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x64.ActiveCfg = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x64.Build.0 = Debug|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x86.ActiveCfg = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Development|x86.Build.0 = Debug|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|ARM.ActiveCfg = Release|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|ARM.Build.0 = Release|ARM + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x64.ActiveCfg = Release|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x64.Build.0 = Release|x64 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x86.ActiveCfg = Release|Win32 + {4B72C796-16D5-4E3A-81C0-3E36F531E578}.Release|x86.Build.0 = Release|Win32 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|ARM.ActiveCfg = Debug|ARM + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|ARM.Build.0 = Debug|ARM + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x64.ActiveCfg = Debug|x64 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x64.Build.0 = Debug|x64 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x86.ActiveCfg = Debug|x86 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Debug|x86.Build.0 = Debug|x86 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Development|ARM.ActiveCfg = Debug|ARM + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Development|ARM.Build.0 = Debug|ARM + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Development|x64.ActiveCfg = Debug|x64 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Development|x64.Build.0 = Debug|x64 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Development|x86.ActiveCfg = Debug|x86 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Development|x86.Build.0 = Debug|x86 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|ARM.ActiveCfg = Release|ARM + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|ARM.Build.0 = Release|ARM + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x64.ActiveCfg = Release|x64 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x64.Build.0 = Release|x64 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x86.ActiveCfg = Release|x86 + {22CBFF9C-FE36-43E8-A246-266C7635E662}.Release|x86.Build.0 = Release|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Debug|ARM.ActiveCfg = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Debug|ARM.Build.0 = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Debug|x64.ActiveCfg = Debug|x64 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Debug|x64.Build.0 = Debug|x64 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Debug|x86.ActiveCfg = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Debug|x86.Build.0 = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Development|ARM.ActiveCfg = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Development|ARM.Build.0 = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Development|x64.ActiveCfg = Debug|x64 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Development|x64.Build.0 = Debug|x64 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Development|x86.ActiveCfg = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Development|x86.Build.0 = Debug|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Release|ARM.ActiveCfg = Release|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Release|ARM.Build.0 = Release|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Release|x64.ActiveCfg = Release|x64 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Release|x64.Build.0 = Release|x64 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Release|x86.ActiveCfg = Release|x86 + {1DD45D00-DDF4-43B4-AB28-F98E6DC325B5}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/windows/RNViewShot/Properties/AssemblyInfo.cs b/windows/RNViewShot/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..1a102deb --- /dev/null +++ b/windows/RNViewShot/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("RNViewShot")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RNViewShot")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] + \ No newline at end of file diff --git a/windows/RNViewShot/Properties/RNViewShot.rd.xml b/windows/RNViewShot/Properties/RNViewShot.rd.xml new file mode 100644 index 00000000..e7d6333c --- /dev/null +++ b/windows/RNViewShot/Properties/RNViewShot.rd.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/windows/RNViewShot/RNViewShot.csproj b/windows/RNViewShot/RNViewShot.csproj new file mode 100644 index 00000000..da4b0ea6 --- /dev/null +++ b/windows/RNViewShot/RNViewShot.csproj @@ -0,0 +1,184 @@ + + + + + Debug + AnyCPU + {391A35D0-FEBA-11E6-9171-BD5177E581B7} + Library + Properties + RNViewShot + RNViewShot + en-US + UAP + 10.0.10586.0 + 10.0.10240.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + $(SolutionDir)..\node_modules + + + ..\.. + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + + + + + + + + + + + + + + + {c7673ad5-e3aa-468c-a5fd-fa38154e205c} + ReactNative + + + + 14.0 + + + true + bin\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + true + full + AnyCPU + false + prompt + MinimumRecommendedRules.ruleset + + + true + bin\x86\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + true + full + x86 + false + prompt + MinimumRecommendedRules.ruleset + + + true + bin\ARM\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + true + full + ARM + false + prompt + MinimumRecommendedRules.ruleset + + + true + bin\x64\Development\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + true + full + x64 + false + prompt + MinimumRecommendedRules.ruleset + + + + \ No newline at end of file diff --git a/windows/RNViewShot/RNViewShotModule.cs b/windows/RNViewShot/RNViewShotModule.cs new file mode 100644 index 00000000..f55ce82c --- /dev/null +++ b/windows/RNViewShot/RNViewShotModule.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json.Linq; +using ReactNative.Bridge; +using ReactNative.UIManager; +using System; +using System.IO; +using System.Collections.Generic; + +namespace RNViewShot +{ + /// + /// A module that allows JS to share data. + /// + class RNViewShotModule : ReactContextNativeModuleBase + { + private const string ErrorUnableToSnapshot = "E_UNABLE_TO_SNAPSHOT"; + private readonly ReactContext _reactContext; + + /// + /// Instantiates the . + /// + public RNViewShotModule(ReactContext reactContext) : base(reactContext) + { + this._reactContext = reactContext; + } + + /// + /// The name of the native module. + /// + public override string Name + { + get + { + return "RNViewShot"; + } + } + + [ReactMethod] + public void takeSnapshot(int tag, JObject options, IPromise promise) + { + string format = options["format"] != null ? options.Value("format") : "png"; + double quality = options["quality"] != null ? options.Value("quality") : 1.0; + int? width = options["width"] != null ? options.Value("width") : null; + int? height = options["height"] != null ? options.Value("height") : null; + string result = options["result"] != null ? options.Value("result") : "file"; + string path = options["path"] != null ? options.Value("path") : null; + + if (format != "png" && format != "jpg" && format != "jpeg") + { + promise.Reject(ViewShot.ErrorUnableToSnapshot, "Unsupported image format: " + format + ". Try one of: png | jpg | jpeg"); + return; + } + + UIManagerModule uiManager = this._reactContext.GetNativeModule(); + var viewShot = new ViewShot(tag, format, quality, width, height, path, result, promise); + uiManager.AddUIBlock(viewShot); + } + } +} diff --git a/windows/RNViewShot/RNViewShotPackage.cs b/windows/RNViewShot/RNViewShotPackage.cs new file mode 100644 index 00000000..97d676a0 --- /dev/null +++ b/windows/RNViewShot/RNViewShotPackage.cs @@ -0,0 +1,53 @@ +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using System; +using System.Collections.Generic; + +namespace RNViewShot +{ + /// + /// Package defining core framework modules (e.g., ). + /// It should be used for modules that require special integration with + /// other framework parts (e.g., with the list of packages to load view + /// managers from). + /// + public class RNViewShotPackage : IReactPackage + { + /// + /// Creates the list of native modules to register with the react + /// instance. + /// + /// The react application context. + /// The list of native modules. + public IReadOnlyList CreateNativeModules(ReactContext reactContext) + { + return new List + { + new RNViewShotModule(reactContext), + }; + } + + /// + /// Creates the list of JavaScript modules to register with the + /// react instance. + /// + /// The list of JavaScript modules. + public IReadOnlyList CreateJavaScriptModulesConfig() + { + return new List(0); + } + + /// + /// Creates the list of view managers that should be registered with + /// the . + /// + /// The react application context. + /// The list of view managers. + public IReadOnlyList CreateViewManagers( + ReactContext reactContext) + { + return new List(0); + } + } +} diff --git a/windows/RNViewShot/ViewShot.cs b/windows/RNViewShot/ViewShot.cs new file mode 100644 index 00000000..5b80fdc6 --- /dev/null +++ b/windows/RNViewShot/ViewShot.cs @@ -0,0 +1,170 @@ +using ReactNative.Bridge; +using ReactNative.UIManager; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Windows.Graphics.Display; +using Windows.Graphics.Imaging; +using Windows.Storage; +using Windows.Storage.Streams; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media.Imaging; + +namespace RNViewShot +{ + public class ViewShot : IUIBlock + { + public const string ErrorUnableToSnapshot = "E_UNABLE_TO_SNAPSHOT"; + private int tag; + private string extension; + private double quality; + private int? width; + private int? height; + private string path; + private string result; + private IPromise promise; + + public ViewShot( + int tag, + string extension, + double quality, + int? width, + int? height, + string path, + string result, + IPromise promise) + { + this.tag = tag; + this.extension = extension; + this.quality = quality; + this.width = width; + this.height = height; + this.path = path; + this.result = result; + this.promise = promise; + } + + public async void Execute(NativeViewHierarchyManager nvhm) + { + var view = nvhm.ResolveView(tag) as FrameworkElement; + if (view == null) + { + promise.Reject(ErrorUnableToSnapshot, "No view found with reactTag: " + tag); + return; + } + + try + { + if ("file" == result) + { + using (var ras = new InMemoryRandomAccessStream()) + { + await CaptureView(view, ras); + var file = await GetStorageFile(); + using (var fileStream = await file.OpenAsync(FileAccessMode.ReadWrite)) + { + await RandomAccessStream.CopyAndCloseAsync(ras.GetInputStreamAt(0), fileStream.GetOutputStreamAt(0)); + promise.Resolve(file.Path); + } + } + } + else if ("base64" == result) + { + using (var ras = new InMemoryRandomAccessStream()) + { + await CaptureView(view, ras); + var imageBytes = new byte[ras.Size]; + await ras.AsStream().ReadAsync(imageBytes, 0, imageBytes.Length); + string data = Convert.ToBase64String(imageBytes); + promise.Resolve(data); + } + } + else if ("data-uri" == result) + { + using (var ras = new InMemoryRandomAccessStream()) + { + await CaptureView(view, ras); + var imageBytes = new byte[ras.Size]; + await ras.AsStream().ReadAsync(imageBytes, 0, imageBytes.Length); + string data = Convert.ToBase64String(imageBytes); + data = "data:image/" + extension + ";base64," + data; + promise.Resolve(data); + } + } + else + { + promise.Reject(ErrorUnableToSnapshot, "Unsupported result: " + result + ". Try one of: file | base64 | data-uri"); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + promise.Reject(ErrorUnableToSnapshot, "Failed to capture view snapshot"); + } + } + + private async Task CaptureView(FrameworkElement view, IRandomAccessStream stream) + { + int w = (int)view.ActualWidth; + int h = (int)view.ActualHeight; + + if (w <= 0 || h <= 0) + { + throw new InvalidOperationException("Impossible to snapshot the view: view is invalid"); + } + + RenderTargetBitmap targetBitmap = new RenderTargetBitmap(); + await targetBitmap.RenderAsync(view, w, h); + + BitmapEncoder encoder; + if (extension != "png") + { + var propertySet = new BitmapPropertySet(); + var qualityValue = new BitmapTypedValue(quality, Windows.Foundation.PropertyType.Single); + propertySet.Add("ImageQuality", qualityValue); + encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream, propertySet); + } + else + { + encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream); + } + + var displayInformation = DisplayInformation.GetForCurrentView(); + var pixelBuffer = await targetBitmap.GetPixelsAsync(); + + encoder.SetPixelData( + BitmapPixelFormat.Bgra8, + BitmapAlphaMode.Ignore, + (uint)targetBitmap.PixelWidth, + (uint)targetBitmap.PixelHeight, + displayInformation.LogicalDpi, + displayInformation.LogicalDpi, + pixelBuffer.ToArray()); + + + if (width != null && height != null && (width != w || height != h)) + { + encoder.BitmapTransform.ScaledWidth = (uint)width; + encoder.BitmapTransform.ScaledWidth = (uint)height; + } + + if (encoder == null) + { + throw new InvalidOperationException("Impossible to snapshot the view"); + } + + await encoder.FlushAsync(); + + return encoder; + } + + private async Task GetStorageFile() + { + var storageFolder = ApplicationData.Current.LocalFolder; + var fileName = string.IsNullOrEmpty(path) ? path : Path.ChangeExtension(Guid.NewGuid().ToString(), extension); + return await storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting); + } + } +} diff --git a/windows/RNViewShot/project.json b/windows/RNViewShot/project.json new file mode 100644 index 00000000..f5c3d2ac --- /dev/null +++ b/windows/RNViewShot/project.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "Microsoft.NETCore.UniversalWindowsPlatform": "5.2.2", + "Newtonsoft.Json": "10.0.1-beta1" + }, + "frameworks": { + "uap10.0": {} + }, + "runtimes": { + "win10-arm": {}, + "win10-arm-aot": {}, + "win10-x86": {}, + "win10-x86-aot": {}, + "win10-x64": {}, + "win10-x64-aot": {} + } +} \ No newline at end of file