[Peek] Fix thumbnails being created and not used. Fix icon bitmaps leaking memory. Simplify ImagePreviewer. (#34544)

Consolidated IconHelper and ThumbnailHelper. Fixed icon memory leak. Fixed ImagePreviewer thumbnails being created and then not used. Refactored ImagePreviewer.
This commit is contained in:
Dave Rayment 2024-09-23 17:00:34 +01:00 коммит произвёл GitHub
Родитель a70aafb3b8
Коммит 360b6d0ccf
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 138 добавлений и 333 удалений

Просмотреть файл

@ -20,20 +20,11 @@ namespace Peek.Common.Models
}
[StructLayout(LayoutKind.Sequential)]
public struct NativeSize
public struct NativeSize(int width, int height)
{
private int width;
private int height;
public int Width { get; set; } = width;
public int Width
{
set { width = value; }
}
public int Height
{
set { height = value; }
}
public int Height { get; set; } = height;
}
[Flags]

Просмотреть файл

@ -83,7 +83,7 @@ namespace Peek.FilePreviewer.Previewers.Drive
}
cancellationToken.ThrowIfCancellationRequested();
var iconBitmap = await IconHelper.GetIconAsync(Item.Path, cancellationToken);
var iconBitmap = await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken);
preview.IconPreview = iconBitmap ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
Preview = preview;

Просмотреть файл

@ -1,78 +0,0 @@
// 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.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media;
using Peek.Common;
using Peek.Common.Models;
namespace Peek.FilePreviewer.Previewers.Helpers
{
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 async Task<ImageSource?> GetIconAsync(string fileName, CancellationToken cancellationToken)
{
return await GetImageAsync(fileName, ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly, true, cancellationToken);
}
public static async Task<ImageSource?> GetThumbnailAsync(string fileName, CancellationToken cancellationToken)
{
return await GetImageAsync(fileName, ThumbnailOptions.ThumbnailOnly, true, cancellationToken);
}
public static async Task<ImageSource?> GetImageAsync(string fileName, ThumbnailOptions options, bool isSupportingTransparency, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(fileName))
{
return null;
}
ImageSource? imageSource = null;
IShellItem? nativeShellItem = null;
IntPtr hbitmap = IntPtr.Zero;
try
{
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 };
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out hbitmap);
cancellationToken.ThrowIfCancellationRequested();
imageSource = hr == HResult.Ok ? await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, isSupportingTransparency, cancellationToken) : null;
hbitmap = IntPtr.Zero;
}
finally
{
if (hbitmap != IntPtr.Zero)
{
NativeMethods.DeleteObject(hbitmap);
}
if (nativeShellItem != null)
{
Marshal.ReleaseComObject(nativeShellItem);
}
}
return imageSource;
}
}
}

Просмотреть файл

