28

What's the most efficient way of extracting thumbnails from any file, not just just images at varying sizes?

I've looked all over, most promising of which was Windows API ShellFile yet this didn't appear to install properly. I'm using windows 7.

Daniel Peñalba
  • 30,507
  • 32
  • 137
  • 219
matinau
  • 305
  • 1
  • 3
  • 10
  • 2
    What is ineffcient in your way that your are asking the *most effcient* one? – L.B Feb 13 '14 at 10:41
  • Have a look at the top answer for this... http://stackoverflow.com/questions/1439719/c-sharp-get-thumbnail-from-file-via-windows-api – Matt W Feb 13 '14 at 10:45

2 Answers2

60

Some time ago I wrote a ThumbnailProvider that loads a thumbnail from the Win API. Supports transparent images. This is the implementation:

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.IO;

    namespace ThumbnailGenerator
    {
        [Flags]
        public enum ThumbnailOptions
        {
            None = 0x00,
            BiggerSizeOk = 0x01,
            InMemoryOnly = 0x02,
            IconOnly = 0x04,
            ThumbnailOnly = 0x08,
            InCacheOnly = 0x10,
        }

        public class WindowsThumbnailProvider
        {
            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,
                // The following parameter is not used - binding context.
                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),
                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; } }
            };

            [StructLayout(LayoutKind.Sequential)]
            public struct RGBQUAD
            {
                public byte rgbBlue;
                public byte rgbGreen;
                public byte rgbRed;
                public byte rgbReserved;
            }

            public static Bitmap GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
            {
                IntPtr hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);

                try
                {
                    // return a System.Drawing.Bitmap from the hBitmap
                    return GetBitmapFromHBitmap(hBitmap);
                }
                finally
                {
                    // delete HBitmap to avoid memory leaks
                    DeleteObject(hBitmap);
                }
            }

            public static Bitmap GetBitmapFromHBitmap(IntPtr nativeHBitmap)
            {
                Bitmap bmp = Bitmap.FromHbitmap(nativeHBitmap);

                if (Bitmap.GetPixelFormatSize(bmp.PixelFormat) < 32)
                    return bmp;

                return CreateAlphaBitmap(bmp, PixelFormat.Format32bppArgb);
            }

            public static Bitmap CreateAlphaBitmap(Bitmap srcBitmap, PixelFormat targetPixelFormat)
            {
                Bitmap result = new Bitmap(srcBitmap.Width, srcBitmap.Height, targetPixelFormat);

                Rectangle bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);

                BitmapData srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);

                bool isAlplaBitmap = false;

                try
                {
                    for (int y = 0; y <= srcData.Height - 1; y++)
                    {
                        for (int x = 0; x <= srcData.Width - 1; x++)
                        {
                            Color pixelColor = Color.FromArgb(
                                Marshal.ReadInt32(srcData.Scan0, (srcData.Stride * y) + (4 * x)));

                            if (pixelColor.A > 0 & pixelColor.A < 255)
                            {
                                isAlplaBitmap = true;
                            }

                            result.SetPixel(x, y, pixelColor);
                        }
                    }
                }
                finally
                {
                    srcBitmap.UnlockBits(srcData);
                }

                if (isAlplaBitmap)
                {
                    return result;
                }
                else
                {
                    return srcBitmap;
                }
            }

            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();
                nativeSize.Width = width;
                nativeSize.Height = height;

                IntPtr hBitmap;
                HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(nativeSize, options, out hBitmap);

                Marshal.ReleaseComObject(nativeShellItem);

                if (hr == HResult.Ok) return hBitmap;

                throw Marshal.GetExceptionForHR((int)hr);
            }
        }
    }

Then you can use it in the following way:

int THUMB_SIZE = 256;
Bitmap thumbnail = WindowsThumbnailProvider.GetThumbnail(
   fileName, THUMB_SIZE, THUMB_SIZE, ThumbnailOptions.None);

Remember that you need to Dispose() the bitmap after using it.

JKennedy
  • 18,150
  • 17
  • 114
  • 198
Daniel Peñalba
  • 30,507
  • 32
  • 137
  • 219
12

