mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-08 04:07:40 +02:00
330 lines
11 KiB
C#
330 lines
11 KiB
C#
|
|
// 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.IO;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||
|
|
using Microsoft.UI.Dispatching;
|
||
|
|
using Microsoft.UI.Xaml.Media;
|
||
|
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||
|
|
using Peek.Common.Extensions;
|
||
|
|
using Peek.Common.Helpers;
|
||
|
|
using Peek.Common.Models;
|
||
|
|
using Peek.FilePreviewer.Exceptions;
|
||
|
|
using Peek.FilePreviewer.Previewers.Helpers;
|
||
|
|
using Peek.FilePreviewer.Previewers.Interfaces;
|
||
|
|
using Windows.Foundation;
|
||
|
|
|
||
|
|
namespace Peek.FilePreviewer.Previewers
|
||
|
|
{
|
||
|
|
public partial class ImagePreviewer : ObservableObject, IImagePreviewer, IDisposable
|
||
|
|
{
|
||
|
|
[ObservableProperty]
|
||
|
|
private ImageSource? preview;
|
||
|
|
|
||
|
|
[ObservableProperty]
|
||
|
|
private PreviewState state;
|
||
|
|
|
||
|
|
[ObservableProperty]
|
||
|
|
private Size? imageSize;
|
||
|
|
|
||
|
|
[ObservableProperty]
|
||
|
|
private Size maxImageSize;
|
||
|
|
|
||
|
|
[ObservableProperty]
|
||
|
|
private double scalingFactor;
|
||
|
|
|
||
|
|
public ImagePreviewer(IFileSystemItem file)
|
||
|
|
{
|
||
|
|
Item = file;
|
||
|
|
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
||
|
|
}
|
||
|
|
|
||
|
|
private IFileSystemItem Item { get; }
|
||
|
|
|
||
|
|
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;
|
||
|
|
|
||
|
|
public static bool IsFileTypeSupported(string fileExt)
|
||
|
|
{
|
||
|
|
return _supportedFileTypes.Contains(fileExt);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Dispose()
|
||
|
|
{
|
||
|
|
GC.SuppressFinalize(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
if (IsSvg(Item))
|
||
|
|
{
|
||
|
|
var size = await Task.Run(Item.GetSvgSize);
|
||
|
|
if (size != null)
|
||
|
|
{
|
||
|
|
ImageSize = size.Value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ImageSize = await Task.Run(Item.GetImageSize);
|
||
|
|
if (ImageSize == null)
|
||
|
|
{
|
||
|
|
ImageSize = await WICHelper.GetImageSize(Item.Path);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return ImageSize;
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
State = PreviewState.Loading;
|
||
|
|
|
||
|
|
LowQualityThumbnailTask = LoadLowQualityThumbnailAsync(cancellationToken);
|
||
|
|
HighQualityThumbnailTask = LoadHighQualityThumbnailAsync(cancellationToken);
|
||
|
|
FullQualityImageTask = LoadFullQualityImageAsync(cancellationToken);
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
await Task.WhenAll(LowQualityThumbnailTask, HighQualityThumbnailTask, FullQualityImageTask);
|
||
|
|
|
||
|
|
if (Preview == null && HasFailedLoadingPreview())
|
||
|
|
{
|
||
|
|
State = PreviewState.Error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task CopyAsync()
|
||
|
|
{
|
||
|
|
await Dispatcher.RunOnUiThread(async () =>
|
||
|
|
{
|
||
|
|
var storageItem = await Item.GetStorageItemAsync();
|
||
|
|
ClipboardHelper.SaveToClipboard(storageItem);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
partial void OnPreviewChanged(ImageSource? value)
|
||
|
|
{
|
||
|
|
if (Preview != null)
|
||
|
|
{
|
||
|
|
State = PreviewState.Loaded;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
partial void OnScalingFactorChanged(double value)
|
||
|
|
{
|
||
|
|
UpdateMaxImageSize();
|
||
|
|
}
|
||
|
|
|
||
|
|
partial void OnImageSizeChanged(Size? value)
|
||
|
|
{
|
||
|
|
UpdateMaxImageSize();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpdateMaxImageSize()
|
||
|
|
{
|
||
|
|
var imageWidth = ImageSize?.Width ?? 0;
|
||
|
|
var imageHeight = ImageSize?.Height ?? 0;
|
||
|
|
|
||
|
|
if (ScalingFactor != 0)
|
||
|
|
{
|
||
|
|
MaxImageSize = new Size(imageWidth / ScalingFactor, imageHeight / ScalingFactor);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
MaxImageSize = new Size(imageWidth, imageHeight);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<bool> LoadLowQualityThumbnailAsync(CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
return TaskExtension.RunSafe(async () =>
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(Item.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
|
||
|
|
if (hr != HResult.Ok)
|
||
|
|
{
|
||
|
|
Logger.LogError("Error loading low quality thumbnail - hresult: " + hr);
|
||
|
|
throw new ImageLoadingException(nameof(hbitmap));
|
||
|
|
}
|
||
|
|
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
await Dispatcher.RunOnUiThread(async () =>
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, IsPng(Item), cancellationToken);
|
||
|
|
if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
|
||
|
|
{
|
||
|
|
Preview = thumbnailBitmap;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<bool> LoadHighQualityThumbnailAsync(CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
return TaskExtension.RunSafe(async () =>
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(Item.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
|
||
|
|
if (hr != HResult.Ok)
|
||
|
|
{
|
||
|
|
Logger.LogError("Error loading high quality thumbnail - hresult: " + hr);
|
||
|
|
throw new ImageLoadingException(nameof(hbitmap));
|
||
|
|
}
|
||
|
|
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
await Dispatcher.RunOnUiThread(async () =>
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, IsPng(Item), cancellationToken);
|
||
|
|
if (!IsFullImageLoaded)
|
||
|
|
{
|
||
|
|
Preview = thumbnailBitmap;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<bool> LoadFullQualityImageAsync(CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
return TaskExtension.RunSafe(async () =>
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
using FileStream stream = File.OpenRead(Item.Path);
|
||
|
|
|
||
|
|
await Dispatcher.RunOnUiThread(async () =>
|
||
|
|
{
|
||
|
|
cancellationToken.ThrowIfCancellationRequested();
|
||
|
|
|
||
|
|
if (IsSvg(Item))
|
||
|
|
{
|
||
|
|
var source = new SvgImageSource();
|
||
|
|
source.RasterizePixelHeight = ImageSize?.Height ?? 0;
|
||
|
|
source.RasterizePixelWidth = ImageSize?.Width ?? 0;
|
||
|
|
|
||
|
|
var loadStatus = await source.SetSourceAsync(stream.AsRandomAccessStream());
|
||
|
|
if (loadStatus != SvgImageSourceLoadStatus.Success)
|
||
|
|
{
|
||
|
|
Logger.LogError("Error loading SVG: " + loadStatus.ToString());
|
||
|
|
throw new ImageLoadingException(nameof(source));
|
||
|
|
}
|
||
|
|
|
||
|
|
Preview = source;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
var bitmap = new BitmapImage();
|
||
|
|
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
|
||
|
|
Preview = bitmap;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
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 static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
|
||
|
|
{
|
||
|
|
// Image types
|
||
|
|
".bmp",
|
||
|
|
".gif",
|
||
|
|
".jpg",
|
||
|
|
".jfif",
|
||
|
|
".jfi",
|
||
|
|
".jif",
|
||
|
|
".jpeg",
|
||
|
|
".jpe",
|
||
|
|
".png",
|
||
|
|
".tif", // very slow for large files: no thumbnail?
|
||
|
|
".tiff", // NEED TO TEST
|
||
|
|
".dib", // NEED TO TEST
|
||
|
|
".heic",
|
||
|
|
".heif",
|
||
|
|
".hif", // NEED TO TEST
|
||
|
|
".avif", // NEED TO TEST
|
||
|
|
".jxr",
|
||
|
|
".wdp",
|
||
|
|
".ico", // NEED TO TEST
|
||
|
|
".thumb", // NEED TO TEST
|
||
|
|
|
||
|
|
// Raw types
|
||
|
|
".arw",
|
||
|
|
".cr2",
|
||
|
|
".crw",
|
||
|
|
".erf",
|
||
|
|
".kdc", // NEED TO TEST
|
||
|
|
".mrw",
|
||
|
|
".nef",
|
||
|
|
".nrw",
|
||
|
|
".orf",
|
||
|
|
".pef",
|
||
|
|
".raf",
|
||
|
|
".raw",
|
||
|
|
".rw2",
|
||
|
|
".rwl",
|
||
|
|
".sr2",
|
||
|
|
".srw",
|
||
|
|
".srf",
|
||
|
|
".dcs", // NEED TO TEST
|
||
|
|
".dcr",
|
||
|
|
".drf", // NEED TO TEST
|
||
|
|
".k25",
|
||
|
|
".3fr",
|
||
|
|
".ari", // NEED TO TEST
|
||
|
|
".bay", // NEED TO TEST
|
||
|
|
".cap", // NEED TO TEST
|
||
|
|
".iiq",
|
||
|
|
".eip", // NEED TO TEST
|
||
|
|
".fff",
|
||
|
|
".mef",
|
||
|
|
|
||
|
|
// ".mdc", // Crashes in GetFullBitmapFromPathAsync
|
||
|
|
".mos",
|
||
|
|
".R3D",
|
||
|
|
".rwz", // NEED TO TEST
|
||
|
|
".x3f",
|
||
|
|
".ori",
|
||
|
|
".cr3",
|
||
|
|
|
||
|
|
".svg",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|