@ -0,0 +1,103 @@
// 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.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media;
using Peek.Common;
using Peek.Common.Models;
namespace Peek.FilePreviewer.Previewers.Helpers;
/// <summary>
/// Create thumbnail or icon images for a file, or retrieve them from the Windows Explorer thumbnail and icon caches.
/// </summary>
/// <remarks>Inspired by https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows</remarks>
public static class ThumbnailHelper
{
// The maximum size Explorer's thumbnail cache currently supports. Used for cache retrieval.
private static readonly NativeSize MaxThumbnailSize = new(2560, 2560);
// Default size for all previewers except ImagePreviewer.
private static readonly NativeSize DefaultThumbnailSize = new(256, 256);
// Used to retrieve the Shell Item for a given Windows Explorer resource, so its thumbnail/icon can be retrieved.
private static Guid _shellItem2Guid = new("7E9FB0D3-919F-4307-AB2E-9B1860310C93");
/// <summary>
/// Get a file's icon bitmap.
/// </summary>
/// <param name="path">Path to the file.</param>
/// <param name="cancellationToken">The async task cancellation token.</param>
/// <returns>An <see cref="ImageSource"/> corresponding to the file's icon bitmap, or null if retrieval failed.</returns>
/// <remarks>If a cached icon cannot be found, a new icon will be created and added to the Explorer icon cache.</remarks>
public static async Task<ImageSource?> GetIconAsync(string path, CancellationToken cancellationToken)
{
return await GetImageAsync(path, ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly, true, cancellationToken);
}
/// <summary>
/// Get a file's thumbnail bitmap.
/// </summary>
/// <param name="path">Path to the file.</param>
/// <param name="cancellationToken">The async task cancellation token.</param>
/// <returns>An <see cref="ImageSource"/> corresponding to the file's thumbnail bitmap, or null if retrieval failed.</returns>
/// <remarks>If a cached thumbnail cannot be found, a new thumbnail will be created and added to the Explorer thumbnail cache.</remarks>
public static async Task<ImageSource?> GetThumbnailAsync(string path, CancellationToken cancellationToken)
{
return await GetImageAsync(path, ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.ThumbnailOnly, true, cancellationToken);
}
/// <summary>
/// Get the highest-resolution image available for a file from the Explorer thumbnail and icon caches.
/// </summary>
/// <param name="path">Path to the file.</param>
/// <param name="supportsTransparency">Whether the file's type supports transparency. Set to true for PNG image files.</param>
/// <param name="cancellationToken">The async task cancellation token.</param>
/// <returns>An <see cref="ImageSource"/> corresponding to the thumbnail or icon, or null if there is no icon or thumbnail cached for the file.</returns>
public static async Task<ImageSource?> GetCachedThumbnailAsync(string path, bool supportsTransparency, CancellationToken cancellationToken)
{
return await GetImageAsync(path, ThumbnailOptions.InCacheOnly, supportsTransparency, cancellationToken);
}
private static async Task<ImageSource?> GetImageAsync(string path, ThumbnailOptions options, bool supportsTransparency, CancellationToken cancellationToken)
{
IntPtr hBitmap = IntPtr.Zero;
IShellItem? nativeShellItem = null;
bool checkCacheOnly = options.HasFlag(ThumbnailOptions.InCacheOnly);
try
{
int retCode = NativeMethods.SHCreateItemFromParsingName(path, IntPtr.Zero, ref _shellItem2Guid, out nativeShellItem);
if (retCode != 0)
{
throw Marshal.GetExceptionForHR(retCode)!;
}
cancellationToken.ThrowIfCancellationRequested();
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(checkCacheOnly ? MaxThumbnailSize : DefaultThumbnailSize, options, out hBitmap);
cancellationToken.ThrowIfCancellationRequested();
return hr == HResult.Ok ? await BitmapHelper.GetBitmapFromHBitmapAsync(hBitmap, supportsTransparency, cancellationToken) : null;
}
finally
{
// Delete the unmanaged bitmap resource.
if (hBitmap != IntPtr.Zero)
{
NativeMethods.DeleteObject(hBitmap);
}
if (nativeShellItem != null)
{
Marshal.ReleaseComObject(nativeShellItem);
}
}
}
}

Просмотреть файл

@ -88,8 +88,8 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer
{
cancellationToken.ThrowIfCancellationRequested();
var thumbnail = await IconHelper.GetThumbnailAsync(Item.Path, cancellationToken)
?? await IconHelper.GetIconAsync(Item.Path, cancellationToken);
var thumbnail = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken)
?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

Просмотреть файл

@ -1,87 +0,0 @@
// 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.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Models;
using Windows.Storage;
namespace Peek.FilePreviewer.Previewers
{
public static class ThumbnailHelper
{
// 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 readonly NativeSize HighQualityThumbnailSize = new NativeSize { Width = 720, Height = 720, };
public static readonly NativeSize LowQualityThumbnailSize = new NativeSize { Width = 256, Height = 256, };
private static readonly NativeSize FallBackThumbnailSize = new NativeSize { Width = 96, Height = 96, };
private static readonly NativeSize LastFallBackThumbnailSize = new NativeSize { Width = 32, Height = 32, };
private static readonly List<NativeSize> ThumbnailFallBackSizes = new List<NativeSize>
{
HighQualityThumbnailSize,
LowQualityThumbnailSize,
FallBackThumbnailSize,
LastFallBackThumbnailSize,
};
// TODO: Add a re-try system if there is no thumbnail of requested size.
public static HResult GetThumbnail(string filename, out IntPtr hbitmap, NativeSize thumbnailSize)
{
Guid shellItem2Guid = new Guid(IShellItem2Guid);
int retCode = NativeMethods.SHCreateItemFromParsingName(filename, IntPtr.Zero, ref shellItem2Guid, out IShellItem nativeShellItem);
if (retCode != 0)
{
throw Marshal.GetExceptionForHR(retCode)!;
}
var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.ThumbnailOnly | ThumbnailOptions.ScaleUp;
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(thumbnailSize, options, out hbitmap);
// Try to get thumbnail using the fallback sizes order
if (hr != HResult.Ok)
{
var currentThumbnailFallBackIndex = ThumbnailFallBackSizes.IndexOf(thumbnailSize);
var nextThumbnailFallBackIndex = currentThumbnailFallBackIndex + 1;
if (nextThumbnailFallBackIndex < ThumbnailFallBackSizes.Count - 1)
{
hr = GetThumbnail(filename, out hbitmap, ThumbnailFallBackSizes[nextThumbnailFallBackIndex]);
}
}
Marshal.ReleaseComObject(nativeShellItem);
return hr;
}
public static async Task<BitmapImage?> GetThumbnailAsync(StorageFile? storageFile, uint size)
{
BitmapImage? bitmapImage = null;
var imageStream = await storageFile?.GetThumbnailAsync(
Windows.Storage.FileProperties.ThumbnailMode.SingleItem,
size,
Windows.Storage.FileProperties.ThumbnailOptions.None);
if (imageStream == null)
{
return bitmapImage;
}
bitmapImage = new BitmapImage();
bitmapImage.SetSource(imageStream);
return bitmapImage;
}
}
}

