Search code examples
c#winapiicons

Extract largest available icon with ExtractIconEx (or otherwise)


I am using ExtractIconEx to extract an icon by its index from shell32.dll as per this:

https://stackoverflow.com/a/62504226/9420881

I then take the output and convert it to an ImageSource like this:

ImageSource imageSource = Imaging.CreateBitmapSourceFromHIcon(
                hIconLarge,
                Int32Rect.Empty,
                BitmapSizeOptions.FromEmptyOptions());

From looking at shell32.dll I can see that a 256x256 icon is available for index 0, but it is a PNG rather than BMP:

enter image description here

The icon returned is not the 256x256 one, I assume its the 64x64 BMP:

enter image description here

How do I get the 256x256 PNG icon for index 0 (or any other index), if one is available?


Solution

  • Here is an utility class that exposes the list of all icons handles (all sizes, all color counts) for a binary file (note: it cannot read .ico files).

    It's based on ICO Format specification.

    Example 1: loading an icon by index and choosing by size:

    // load all icons handles with index 0 from a dll
    var icons0 = IconInfo.LoadIconsFromBinary(@"c:\windows\system32\shell32.dll", 0);
    
    // get the "jumbo" (256x256) one
    var icon0_256 = icons0.First(i => i.WithIcon(i => i.Size.Width == 256));
    
    // dispose loaded icons
    icons0.ForEach(i => i.Dispose());
    

    Example 2: Loading all icons for a binary file

    // load all icons handles from a dll
    var icons = IconInfo.LoadIconsFromBinary(@"c:\windows\system32\shell32.dll");
    
    // ... do something
    
    // dispose loaded icons
    icons.ForEach(i => i.Dispose());
    

    Example 3: Loading icons handles by id

    // load icons with id "1" from a dll (same as index 0 for shell32)
    // id is the value you see when opening the .dll from Visual Studio as a binary file in the ICON list of Win32 resources
    // we pass it as a negative value
    var iconsId = IconInfo.LoadIconsFromBinary(@"c:\windows\system32\shell32.dll", -1);
    
    // get the jumbo one
    var iconId_256 = iconsId.First(i => i.WithIcon(i => i.Size.Width == 256));
    
    // dispose loaded icons
    iconsId.ForEach(i => i.Dispose());
    

    enter image description here This is icon index 0, id "1"

    Utility class:

    public sealed class IconInfo : IDisposable
    {
        private IconInfo(int groupIndex, string groupId, int index, string id, IntPtr handle)
        {
            GroupIndex = groupIndex;
            GroupId = groupId;
            Index = index;
            Id = id;
            Handle = handle;
        }
    
        public IntPtr Handle { get; }
        public int Index { get; }
        public int GroupIndex { get; }
        public string Id { get; }
        public string GroupId { get; }
    
        public void WithIcon(Action<Icon> action)
        {
            if (action == null)
                throw new ArgumentNullException(nameof(action));
    
            using var icon = Icon.FromHandle(Handle);
            action(icon);
        }
    
        public T WithIcon<T>(Func<Icon, T> func)
        {
            if (func == null)
                throw new ArgumentNullException(nameof(func));
    
            using var icon = Icon.FromHandle(Handle);
            return func(icon);
        }
    
        public override string ToString() => Index.ToString();
        public void Dispose() => DestroyIcon(Handle);
    
        public static List<IconInfo> LoadIconsFromBinary(string iconFilePath, int? byIndexOrResourceId = null)
        {
            if (iconFilePath == null)
                throw new ArgumentNullException(nameof(iconFilePath));
    
            var list = new List<IconInfo>();
            var handle = LoadLibraryEx(iconFilePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
            if (handle != IntPtr.Zero)
            {
                try
                {
                    list = LoadIconsFromBinary(handle, byIndexOrResourceId);
                }
                finally
                {
                    FreeLibrary(handle);
                }
            }
            return list;
        }
    
        private static List<IconInfo> LoadIconsFromBinary(IntPtr handle, int? byIndexOrResourceId)
        {
            var list = new List<IconInfo>();
            var entries = new Dictionary<ushort, GRPICONDIRENTRY>();
            var groupIndices = new Dictionary<ushort, int>();
            var groupIds = new Dictionary<ushort, string>();
            var groupIndex = 0;
            if (EnumResourceNames(handle, new IntPtr(RT_GROUP_ICON), (m, t, n, lp) =>
            {
                if (byIndexOrResourceId.HasValue && byIndexOrResourceId.Value >= 0 && byIndexOrResourceId.Value != groupIndex)
                {
                    groupIndex++;
                    return true;
                }
    
                string name;
                if (n.ToInt64() > ushort.MaxValue)
                {
                    name = Marshal.PtrToStringAuto(n);
                }
                else
                {
                    name = n.ToInt32().ToString(CultureInfo.InvariantCulture);
                }
    
                if (byIndexOrResourceId.HasValue && byIndexOrResourceId.Value < 0 && !string.Equals((-byIndexOrResourceId.Value).ToString(CultureInfo.InvariantCulture), name, StringComparison.Ordinal))
                {
                    groupIndex++;
                    return true;
                }
    
                try
                {
                    ExtractIconGroupEntries(handle, n, t, groupIndex, entries, groupIndices, groupIds);
                    groupIndex++;
                }
                catch
                {
                    // do nothing
                }
                return true;
            }, IntPtr.Zero))
            {
                EnumResourceNames(handle, new IntPtr(RT_ICON), (m, t, n, lp) =>
                {
                    var iconHandle = ExtractIcon(handle, n, t, entries);
                    if (iconHandle != IntPtr.Zero)
                    {
                        var info = new IconInfo(groupIndices[(ushort)n.ToInt32()], groupIds[(ushort)n.ToInt32()], n.ToInt32() - 1, n.ToString(), iconHandle);
                        list.Add(info);
                    }
                    return true;
                }, IntPtr.Zero);
            }
            return list;
        }
    
        private static void ExtractIconGroupEntries(IntPtr module, IntPtr name, IntPtr type, int index, Dictionary<ushort, GRPICONDIRENTRY> entries, Dictionary<ushort, int> groupIndices, Dictionary<ushort, string> groupIds)
        {
            var handle = FindResource(module, name, type);
            if (handle == IntPtr.Zero)
                return;
    
            var size = SizeofResource(module, handle);
            if (size == 0)
                return;
    
            var resource = LoadResource(module, handle);
            if (resource == IntPtr.Zero)
                return;
    
            var ptr = LockResource(resource);
            if (ptr == IntPtr.Zero)
                return;
    
            // GRPICONDIR
            ptr += 2; // idReserved;
            var idtype = Marshal.ReadInt16(ptr);
            if (idtype != 1)  // idType, 1 for ICO
                return;
    
            var elementSize = Marshal.SizeOf<GRPICONDIRENTRY>();
    
            ptr += 2;
            var count = Marshal.ReadInt16(ptr);
            ptr += 2;
            for (var i = 0; i < count; i++)
            {
                var entry = Marshal.PtrToStructure<GRPICONDIRENTRY>(ptr);
                ptr += elementSize;
                entries[entry.nId] = entry;
    
                // is it a string or an id?
                groupIndices[entry.nId] = index;
                if (name.ToInt64() > ushort.MaxValue)
                {
                    var id = Marshal.PtrToStringAuto(name);
                    groupIds[entry.nId] = id;
                }
                else
                {
                    groupIds[entry.nId] = "#" + name.ToInt32();
                }
            }
        }
    
        private static IntPtr ExtractIcon(IntPtr module, IntPtr name, IntPtr type, Dictionary<ushort, GRPICONDIRENTRY> entries)
        {
            if (!entries.TryGetValue((ushort)name.ToInt32(), out _))
                return IntPtr.Zero;
    
            var hres = FindResource(module, name, type);
            if (hres == IntPtr.Zero)
                return IntPtr.Zero;
    
            var size = SizeofResource(module, hres);
            if (size == 0)
                return IntPtr.Zero;
    
            var res = LoadResource(module, hres);
            if (res == IntPtr.Zero)
                return IntPtr.Zero;
    
            var ptr = LockResource(res);
            if (ptr == IntPtr.Zero)
                return IntPtr.Zero;
    
            return CreateIconFromResourceEx(ptr, size, true, 0x30000, 0, 0, 0);
        }
    
        private delegate bool EnumResNameProc(IntPtr hModule, IntPtr lpszType, IntPtr lpszName, IntPtr lParam);
    
        [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern bool EnumResourceNames(IntPtr hModule, IntPtr lpszType, EnumResNameProc lpEnumFunc, IntPtr lParam);
    
        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        private static extern IntPtr FindResource(IntPtr hModule, IntPtr lpName, IntPtr lpType);
    
        [DllImport("kernel32")]
        private static extern int SizeofResource(IntPtr hModule, IntPtr hResInfo);
    
        [DllImport("kernel32")]
        private static extern IntPtr LoadResource(IntPtr hModule, IntPtr hResInfo);
    
        [DllImport("user32")]
        private static extern IntPtr CreateIconFromResourceEx(IntPtr presbits, int dwResSize, bool fIcon, int dwVer, int cxDesired, int cyDesired, int flags);
    
        [DllImport("user32")]
        private static extern bool DestroyIcon(IntPtr handle);
    
        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        private static extern IntPtr LockResource(IntPtr hResData);
    
        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        private static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hFile, int dwFlags);
    
        [DllImport("kernel32")]
        private static extern bool FreeLibrary(IntPtr hModule);
    
        private const int LOAD_LIBRARY_AS_DATAFILE = 0x2;
        private const int LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x20;
        private const int RT_ICON = 3;
        private const int RT_GROUP_ICON = RT_ICON + 11;
    
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        private struct GRPICONDIRENTRY
        {
            public byte bWidth;
            public byte bHeight;
            public byte bColorCount;
            public byte bReserved;
            public short wPlanes;
            public short wBitCount;
            public int dwBytesInRes;
            public ushort nId;
        }
    }