ImageLoader now loads everything through IShellItemImageFactory::GetImage (#1836)

* Added thumbnail loader

* Deleted old shell icon extraction logic.
Refactored ImageLoader.Load to improve readibility.

* Moved error handling down into the API call itself

* Minor renamings in ImageLoader

* Load icons only for files that are not images. Fixes stutters when loading folders.

* Added the ability to load a full image through ImageLoader.
ImageLoader.Load now also has a "loadFullImage" parameter.

* Max image cache is now 5000 instead of 200.

* Added some commentaries on how thumbnails are loaded
This commit is contained in:
Boris Makogonyuk
2018-03-31 09:19:55 +02:00
committed by CHU Zhaowei
parent 553a6e8ff6
commit 343b904607
5 changed files with 218 additions and 99 deletions

View File

@@ -9,7 +9,7 @@ namespace Wox.Infrastructure.Image
[Serializable]
public class ImageCache
{
private const int MaxCached = 200;
private const int MaxCached = 5000;
public ConcurrentDictionary<string, int> Usage = new ConcurrentDictionary<string, int>();
private readonly ConcurrentDictionary<string, ImageSource> _data = new ConcurrentDictionary<string, ImageSource>();

View File

@@ -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<ConcurrentDictionary<string, int>> _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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -72,6 +72,7 @@
<Compile Include="Hotkey\KeyEvent.cs" />
<Compile Include="Image\ImageCache.cs" />
<Compile Include="Image\ImageLoader.cs" />
<Compile Include="Image\ThumbnailReader.cs" />
<Compile Include="Logger\Log.cs" />
<Compile Include="Storage\ISavable.cs" />
<Compile Include="Storage\PluginJsonStorage.cs" />

View File

@@ -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");