Просмотреть файл

@ -14,7 +14,6 @@ using Microsoft.PowerToys.FilePreviewCommon;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Peek.Common;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
@ -26,7 +25,7 @@ using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public partial class ImagePreviewer : ObservableObject, IImagePreviewer, IDisposable
public partial class ImagePreviewer : ObservableObject, IImagePreviewer
{
[ObservableProperty]
private ImageSource? preview;
@ -51,41 +50,24 @@ namespace Peek.FilePreviewer.Previewers
private IFileSystemItem Item { get; }
private bool IsPng() => Item.Extension == ".png";
private bool IsSvg() => Item.Extension == ".svg";
private bool IsQoi() => Item.Extension == ".qoi";
private DispatcherQueue Dispatcher { get; }
private Task<bool>? LowQualityThumbnailTask { get; set; }
private Task<bool>? HighQualityThumbnailTask { get; set; }
private Task<bool>? FullQualityImageTask { get; set; }
private bool IsHighQualityThumbnailLoaded => HighQualityThumbnailTask?.Status == TaskStatus.RanToCompletion;
private bool IsFullImageLoaded => FullQualityImageTask?.Status == TaskStatus.RanToCompletion;
private IntPtr lowQualityThumbnail;
private ImageSource? lowQualityThumbnailPreview;
private IntPtr highQualityThumbnail;
private ImageSource? highQualityThumbnailPreview;
public static bool IsItemSupported(IFileSystemItem item)
{
return _supportedFileTypes.Contains(item.Extension);
}
public void Dispose()
{
Clear();
GC.SuppressFinalize(this);
}
public async Task<PreviewSize> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (IsSvg(Item))
if (IsSvg())
{
var size = await Task.Run(Item.GetSvgSize);
if (size != null)
@ -93,7 +75,7 @@ namespace Peek.FilePreviewer.Previewers
ImageSize = size.Value;
}
}
else if (IsQoi(Item))
else if (IsQoi())
{
var size = await Task.Run(Item.GetQoiSize);
if (size != null)
@ -115,30 +97,12 @@ namespace Peek.FilePreviewer.Previewers
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
Clear();
State = PreviewState.Loading;
LowQualityThumbnailTask = LoadLowQualityThumbnailAsync(cancellationToken);
HighQualityThumbnailTask = LoadHighQualityThumbnailAsync(cancellationToken);
FullQualityImageTask = LoadFullQualityImageAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await Task.WhenAll(LowQualityThumbnailTask, HighQualityThumbnailTask, FullQualityImageTask);
State = PreviewState.Loading;
// If Preview is still null, FullQualityImage was not available. Preview the thumbnail instead.
if (Preview == null)
{
if (highQualityThumbnailPreview != null)
{
Preview = highQualityThumbnailPreview;
}
else
{
Preview = lowQualityThumbnailPreview;
}
}
if (Preview == null && HasFailedLoadingPreview())
if (!await LoadFullQualityImageAsync(cancellationToken) &&
!await LoadThumbnailAsync(cancellationToken))
{
State = PreviewState.Error;
}
@ -173,69 +137,23 @@ namespace Peek.FilePreviewer.Previewers
private void UpdateMaxImageSize()
{
var imageWidth = ImageSize?.Width ?? 0;
var imageHeight = ImageSize?.Height ?? 0;
double imageWidth = ImageSize?.Width ?? 0;
double imageHeight = ImageSize?.Height ?? 0;
if (ScalingFactor != 0)
{
MaxImageSize = new Size(imageWidth / ScalingFactor, imageHeight / ScalingFactor);
}
else
{
MaxImageSize = new Size(imageWidth, imageHeight);
}
MaxImageSize = ScalingFactor != 0 ?
new Size(imageWidth / ScalingFactor, imageHeight / ScalingFactor) :
new Size(imageWidth, imageHeight);
}
private Task<bool> LoadLowQualityThumbnailAsync(CancellationToken cancellationToken)
private Task<bool> LoadThumbnailAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var hr = ThumbnailHelper.GetThumbnail(Item.Path, out lowQualityThumbnail, ThumbnailHelper.LowQualityThumbnailSize);
if (hr != HResult.Ok)
{
Logger.LogError("Error loading low quality thumbnail - hresult: " + hr);
throw new ImageLoadingException(nameof(lowQualityThumbnail));
}
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
{
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(lowQualityThumbnail, IsPng(Item), cancellationToken);
lowQualityThumbnailPreview = thumbnailBitmap;
}
});
});
}
private Task<bool> LoadHighQualityThumbnailAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
var hr = ThumbnailHelper.GetThumbnail(Item.Path, out highQualityThumbnail, ThumbnailHelper.HighQualityThumbnailSize);
if (hr != HResult.Ok)
{
Logger.LogError("Error loading high quality thumbnail - hresult: " + hr);
throw new ImageLoadingException(nameof(highQualityThumbnail));
}
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
if (!IsFullImageLoaded)
{
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(highQualityThumbnail, IsPng(Item), cancellationToken);
highQualityThumbnailPreview = thumbnailBitmap;
}
Preview = await ThumbnailHelper.GetCachedThumbnailAsync(Item.Path, IsPng(), cancellationToken);
});
});
}
@ -252,7 +170,7 @@ namespace Peek.FilePreviewer.Previewers
using FileStream stream = ReadHelper.OpenReadOnly(Item.Path);
if (IsSvg(Item))
if (IsSvg())
{
var source = new SvgImageSource();
source.RasterizePixelHeight = ImageSize?.Height ?? 0;
@ -267,7 +185,7 @@ namespace Peek.FilePreviewer.Previewers
Preview = source;
}
else if (IsQoi(Item))
else if (IsQoi())
{
using var bitmap = QoiImage.FromStream(stream);
@ -275,55 +193,13 @@ namespace Peek.FilePreviewer.Previewers
}
else
{
var bitmap = new BitmapImage();
Preview = bitmap;
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
Preview = new BitmapImage();
await ((BitmapImage)Preview).SetSourceAsync(stream.AsRandomAccessStream());
}
});
});
}
private bool HasFailedLoadingPreview()
{
var hasFailedLoadingLowQualityThumbnail = !(LowQualityThumbnailTask?.Result ?? true);
var hasFailedLoadingHighQualityThumbnail = !(HighQualityThumbnailTask?.Result ?? true);
var hasFailedLoadingFullQualityImage = !(FullQualityImageTask?.Result ?? true);
return hasFailedLoadingLowQualityThumbnail && hasFailedLoadingHighQualityThumbnail && hasFailedLoadingFullQualityImage;
}
private bool IsPng(IFileSystemItem item)
{
return item.Extension == ".png";
}
private bool IsSvg(IFileSystemItem item)
{
return item.Extension == ".svg";
}
private bool IsQoi(IFileSystemItem item)
{
return item.Extension == ".qoi";
}
private void Clear()
{
lowQualityThumbnailPreview = null;
highQualityThumbnailPreview = null;
Preview = null;
if (lowQualityThumbnail != IntPtr.Zero)
{
NativeMethods.DeleteObject(lowQualityThumbnail);
}
if (highQualityThumbnail != IntPtr.Zero)
{
NativeMethods.DeleteObject(highQualityThumbnail);
}
}
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
{
// Image types

Просмотреть файл

@ -106,7 +106,7 @@ public partial class SpecialFolderPreviewer : ObservableObject, ISpecialFolderPr
{
cancellationToken.ThrowIfCancellationRequested();
var iconBitmap = await IconHelper.GetIconAsync(Item.ParsingName, cancellationToken);
var iconBitmap = await ThumbnailHelper.GetIconAsync(Item.ParsingName, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

Просмотреть файл

@ -108,8 +108,8 @@ namespace Peek.FilePreviewer.Previewers
{
cancellationToken.ThrowIfCancellationRequested();
var iconBitmap = await IconHelper.GetThumbnailAsync(Item.Path, cancellationToken)
?? await IconHelper.GetIconAsync(Item.Path, cancellationToken);
var iconBitmap = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken)
?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();