diff --git a/Wox.Infrastructure/Image/ImageCache.cs b/Wox.Infrastructure/Image/ImageCache.cs index a0fac88cca..599b69066d 100644 --- a/Wox.Infrastructure/Image/ImageCache.cs +++ b/Wox.Infrastructure/Image/ImageCache.cs @@ -9,7 +9,7 @@ namespace Wox.Infrastructure.Image [Serializable] public class ImageCache { - private const int MaxCached = 200; + private const int MaxCached = 5000; public ConcurrentDictionary Usage = new ConcurrentDictionary(); private readonly ConcurrentDictionary _data = new ConcurrentDictionary(); diff --git a/Wox.Infrastructure/Image/ImageLoader.cs b/Wox.Infrastructure/Image/ImageLoader.cs index 791cf1dd30..3498e4f3b2 100644 --- a/Wox.Infrastructure/Image/ImageLoader.cs +++ b/Wox.Infrastructure/Image/ImageLoader.cs @@ -2,10 +2,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; -using System.Windows; -using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; using Wox.Infrastructure.Logger; @@ -19,7 +16,7 @@ namespace Wox.Infrastructure.Image private static BinaryStorage> _storage; - private static readonly string[] ImageExtions = + private static readonly string[] ImageExtensions = { ".png", ".jpg", @@ -64,128 +61,94 @@ namespace Wox.Infrastructure.Image ImageCache.Cleanup(); _storage.Save(ImageCache.Usage); } - - private static ImageSource ShellIcon(string fileName) - { - try - { - // http://blogs.msdn.com/b/oldnewthing/archive/2011/01/27/10120844.aspx - var shfi = new SHFILEINFO(); - var himl = SHGetFileInfo( - fileName, - FILE_ATTRIBUTE_NORMAL, - ref shfi, - (uint)Marshal.SizeOf(shfi), - SHGFI_SYSICONINDEX - ); - - if (himl != IntPtr.Zero) - { - var hIcon = ImageList_GetIcon(himl, shfi.iIcon, ILD_NORMAL); - // http://stackoverflow.com/questions/1325625/how-do-i-display-a-windows-file-icon-in-wpf - var img = Imaging.CreateBitmapSourceFromHIcon( - hIcon, - Int32Rect.Empty, - BitmapSizeOptions.FromEmptyOptions() - ); - DestroyIcon(hIcon); - return img; - } - else - { - return new BitmapImage(new Uri(Constant.ErrorIcon)); - } - } - catch (System.Exception e) - { - Log.Exception($"|ImageLoader.ShellIcon|can't get shell icon for <{fileName}>", e); - return ImageCache[Constant.ErrorIcon]; - } - } - - public static ImageSource Load(string path) + + public static ImageSource Load(string path, bool loadFullImage = false) { ImageSource image; - if (string.IsNullOrEmpty(path)) - { - image = ImageCache[Constant.ErrorIcon]; - } - else if (ImageCache.ContainsKey(path)) - { - image = ImageCache[path]; - } - else + try { + if (string.IsNullOrEmpty(path)) + { + return ImageCache[Constant.ErrorIcon]; + } + if (ImageCache.ContainsKey(path)) + { + return ImageCache[path]; + } + if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { - image = new BitmapImage(new Uri(path)); + return new BitmapImage(new Uri(path)); } - else if (Path.IsPathRooted(path)) + + if (!Path.IsPathRooted(path)) { - if (Directory.Exists(path)) + path = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path)); + } + + if (Directory.Exists(path)) + { + /* Directories can also have thumbnails instead of shell icons. + * Generating thumbnails for a bunch of folders while scrolling through + * results from Everything makes a big impact on performance and + * Wox responsibility. + * - Solution: just load the icon + */ + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, + Constant.ThumbnailSize, ThumbnailOptions.IconOnly); + } + else if (File.Exists(path)) + { + var extension = Path.GetExtension(path).ToLower(); + if (ImageExtensions.Contains(extension)) { - image = ShellIcon(path); - } - else if (File.Exists(path)) - { - var externsion = Path.GetExtension(path).ToLower(); - if (ImageExtions.Contains(externsion)) + if (loadFullImage) { - image = new BitmapImage(new Uri(path)); + image = LoadFullImage(path); } else { - image = ShellIcon(path); + /* Although the documentation for GetImage on MSDN indicates that + * if a thumbnail is available it will return one, this has proved to not + * be the case in many situations while testing. + * - Solution: explicitly pass the ThumbnailOnly flag + */ + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, + Constant.ThumbnailSize, ThumbnailOptions.ThumbnailOnly); } } else { - image = ImageCache[Constant.ErrorIcon]; - path = Constant.ErrorIcon; + image = WindowsThumbnailProvider.GetThumbnail(path, Constant.ThumbnailSize, + Constant.ThumbnailSize, ThumbnailOptions.None); } } else { - var defaultDirectoryPath = Path.Combine(Constant.ProgramDirectory, "Images", Path.GetFileName(path)); - if (File.Exists(defaultDirectoryPath)) - { - image = new BitmapImage(new Uri(defaultDirectoryPath)); - } - else - { - image = ImageCache[Constant.ErrorIcon]; - path = Constant.ErrorIcon; - } + image = ImageCache[Constant.ErrorIcon]; + path = Constant.ErrorIcon; } ImageCache[path] = image; image.Freeze(); } + catch (System.Exception e) + { + Log.Exception($"|ImageLoader.Load|Failed to get thumbnail for {path}", e); + + image = ImageCache[Constant.ErrorIcon]; + ImageCache[path] = image; + } return image; } - private const int NAMESIZE = 80; - private const int MAX_PATH = 256; - private const uint SHGFI_SYSICONINDEX = 0x000004000; // get system icon index - private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080; - private const uint ILD_NORMAL = 0x00000000; - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - private struct SHFILEINFO + private static BitmapImage LoadFullImage(string path) { - readonly IntPtr hIcon; - internal readonly int iIcon; - readonly uint dwAttributes; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)] readonly string szDisplayName; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NAMESIZE)] readonly string szTypeName; + BitmapImage image = new BitmapImage(); + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.UriSource = new Uri(path); + image.EndInit(); + return image; } - - [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags); - - [DllImport("User32.dll")] - private static extern int DestroyIcon(IntPtr hIcon); - - [DllImport("comctl32.dll")] - private static extern IntPtr ImageList_GetIcon(IntPtr himl, int i, uint flags); } } diff --git a/Wox.Infrastructure/Image/ThumbnailReader.cs b/Wox.Infrastructure/Image/ThumbnailReader.cs new file mode 100644 index 0000000000..df02e7ca77 --- /dev/null +++ b/Wox.Infrastructure/Image/ThumbnailReader.cs @@ -0,0 +1,154 @@ +using System; +using System.Runtime.InteropServices; +using System.IO; +using System.Windows.Interop; +using System.Windows.Media.Imaging; +using System.Windows; + +namespace Wox.Infrastructure.Image +{ + [Flags] + public enum ThumbnailOptions + { + None = 0x00, + BiggerSizeOk = 0x01, + InMemoryOnly = 0x02, + IconOnly = 0x04, + ThumbnailOnly = 0x08, + InCacheOnly = 0x10, + } + + public class WindowsThumbnailProvider + { + // Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows + + private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93"; + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int SHCreateItemFromParsingName( + [MarshalAs(UnmanagedType.LPWStr)] string path, + IntPtr pbc, + ref Guid riid, + [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem); + + [DllImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DeleteObject(IntPtr hObject); + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] + internal interface IShellItem + { + void BindToHandler(IntPtr pbc, + [MarshalAs(UnmanagedType.LPStruct)]Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)]Guid riid, + out IntPtr ppv); + + void GetParent(out IShellItem ppsi); + void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName); + void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); + void Compare(IShellItem psi, uint hint, out int piOrder); + }; + + internal enum SIGDN : uint + { + NORMALDISPLAY = 0, + PARENTRELATIVEPARSING = 0x80018001, + PARENTRELATIVEFORADDRESSBAR = 0x8001c001, + DESKTOPABSOLUTEPARSING = 0x80028000, + PARENTRELATIVEEDITING = 0x80031001, + DESKTOPABSOLUTEEDITING = 0x8004c000, + FILESYSPATH = 0x80058000, + URL = 0x80068000 + } + + internal enum HResult + { + Ok = 0x0000, + False = 0x0001, + InvalidArguments = unchecked((int)0x80070057), + OutOfMemory = unchecked((int)0x8007000E), + NoInterface = unchecked((int)0x80004002), + Fail = unchecked((int)0x80004005), + ExtractionFailed = unchecked((int)0x8004B200), + ElementNotFound = unchecked((int)0x80070490), + TypeElementNotFound = unchecked((int)0x8002802B), + NoObject = unchecked((int)0x800401E5), + Win32ErrorCanceled = 1223, + Canceled = unchecked((int)0x800704C7), + ResourceInUse = unchecked((int)0x800700AA), + AccessDenied = unchecked((int)0x80030005) + } + + [ComImportAttribute()] + [GuidAttribute("bcc18b79-ba16-442f-80c4-8a59c30c463b")] + [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IShellItemImageFactory + { + [PreserveSig] + HResult GetImage( + [In, MarshalAs(UnmanagedType.Struct)] NativeSize size, + [In] ThumbnailOptions flags, + [Out] out IntPtr phbm); + } + + [StructLayout(LayoutKind.Sequential)] + internal struct NativeSize + { + private int width; + private int height; + + public int Width { set => width = value; } + public int Height { set => height = value; } + }; + + + public static BitmapSource 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()); + } + finally + { + // delete HBitmap to avoid memory leaks + DeleteObject(hBitmap); + } + } + + private static IntPtr GetHBitmap(string fileName, int width, int height, ThumbnailOptions options) + { + IShellItem nativeShellItem; + Guid shellItem2Guid = new Guid(IShellItem2Guid); + int retCode = SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem); + + if (retCode != 0) + throw Marshal.GetExceptionForHR(retCode); + + NativeSize nativeSize = new NativeSize + { + Width = width, + Height = height + }; + + IntPtr hBitmap; + HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap); + + // 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); + } + + Marshal.ReleaseComObject(nativeShellItem); + + if (hr == HResult.Ok) return hBitmap; + + throw new COMException($"Error while extracting thumbnail for {fileName}", Marshal.GetExceptionForHR((int)hr)); + } + } +} \ No newline at end of file diff --git a/Wox.Infrastructure/Wox.Infrastructure.csproj b/Wox.Infrastructure/Wox.Infrastructure.csproj index 5ae7b5701d..af76894ed4 100644 --- a/Wox.Infrastructure/Wox.Infrastructure.csproj +++ b/Wox.Infrastructure/Wox.Infrastructure.csproj @@ -72,6 +72,7 @@ + diff --git a/Wox.Infrastructure/Wox.cs b/Wox.Infrastructure/Wox.cs index dcad17d7f3..50107737b4 100644 --- a/Wox.Infrastructure/Wox.cs +++ b/Wox.Infrastructure/Wox.cs @@ -20,6 +20,7 @@ namespace Wox.Infrastructure public const string Issue = "https://github.com/Wox-launcher/Wox/issues/new"; public static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location.NonNull()).ProductVersion; + public static readonly int ThumbnailSize = 64; public static readonly string DefaultIcon = Path.Combine(ProgramDirectory, "Images", "app.png"); public static readonly string ErrorIcon = Path.Combine(ProgramDirectory, "Images", "app_error.png");