From dc4e1c4b75e09acf56c61857006a88d5a5ec2e5f Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Thu, 18 Jan 2024 17:31:04 -0600 Subject: [PATCH] port image --- .../Flow.Launcher.Infrastructure.csproj | 1 + .../Image/ImageCache.cs | 9 +- .../Image/ImageHelper.cs | 49 +++++++ .../Image/ImageLoader.cs | 136 ++++++------------ .../Image/ThumbnailReader.cs | 56 +++++--- Flow.Launcher/Msg.xaml.cs | 36 ++--- Flow.Launcher/ResultListBox.axaml | 4 +- Flow.Launcher/ViewModel/PluginViewModel.cs | 9 +- Flow.Launcher/ViewModel/ResultViewModel.cs | 21 +-- .../SearchSourceSetting.xaml.cs | 4 +- .../SearchSourceViewModel.cs | 3 +- 11 files changed, 177 insertions(+), 151 deletions(-) create mode 100644 Flow.Launcher.Infrastructure/Image/ImageHelper.cs diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj index 204caecaa..09f33525f 100644 --- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj +++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj @@ -48,6 +48,7 @@ + all diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index 7a2b57637..70e4ac681 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Windows.Media; +using Avalonia.Media.Imaging; namespace Flow.Launcher.Infrastructure.Image { @@ -12,9 +13,9 @@ namespace Flow.Launcher.Infrastructure.Image { public int usage; - public ImageSource imageSource; + public Bitmap imageSource; - public ImageUsage(int usage, ImageSource image) + public ImageUsage(int usage, Bitmap image) { this.usage = usage; imageSource = image; @@ -36,7 +37,7 @@ namespace Flow.Launcher.Infrastructure.Image } } - public ImageSource this[string path, bool isFullImage = false] + public Bitmap this[string path, bool isFullImage = false] { get { @@ -86,7 +87,7 @@ namespace Flow.Launcher.Infrastructure.Image return key is not null && Data.ContainsKey((key, isFullImage)) && Data[(key, isFullImage)].imageSource != null; } - public bool TryGetValue(string key, bool isFullImage, out ImageSource image) + public bool TryGetValue(string key, bool isFullImage, out Bitmap image) { if (key is not null) { diff --git a/Flow.Launcher.Infrastructure/Image/ImageHelper.cs b/Flow.Launcher.Infrastructure/Image/ImageHelper.cs new file mode 100644 index 000000000..4827f8755 --- /dev/null +++ b/Flow.Launcher.Infrastructure/Image/ImageHelper.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Flow.Launcher.Infrastructure.Logger; + +namespace Flow.Launcher.Infrastructure.Image +{ + public static class ImageHelper + { + public static Bitmap LoadFromResource(Uri resourceUri) + { + return new Bitmap(AssetLoader.Open(resourceUri)); + } + + public static async Task LoadFromFile(string path, int? width) + { + if (width is null) + { + return new Bitmap(path); + } + + await using var stream = File.OpenRead(path); + MemoryStream memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + return Bitmap.DecodeToWidth(memoryStream, width.Value); + } + + public static async Task LoadFromWeb(Uri url) + { + using var httpClient = new HttpClient(); + try + { + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + var data = await response.Content.ReadAsByteArrayAsync(); + return new Bitmap(new MemoryStream(data)); + } + catch (HttpRequestException ex) + { + Log.Error($"An error occurred while downloading image '{url}' : {ex.Message}"); + return null; + } + } + } +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs index add6d4e92..c624241e3 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; +using Avalonia.Media.Imaging; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Storage; using static Flow.Launcher.Infrastructure.Http.Http; @@ -21,8 +22,8 @@ namespace Flow.Launcher.Infrastructure.Image private static readonly ConcurrentDictionary GuidToKey = new(); private static IImageHashGenerator _hashGenerator; private static readonly bool EnableImageHash = true; - public static ImageSource MissingImage { get; } = new BitmapImage(new Uri(Constant.MissingImgIcon)); - public static ImageSource LoadingImage { get; } = new BitmapImage(new Uri(Constant.LoadingImgIcon)); + public static Bitmap MissingImage { get; } = new Bitmap(Constant.MissingImgIcon); + public static Bitmap LoadingImage { get; } = new Bitmap(Constant.LoadingImgIcon); public const int SmallIconSize = 64; public const int FullIconSize = 256; @@ -40,8 +41,7 @@ namespace Flow.Launcher.Infrastructure.Image foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon }) { - ImageSource img = new BitmapImage(new Uri(icon)); - img.Freeze(); + var img = new Bitmap(icon); ImageCache[icon, false] = img; } @@ -93,14 +93,14 @@ namespace Flow.Launcher.Infrastructure.Image private class ImageResult { - public ImageResult(ImageSource imageSource, ImageType imageType) + public ImageResult(Bitmap image, ImageType imageType) { - ImageSource = imageSource; + Image = image; ImageType = imageType; } public ImageType ImageType { get; } - public ImageSource ImageSource { get; } + public Bitmap Image { get; } } private enum ImageType @@ -138,28 +138,28 @@ namespace Flow.Launcher.Infrastructure.Image return new ImageResult(image, ImageType.ImageFile); } - if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) - { - var imageSource = new BitmapImage(new Uri(path)); - imageSource.Freeze(); - return new ImageResult(imageSource, ImageType.Data); - } + // if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + // { + // var imageSource = new BitmapImage(new Uri(path)); + // imageSource.Freeze(); + // return new ImageResult(imageSource, ImageType.Data); + // } - imageResult = await Task.Run(() => GetThumbnailResult(ref path, loadFullImage)); + imageResult = await Task.Run(async () => await GetThumbnailResult(path, loadFullImage)); } catch (System.Exception e) { try { // Get thumbnail may fail for certain images on the first try, retry again has proven to work - imageResult = GetThumbnailResult(ref path, loadFullImage); + imageResult = await GetThumbnailResult(path, loadFullImage); } catch (System.Exception e2) { Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on first try", e); Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path} on second try", e2); - ImageSource image = ImageCache[Constant.MissingImgIcon, false]; + var image = ImageCache[Constant.MissingImgIcon, false]; ImageCache[path, false] = image; imageResult = new ImageResult(image, ImageType.Error); } @@ -168,32 +168,29 @@ namespace Flow.Launcher.Infrastructure.Image return imageResult; } - private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) + private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult) { // Download image from url await using var resp = await GetStreamAsync(uriResult); await using var buffer = new MemoryStream(); await resp.CopyToAsync(buffer); buffer.Seek(0, SeekOrigin.Begin); - var image = new BitmapImage(); - image.BeginInit(); - image.CacheOption = BitmapCacheOption.OnLoad; + Bitmap image; if (!loadFullImage) { - image.DecodePixelHeight = SmallIconSize; - image.DecodePixelWidth = SmallIconSize; + image = Bitmap.DecodeToWidth(buffer, SmallIconSize); + } + else + { + image = new Bitmap(buffer); } - image.StreamSource = buffer; - image.EndInit(); - image.StreamSource = null; - image.Freeze(); return image; } - private static ImageResult GetThumbnailResult(ref string path, bool loadFullImage = false) + private static async ValueTask GetThumbnailResult(string path, bool loadFullImage = false) { - ImageSource image; + Bitmap image; ImageType type = ImageType.Error; if (Directory.Exists(path)) @@ -214,7 +211,7 @@ namespace Flow.Launcher.Infrastructure.Image type = ImageType.ImageFile; if (loadFullImage) { - image = LoadFullImage(path); + image = new Bitmap(path); type = ImageType.FullImageFile; } else @@ -224,7 +221,7 @@ namespace Flow.Launcher.Infrastructure.Image * be the case in many situations while testing. * - Solution: explicitly pass the ThumbnailOnly flag */ - image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly); + image = await ImageHelper.LoadFromFile(path, SmallIconSize); } } else @@ -239,15 +236,11 @@ namespace Flow.Launcher.Infrastructure.Image path = Constant.MissingImgIcon; } - if (type != ImageType.Error) - { - image.Freeze(); - } return new ImageResult(image, type); } - private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, + private static Bitmap GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize) { return WindowsThumbnailProvider.GetThumbnail( @@ -262,34 +255,34 @@ namespace Flow.Launcher.Infrastructure.Image return ImageCache.ContainsKey(path, loadFullImage); } - public static bool TryGetValue(string path, bool loadFullImage, out ImageSource image) + public static bool TryGetValue(string path, bool loadFullImage, out Bitmap image) { return ImageCache.TryGetValue(path, loadFullImage, out image); } - public static async ValueTask LoadAsync(string path, bool loadFullImage = false) + public static async ValueTask LoadAsync(string path, bool loadFullImage = false) { var imageResult = await LoadInternalAsync(path, loadFullImage); - var img = imageResult.ImageSource; + var img = imageResult.Image; if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache) { // we need to get image hash - string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null; - if (hash != null) - { - if (GuidToKey.TryGetValue(hash, out string key)) - { - // image already exists - img = ImageCache[key, loadFullImage] ?? img; - } - else - { - // new guid - - GuidToKey[hash] = path; - } - } + // string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null; + // if (hash != null) + // { + // if (GuidToKey.TryGetValue(hash, out string key)) + // { + // // image already exists + // img = ImageCache[key, loadFullImage] ?? img; + // } + // else + // { + // // new guid + // + // GuidToKey[hash] = path; + // } + // } // update cache ImageCache[path, loadFullImage] = img; @@ -297,42 +290,5 @@ namespace Flow.Launcher.Infrastructure.Image return img; } - - private static BitmapImage LoadFullImage(string path) - { - BitmapImage image = new BitmapImage(); - image.BeginInit(); - image.CacheOption = BitmapCacheOption.OnLoad; - image.UriSource = new Uri(path); - image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; - image.EndInit(); - - if (image.PixelWidth > 320) - { - BitmapImage resizedWidth = new BitmapImage(); - resizedWidth.BeginInit(); - resizedWidth.CacheOption = BitmapCacheOption.OnLoad; - resizedWidth.UriSource = new Uri(path); - resizedWidth.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; - resizedWidth.DecodePixelWidth = 320; - resizedWidth.EndInit(); - - if (resizedWidth.PixelHeight > 320) - { - BitmapImage resizedHeight = new BitmapImage(); - resizedHeight.BeginInit(); - resizedHeight.CacheOption = BitmapCacheOption.OnLoad; - resizedHeight.UriSource = new Uri(path); - resizedHeight.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; - resizedHeight.DecodePixelHeight = 320; - resizedHeight.EndInit(); - return resizedHeight; - } - - return resizedWidth; - } - - return image; - } } } diff --git a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs index 247238bb6..7909ec0fb 100644 --- a/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs +++ b/Flow.Launcher.Infrastructure/Image/ThumbnailReader.cs @@ -1,9 +1,12 @@ using System; +using System.Drawing; +using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.IO; using System.Windows.Interop; using System.Windows.Media.Imaging; using System.Windows; +using Bitmap = Avalonia.Media.Imaging.Bitmap; namespace Flow.Launcher.Infrastructure.Image { @@ -41,8 +44,8 @@ namespace Flow.Launcher.Infrastructure.Image internal interface IShellItem { void BindToHandler(IntPtr pbc, - [MarshalAs(UnmanagedType.LPStruct)]Guid bhid, - [MarshalAs(UnmanagedType.LPStruct)]Guid riid, + [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv); void GetParent(out IShellItem ppsi); @@ -88,9 +91,9 @@ namespace Flow.Launcher.Infrastructure.Image { [PreserveSig] HResult GetImage( - [In, MarshalAs(UnmanagedType.Struct)] NativeSize size, - [In] ThumbnailOptions flags, - [Out] out IntPtr phbm); + [In, MarshalAs(UnmanagedType.Struct)] NativeSize size, + [In] ThumbnailOptions flags, + [Out] out IntPtr phbm); } [StructLayout(LayoutKind.Sequential)] @@ -104,21 +107,34 @@ namespace Flow.Launcher.Infrastructure.Image }; - public static BitmapSource GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) + public static Bitmap GetThumbnail(string fileName, int width, int height, ThumbnailOptions options) { - IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options); - try { - return Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); + var icon = Icon.ExtractAssociatedIcon(fileName); + if (icon == null) + { + return null; + } + + var bitmapTmp = icon.ToBitmap(); + var bitmapdata = bitmapTmp.LockBits(new Rectangle(0, 0, bitmapTmp.Width, bitmapTmp.Height), + ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + var bitmap1 = new Bitmap(Avalonia.Platform.PixelFormat.Bgra8888, Avalonia.Platform.AlphaFormat.Unpremul, + bitmapdata.Scan0, + new Avalonia.PixelSize(bitmapdata.Width, bitmapdata.Height), + new Avalonia.Vector(96, 96), + bitmapdata.Stride); + bitmapTmp.UnlockBits(bitmapdata); + bitmapTmp.Dispose(); + return bitmap1; } - finally + catch (System.Exception e) { - // delete HBitmap to avoid memory leaks - DeleteObject(hBitmap); + return null; } } - + private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options) { IShellItem nativeShellItem; @@ -128,11 +144,7 @@ namespace Flow.Launcher.Infrastructure.Image if (retCode != 0) throw Marshal.GetExceptionForHR(retCode); - NativeSize nativeSize = new NativeSize - { - Width = width, - Height = height - }; + NativeSize nativeSize = new NativeSize { Width = width, Height = height }; IntPtr hBitmap; HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap); @@ -140,14 +152,16 @@ namespace Flow.Launcher.Infrastructure.Image // if extracting image thumbnail and failed, extract shell icon if (options == ThumbnailOptions.ThumbnailOnly && hr == HResult.ExtractionFailed) { - hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, out hBitmap); + hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, ThumbnailOptions.IconOnly, + out hBitmap); } Marshal.ReleaseComObject(nativeShellItem); if (hr == HResult.Ok) return hBitmap; - throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr)); + throw new COMException($"Error while extracting thumbnail for {fileName}", + Marshal.GetExceptionForHR((int)hr)); } } -} \ No newline at end of file +} diff --git a/Flow.Launcher/Msg.xaml.cs b/Flow.Launcher/Msg.xaml.cs index 0bb02bbc5..cf289ac29 100644 --- a/Flow.Launcher/Msg.xaml.cs +++ b/Flow.Launcher/Msg.xaml.cs @@ -29,22 +29,24 @@ namespace Flow.Launcher // Create the fade out storyboard fadeOutStoryboard.Completed += fadeOutStoryboard_Completed; - DoubleAnimation fadeOutAnimation = new DoubleAnimation(dipWorkingArea.Y - Height, dipWorkingArea.Y, new Duration(TimeSpan.FromSeconds(5))) - { - AccelerationRatio = 0.2 - }; + DoubleAnimation fadeOutAnimation = + new DoubleAnimation(dipWorkingArea.Y - Height, dipWorkingArea.Y, new Duration(TimeSpan.FromSeconds(5))) + { + AccelerationRatio = 0.2 + }; Storyboard.SetTarget(fadeOutAnimation, this); Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(TopProperty)); fadeOutStoryboard.Children.Add(fadeOutAnimation); _ = LoadImageAsync(); - + imgClose.MouseUp += imgClose_MouseUp; } private async System.Threading.Tasks.Task LoadImageAsync() { - imgClose.Source = await ImageLoader.LoadAsync(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\close.png")); + imgClose.Source = + default; //await ImageLoader.LoadAsync(Path.Combine(Infrastructure.Constant.ProgramDirectory, "Images\\close.png")); } void imgClose_MouseUp(object sender, MouseButtonEventArgs e) @@ -69,26 +71,26 @@ namespace Flow.Launcher { tbSubTitle.Visibility = Visibility.Collapsed; } - + if (!File.Exists(iconPath)) { - imgIco.Source = await ImageLoader.LoadAsync(Path.Combine(Constant.ProgramDirectory, "Images\\app.png")); + // imgIco.Source = await ImageLoader.LoadAsync(Path.Combine(Constant.ProgramDirectory, "Images\\app.png")); } - else + else { - imgIco.Source = await ImageLoader.LoadAsync(iconPath); + // imgIco.Source = await ImageLoader.LoadAsync(iconPath); } Show(); await Dispatcher.InvokeAsync(async () => - { - if (!closing) - { - closing = true; - await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin); - } - }); + { + if (!closing) + { + closing = true; + await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin); + } + }); } } } diff --git a/Flow.Launcher/ResultListBox.axaml b/Flow.Launcher/ResultListBox.axaml index e047f58dd..ab7ccf110 100644 --- a/Flow.Launcher/ResultListBox.axaml +++ b/Flow.Launcher/ResultListBox.axaml @@ -12,12 +12,12 @@ SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" SelectionMode="AlwaysSelected" - AutoScrollToSelectedItem="True" + AutoScrollToSelectedItem="False" IsVisible="{Binding Visibility}" MaxHeight="{Binding MaxHeight}"> -