From 07e9780420809a29642c1c4a99079fd036d55682 Mon Sep 17 00:00:00 2001 From: Samuel Chapleau Date: Thu, 2 Feb 2023 09:54:08 -0800 Subject: [PATCH] [Peek] Add unsupported file icon fallback (#23735) * Refactor icon retrieval, refactor hbitmap to bitmap conversion, add icon fallback * Add svg to assets in installer --- installer/PowerToysSetup/Product.wxs | 2 +- .../UnsupportedFilePreview.xaml | 2 +- .../UnsupportedFilePreview.xaml.cs | 6 +- .../peek/Peek.FilePreviewer/FilePreview.xaml | 6 +- .../Previewers/Helpers/BitmapHelper.cs | 48 +++++++++++++++ .../Previewers/Helpers/IconHelper.cs | 59 +++++++++++++------ .../Previewers/IUnsupportedFilePreviewer.cs | 4 +- .../ImagePreviewer/ImagePreviewer.cs | 31 +--------- .../UnsupportedFilePreviewer.cs | 40 +------------ .../peek/Peek.UI/Assets/DefaultFileIcon.svg | 5 ++ 10 files changed, 109 insertions(+), 94 deletions(-) rename src/modules/peek/Peek.FilePreviewer/{ => Controls}/UnsupportedFilePreview.xaml (95%) rename src/modules/peek/Peek.FilePreviewer/{ => Controls}/UnsupportedFilePreview.xaml.cs (92%) create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs create mode 100644 src/modules/peek/Peek.UI/Assets/DefaultFileIcon.svg diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 33f47678f4..4ec3826cac 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -128,7 +128,7 @@ - + diff --git a/src/modules/peek/Peek.FilePreviewer/UnsupportedFilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/UnsupportedFilePreview.xaml similarity index 95% rename from src/modules/peek/Peek.FilePreviewer/UnsupportedFilePreview.xaml rename to src/modules/peek/Peek.FilePreviewer/Controls/UnsupportedFilePreview.xaml index f82033de80..08fb341d2b 100644 --- a/src/modules/peek/Peek.FilePreviewer/UnsupportedFilePreview.xaml +++ b/src/modules/peek/Peek.FilePreviewer/Controls/UnsupportedFilePreview.xaml @@ -2,7 +2,7 @@ @@ -34,7 +34,7 @@ Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}" Visibility="{x:Bind IsPreviewVisible(BrowserPreviewer, Previewer.State), Mode=OneWay, FallbackValue=Collapsed}" /> - GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken) + { + try + { + var bitmap = System.Drawing.Image.FromHbitmap(hbitmap); + if (isSupportingTransparency) + { + bitmap.MakeTransparent(); + } + + var bitmapImage = new BitmapImage(); + + cancellationToken.ThrowIfCancellationRequested(); + using (var stream = new MemoryStream()) + { + bitmap.Save(stream, isSupportingTransparency ? ImageFormat.Png : ImageFormat.Bmp); + stream.Position = 0; + + cancellationToken.ThrowIfCancellationRequested(); + await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); + } + + return bitmapImage; + } + finally + { + // delete HBitmap to avoid memory leaks + NativeMethods.DeleteObject(hbitmap); + } + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/IconHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/IconHelper.cs index 2e703ed197..0f54a45ecd 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/IconHelper.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/IconHelper.cs @@ -5,6 +5,10 @@ using System; using System.Drawing; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; using Peek.Common; using Peek.Common.Models; @@ -15,29 +19,46 @@ namespace Peek.FilePreviewer.Previewers.Helpers // Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93"; - public static HResult GetIcon(string fileName, out IntPtr hbitmap) + public static async Task GetIconAsync(string fileName, CancellationToken cancellationToken) { - Guid shellItem2Guid = new Guid(IShellItem2Guid); - int retCode = NativeMethods.SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out IShellItem nativeShellItem); - - if (retCode != 0) + ImageSource? imageSource = null; + IShellItem? nativeShellItem = null; + try { - throw Marshal.GetExceptionForHR(retCode)!; + Guid shellItem2Guid = new(IShellItem2Guid); + int retCode = NativeMethods.SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem); + + if (retCode != 0) + { + throw Marshal.GetExceptionForHR(retCode)!; + } + + NativeSize large = new NativeSize { Width = 256, Height = 256 }; + var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly; + + HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out IntPtr hbitmap); + + cancellationToken.ThrowIfCancellationRequested(); + + if (hr == HResult.Ok) + { + imageSource = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, true, cancellationToken); + } + else + { + var svgImageSource = new SvgImageSource(new Uri("ms-appx:///Assets/DefaultFileIcon.svg")); + imageSource = svgImageSource; + } + } + finally + { + if (nativeShellItem != null) + { + Marshal.ReleaseComObject(nativeShellItem); + } } - NativeSize large = new NativeSize { Width = 256, Height = 256 }; - var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly; - - HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out hbitmap); - - if (hr != HResult.Ok) - { - // TODO: fallback to a generic icon - } - - Marshal.ReleaseComObject(nativeShellItem); - - return hr; + return imageSource; } } } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/IUnsupportedFilePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/IUnsupportedFilePreviewer.cs index a1733b48ac..8c90d54ca3 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/IUnsupportedFilePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/IUnsupportedFilePreviewer.cs @@ -2,13 +2,13 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.UI.Xaml.Media; namespace Peek.FilePreviewer.Previewers { public interface IUnsupportedFilePreviewer : IPreviewer { - public BitmapSource? IconPreview { get; } + public ImageSource? IconPreview { get; } public string? FileName { get; } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs index 5690c43d36..610904ad14 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ImagePreviewer/ImagePreviewer.cs @@ -14,6 +14,7 @@ using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media.Imaging; using Peek.Common; using Peek.Common.Extensions; +using Peek.FilePreviewer.Previewers.Helpers; using Windows.ApplicationModel.DataTransfer; using Windows.Foundation; using Windows.Storage; @@ -153,7 +154,7 @@ namespace Peek.FilePreviewer.Previewers await Dispatcher.RunOnUiThread(async () => { cancellationToken.ThrowIfCancellationRequested(); - var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap, cancellationToken); + var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, false, cancellationToken); if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded) { Preview = thumbnailBitmap; @@ -181,7 +182,7 @@ namespace Peek.FilePreviewer.Previewers await Dispatcher.RunOnUiThread(async () => { cancellationToken.ThrowIfCancellationRequested(); - var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap, cancellationToken); + var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, false, cancellationToken); if (!IsFullImageLoaded) { Preview = thumbnailBitmap; @@ -229,32 +230,6 @@ namespace Peek.FilePreviewer.Previewers return bitmap; } - private static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, CancellationToken cancellationToken) - { - try - { - var bitmap = System.Drawing.Image.FromHbitmap(hbitmap); - var bitmapImage = new BitmapImage(); - - cancellationToken.ThrowIfCancellationRequested(); - using (var stream = new MemoryStream()) - { - bitmap.Save(stream, ImageFormat.Bmp); - stream.Position = 0; - - cancellationToken.ThrowIfCancellationRequested(); - await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); - } - - return bitmapImage; - } - finally - { - // delete HBitmap to avoid memory leaks - NativeMethods.DeleteObject(hbitmap); - } - } - public static bool IsFileTypeSupported(string fileExt) { return _supportedFileTypes.Contains(fileExt); diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs index b3410040bd..70b2d8a828 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.UI.Dispatching; -using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Peek.Common; using Peek.Common.Extensions; @@ -26,7 +26,7 @@ namespace Peek.FilePreviewer.Previewers public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer, IDisposable { [ObservableProperty] - private BitmapSource? iconPreview; + private ImageSource? iconPreview; [ObservableProperty] private string? fileName; @@ -123,15 +123,11 @@ namespace Peek.FilePreviewer.Previewers { return TaskExtension.RunSafe(async () => { - cancellationToken.ThrowIfCancellationRequested(); - - IconHelper.GetIcon(Path.GetFullPath(File.Path), out IntPtr hbitmap); - cancellationToken.ThrowIfCancellationRequested(); await Dispatcher.RunOnUiThread(async () => { cancellationToken.ThrowIfCancellationRequested(); - var iconBitmap = await GetBitmapFromHBitmapWithTransparencyAsync(hbitmap, cancellationToken); + var iconBitmap = await IconHelper.GetIconAsync(Path.GetFullPath(File.Path), cancellationToken); IconPreview = iconBitmap; }); }); @@ -175,35 +171,5 @@ namespace Peek.FilePreviewer.Previewers return hasFailedLoadingIconPreview && hasFailedLoadingDisplayInfo; } - - // TODO: Move this to a common helper file and make transparency a parameter (ImagePrevier uses the same code) - private static async Task GetBitmapFromHBitmapWithTransparencyAsync(IntPtr hbitmap, CancellationToken cancellationToken) - { - try - { - cancellationToken.ThrowIfCancellationRequested(); - var bitmap = System.Drawing.Image.FromHbitmap(hbitmap); - bitmap.MakeTransparent(); - - var bitmapImage = new BitmapImage(); - - cancellationToken.ThrowIfCancellationRequested(); - using (var stream = new MemoryStream()) - { - bitmap.Save(stream, ImageFormat.Png); - stream.Position = 0; - - cancellationToken.ThrowIfCancellationRequested(); - await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); - } - - return bitmapImage; - } - finally - { - // delete HBitmap to avoid memory leaks - NativeMethods.DeleteObject(hbitmap); - } - } } } diff --git a/src/modules/peek/Peek.UI/Assets/DefaultFileIcon.svg b/src/modules/peek/Peek.UI/Assets/DefaultFileIcon.svg new file mode 100644 index 0000000000..afea45d77a --- /dev/null +++ b/src/modules/peek/Peek.UI/Assets/DefaultFileIcon.svg @@ -0,0 +1,5 @@ + + + + +