├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── Assets └── RunningUnderIIS.png ├── Changelog.md ├── HtmlToPdf.md ├── LICENSE.md ├── README.md ├── Samples ├── AspNetHtmlToPdfSample │ ├── AspNetHtmlToPdfSample.csproj │ ├── GeneratedPdf.pdf │ ├── HtmlSampleFile-SelfContained.html │ ├── HtmlSampleFileLonger-SelfContained.html │ ├── PdfController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── WebApplication1.http │ ├── WebApplication1.websurge │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── publish.cmd │ └── web.config ├── ConsoleHtmlToPdfSample │ ├── ConsoleHtmlToPdfSample.csproj │ ├── HtmlSampleFile-SelfContained.html │ ├── HtmlSampleFileLonger-SelfContained.html │ ├── Program.cs │ └── Properties │ │ └── launchsettings.json └── WpfSample │ ├── App.xaml │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── Assets │ └── MarkdownMonster_Icon_256.png │ ├── BasicInterop.xaml │ ├── BasicInterop.xaml.cs │ ├── EmojiWindow.xaml │ ├── EmojiWindow.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── PreviewThemes │ ├── EmojiViewer.js │ ├── Interop.html │ ├── Interop.js │ ├── Westwind │ │ ├── Theme.css │ │ └── Theme.html │ ├── _EmojiViewer.html │ └── scripts │ │ ├── jquery.min.js │ │ └── vue │ │ ├── vue.js │ │ └── vue.min.js │ ├── StringNavigationIssues.xaml │ ├── StringNavigationIssues.xaml.cs │ ├── WebContent │ ├── ExportedHtml.html │ └── LargeImage.jpg │ └── WpfSample.csproj ├── Westwind.WebView.Test ├── HtmlToPdf │ ├── HtmlSampleFile-SelfContained.html │ ├── HtmlSampleFileLonger-SelfContained.html │ ├── HtmlToPdfTests.cs │ └── PdfSampleFile.pdf └── Westwind.WebView.Test.csproj ├── Westwind.WebView.sln ├── Westwind.WebView ├── HtmlToPdf │ ├── CoreWebViewHeadlessHost.cs │ ├── Enums.cs │ ├── HtmlToPdfDefaults.cs │ ├── HtmlToPdfHost.cs │ ├── PdfPrintOutputModes.cs │ ├── PdfPrintResult.cs │ └── WebViewPrintSettings.cs ├── Utilities │ ├── AsyncUtils.cs │ ├── JsonSerializationUtils.cs │ ├── StreamExtensions.cs │ └── StringUtils.cs ├── Westwind.WebView.csproj ├── Wpf │ ├── BaseJavaScriptInterop.cs │ ├── CachedWebViewEnvironment.cs │ ├── WebView2Extensions.cs │ ├── WebViewHandler.cs │ ├── WebViewInitializationException.cs │ └── WebViewUtilities.cs └── publish-nuget.ps1 └── icon.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: RickStrahl 2 | custom: "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K677THUA2MJSE&source=url" 3 | custom: "https://store.west-wind.com/product/donation" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *Publish/ 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | Test/ 7 | UnitTestProject2/ 8 | EBWebView/ 9 | 10 | 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # DNX 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opendb 89 | *.opensdf 90 | *.sdf 91 | *.cachefile 92 | *.VC.db 93 | *.VC.VC.opendb 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | #*.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | *.snupkg 165 | # The packages folder can be ignored because of Package Restore 166 | **/packages/* 167 | # except build/, which is used as an MSBuild target. 168 | !**/packages/build/ 169 | # Uncomment if necessary however generally it will be regenerated when needed 170 | #!**/packages/repositories.config 171 | # NuGet v3's project.json files produces more ignoreable files 172 | *.nuget.props 173 | *.nuget.targets 174 | 175 | # Microsoft Azure Build Output 176 | csx/ 177 | *.build.csdef 178 | 179 | # Microsoft Azure Emulator 180 | ecf/ 181 | rcf/ 182 | 183 | # Windows Store app package directories and files 184 | AppPackages/ 185 | BundleArtifacts/ 186 | Package.StoreAssociation.xml 187 | _pkginfo.txt 188 | 189 | # Visual Studio cache files 190 | # files ending in .cache can be ignored 191 | *.[Cc]ache 192 | # but keep track of directories ending in .cache 193 | !*.[Cc]ache/ 194 | 195 | # Others 196 | ClientBin/ 197 | ~$* 198 | *~ 199 | *.dbmdl 200 | *.dbproj.schemaview 201 | *.jfm 202 | *.pfx 203 | *.publishsettings 204 | node_modules/ 205 | orleans.codegen.cs 206 | 207 | # Since there are multiple workflows, uncomment next line to ignore bower_components 208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 209 | #bower_components/ 210 | 211 | # RIA/Silverlight projects 212 | Generated_Code/ 213 | 214 | # Backup & report files from converting an old project file 215 | # to a newer Visual Studio version. Backup files are not needed, 216 | # because we have git ;-) 217 | _UpgradeReport_Files/ 218 | Backup*/ 219 | UpgradeLog*.XML 220 | UpgradeLog*.htm 221 | 222 | # SQL Server files 223 | *.mdf 224 | *.ldf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | 240 | # Visual Studio 6 build log 241 | *.plg 242 | 243 | # Visual Studio 6 workspace options file 244 | *.opt 245 | 246 | # Visual Studio LightSwitch build output 247 | **/*.HTMLClient/GeneratedArtifacts 248 | **/*.DesktopClient/GeneratedArtifacts 249 | **/*.DesktopClient/ModelManifest.xml 250 | **/*.Server/GeneratedArtifacts 251 | **/*.Server/ModelManifest.xml 252 | _Pvt_Extensions 253 | 254 | # Paket dependency manager 255 | .paket/paket.exe 256 | paket-files/ 257 | 258 | # FAKE - F# Make 259 | .fake/ 260 | 261 | # JetBrains Rider 262 | .idea/ 263 | *.sln.iml 264 | 265 | # CodeRush 266 | .cr/ 267 | 268 | # Python Tools for Visual Studio (PTVS) 269 | __pycache__/ 270 | *.pyc 271 | *.saved.bak 272 | -------------------------------------------------------------------------------- /Assets/RunningUnderIIS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickStrahl/Westwind.WebView/cbf7323a89ba99a91ce1a395f80dcf29a6899d12/Assets/RunningUnderIIS.png -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Westwind.WebView Changelog 2 | 3 | 4 | ### 0.2 5 | 6 | * **Merge Westwind.WebView.HtmlToPdf into Library** 7 | Moved the HtmlToPdf generation library into this library to reduce multiple runtime mapping requirements if both libraries are used. Both libraries share the runtime reference and some support code. 8 | 9 | * **Add .NET 9.0 target, drop .NET 6.0 target** 10 | With release of .NET 9.0 we're adding the new release target and dropping off the old out of maintenance version of .NET. 11 | 12 | ### 0.1.22 13 | 14 | * **Rollback to previously used runtime (temporary)** 15 | Due to a targeting bug in the WebView2 SDK, I'm rolling back to `v1.0.2592.51` of the SDK. Later versions prior to `.2800` will not push forward the transitive WebView sdk dependencies into the top level projects so an explicit reference to `Microsoft.Web.WebView2` was required. I'll update as soon as that bug has been fixed. 16 | 17 | ### 0.1.21 18 | 19 | * **Update to latest WebView Runtime** 20 | 21 | ### 0.1.15 22 | 23 | * **Add support for `NavigateToString()` and `NavigateFromStream()` with large Content** 24 | The native `NavigateToString()` is [restricted to HTML content of less than 2mb](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.navigatetostring?view=webview2-dotnet-1.0.2045.28#remarks) and has no stream support. These functions avoid the limitation and add stream support. -------------------------------------------------------------------------------- /HtmlToPdf.md: -------------------------------------------------------------------------------- 1 | # .NET Html to Pdf Conversion using WebView on Windows 2 | 3 | #### *Creating Pdf from Html with .NET on Windows using the WebView2 control* 4 | 5 | [![](https://img.shields.io/nuget/v/Westwind.WebView.svg)](https://www.nuget.org/packages/Westwind.WebView/) [![](https://img.shields.io/nuget/dt/Westwind.WebView.svg)](https://www.nuget.org/packages/Westwind.WebView/) 6 | 7 | 8 | The **HtmlToPdf feature** of the `Westwind.WebView` library provides a quick way to print Html to Pdf on Windows. It uses the WebView2 Control in headless mode that doesn't require any UI. It can be run from any Windows .NET application, including non-UI and service applications and IIS. 9 | 10 | > Because Html To Pdf generation uses the built-in Windows WebView2 Runtime and SDK, there are no large dependencies required to use it unlike other tools. 11 | 12 | Pdf output can be generated from: 13 | 14 | * An HTML Url 15 | * An HTML File on disk 16 | 17 | Output can be generated to: 18 | 19 | * File 20 | * Stream 21 | 22 | Using the following callback methods: 23 | 24 | * Using an Async Call 25 | * Using Event Callbacks 26 | 27 | The library uses the built-in **WebView2 Runtime in Windows so it has no external dependencies for your applications** assuming you are running on a recent version of Windows that has the WebView2 Runtime installed. 28 | 29 | > #### Server Unattended Usage Requires Special Consideration 30 | > HtmlToPdf generation works in server environments, but there is one limitation in that you cannot generate a table of contents at the moment. There's a server specific configuration that must be set in order to run inside of a service environment like IIS or as a Windows Service. For more info [see below](#unattended-server-operation-iis-windows-services-etc). 31 | 32 | If you would like to find out more how this library works and how the original code and Pdf code was build, you can check out this blog post: 33 | 34 | * [Programmatic Html to Pdf Generation using the WebView2 Control](https://weblog.west-wind.com/posts/2024/Mar/26/Programmatic-Html-to-PDF-Generation-using-the-WebView2-Control-with-NET) 35 | 36 | ## Prerequisites 37 | The library is Windows specific and works with: 38 | 39 | ### Support for 40 | * Windows 11/10 Server 2019/2022 41 | * Desktop Applications 42 | * Console Applications 43 | * Service Applications 44 | 45 | The component does not support: 46 | 47 | * Non Windows platforms 48 | 49 | ### Targets 50 | 51 | * net9.0-windows 52 | * net8.0-windows 53 | * net472 54 | 55 | ### Dependencies 56 | Deployed applications have the following dependencies: 57 | 58 | * [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH&ch=1#download) 59 | On recent updates of Windows 11 and 10, the WebView is pre-installed as a system component. On Servers however, you may have to explicitly install the WebView Runtime. 60 | 61 | * [Windows Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 62 | The WebView2 component is dependent on Windows Desktop Runtime libraries and therefore requires the Desktop runtime to be installed **even for server applications**. On Server OS versions the WebView2 runtime has to be explicitly installed as it's not pre-installed by Windows. 63 | 64 | ## Using the library 65 | This library only has a single dependency on the WebView control and provides very fast base Html to PDF conversion. 66 | 67 | You can install this NuGet package: 68 | 69 | ```ps 70 | dotnet add package westwind.webview 71 | ``` 72 | 73 | The library exposes 4 separate output methods: 74 | 75 | * PrintToPdfStreamAsync() - Runs async and returns a `result.ResultStream` 76 | * PrintToPdfAsync() - Runs async and creates a PDF output file 77 | * PrintToPdfStream() - Creates a PDF and returns it via Callbacks in `result.ResultStream` 78 | * PrintToPdf() - Creates a PDF to file a notifies completion via Callback 79 | 80 | All of the methods take a file or Url as input. File names have to be fully qualified with a path. Output to file requires that you provide a filename. 81 | 82 | All requests return a `PdfPrintResult` structure which has a `IsSuccess` flag you can check. For stream results, the `ResultStream` property will be set with a `MemoryStream` instance on success. Errors can use the `Message` or `LastException` to retrieve error information. 83 | 84 | ### Async Call Syntax for Stream Result 85 | 86 | ```cs 87 | var outputFile = Path.GetFullPath(@".\test3.pdf"); 88 | var htmlFile = Path.GetFullPath("HtmlSampleFileLonger-SelfContained.html"); 89 | 90 | var host = new HtmlToPdfHost() 91 | { 92 | BackgroundHtmlColor = "#ffffff" 93 | }; 94 | host.CssAndScriptOptions.KeepTextTogether = true; 95 | 96 | var pdfPrintSettings = new WebViewPrintSettings() 97 | { 98 | // margins are 0.4F default 99 | MarginTop = 0.5, 100 | MarginBottom = 0.3F, 101 | ScaleFactor = 0.9F, // 1 is default 102 | 103 | ShouldPrintHeaderAndFooter = true, 104 | HeaderTitle = "Custom Header (centered)", 105 | FooterText = "Custom Footer (lower right)", 106 | 107 | // Optionally customize the header and footer completely - WebView syntax 108 | // HeaderTemplate = "
" + 109 | // "
", 110 | // FooterTemplate = "
" + 111 | // " of " + 112 | // "
", 113 | 114 | GenerateDocumentOutline = true // default 115 | }; 116 | 117 | // We're interested in result.ResultStream 118 | var result = await host.PrintToPdfStreamAsync(htmlFile, pdfPrintSettings); 119 | 120 | Assert.IsTrue(result.IsSuccess, result.Message); 121 | Assert.IsNotNull(result.ResultStream); // THIS 122 | 123 | // Copy resultstream to output file 124 | File.Delete(outputFile); 125 | using (var fstream = new FileStream(outputFile, FileMode.OpenOrCreate, FileAccess.Write)) 126 | { 127 | result.ResultStream.CopyTo(fstream); 128 | result.ResultStream.Close(); // Close returned stream! 129 | } 130 | ShellUtils.OpenUrl(outputFile); 131 | ``` 132 | 133 | ### Async Stream Example in a Web Application 134 | 135 | ```csharp 136 | [HttpGet("rawpdf")] 137 | public async Task RawPdf() 138 | { 139 | // source file or URL to render to Pdf 140 | var file = Path.GetFullPath("./HtmlSampleFile-SelfContained.html"); 141 | 142 | var pdf = new HtmlToPdfHost(); 143 | var pdfResult = await pdf.PrintToPdfStreamAsync(file, new WebViewPrintSettings { PageRanges = "1-10"}); 144 | 145 | if (pdfResult == null || !pdfResult.IsSuccess) 146 | { 147 | Response.StatusCode = 500; 148 | return new JsonResult(new 149 | { 150 | isError = true, 151 | message = pdfResult.Message 152 | }); 153 | } 154 | 155 | return new FileStreamResult(pdfResult.ResultStream, "application/pdf"); 156 | } 157 | ``` 158 | 159 | ### Async Call Syntax for File Output 160 | 161 | ```csharp 162 | // Url or full qualified file path 163 | var htmlFile = Path.GetFullPath("HtmlSampleFileLonger-SelfContained.html"); 164 | var outputFile = Path.GetFullPath(@".\test2.pdf"); 165 | File.Delete(outputFile); 166 | 167 | var host = new HtmlToPdfHost(); // or new HtmlToPdfHostExtended() 168 | var result = await host.PrintToPdfAsync(htmlFile, outputFile); 169 | 170 | Assert.IsTrue(result.IsSuccess, result.Message); 171 | ShellUtils.OpenUrl(outputFile); // display the Pdf file you specified 172 | ``` 173 | 174 | 175 | ### Callback Syntax to Pdf File 176 | 177 | ```csharp 178 | var htmlFile = Path.GetFullPath("HtmlSampleFile-SelfContained.html"); 179 | var outputFile = Path.GetFullPath(@".\test.pdf"); 180 | File.Delete(outputFile); 181 | 182 | var host = new HtmlToPdfHost(); 183 | 184 | // Callback when complete 185 | var onPrintComplete = (PdfPrintResult result) => 186 | { 187 | if (result.IsSuccess) 188 | { 189 | ShellUtils.OpenUrl(outputFile); 190 | Assert.IsTrue(true); 191 | } 192 | else 193 | { 194 | Assert.Fail(result.Message); 195 | } 196 | }; 197 | var pdfPrintSettings = new WebViewPrintSettings() 198 | { 199 | // default margins are 0.4F 200 | MarginBottom = 0.2F, 201 | MarginLeft = 0.2f, 202 | MarginRight = 0.2f, 203 | MarginTop = 0.4f, 204 | ScaleFactor = 0.8f, 205 | PageRanges = "1,2,5-7" 206 | }; 207 | host.PrintToPdf(htmlFile, outputFile, onPrintComplete, pdfPrintSettings); 208 | 209 | // make sure app keeps running 210 | ``` 211 | 212 | ### Event Syntax to Stream 213 | 214 | ```csharp 215 | // File or URL 216 | var htmlFile = Path.GetFullPath("HtmlSampleFile-SelfContained.html"); 217 | var host = new HtmlToPdfHost(); 218 | 219 | // Callback on completion 220 | var onPrintComplete = (PdfPrintResult result) => 221 | { 222 | if (result.IsSuccess) 223 | { 224 | // create file so we can display 225 | var outputFile = Path.GetFullPath(@".\test1.pdf"); 226 | File.Delete(outputFile); 227 | 228 | using var fstream = new FileStream(outputFile, FileMode.OpenOrCreate, FileAccess.Write); 229 | result.ResultStream.CopyTo(fstream); 230 | 231 | result.ResultStream.Close(); // Close returned stream! 232 | 233 | ShellUtils.OpenUrl(outputFile); 234 | Assert.IsTrue(true); 235 | } 236 | else 237 | { 238 | Assert.Fail(result.Message); 239 | } 240 | }; 241 | var pdfPrintSettings = new WebViewPrintSettings() 242 | { 243 | MarginBottom = 0.2F, 244 | MarginLeft = 0.2f, 245 | MarginRight = 0.2f, 246 | MarginTop = 0.4f, 247 | ScaleFactor = 0.8f, 248 | }; 249 | host.PrintToPdfStream(htmlFile, onPrintComplete, pdfPrintSettings); 250 | 251 | // make sure app keeps running 252 | ``` 253 | 254 | The `Task` based methods are easiest to use so that's the recommended syntax. The callback based methods are there so you can more easily use this if you are running in a non-async and can't easily transition to async. 255 | 256 | Both approaches run on a separate STA thread to ensure that the WebView can run regardless of whether you are running inside of an application that has a main UI/STA thread and it works inside of Windows Service contexts. 257 | 258 | ## Unattended Server Operation (IIS, Windows Services etc.) 259 | HtmlToPdf generation is supported in unattended mode, but there are some requirements and limitations in this environment. 260 | 261 | ![Running Under IIS](Assets/RunningUnderIIS.png) 262 | 263 | Requirements and limitations: 264 | 265 | * Table of Content Generation is not supported (no DevTools operation) 266 | * File output may not be supported (depending on permissions) 267 | * Application has to be set for server operation on startup 268 | 269 | Before looking at some of the limitations and requirements lets look at how you can use this library in ASP.NET Core. 270 | 271 | ### Running HtmlToPdf in ASP.NET 272 | You can look at the [AspNetSample](/AspNetSample) which contains an ASP.NET Core test project that demonstrates Web usage. To see the permissions issues though you have to run IIS using a default or non-interactive account. 273 | 274 | To return a PDF as a document in a Controller: 275 | 276 | ```csharp 277 | [HttpGet("rawpdf")] 278 | public async Task RawPdf() 279 | { 280 | var file = Path.GetFullPath("./HtmlSampleFile-SelfContained.html"); 281 | 282 | var pdf = new HtmlToPdfHost(); 283 | var pdfResult = await pdf.PrintToPdfStreamAsync(file, new WebViewPrintSettings { PageRanges = "1-10"}); 284 | 285 | if (pdfResult == null || !pdfResult.IsSuccess) 286 | { 287 | Response.StatusCode = 500; 288 | return new JsonResult(new 289 | { 290 | isError = true, 291 | message = pdfResult.Message 292 | }); 293 | } 294 | 295 | return new FileStreamResult(pdfResult.ResultStream, "application/pdf"); 296 | } 297 | ``` 298 | 299 | Typically you'll want to print to stream so no files are created on disk. You can also write to file and store the output, but make sure you have adequate permissions to write in the target location. 300 | 301 | ### Server vs. non-Server Operation 302 | This library by default runs in non-server mode and uses the W**ebView2 DevTools Protocol** to print PDF files. For server mode it uses the WebView's built in PDF generation that's also used by actual Chromium browser UIs. 303 | 304 | The difference between these two APIs is that the DevTools API provides a few additional features not available in the built-in print API - most notably for this library the PDF Table Of Contents generation. 305 | 306 | > #### Server Limitations 307 | > Unfortunately the DevTools API currently does not work in unattended, non-active-desktop environments and so this library falls back to the WebView functionality. 308 | > 309 | > Therefore: 310 | > 311 | > Server Operation **does not** support the auto generated PDF Table of Contents 312 | 313 | ### Configure for running in Server Mode 314 | In order to run in 'server' mode you need to configure the application during startup (before any PDF generation). This effectively involves setting the `HtmlToPdfDefaults.UseServerPdfGeneration = true`. 315 | 316 | You can do this in two ways: 317 | 318 | * Setting the variable directly in your server startup code 319 | * Calling `HtmlToPdfDefaults.ServerPreInitialize()` *(recommended)* 320 | 321 | `ServerPreInitialize()` sets the server flag, and also runs a small empty document dummy PDF generation to prime the PDF engine for faster startup. This also fixes an occasional issue with first time rendering of PDF output. 322 | 323 | ```cs 324 | // Program.cs 325 | ... 326 | 327 | var webViewEnvPath = Path.GetFullPath("WebViewEnvironment") 328 | HtmlToPdfDefaults.ServerPreInitialize(webViewEnvPath); 329 | 330 | // alternately set just 331 | // HtmlToPdfDefaults.UseServerPdfGeneration = true; 332 | 333 | // preferably before builder.Build() is called 334 | var app = builder.Build(); 335 | ``` 336 | 337 | ### Permissions 338 | But as is always the case when you run in a server environment, you have to make sure you have the right permissions to access the files you want to convert and if you're generating to a file that you have write access. Generally you'll want to generate to stream to avoid having to write files to disk 339 | 340 | #### WebView Environment Permissions 341 | The code above specifies an explicit path for the WebView environment. By default the `%TEMP%` folder is used, but in server environments there may be no temp folder for some accounts like the ApplicationPoolIdentity. So you can specify a path here. 342 | 343 | I've found that you can write into your content root and that works even when running with IIS Application Pool Identity (which is odd, but it works for me). Your mileage may vary. 344 | 345 | 346 | ## Support us 347 | If you use this project and it provides value to you, please consider supporting by contributing or supporting via the sponsor link or one time donation at the top of this page. Value for value. 348 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | West Wind WebView Library for .NET 2 | ================================== 3 | 4 | MIT License 5 | ----------- 6 | 7 | Copyright (c) 2023 West Wind Technologies 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Westwind.WebView Interop Support and Html to Pdf Generation 2 | 3 | ![](https://img.shields.io/nuget/v/Westwind.WebView.svg) ![](https://img.shields.io/nuget/dt/Westwind.WebView.svg) 4 | 5 | A .NET support library for the `Microsoft.Web.WebView2` control to aid with common operations and .NET / JavaScript interop as well as [Html to Pdf generation](https://github.com/RickStrahl/Westwind.WebView/blob/master/HtmlToPdf.md). 6 | 7 | The library provides: 8 | 9 | * **Base `WebViewHandler` behavior class** 10 | Base class that can be overridden or used as is to facilitate initializing the WebView control for interaction via JavaScript and custom navigation. Provides easy support for folder to virtual site mapping, easy Interop calls into JavaScript and a number of helpers for navigation and request interception. 11 | 12 | * **JavaScript Interop Class** 13 | Provides a base class that can be used for easily wrapping Javascript interop calls. The class allows JS method invocation and handles all inbound and outbound data serialization. 14 | 15 | * **Cached WebView Environment** 16 | The WebView handler optionally uses a cached WebView environment to ensure consistent WebView environment reuse. For more info see:* [A CachedWebView Environment to ensure consistent Environment reuse](https://weblog.west-wind.com/posts/2023/Oct/31/Caching-your-WebView-Environment-to-manage-multiple-WebView2-Controls) 17 | 18 | * **Html To Pdf Conversion: HtmlToPdfHost** 19 | A standalone `HtmlToPdfHost` component that can be used to convert Html content from any Url or local file into PDF files or streams using the Chromium PDF generation engine. Unlike many other Html to Pdf generation tools, this component uses the built-in Windows WebView Runtime, so **there's no large runtime dependency** in your applications. 20 | [HtmlToPdf functionality is documented separately](https://github.com/RickStrahl/Westwind.WebView/blob/master/HtmlToPdf.md). 21 | 22 | The WebView Handler is meant to be used when you need to do a lot of Interop between your .NET and JavaScript code. It ties together the WebView initialization, calling of methods in JavaScript and receiving callbacks back into .NET from JavaScript and hooking common events that you might have to deal with. 23 | 24 | ## Installation 25 | To install the library install the NuGet package from: 26 | 27 | ```ps 28 | install-package Westwind.WebView 29 | ``` 30 | 31 | ## WebViewHandler Usage 32 | The WebView Handler is primarily meant to be used when you need to do a lot of Interop between your .NET and JavaScript code. It ties together the WebView initialization, calling of methods in JavaScript and receiving callbacks back into .NET from JavaScript. Initialization initializes the WebView but also provides hooks for when content has loaded so you can start running JavaScript code and pass in state when the WebView initially loads. 33 | 34 | There are three distinct components: 35 | 36 | * **The `WebViewHandler`** 37 | This is the top level object that handles WebView initialization, setting up things like mapping a local file path to a Web domain (if needed), hooking up a .NET callback object that can be called from .NET and creating an instance of a JavaScript proxy that allows more easily calling into JavaScript. 38 | 39 | * **The JavaScript Interop Object** 40 | This is a class that acts as an RPC proxy into .NET that basically helps you make `ExecuteScriptAsync()` calls into .NET by automatically handling parameter serialization and result deserialization. Using a Reflection like interface that lets you use `Invoke()` and `Get()`, `Set()` methods to interop with JavaScript code. 41 | 42 | * **A Dotnet Callback Object** 43 | This objects is 'passed into JavaScript' and accessible as a host object in JavaScript via: 44 | 45 | ```js 46 | // Async 47 | let result = await window.chrome.webview.hostObjects.websurge.RunRequest(url); 48 | 49 | // Sync 50 | let success = window.chrome.webview.hostObjects.sync.websurge.NavigateLink(url); 51 | ``` 52 | 53 | A host object is just a .NET POCO object that contains methods to callback to either sync or async. You don't need to provide this if you don't have callbacks, or if you have only very few you can just use the JavaScriptInterop object to hold those methods and pass that. 54 | 55 | > Both the JavaScript and Dotnet objects are optional. You can pass those in as null values and they won't be set or used for anything, but if that's the case you probably don't have much need for the `WebViewHandler` in the first place. 56 | 57 | * **A Cached WebView Environment Class** 58 | This class handles consistently initializing and re-using a single WebView environment inside of an application to avoid odd behaviors due to incompatible environment settings. Avoids among other things creating a default environment which may not work in some application if permissions don't allow access to create the default environment folder. The static methods of this class are used by default by the `WebViewHandler` to initialize the environment via `CoreWebView2Environment.CreateAsync()`. 59 | 60 | `WebViewHandler` is a behavior class that is attached to an existing instance of a WebView control, typically assigned in the constructor of the WebView host control. 61 | 62 | The recommended way to use these tools is: 63 | 64 | * Create an application specific subclass for `WebViewHandler` and - as needed - `JavaScriptInterop` and `DotnetInterop` objects 65 | * Attach the behavior to a WebView control in the host's control or form CTOR. 66 | 67 | ### Creating Application Specific WebViewHandlers 68 | The recommended way to use these classes is by deriving an application specific subclass for the `WebViewHandler`, and if needed `JavaScriptInterop` and `DotnetInterop` objects: 69 | 70 | ```csharp 71 | /// 72 | /// Create an application specific implementation of the WebView Handler 73 | /// 74 | /// CTOR/base configures the optional dependencies for the JavaScript and Dotnet 75 | /// interop objects. 76 | /// 77 | public class DocumentationPreviewHandler : WebViewHandler 78 | { 79 | public DocumentationPreviewHandler(WebView2 webBrowser) : 80 | base(webBrowser, wsApp.Constants.WebViewEnvironmentFolderName, new DocumentationPreviewDotnetInterop()) 81 | { 82 | 83 | } 84 | } 85 | 86 | 87 | /// 88 | /// Subclass from the BaseJavaScriptInterop class to get the abililty to easily 89 | /// call methods in the JavaScript code. 90 | /// 91 | /// Recommend you create a method for each JavaScript call you make using `Invoke()` 92 | /// 93 | public class DocumentationPreviewJavaScriptInterop : BaseJavaScriptInterop 94 | { 95 | public DocumentationViewer DocumentationViewer { get; set; } 96 | 97 | public DocumentationPreviewJavaScriptInterop(WebView2 webBrowser, string baseInvocationTarget = "window") : base(webBrowser,baseInvocationTarget) 98 | { 99 | } 100 | 101 | 102 | /// 103 | /// Update the document with an HTML string. Optional line number 104 | /// on where to scroll the document to. 105 | /// 106 | /// 107 | /// 108 | public async Task UpdateDocumentContent(string html, int lineNo) 109 | { 110 | await Invoke("updateDocumentContent", html, lineNo); 111 | } 112 | 113 | 114 | /// 115 | /// Scroll to a specific line in the document 116 | /// 117 | public async Task ScrollToPragmaLine(int editorLineNumber = -1, 118 | string headerId = null, 119 | bool updateCodeBlocks = true, 120 | bool noScrollTimeout = false, bool noScrollTopAdjustment = false) 121 | { 122 | await Invoke("scrollToPragmaLine", 123 | editorLineNumber, headerId, 124 | noScrollTimeout, noScrollTopAdjustment); 125 | } 126 | } 127 | 128 | 129 | /// 130 | /// If you have a lot of callbacks use a separate object. 131 | /// Otherwise you may just use the JavaScript object above 132 | /// to send callbacks to. 133 | /// 134 | /// This is a plain .NET object - keep it simple as this it 135 | /// uses COM for its marshaling. 136 | /// 137 | [ComVisible(true)] 138 | public class DocumentationPreviewDotnetInterop 139 | { 140 | } 141 | ``` 142 | 143 | ### Using the Custom WebViewHandler 144 | Once you've created the handler you can then assign it to a Web View control. 145 | 146 | In its simplest for you can just instantiate the handler: 147 | 148 | ```cs 149 | // Host control 150 | public partial class DocumentationViewer : UserControl 151 | { 152 | DocumentationViewerModel Model { get; } 153 | 154 | public DocumentationPreviewHandler PreviewHandler { get; set; } 155 | 156 | // do this either in the CTOR or Loaded 157 | private void DocumentationViewer_Loaded(object sender, System.Windows.RoutedEventArgs e) 158 | { 159 | var jsInterop = new DocumentationPreviewJavaScriptInterop(PreviewBrowser, "window") 160 | { 161 | DocumentationViewer = this // custom state 162 | }; 163 | PreviewHandler = new DocumentationPreviewHandler(PreviewBrowser) 164 | { 165 | JsInterop = jsInterop // optional 166 | }; 167 | } 168 | } 169 | ``` 170 | 171 | Something a little more sophisticated might look like this where we specify a host of additional settings: 172 | 173 | 174 | ```csharp 175 | void ConfigureEditor(RequestDocumentationItem documentation) 176 | { 177 | var dotnetHostObject = new DocumentationEditorDotnetHostObject(AppModel.Current, this, null) 178 | { 179 | DocItem = documentation 180 | }; 181 | var jsInterop = new DocumentationEditorJavaScriptInterop(EditorBrowser, "window.textEditor"); 182 | //Loaded += DocumentationViewer_Loaded; 183 | 184 | #if DEBUG 185 | //var editorPath = "Editor"; // production folder 186 | var editorPath = @"d:\projects\WebSurge2\WebSurge\Html\Editor"; 187 | #else 188 | var editorPath = System.IO.Path.GetFullPath(".\\HTML\\Editor"); // production folder 189 | #endif 190 | EditorHandler = new DocumentationEditorWebViewHandler(EditorBrowser, dotnetHostObject) 191 | { 192 | JsInterop = jsInterop, 193 | HostObjectName = "mm", // HostObject name inside of WebView 194 | ShowDevTools = false, 195 | 196 | HostWebRootFolder = editorPath, // folder used as web site 197 | HostWebHostNameForFolder = "websurge.doceditor", // mapped domain 198 | InitialUrl = "https://websurge.doceditor/editor.htm" 199 | }; 200 | 201 | // additional app specific properties in custom version that are used for initial nav 202 | EditorHandler.InitialValue = documentation.Documentation; // custom logic applied 203 | } 204 | ``` 205 | 206 | This initial assignment triggers the initialization of the WebView and essentially starts an initial navigation with the assigned `InitialUrl` (or `Source` if not assigned). InitialUrl is a delayed navigation that ensures that the URL is not set until after the Host folder is mapped. This avoids failed navigations on initial display of the WebView. 207 | 208 | 209 | ## CachedWebViewEnvironment Usage 210 | This class can be used to initialize the WebView Environment consistently. It's also used internally by `WebViewHandler` if no explicit environment is passed set in the CTOR. 211 | 212 | This class can be used independently of `WebViewHandler` if you manually instantiate your WebView environment. 213 | 214 | ### Initialize Environment Folder and Options 215 | The first step is to initialize the WebView Environment folder location and set any options. This should be done **before the WebView is first instantiated** preferably during startup of the application. 216 | 217 | In WPF `OnStartup()` is a good place: 218 | 219 | ```csharp 220 | protected override void OnStartup(StartupEventArgs e) 221 | { 222 | 223 | // initialize single environment folder for all WebViews 224 | CachedWebViewEnvironment.Current.EnvironmentFolderName = Path.Combine( 225 | mmApp.Configuration.LocalAppDataFolder, 226 | mmApp.Constants.WebViewEnvironmentFolderName); 227 | 228 | // Optionally - set any custom startup flags and options. 229 | // Typically this can be left at null 230 | // CachedWebViewEnvironment.Current.EnvironmentOptions = null; 231 | 232 | ... 233 | } 234 | ``` 235 | 236 | ### Initializing the WebView Control 237 | Then, anywhere you need to use a WebView Environment, you can then initialize the WebView with this environment via the `InitializeWebViewEnvironment()` method, which either creates a new environment if it doesn't exist yet, or reuses the previously created one that is cached. 238 | 239 | This method calls `webBrowser.EnsureCoreWebView2Async()` to wait for the WebView to be initialized **and become UI active** (!) using the cached environment as its parameter. 240 | 241 | > Note the `EnsureCoreWebView2Async()` and by extension `InitializeWebViewEnvironment()` can take a long time to complete as it waits for UI activation before returning. 242 | > 243 | > If the WebView is not visible (ie. inactive on another tab, or otherwise not visible) it will not return until it becomes active. 244 | 245 | In a usage scenario you can use `InitializeWebViewEnvironment()` like this during WebView initialization: 246 | 247 | ```cs 248 | // Manual WebView Initialization 249 | protected async Task InitializeAsync() 250 | { 251 | if (JsInterop == null) 252 | JsInterop = CreateJsInteropInstance(); 253 | 254 | if (!IsInitialized) // Ensure this doesn't run more than once 255 | { 256 | // THIS 257 | await CachedWebViewEnvironment.Current.InitializeWebViewEnvironment(WebBrowser); 258 | 259 | if(InitializeComplete != null) 260 | InitializeComplete(); 261 | } 262 | ... 263 | // Code to set up Virtual Folder mapping 264 | // initial navigation etc. 265 | } 266 | ``` 267 | 268 | Rinse and repeat this process if you have multiple WebView controls in your application, or if you are repeatedly creating the same control. -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/AspNetHtmlToPdfSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0-windows 5 | 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | PreserveNewest 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/GeneratedPdf.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickStrahl/Westwind.WebView/cbf7323a89ba99a91ce1a395f80dcf29a6899d12/Samples/AspNetHtmlToPdfSample/GeneratedPdf.pdf -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/PdfController.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Net.Http.Headers; 4 | using Westwind.WebView.HtmlToPdf; 5 | 6 | namespace WebApplication1 7 | { 8 | 9 | /// 10 | /// IMPORTANT: This works when running locally using Kestrel on the desktop 11 | /// It **does not work** inside of a system context - ie. inside of IIS without a loging 12 | /// (unless you run as INTERACTIVE) 13 | /// 14 | [ApiController] 15 | [Route("pdf")] 16 | public class PdfController : ControllerBase 17 | { 18 | /// 19 | /// Default result = return JSON object with embedded binary data 20 | /// 21 | /// 22 | [HttpGet] 23 | public async Task Get() 24 | { 25 | var file = Path.GetFullPath("./HtmlSampleFile-SelfContained.html"); 26 | 27 | var pdf = new HtmlToPdfHost(); 28 | var pdfResult = await pdf.PrintToPdfStreamAsync(file, new WebViewPrintSettings { PageRanges = "1-5"}); 29 | 30 | if (pdfResult == null || !pdfResult.IsSuccess) 31 | { 32 | return new 33 | { 34 | IsError = true, 35 | Message = pdfResult.Message 36 | }; 37 | } 38 | Response.StatusCode = 200; 39 | 40 | return new 41 | { 42 | IsError = false, 43 | PdfBytes = (pdfResult.ResultStream as MemoryStream).ToArray() 44 | }; 45 | } 46 | 47 | /// 48 | /// Return raw data as PDF 49 | /// 50 | /// 51 | [HttpGet("rawpdf")] 52 | public async Task RawPdf() 53 | { 54 | var file = Path.GetFullPath("./HtmlSampleFile-SelfContained.html"); 55 | 56 | var pdf = new HtmlToPdfHost(); 57 | var pdfResult = await pdf.PrintToPdfStreamAsync(file, new WebViewPrintSettings { PageRanges = "1-10"}); 58 | 59 | if (pdfResult == null || !pdfResult.IsSuccess) 60 | { 61 | Response.StatusCode = 500; 62 | return new JsonResult(new 63 | { 64 | isError = true, 65 | message = pdfResult.Message 66 | }); 67 | } 68 | 69 | return new FileStreamResult(pdfResult.ResultStream, "application/pdf"); 70 | } 71 | 72 | /// 73 | /// Return raw data as PDF 74 | /// 75 | /// 76 | [HttpGet("pdftofile")] 77 | public async Task PdfToFile() 78 | { 79 | var file = Path.GetFullPath("./HtmlSampleFile-SelfContained.html"); 80 | 81 | var pdf = new HtmlToPdfHost(); 82 | var pdfResult = await pdf.PrintToPdfAsync(file, "GeneratedPdf.pdf", new WebViewPrintSettings { PageRanges = "1-10" }); 83 | 84 | if (pdfResult == null || !pdfResult.IsSuccess) 85 | { 86 | Response.StatusCode = 500; 87 | return new JsonResult(new 88 | { 89 | isError = true, 90 | message = pdfResult.Message 91 | }); 92 | } 93 | 94 | var path = Path.GetFullPath("GeneratedPdf.pdf"); 95 | return PhysicalFile(path, "application/pdf"); 96 | } 97 | 98 | /// 99 | /// Return raw data as PDF 100 | /// 101 | /// 102 | [HttpGet("PdfFromUrl")] 103 | public async Task PdfFromUrl([FromQuery] string url) 104 | { 105 | if (string.IsNullOrEmpty(url)) 106 | url = Path.GetFullPath("./HtmlSampleFile-SelfContained.html"); 107 | 108 | var pdf = new HtmlToPdfHost(); 109 | var pdfResult = await pdf.PrintToPdfStreamAsync(url, new WebViewPrintSettings { }); 110 | 111 | if (pdfResult == null || !pdfResult.IsSuccess) 112 | { 113 | Response.StatusCode = 500; 114 | return new JsonResult(new 115 | { 116 | isError = true, 117 | message = pdfResult.Message 118 | }); 119 | } 120 | 121 | return new FileStreamResult(pdfResult.ResultStream, "application/pdf"); 122 | } 123 | 124 | /// 125 | /// Status info to ensure app works 126 | /// 127 | /// 128 | [HttpGet("ping")] 129 | public object Ping() 130 | { 131 | return new 132 | { 133 | Message = "Hello World.", 134 | Time = DateTime.Now, 135 | User = Environment.UserName, 136 | Principal = System.Security.Principal.WindowsIdentity.GetCurrent().Name, 137 | WebViewEnvironmentPath = HtmlToPdfDefaults.WebViewEnvironmentPath, 138 | OS = System.Runtime.InteropServices.RuntimeInformation.OSDescription, 139 | System.Runtime.InteropServices.RuntimeInformation.OSArchitecture, 140 | Framework = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, 141 | Executable = Assembly.GetEntryAssembly().Location 142 | }; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.FileProviders; 2 | using Westwind.WebView.HtmlToPdf; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | // Add services to the container. 7 | 8 | builder.Services.AddControllers(); 9 | 10 | // ***IMPORTANT!*** 11 | // Initialize Server so that WebView can render - required only for non-desktop environments/services/IIS 12 | HtmlToPdfDefaults.ServerPreInitialize( Path.Combine(builder.Environment.ContentRootPath,"WebViewEnvironment") ); 13 | 14 | var app = builder.Build(); 15 | 16 | // Configure the HTTP request pipeline. 17 | 18 | if (builder.Environment.IsDevelopment()) 19 | { 20 | app.UseDeveloperExceptionPage(); 21 | } 22 | 23 | app.UseStaticFiles(); 24 | 25 | app.UseAuthorization(); 26 | 27 | app.MapControllers(); 28 | 29 | app.Run(); -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:28181", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "", 17 | "applicationUrl": "http://localhost:5063", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/WebApplication1.http: -------------------------------------------------------------------------------- 1 | @WebApplication1_HostAddress=http://localhost:5063 2 | 3 | GET http://localhost:5063/pdf HTTP/2.0 4 | Accept: application/json 5 | Websurge-Request-Name: PDF as Json 6 | 7 | ### 8 | 9 | GET http://localhost:5063/pdf/rawpdf HTTP/2.0 10 | Accept-Encoding: gzip,deflate 11 | Websurge-Request-Name: Direct PDF access 12 | 13 | ### 14 | 15 | GET http://localhost:5063/pdf/ping HTTP/2.0 16 | Accept-Encoding: gzip,deflate 17 | Websurge-Request-Name: Ping Info 18 | 19 | ### 20 | 21 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/WebApplication1.websurge: -------------------------------------------------------------------------------- 1 | GET pdf HTTP/2.0 2 | Accept: application/json 3 | Websurge-Request-Name: PDF as Json 4 | 5 | ------------------------------------------------------------------ 6 | 7 | GET pdf/rawpdf HTTP/2.0 8 | Accept-Encoding: gzip,deflate 9 | Websurge-Request-Name: Direct PDF access 10 | 11 | ------------------------------------------------------------------ 12 | 13 | GET pdf/pdftofile HTTP/2.0 14 | Accept-Encoding: gzip,deflate 15 | Websurge-Request-Name: PrintToFile 16 | 17 | ------------------------------------------------------------------ 18 | 19 | GET pdf/pdffromurl?url=https://microsoft.com HTTP/2.0 20 | Accept-Encoding: gzip,deflate 21 | Websurge-Request-Name: Direct PDF access EX 22 | 23 | ------------------------------------------------------------------ 24 | 25 | GET pdf/ping HTTP/2.0 26 | Accept-Encoding: gzip,deflate 27 | Websurge-Request-Name: Ping Info 28 | 29 | ------------------------------------------------------------------ 30 | 31 | 32 | ----- Start WebSurge Options ----- 33 | 34 | { 35 | "SiteBaseUrl": "http://localhost:5063/", 36 | "RecentSiteBaseUrlList": [ 37 | "http://localhost:5063/", 38 | "http://localhost:2205/", 39 | ], 40 | "OnlineSessionId": null, 41 | "SessionVariables": { 42 | "WebApplication1_HostAddress": "http://localhost:5063" 43 | }, 44 | "UseCustomUsers": true, 45 | "HttpProtocolVersion": "1.1", 46 | "IgnoreCertificateErrors": false, 47 | "NoContentDecompression": false, 48 | "UpdateHeadersFromRequest": false, 49 | "SecondsToRun": 60, 50 | "ThreadCount": 2, 51 | "DelayTimeMs": 0, 52 | "WarmupSeconds": 2, 53 | "RequestTimeoutMs": 15000, 54 | "RandomizeRequests": false, 55 | "MaxConnections": 100, 56 | "NoProgressEvents": false, 57 | "RemoveStartAndEndPercentile": 0, 58 | "ReplaceQueryStringValuePairs": null, 59 | "ReplaceCookieValue": null, 60 | "TrackPerSessionCookies": true, 61 | "ReplaceAuthorization": null, 62 | "Username": null, 63 | "Password": null, 64 | "UsernamePasswordType": "Negotiate", 65 | "Users": [], 66 | "UserAgent": null, 67 | "CaptureMinimalResponseData": false, 68 | "MaxResponseSize": 9999999, 69 | "Documentation": [] 70 | } 71 | 72 | // This file was generated by West Wind WebSurge 73 | // Get your free copy at https://websurge.west-wind.com 74 | // to easily test or load test the requests in this file. 75 | 76 | ----- End WebSurge Options ----- 77 | 78 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/publish.cmd: -------------------------------------------------------------------------------- 1 | IISRESET 2 | REM Restart-WebAppPool PdfWebApp 3 | dotnet publish -o ../WebApp1_Publish -c Release -------------------------------------------------------------------------------- /Samples/AspNetHtmlToPdfSample/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Samples/ConsoleHtmlToPdfSample/ConsoleHtmlToPdfSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0-windows 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Samples/ConsoleHtmlToPdfSample/Program.cs: -------------------------------------------------------------------------------- 1 | // Async or Callback 2 | //#define UseAsync 3 | 4 | using Westwind.WebView.HtmlToPdf; 5 | using Westwind.Utilities; 6 | using System; 7 | 8 | 9 | 10 | namespace ConsoleApp1 11 | { 12 | internal class Program 13 | { 14 | 15 | #if UseAsync 16 | 17 | public static async Task Main(string[] args) 18 | { 19 | Console.WriteLine("Generating Pdf file..."); 20 | 21 | string outputFile = Path.Combine("c:\\temp", "test.pdf"); 22 | File.Delete(outputFile); 23 | var pdfHost = new HtmlToPdfHost() 24 | { 25 | WebViewEnvironmentPath = "C:\\temp\\WebViewEnvironment" 26 | }; 27 | 28 | // full file path or url 29 | var result = await pdfHost.PrintToPdfAsync(Path.GetFullPath("./HtmlSampleFileLonger-SelfContained.html"), outputFile); 30 | 31 | if (result.IsSuccess) 32 | { 33 | Console.WriteLine("Opening Pdf file (async): " + outputFile); 34 | ShellUtils.OpenUrl(outputFile); 35 | }else 36 | { 37 | Console.WriteLine("Pdf generation failed: " + result.Message); 38 | } 39 | } 40 | #else 41 | // Use Events 42 | public static void Main(string[] args) 43 | { 44 | Console.WriteLine("Generating Pdf file..."); 45 | 46 | string outputFile = Path.Combine("c:\\temp", "test.pdf"); 47 | File.Delete(outputFile); 48 | 49 | // Using the non-extended version of the host (no TOC support) 50 | var pdfHost = new HtmlToPdfHost() 51 | { 52 | WebViewEnvironmentPath = "C:\\temp\\WebViewEnvironment" 53 | }; 54 | var onPrintResult = (PdfPrintResult result) => { 55 | if (result.IsSuccess) 56 | { 57 | Console.WriteLine("Opening Pdf file (Callback): " + outputFile); 58 | ShellUtils.OpenUrl(outputFile); 59 | } 60 | else 61 | { 62 | Console.WriteLine("Pdf generation failed: " + result.Message); 63 | } 64 | 65 | Environment.Exit(0); 66 | }; 67 | 68 | // full file path or url 69 | pdfHost.PrintToPdf(Path.GetFullPath("./HtmlSampleFileLonger-SelfContained.html"), outputFile, onPrintResult); 70 | 71 | // wait for completion 72 | Console.ReadKey(); 73 | } 74 | #endif 75 | } 76 | } -------------------------------------------------------------------------------- /Samples/ConsoleHtmlToPdfSample/Properties/launchsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ConsoleApp1": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /Samples/WpfSample/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Samples/WpfSample/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using MahApps.Metro.Controls; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Configuration; 5 | using System.Data; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Media; 10 | 11 | namespace WpfSample 12 | { 13 | /// 14 | /// Interaction logic for App.xaml 15 | /// 16 | public partial class App : Application 17 | { 18 | public static string InitialStartDirectory { get; set; } 19 | 20 | public App() 21 | { 22 | InitialStartDirectory = Environment.CurrentDirectory; 23 | } 24 | } 25 | 26 | 27 | public class ThemeOverride 28 | { 29 | 30 | public static void SetThemeWindowOverride(MetroWindow window, string forceTheme = null) 31 | { 32 | if (string.IsNullOrEmpty(forceTheme)) 33 | forceTheme = "Dark"; 34 | 35 | if (forceTheme == "Dark") 36 | { 37 | window.WindowTitleBrush = (SolidColorBrush)new BrushConverter().ConvertFrom("#333"); 38 | window.NonActiveWindowTitleBrush = (SolidColorBrush)new BrushConverter().ConvertFrom("#222"); 39 | window.BorderBrush = (SolidColorBrush)new BrushConverter().ConvertFrom("#494949"); 40 | } 41 | else 42 | { 43 | // Light theme 44 | window.WindowTitleBrush = (SolidColorBrush)new BrushConverter().ConvertFrom("#CBD4EF"); 45 | window.NonActiveWindowTitleBrush = (SolidColorBrush)(new BrushConverter().ConvertFrom("#CBD4EF")); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Samples/WpfSample/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /Samples/WpfSample/Assets/MarkdownMonster_Icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickStrahl/Westwind.WebView/cbf7323a89ba99a91ce1a395f80dcf29a6899d12/Samples/WpfSample/Assets/MarkdownMonster_Icon_256.png -------------------------------------------------------------------------------- /Samples/WpfSample/BasicInterop.xaml: -------------------------------------------------------------------------------- 1 |  20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Ready 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Samples/WpfSample/BasicInterop.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data.Common; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | using System.Windows.Data; 13 | using System.Windows.Documents; 14 | using System.Windows.Input; 15 | using System.Windows.Media; 16 | using System.Windows.Media.Imaging; 17 | using System.Windows.Shapes; 18 | using MahApps.Metro.Controls; 19 | using Microsoft.Web.WebView2.Wpf; 20 | using Westwind.WebView.Wpf; 21 | 22 | namespace WpfSample 23 | { 24 | /// 25 | /// Interaction logic for BasicInterop.xaml 26 | /// 27 | public partial class BasicInterop : MetroWindow 28 | { 29 | BasicInteropWebviewHandler WebViewHandler { get; } 30 | 31 | 32 | public BasicInterop() 33 | { 34 | InitializeComponent(); 35 | ThemeOverride.SetThemeWindowOverride(this, "Dark"); 36 | 37 | Model = new BasicInteropModel(); 38 | DataContext = Model; 39 | 40 | Loaded += BasicInterop_Loaded; 41 | 42 | 43 | #if DEBUG 44 | // for debug use your actual dev source path for the HTML content so you can F5 reload without restart 45 | var previewPath = @"d:\projects\Libraries\Westwind.WebView\WpfSample\PreviewThemes"; 46 | if (!Directory.Exists(previewPath)) 47 | { 48 | previewPath = System.IO.Path.Combine(App.InitialStartDirectory, "PreviewThemes"); // production folder 49 | } 50 | #else 51 | // Production uses the runtime location which only updates on compile (unless you change in place) 52 | var previewPath = System.IO.Path.Combine(App.InitialStartDirectory,"PreviewThemes"); // production folder 53 | #endif 54 | 55 | // page to load in WebView 56 | var url = "https://WebViewSample.basicinterop/Interop.html"; 57 | url += "?mode=dark"; // pass dark theme in (light without this) 58 | 59 | WebViewHandler = new BasicInteropWebviewHandler(WebBrowser, 60 | System.IO.Path.Combine(System.IO.Path.GetTempPath(), "WpfSample_WebView")) 61 | { 62 | // virutal host name for the folder 63 | HostWebHostNameForFolder = "WebViewSample.basicinterop", 64 | HostWebRootFolder = previewPath, 65 | ShowDevTools = true, // show dev tools on startup 66 | 67 | // if using a custom interop handler assign and configure here 68 | JsInterop = new BasicInteropWebViewInterop(WebBrowser), 69 | 70 | // Initial page to load after loading is complete - ensures no invalid URL before host is assigned 71 | InitialUrl = url 72 | }; 73 | 74 | } 75 | 76 | public BasicInteropModel Model { get; set; } 77 | 78 | private void BasicInterop_Loaded(object sender, RoutedEventArgs e) 79 | { 80 | if (Owner != null) 81 | { 82 | Top = Owner.Top + 65; 83 | Left = Owner.Left + 60; 84 | } 85 | } 86 | 87 | private void Button_Click(object sender, RoutedEventArgs e) 88 | { 89 | 90 | } 91 | 92 | private async void ButtonUpdate_Click(object sender, RoutedEventArgs e) 93 | { 94 | await WebViewHandler.JsInterop.UpdatePerson(Model.Person); 95 | 96 | } 97 | } 98 | 99 | #region Model Data 100 | 101 | public class BasicInteropModel : INotifyPropertyChanged 102 | { 103 | public Person Person { get; set; } = new Person(); 104 | 105 | public event PropertyChangedEventHandler PropertyChanged; 106 | 107 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 108 | { 109 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 110 | } 111 | 112 | protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) 113 | { 114 | if (EqualityComparer.Default.Equals(field, value)) return false; 115 | field = value; 116 | OnPropertyChanged(propertyName); 117 | return true; 118 | } 119 | } 120 | 121 | 122 | public class Person : INotifyPropertyChanged 123 | { 124 | private string _firstname = "Billy"; 125 | private string _lastname = "Bopp"; 126 | private string _email = "bopp@beebopp.com"; 127 | private string _company = "beebob.com"; 128 | private Address _address = new Address(); 129 | 130 | public string Firstname 131 | { 132 | get => _firstname; 133 | set 134 | { 135 | if (value == _firstname) return; 136 | _firstname = value; 137 | OnPropertyChanged(); 138 | } 139 | } 140 | 141 | public string Lastname 142 | { 143 | get => _lastname; 144 | set 145 | { 146 | if (value == _lastname) return; 147 | _lastname = value; 148 | OnPropertyChanged(); 149 | } 150 | } 151 | 152 | public string Company 153 | 154 | { 155 | get => _company; 156 | set 157 | { 158 | if (value == _company) return; 159 | _company = value; 160 | OnPropertyChanged(); 161 | } 162 | } 163 | 164 | public string Email 165 | { 166 | get => _email; 167 | set 168 | { 169 | if (value == _email) return; 170 | _email = value; 171 | OnPropertyChanged(); 172 | } 173 | } 174 | 175 | public Address Address 176 | { 177 | get => _address; 178 | set 179 | { 180 | if (Equals(value, _address)) return; 181 | _address = value; 182 | OnPropertyChanged(); 183 | } 184 | } 185 | 186 | 187 | public event PropertyChangedEventHandler PropertyChanged; 188 | 189 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 190 | { 191 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 192 | } 193 | 194 | protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) 195 | { 196 | if (EqualityComparer.Default.Equals(field, value)) return false; 197 | field = value; 198 | OnPropertyChanged(propertyName); 199 | return true; 200 | } 201 | } 202 | 203 | public class Address : INotifyPropertyChanged 204 | { 205 | private string _street = "123 Nowher Lane"; 206 | private string _city = "Anytown"; 207 | private string _state = "UT"; 208 | private string _country = "USA"; 209 | private string _postalCode = "12314"; 210 | 211 | public string Street 212 | { 213 | get => _street; 214 | set 215 | { 216 | if (value == _street) return; 217 | _street = value; 218 | OnPropertyChanged(); 219 | } 220 | } 221 | 222 | public string City 223 | { 224 | get => _city; 225 | set 226 | { 227 | if (value == _city) return; 228 | _city = value; 229 | OnPropertyChanged(); 230 | } 231 | } 232 | 233 | public string State 234 | { 235 | get => _state; 236 | set 237 | { 238 | if (value == _state) return; 239 | _state = value; 240 | OnPropertyChanged(); 241 | } 242 | } 243 | 244 | public string PostalCode 245 | 246 | { 247 | get => _postalCode; 248 | set 249 | { 250 | if (value == _postalCode) return; 251 | _postalCode = value; 252 | OnPropertyChanged(); 253 | } 254 | } 255 | 256 | public string Country 257 | { 258 | get => _country; 259 | set 260 | { 261 | if (value == _country) return; 262 | _country = value; 263 | OnPropertyChanged(); 264 | } 265 | } 266 | 267 | public event PropertyChangedEventHandler PropertyChanged; 268 | 269 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 270 | { 271 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 272 | } 273 | 274 | protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) 275 | { 276 | if (EqualityComparer.Default.Equals(field, value)) return false; 277 | field = value; 278 | OnPropertyChanged(propertyName); 279 | return true; 280 | } 281 | } 282 | 283 | #endregion 284 | 285 | 286 | #region WebView Handler 287 | 288 | public class BasicInteropWebviewHandler : WebViewHandler 289 | { 290 | public BasicInteropWebviewHandler(WebView2 webViewBrowser, string webViewEnvironmentFolder = null, 291 | object dotnetCallbackObject = null) : 292 | base(webViewBrowser, webViewEnvironmentFolder, dotnetCallbackObject) 293 | { 294 | JsInterop = new BasicInteropWebViewInterop(webViewBrowser); 295 | HostObject = JsInterop; 296 | } 297 | 298 | } 299 | 300 | 301 | public class BasicInteropWebViewInterop : BaseJavaScriptInterop 302 | { 303 | 304 | /// 305 | /// Must implement this constructor, so this type can be constructed 306 | /// 307 | /// 308 | /// 309 | public BasicInteropWebViewInterop(WebView2 webView, string baseInvocationTarget="window.page") : base(webView, baseInvocationTarget) 310 | { 311 | 312 | } 313 | 314 | 315 | public async Task UpdatePerson(Person person) 316 | { 317 | await Invoke("updatePerson", person); 318 | } 319 | 320 | } 321 | 322 | #endregion 323 | } 324 | -------------------------------------------------------------------------------- /Samples/WpfSample/EmojiWindow.xaml: -------------------------------------------------------------------------------- 1 |  17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 40 | 41 | 42 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Ready 71 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Samples/WpfSample/EmojiWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.IO; 4 | using System.Windows; 5 | using System.Windows.Input; 6 | using MahApps.Metro.Controls; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading.Tasks; 9 | using Microsoft.Web.WebView2.Wpf; 10 | using Westwind.WebView.Utilities; 11 | using Westwind.WebView.Wpf; 12 | 13 | 14 | namespace WpfSample 15 | { 16 | /// 17 | /// Example that demonstrates hosting a WebView control and 18 | /// interacting with script code in the page: 19 | /// 20 | /// * Search Term is captured in WPF 21 | /// * Value is passed into JavaScript (JsInterop) 22 | /// * JavaScript searches and filters the list displayed 23 | /// * Click is handled in JavaScript 24 | /// * Result is passed back out into WPF (JsInterop) 25 | /// 26 | /// 27 | public partial class EmojiWindow : MetroWindow, INotifyPropertyChanged 28 | { 29 | public bool Cancelled { get; set; } 30 | 31 | /// 32 | /// Search text is captured in WPF and passed into JavaScript 33 | /// 34 | /// Click is captured in JavaScript and fires event in WPF 35 | /// 36 | public string SearchText 37 | { 38 | get { return _searchText; } 39 | set 40 | { 41 | if (value == _searchText) return; 42 | WebViewHandler.InitialSearchText = value; 43 | 44 | _searchText = value; 45 | OnPropertyChanged(); 46 | } 47 | } 48 | 49 | private string _searchText; 50 | 51 | public EmojiWebViewHandler WebViewHandler { get; set; } 52 | 53 | 54 | public string EmojiString 55 | { 56 | get { return _emojiString; } 57 | set 58 | { 59 | if (value == _emojiString) return; 60 | _emojiString = value; 61 | OnPropertyChanged(nameof(EmojiString)); 62 | } 63 | } 64 | 65 | private string _emojiString; 66 | 67 | 68 | public EmojiWindow() 69 | { 70 | 71 | InitializeComponent(); 72 | 73 | DataContext = this; 74 | ThemeOverride.SetThemeWindowOverride(this, "Dark"); 75 | 76 | #if DEBUG 77 | // for debug use your actual dev source path for the HTML content so you can F5 reload without restart 78 | var previewPath = @"d:\projects\Libraries\Westwind.WebView\WpfSample\PreviewThemes"; 79 | if (!Directory.Exists(previewPath)) 80 | { 81 | previewPath = System.IO.Path.Combine(App.InitialStartDirectory, "PreviewThemes"); // production folder 82 | } 83 | #else 84 | // Production uses the runtime location which only updates on compile (unless you change in place) 85 | var previewPath = System.IO.Path.Combine(App.InitialStartDirectory,"PreviewThemes"); // production folder 86 | #endif 87 | 88 | // page to load in WebView 89 | var url = "https://markdownmonster.emoji/_EmojiViewer.html"; 90 | url += "?mode=dark"; // pass dark theme in (light without this) 91 | 92 | WebViewHandler = new EmojiWebViewHandler(WebBrowser, 93 | System.IO.Path.Combine(Path.GetTempPath(), "WpfSample_WebView")) 94 | { 95 | // virutal host name for the folder 96 | HostWebHostNameForFolder = "markdownmonster.emoji", 97 | HostWebRootFolder = previewPath, 98 | ShowDevTools = false, // show dev tools on startup 99 | 100 | // if using a custom interop handler assign and configure here 101 | JsInterop = new EmojiWebViewInterop(WebBrowser), 102 | 103 | // Initial page to load after loading is complete - ensures no invalid URL before host is assigned 104 | InitialUrl = url 105 | }; 106 | 107 | Loaded += EmojiWindow_Loaded; 108 | 109 | 110 | } 111 | 112 | private void EmojiWindow_Loaded(object sender, RoutedEventArgs e) 113 | { 114 | if (Owner != null) 115 | { 116 | Top = Owner.Top + 45; 117 | Left = Owner.Left + 30; 118 | } 119 | TextSearchText.Focus(); 120 | } 121 | 122 | private void EmojiWindow_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) 123 | { 124 | if (e.Key == System.Windows.Input.Key.Escape) 125 | { 126 | Cancelled = true; 127 | Close(); 128 | } 129 | } 130 | 131 | private void TextSearchText_Keydown(object sender, KeyEventArgs e) 132 | { 133 | 134 | if (e.Key == Key.Escape) 135 | { 136 | if (!string.IsNullOrEmpty(SearchText)) 137 | { 138 | SearchText = null; 139 | e.Handled = true; 140 | } 141 | } 142 | 143 | 144 | 145 | } 146 | 147 | private void TextSearchText_Keyup(object sender, KeyEventArgs e) 148 | { 149 | if (e.Key == Key.Back || e.Key == Key.Delete) 150 | { 151 | WebViewHandler.JsInterop.SearchEmoji(SearchText).FireAndForget(); 152 | } 153 | 154 | WebViewHandler.JsInterop.SearchEmoji(SearchText).FireAndForget(); 155 | } 156 | 157 | 158 | 159 | public event PropertyChangedEventHandler PropertyChanged; 160 | 161 | 162 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 163 | { 164 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 165 | } 166 | 167 | 168 | public void SelectEmoji(string emojiValue) 169 | { 170 | EmojiString = emojiValue; 171 | if (!DialogResult.HasValue) 172 | DialogResult = true; 173 | Close(); 174 | } 175 | 176 | private void BtnDebugger_OnClick(object sender, RoutedEventArgs e) 177 | { 178 | WebViewHandler.WebBrowser?.CoreWebView2?.OpenDevToolsWindow(); 179 | } 180 | } 181 | 182 | 183 | #region WebView Handler 184 | 185 | 186 | /// 187 | /// Custom WebView Handler Implementation that overrides some default behaviors 188 | /// 189 | /// Subclassing here allows you to isolate the WebView specific behaviors 190 | /// from the main part of your form. You can pass in relevant data, or 191 | /// your model, or form here if necessary to get access to your app state. 192 | /// 193 | public class EmojiWebViewHandler : WebViewHandler 194 | { 195 | public string InitialSearchText { get; set; } 196 | 197 | 198 | public EmojiWebViewHandler(WebView2 webViewBrowser, string webViewEnvironmentFolder = null, 199 | object dotnetCallbackObject = null) : 200 | base(webViewBrowser, webViewEnvironmentFolder, dotnetCallbackObject) 201 | { 202 | //JsInterop = new EmojiWebViewInterop(webViewBrowser); 203 | HostObject = JsInterop; 204 | } 205 | 206 | protected override async void OnDomContentLoaded(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e) 207 | { 208 | base.OnDomContentLoaded(sender,e); 209 | 210 | if (!string.IsNullOrEmpty(InitialSearchText)) 211 | { 212 | await JsInterop.SearchEmoji(InitialSearchText); 213 | } 214 | } 215 | } 216 | 217 | /// 218 | /// This class handles all interop between JavaScript and .NET. Typically 219 | /// I would break these out into two classes for .NET -> JavaScript and JavaScript->.NET 220 | /// but since we have only 1 method each a single class suffices to handle both. 221 | /// 222 | /// The JS Interop allows making `Invoke` calls to call into JavaScript methods using 223 | /// provided base interop target in JS code (in this case `window.page`) on which methods 224 | /// are invoked. Invoke calls allow passing complex data which is serialized into JSON 225 | /// automatically. 226 | /// 227 | /// Callbacks are just methods that receive results from JS code. Callback parameters 228 | /// need to be simple type values and any complex data has to be passed back as JSON. 229 | /// 230 | public class EmojiWebViewInterop : BaseJavaScriptInterop 231 | { 232 | /// 233 | /// must implement this constructor! 234 | /// 235 | /// 236 | /// 237 | public EmojiWebViewInterop(WebView2 webView, string baseInvocationTarget = "window.page" ) : base(webView, baseInvocationTarget) 238 | { 239 | 240 | } 241 | 242 | 243 | /// 244 | /// All calls into JS code are async! 245 | /// 246 | /// 247 | /// 248 | public async Task SearchEmoji(string searchText) 249 | { 250 | await Invoke("searchEmoji", searchText); 251 | } 252 | 253 | 254 | 255 | /// 256 | /// Callback that receives the selected Emoji value on a click, 257 | /// 258 | /// 259 | public void EmojiUpdated(string emojiValue) 260 | { 261 | var window = WebBrowser.TryFindParent() as EmojiWindow; 262 | window.SelectEmoji(emojiValue); 263 | } 264 | 265 | 266 | } 267 | 268 | #endregion 269 | } 270 | -------------------------------------------------------------------------------- /Samples/WpfSample/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 17 | 18 | 27 | 28 | 36 | 39 | 40 | 41 | 42 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Samples/WpfSample/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | using System.Runtime.CompilerServices; 4 | using System.Windows; 5 | using MahApps.Metro.Controls; 6 | 7 | namespace WpfSample 8 | { 9 | /// 10 | /// Interaction logic for MainWindow.xaml 11 | /// 12 | public partial class MainWindow : MetroWindow 13 | { 14 | private MainWindowModel Model = new MainWindowModel(); 15 | 16 | public MainWindow() 17 | { 18 | InitializeComponent(); 19 | 20 | ThemeOverride.SetThemeWindowOverride(this, "Dark"); 21 | DataContext = Model; 22 | } 23 | 24 | 25 | private void BtnEmoji_OnClick(object sender, RoutedEventArgs e) 26 | { 27 | Model.EmojiForm = new EmojiWindow(); 28 | Model.EmojiForm.Owner = this; 29 | Model.EmojiForm.SearchText = Model.EmojiSearchText; 30 | 31 | var result = Model.EmojiForm.ShowDialog(); 32 | 33 | if (result == true) 34 | { 35 | MessageBox.Show(this, Model.EmojiForm.EmojiString, "Emoji Picker Selection", MessageBoxButton.OK, 36 | MessageBoxImage.Information); 37 | } 38 | } 39 | 40 | 41 | private void BtnSimple_OnClick(object sender, RoutedEventArgs e) 42 | { 43 | Model.BasicInteropForm = new BasicInterop(); 44 | Model.BasicInteropForm.Owner = this; 45 | Model.BasicInteropForm.Show(); 46 | } 47 | 48 | 49 | private void BtnTest_OnClick(object sender, RoutedEventArgs e) 50 | { 51 | var form = new StringNavigationIssues(); 52 | form.Show(); 53 | } 54 | } 55 | 56 | 57 | 58 | 59 | public class MainWindowModel : INotifyPropertyChanged 60 | { 61 | public EmojiWindow EmojiForm { get; set; } 62 | 63 | public BasicInterop BasicInteropForm { get; set; } 64 | 65 | public string EmojiSearchText 66 | { 67 | get => _emojiSearchText; 68 | set 69 | { 70 | if (value == _emojiSearchText) return; 71 | _emojiSearchText = value; 72 | OnPropertyChanged(); 73 | } 74 | } 75 | private string _emojiSearchText; 76 | 77 | 78 | public event PropertyChangedEventHandler PropertyChanged; 79 | 80 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 81 | { 82 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 83 | } 84 | 85 | protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) 86 | { 87 | if (EqualityComparer.Default.Equals(field, value)) return false; 88 | field = value; 89 | OnPropertyChanged(propertyName); 90 | return true; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Samples/WpfSample/PreviewThemes/EmojiViewer.js: -------------------------------------------------------------------------------- 1 | var page = { 2 | 3 | dotnet: { 4 | getDotnet: function() { 5 | return window.chrome.webview.hostObjects.sync.dotnet; 6 | }, 7 | getDotnetAsync: function() { 8 | return window.chrome.webview.hostObjects.dotnet; 9 | }, 10 | isDotnet: function() { 11 | return window.chrome.webview.hostObjects ? true : false; 12 | }, 13 | emojiUpdated: function(iconText){ 14 | page.dotnet.getDotnet().EmojiUpdated(iconText ?? ''); 15 | } 16 | }, 17 | initialize: function(mode) { 18 | $(".emoji-container").on("click", "a", function() { 19 | var $el = $(this); 20 | var iconText = this.title; 21 | 22 | if (iconText){ 23 | if (page.dotnet.isDotnet()) { 24 | page.dotnet.emojiUpdated(iconText ?? ''); 25 | } 26 | else 27 | alert('selected ' + iconText); 28 | } 29 | }); 30 | $("#Search").focus(); 31 | 32 | var keyupFn = debounce(page.searchEmoji, 150); 33 | $("#Search") 34 | .keyup( keyupFn ) 35 | .keydown(function (e) { 36 | var key = e.keyCode; 37 | if (key == 8 || key == 46) // backspace 38 | keyupFn(); 39 | }); 40 | 41 | 42 | 43 | 44 | }, 45 | searchEmoji: function searchEmoji(search) { 46 | if (typeof search !== "string") 47 | search = this.value; 48 | 49 | $(".emoji-container>a").show(); 50 | if (!search) { 51 | return; 52 | } 53 | 54 | $(".emoji-container>a").each(function() { 55 | var title = this.title; 56 | 57 | if (title.toLowerCase().indexOf(search.toLowerCase()) > -1) 58 | return; 59 | 60 | this.style.display = "none"; 61 | }); 62 | } 63 | }; // page 64 | 65 | 66 | $.expr[":"].containsNoCase = function(el, i, m) { 67 | var search = m[3]; 68 | if (!search) return false; 69 | return new RegExp(search, "i").test($(el).text()); 70 | }; 71 | page.initialize(); 72 | 73 | function initializeInterop() { 74 | 75 | } 76 | 77 | 78 | function debounce(func, wait, immediate) { 79 | var timeout; 80 | return function () { 81 | var context = this, args = arguments; 82 | var later = function () { 83 | timeout = null; 84 | if (!immediate) func.apply(context, args); 85 | }; 86 | var callNow = immediate && !timeout; 87 | clearTimeout(timeout); 88 | timeout = setTimeout(later, wait); 89 | if (callNow) 90 | func.apply(context, args); 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /Samples/WpfSample/PreviewThemes/Interop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Markdown Monster Emoji Picker 8 | 9 | 21 | 22 | 23 |
24 |

