// 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; using System.Drawing; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices.ComTypes; using System.Windows.Forms; using Common; using Common.Utilities; using Microsoft.PowerToys.PreviewHandler.Svg.Telemetry.Events; using Microsoft.PowerToys.Telemetry; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; namespace Microsoft.PowerToys.PreviewHandler.Svg { /// /// Implementation of Control for Svg Preview Handler. /// public class SvgPreviewControl : FormHandlerControl { /// /// WebView2 Control to display Svg. /// private WebView2 _browser; /// /// WebView2 Environment /// private CoreWebView2Environment _webView2Environment; /// /// Name of the virtual host /// private const string VirtualHostName = "PowerToysLocalSvg"; /// /// 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); } } /// /// Text box to display the information about blocked elements from Svg. /// private RichTextBox _textBox; /// /// Represent if an text box info bar is added for showing message. /// private bool _infoBarAdded; /// /// Represent WebView2 user data folder path. /// private string _webView2UserDataFolder = System.Environment.GetEnvironmentVariable("USERPROFILE") + "\\AppData\\LocalLow\\Microsoft\\PowerToys\\SvgPreview-Temp"; /// /// Start the preview on the Control. /// /// Stream reference to access source file. public override void DoPreview(T dataSource) { CleanupWebView2UserDataFolder(); string svgData = null; bool blocked = false; try { using (var stream = new ReadonlyStream(dataSource as IStream)) { using (var reader = new StreamReader(stream)) { svgData = reader.ReadToEnd(); } } blocked = SvgPreviewHandlerHelper.CheckBlockedElements(svgData); } catch (Exception ex) { PreviewError(ex, dataSource); return; } try { svgData = SvgPreviewHandlerHelper.AddStyleSVG(svgData); } catch (Exception ex) { PowerToysTelemetry.Log.WriteEvent(new SvgFilePreviewError { Message = ex.Message }); } InvokeOnControlThread(() => { try { _infoBarAdded = false; // Add a info bar on top of the Preview if any blocked element is present. if (blocked) { _infoBarAdded = true; AddTextBoxControl(Properties.Resource.BlockedElementInfoText); } AddWebViewControl(svgData); Resize += FormResized; base.DoPreview(dataSource); PowerToysTelemetry.Log.WriteEvent(new SvgFilePreviewed()); } catch (Exception ex) { PreviewError(ex, dataSource); } }); } /// /// Occurs when RichtextBox is resized. /// /// Reference to resized control. /// Provides data for the ContentsResized event. private void RTBContentsResized(object sender, ContentsResizedEventArgs e) { var richTextBox = sender as RichTextBox; richTextBox.Height = e.NewRectangle.Height + 5; } /// /// Occurs when form is resized. /// /// Reference to resized control. /// Provides data for the resize event. private void FormResized(object sender, EventArgs e) { if (_infoBarAdded) { _textBox.Width = Width; } } /// /// Adds a WebView2 Control to Control Collection. /// /// Svg to display on Browser Control. private void AddWebViewControl(string svgData) { _browser = new WebView2(); _browser.Dock = DockStyle.Fill; ConfiguredTaskAwaitable.ConfiguredTaskAwaiter webView2EnvironmentAwaiter = CoreWebView2Environment .CreateAsync(userDataFolder: _webView2UserDataFolder) .ConfigureAwait(true).GetAwaiter(); webView2EnvironmentAwaiter.OnCompleted(() => { InvokeOnControlThread(async () => { try { _webView2Environment = webView2EnvironmentAwaiter.GetResult(); await _browser.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true); await _browser.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.addEventListener('contextmenu', window => {window.preventDefault();});"); _browser.CoreWebView2.SetVirtualHostNameToFolderMapping(VirtualHostName, AssemblyDirectory, CoreWebView2HostResourceAccessKind.Allow); _browser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; // WebView2.NavigateToString() limitation // See https://docs.microsoft.com/en-us/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 if (svgData.Length > 1_500_000) { string filename = _webView2UserDataFolder + "\\" + Guid.NewGuid().ToString() + ".html"; File.WriteAllText(filename, svgData); _browser.Source = new Uri(filename); } else { _browser.NavigateToString(svgData); } Controls.Add(_browser); } catch (Exception) { } }); }); } /// /// Adds a Text Box in Controls for showing information about blocked elements. /// /// Message to be displayed in textbox. private void AddTextBoxControl(string message) { _textBox = new RichTextBox(); _textBox.Text = message; _textBox.BackColor = Color.LightYellow; _textBox.Multiline = true; _textBox.Dock = DockStyle.Top; _textBox.ReadOnly = true; _textBox.ContentsResized += RTBContentsResized; _textBox.ScrollBars = RichTextBoxScrollBars.None; _textBox.BorderStyle = BorderStyle.None; Controls.Add(_textBox); } /// /// Called when an error occurs during preview. /// /// The exception which occurred. /// Stream reference to access source file. private void PreviewError(Exception exception, T dataSource) { PowerToysTelemetry.Log.WriteEvent(new SvgFilePreviewError { Message = exception.Message }); Controls.Clear(); _infoBarAdded = true; AddTextBoxControl(Properties.Resource.SvgNotPreviewedError); base.DoPreview(dataSource); } /// /// 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) { } } } }