Add SVG Thumbnail Provider for Windows Explorer (#5048)

* Add SVG Thumbnail Provider

* Some cleanup

* Small settings changes

* Update PowerToys.sln

Remove Any CPU entries

* Fix project configuration issues

* Fix bad merge

* Update output path for SVG thumbnail provider

* Sync with latest
This commit is contained in:
Chris Davis
2020-07-21 16:27:12 -07:00
committed by GitHub
parent 8e5a877997
commit 40d2ef4f0a
28 changed files with 1610 additions and 687 deletions

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "Microsoft Corporation",
"copyrightText": "Copyright (c) {companyName}\r\nThe {companyName} licenses this file to you under the MIT license.\r\nSee the LICENSE file in the project root for more information.",
"xmlHeader": false,
"headerDecoration": "",
"fileNamingConvention": "metadata",
"documentInterfaces": false,
"documentExposedElements": false,
"documentInternalElements": false
},
"layoutRules": {
"newlineAtEndOfFile": "require"
},
"orderingRules": {
"usingDirectivesPlacement": "outsideNamespace"
}
}
}

View File

@@ -0,0 +1,244 @@
// 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.
namespace SvgThumbnailProvider
{
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Windows.Forms;
using Common;
using Common.ComInterlop;
using Common.Utilities;
using Microsoft.PowerToys.Telemetry;
using PreviewHandlerCommon;
/// <summary>
/// SVG Thumbnail Provider.
/// </summary>
[Guid("36B27788-A8BB-4698-A756-DF9F11F64F84")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class SvgThumbnailProvider : IInitializeWithStream, IThumbnailProvider
{
/// <summary>
/// Gets the stream object to access file.
/// </summary>
public IStream Stream { get; private set; }
/// <summary>
/// The maxium dimension (width or height) thumbnail we will generate.
/// </summary>
private const uint MaxThumbnailSize = 10000;
/// <summary>
/// Captures an image representation of browser contents.
/// </summary>
/// <param name="browser">The WebBrowser instance rendring the SVG.</param>
/// <param name="rectangle">The client rectangle to capture from.</param>
/// <param name="backgroundColor">The default background color to apply.</param>
/// <returns>A Bitmap representing the browser contents.</returns>
public static Bitmap GetBrowserContentImage(WebBrowser browser, Rectangle rectangle, Color backgroundColor)
{
Bitmap image = new Bitmap(rectangle.Width, rectangle.Height);
using (Graphics graphics = Graphics.FromImage(image))
{
IntPtr deviceContextHandle = IntPtr.Zero;
RECT rect = new RECT
{
Left = rectangle.Left,
Top = rectangle.Top,
Right = rectangle.Right,
Bottom = rectangle.Bottom,
};
graphics.Clear(backgroundColor);
try
{
deviceContextHandle = graphics.GetHdc();
IViewObject viewObject = browser.ActiveXInstance as IViewObject;
viewObject.Draw(1, -1, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, deviceContextHandle, ref rect, IntPtr.Zero, IntPtr.Zero, 0);
}
finally
{
if (deviceContextHandle != IntPtr.Zero)
{
graphics.ReleaseHdc(deviceContextHandle);
}
}
}
return image;
}
/// <summary>
/// Wrap the SVG markup in HTML with a meta tag to ensure the
/// WebBrowser control is in Edge mode to enable SVG rendering.
/// We also set the padding and margin for the body to zero as
/// there is a default margin of 8.
/// </summary>
/// <param name="content">The content to render.</param>
/// <param name="cx">The maximum thumbnail size, in pixels.</param>
/// <returns>A thumbnail of the rendered content.</returns>
public static Bitmap GetThumbnail(string content, uint cx)
{
if (cx > MaxThumbnailSize)
{
return null;
}
Bitmap thumbnail = null;
// Wrap the SVG content in HTML in IE Edge mode so we can ensure
// we render properly.
string wrappedContent = WrapSVGInHTML(content);
WebBrowserExt browser = new WebBrowserExt();
browser.Dock = DockStyle.Fill;
browser.IsWebBrowserContextMenuEnabled = false;
browser.ScriptErrorsSuppressed = true;
browser.ScrollBarsEnabled = false;
browser.AllowNavigation = false;
browser.Width = (int)cx;
browser.Height = (int)cx;
browser.DocumentText = wrappedContent;
// Wait for the browser to render the content.
while (browser.IsBusy || browser.ReadyState != WebBrowserReadyState.Complete)
{
Application.DoEvents();
}
// Check size of the rendered SVG.
var svg = browser.Document.GetElementsByTagName("svg").Cast<HtmlElement>().FirstOrDefault();
if (svg != null)
{
// Update the size of the browser control to fit the SVG
// in the visible viewport.
browser.Width = svg.OffsetRectangle.Width;
browser.Height = svg.OffsetRectangle.Height;
// Wait for the browser to render the content.
while (browser.IsBusy || browser.ReadyState != WebBrowserReadyState.Complete)
{
Application.DoEvents();
}
// Capture the image of the SVG from the browser.
thumbnail = GetBrowserContentImage(browser, svg.OffsetRectangle, Color.White);
if (thumbnail.Width != cx && thumbnail.Height != cx)
{
// 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);
}
}
return thumbnail;
}
/// <summary>
/// Wrap the SVG markup in HTML with a meta tag to ensure the
/// WebBrowser control is in Edge mode to enable SVG rendering.
/// We also set the padding and margin for the body to zero as
/// there is a default margin of 8.
/// </summary>
/// <param name="svg">The original SVG markup.</param>
/// <returns>The SVG content wrapped in HTML.</returns>
public static string WrapSVGInHTML(string svg)
{
string html = @"
<head>
<meta http-equiv='X-UA-Compatible' content='IE=Edge'>
</head>
<html>
<body style='padding:0px;margin:0px;' scroll='no'>
{0}
</body>
</html>";
return string.Format(html, svg);
}
/// <summary>
/// Resize the image with high quality to the specified width and height.
/// </summary>
/// <param name="image">The image to resize.</param>
/// <param name="width">The width to resize to.</param>
/// <param name="height">The height to resize to.</param>
/// <returns>The resized image.</returns>
public static Bitmap ResizeImage(Image image, int width, int height)
{
if (width <= 0 || height <= 0 ||
width > MaxThumbnailSize || height > MaxThumbnailSize)
{
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;
}
/// <inheritdoc/>
public void Initialize(IStream pstream, uint grfMode)
{
// Ignore the grfMode always use read mode to access the file.
this.Stream = pstream;
}
/// <inheritdoc/>
public void GetThumbnail(uint cx, out IntPtr phbmp, out WTS_ALPHATYPE pdwAlpha)
{
phbmp = IntPtr.Zero;
pdwAlpha = WTS_ALPHATYPE.WTSAT_UNKNOWN;
if (cx == 0 || cx > MaxThumbnailSize)
{
return;
}
string svgData = null;
using (var stream = new StreamWrapper(this.Stream as IStream))
{
using (var reader = new StreamReader(stream))
{
svgData = reader.ReadToEnd();
}
}
if (svgData != null)
{
Bitmap thumbnail = GetThumbnail(svgData, cx);
if (thumbnail != null && thumbnail.Size.Width > 0 && thumbnail.Size.Height > 0)
{
phbmp = thumbnail.GetHbitmap();
pdwAlpha = WTS_ALPHATYPE.WTSAT_RGB;
}
}
}
}
}

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>SVGThumbnailProvider</RootNamespace>
<AssemblyName>SVGThumbnailProvider</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\..\..\x64\Debug\modules\FileExplorerPreview\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>2</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>%24%28SolutionDir%29%24%28Platform%29\%24%28Configuration%29\modules\FileExplorerPreview\SvgThumbnailProvider.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\..\..\x64\Release\modules\FileExplorerPreview\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>%24%28SolutionDir%29%24%28Platform%29\%24%28Configuration%29\modules\FileExplorerPreview\SvgThumbnailProvider.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>..\..\..\..\x64\Debug\modules\FileExplorerPreview\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>..\..\..\..\x64\Release\modules\FileExplorerPreview\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="SvgThumbnailProvider.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\Telemetry.csproj">
<Project>{5d00d290-4016-4cfe-9e41-1e7c724509ba}</Project>
<Name>Telemetry</Name>
</ProjectReference>
<ProjectReference Include="..\Common\PreviewHandlerCommon.csproj">
<Project>{af2349b8-e5b6-4004-9502-687c1c7730b1}</Project>
<Name>PreviewHandlerCommon</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Include="StyleCop.json" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>