2018-03-31 07:19:55 +00:00
using System ;
using System.IO ;
2025-09-23 00:40:58 +00:00
using System.Runtime.InteropServices ;
2024-12-10 12:44:28 +00:00
using System.Windows ;
2018-03-31 07:19:55 +00:00
using System.Windows.Interop ;
using System.Windows.Media.Imaging ;
2025-09-23 00:40:58 +00:00
using IniParser ;
2024-12-10 12:44:28 +00:00
using Windows.Win32 ;
using Windows.Win32.Foundation ;
using Windows.Win32.Graphics.Gdi ;
2025-09-23 00:40:58 +00:00
using Windows.Win32.UI.Shell ;
2018-03-31 07:19:55 +00:00
2020-04-21 09:12:17 +00:00
namespace Flow.Launcher.Infrastructure.Image
2018-03-31 07:19:55 +00:00
{
2024-12-10 12:44:28 +00:00
/// <summary>
2025-04-15 04:52:09 +00:00
/// Subclass of <see cref="SIIGBF"/>
2024-12-10 12:44:28 +00:00
/// </summary>
2018-03-31 07:19:55 +00:00
[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
2024-12-10 12:44:28 +00:00
private static readonly Guid GUID_IShellItem = typeof ( IShellItem ) . GUID ;
2018-03-31 07:19:55 +00:00
2025-04-15 04:59:58 +00:00
private static readonly HRESULT S_EXTRACTIONFAILED = ( HRESULT ) 0x8004B200 ;
2025-02-17 11:07:01 +00:00
2025-04-15 04:52:09 +00:00
private static readonly HRESULT S_PATHNOTFOUND = ( HRESULT ) 0x8004B205 ;
2025-09-23 00:40:58 +00:00
private const string UrlExtension = ".url" ;
2025-09-23 13:39:42 +00:00
/// <summary>
/// Obtains a BitmapSource thumbnail for the specified file.
/// </summary>
/// <remarks>
/// If the file is a Windows URL shortcut (".url"), the method attempts to resolve the shortcut's icon and use that for the thumbnail; otherwise it requests a thumbnail for the file path. The native HBITMAP used to create the BitmapSource is always released to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file (can be a regular file or a ".url" shortcut).</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail extraction options (flags) controlling fallback and caching behavior.</param>
/// <returns>A BitmapSource representing the requested thumbnail.</returns>
2018-03-31 07:19:55 +00:00
public static BitmapSource GetThumbnail ( string fileName , int width , int height , ThumbnailOptions options )
{
2025-09-23 00:40:58 +00:00
HBITMAP hBitmap ;
2025-09-23 02:53:54 +00:00
var extension = Path . GetExtension ( fileName ) ;
if ( string . Equals ( extension , UrlExtension , StringComparison . OrdinalIgnoreCase ) )
2025-09-23 00:40:58 +00:00
{
hBitmap = GetHBitmapForUrlFile ( fileName , width , height , options ) ;
}
else
{
hBitmap = GetHBitmap ( Path . GetFullPath ( fileName ) , width , height , options ) ;
}
2018-03-31 07:19:55 +00:00
try
{
return Imaging . CreateBitmapSourceFromHBitmap ( hBitmap , IntPtr . Zero , Int32Rect . Empty , BitmapSizeOptions . FromEmptyOptions ( ) ) ;
}
finally
{
// delete HBitmap to avoid memory leaks
2024-12-10 12:44:28 +00:00
PInvoke . DeleteObject ( hBitmap ) ;
2018-03-31 07:19:55 +00:00
}
}
2024-12-10 12:44:28 +00:00
2025-09-23 13:39:42 +00:00
/// <summary>
/// Obtains a native HBITMAP for the specified file at the requested size using the Windows Shell image factory.
/// </summary>
/// <remarks>
/// If <paramref name="options"/> is <see cref="ThumbnailOptions.ThumbnailOnly"/> and thumbnail extraction fails
/// due to extraction errors or a missing path, the method falls back to requesting an icon (<see cref="ThumbnailOptions.IconOnly"/>).
/// The returned HBITMAP is a raw GDI handle; the caller is responsible for releasing it (e.g., via DeleteObject) to avoid native memory leaks.
/// </remarks>
/// <param name="fileName">Path to the file to thumbnail.</param>
/// <param name="width">Requested thumbnail width in pixels.</param>
/// <param name="height">Requested thumbnail height in pixels.</param>
/// <param name="options">Thumbnail request flags that control behavior (e.g., ThumbnailOnly, IconOnly).</param>
/// <returns>An HBITMAP handle containing the image. Caller must free the handle when finished.</returns>
/// <exception cref="COMException">If creating the shell item fails (HRESULT returned by SHCreateItemFromParsingName).</exception>
/// <exception cref="InvalidOperationException">If the shell item does not expose IShellItemImageFactory or if an unexpected error occurs while obtaining the image.</exception>
2024-12-10 12:44:28 +00:00
private static unsafe HBITMAP GetHBitmap ( string fileName , int width , int height , ThumbnailOptions options )
2018-03-31 07:19:55 +00:00
{
2024-12-10 12:44:28 +00:00
var retCode = PInvoke . SHCreateItemFromParsingName (
fileName ,
null ,
GUID_IShellItem ,
out var nativeShellItem ) ;
2018-03-31 07:19:55 +00:00
2024-12-10 12:44:28 +00:00
if ( retCode ! = HRESULT . S_OK )
2018-03-31 07:19:55 +00:00
throw Marshal . GetExceptionForHR ( retCode ) ;
2024-12-10 12:44:28 +00:00
if ( nativeShellItem is not IShellItemImageFactory imageFactory )
2018-03-31 07:19:55 +00:00
{
2024-12-10 12:44:28 +00:00
Marshal . ReleaseComObject ( nativeShellItem ) ;
2024-12-11 03:08:57 +00:00
nativeShellItem = null ;
2024-12-10 12:44:28 +00:00
throw new InvalidOperationException ( "Failed to get IShellItemImageFactory" ) ;
}
2018-03-31 07:19:55 +00:00
2024-12-10 12:44:28 +00:00
SIZE size = new SIZE
{
cx = width ,
cy = height
} ;
2018-03-31 07:19:55 +00:00
2024-12-10 12:44:28 +00:00
HBITMAP hBitmap = default ;
try
2018-03-31 07:19:55 +00:00
{
2024-12-10 12:44:28 +00:00
try
{
imageFactory . GetImage ( size , ( SIIGBF ) options , & hBitmap ) ;
}
2025-04-15 04:52:09 +00:00
catch ( COMException ex ) when ( options = = ThumbnailOptions . ThumbnailOnly & &
2025-04-15 04:59:58 +00:00
( ex . HResult = = S_PATHNOTFOUND | | ex . HResult = = S_EXTRACTIONFAILED ) )
2025-02-16 14:47:10 +00:00
{
2025-04-15 04:52:09 +00:00
// Fallback to IconOnly if extraction fails or files cannot be found
2025-02-16 14:47:10 +00:00
imageFactory . GetImage ( size , ( SIIGBF ) ThumbnailOptions . IconOnly , & hBitmap ) ;
}
2025-02-17 11:07:01 +00:00
catch ( FileNotFoundException ) when ( options = = ThumbnailOptions . ThumbnailOnly )
2024-12-10 12:44:28 +00:00
{
2025-02-16 14:50:43 +00:00
// Fallback to IconOnly if files cannot be found
2024-12-10 12:44:28 +00:00
imageFactory . GetImage ( size , ( SIIGBF ) ThumbnailOptions . IconOnly , & hBitmap ) ;
}
2025-04-15 04:52:09 +00:00
catch ( System . Exception ex )
{
// Handle other exceptions
throw new InvalidOperationException ( "Failed to get thumbnail" , ex ) ;
}
2024-12-10 12:44:28 +00:00
}
finally
{
2024-12-11 03:08:57 +00:00
if ( nativeShellItem ! = null )
{
Marshal . ReleaseComObject ( nativeShellItem ) ;
}
2018-03-31 07:19:55 +00:00
}
2024-12-10 12:44:28 +00:00
return hBitmap ;
2018-03-31 07:19:55 +00:00
}
2025-09-23 00:40:58 +00:00
2025-09-23 13:39:42 +00:00
/// <summary>
/// Obtains an HBITMAP for a Windows .url shortcut by resolving its IconFile entry and delegating to GetHBitmap.
/// </summary>
/// <remarks>
/// The method parses the .url file as an INI, looks in the "InternetShortcut" section for the "IconFile" entry,
/// and requests a bitmap for that icon path. If no IconFile is present or any error occurs while reading or
/// resolving the icon, it falls back to requesting a thumbnail for the .url file itself.
/// </remarks>
/// <param name="fileName">Path to the .url shortcut file.</param>
/// <param name="width">Requested thumbnail width (pixels).</param>
/// <param name="height">Requested thumbnail height (pixels).</param>
/// <param name="options">ThumbnailOptions flags controlling extraction behavior.</param>
/// <returns>An HBITMAP containing the requested image; callers are responsible for freeing the native handle.</returns>
2025-09-23 00:40:58 +00:00
private static unsafe HBITMAP GetHBitmapForUrlFile ( string fileName , int width , int height , ThumbnailOptions options )
{
HBITMAP hBitmap ;
try
{
var parser = new FileIniDataParser ( ) ;
var data = parser . ReadFile ( fileName ) ;
var urlSection = data [ "InternetShortcut" ] ;
var iconPath = urlSection ? [ "IconFile" ] ;
2025-09-23 12:19:39 +00:00
if ( ! File . Exists ( iconPath ) )
2025-09-23 00:40:58 +00:00
{
2025-09-23 12:19:39 +00:00
// If the IconFile is missing, throw exception to fallback to the default icon
2025-09-23 04:22:46 +00:00
throw new FileNotFoundException ( "Icon file not specified in Internet shortcut (.url) file." ) ;
2025-09-23 00:40:58 +00:00
}
hBitmap = GetHBitmap ( Path . GetFullPath ( iconPath ) , width , height , options ) ;
}
catch
{
2025-09-23 12:48:41 +00:00
hBitmap = GetHBitmap ( Path . GetFullPath ( fileName ) , width , height , options ) ;
2025-09-23 00:40:58 +00:00
}
return hBitmap ;
}
2018-03-31 07:19:55 +00:00
}
2024-12-10 12:44:28 +00:00
}