Files
PowerToys/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs
2024-03-26 16:50:08 +00:00

396 lines
13 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 ManagedCommon;
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;
using Peek.FilePreviewer.Exceptions;
using Peek.FilePreviewer.Models;
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;
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))
{
var size = await Task.Run(Item.GetSvgSize);
if (size != null)
{
ImageSize = size.Value;
}
}
else if (IsQoi(Item))
{
var size = await Task.Run(Item.GetQoiSize);
if (size != null)
{
ImageSize = size.Value;
}
}
else
{
ImageSize = await Task.Run(Item.GetImageSize);
if (ImageSize == null)
{
ImageSize = await WICHelper.GetImageSize(Item.Path);
}
}
return new PreviewSize { MonitorSize = ImageSize };
}
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);
// 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())
{
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(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;
}
});
});
}
private Task<bool> LoadFullQualityImageAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
using FileStream stream = ReadHelper.OpenReadOnly(Item.Path);
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 if (IsQoi(Item))
{
using var bitmap = QoiImage.FromStream(stream);
Preview = await BitmapHelper.BitmapToImageSource(bitmap, true, cancellationToken);
}
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 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
".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
".webp",
// 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",
".qoi",
};
}
}