Hello World

25 |

26 | Html Sample Page using VueJs 27 |

28 | 29 |
30 | 31 |
address:
32 | 33 |
{{ person.firstname}} {{person.lastname}}
34 |
{{ person.company}}
35 |
36 |
{{ person.address.street }}
37 |
{{ person.address.city}}
38 |
39 |
40 | 41 |
42 |              {{ person}}
43 |             
44 |
45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Samples/WpfSample/PreviewThemes/Interop.js: -------------------------------------------------------------------------------- 1 | import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js' 2 | 3 | window.page = { 4 | person: { 5 | firstname: "Rick", 6 | lastname: "Strahl", 7 | company: "West Wind", 8 | email: "", 9 | address: { 10 | street: "111 Blue Skies Blvd.", 11 | city: "Paia, Hawaii", 12 | state: "", 13 | zip: "", 14 | country: "" 15 | } 16 | }, 17 | updatePerson(person) { 18 | Object.assign(window.page.person, person); 19 | console.log(person, window.page.person); 20 | }, 21 | // .NET calls this to explicitly retrieve the value 22 | getPerson() { 23 | return window.page.person; 24 | } 25 | } 26 | 27 | // *** Initialize Vue for the Page *** // 28 | var vueApp; 29 | 30 | window.Initialize = function() { 31 | vueApp = new Vue({ 32 | el: '#app', 33 | data: function () { 34 | var model = { person: window.page.person }; 35 | return model; 36 | }, 37 | }); 38 | } 39 | 40 | window.Initialize(); 41 | -------------------------------------------------------------------------------- /Samples/WpfSample/PreviewThemes/Westwind/Theme.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | color: #333; 3 | background: white; 4 | font-family: -apple-system, BlinkMacSystemFont,"Segoe UI", Helvetica, Helvetica, Arial, sans-serif, "Segoe UI Emoji", "Apple Color Emoji"; 5 | font-size: 17.25px; 6 | line-height: 1.5; 7 | -webkit-font-smoothing: antialiased; 8 | height: 100%; 9 | margin: 0; 10 | } 11 | 12 | body::-webkit-scrollbar { 13 | width: 0.6em; 14 | /* background: #eee; */ 15 | } 16 | 17 | body::-webkit-scrollbar-thumb { 18 | background: #999; 19 | border-radius: 0.2em; 20 | } 21 | 22 | 23 | #MainContent { 24 | margin: 20px 25px 0 40px; 25 | padding-bottom: 20px; 26 | color: #555; 27 | } 28 | 29 | h1, h2, h3, h4, h5, h6, .byline, .content-title { 30 | color: steelblue; 31 | font-weight: 600; 32 | margin-top: 1.2em; 33 | margin-bottom: 0.3em; 34 | } 35 | 36 | h1 { 37 | font-size: 1.8em; 38 | padding-bottom: 10px; 39 | border-bottom: 1px solid #eee; 40 | } 41 | 42 | h2 { 43 | font-size: 1.6em; 44 | } 45 | 46 | h3 { 47 | font-size: 1.35em; 48 | } 49 | 50 | h4 { 51 | color: #555; 52 | font-size: 1.18em; 53 | } 54 | 55 | h5, h6 { 56 | color: #606060; 57 | font-size: 1.09em; 58 | } 59 | 60 | h6 { 61 | color: #707070; 62 | font-size: 1.03em; 63 | } 64 | 65 | 66 | p { 67 | margin: 0.5rem 0 1rem 0; 68 | } 69 | 70 | a { 71 | font-weight: 600; 72 | text-decoration: none; 73 | color: #4582B4; 74 | } 75 | 76 | a:hover { 77 | text-decoration: underline; 78 | } 79 | 80 | img { 81 | max-width: 100%; 82 | } 83 | 84 | ul, ol { 85 | margin: 0.7em 0; 86 | } 87 | 88 | li { 89 | margin: 0.4em 0.2em 0.4em 1em; 90 | } 91 | 92 | ul > li > ul > li > ul > li { 93 | list-style: disc; 94 | } 95 | 96 | ul > li > ul > li { 97 | list-style: square; 98 | } 99 | 100 | .task-list-item { 101 | list-style: none; 102 | margin-left: -2em; 103 | } 104 | 105 | /* definition lists can be toggled*/ 106 | dt { 107 | font-size: 1.08em; 108 | font-weight: bold; 109 | text-decoration: underline; 110 | padding-top: 0.5em; 111 | cursor: pointer; 112 | } 113 | 114 | dd { 115 | margin: 0.1em; 116 | padding: 0 0 0.2em 1em; 117 | } 118 | 119 | b, strong { 120 | font-weight: 600; 121 | } 122 | 123 | 124 | 125 | 126 | @media(min-width: 1080px) { 127 | html, body { 128 | font-size: 1.06em; 129 | } 130 | } 131 | 132 | 133 | blockquote { 134 | background: #f2f7fb; 135 | font-size: 1.02em; 136 | padding: 10px 20px; 137 | margin: 1.2em; 138 | border-left: 9px #569ad4 solid; 139 | border-radius: 4px 0 0 4px; 140 | } 141 | 142 | blockquote *:first-child { 143 | margin-top: 0; 144 | } 145 | 146 | blockquote *:last-child { 147 | margin-bottom: 0; 148 | } 149 | 150 | 151 | hr { 152 | margin: 12px 0; 153 | } 154 | 155 | .figure .caption, figure figcaption { 156 | font-size: 0.8em; 157 | font-style: italic; 158 | margin-top: 0; 159 | } 160 | 161 | code { 162 | padding: 2px 5px; 163 | font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; 164 | border-radius: 3px; 165 | font-weight: 500; 166 | background: #eee; 167 | color: #555; 168 | } 169 | 170 | pre { 171 | font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; 172 | font-size: 0.875em !important; 173 | font-weight: normal; 174 | line-height: 1.45; 175 | margin: 7px 0 !important; 176 | padding: 0; 177 | border: 1px solid silver; 178 | border-radius: 4px; 179 | overflow-x: auto; 180 | white-space: pre; /* pre-line if you want to wrap*/ 181 | word-break: break-all; 182 | 183 | /* handles non-formatted code snippets, but won't work with light syntax */ 184 | /* background: #252525; 185 | color: #eee; */ 186 | } 187 | 188 | pre > code { 189 | padding: 0.8em !important; 190 | display: block; 191 | color: #f5f5f5 !important; 192 | 193 | white-space: pre; /* pre-line if you want to wrap*/ 194 | 195 | /* handles non-formatted code snippets, but won't work with light syntax */ 196 | /* background: transparent; 197 | font-weight: normal; */ 198 | } 199 | pre>code::-webkit-scrollbar { 200 | width: 8px; 201 | height: 8px; 202 | background: #444; 203 | } 204 | pre > code::-webkit-scrollbar-thumb { 205 | background:#999; 206 | } 207 | table { 208 | width: 100%; 209 | overflow: auto; 210 | border-spacing: 0; 211 | border-collapse: collapse; 212 | margin: 15px 0; 213 | border-color: gray; 214 | } 215 | 216 | td, th { 217 | border: 1px solid #ddd; 218 | padding: 6px 11px; 219 | display: table-cell; 220 | vertical-align: top; 221 | } 222 | 223 | th { 224 | font-weight: bold; 225 | color: white; 226 | background: #555; 227 | } 228 | 229 | tbody > tr:nth-child(even) { 230 | background: #eee; 231 | } 232 | 233 | .line-highlight { 234 | background: #e9f5ff !important; 235 | } 236 | 237 | p.line-highlight, h1.line-highlight, h2.line-highlight, h3.line-highlight, h4.line-highlight { 238 | border-radius: 4px; 239 | margin-left: -10px; 240 | margin-right: -10px; 241 | padding-left: 10px; 242 | padding-right: 10px; 243 | } 244 | 245 | code.line-highlight { 246 | background: #555 !important; 247 | } 248 | 249 | 250 | /* hidden display, but still part of document flow */ 251 | .hidden { 252 | display: none; 253 | } 254 | 255 | .hidden-nowidth { 256 | width: 0; 257 | } 258 | 259 | .visually-hidden { 260 | border: 0; 261 | clip: rect(0 0 0 0); 262 | height: 1px; 263 | margin: -1px; 264 | overflow: hidden; 265 | padding: 0; 266 | position: absolute; 267 | width: 1px; 268 | } 269 | 270 | .line-highlight { 271 | background: #f5f5f5; 272 | border-radius: 3px; 273 | } 274 | 275 | .text-success { 276 | color: lightgreen 277 | } 278 | 279 | .text-info { 280 | color: lightsteelblue 281 | } 282 | 283 | .text-error { 284 | color: firebrick 285 | } 286 | 287 | .text-warning { 288 | color: gold 289 | } 290 | 291 | 292 | /* DocFx Styles*/ 293 | .CAUTION, .IMPORTANT, .INFO, .TIP, .NOTE, .WARNING { 294 | padding: 0.1px 20px; 295 | margin: 15px 0; 296 | border-radius: 4px; 297 | } 298 | 299 | .CAUTION > h5, .IMPORTANT > h5, .INFO > h5, .TIP > h5, .NOTE > h5, .WARNING > h5 { 300 | color: inherit; 301 | } 302 | 303 | .CAUTION, .IMPORTANT { 304 | color: #a94442; 305 | background-color: #f2dede; 306 | border-color: #ebccd1; 307 | } 308 | 309 | .WARNING { 310 | color: #8a6d3b; 311 | background-color: #fcf8e3; 312 | border-color: #faebcc; 313 | } 314 | 315 | .INFO, .TIP, .NOTE { 316 | color: #31708f; 317 | background-color: #d9edf7; 318 | border-color: #bce8f1; 319 | 320 | } 321 | 322 | .NOTE h5:before, .TIP h5:before { 323 | content: "\f05a"; 324 | font-family: FontAwesome; 325 | padding-right: 6px; 326 | } 327 | .WARNING h5:before, .CAUTION h5:before { 328 | content: "\f071"; 329 | font-family: FontAwesome; 330 | padding-right: 6px; 331 | } 332 | 333 | .IMPORTANT h5:before { 334 | content: "\f06a"; 335 | font-family: FontAwesome; 336 | padding-right: 6px; 337 | } 338 | xref { 339 | display: block; 340 | } 341 | 342 | 343 | 344 | 345 | @media(min-width: 1080px) { 346 | html, body { 347 | font-size: 1.1em; 348 | } 349 | } 350 | .alert-warning h5:before { 351 | content: "\e127"; 352 | } 353 | 354 | 355 | 356 | /* for PDF generation and print output */ 357 | @media print { 358 | html, body { 359 | font-family: "Segoe UI Emoji", "Apple Color Emoji", -apple-system, BlinkMacSystemFont,"Segoe UI", Helvetica, Helvetica, Arial, sans-serif; 360 | text-rendering: optimizeLegibility; 361 | height: auto; 362 | } 363 | pre { 364 | white-space: pre-wrap; 365 | word-break: normal; 366 | word-wrap: normal; 367 | } 368 | 369 | pre > code { 370 | white-space: pre-wrap; 371 | padding: 1em !important; 372 | 373 | /* match highlightjs theme colors - must override for override */ 374 | background: #1E1E1E; 375 | color: #DCDCDC; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /Samples/WpfSample/PreviewThemes/Westwind/Theme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {$extraHeaders} 20 | 21 | 22 | 23 |
24 | 25 | {$markdownHtml} 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Samples/WpfSample/StringNavigationIssues.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Samples/WpfSample/StringNavigationIssues.xaml.cs: -------------------------------------------------------------------------------- 1 |  2 | using Newtonsoft.Json; 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using MahApps.Metro.Controls; 9 | using Microsoft.Web.WebView2.Wpf; 10 | using Westwind.Utilities; 11 | 12 | namespace WpfSample 13 | { 14 | /// 15 | /// Interaction logic for Window1.xaml 16 | /// 17 | public partial class StringNavigationIssues : MetroWindow 18 | { 19 | public StringNavigationIssues() 20 | { 21 | 22 | 23 | InitializeComponent(); 24 | 25 | //File.WriteAllText("WebContent\\LargeEmbeddedImage.html", html); 26 | //var file = System.IO.Path.GetFullPath("WebContent\\LargeEmbeddedImage.html"); 27 | 28 | WebView.DefaultBackgroundColor = System.Drawing.Color.Silver; 29 | WebView.Source = new Uri(Path.GetFullPath("WebContent\\ExportedHtml.html")); 30 | } 31 | 32 | private void OpenFromString_Click(object sender, RoutedEventArgs e) 33 | { 34 | // 3mb string 35 | var fileContent = HtmlUtils.BinaryToEmbeddedBase64(File.ReadAllBytes( 36 | "WebContent\\LargeImage.jpg"), "image/jpeg"); 37 | 38 | var html = 39 | $""" 40 | 41 | 42 |

Embedded Large Image

43 | Large Image 44 | 45 | 46 | """; 47 | 48 | WebView.NavigateToString(html); 49 | } 50 | 51 | private async void OpenFromStringSafe_Click(object sender, RoutedEventArgs e) 52 | { 53 | // 3mb string 54 | var fileContent = HtmlUtils.BinaryToEmbeddedBase64(File.ReadAllBytes( 55 | "WebContent\\LargeImage.jpg"), "image/jpeg"); 56 | 57 | var html = 58 | $""" 59 | 60 | 61 |

Embedded Large Image

62 | Large Image 63 | 64 | 65 | """; 66 | 67 | await NavigateToString(html); 68 | //await WebView.NavigateToStringSafe(html); 69 | } 70 | 71 | private void OpenFromStringFile_Click(object sender, RoutedEventArgs e) 72 | { 73 | var file = System.IO.Path.GetFullPath("WebContent\\RenderedHtml.html"); 74 | // 3mb string 75 | var fileContent = HtmlUtils.BinaryToEmbeddedBase64(File.ReadAllBytes( 76 | "WebContent\\LargeImage.jpg"), "image/jpeg"); 77 | 78 | var html = 79 | $""" 80 | 81 | 82 |

Embedded Large Image

83 | Large Image 84 | 85 | 86 | """; 87 | 88 | NavigateToStringFile(html); 89 | } 90 | 91 | async Task NavigateToString(string html) 92 | { 93 | WebView.Source = new Uri("about:blank"); 94 | 95 | string encodedHtml = JsonConvert.SerializeObject(html); 96 | string script = "window.document.write(" + encodedHtml + ")"; 97 | 98 | await WebView.EnsureCoreWebView2Async(); // make sure WebView is ready 99 | await WebView.ExecuteScriptAsync(script); 100 | } 101 | 102 | void NavigateToStringFile(string html) 103 | { 104 | var file = Path.Combine(Path.GetTempPath(), "MyApp_RenderedHtml.html"); 105 | File.WriteAllText(file, html); 106 | WebView.Source = new Uri(file); 107 | } 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /Samples/WpfSample/WebContent/LargeImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickStrahl/Westwind.WebView/cbf7323a89ba99a91ce1a395f80dcf29a6899d12/Samples/WpfSample/WebContent/LargeImage.jpg -------------------------------------------------------------------------------- /Samples/WpfSample/WpfSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net9.0-windows 6 | disable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | PreserveNewest 52 | 53 | 54 | PreserveNewest 55 | 56 | 57 | PreserveNewest 58 | 59 | 60 | PreserveNewest 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | PreserveNewest 67 | 68 | 69 | PreserveNewest 70 | 71 | 72 | PreserveNewest 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Westwind.WebView.Test/HtmlToPdf/HtmlToPdfTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using Westwind.Utilities; 7 | using Westwind.WebView.HtmlToPdf; 8 | 9 | namespace Westwind.WebView.Test.HtmlToPdf 10 | { 11 | 12 | [TestClass] 13 | public class HtmlToPdfTests 14 | { 15 | /// 16 | /// Async Result operation - to file 17 | /// 18 | [TestMethod] 19 | public async Task PrintToPdfFileAsyncTest() 20 | { 21 | // File or URL to render 22 | //var url = "file:///C:/temp/TMPLOCAL/_MarkdownMonster_Preview.html"; 23 | //var url = "C:\\temp\\TestReport.html"; 24 | var url = Path.GetFullPath("./HtmlToPdf/HtmlSampleFileLonger-SelfContained.html"); 25 | 26 | 27 | var htmlFile = url; 28 | var outputFile = Path.GetFullPath(@".\test2.pdf"); 29 | 30 | File.Delete(outputFile); 31 | 32 | var host = new HtmlToPdfHost() 33 | { 34 | BackgroundHtmlColor = "#ffffff" 35 | }; 36 | host.CssAndScriptOptions.KeepTextTogether = true; 37 | //host.CssAndScriptOptions.CssToInject = "h1 { color: red } h2 { color: green } h3 { color: goldenrod }"; 38 | 39 | var pdfPrintSettings = new WebViewPrintSettings() 40 | { 41 | // margins are 0.4F default 42 | MarginTop = 0.5, 43 | MarginBottom = 0.3F, 44 | //ScaleFactor = 0.9F, 45 | 46 | ShouldPrintHeaderAndFooter = true, 47 | HeaderTitle = "Custom Header (centered)", 48 | FooterText = "Custom Footer (lower right)", 49 | 50 | // Optionally customize the header and footer completely - WebView syntax 51 | // HeaderTemplate = "
", 52 | // FooterTemplate = "
of " + 53 | // "
", 54 | 55 | GenerateDocumentOutline = true // default 56 | }; 57 | 58 | // output file is created 59 | var result = await host.PrintToPdfAsync(htmlFile, outputFile, pdfPrintSettings); 60 | 61 | Assert.IsTrue(result.IsSuccess, result.Message); 62 | ShellUtils.OpenUrl(outputFile); // display it 63 | } 64 | 65 | 66 | 67 | /// 68 | /// Async Result Operation - to stream 69 | /// 70 | [TestMethod] 71 | public async Task PrintToPdfStreamAsyncTest() 72 | { 73 | var outputFile = Path.GetFullPath(@".\test3.pdf"); 74 | var htmlFile = Path.GetFullPath("./HtmlToPdf/HtmlSampleFileLonger-SelfContained.html"); 75 | 76 | var host = new HtmlToPdfHost() 77 | { 78 | BackgroundHtmlColor = "#ffffff" 79 | }; 80 | host.CssAndScriptOptions.KeepTextTogether = true; 81 | 82 | var pdfPrintSettings = new WebViewPrintSettings() 83 | { 84 | // margins are 0.4F default 85 | MarginTop = 0.5, 86 | MarginBottom = 0.3F, 87 | ScaleFactor = 0.9F, // 1 is default 88 | 89 | ShouldPrintHeaderAndFooter = true, 90 | HeaderTitle = "Custom Header (centered)", 91 | FooterText = "Custom Footer (lower right)", 92 | 93 | // Optionally customize the header and footer completely - WebView syntax 94 | // HeaderTemplate = "
", 95 | // FooterTemplate = "
of " + 96 | // "
", 97 | 98 | GenerateDocumentOutline = true // default 99 | }; 100 | 101 | // We're interested in result.ResultStream 102 | var result = await host.PrintToPdfStreamAsync(htmlFile, pdfPrintSettings); 103 | 104 | Assert.IsTrue(result.IsSuccess, result.Message); 105 | Assert.IsNotNull(result.ResultStream); // THIS 106 | 107 | // Copy resultstream to output file 108 | File.Delete(outputFile); 109 | using (var fstream = new FileStream(outputFile, FileMode.OpenOrCreate, FileAccess.Write)) 110 | { 111 | result.ResultStream.CopyTo(fstream); 112 | result.ResultStream.Close(); // Close returned stream! 113 | } 114 | ShellUtils.OpenUrl(outputFile); 115 | } 116 | 117 | /// 118 | /// Event callback on completion - to stream (in-memory) 119 | /// 120 | /// 121 | /// Using async here only to facilitate waiting for completion. 122 | /// actual call does not require async calling method 123 | /// 124 | [TestMethod] 125 | public async Task PrintToPdfStreamTest() 126 | { 127 | // File or URL 128 | var htmlFile = Path.GetFullPath("./HtmlToPdf/HtmlSampleFile-SelfContained.html"); 129 | 130 | var tcs = new TaskCompletionSource(); 131 | 132 | var host = new HtmlToPdfHost(); 133 | Action onPrintComplete = (result) => 134 | { 135 | if (result.IsSuccess) 136 | { 137 | // create file so we can display 138 | var outputFile = Path.GetFullPath(@".\test1.pdf"); 139 | File.Delete(outputFile); 140 | 141 | using (var fstream = new FileStream(outputFile, FileMode.OpenOrCreate, FileAccess.Write)) 142 | { 143 | result.ResultStream.CopyTo(fstream); 144 | 145 | result.ResultStream.Close(); // Close returned stream! 146 | Assert.IsTrue(true); 147 | ShellUtils.OpenUrl(outputFile); 148 | } 149 | } 150 | else 151 | { 152 | Assert.Fail(result.Message); 153 | } 154 | 155 | tcs.SetResult(true); 156 | }; 157 | var pdfPrintSettings = new WebViewPrintSettings() 158 | { 159 | // default margins are 0.4F 160 | MarginBottom = 0.2F, 161 | MarginLeft = 0.2f, 162 | MarginRight = 0.2f, 163 | MarginTop = 0.4f, 164 | ScaleFactor = 0.8f, 165 | PageRanges = "1,2,5-8" 166 | }; 167 | // doesn't wait for completion 168 | host.PrintToPdfStream(htmlFile, onPrintComplete, pdfPrintSettings); 169 | 170 | 171 | // wait for completion 172 | await tcs.Task; 173 | } 174 | 175 | /// 176 | /// Event callback on completion - to file 177 | /// 178 | /// 179 | /// Using async here only to facilitate waiting for completion. 180 | /// actual call does not require async calling method 181 | /// 182 | [TestMethod] 183 | public async Task PrintToPdfFileTest() 184 | { 185 | // File or URL 186 | var htmlFile = Path.GetFullPath("./HtmlToPdf/HtmlSampleFile-SelfContained.html"); 187 | // Full Path to output file 188 | var outputFile = Path.GetFullPath(@".\test.pdf"); 189 | File.Delete(outputFile); 190 | 191 | var tcs = new TaskCompletionSource(); 192 | 193 | var host = new HtmlToPdfHost(); 194 | 195 | Action onPrintComplete = (result) => 196 | { 197 | if (result.IsSuccess) 198 | { 199 | Assert.IsTrue(true); 200 | ShellUtils.OpenUrl(outputFile); 201 | } 202 | else 203 | { 204 | Assert.Fail(result.Message); 205 | } 206 | 207 | tcs.SetResult(true); 208 | }; 209 | 210 | // doesn't wait for completion 211 | host.PrintToPdf(htmlFile, outputFile, onPrintComplete); 212 | 213 | // wait for completion 214 | await tcs.Task; 215 | } 216 | 217 | 218 | [TestMethod] 219 | public async Task InjectedCssTest() 220 | { 221 | var outputFile = Path.GetFullPath(@".\test3.pdf"); 222 | var htmlFile = Path.GetFullPath("./HtmlToPdf/HtmlSampleFileLonger-SelfContained.html"); 223 | 224 | var host = new HtmlToPdfHost(); 225 | //host.CssAndScriptOptions.KeepTextTogether = true; 226 | host.CssAndScriptOptions.OptimizePdfFonts = true; // force built-in OS fonts (Segoe UI, apple-system, Helvetica) 227 | host.CssAndScriptOptions.CssToInject = "h1 { color: red } h2 { color: green } h3 { color: goldenrod }"; 228 | 229 | // We're interested in result.ResultStream 230 | var result = await host.PrintToPdfStreamAsync(htmlFile); 231 | 232 | Assert.IsTrue(result.IsSuccess, result.Message); 233 | Assert.IsNotNull(result.ResultStream); // THIS 234 | 235 | // Copy resultstream to output file 236 | File.Delete(outputFile); 237 | using (var fstream = new FileStream(outputFile, FileMode.OpenOrCreate, FileAccess.Write)) 238 | { 239 | result.ResultStream.CopyTo(fstream); 240 | result.ResultStream.Close(); // Close returned stream! 241 | } 242 | ShellUtils.OpenUrl(outputFile); 243 | } 244 | 245 | 246 | [TestMethod] 247 | public async Task PrintToPdfDarkMarginsFileAsyncTest() 248 | { 249 | // File or URL to render 250 | //var url = "file:///C:/temp/TMPLOCAL/_MarkdownMonster_Preview.html"; 251 | //var url = "C:\\temp\\TestReport.html"; 252 | var url = Path.GetFullPath("./HtmlToPdf/HtmlSampleFileLonger-SelfContained.html"); 253 | 254 | 255 | var htmlFile = url; 256 | var outputFile = Path.GetFullPath(@".\test2.pdf"); 257 | 258 | File.Delete(outputFile); 259 | 260 | var host = new HtmlToPdfHost() 261 | { 262 | BackgroundHtmlColor = "#111" 263 | }; 264 | host.CssAndScriptOptions.KeepTextTogether = true; 265 | 266 | var pdfPrintSettings = new WebViewPrintSettings() 267 | { 268 | // margins are 0.4F default 269 | MarginTop = 0.5, 270 | MarginBottom = 0.5F, 271 | //ScaleFactor = 0.9F, 272 | 273 | // Custom Templates required for dark background so we can set text color 274 | ShouldPrintHeaderAndFooter = true, 275 | HeaderTemplate = "
", 276 | FooterTemplate = "
of " + 277 | "
", 278 | 279 | 280 | GenerateDocumentOutline = true // default 281 | }; 282 | 283 | // output file is created 284 | var result = await host.PrintToPdfAsync(htmlFile, outputFile, pdfPrintSettings); 285 | 286 | Assert.IsTrue(result.IsSuccess, result.Message); 287 | ShellUtils.OpenUrl(outputFile); // display it 288 | } 289 | 290 | 291 | [TestMethod] 292 | public void SettingsCultureJsonSerializationTests() 293 | { 294 | string expectedScale = "1.22"; 295 | // Arrange 296 | var settings = new DevToolsPrintToPdfSettings 297 | { 298 | scale = 1.22, 299 | 300 | }; 301 | CultureInfo.CurrentCulture = new CultureInfo("de-de"); 302 | 303 | // Act 304 | var json = settings.ToJson(); 305 | 306 | Console.WriteLine(json); 307 | 308 | // Assert 309 | Assert.IsTrue(json.Contains($"\"scale\": {expectedScale}")); 310 | } 311 | 312 | } 313 | } -------------------------------------------------------------------------------- /Westwind.WebView.Test/HtmlToPdf/PdfSampleFile.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickStrahl/Westwind.WebView/cbf7323a89ba99a91ce1a395f80dcf29a6899d12/Westwind.WebView.Test/HtmlToPdf/PdfSampleFile.pdf -------------------------------------------------------------------------------- /Westwind.WebView.Test/Westwind.WebView.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0-windows;net472 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | all 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | Always 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Westwind.WebView.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Westwind.WebView", "Westwind.WebView\Westwind.WebView.csproj", "{93CF86E2-D19B-442E-9117-B87376C55F80}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution Files", "{2632BA6B-BDBC-4573-8E54-A424FF12E4E3}" 9 | ProjectSection(SolutionItems) = preProject 10 | HtmlToPdf.md = HtmlToPdf.md 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Westwind.WebView.Test", "Westwind.WebView.Test\Westwind.WebView.Test.csproj", "{868E3D99-0F22-4149-87E0-1857F4414772}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{959E756A-5792-4E8E-9136-4B82798A2C28}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfSample", "Samples\WpfSample\WpfSample.csproj", "{638921E9-7DB1-474D-B721-8EFF16FDC797}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleHtmlToPdfSample", "Samples\ConsoleHtmlToPdfSample\ConsoleHtmlToPdfSample.csproj", "{E2032CDA-877E-41EA-A6BF-737B627E9724}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetHtmlToPdfSample", "Samples\AspNetHtmlToPdfSample\AspNetHtmlToPdfSample.csproj", "{C1D5A726-741E-49E0-B462-AF2DB53188A6}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {93CF86E2-D19B-442E-9117-B87376C55F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {93CF86E2-D19B-442E-9117-B87376C55F80}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {93CF86E2-D19B-442E-9117-B87376C55F80}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {93CF86E2-D19B-442E-9117-B87376C55F80}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {868E3D99-0F22-4149-87E0-1857F4414772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {868E3D99-0F22-4149-87E0-1857F4414772}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {868E3D99-0F22-4149-87E0-1857F4414772}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {868E3D99-0F22-4149-87E0-1857F4414772}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {638921E9-7DB1-474D-B721-8EFF16FDC797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {638921E9-7DB1-474D-B721-8EFF16FDC797}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {638921E9-7DB1-474D-B721-8EFF16FDC797}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {638921E9-7DB1-474D-B721-8EFF16FDC797}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {E2032CDA-877E-41EA-A6BF-737B627E9724}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {E2032CDA-877E-41EA-A6BF-737B627E9724}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {E2032CDA-877E-41EA-A6BF-737B627E9724}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {E2032CDA-877E-41EA-A6BF-737B627E9724}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {C1D5A726-741E-49E0-B462-AF2DB53188A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {C1D5A726-741E-49E0-B462-AF2DB53188A6}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {C1D5A726-741E-49E0-B462-AF2DB53188A6}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {C1D5A726-741E-49E0-B462-AF2DB53188A6}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {638921E9-7DB1-474D-B721-8EFF16FDC797} = {959E756A-5792-4E8E-9136-4B82798A2C28} 56 | {E2032CDA-877E-41EA-A6BF-737B627E9724} = {959E756A-5792-4E8E-9136-4B82798A2C28} 57 | {C1D5A726-741E-49E0-B462-AF2DB53188A6} = {959E756A-5792-4E8E-9136-4B82798A2C28} 58 | EndGlobalSection 59 | GlobalSection(ExtensibilityGlobals) = postSolution 60 | SolutionGuid = {82453F51-120C-41DF-A555-5BA9374AE82F} 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /Westwind.WebView/HtmlToPdf/CoreWebViewHeadlessHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Web.WebView2.Core; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Drawing; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Newtonsoft.Json; 10 | using Westwind.WebView.HtmlToPdf.Utilities; 11 | 12 | 13 | namespace Westwind.WebView.HtmlToPdf 14 | { 15 | 16 | /// 17 | /// This class provides the invisible WebView instance used to 18 | /// print the PDF. 19 | /// 20 | internal class CoreWebViewHeadlessHost 21 | { 22 | /// 23 | /// The internal print settings picked up from the passed in host 24 | /// 25 | internal WebViewPrintSettings WebViewPrintSettings { get; set; } = new WebViewPrintSettings(); 26 | 27 | private string _outputFile { get; set; } 28 | 29 | /// 30 | /// Passed in high level host 31 | /// 32 | internal HtmlToPdfHost HtmlToPdfHost { get; set; } 33 | 34 | 35 | 36 | internal bool IsSuccess { get; set; } = false; 37 | 38 | internal Exception LastException { get; set; } 39 | 40 | internal Stream ResultStream { get; set; } 41 | 42 | //internal Color Color { get; set; } = Color.White; 43 | 44 | /// 45 | /// Determines when PDF output generation is complete 46 | /// 47 | internal bool IsComplete { get; set; } 48 | 49 | /// 50 | /// The internal WebView instance we load and print from 51 | /// 52 | CoreWebView2 WebView { get; set; } 53 | 54 | private PdfPrintOutputModes PdfPrintOutputMode { get; set; } = PdfPrintOutputModes.File; 55 | 56 | protected TaskCompletionSource IsInitializedTaskCompletionSource = new TaskCompletionSource(); 57 | 58 | internal CoreWebViewHeadlessHost(HtmlToPdfHost htmlToPdfHost) 59 | { 60 | HtmlToPdfHost = htmlToPdfHost; 61 | WebViewPrintSettings = htmlToPdfHost.WebViewPrintSettings; 62 | InitializeAsync(); 63 | } 64 | 65 | private IntPtr HWND_MESSAGE = new IntPtr(-3); 66 | 67 | protected async void InitializeAsync() 68 | { 69 | 70 | // must create a data folder if running out of a secured folder that can't write like Program Files 71 | var environment = await CoreWebView2Environment.CreateAsync(userDataFolder: HtmlToPdfHost.WebViewEnvironmentPath); 72 | 73 | var controller = await environment.CreateCoreWebView2ControllerAsync(HWND_MESSAGE); 74 | 75 | // StringUtils.LogString("WebView Controller Rendered"); 76 | controller.DefaultBackgroundColor = ColorTranslator.FromHtml(HtmlToPdfHost.BackgroundHtmlColor ?? "white"); 77 | 78 | WebView = controller.CoreWebView2; 79 | WebView.DOMContentLoaded += CoreWebView2_DOMContentLoaded; 80 | 81 | // Ensure that control is initialized before we can navigate! 82 | IsInitializedTaskCompletionSource.SetResult(true); 83 | } 84 | 85 | 86 | 87 | /// 88 | /// Internally navigates the the browser to the document to render 89 | /// 90 | /// 91 | /// 92 | /// 93 | internal async Task PrintFromUrl(string url, string outputFile) 94 | { 95 | await IsInitializedTaskCompletionSource.Task; 96 | 97 | PdfPrintOutputMode = PdfPrintOutputModes.File; 98 | _outputFile = outputFile; 99 | WebView.Navigate(url); 100 | } 101 | 102 | /// 103 | /// Internally navigates t 104 | /// 105 | /// 106 | /// 107 | public async Task PrintFromUrlStream(string url) 108 | { 109 | // Can't navigate until initialized 110 | await IsInitializedTaskCompletionSource.Task; 111 | 112 | PdfPrintOutputMode = PdfPrintOutputModes.Stream; 113 | WebView.Navigate(url); 114 | } 115 | 116 | /// 117 | /// Prints from an HTML stream. This allows HTML to be generated from 118 | /// in-memory sources 119 | /// 120 | /// 121 | /// 122 | public async Task PrintFromHtmlStreamToStream(Stream htmlStream, Encoding encoding = null) 123 | { 124 | if (encoding == null) 125 | encoding = Encoding.UTF8; 126 | 127 | // Can't navigate until initialized 128 | await IsInitializedTaskCompletionSource.Task; 129 | 130 | WebView.Navigate("about:blank"); 131 | 132 | PdfPrintOutputMode = PdfPrintOutputModes.Stream; 133 | htmlStream.Position = 0; 134 | string html = htmlStream.AsString(encoding); 135 | 136 | 137 | string encodedHtml = html.ToJson(); 138 | string script = "window.document.write(" + encodedHtml + ")"; 139 | 140 | try 141 | { 142 | await WebView.ExecuteScriptAsync(script); 143 | } 144 | catch(Exception ex) 145 | { 146 | this.LastException = ex; 147 | } 148 | } 149 | 150 | 151 | 152 | private async void CoreWebView2_DOMContentLoaded(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs e) 153 | { 154 | try 155 | { 156 | await InjectCssAndScript(); 157 | 158 | if (PdfPrintOutputMode == PdfPrintOutputModes.File) 159 | await PrintToPdf(); 160 | else 161 | await PrintToPdfStream(); 162 | } 163 | finally 164 | { 165 | IsComplete = true; 166 | HtmlToPdfHost.IsCompleteTaskCompletionSource.SetResult(true); 167 | } 168 | } 169 | 170 | private async Task InjectCssAndScript() 171 | { 172 | var css = new StringBuilder(); 173 | 174 | if (HtmlToPdfHost.CssAndScriptOptions.OptimizePdfFonts) 175 | { 176 | css.AppendLine(OptimizedFontCss); 177 | } 178 | if (HtmlToPdfHost.CssAndScriptOptions.KeepTextTogether) 179 | { 180 | css.AppendLine(PageBreakCss); 181 | } 182 | if (!string.IsNullOrEmpty(HtmlToPdfHost.CssAndScriptOptions.CssToInject)) 183 | { 184 | css.AppendLine(HtmlToPdfHost.CssAndScriptOptions.CssToInject); 185 | } 186 | 187 | 188 | if (css.Length > 0) 189 | { 190 | var script = "document.head.appendChild(document.createElement('style')).innerHTML = " + StringUtils.ToJson(css.ToString()) + ";"; 191 | await WebView.ExecuteScriptAsync(script); 192 | } 193 | } 194 | 195 | 196 | 197 | /// 198 | /// Prints PDF to an output file 199 | /// 200 | /// 201 | internal async Task PrintToPdf() 202 | { 203 | if (File.Exists(_outputFile)) 204 | { 205 | File.Delete(_outputFile); 206 | } 207 | 208 | if (HtmlToPdfHost.DelayPdfGenerationMs > 0) 209 | { 210 | await Task.Delay(HtmlToPdfHost.DelayPdfGenerationMs); 211 | } 212 | 213 | try 214 | { 215 | var file = Path.GetFullPath(_outputFile); 216 | 217 | if (File.Exists(file)) 218 | File.Delete(file); 219 | 220 | if (HtmlToPdfHost.UseServerPdfGeneration) 221 | { 222 | var webViewPrintSettings = GetWebViewPrintSettings(); 223 | Debug.WriteLine($"WebViewPrintSettings to file {file}:\n\n" + JsonConvert.SerializeObject(webViewPrintSettings, Formatting.Indented)); 224 | 225 | await WebView.PrintToPdfAsync(file, webViewPrintSettings); 226 | } 227 | else 228 | { 229 | // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF 230 | //{ 231 | // "landscape": false, 232 | // "printBackground": true, 233 | // "scale": 1, 234 | // "paperWidth": 8.5, 235 | // "paperHeight": 11, 236 | // "marginTop": 0.50, 237 | // "marginBottom": 0.30, 238 | // "marginLeft": 0.40, 239 | // "marginRight": 0.40, 240 | // "pageRanges": "", 241 | // "headerTemplate": "
", 242 | // "footerTemplate": "
of
", 243 | // "displayHeaderFooter": true, 244 | // "preferCSSPageSize": false, 245 | // "generateDocumentOutline": true 246 | //} 247 | var json = GetDevToolsWebViewPrintSettingsJson(); 248 | var pdfBase64 = await WebView.CallDevToolsProtocolMethodAsync("Page.printToPDF", json); 249 | 250 | if (!string.IsNullOrEmpty(pdfBase64)) 251 | { 252 | // avoid JSON Serializer Dependency 253 | var b64Data = StringUtils.ExtractString(pdfBase64, "\"data\":\"", "\"}"); 254 | var pdfData = Convert.FromBase64String(b64Data); 255 | File.WriteAllBytes(_outputFile, pdfData); // 256 | } 257 | } 258 | 259 | if (File.Exists(_outputFile)) 260 | IsSuccess = true; 261 | else 262 | IsSuccess = false; 263 | } 264 | catch (Exception ex) 265 | { 266 | IsSuccess = false; 267 | LastException = ex; 268 | } 269 | } 270 | 271 | 272 | 273 | 274 | /// 275 | /// Prints the current document in the WebView to a MemoryStream 276 | /// 277 | /// 278 | internal async Task PrintToPdfStream() 279 | { 280 | 281 | // THIS WORKS ON FIRST REQUEST IN IIS 282 | try 283 | { 284 | // Server Apps should use direct WebView PDF generation 285 | // as Server environments don't support DevTools 286 | if (HtmlToPdfHost.UseServerPdfGeneration) 287 | { 288 | var parms = GetWebViewPrintSettings(); 289 | 290 | // we have to turn the stream into something physical because the form won't stay alive 291 | Debug.WriteLine("WebViewPrintSettings:\n\n" + JsonConvert.SerializeObject(parms, Formatting.Indented)); 292 | using var stream = await WebView.PrintToPdfStreamAsync(parms); 293 | 294 | var ms = new MemoryStream(); 295 | await stream.CopyToAsync(ms); 296 | ms.Position = 0; 297 | 298 | ResultStream = ms; // don't Close()/Dispose()! 299 | IsSuccess = true; 300 | return ResultStream; 301 | } 302 | 303 | // Interactive can use the WebView DevTools API 304 | var json = GetDevToolsWebViewPrintSettingsJson(); 305 | 306 | Debug.WriteLine(json); 307 | var pdfBase64 = await WebView.CallDevToolsProtocolMethodAsync("Page.printToPDF", json); 308 | 309 | if (!string.IsNullOrEmpty(pdfBase64)) 310 | { 311 | // avoid JSON Serializer Dependency 312 | var b64Data = StringUtils.ExtractString(pdfBase64, "\"data\":\"", "\"}"); 313 | var pdfData = Convert.FromBase64String(b64Data); 314 | 315 | var ms = new MemoryStream(pdfData); 316 | ResultStream = ms; 317 | IsSuccess = true; 318 | return ResultStream; 319 | } 320 | 321 | IsSuccess = false; 322 | LastException = new InvalidOperationException("No PDF output was generated."); 323 | return null; 324 | } 325 | catch (Exception ex) 326 | { 327 | IsSuccess = false; 328 | LastException = ex; 329 | return null; 330 | } 331 | } 332 | 333 | /// 334 | /// Map our private type to the CoreWebView type. 335 | /// 336 | /// 337 | 338 | private CoreWebView2PrintSettings GetWebViewPrintSettings() 339 | { 340 | var wvps = WebView.Environment.CreatePrintSettings(); 341 | 342 | var ps = WebViewPrintSettings; 343 | 344 | wvps.ScaleFactor = ps.ScaleFactor; 345 | wvps.MarginTop = ps.MarginTop; 346 | wvps.MarginBottom = ps.MarginBottom; 347 | wvps.MarginLeft = ps.MarginLeft; 348 | wvps.MarginRight = ps.MarginRight; 349 | 350 | wvps.PageWidth = ps.PageWidth; 351 | wvps.PageHeight = ps.PageHeight; 352 | 353 | wvps.Copies = ps.Copies; 354 | wvps.PageRanges = ps.PageRanges; 355 | 356 | wvps.ShouldPrintBackgrounds = ps.ShouldPrintBackgrounds; 357 | 358 | wvps.ShouldPrintHeaderAndFooter = ps.ShouldPrintHeaderAndFooter; 359 | wvps.HeaderTitle = ps.HeaderTemplate; 360 | wvps.FooterUri = ps.FooterTemplate; 361 | 362 | wvps.ShouldPrintSelectionOnly = ps.ShouldPrintSelectionOnly; 363 | wvps.Orientation = ps.Orientation == WebViewPrintOrientations.Portrait ? CoreWebView2PrintOrientation.Portrait : CoreWebView2PrintOrientation.Landscape; 364 | wvps.Duplex = ps.Duplex == WebViewPrintDuplexes.Default ? CoreWebView2PrintDuplex.Default : 365 | ps.Duplex == WebViewPrintDuplexes.OneSided ? CoreWebView2PrintDuplex.OneSided : 366 | ps.Duplex == WebViewPrintDuplexes.TwoSidedLongEdge ? CoreWebView2PrintDuplex.TwoSidedLongEdge : 367 | CoreWebView2PrintDuplex.TwoSidedShortEdge; 368 | wvps.Collation = ps.Collation == WebViewPrintCollations.Default ? CoreWebView2PrintCollation.Default : 369 | ps.Collation == WebViewPrintCollations.Collated ? CoreWebView2PrintCollation.Collated : 370 | CoreWebView2PrintCollation.Uncollated; 371 | wvps.ColorMode = ps.ColorMode == WebViewPrintColorModes.Color ? CoreWebView2PrintColorMode.Color : CoreWebView2PrintColorMode.Grayscale; 372 | 373 | 374 | wvps.PrinterName = ps.PrinterName; 375 | wvps.PagesPerSide = ps.PagesPerSide; 376 | 377 | return wvps; 378 | } 379 | 380 | 381 | /// 382 | /// Map WebViewPrintSettings to DevToolsPrintSettings and return as JSON 383 | /// that needs to be passed to the API. 384 | /// 385 | /// 386 | public string GetDevToolsWebViewPrintSettingsJson() 387 | { 388 | var wvps = new DevToolsPrintToPdfSettings(); 389 | 390 | var ps = WebViewPrintSettings; 391 | 392 | wvps.landscape = ps.Orientation == WebViewPrintOrientations.Landscape; 393 | wvps.printBackground = ps.ShouldPrintBackgrounds; 394 | wvps.scale = ps.ScaleFactor; 395 | wvps.paperWidth = ps.PageWidth; 396 | wvps.paperHeight = ps.PageHeight; 397 | wvps.marginTop = ps.MarginTop; 398 | wvps.marginBottom = ps.MarginBottom; 399 | wvps.marginLeft = ps.MarginLeft; 400 | wvps.marginRight = ps.MarginRight; 401 | 402 | wvps.pageRanges = ps.PageRanges ?? string.Empty; 403 | 404 | wvps.displayHeaderFooter = ps.ShouldPrintHeaderAndFooter; 405 | wvps.headerTemplate = ps.HeaderTemplate ?? string.Empty; 406 | wvps.footerTemplate = ps.FooterTemplate ?? string.Empty; 407 | 408 | wvps.generateDocumentOutline = ps.GenerateDocumentOutline; 409 | 410 | return wvps.ToJson(); 411 | } 412 | 413 | 414 | string PageBreakCss { get; } = @" 415 | html, body { 416 | text-rendering: optimizeLegibility; 417 | height: auto; 418 | } 419 | 420 | pre { 421 | white-space: pre-wrap; 422 | word-break: normal; 423 | word-wrap: normal; 424 | } 425 | pre > code { 426 | white-space: pre-wrap; 427 | padding: 1em !important; 428 | } 429 | 430 | /* keep paragraphs together */ 431 | p, li, ul, code, pre { 432 | page-break-inside: avoid; 433 | break-inside: avoid; 434 | } 435 | 436 | /* keep headers and content together */ 437 | h1, h2, h3, h4, h5, h6 { 438 | page-break-after: avoid; 439 | break-after: avoid; 440 | } 441 | "; 442 | string OptimizedFontCss { get; } = 443 | @"html, body { font-family: ""Segoe UI Emoji"", ""Apple Color Emoji"", -apple-system, BlinkMacSystemFont,""Segoe UI"", Helvetica, Arial, sans-serif; }"; 444 | } 445 | } 446 | 447 | 448 | /// 449 | /// DevTools PrintToPdf settings that matches the Chromium structure 450 | /// 451 | public class DevToolsPrintToPdfSettings 452 | { 453 | public bool landscape { get; set; } = false; 454 | 455 | public bool printBackground { get; set; } = true; 456 | 457 | public double scale { get; set; } = 1; 458 | public double paperWidth { get; set; } = 8.5; 459 | public double paperHeight { get; set; } = 11; 460 | public double marginTop { get; set; } = 0.4; 461 | public double marginBottom { get; set; } = 0.4; 462 | public double marginLeft { get; set; } = 0.4; 463 | public double marginRight { get; set; } = 0.4; 464 | public string pageRanges { get; set; } = string.Empty; 465 | 466 | public bool displayHeaderFooter { get; set; } = true; 467 | public string headerTemplate { get; set; } = "
"; 468 | public string footerTemplate { get; set; } = "
of "; 469 | 470 | public bool preferCSSPageSize { get; set; } = false; 471 | public bool generateDocumentOutline { get; set; } = true; 472 | 473 | public string ToJson() 474 | { 475 | pageRanges = pageRanges ?? string.Empty; 476 | headerTemplate = headerTemplate ?? string.Empty; 477 | footerTemplate = footerTemplate ?? string.Empty; 478 | 479 | #if DEBUG 480 | var json = JsonConvert.SerializeObject(this, Formatting.Indented); 481 | Debug.WriteLine("PrintToPdf DevTools:\n" + json); 482 | return json; 483 | #endif 484 | 485 | return JsonConvert.SerializeObject(this, Formatting.None); 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /Westwind.WebView/HtmlToPdf/Enums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Westwind.WebView.HtmlToPdf 8 | { 9 | public enum WebViewPrintColorModes 10 | { 11 | Color, 12 | Grayscale, 13 | } 14 | 15 | public enum WebViewPrintOrientations 16 | { 17 | Portrait, 18 | Landscape 19 | } 20 | 21 | public enum WebViewPrintCollations 22 | { 23 | Default, 24 | Collated, 25 | UnCollated 26 | } 27 | 28 | public enum WebViewPrintDuplexes 29 | { 30 | Default, 31 | OneSided, 32 | TwoSidedLongEdge, 33 | TwoSidedShortEdge 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Westwind.WebView/HtmlToPdf/HtmlToPdfDefaults.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Westwind.WebView.HtmlToPdf 9 | { 10 | public class HtmlToPdfDefaults 11 | { 12 | /// 13 | /// Specify the background color of the PDF frame which contains 14 | /// the margins of the document. 15 | /// 16 | /// Defaults to white, but if you use a non-white background for your 17 | /// document you'll likely want to match it to your document background. 18 | /// 19 | /// Also note that non-white colors may have to use custom HeaderTemplate and 20 | /// FooterTemplate to set the foregraound color of the text to match the background. 21 | /// 22 | public static string BackgroundHtmlColor { get; set; } = "#ffffff"; 23 | 24 | /// 25 | /// If true uses WebView.PrintToPdfStreamAsync() rather than the DevTools version 26 | /// to generate PDF output. Use this for server operation as the DevTools printing 27 | /// is not supported in server environments like IIS. 28 | /// 29 | public static bool UseServerPdfGeneration { get; set; } = false; 30 | 31 | 32 | /// 33 | /// The default folder location for the WebView environment folder that is used when 34 | /// no explicit path is provided. This is a static value that is global to 35 | /// the Application, so it's best set during application startup to ensure 36 | /// that the same folder is used. 37 | /// 38 | /// Make sure this folder is writable by the application's user identity! 39 | /// 40 | public static string WebViewEnvironmentPath { get; set; } = Path.Combine(Path.GetTempPath(), "WebView2_Environment"); 41 | 42 | 43 | 44 | /// 45 | /// Pre-initializes the Print Service which is necessary when running under 46 | /// server environment 47 | /// 48 | /// 49 | /// Provide a folder where the WebView Environment can be written to. 50 | /// 51 | /// *** IMPORTANT: *** 52 | /// This location has to be writeable using the server's identity so 53 | /// be sure to set folder permissions for limited user accounts. 54 | /// 55 | /// Defaults to the User Temp folder, but for server apps that folder may 56 | /// not be accessible, so it's best to explicitly set and configure this 57 | /// folder. 58 | /// 59 | public static void ServerPreInitialize(string defaultWebViewEnvironmentPath = null) 60 | { 61 | if (!string.IsNullOrEmpty(defaultWebViewEnvironmentPath)) 62 | HtmlToPdfDefaults.WebViewEnvironmentPath = defaultWebViewEnvironmentPath; 63 | 64 | UseServerPdfGeneration = true; 65 | 66 | Task.Run(async () => 67 | { 68 | try 69 | { 70 | var host = new HtmlToPdfHost(); 71 | var result = await host.PrintToPdfStreamAsync("about:blank"); 72 | } 73 | catch 74 | { 75 | } 76 | }); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Westwind.WebView/HtmlToPdf/PdfPrintOutputModes.cs: -------------------------------------------------------------------------------- 1 | namespace Westwind.WebView.HtmlToPdf 2 | { 3 | internal enum PdfPrintOutputModes 4 | { 5 | File, 6 | Stream 7 | } 8 | } -------------------------------------------------------------------------------- /Westwind.WebView/HtmlToPdf/PdfPrintResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Westwind.WebView.HtmlToPdf 5 | { 6 | 7 | /// 8 | /// Result from a Print to PDF operation. ResultStream is set only 9 | /// on stream operations. 10 | /// 11 | public class PdfPrintResult 12 | { 13 | /// 14 | /// Notifies of sucess or failure of operation 15 | /// 16 | public bool IsSuccess { get; set; } 17 | 18 | /// 19 | /// If in stream mode, the resulting MemoryStream will be assigned 20 | /// to this property. You need to close/dispose of this stream when 21 | /// done with it. 22 | /// 23 | public Stream ResultStream { get; set; } 24 | 25 | /// 26 | /// A message related to the operation - use for error messages if 27 | /// an error occured. 28 | /// 29 | public string Message { get; set; } 30 | 31 | /// 32 | /// The exception that triggered a failed PDF conversion operation 33 | /// 34 | public Exception LastException { get; set; } 35 | } 36 | } -------------------------------------------------------------------------------- /Westwind.WebView/HtmlToPdf/WebViewPrintSettings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Web.WebView2.Core; 2 | 3 | namespace Westwind.WebView.HtmlToPdf 4 | { 5 | 6 | /// 7 | /// Proxy object of Core WebView settings options to avoid requiring 8 | /// a direct reference to the WebView control in the calling 9 | /// application/project. 10 | /// 11 | /// Settings map to these specific settings in the WebView: 12 | /// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF 13 | /// 14 | public class WebViewPrintSettings 15 | { 16 | /// 17 | /// Scale Factor up to 2 18 | /// 19 | public float ScaleFactor 20 | { 21 | get => _scaleFactor; 22 | set 23 | { 24 | _scaleFactor = value; 25 | if (_scaleFactor > 2F) 26 | ScaleFactor = 2F; 27 | } 28 | } 29 | private float _scaleFactor = 1F; 30 | 31 | 32 | /// 33 | /// Portrait, Landscape 34 | /// 35 | public WebViewPrintOrientations Orientation { get; set; } = WebViewPrintOrientations.Portrait; 36 | 37 | /// 38 | /// Width in inches 39 | /// 40 | public double PageWidth { get; set; } = 8.5F; 41 | 42 | /// 43 | /// Height in inches 44 | /// 45 | public double PageHeight { get; set; } = 11F; 46 | 47 | 48 | /// 49 | /// Top Margin in inches 50 | /// 51 | public double MarginTop { get; set; } = 0.4F; 52 | 53 | /// 54 | /// Bottom Margin in inches 55 | /// 56 | public double MarginBottom { get; set; } = 0.30F; 57 | 58 | /// 59 | /// Left Margin in inches 60 | /// 61 | public double MarginLeft { get; set; } = 0.25F; 62 | 63 | /// 64 | /// Right Margin in inches 65 | /// 66 | public double MarginRight { get; set; } = 0.25F; 67 | 68 | 69 | /// 70 | /// Page ranges as specified 1,2,3,5-7 71 | /// 72 | public string PageRanges { get; set; } = string.Empty; 73 | 74 | 75 | /// 76 | /// Determines whether background colors are printed. Use to 77 | /// save ink on printing or for more legible in print/pdf scenarios 78 | /// 79 | public bool ShouldPrintBackgrounds { get; set; } = true; 80 | 81 | 82 | /// 83 | /// Color, Grayscale, Monochrome 84 | /// 85 | /// CURRENTLY DOESN'T WORK FOR PDF GENERATION 86 | /// 87 | public WebViewPrintColorModes ColorMode { get; set; } = WebViewPrintColorModes.Color; 88 | 89 | 90 | /// 91 | /// When true prints only the section of the document selected 92 | /// 93 | public bool ShouldPrintSelectionOnly { get; set; } = false; 94 | 95 | /// 96 | /// Determines whether headers and footers are printed 97 | /// 98 | public bool ShouldPrintHeaderAndFooter { get; set; } = false; 99 | 100 | 101 | public bool GenerateDocumentOutline { get; set; } = true; 102 | 103 | 104 | /// 105 | /// Html Template that renders the header. 106 | /// Refer to for embeddable styles and formatting: 107 | /// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF 108 | /// 109 | public string HeaderTemplate { get; set; } = "
"; 110 | 111 | 112 | /// 113 | /// Html template that renders the footer 114 | /// Refer to for embeddable styles and formatting: 115 | /// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF 116 | /// 117 | public string FooterTemplate { get; set; } = "
of
"; 118 | 119 | 120 | 121 | /// 122 | /// This a shortcut for the HeaderTemplate that sets the top of the page header. For more control 123 | /// set the HeaderTemplate directly. 124 | /// 125 | public string HeaderTitle 126 | { 127 | set 128 | { 129 | if (string.IsNullOrEmpty(value)) 130 | HeaderTemplate = string.Empty; 131 | else 132 | HeaderTemplate = $"
{value}
"; 133 | } 134 | } 135 | 136 | /// 137 | /// This a shortcut for the FooterTemplate that sets the bottom of the page footer. For more control 138 | /// set the FooterTemplate directly. 139 | /// 140 | public string FooterText 141 | { 142 | set 143 | { 144 | if (string.IsNullOrEmpty(value)) 145 | FooterTemplate = string.Empty; 146 | else 147 | FooterTemplate = $"
{value}
"; 148 | } 149 | } 150 | 151 | #region Print Settings - ignored for PDF 152 | 153 | /// 154 | /// Printer name when printing to a printer (not applicable for PDF) 155 | /// 156 | /// NO EFFECT ON PDF PRINTING 157 | /// 158 | public string PrinterName { get; set; } 159 | 160 | /// 161 | /// Number of Copies to print 162 | /// 163 | /// NO EFFECT ON PDF PRINTING 164 | /// 165 | public int Copies { get; set; } = 1; 166 | 167 | /// 168 | /// Default, OneSided, TwoSidedLongEdge, TwoSidedShortEdge 169 | /// 170 | /// NO EFFECT ON PDF PRINTING 171 | /// 172 | public WebViewPrintDuplexes Duplex { get; set; } = WebViewPrintDuplexes.Default; 173 | 174 | /// 175 | /// Default, Collated, Uncollated 176 | /// 177 | /// NO EFFECT OF PDF PRINTING 178 | /// 179 | public WebViewPrintCollations Collation { get; set; } = WebViewPrintCollations.Default; 180 | 181 | /// 182 | /// Allows multiple pages to be packed into a single page. 183 | /// 184 | /// NO EFFECT ON PDF PRINTING 185 | /// 186 | public int PagesPerSide { get; set; } = 1; 187 | 188 | #endregion 189 | } 190 | } -------------------------------------------------------------------------------- /Westwind.WebView/Utilities/AsyncUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Westwind.WebView.Utilities 9 | { 10 | /// 11 | /// Helper class to run async methods within a sync process. 12 | /// Source: https://www.ryadel.com/en/asyncutil-c-helper-class-async-method-sync-result-wait/ 13 | /// 14 | public static class AsyncUtils 15 | { 16 | private static readonly TaskFactory _taskFactory = new 17 | TaskFactory(CancellationToken.None, 18 | TaskCreationOptions.None, 19 | TaskContinuationOptions.None, 20 | TaskScheduler.Default); 21 | 22 | /// 23 | /// Executes an async Task method which has a void return value synchronously 24 | /// USAGE: AsyncUtil.RunSync(() => AsyncMethod()); 25 | /// 26 | /// Task method to execute 27 | public static void RunSync(Func task) 28 | => _taskFactory 29 | .StartNew(task) 30 | .Unwrap() 31 | .GetAwaiter() 32 | .GetResult(); 33 | 34 | /// 35 | /// Executes an async Task method which has a void return value synchronously 36 | /// USAGE: AsyncUtil.RunSync(() => AsyncMethod()); 37 | /// 38 | /// Task method to execute 39 | public static void RunSync(Func task, 40 | CancellationToken cancellationToken, 41 | TaskCreationOptions taskCreation = TaskCreationOptions.None, 42 | TaskContinuationOptions taskContinuation = TaskContinuationOptions.None, 43 | TaskScheduler taskScheduler = null) 44 | { 45 | if (taskScheduler == null) 46 | taskScheduler = TaskScheduler.Default; 47 | 48 | new TaskFactory(cancellationToken, 49 | taskCreation, 50 | taskContinuation, 51 | taskScheduler) 52 | .StartNew(task) 53 | .Unwrap() 54 | .GetAwaiter() 55 | .GetResult(); 56 | } 57 | 58 | /// 59 | /// Executes an async Task<T> method which has a T return type synchronously 60 | /// USAGE: T result = AsyncUtil.RunSync(() => AsyncMethod<T>()); 61 | /// 62 | /// Return Type 63 | /// Task<T> method to execute 64 | /// 65 | public static TResult RunSync(Func> task) 66 | => _taskFactory 67 | .StartNew(task) 68 | .Unwrap() 69 | .GetAwaiter() 70 | .GetResult(); 71 | 72 | 73 | /// 74 | /// Executes an async Task<T> method which has a T return type synchronously 75 | /// USAGE: T result = AsyncUtil.RunSync(() => AsyncMethod<T>()); 76 | /// 77 | /// Return Type 78 | /// Task<T> method to execute 79 | /// 80 | public static TResult RunSync(Func> func, 81 | CancellationToken cancellationToken, 82 | TaskCreationOptions taskCreation = TaskCreationOptions.None, 83 | TaskContinuationOptions taskContinuation = TaskContinuationOptions.None, 84 | TaskScheduler taskScheduler = null) 85 | { 86 | if (taskScheduler == null) 87 | taskScheduler = TaskScheduler.Default; 88 | 89 | return new TaskFactory(cancellationToken, 90 | taskCreation, 91 | taskContinuation, 92 | taskScheduler) 93 | .StartNew(func, cancellationToken) 94 | .Unwrap() 95 | .GetAwaiter() 96 | .GetResult(); 97 | } 98 | 99 | 100 | /// 101 | /// Ensures safe operation of a task without await even if 102 | /// an execution fails with an exception. This forces the 103 | /// exception to be cleared unlike a non-continued task. 104 | /// 105 | /// Task Instance 106 | public static void FireAndForget(this Task t) 107 | { 108 | t.ContinueWith(tsk => tsk.Exception, 109 | TaskContinuationOptions.OnlyOnFaulted); 110 | } 111 | 112 | 113 | /// 114 | /// Ensures safe operation of a task without await even if 115 | /// an execution fails with an exception. This forces the 116 | /// exception to be cleared unlike a non-continued task. 117 | /// 118 | /// This version allows you to capture and respond to any 119 | /// exceptions caused by the Task code executing. 120 | /// 121 | /// 122 | /// Action delegate that receives an Exception parameter you can use to log or otherwise handle (or ignore) any exceptions 123 | public static void FireAndForget(this Task t, Action del) 124 | { 125 | t.ContinueWith( (tsk) => del?.Invoke(tsk.Exception), TaskContinuationOptions.OnlyOnFaulted); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Westwind.WebView/Utilities/JsonSerializationUtils.cs: -------------------------------------------------------------------------------- 1 | #region License 2 | /* 3 | ************************************************************** 4 | * Author: Rick Strahl 5 | * © West Wind Technologies, 2008 - 2009 6 | * http://www.west-wind.com/ 7 | * 8 | * Created: 09/08/2008 9 | * 10 | * Permission is hereby granted, free of charge, to any person 11 | * obtaining a copy of this software and associated documentation 12 | * files (the "Software"), to deal in the Software without 13 | * restriction, including without limitation the rights to use, 14 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following 17 | * conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be 20 | * included in all copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 24 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 26 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 27 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 29 | * OTHER DEALINGS IN THE SOFTWARE. 30 | ************************************************************** 31 | */ 32 | #endregion 33 | 34 | using System; 35 | using System.IO; 36 | using System.Text; 37 | using System.Diagnostics; 38 | using Newtonsoft.Json; 39 | using Newtonsoft.Json.Converters; 40 | using Newtonsoft.Json.Linq; 41 | using Newtonsoft.Json.Serialization; 42 | 43 | namespace Westwind.Utilities 44 | { 45 | /// 46 | /// JSON Serialization helper class that uses JSON.NET. 47 | /// This class serializes JSON to and from string and 48 | /// files on disk. 49 | /// 50 | /// 51 | /// JSON.NET is loaded dynamically at runtime to avoid hard 52 | /// linking the Newtonsoft.Json.dll to Westwind.Utilities. 53 | /// Just make sure that your project includes a reference 54 | /// to JSON.NET when using this class. 55 | /// 56 | internal static class JsonSerializationUtils 57 | { 58 | //capture reused type instances 59 | private static JsonSerializer JsonNet = null; 60 | 61 | private static object SyncLock = new Object(); 62 | 63 | 64 | 65 | /// 66 | /// Serializes an object to an XML string. Unlike the other SerializeObject overloads 67 | /// this methods *returns a string* rather than a bool result! 68 | /// 69 | /// Value to serialize 70 | /// if true 71 | /// Determines if a failure throws or returns null 72 | /// 73 | /// null on error otherwise the Xml String. 74 | /// 75 | /// 76 | /// If null is passed in null is also returned so you might want 77 | /// to check for null before calling this method. 78 | /// 79 | public static string Serialize(object value, bool formatJsonOutput = false, bool camelCase = false, bool throwExceptions = false, bool forceNewInstance = false ) 80 | { 81 | if (value is null) return "null"; 82 | 83 | string jsonResult = null; 84 | Type type = value.GetType(); 85 | JsonTextWriter writer = null; 86 | try 87 | { 88 | if (forceNewInstance) 89 | JsonNet = null; 90 | var json = CreateJsonNet(throwExceptions, camelCase); 91 | 92 | StringWriter sw = new StringWriter(); 93 | 94 | writer = new JsonTextWriter(sw); 95 | 96 | if (formatJsonOutput) 97 | writer.Formatting = Formatting.Indented; 98 | 99 | 100 | writer.QuoteChar = '"'; 101 | json.Serialize(writer, value); 102 | 103 | jsonResult = sw.ToString(); 104 | writer.Close(); 105 | } 106 | catch 107 | { 108 | if (throwExceptions) 109 | throw; 110 | 111 | jsonResult = null; 112 | } 113 | finally 114 | { 115 | if (writer != null) 116 | writer.Close(); 117 | } 118 | 119 | return jsonResult; 120 | } 121 | 122 | /// 123 | /// Serializes an object instance to a JSON file. 124 | /// 125 | /// the value to serialize 126 | /// Full path to the file to write out with JSON. 127 | /// Determines whether exceptions are thrown or false is returned 128 | /// if true pretty-formats the JSON with line breaks 129 | /// true or false 130 | public static bool SerializeToFile(object value, string fileName, bool throwExceptions = false, bool formatJsonOutput = false, bool camelCase = false) 131 | { 132 | try 133 | { 134 | Type type = value.GetType(); 135 | 136 | var json = CreateJsonNet(throwExceptions, camelCase); 137 | if (json == null) 138 | return false; 139 | 140 | 141 | using (FileStream fs = new FileStream(fileName, FileMode.Create)) 142 | { 143 | using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8)) 144 | { 145 | using (var writer = new JsonTextWriter(sw)) 146 | { 147 | if (formatJsonOutput) 148 | writer.Formatting = Formatting.Indented; 149 | 150 | writer.QuoteChar = '"'; 151 | json.Serialize(writer, value); 152 | } 153 | } 154 | } 155 | } 156 | catch (Exception ex) 157 | { 158 | Debug.WriteLine("JsonSerializer Serialize error: " + ex.Message); 159 | if (throwExceptions) 160 | throw; 161 | return false; 162 | } 163 | 164 | return true; 165 | } 166 | 167 | /// 168 | /// Deserializes an object, array or value from JSON string to an object or value 169 | /// 170 | /// 171 | /// 172 | /// 173 | /// 174 | public static object Deserialize(string jsonText, Type type, bool throwExceptions = false) 175 | { 176 | var json = CreateJsonNet(throwExceptions); 177 | if (json == null) 178 | return null; 179 | 180 | object result = null; 181 | JsonTextReader reader = null; 182 | try 183 | { 184 | StringReader sr = new StringReader(jsonText); 185 | reader = new JsonTextReader(sr); 186 | result = json.Deserialize(reader, type); 187 | reader.Close(); 188 | } 189 | catch (Exception ex) 190 | { 191 | Debug.WriteLine("JsonSerializer Deserialize error: " + ex.Message); 192 | if (throwExceptions) 193 | throw; 194 | 195 | return null; 196 | } 197 | finally 198 | { 199 | if (reader != null) 200 | reader.Close(); 201 | } 202 | 203 | return result; 204 | } 205 | 206 | /// 207 | /// Deserializes an object, array or value from JSON string to an object or value 208 | /// 209 | /// 210 | /// 211 | /// 212 | public static T Deserialize(string jsonText, bool throwExceptions = false) 213 | { 214 | var res = Deserialize(jsonText, typeof(T), throwExceptions); 215 | if (res == null) 216 | return default(T); 217 | 218 | return (T)res; 219 | } 220 | 221 | /// 222 | /// Deserializes an object from file and returns a reference. 223 | /// 224 | /// name of the file to serialize to 225 | /// The Type of the object. Use typeof(yourobject class) 226 | /// determines whether we use Xml or Binary serialization 227 | /// determines whether failure will throw rather than return null on failure 228 | /// Instance of the deserialized object or null. Must be cast to your object type 229 | public static object DeserializeFromFile(string fileName, Type objectType, bool throwExceptions = false) 230 | { 231 | var json = CreateJsonNet(throwExceptions); 232 | if (json == null) 233 | return null; 234 | 235 | object result; 236 | JsonTextReader reader; 237 | FileStream fs; 238 | 239 | try 240 | { 241 | using (fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) 242 | { 243 | using (var sr = new StreamReader(fs, Encoding.UTF8)) 244 | { 245 | using (reader = new JsonTextReader(sr)) 246 | { 247 | result = json.Deserialize(reader, objectType); 248 | } 249 | } 250 | } 251 | } 252 | catch (Exception ex) 253 | { 254 | Debug.WriteLine("JsonNetSerialization Deserialization Error: " + ex.Message); 255 | if (throwExceptions) 256 | throw; 257 | 258 | return null; 259 | } 260 | 261 | return result; 262 | } 263 | 264 | /// 265 | /// Deserializes an object from file and returns a reference. 266 | /// 267 | /// name of the file to serialize to 268 | /// determines whether we use Xml or Binary serialization 269 | /// determines whether failure will throw rather than return null on failure 270 | /// Instance of the deserialized object or null. Must be cast to your object type 271 | public static T DeserializeFromFile(string fileName, bool throwExceptions = false) 272 | { 273 | var res = DeserializeFromFile(fileName, typeof(T), throwExceptions); 274 | if (res == null) 275 | return default(T); 276 | return (T)res; 277 | } 278 | 279 | /// 280 | /// Takes a single line JSON string and pretty formats 281 | /// it using indented formatting. 282 | /// 283 | /// 284 | /// 285 | public static string FormatJsonString(string json) 286 | { 287 | return JToken.Parse(json).ToString(Formatting.Indented) as string; 288 | } 289 | 290 | /// 291 | /// Dynamically creates an instance of JSON.NET 292 | /// 293 | /// If true throws exceptions otherwise returns null 294 | /// Dynamic JsonSerializer instance 295 | public static JsonSerializer CreateJsonNet(bool throwExceptions = true, bool camelCase = false) 296 | { 297 | if (JsonNet != null) 298 | return JsonNet; 299 | 300 | lock (SyncLock) 301 | { 302 | if (JsonNet != null) 303 | return JsonNet; 304 | 305 | // Try to create instance 306 | JsonSerializer json; 307 | try 308 | { 309 | json = new JsonSerializer(); 310 | } 311 | catch 312 | { 313 | if (throwExceptions) 314 | throw; 315 | return null; 316 | } 317 | 318 | if (json == null) 319 | return null; 320 | 321 | json.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; 322 | 323 | // Enums as strings in JSON 324 | var enumConverter = new StringEnumConverter(); 325 | json.Converters.Add(enumConverter); 326 | 327 | if (camelCase) 328 | json.ContractResolver = new CamelCasePropertyNamesContractResolver(); 329 | 330 | JsonNet = json; 331 | } 332 | 333 | return JsonNet; 334 | } 335 | } 336 | } -------------------------------------------------------------------------------- /Westwind.WebView/Utilities/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace System.IO 4 | { 5 | /// 6 | /// MemoryStream Extension Methods that provide conversions to and from strings 7 | /// 8 | internal static class MemoryStreamExtensions 9 | { 10 | /// 11 | /// Returns the content of the stream as a string 12 | /// 13 | /// Memory stream 14 | /// Encoding to use - defaults to Unicode 15 | /// 16 | public static string AsString(this MemoryStream ms, Encoding encoding = null) 17 | { 18 | if (encoding == null) 19 | encoding = Encoding.Unicode; 20 | 21 | return encoding.GetString(ms.ToArray()); 22 | } 23 | 24 | /// 25 | /// Writes the specified string into the memory stream 26 | /// 27 | /// 28 | /// 29 | /// 30 | public static void FromString(this MemoryStream ms, string inputString, Encoding encoding = null) 31 | { 32 | if (encoding == null) 33 | encoding = Encoding.Unicode; 34 | 35 | byte[] buffer = encoding.GetBytes(inputString); 36 | ms.Write(buffer, 0, buffer.Length); 37 | ms.Position = 0; 38 | } 39 | } 40 | 41 | /// 42 | /// Stream Extensions 43 | /// 44 | internal static class StreamExtensions 45 | { 46 | /// 47 | /// Converts a stream by copying it to a memory stream and returning 48 | /// as a string with encoding. 49 | /// 50 | /// stream to turn into a string 51 | /// Encoding of the stream. Defaults to Unicode 52 | /// string 53 | public static string AsString(this Stream s, Encoding encoding = null) 54 | { 55 | using (var ms = new MemoryStream()) 56 | { 57 | s.CopyTo(ms); 58 | s.Position = 0; 59 | return ms.AsString(encoding); 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Westwind.WebView/Utilities/StringUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace Westwind.WebView.HtmlToPdf.Utilities 7 | { 8 | internal static class StringUtils 9 | { 10 | /// 11 | /// A helper to generate a JSON string from a string value 12 | /// 13 | /// Use this to avoid bringing in a full JSON Serializer for 14 | /// scenarios of string serialization. 15 | /// 16 | /// Note: Function includes surrounding quotes! 17 | /// 18 | /// 19 | /// JSON encoded string ("text"), empty ("") or "null". 20 | internal static string ToJson(this string text, bool noQuotes = false) 21 | { 22 | if (text is null) 23 | return "null"; 24 | 25 | var sb = new StringBuilder(text.Length); 26 | 27 | if (!noQuotes) 28 | sb.Append("\""); 29 | 30 | var ct = text.Length; 31 | 32 | for (int x = 0; x < ct; x++) 33 | { 34 | var c = text[x]; 35 | 36 | switch (c) 37 | { 38 | case '\"': 39 | sb.Append("\\\""); 40 | break; 41 | case '\\': 42 | sb.Append("\\\\"); 43 | break; 44 | case '\b': 45 | sb.Append("\\b"); 46 | break; 47 | case '\f': 48 | sb.Append("\\f"); 49 | break; 50 | case '\n': 51 | sb.Append("\\n"); 52 | break; 53 | case '\r': 54 | sb.Append("\\r"); 55 | break; 56 | case '\t': 57 | sb.Append("\\t"); 58 | break; 59 | default: 60 | uint i = c; 61 | if (i < 32) // || i > 255 62 | sb.Append($"\\u{i:x4}"); 63 | else 64 | sb.Append(c); 65 | break; 66 | } 67 | } 68 | if (!noQuotes) 69 | sb.Append("\""); 70 | 71 | return sb.ToString(); 72 | } 73 | 74 | internal static string ToJson(this double value, int maxDecimals = 2) 75 | { 76 | if (maxDecimals > -1) 77 | value = Math.Round(value, maxDecimals); 78 | 79 | return value.ToString(CultureInfo.InvariantCulture); 80 | } 81 | internal static string ToJson(this bool value) 82 | { 83 | return value ? "true" : "false"; 84 | } 85 | 86 | internal static string ExtractString(string source, 87 | string beginDelim, 88 | string endDelim, 89 | bool caseSensitive = false, 90 | bool allowMissingEndDelimiter = false, 91 | bool returnDelimiters = false) 92 | { 93 | int at1, at2; 94 | 95 | if (string.IsNullOrEmpty(source)) 96 | return string.Empty; 97 | 98 | if (caseSensitive) 99 | { 100 | at1 = source.IndexOf(beginDelim); 101 | if (at1 == -1) 102 | return string.Empty; 103 | 104 | at2 = source.IndexOf(endDelim, at1 + beginDelim.Length); 105 | } 106 | else 107 | { 108 | //string Lower = source.ToLower(); 109 | at1 = source.IndexOf(beginDelim, 0, source.Length, StringComparison.OrdinalIgnoreCase); 110 | if (at1 == -1) 111 | return string.Empty; 112 | 113 | at2 = source.IndexOf(endDelim, at1 + beginDelim.Length, StringComparison.OrdinalIgnoreCase); 114 | } 115 | 116 | if (allowMissingEndDelimiter && at2 < 0) 117 | { 118 | if (!returnDelimiters) 119 | return source.Substring(at1 + beginDelim.Length); 120 | 121 | return source.Substring(at1); 122 | } 123 | 124 | if (at1 > -1 && at2 > 1) 125 | { 126 | if (!returnDelimiters) 127 | return source.Substring(at1 + beginDelim.Length, at2 - at1 - beginDelim.Length); 128 | 129 | return source.Substring(at1, at2 - at1 + endDelim.Length); 130 | } 131 | 132 | return string.Empty; 133 | } 134 | 135 | internal static string LogStringDefaultFile { get; set; } = "d:\\temp\\_LogOutput.txt"; // Path.Combine(Path.GetTempPath(), "_LogOutput.txt"); 136 | 137 | /// 138 | /// Simple Logging method that allows quickly writing a string to a file 139 | /// 140 | /// 141 | /// 142 | /// if not specified used UTF-8 143 | internal static void LogString(string output, string filename=null, Encoding encoding = null) 144 | { 145 | if (encoding == null) 146 | encoding = Encoding.UTF8; 147 | 148 | if (string.IsNullOrEmpty(filename)) 149 | filename = LogStringDefaultFile; 150 | 151 | lock (_logLock) 152 | { 153 | var writer = new StreamWriter(filename, true, encoding); 154 | writer.WriteLine(DateTime.Now + " - " + output); 155 | writer.Close(); 156 | } 157 | } 158 | 159 | private static object _logLock = new object(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Westwind.WebView/Westwind.WebView.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net472;net9.0-windows;net8.0-windows; 4 | true 5 | true 6 | Latest 7 | 0.3.3 8 | Rick Strahl 9 | disable 10 | false 11 | en-US 12 | Westwind.WebView 13 | West Wind WebView Interop Helpers 14 | en-US 15 | Westwind.WebView 16 | Westwind.WebView 17 | West Wind WebView Interop Helpers 18 | A .NET library to aid WebView2 control hosting, .NET/JavaScript interop and Html to Pdf Conversion 19 | A .NET library to aid WebView2 control hosting, .NET/JavaScript interop and Html to Pdf Conversion. 20 | Rick Strahl, West Wind Technologies 2023-2024 21 | WebView Westwind 22 | 23 | http://github.com/rickstrahl/westwind.webview 24 | icon.png 25 | LICENSE.md 26 | true 27 | Rick Strahl, West Wind Technologies, 2023-2024 28 | Github 29 | West Wind Technologies 30 | https://github.com/RickStrahl/Westwind.WebView 31 | git 32 | 33 | 34 | 35 | embedded 36 | $(NoWarn);CS1591;CS1572;CS1573 37 | true 38 | True 39 | ./nupkg 40 | true 41 | RELEASE 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Westwind.WebView/Wpf/BaseJavaScriptInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.Web.WebView2.Wpf; 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Serialization; 8 | 9 | namespace Westwind.WebView.Wpf 10 | { 11 | 12 | /// 13 | /// Helper class that simplifies calling JavaScript functions 14 | /// in a WebView document. Helps with formatting calls using 15 | /// ExecuteScriptAsync() and properly formatting/encoding parameters. 16 | /// It provides an easy way to Invoke methods on a global 17 | /// object that you specify (ie `window` or some other object) using 18 | /// a Reflection like Invocation interface. 19 | /// 20 | /// For generic Window function invocation you can use this class 21 | /// as is. 22 | /// 23 | /// But we recommend that you subclass this class for your application 24 | /// and then implement wrapper methods around each interop call 25 | /// you make into the JavaScript document rather than making the 26 | /// interop calls in your application code. 27 | /// 28 | /// Operations are applied to the `BaseInvocationTarget` which is 29 | /// the base object that operations are run on. This can be the 30 | /// `window.` or any globally accessibly object ie. `window.textEditor.`. 31 | /// 32 | /// You can use Invoke, Set, Get to access object properties, or you can 33 | /// use `ExecuteScriptAsync` to fire raw requests. 34 | /// 35 | /// Parameterize helps with encoding parameters when calling methods 36 | /// and turning them into string parseable values using JSON. 37 | /// 38 | /// *** IMPORTANT *** 39 | /// When subclassing make sure you create the class with 40 | /// ***the same constructor signature as this class*** 41 | /// and preferrably pre-set the `baseInvocationTarget` parameter 42 | /// (`window` or `window.someObject`) 43 | /// 44 | public class BaseJavaScriptInterop 45 | { 46 | /// 47 | /// A string that is used as the object for object invocation 48 | /// By default this is empty which effectively invokes objects 49 | /// in the root namespace (window.). This string should reflect 50 | /// a base that supports appending a method or property acces, 51 | /// meaning it usually will end in a `.` (except for root/empty) 52 | /// such as `object.property.` 53 | /// 54 | /// For other components this might be an root object. In the MM 55 | /// editor for example it's: 56 | /// 57 | /// `window.textEditor.` 58 | /// 59 | public string BaseInvocationTargetString { get; set; } 60 | 61 | /// 62 | /// WebBrowser instance that the interop object operates on 63 | /// 64 | public WebView2 WebBrowser { get; } 65 | 66 | 67 | /// 68 | /// Creates an instance of this interop object to call JavaScript functions 69 | /// in the loaded DOM document. 70 | /// 71 | /// 72 | /// The base 'object' to execute 73 | /// commands on. The default is the global `window.` object. Set with the 74 | /// `.` at the end. 75 | public BaseJavaScriptInterop(WebView2 webBrowser, string baseInvocationTarget = "window") 76 | { 77 | WebBrowser = webBrowser; 78 | if (string.IsNullOrEmpty(baseInvocationTarget)) 79 | baseInvocationTarget = "window."; 80 | if (!baseInvocationTarget.TrimEnd().EndsWith(".")) 81 | baseInvocationTarget += "."; 82 | 83 | BaseInvocationTargetString = baseInvocationTarget; 84 | } 85 | 86 | 87 | #region Serialization 88 | 89 | static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings() 90 | { 91 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 92 | }; 93 | 94 | 95 | /// 96 | /// Helper method that consistently serializes JavaScript with Camelcase 97 | /// 98 | /// 99 | /// 100 | public static string SerializeObject(object data) 101 | { 102 | return JsonConvert.SerializeObject(data, _serializerSettings); 103 | } 104 | 105 | /// 106 | /// Helper method to deserialize json content 107 | /// 108 | /// 109 | /// 110 | public static TResult DeserializeObject(string json) 111 | { 112 | return JsonConvert.DeserializeObject(json, _serializerSettings); 113 | } 114 | 115 | #endregion 116 | 117 | 118 | #region Async Invocation Utilities 119 | 120 | /// 121 | /// Calls a method with simple or no parameters: string, boolean, numbers 122 | /// 123 | /// Method to call 124 | /// Parameters to path or none 125 | /// object result as specified by TResult type 126 | public async Task Invoke(string method, params object[] parameters) 127 | { 128 | StringBuilder sb = new StringBuilder(); 129 | sb.Append(BaseInvocationTargetString + method + "("); 130 | 131 | if (parameters != null) 132 | { 133 | for (var index = 0; index < parameters.Length; index++) 134 | { 135 | object parm = parameters[index]; 136 | var jsonParm = SerializeObject(parm); 137 | sb.Append(jsonParm); 138 | if (index < parameters.Length - 1) 139 | sb.Append(","); 140 | } 141 | } 142 | sb.Append(")"); 143 | 144 | var cmd = sb.ToString(); 145 | string result = await WebBrowser.CoreWebView2.ExecuteScriptAsync(cmd); 146 | 147 | Type resultType = typeof(TResult); 148 | return (TResult) JsonConvert.DeserializeObject(result, resultType); 149 | } 150 | 151 | /// 152 | /// Calls a method with simple parameters: String, number, boolean 153 | /// This version returns no results. 154 | /// 155 | /// 156 | /// 157 | /// 158 | public async Task Invoke(string method, params object[] parameters) 159 | { 160 | StringBuilder sb = new StringBuilder(); 161 | sb.Append(BaseInvocationTargetString + method + "("); 162 | 163 | if (parameters != null) 164 | { 165 | for (var index = 0; index < parameters.Length; index++) 166 | { 167 | object parm = parameters[index]; 168 | var jsonParm = SerializeObject(parm); 169 | sb.Append(jsonParm); 170 | if (index < parameters.Length - 1) 171 | sb.Append(","); 172 | } 173 | } 174 | sb.Append(")"); 175 | 176 | await WebBrowser.CoreWebView2.ExecuteScriptAsync(sb.ToString()); 177 | } 178 | 179 | /// 180 | /// Parameterizes a set of value parameters into string 181 | /// form that can be used in `ExecuteScriptAsync()` calls. 182 | /// Parameters are turned into a string using JSON values 183 | /// that are literal representations of values passed. 184 | /// 185 | /// You can wrap the result into a method call like this: 186 | /// 187 | /// ```csharp 188 | /// var parmData = js.Parameterize( new [] { 'parm1', pos } ); 189 | /// "method(" + parmData + ")" 190 | /// ``` 191 | /// 192 | /// 193 | /// 194 | public string Parameterize(object[] parameters) 195 | { 196 | StringBuilder sb = new StringBuilder(); 197 | if (parameters != null) 198 | { 199 | for (var index = 0; index < parameters.Length; index++) 200 | { 201 | object parm = parameters[index]; 202 | var jsonParm = SerializeObject(parm); 203 | sb.Append(jsonParm); 204 | if (index < parameters.Length - 1) 205 | sb.Append(","); 206 | } 207 | } 208 | 209 | return sb.ToString(); 210 | } 211 | 212 | 213 | /// 214 | /// Sets a property on the editor by name 215 | /// 216 | /// Single property or hierarchical property off window.textEditor 217 | /// Value to set - should be simple value 218 | public async Task Set(string propertyName, object value) 219 | { 220 | 221 | var cmd = BaseInvocationTargetString + propertyName + " = " + 222 | SerializeObject(value) + ";"; 223 | 224 | await WebBrowser.CoreWebView2.ExecuteScriptAsync(cmd); 225 | } 226 | 227 | 228 | /// 229 | /// Gets a property from the window.textEditor object 230 | /// 231 | /// 232 | public async Task Get(string propertyName) 233 | { 234 | var cmd = "return " + BaseInvocationTargetString + propertyName + ";"; 235 | string result = await WebBrowser.CoreWebView2.ExecuteScriptAsync(cmd); 236 | 237 | Type resultType = typeof(TResult); 238 | return DeserializeObject(result); 239 | } 240 | 241 | /// 242 | /// Calls a method on the TextEditor in JavaScript a single JSON encoded 243 | /// value or object. The receiving function should expect a JSON object and parse it. 244 | /// 245 | /// This version returns no result value. 246 | /// 247 | public async Task CallMethodWithJson(string method, object parameter = null) 248 | { 249 | string cmd = method; 250 | 251 | if (parameter != null) 252 | { 253 | var jsonParm = SerializeObject(parameter); 254 | cmd += "(" + jsonParm + ")"; 255 | } 256 | 257 | await WebBrowser.CoreWebView2.ExecuteScriptAsync(cmd); 258 | } 259 | 260 | /// 261 | /// Calls a method on the TextEditor in JavaScript a single JSON encoded 262 | /// value or object. The receiving function should expect a JSON object and parse it. 263 | /// 264 | /// 265 | /// 266 | /// 267 | public async Task CallMethodWithJson(string method, object parameter = null) 268 | { 269 | string cmd = method; 270 | 271 | if (parameter != null) 272 | { 273 | var jsonParm = SerializeObject(parameter); 274 | cmd += "(" + jsonParm + ")"; 275 | } 276 | 277 | string result = await WebBrowser.CoreWebView2.ExecuteScriptAsync(cmd); 278 | return DeserializeObject(result); 279 | } 280 | 281 | /// 282 | /// Calls a method on the TextEditor in JavaScript a single JSON encoded 283 | /// value or object. The receiving function should expect a JSON object and parse it. 284 | /// 285 | public async Task ExecuteScriptAsync(string script) 286 | { 287 | await WebBrowser.CoreWebView2.ExecuteScriptAsync(script); 288 | } 289 | 290 | /// 291 | /// Calls a method on the TextEditor in JavaScript a single JSON encoded 292 | /// value or object. 293 | /// 294 | public async Task ExecuteScriptAsyncWithResult(string script) 295 | { 296 | var result = await WebBrowser.CoreWebView2.ExecuteScriptAsync(script); 297 | if (result == null) 298 | return default(TResult); 299 | 300 | return DeserializeObject(result); 301 | } 302 | 303 | #endregion 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /Westwind.WebView/Wpf/CachedWebViewEnvironment.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Web.WebView2.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | using Microsoft.Web.WebView2.Wpf; 9 | using System.Reflection; 10 | using System.Threading; 11 | 12 | namespace Westwind.WebView.Wpf 13 | { 14 | 15 | 16 | /// 17 | /// A wrapper class that provide a single point of initialization for the WebView2 18 | /// environment to ensure that only a single instance of the environment is created 19 | /// and used. This is important to avoid failures loading the environment with different 20 | /// environment settings is not actively supported by the WebView - IOW, it only 21 | /// supports a single environment per process and the first one determines the settings used, 22 | /// and unfortunately repeated instantiations with different settings can and often fail. 23 | /// 24 | /// This cached environment ensures that only a single instance of the environment is created 25 | /// and used and provides a single point of initialization for the environment. 26 | /// 27 | public class CachedWebViewEnvironment 28 | { 29 | /// 30 | /// Cached instance used by default 31 | /// 32 | public static CachedWebViewEnvironment Current { get; set; } = new CachedWebViewEnvironment(); 33 | 34 | /// 35 | /// The Cached WebView Environment - one copy per running process 36 | /// 37 | public CoreWebView2Environment Environment { get; set; } 38 | 39 | /// 40 | /// The global setting for the folder where the WebView2 environment is stored. 41 | /// Defaults to the Temp folder of the current user. If not set. We recommend 42 | /// you set this folder **early** in your application startup. 43 | /// 44 | public string EnvironmentFolderName { get; set; } 45 | 46 | /// 47 | /// Optional WebView Environment options that can be set before the environment is created. 48 | /// Like the folder we recommend you set this early in your application startup. 49 | /// 50 | public CoreWebView2EnvironmentOptions EnvironmentOptions { get; set; } 51 | 52 | 53 | /// 54 | /// Ensure only one instance initializes the environment at a time to avoid 55 | /// multiple environment versions. Only applies to environment load, not waiting 56 | /// for the initialization to complete which can take a long time. 57 | /// 58 | private static SemaphoreSlim _EnvironmentLoadLock = new SemaphoreSlim(1, 1); 59 | 60 | 61 | /// 62 | /// This method provides a single point of initialization for the WebView2 environment 63 | /// to ensure that only a single instance of the environment is created. This is important 64 | /// to avoid failures loading the environment with different settings. 65 | /// 66 | /// Once the environment is created it's cached in the Environment property and reused 67 | /// on subsequent calls. While possible to override the environment **we don't recommend it**. 68 | /// 69 | /// 70 | /// This method does not complete until the WebView is UI activated. If not visible 71 | /// an `await` call will not complete until it becomes visible 72 | /// (due to internal `WebView2.EnsureCoreWebView2Async()` behavior) 73 | /// 74 | /// WebBrowser instance to set the environment on 75 | /// Optionally pass in an existing configured environment 76 | /// 77 | /// 78 | public async Task InitializeWebViewEnvironment(WebView2 webBrowser, CoreWebView2Environment environment = null, string webViewEnvironemntPath = null) 79 | { 80 | try 81 | { 82 | 83 | if (environment == null) 84 | environment = Environment; 85 | 86 | if (environment == null) 87 | { 88 | // lock 89 | await _EnvironmentLoadLock.WaitAsync(); 90 | 91 | if (environment == null) 92 | { 93 | var envPath = webViewEnvironemntPath ?? Current.EnvironmentFolderName; 94 | if (string.IsNullOrEmpty(envPath)) 95 | Current.EnvironmentFolderName = Path.Combine(Path.GetTempPath(), 96 | Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location) + "_WebView"); 97 | 98 | // must create a data folder if running out of a secured folder that can't write like Program Files 99 | environment = await CoreWebView2Environment.CreateAsync(userDataFolder: EnvironmentFolderName, 100 | options: EnvironmentOptions); 101 | 102 | Environment = environment; 103 | } 104 | 105 | _EnvironmentLoadLock.Release(); 106 | } 107 | await webBrowser.EnsureCoreWebView2Async(environment); 108 | } 109 | catch (Exception ex) 110 | { 111 | throw new WebViewInitializationException($"WebView EnsureCoreWebView2AsyncCall failed.\nFolder: {EnvironmentFolderName}", ex); 112 | } 113 | 114 | } 115 | 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Westwind.WebView/Wpf/WebView2Extensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Microsoft.Web.WebView2.Wpf 6 | { 7 | public static class WebView2Extensions 8 | { 9 | public static async Task NavigateToStringSafe(this WebView2 webView, string html) 10 | { 11 | webView.Source = new Uri("about:blank"); 12 | 13 | string encodedHtml = JsonConvert.SerializeObject(html); 14 | string script = "window.document.write(" + encodedHtml + ")"; 15 | 16 | await webView.EnsureCoreWebView2Async(); // make sure WebView is ready 17 | await webView.ExecuteScriptAsync(script); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Westwind.WebView/Wpf/WebViewInitializationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Westwind.WebView.Wpf 4 | { 5 | public class WebViewInitializationException : Exception 6 | { 7 | public WebViewInitializationException() 8 | { } 9 | public WebViewInitializationException(string message) : base(message) 10 | { } 11 | public WebViewInitializationException(string message, Exception innerException) : base(message, innerException) 12 | { } 13 | } 14 | } -------------------------------------------------------------------------------- /Westwind.WebView/Wpf/WebViewUtilities.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Web.WebView2.Core; 2 | using Microsoft.Win32; 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using Westwind.WebView.Utilities; 7 | 8 | namespace Westwind.WebView.Wpf 9 | { 10 | /// 11 | /// Installation and environment helpers for the WebView2 12 | /// control 13 | /// 14 | public class WebViewUtilities 15 | { 16 | 17 | /// 18 | /// This method checks to see if the WebView runtime is installed. It doesn't 19 | /// check for a specific version, just whether the runtime is installed at all. 20 | /// For a specific version check use IsWebViewVersionInstalled() 21 | /// 22 | /// HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5} 23 | /// HKEY_CURRENT_USER\Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5 24 | /// 25 | /// 26 | public static bool IsWebViewRuntimeInstalled() 27 | { 28 | 29 | string regKey = @"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients"; 30 | using (RegistryKey edgeKey = Registry.LocalMachine.OpenSubKey(regKey)) 31 | { 32 | if (edgeKey != null) 33 | { 34 | string[] productKeys = edgeKey.GetSubKeyNames(); 35 | if (productKeys.Any()) 36 | { 37 | return true; 38 | } 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | 46 | /// 47 | /// This method checks to see if the WebView runtime is installed and whether it's 48 | /// equal or greater than the WebView .NET component. 49 | /// 50 | /// This ensures that you use a version of the WebView Runtime that supports the features 51 | /// of the .NET Component. 52 | /// 53 | /// Should be called during app startup to ensure the WebView Runtime is available. 54 | /// 55 | public static bool IsWebViewVersionInstalled() 56 | { 57 | try 58 | { 59 | var versionNo = CoreWebView2Environment.GetAvailableBrowserVersionString(null); 60 | 61 | // strip off 'canary' or 'stable' version 62 | var at = versionNo.IndexOf(" "); 63 | if (at < 1) return false; 64 | versionNo = versionNo.Substring(0, at -1); 65 | 66 | //versionNo = StringUtils.ExtractString(versionNo, "", " ", allowMissingEndDelimiter: true)?.Trim(); 67 | var ver = new Version(versionNo); 68 | 69 | var asmVersion = typeof(CoreWebView2Environment).Assembly.GetName().Version; 70 | 71 | if (ver.Build >= asmVersion.Build) 72 | return true; 73 | } 74 | catch 75 | { 76 | // ignored 77 | } 78 | 79 | return false; 80 | } 81 | 82 | 83 | /// 84 | /// Removes the applications local WebView Environment 85 | /// 86 | /// 87 | /// true if the directory exists and was successfully deleted. 88 | /// false if directory doesn't exist, or the directory deletion fails. 89 | /// 90 | public static bool RemoveEnvironmentFolder(string folder) 91 | { 92 | if (string.IsNullOrEmpty(folder)) 93 | return false; 94 | 95 | var checkPath = Path.Combine(folder, "EbWebView"); 96 | if (Directory.Exists(checkPath)) 97 | { 98 | Directory.Delete(folder); 99 | if (!Directory.Exists(checkPath)) 100 | return true; 101 | } 102 | 103 | return false; 104 | } 105 | 106 | /// 107 | /// Returns the WebView SDK 108 | /// 109 | /// 110 | /// 111 | public static string GetWebViewRuntimeVersion(bool returnSdk = false) 112 | { 113 | try 114 | { 115 | if (!returnSdk) 116 | return CoreWebView2Environment.GetAvailableBrowserVersionString(null); 117 | 118 | return typeof(CoreWebView2Environment).Assembly.GetName().Version.ToString(); 119 | } 120 | catch 121 | { 122 | return null; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Westwind.WebView/publish-nuget.ps1: -------------------------------------------------------------------------------- 1 | if (test-path ./nupkg) { 2 | remove-item ./nupkg -Force -Recurse 3 | } 4 | 5 | dotnet build -c Release 6 | 7 | $filename = gci "./nupkg/*.nupkg" | sort LastWriteTime | select -last 1 | select -ExpandProperty "Name" 8 | Write-host $filename 9 | $len = $filename.length 10 | 11 | if ($len -gt 0) { 12 | Write-Host "signing... $filename" 13 | nuget sign ".\nupkg\$filename" -CertificateSubject "West Wind Technologies" -timestamper " http://timestamp.digicert.com" 14 | nuget push ".\nupkg\$filename" -source "https://nuget.org" 15 | 16 | Write-Host "Done." 17 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickStrahl/Westwind.WebView/cbf7323a89ba99a91ce1a395f80dcf29a6899d12/icon.png --------------------------------------------------------------------------------