From 1d358af600be4d2db060369f64bde9595f0b14e2 Mon Sep 17 00:00:00 2001 From: gokcekantarci <115616017+gokcekantarci@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:25:51 +0300 Subject: [PATCH] Svg thumbnail failed rendering (#32936) * [SVGThumbnail] Check fill-rule in SVG file and apply it. * [SVGThumbnail] Comment added * [SVGThumbnail] SvgContents is used instead of all html content. * [SVGThumbnail] - Use SvgContents to extract and modify SVG styles efficiently - Add retry logic and error handling for WebView2 initialization * use Linq --------- Co-authored-by: Jaime Bernardo Co-authored-by: Leilei Zhang --- .../SvgThumbnailProvider.cs | 192 ++++++++++++------ 1 file changed, 125 insertions(+), 67 deletions(-) diff --git a/src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.cs b/src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.cs index 2a5a8f2eae..c312f5d5b4 100644 --- a/src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.cs +++ b/src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.cs @@ -5,8 +5,7 @@ using System.Drawing.Drawing2D; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; -using System.Threading; - +using System.Xml.Linq; using Common.Utilities; using ManagedCommon; using Microsoft.Web.WebView2.Core; @@ -118,92 +117,151 @@ namespace Microsoft.PowerToys.ThumbnailHandler.Svg _browser.Height = (int)cx; _browser.NavigationCompleted += async (object sender, CoreWebView2NavigationCompletedEventArgs args) => { - var a = await _browser.ExecuteScriptAsync($"document.getElementsByTagName('svg')[0].viewBox;"); - if (a != null) + try { - await _browser.ExecuteScriptAsync($"document.getElementsByTagName('svg')[0].style = 'width:100%;height:100%';"); + // Check if the SVG element is present + var a = await _browser.ExecuteScriptAsync($"document.getElementsByTagName('svg')[0].viewBox;"); + if (a != null) + { + var svgContent = SvgContents.Substring(SvgContents.IndexOf("", StringComparison.OrdinalIgnoreCase) - SvgContents.IndexOf("".Length); + + Dictionary styleDict = new Dictionary(); + + // Try to parse the SVG content + try + { + // Attempt to parse the svgContent + var svgDocument = XDocument.Parse(svgContent); + var svgElement = svgDocument.Root; + var currentStyle = svgElement?.Attribute("style")?.Value; + + // If style attribute exists, preserve existing styles + if (!string.IsNullOrEmpty(currentStyle) && currentStyle != "null") + { + styleDict = currentStyle + .Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(stylePart => stylePart.Split(':', 2, StringSplitOptions.TrimEntries)) + .Where(styleKeyValue => styleKeyValue.Length == 2 && !string.IsNullOrEmpty(styleKeyValue[0])) + .ToDictionary( + styleKeyValue => styleKeyValue[0], + styleKeyValue => styleKeyValue[1]); + } + } + catch (Exception ex) + { + // Log the error if the SVG content is not valid or parsing fails + Logger.LogError($"Failed to parse SVG content: {ex.Message}"); + } + + // Add or replace width and height in the existing style + styleDict["width"] = "100%"; + styleDict["height"] = "100%"; + + // Construct a single JavaScript string to set all properties + var styleScript = string.Join(";", styleDict.Select(kv => $"document.getElementsByTagName('svg')[0].style.setProperty('{kv.Key}', '{kv.Value}');")); + + // Apply the new style attributes using the constructed script + await _browser.ExecuteScriptAsync(styleScript); + } + + // 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(); } - - // 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) + catch (Exception ex) { - // 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); + Logger.LogError("Error during NavigationCompleted: ", ex); + thumbnailDone.Set(); } - - thumbnailDone.Set(); }; var webView2Options = new CoreWebView2EnvironmentOptions("--block-new-web-contents"); ConfiguredTaskAwaitable.ConfiguredTaskAwaiter - webView2EnvironmentAwaiter = CoreWebView2Environment - .CreateAsync(userDataFolder: _webView2UserDataFolder, options: webView2Options) - .ConfigureAwait(true).GetAwaiter(); + webView2EnvironmentAwaiter = CoreWebView2Environment + .CreateAsync(userDataFolder: _webView2UserDataFolder, options: webView2Options) + .ConfigureAwait(true).GetAwaiter(); webView2EnvironmentAwaiter.OnCompleted(async () => { - try + for (int attempt = 0; attempt < 3; attempt++) { - _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) => + try { - // Show local file we've saved with the svg contents. Block all else. - if (new Uri(e.Request.Uri) != _localFileURI) + _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) => { - e.Response = _browser.CoreWebView2.Environment.CreateWebResourceResponse(null, 403, "Forbidden", null); + // 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; } - }; - // 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); + } - if (SvgContents.Length > 1_500_000) - { - string filename = _webView2UserDataFolder + "\\" + Guid.NewGuid().ToString() + ".html"; - File.WriteAllText(filename, SvgContents); - _localFileURI = new Uri(filename); - _browser.Source = _localFileURI; + break; // Exit the retry loop if initialization succeeds } - else + catch (Exception ex) { - _browser.NavigateToString(SvgContents); + Logger.LogError($"Initialization attempt {attempt} failed: {ex.Message}"); + if (attempt == 2) + { + Logger.LogError($"Failed running webView2Environment completed for {FilePath} : ", ex); + thumbnailDone.Set(); + return; + } + + await Task.Delay(1000); // Delay before retrying } } - catch (Exception ex) - { - Logger.LogError($"Failed running webView2Environment completed for {FilePath} : ", ex); - thumbnailDone.Set(); - } }); while (!thumbnailDone.Wait(75))