This is an improved version of the (awesome) answer that was accepted. The following was changed:

  • Use unsafe code instead of SetPixel() -> that improves performance drastically
  • remove the whole isAlphaBitmap check. I have no idea why that was implemented in the first place, but I see no reason to determine between two images which contain the same content (I just return the Bitmap with transparency, regardless if that changed something)

    [Flags]
    public enum ThumbnailOptions
    {
        None = 0x00,
        BiggerSizeOk = 0x01,
        InMemoryOnly = 0x02,
        IconOnly = 0x04,
        ThumbnailOnly = 0x08,
        InCacheOnly = 0x10
    }
    
    public class WindowsThumbnailProvider
    {
        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,
            // The following parameter is not used - binding context.
            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);
    
        [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern unsafe IntPtr memcpy(void* dst, void* src, UIntPtr count);
    
        public static Bitmap GetThumbnail(string fileName, int width, int height, ThumbnailOptions options)
        {
            var hBitmap = GetHBitmap(Path.GetFullPath(fileName), width, height, options);
    
            try
            {
                // return a System.Drawing.Bitmap from the hBitmap
                return GetBitmapFromHBitmap(hBitmap);
            }
            finally
            {
                // delete HBitmap to avoid memory leaks
                DeleteObject(hBitmap);
            }
        }
    
        public static Bitmap GetBitmapFromHBitmap(IntPtr nativeHBitmap)
        {
            Bitmap bmp = Image.FromHbitmap(nativeHBitmap);
    
            if (Image.GetPixelFormatSize(bmp.PixelFormat) < 32)
                return bmp;
    
            using (bmp)
                return CreateAlphaBitmap(bmp, PixelFormat.Format32bppArgb);
        }
    
        public static unsafe Bitmap CreateAlphaBitmap(Bitmap srcBitmap, PixelFormat targetPixelFormat)
        {
            var result = new Bitmap(srcBitmap.Width, srcBitmap.Height, targetPixelFormat);
    
            var bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);
            var srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);
            var destData = result.LockBits(bmpBounds, ImageLockMode.ReadOnly, targetPixelFormat);
    
            var srcDataPtr = (byte*) srcData.Scan0;
            var destDataPtr = (byte*) destData.Scan0;
    
            try
            {
                for (int y = 0; y <= srcData.Height - 1; y++)
                {
                    for (int x = 0; x <= srcData.Width - 1; x++)
                    {
                        //this is really important because one stride may be positive and the other negative
                        var position = srcData.Stride * y + 4 * x;
                        var position2 = destData.Stride * y + 4 * x;
    
                        memcpy(destDataPtr + position2, srcDataPtr + position, (UIntPtr) 4);
                    }
                }
            }
            finally
            {
                srcBitmap.UnlockBits(srcData);
                result.UnlockBits(destData);
            }
    
            return result;
        }
    
        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();
            nativeSize.Width = width;
            nativeSize.Height = height;
    
            IntPtr hBitmap;
            HResult hr = ((IShellItemImageFactory) nativeShellItem).GetImage(nativeSize, options, out hBitmap);
    
            Marshal.ReleaseComObject(nativeShellItem);
    
            if (hr == HResult.Ok) return hBitmap;
    
            throw Marshal.GetExceptionForHR((int) hr);
        }
    
        [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),
            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)
        }
    
        [ComImport]
        [Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
        [InterfaceType(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; }
            }
        }
    
        [StructLayout(LayoutKind.Sequential)]
        public struct RGBQUAD
        {
            public byte rgbBlue;
            public byte rgbGreen;
            public byte rgbRed;
            public byte rgbReserved;
        }
    }
    
Snicker
  • 957
  • 10
  • 16
  • Your `using (srcBitmap)…` should be done by caller(s), in this case, `GetBitmapFromHBitmap`, so the last line of that function should be replaced with `using (srcBitmap) return CreateAlphaBitmap(bmp, PixelFormat.Format32bppArgb);`. The way you have it, your "use" of srcBitmap is **not** inside its own "using" block. While special knowledge of the `using` keyword tells us that this unconventional code works, it violates RAII, thus ambiguating cleanup responsibility. Note that it only works now given special knowledge of your (only) caller; a different caller might want to continue using `bmp`. – Glenn Slayden Apr 19 '17 at 01:44
  • p.s. If for some reason you want to keep it the way you have it, then IMHO it would be clearer to simply call `srcBitmap.Dispose()`, instead of using the C# `using` construct. And if you'd like me to edit your answer as I proposed above, just let me know. – Glenn Slayden Apr 19 '17 at 01:49
  • @GlennSlayden you are completely right, thank you for your feedback. I thought about it like if you create a transparent image of your image, you never want to keep the non-transparent image, but you are right, it is better like that. I just edited my answer, please check if that's alright, else change it the way you like. Have a nice day :) – Snicker Apr 19 '17 at 13:45
  • Looks good, but looking further I think you can actually get rid of the bitmap re-copying altogether (i.e. delete the entire `CreateAlphaBitmap` function). Isn't the bitmap returned from `Image.FromHbitmap(...)` perfectly good enough? Also, you should definitely call `Freeze()` on the final bitmap before returning it.. – Glenn Slayden Apr 19 '17 at 21:36
  • @GlennSlayden You can freeze System.Drawing.Bitmap? Isn't that a WPF-only thing? As much as I remember, `CreateAlphaBitmap` was quite important, feel free to check out the difference. – Snicker Apr 20 '17 at 20:08
  • My bad. Yes, it's a WPF thing, which is what I was thinking of. – Glenn Slayden Apr 21 '17 at 17:16