Skip to content

Commit

Permalink
[Peek] Add unsupported file icon fallback (#23735)
Browse files Browse the repository at this point in the history
* Refactor icon retrieval, refactor hbitmap to bitmap conversion, add icon fallback

* Add svg to assets in installer
  • Loading branch information
SamChaps authored Feb 2, 2023
1 parent 8fe3d8a commit 07e9780
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 90 deletions.
2 changes: 1 addition & 1 deletion installer/PowerToysSetup/Product.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@

<?define PeekFiles=CommunityToolkit.Mvvm.dll;Microsoft.InteractiveExperiences.Projection.dll;WinRT.Runtime.dll;Ijwhost.dll;Microsoft.Windows.ApplicationModel.DynamicDependency.Projection.dll;Microsoft.Windows.ApplicationModel.Resources.Projection.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Power.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WinUI.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;Powertoys.Interop.dll;Peek.Common.dll;Peek.FilePreviewer.dll;Powertoys.Peek.UI.dll;Powertoys.Peek.UI.exe;Powertoys.Peek.UI.deps.json;Powertoys.Peek.UI.runtimeconfig.json;resources.pri;System.CodeDom.dll;System.Drawing.Common.dll;System.Management.dll;WIC.dll;WinUIEx.dll;System.IO.Abstractions.dll;PowerToys.Settings.UI.Lib.dll?>

<?define PeekAssetsFiles=Icon.ico?>
<?define PeekAssetsFiles=Icon.ico;DefaultFileIcon.svg?>

<?define PowerRenameSparsePackageAssets=LargeTile.png;SmallTile.png;SplashScreen.png;Square150x150Logo.png;Square44x44Logo.png;storelogo.png;Wide310x150Logo.png?>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- Licensed under the MIT License. -->

<UserControl
x:Class="Peek.FilePreviewer.UnsupportedFilePreview"
x:Class="Peek.FilePreviewer.Controls.UnsupportedFilePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.UI.Xaml.Media;
using Peek.Common.Helpers;

namespace Peek.FilePreviewer
namespace Peek.FilePreviewer.Controls
{
[INotifyPropertyChanged]
public sealed partial class UnsupportedFilePreview : UserControl
{
[ObservableProperty]
private BitmapSource? iconPreview;
private ImageSource? iconPreview;

[ObservableProperty]
private string? fileName;
Expand Down
6 changes: 3 additions & 3 deletions src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

<Image
x:Name="ImagePreview"
Source="{x:Bind BitmapPreviewer.Preview, Mode=OneWay}"
MaxHeight="{x:Bind BitmapPreviewer.MaxImageSize.Height, Mode=OneWay}"
MaxWidth="{x:Bind BitmapPreviewer.MaxImageSize.Width, Mode=OneWay}"
MaxHeight="{x:Bind BitmapPreviewer.MaxImageSize.Height, Mode=OneWay}"
Source="{x:Bind BitmapPreviewer.Preview, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ImageInfoTooltip, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(BitmapPreviewer, Previewer.State), Mode=OneWay}" />

Expand All @@ -34,7 +34,7 @@
Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(BrowserPreviewer, Previewer.State), Mode=OneWay, FallbackValue=Collapsed}" />

<local:UnsupportedFilePreview
<controls:UnsupportedFilePreview
x:Name="UnsupportedFilePreview"
DateModified="{x:Bind UnsupportedFilePreviewer.DateModified, Mode=OneWay}"
FileName="{x:Bind UnsupportedFilePreviewer.FileName, Mode=OneWay}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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.Imaging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;

namespace Peek.FilePreviewer.Previewers.Helpers
{
public static class BitmapHelper
{
public static async Task<BitmapSource> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,29 +19,46 @@ public static class IconHelper
// 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<ImageSource?> 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;
NativeSize large = new NativeSize { Width = 256, Height = 256 };
var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly;

HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out hbitmap);
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out IntPtr hbitmap);

if (hr != HResult.Ok)
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
{
// TODO: fallback to a generic icon
if (nativeShellItem != null)
{
Marshal.ReleaseComObject(nativeShellItem);
}
}

Marshal.ReleaseComObject(nativeShellItem);

return hr;
return imageSource;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -153,7 +154,7 @@ private Task<bool> LoadLowQualityThumbnailAsync(CancellationToken cancellationTo
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;
Expand Down Expand Up @@ -181,7 +182,7 @@ private Task<bool> LoadHighQualityThumbnailAsync(CancellationToken cancellationT
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var thumbnailBitmap = await GetBitmapFromHBitmapAsync(hbitmap, cancellationToken);
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, false, cancellationToken);
if (!IsFullImageLoaded)
{
Preview = thumbnailBitmap;
Expand Down Expand Up @@ -229,32 +230,6 @@ private static async Task<BitmapImage> GetFullBitmapFromPathAsync(string path, C
return bitmap;
}

private static async Task<BitmapSource> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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;
Expand All @@ -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;
Expand Down Expand Up @@ -123,15 +123,11 @@ public Task<bool> LoadIconPreviewAsync(CancellationToken cancellationToken)
{
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;
});
});
Expand Down Expand Up @@ -175,35 +171,5 @@ private bool HasFailedLoadingPreview()

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<BitmapSource> 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);
}
}
}
}
5 changes: 5 additions & 0 deletions src/modules/peek/Peek.UI/Assets/DefaultFileIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

