// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Drawing.Drawing2D; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using Common.Utilities; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; namespace Microsoft.PowerToys.ThumbnailHandler.Svg { /// /// SVG Thumbnail Provider. /// public class SvgThumbnailProvider : IDisposable { public SvgThumbnailProvider(string filePath) { FilePath = filePath; if (FilePath != null && File.Exists(FilePath)) { Stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); } } /// /// Gets the file path to the file creating thumbnail for. /// public string FilePath { get; private set; } /// /// Gets the stream object to access file. /// public Stream Stream { get; private set; } /// /// Gets or sets signalled when the main thread can use preprocessed svg contents. /// public ManualResetEventSlim SvgContentsReady { get; set; } = new ManualResetEventSlim(false); public string SvgContents { get; set; } = string.Empty; /// /// The maximum dimension (width or height) thumbnail we will generate. /// private const uint MaxThumbnailSize = 10000; /// /// WebView2 Control to display Svg. /// private WebView2 _browser; /// /// WebView2 Environment /// private CoreWebView2Environment _webView2Environment; /// /// Name of the virtual host /// private const string VirtualHostName = "PowerToysLocalSvgThumbnail"; /// /// URI of the local file saved with the contents /// private Uri _localFileURI; /// /// Gets the path of the current assembly. /// /// /// Source: https://stackoverflow.com/a/283917/14774889 /// private static string AssemblyDirectory { get { string codeBase = Assembly.GetExecutingAssembly().Location; UriBuilder uri = new UriBuilder(codeBase); string path = Uri.UnescapeDataString(uri.Path); return Path.GetDirectoryName(path); } } /// /// Represent WebView2 user data folder path. /// private string _webView2UserDataFolder = System.Environment.GetEnvironmentVariable("USERPROFILE") + "\\AppData\\LocalLow\\Microsoft\\PowerToys\\SvgThumbnailPreview-Temp"; /// /// Render SVG using WebView2 control, capture the WebView2 /// preview and create Bitmap out of it. /// /// The maximum thumbnail size, in pixels. public Bitmap GetThumbnailImpl(uint cx) { CleanupWebView2UserDataFolder(); if (cx == 0 || cx > MaxThumbnailSize) { return null; } Bitmap thumbnail = null; var thumbnailDone = new ManualResetEventSlim(false); _browser = new WebView2(); _browser.Dock = DockStyle.Fill; _browser.Visible = true; _browser.Width = (int)cx; _browser.Height = (int)cx; _browser.NavigationCompleted += async (object sender, CoreWebView2NavigationCompletedEventArgs args) => { var a = await _browser.ExecuteScriptAsync($"document.getElementsByTagName('svg')[0].viewBox;"); if (a != null) { await _browser.ExecuteScriptAsync($"document.getElementsByTagName('svg')[0].style = 'width:100%;height:100%';"); } // Hide scrollbar - fixes #18286 await _browser.ExecuteScriptAsync("document.querySelector('body').style.overflow='hidden'"); MemoryStream ms = new MemoryStream(); await _browser.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Png, ms); thumbnail = new Bitmap(ms); if (thumbnail.Width != cx && thumbnail.Height != cx && thumbnail.Width != 0 && thumbnail.Height != 0) { // We are not the appropriate size for caller. Resize now while // respecting the aspect ratio. float scale = Math.Min((float)cx / thumbnail.Width, (float)cx / thumbnail.Height); int scaleWidth = (int)(thumbnail.Width * scale); int scaleHeight = (int)(thumbnail.Height * scale); thumbnail = ResizeImage(thumbnail, scaleWidth, scaleHeight); } thumbnailDone.Set(); }; var webView2Options = new CoreWebView2EnvironmentOptions("--block-new-web-contents"); ConfiguredTaskAwaitable.ConfiguredTaskAwaiter webView2EnvironmentAwaiter = CoreWebView2Environment .CreateAsync(userDataFolder: _webView2UserDataFolder, options: webView2Options) .ConfigureAwait(true).GetAwaiter(); webView2EnvironmentAwaiter.OnCompleted(async () => { try { _webView2Environment = webView2EnvironmentAwaiter.GetResult(); await _browser.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true); _browser.CoreWebView2.SetVirtualHostNameToFolderMapping(VirtualHostName, AssemblyDirectory, CoreWebView2HostResourceAccessKind.Deny); _browser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; _browser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; _browser.CoreWebView2.Settings.AreDevToolsEnabled = false; _browser.CoreWebView2.Settings.AreHostObjectsAllowed = false; _browser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false; _browser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false; _browser.CoreWebView2.Settings.IsScriptEnabled = false; _browser.CoreWebView2.Settings.IsWebMessageEnabled = false; // Don't load any resources. _browser.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); _browser.CoreWebView2.WebResourceRequested += (object sender, CoreWebView2WebResourceRequestedEventArgs e) => { // Show local file we've saved with the svg contents. Block all else. if (new Uri(e.Request.Uri) != _localFileURI) { e.Response = _browser.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Forbidden", null); } }; // WebView2.NavigateToString() limitation // See https://learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2.navigatetostring?view=webview2-dotnet-1.0.864.35#remarks // While testing the limit, it turned out it is ~1.5MB, so to be on a safe side we go for 1.5m bytes SvgContentsReady.Wait(); if (string.IsNullOrEmpty(SvgContents) || !SvgContents.Contains("svg")) { thumbnailDone.Set(); return; } if (SvgContents.Length > 1_500_000) { string filename = _webView2UserDataFolder + "\\" + Guid.NewGuid().ToString() + ".html"; File.WriteAllText(filename, SvgContents); _localFileURI = new Uri(filename); _browser.Source = _localFileURI; } else { _browser.NavigateToString(SvgContents); } } catch (Exception) { } }); while (!thumbnailDone.Wait(75)) { Application.DoEvents(); } _browser.Dispose(); return thumbnail; } /// /// Wrap the SVG markup in HTML with a meta tag to render it /// using WebView2 control. /// We also set the padding and margin for the body to zero as /// there is a default margin of 8. /// /// The original SVG markup. /// The SVG content wrapped in HTML. public static string WrapSVGInHTML(string svg) { string html = @" {0} "; // Using InvariantCulture since this should be displayed as it is return string.Format(CultureInfo.InvariantCulture, html, svg); } /// /// Resize the image with high quality to the specified width and height. /// /// The image to resize. /// The width to resize to. /// The height to resize to. /// The resized image. public static Bitmap ResizeImage(Image image, int width, int height) { if (width <= 0 || height <= 0 || width > MaxThumbnailSize || height > MaxThumbnailSize || image == null) { return null; } Bitmap destImage = new Bitmap(width, height); destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); using (var graphics = Graphics.FromImage(destImage)) { graphics.CompositingMode = CompositingMode.SourceCopy; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.Clear(Color.White); graphics.DrawImage(image, 0, 0, width, height); } return destImage; } /// /// Generate thumbnail bitmap for provided Gcode file/stream. /// /// Maximum thumbnail size, in pixels. /// Generated bitmap public Bitmap GetThumbnail(uint cx) { if (cx == 0 || cx > MaxThumbnailSize) { return null; } if (global::PowerToys.GPOWrapper.GPOWrapper.GetConfiguredSvgThumbnailsEnabledValue() == global::PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) { // GPO is disabling this utility. return null; } if (Stream != null) { new Thread(() => { string svgData = null; using (var reader = new StreamReader(Stream)) { svgData = reader.ReadToEnd(); try { // Fixes #17527 - Inkscape v1.1 swapped order of default and svg namespaces in svg file (default first, svg after). // That resulted in parser being unable to parse it correctly and instead of svg, text was previewed. // MS Edge and Firefox also couldn't preview svg files with mentioned order of namespaces definitions. svgData = SvgPreviewHandlerHelper.SwapNamespaces(svgData); svgData = SvgPreviewHandlerHelper.AddStyleSVG(svgData); SvgContents = WrapSVGInHTML(svgData); SvgContentsReady.Set(); } catch (Exception) { SvgContentsReady.Set(); } } }).Start(); } else { SvgContentsReady.Set(); } using (Bitmap thumbnail = GetThumbnailImpl(cx)) { if (thumbnail != null && thumbnail.Size.Width > 0 && thumbnail.Size.Height > 0) { return (Bitmap)thumbnail.Clone(); } } return null; } public void Dispose() { GC.SuppressFinalize(this); } /// /// Cleanup the previously created tmp html files from svg files bigger than 2MB. /// private void CleanupWebView2UserDataFolder() { try { // Cleanup temp dir var dir = new DirectoryInfo(_webView2UserDataFolder); foreach (var file in dir.EnumerateFiles("*.html")) { file.Delete(); } } catch (Exception) { } } } }