1 comment on commit 07e9780

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

🔴 Please review

See the 📜action log for details.

Unrecognized words (13)
ALLVIEW
Chromakey
GETPROPERTYSTOREFLAGS
holemenu
IPalette
ipreview
pkey
ppshv
rbhid
SHCONTF
SHGDNF
SIATTRIBFLAGS
Strm
Previously acknowledged words that are now absent Asn BESTEFFORT BHIDSF Brotli clrcompression clretwrc clrgc clrjit Cng coreclr dbgshim DCompiler DELAYCREATION Dwma EXTRINSICPROPERTIESONLY FASTPROPERTIESONLY HANDLERPROPERTIESONLY hostfxr hostpolicy IIDI Imc Intrinsics IPREVIEW JSONOf Keybd LANGID mscordaccore mscordbi mscorrc msquic neighborings netstandard OPLOCK Pkcs PKEY PREFERQUERYPROPERTIES Previer psfi Quic rfid RTSS Shcontf Shgno Softcoded Ssl UMsg WMSDK wpfgfx WScan WVk zopfli :arrow_right:
Some files were automatically ignored

These sample patterns would exclude them:

^\Qsrc/modules/peek/Peek.Common/NativeMethods.txt\E$

You should consider adding them to:

.github/actions/spell-check/excludes.txt

File matching is via Perl regular expressions.

To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.

To accept ✔️ these unrecognized words as correct and remove the previously acknowledged and now absent words, run the following commands

... in a clone of the git@github.com:microsoft/PowerToys.git repository
on the peek branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.21/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/4077237830/attempts/1'
Available 📚 dictionaries could cover words not in the 📘 dictionary

This includes both expected items (2294) from .github/actions/spell-check/expect.txt and unrecognized words (13)

Dictionary Entries Covers
cspell:win32/src/win32.txt 53509 134
cspell:cpp/src/cpp.txt 30216 129
cspell:python/src/python/python-lib.txt 3873 32
cspell:php/php.txt 2597 17
cspell:node/node.txt 1768 13
cspell:typescript/typescript.txt 1211 12
cspell:python/src/python/python.txt 453 11
cspell:java/java.txt 7642 11
cspell:aws/aws.txt 218 8
cspell:python/src/common/extra.txt 741 7

Consider adding them using (in .github/workflows/spelling2.yml):

      with:
        extra_dictionaries:
          cspell:win32/src/win32.txt
          cspell:cpp/src/cpp.txt
          cspell:python/src/python/python-lib.txt
          cspell:php/php.txt
          cspell:node/node.txt
          cspell:typescript/typescript.txt
          cspell:python/src/python/python.txt
          cspell:java/java.txt
          cspell:aws/aws.txt
          cspell:python/src/common/extra.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
Warnings (1)

See the 📜action log for details.

ℹ️ Warnings Count
ℹ️ noisy-file 1

See ℹ️ Event descriptions for more information.

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Please sign in to comment.