Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Last active September 7, 2026 12:36
Show Gist options
  • Select an option

  • Save scriptingstudio/0b15927ac5cf9016d0fdcd0dc6e101e6 to your computer and use it in GitHub Desktop.

Select an option

Save scriptingstudio/0b15927ac5cf9016d0fdcd0dc6e101e6 to your computer and use it in GitHub Desktop.
Yet another icon extractor from DLL,EXE,ICO for C#, PowerShell, and Python
/*
SYNOPSIS : Icon Reader for C#
DESCRIPTION : A utility class that extracts the list of all icons handles (all sizes, all color counts) for a binary file like DLL, EXE, or ICO
UTILITIES : GetTotal, ImportIcons, ExportIcons, Dispose
FEATURES : Export: by size and index, multisize icons, split icon groups by size
VERSION : 4.0
LASTUPDATE : 2026-Sep-5
LINK : https://en.wikipedia.org/wiki/ICO_(file_format)
NOTES : Only builtin sizes are exported; GIF,BMP,JPEG always have the background in black
BACKLOG :
// get amount of icons stored in a dll
var icons = IconReader.GetTotal(@"c:\windows\system32\imageres.dll");
// get all icons handles with index 0 from a dll
var icons = IconReader.ImportIcons(@"c:\windows\system32\shell32.dll", 0);
// get all icons handles from a dll
var icons = IconReader.ImportIcons(@"c:\windows\system32\shell32.dll");
// get all icons handles from an ico
var icons = IconReader.ImportIcons(@"<your_ico_filepath>");
// dispose imported icons
icons.ForEach(i => i.Dispose());
*/
// .NET 8+ Edition
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Reflection;
using System.Linq;
using System.Net;
using System.Net.Http;
public sealed class IconReader : IDisposable
{
public IntPtr Handle { get; } // primary ID for icon
public int Index { get; } // global index, inc multi sizing
public int GroupIndex { get; } // EXE,DLL - group (multi-size icon) index; ICO - 0
public string Id { get; } // primarily for EXE,DLL
public string GroupId { get; } // primarily for EXE,DLL
public int Width { get; }
public int Height { get; }
public int Bpp { get; }
public int Colors { get; }
public int ImgLength { get; }
public object? IconData { get; set; } // icon image/object storage; by default contains image bytes
private IconReader(int groupIndex, string groupId, int index, string id, int width, int height, int bpp, int colors, int len, IntPtr handle, object? imgData=null, bool rawData=true)
{
GroupIndex = groupIndex;
GroupId = groupId;
Index = index;
Id = id;
Width = width; // width == 0 ? 256 : width;
Height = height; // height == 0 ? 256 : height;
Bpp = bpp;
Colors = colors;
ImgLength = len;
Handle = handle;
IconData = rawData ? imgData : Icon.FromHandle(handle);
}
// Utilities
//public override string ToString() => Index.ToString();
public void Dispose()
{
if (IconData != null && IconData.GetType() == typeof(Icon)) ((Icon)IconData).Dispose();
DestroyIcon(Handle);
}
// DLL,EXE: the total doesn't take into account multi sizing
public static int GetTotal(string iconFilePath)
{
iconFilePath = ResolveInputURI(iconFilePath);
if (string.IsNullOrEmpty(iconFilePath)) return 0; // or ArgumentNullException?
if (Path.GetExtension(iconFilePath).ToLower() == ".ico")
{
byte[] signature;
FileStream stream;
try {
using (stream = File.Open(iconFilePath, FileMode.Open))
{
using (var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, false))
{ // don't modify "using" syntax
signature = reader.ReadBytes(6); // 4+2
if (signature.Length < 6) // corrupted
throw new ArgumentException($"'{iconFilePath}' is not a valid ICO file.");
}
}
}
catch {throw new ArgumentException($"Unable to read '{iconFilePath}' file.");}
if (BitConverter.ToInt32(signature[..4], 0) != 0x10000) // Basic validation
throw new ArgumentException($"'{iconFilePath}' is not a valid ICO file.");
return BitConverter.ToInt16(signature[4..6], 0);
}
else // dll or exe
{ // version 2, uniform; version 1 is simpler and faster
int icount = 0;
var handle = LoadLibraryEx(iconFilePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (handle != IntPtr.Zero)
{
EnumResourceNames(handle, new IntPtr(RT_GROUP_ICON), (m, t, n, lp) => { icount++; return true; }, IntPtr.Zero);
FreeLibrary(handle);
}
return icount;
}
} // GetTotal
public static List<IconReader> ImportIcons(string iconFilePath, int[]? byIndex=null, int[]? bySize=null, bool rawData=true)
{
iconFilePath = ResolveInputURI(iconFilePath);
if (string.IsNullOrEmpty(iconFilePath))
throw new ArgumentNullException(nameof(iconFilePath));
// Validate input filters
if (bySize != null && bySize.Length > 0)
{
bySize = bySize.Where(n => n > 15 && n < 257).ToArray();
if (bySize.Length == 0) bySize = new[]{-1000}; // stop value
}
else bySize = null;
if (byIndex != null && byIndex.Length > 0)
{
byIndex = byIndex.Where(n => n > -1).ToArray();
if (byIndex.Length == 0) byIndex = new[]{-1000}; // stop value
}
else byIndex = null;
if (Path.GetExtension(iconFilePath).ToLower() == ".ico")
return ImportIco(iconFilePath, byIndex, bySize, rawData);
var icons = new List<IconReader>();
var handle = LoadLibraryEx(iconFilePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (handle != IntPtr.Zero)
{
try
{
icons = ImportIcons(handle, byIndex, bySize, rawData);
}
finally
{
FreeLibrary(handle);
}
}
return icons;
} // ImportIcons
private static List<IconReader> ImportIcons(IntPtr handle, int[]? byIndex=null, int[]? bySize=null, bool rawData=true)
{
var icons = new List<IconReader>();
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 (byIndex != null && !Array.Exists(byIndex, el => el > -1 && el == groupIndex))
{
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) =>
{
if (!entries.TryGetValue((ushort)n.ToInt32(), out _)) return true;
var imgbytes = GetImageBytes(handle, n, t);
if (imgbytes == null) return true;
var iconHandle = CreateIconFromResourceEx(imgbytes, imgbytes.Length, true, 0x00030000,0,0,0);
if (iconHandle != IntPtr.Zero)
{
var id = (ushort)n.ToInt32();
//var ent = entries[id]; // ICONDIR
var w = entries[id].bWidth == 0 ? 256 : entries[id].bWidth;
var h = entries[id].bHeight == 0 ? 256 : entries[id].bHeight;
var l = entries[id].dwBytesInRes;
var bpp = entries[id].wBitCount;
int colors = entries[id].bColorCount;
if (colors == 0 && bpp == 8) colors = 256;
if (bySize == null || Array.Exists(bySize, el => el == w))
{
var info = new IconReader(groupIndices[id], groupIds[id], n.ToInt32() - 1, n.ToString(), w,h,bpp,colors,l, iconHandle,imgbytes,rawData);
icons.Add(info);
}
}
return true;
}, IntPtr.Zero);
}
return icons;
} // ImportIcons
public static bool ExportIcons(string iconFilePath=null, List<IconReader>? iconInfo=null, string? outputPath=null, int[]? bySize=null, int[]? byIndex=null, bool split=false, string? type="ico", Color? background=null)
{
// Resolve parameter sets
iconFilePath = ResolveInputURI(iconFilePath);
var ii = iconInfo == null || iconInfo.Count == 0;
if (string.IsNullOrEmpty(iconFilePath) && ii)
throw new ArgumentNullException("Filename and icon collection are empty.");
if (string.IsNullOrEmpty(iconFilePath) && string.IsNullOrEmpty(outputPath)) return false;
try { iconFilePath = Path.GetFullPath(iconFilePath); } catch { return false; }
var src = Path.GetExtension(iconFilePath).ToLower().Substring(1); // exe,dll,ico
if (src == "ico" && !split && (bySize == null || bySize.Length == 0)) split = true;
if (string.IsNullOrEmpty(type)) type = "ico";
//var raw = type.ToLower() == "ico";
if (ii) iconInfo = ImportIcons(iconFilePath,byIndex,bySize); //,raw
if (iconInfo == null || iconInfo.Count == 0) return false;
// Create output file name template
var fn = Path.GetFileNameWithoutExtension(iconFilePath);
if (string.IsNullOrEmpty(outputPath))
{
var sfx = src == "ico" ? "-extract-%index%.ico" : "-%index%.ico";
outputPath = string.Format(@"{0}\{1}{2}", Path.GetDirectoryName(iconFilePath),fn,sfx);
}
else
{
try { outputPath = Path.GetFullPath(outputPath); } catch { return false; }
outputPath = string.Format(@"{0}\{1}-%index%.ico", outputPath,fn);
}
if (Regex.IsMatch(outputPath,@"c:\\(windows.+|program.+|[^\\]+)$",RegexOptions.IgnoreCase))
Console.WriteLine("WARNING: System location specified as output. Export may fail. Make sure that you have write access rights.");
// Validate input filters
if (bySize != null && bySize.Length > 0)
bySize = bySize.Where(n => n > 15 && n < 257).OrderByDescending(n => n).ToArray();
else bySize = null;
if (byIndex != null && byIndex.Length > 0)
byIndex = byIndex.Where(n => n > -1).OrderByDescending(n => n).ToArray();
else byIndex = null;
// Other formats
if (type.ToLower() != "ico") return ExportImages(iconInfo,outputPath,type,bySize,byIndex,background);
// Group by GroupIndex first
var igroups = iconInfo.Where(i => i.IconData != null).GroupBy(i => i.GroupIndex).OrderBy(i => i.Key);
foreach (var gr in igroups)
{
if (byIndex != null && !Array.Exists(byIndex, el => el == gr.Key)) continue; // filter
var iconGroup = gr.ToList(); // select a group
var series = split ? iconGroup.Count : 1; // mode selector
for (var s = 0; s < series; s++)
{
// select the next chunk to work with
var icoWork = split ? new List<IconReader>{iconGroup[s]} : iconGroup;
// resolve output file name
// PROBLEM: size may be wrong because of filtering (below); TEST!!!
var icofile = outputPath.Replace("%index%",string.Format("{0}%sz%%color%",gr.Key));
if (split) {
if (icoWork[0].Colors == 0)
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}",icoWork[0].Width));
else
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}-{1}",icoWork[0].Width,icoWork[0].Colors));
}
else
icofile = icofile.Replace("%sz%%color%","");
FileStream outputStream;
try {outputStream = new FileStream(icofile, FileMode.OpenOrCreate);}
catch {continue;}
if (outputStream == null) continue;
var iconWriter = new BinaryWriter(outputStream);
if (iconWriter == null)
{
outputStream.Close();
outputStream.Dispose();
continue;
}
bool inew = false; // no icons to export
// 0-1 reserved, 0
iconWriter.Write((byte)0);
iconWriter.Write((byte)0);
// 2-3 image type, 1 = icon, 2 = cursor
iconWriter.Write((short)1);
// 4-5 number of images
iconWriter.Write((short)icoWork.Count);
//var natives = icoWork.Select(n => n.Width).Distinct(); // builtin sizes
int offset = 6 + (16 * icoWork.Count);
short icocount = 0;
foreach (var icon in icoWork)
{
if (bySize != null && !Array.Exists(bySize, el => el == icon.Width)) continue; // filter
var w = icon.Width == 256 ? 0 : icon.Width;
var h = icon.Height == 256 ? 0 : icon.Height;
// image entry
// 0 image width
iconWriter.Write((byte)w);
// 1 image height
iconWriter.Write((byte)h);
// 2 number of colors
iconWriter.Write((byte)0);
// 3 reserved
iconWriter.Write((byte)0);
// 4-5 color planes
iconWriter.Write((short)0);
// 6-7 bits per pixel
iconWriter.Write((short)32);
// 8-11 size of image data
iconWriter.Write((int)icon.ImgLength);
// 12-15 offset of image data
iconWriter.Write((int)offset);
offset += icon.ImgLength;
inew = true;
icocount++;
}
foreach (var icon in icoWork)
{
// image data
// png data must contain the whole png data file
if (bySize == null || Array.Exists(bySize, el => el == icon.Width))
iconWriter.Write(icon.IconData as byte[]);
}
if (inew && icocount != icoWork.Count)
{
// if a filter happend update icon amount value with actual
iconWriter.Write(BitConverter.GetBytes(icocount), 4,2); // TEST!!!!!
}
if (inew) iconWriter.Flush(); // flush buffer to a file
// Cleanup
iconWriter.Close();
iconWriter.Dispose();
outputStream.Close();
outputStream.Dispose();
if (!inew) File.Delete(icofile);
else Console.WriteLine($"Icon '{icofile}' successfully exported.");
//else Console.WriteLine(string.Format("Icon '{0}' successfully exported.",icofile));
} // split mode
} // groups
iconInfo.ForEach(i => i.Dispose()); // cleanup
return true;
} // ExportIcons
// Icon helpers
private static List<IconReader> ImportIco(string iconFilePath, int[]? byIndex, int[]? bySize, bool rawData=true)
{
byte[] bytes;
try {bytes = File.ReadAllBytes(iconFilePath);} catch
{
throw new ArgumentException($"Unable to read '{iconFilePath}' file.");
}
// Basic validation
if (bytes.Length < 6 || BitConverter.ToInt32(bytes[..4],0) != 0x10000)
throw new ArgumentException($"'{iconFilePath}' is not a valid ICO file.");
var icons = new List<IconReader>();
int dirend = 16 * BitConverter.ToInt16(bytes[4..6],0); // dirlen * amount
for (int i = 6, k = 0; i < dirend; i += 16, k++)
{
if (byIndex != null && !Array.Exists(byIndex, el => el >= 0 && el == k)) continue;
var direntry = bytes[i..(i + 16)]; // raw icon resource
/* No advantages over regular byte indexing
GCHandle enthandle = GCHandle.Alloc(direntry, GCHandleType.Pinned);
var entry = Marshal.PtrToStructure<ICONDIRENTRY>(enthandle.AddrOfPinnedObject());
enthandle.Free();*/
int w = direntry[0] == 0 ? 256 : direntry[0];
int h = direntry[1] == 0 ? 256 : direntry[1];
if (bySize != null && !Array.Exists(bySize, el => el >= 0 && el == w)) continue;
var l = (int)BitConverter.ToUInt32(direntry[8..12],0);
var bpp = BitConverter.ToUInt16(direntry[6..8],0);
var col = direntry[2] == 0 && bpp == 8 ? 256 : direntry[2];
var dataoffset = (int)BitConverter.ToUInt32(direntry[12..16],0);
var imgbytes = bytes[dataoffset .. (dataoffset + l)];
var iconHandle = CreateIconFromResourceEx(imgbytes,l,true,0x00030000,w,h,0);
var info = new IconReader(0,"", k,(k+1).ToString(), w,h,bpp,col,l, iconHandle,rawData);
icons.Add(info);
}
return icons;
} // ImportIco
private static bool ExportImages(List<IconReader> iconInfo, string outputPath, string type, int[]? bySize=null, int[]? byIndex=null, Color? background=null)
{
string[] imgType = {"png","jpeg","tiff","gif","bmp","emf","exif","webp","heif"}; // available formats
type = type.ToLower();
if (type == "jpg") type = "jpeg";
else if (type == "tif") type = "tiff";
if (!Array.Exists(imgType, el => el == type)) return false; // input validation
imgType = new[] {"jpeg","bmp","gif"}; // no alpha channel list
if (background is null) background = Color.Transparent;
if (Array.Exists(imgType, el => el == type) && background == Color.Transparent) background = Color.White;
foreach (var icon in iconInfo)
{
if (icon.IconData is null || icon.IconData.GetType() != typeof(IconReader))
icon.IconData = Icon.FromHandle(icon.Handle);
if (icon.IconData is null) continue; // test for image data
var ii = icon.GroupIndex;
// cleanup
if (byIndex != null && byIndex.Length != 0 && !Array.Exists(byIndex, el => el == ii)) continue;
if (bySize != null && bySize.Length != 0 && !Array.Exists(bySize, el => el == icon.Width)) continue;
// resolve file name
var icofile = outputPath.Replace("%index%",string.Format("{0}%sz%%color%",ii));
icofile = Regex.Replace(icofile,@"\.ico$",@"."+type);
if (icon.Colors == 0)
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}",icon.Width));
else
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}-{1}",icon.Width,icon.Colors));
Icon oIcon = (Icon)icon.IconData;
Bitmap bitmap = oIcon.ToBitmap();
// Replace black (primarily) or any background
if (Array.Exists(imgType, el => el == type) && background != Color.Transparent)
{
int w = icon.Width;
int h = icon.Height;
var destRect = new Rectangle(0,0,w,h);
Bitmap destImage = new Bitmap(w,h); //,PixelFormat.Format32bppPArgb
destImage.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);
/////destImage.MakeTransparent();
var graphics = Graphics.FromImage(destImage);
graphics.Clear((Color)background);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
var wrapMode = new ImageAttributes();
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
graphics.DrawImage(bitmap, destRect, 0, 0, w, h, GraphicsUnit.Pixel, wrapMode);
wrapMode.Dispose();
graphics.Dispose();
bitmap = destImage;
}
var ifmt = (ImageFormat)typeof(ImageFormat)
.GetProperty(type, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase)
.GetValue(type,null);
try {
bitmap.Save(icofile,ifmt);
} finally {
bitmap.Dispose();
}
}
iconInfo.ForEach(i => i.Dispose()); // cleanup
return true;
} // ExportImages
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 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) return; // idType: 1 - ICO, 2 - CUR
var entrySize = Marshal.SizeOf<GRPICONDIRENTRY>();
ptr += 2;
var count = Marshal.ReadInt16(ptr);
ptr += 2;
for (var i = 0; i < count; i++)
{
// restore ICONDIR
var entry = Marshal.PtrToStructure<GRPICONDIRENTRY>(ptr);
ptr += entrySize;
entries[entry.nId] = entry;
// GroupId, is it a string or an id?
groupIndices[entry.nId] = index;
if (name.ToInt64() > ushort.MaxValue)
{
groupIds[entry.nId] = Marshal.PtrToStringAuto(name);
}
else
{
groupIds[entry.nId] = "#" + name.ToInt32();
}
}
} // ExtractIconGroupEntries
private static byte[]? GetImageBytes(IntPtr module, IntPtr name, IntPtr type)
{
// https://devblogs.microsoft.com/oldnewthing/?p=7083
var hres = FindResource(module, name, type);
if (hres == IntPtr.Zero) return null;
var size = SizeofResource(module, hres);
if (size == 0) return null;
var res = LoadResource(module, hres);
if (res == IntPtr.Zero) return null;
var ptr = LockResource(res);
if (ptr == IntPtr.Zero) return null;
byte[] imgbytes = new byte[size];
Marshal.Copy(ptr, imgbytes, 0, size);
return imgbytes;
} // GetImageBytes
private static string? ResolveInputURI(string iconFilePath) // experimental
{
if (string.IsNullOrEmpty(iconFilePath)) return null;
if (!Regex.IsMatch(iconFilePath,@"^https?://",RegexOptions.IgnoreCase)) return iconFilePath;
// TODO: resolve aliases
var target = Path.GetTempPath() + Path.GetFileName(iconFilePath);
try
{
using (var webclient = new HttpClient())
{
webclient.Timeout = TimeSpan.FromMilliseconds(4500); // can parameterize
using (var response = webclient.GetStreamAsync(iconFilePath))
{
using (var fs = new FileStream(target, FileMode.OpenOrCreate))
{
response.Result.CopyTo(fs);
//if (!File.Exists(target)) target = null;
}
}
}
} catch {target = null;} //if (File.Exists(target)) File.Delete(target);
return target;
} // ResolveInputURI
// Generic helpers
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 IntPtr CreateIconFromResourceEx(byte[] 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;
}
/*[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct ICONDIRENTRY
{
public byte bWidth;
public byte bHeight;
public byte bColorCount;
public byte bReserved;
public short wPlanes;
public short wBitCount;
public int dwBytesInRes;
public int dwImageOffset;
}*/
} // IconReader
# SYNOPSIS : Icon Reader for PowerShell
# DESCRIPTION : A utility class that extracts the list of all icons handles (all sizes, all color counts) for a binary file like DLL, EXE, or ICO
# UTILITIES : GetTotal, ImportIcons, ExportIcons, Dispose
# FEATURES : Export: by size and index, multisize icons, split icon groups by size
# VERSION : 4.0
# LASTUPDATE : 2026-Sep-5
# LINK : https://en.wikipedia.org/wiki/ICO_(file_format)
# NOTES : Only builtin sizes are exported; GIF,BMP,JPEG always have the background in black
# BACKLOG :
$IconReader = @"
// .NET Framework Edition
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Linq;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Reflection;
using System.Net;
using System.Net.Http;
public sealed class IconReader : IDisposable
{
public IntPtr Handle { get; private set; } // primary ID for icon
public int Index { get; private set; } // global index, inc multi sizing
public int GroupIndex { get; private set; } // EXE,DLL - group (multi-size icon) index; ICO - 0
public string Id { get; private set; } // primarily for EXE,DLL
public string GroupId { get; private set; } // primarily for EXE,DLL
public int Width { get; private set; }
public int Height { get; private set; }
public int Bpp { get; private set; }
public int Colors { get; private set; }
public int ImgLength { get; private set; }
public object IconData { get; set; } // icon image/object storage; by default contains image bytes
private IconReader(int groupIndex, string groupId, int index, string id, int width, int height, int bpp, int colors, int len, IntPtr handle, object imgData=null, bool rawData=true)
{
GroupIndex = groupIndex;
GroupId = groupId;
Index = index;
Id = id;
Width = width; // width == 0 ? 256 : width;
Height = height; // height == 0 ? 256 : height;
Bpp = bpp;
Colors = colors;
ImgLength = len;
Handle = handle;
IconData = rawData ? imgData : Icon.FromHandle(handle);
}
// Utilities
//public override string ToString() => Index.ToString();
public void Dispose()
{
if (IconData != null && IconData.GetType() == typeof(Icon)) ((Icon)IconData).Dispose();
DestroyIcon(Handle);
}
// DLL,EXE: the total doesn't take into account multi sizing
public static int GetTotal(string iconFilePath)
{
iconFilePath = ResolveInputURI(iconFilePath);
if (string.IsNullOrEmpty(iconFilePath)) return 0; // or ArgumentNullException?
if (Path.GetExtension(iconFilePath).ToLower() == ".ico")
{
FileStream stream;
try {stream = File.Open(iconFilePath, FileMode.Open);} catch {return 0;}
var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, false);
byte[] bytes = reader.ReadBytes(6); // 4+2
stream.Close();
stream.Dispose();
reader.Close();
reader.Dispose();
if (bytes.Length < 6)
throw new ArgumentException("'"+iconFilePath+"' is not a valid ICO file.");
var signature = new byte[4];
var total = new byte[2];
Array.Copy(bytes,0, signature,0,4);
Array.Copy(bytes,4, total,0,2);
if (BitConverter.ToInt32(signature,0) != 0x10000) // Basic validation
throw new ArgumentException("'"+iconFilePath+"' is not a valid ICO file.");
return BitConverter.ToInt16(total,0);
}
else // dll or exe
{ // version 2, uniform; version 1 is simpler and faster
int icount = 0;
var handle = LoadLibraryEx(iconFilePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (handle != IntPtr.Zero)
{
EnumResourceNames(handle, new IntPtr(RT_GROUP_ICON), (m, t, n, lp) => { icount++; return true; }, IntPtr.Zero);
FreeLibrary(handle);
}
return icount;
}
} // GetTotal
public static List<IconReader> ImportIcons(string iconFilePath, int[] byIndex=null, int[] bySize=null, bool rawData=true)
{
iconFilePath = ResolveInputURI(iconFilePath);
if (string.IsNullOrEmpty(iconFilePath))
throw new ArgumentNullException("Filename is empty.");
// Validate input filters
if (bySize != null && bySize.Length > 0)
{
bySize = bySize.Where(n => n > 15 && n < 257).ToArray();
if (bySize.Length == 0) bySize = new[]{-1000}; // stop value
}
else bySize = null;
if (byIndex != null && byIndex.Length > 0)
{
byIndex = byIndex.Where(n => n > -1).ToArray();
if (byIndex.Length == 0) byIndex = new[]{-1000}; // stop value
}
else byIndex = null;
if (Path.GetExtension(iconFilePath).ToLower() == ".ico")
return ImportIco(iconFilePath, byIndex, bySize, rawData);
var icons = new List<IconReader>();
var handle = LoadLibraryEx(iconFilePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
if (handle != IntPtr.Zero)
{
try
{
icons = ImportIcons(handle, byIndex, bySize, rawData);
}
finally
{
FreeLibrary(handle);
}
}
return icons;
} // ImportIcons
public static bool ExportIcons(string iconFilePath=null, List<IconReader> iconInfo=null, string outputPath=null, int[] bySize=null, int[] byIndex=null, bool split=false, string type="ico", Color? background=null)
{
// Resolve parameter sets
iconFilePath = ResolveInputURI(iconFilePath);
var ii = iconInfo == null || iconInfo.Count == 0;
if (string.IsNullOrEmpty(iconFilePath) && ii)
throw new ArgumentNullException("Filename and icon collection are empty.");
if (string.IsNullOrEmpty(iconFilePath) && string.IsNullOrEmpty(outputPath)) return false;
try { iconFilePath = Path.GetFullPath(iconFilePath); } catch { return false; }
var src = Path.GetExtension(iconFilePath).ToLower().Substring(1); // exe,dll,ico
if (src == "ico" && !split && (bySize == null || bySize.Length == 0)) split = true;
if (string.IsNullOrEmpty(type)) type = "ico";
//var raw = type.ToLower() == "ico";
if (ii) iconInfo = ImportIcons(iconFilePath,byIndex,bySize); //,raw
if (iconInfo == null || iconInfo.Count == 0) return false;
// Create output file name template
var fn = Path.GetFileNameWithoutExtension(iconFilePath);
if (string.IsNullOrEmpty(outputPath))
{
var sfx = src == "ico" ? "-extract-%index%.ico" : "-%index%.ico";
outputPath = string.Format(@"{0}\{1}{2}", Path.GetDirectoryName(iconFilePath),fn,sfx);
}
else
{
try { outputPath = Path.GetFullPath(outputPath); } catch { return false; }
outputPath = string.Format(@"{0}\{1}-%index%.ico", outputPath,fn);
}
if (Regex.IsMatch(outputPath,@"c:\\(windows.+|program.+|[^\\]+)$",RegexOptions.IgnoreCase))
Console.WriteLine("WARNING: System location specified as output. Export may fail. Make sure that you have write access.");
// Validate input filters
if (bySize != null && bySize.Length > 0)
bySize = bySize.Where(n => n > 15 && n < 257).OrderByDescending(n => n).ToArray();
else bySize = null;
if (byIndex != null && byIndex.Length > 0)
byIndex = byIndex.Where(n => n > -1).ToArray();
else byIndex = null;
// Other formats
if (type.ToLower() != "ico") return ExportImages(iconInfo,outputPath,type,bySize,byIndex,background);
// Group by GroupIndex first
var igroups = iconInfo.Where(i => i.IconData != null).GroupBy(i => i.GroupIndex).OrderBy(i => i.Key);
foreach (var gr in igroups)
{
if (byIndex != null && !Array.Exists(byIndex, el => el == gr.Key)) continue; // filter
var iconGroup = gr.ToList(); // select a group
var series = split ? iconGroup.Count : 1; // mode selector
for (var s=0; s < series; s++)
{
// select the next chunk to work with
var icoWork = split ? new List<IconReader>{iconGroup[s]} : iconGroup;
// resolve output file name
// PROBLEM: size may be wrong because of filtering (below); TEST!!!
var icofile = outputPath.Replace("%index%",string.Format("{0}%sz%%color%",gr.Key));
if (split) {
if (icoWork[0].Colors == 0)
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}",icoWork[0].Width));
else
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}-{1}",icoWork[0].Width,icoWork[0].Colors));
}
else
icofile = icofile.Replace("%sz%%color%","");
FileStream outputStream;
try {outputStream = new FileStream(icofile, FileMode.OpenOrCreate);}
catch {continue;}
if (outputStream == null) continue;
var iconWriter = new BinaryWriter(outputStream);
if (iconWriter == null)
{
outputStream.Close();
outputStream.Dispose();
continue;
}
bool inew = false; // no icons to export
// 0-1 reserved, 0
iconWriter.Write((byte)0);
iconWriter.Write((byte)0);
// 2-3 image type, 1 = icon, 2 = cursor
iconWriter.Write((short)1);
// 4-5 number of images
iconWriter.Write((short)icoWork.Count);
//var natives = icoWork.Select(n => n.Width).Distinct(); // builtin sizes
int offset = 6 + (16 * icoWork.Count);
short icocount = 0;
foreach (var icon in icoWork)
{
if (bySize != null && !Array.Exists(bySize, el => el == icon.Width)) continue; // filter
var w = icon.Width == 256 ? 0 : icon.Width;
var h = icon.Height == 256 ? 0 : icon.Height;
// image entry
// 0 image width
iconWriter.Write((byte)w);
// 1 image height
iconWriter.Write((byte)h);
// 2 number of colors
iconWriter.Write((byte)0);
// 3 reserved
iconWriter.Write((byte)0);
// 4-5 color planes
iconWriter.Write((short)0);
// 6-7 bits per pixel
iconWriter.Write((short)32);
// 8-11 size of image data
iconWriter.Write((int)icon.ImgLength);
// 12-15 offset of image data
iconWriter.Write((int)offset);
offset += icon.ImgLength;
inew = true;
icocount++;
}
foreach (var icon in icoWork)
{
// image data
// png data must contain the whole png data file
if (bySize == null || Array.Exists(bySize, el => el == icon.Width))
iconWriter.Write(icon.IconData as byte[]);
}
if (inew && icocount != icoWork.Count)
{
// if a filter happend update icon amount value with actual
iconWriter.Write(BitConverter.GetBytes(icocount), 4,2); // TEST!!!!!
}
if (inew) iconWriter.Flush(); // flush buffer to a file
// Cleanup
iconWriter.Close();
iconWriter.Dispose();
outputStream.Close();
outputStream.Dispose();
if (!inew) File.Delete(icofile);
else Console.WriteLine(string.Format("Icon '{0}' successfully exported.",icofile));
} // split mode
} // groups
iconInfo.ForEach(i => i.Dispose()); // cleanup
return true;
} // ExportIcons
// Icon helpers
private static List<IconReader> ImportIcons(IntPtr handle, int[] byIndex=null, int[] bySize=null, bool rawData=true)
{
var icons = new List<IconReader>();
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 (byIndex != null && !Array.Exists(byIndex, el => el > -1 && el == groupIndex)) //el.GetType() == typeof(int) &&
{
groupIndex++;
return true;
}
/*
// experimental; not very useful option;
// will require to replace all corresponding "byIndex" parameters with dynamic[] type and type checking
string name;
if (n.ToInt64() > ushort.MaxValue)
name = Marshal.PtrToStringAuto(n); // alfabet string
else
name = n.ToInt32().ToString(CultureInfo.InvariantCulture); // decimal string
if (byIndex != null && !Array.Exists(byIndex, el => el.GetType() == typeof(string) && string.Equals(el, name, StringComparison.OrdinalIgnoreCase)))
{
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) =>
{
GRPICONDIRENTRY x;
if (!entries.TryGetValue((ushort)n.ToInt32(), out x)) return true;
var imgbytes = GetImageBytes(handle, n, t);
if (imgbytes == null) return true;
var iconHandle = CreateIconFromResourceEx(imgbytes, imgbytes.Length, true, 0x00030000,0,0,0);
if (iconHandle != IntPtr.Zero)
{
var id = (ushort)n.ToInt32();
//var ent = entries[id]; // ICONDIR
var w = entries[id].bWidth == 0 ? 256 : entries[id].bWidth;
var h = entries[id].bHeight == 0 ? 256 : entries[id].bHeight;
var l = entries[id].dwBytesInRes;
var bpp = entries[id].wBitCount;
int colors = entries[id].bColorCount;
if (colors == 0 && bpp == 8) colors = 256;
if (bySize == null || Array.Exists(bySize, el => el == w))
{
var info = new IconReader(groupIndices[id], groupIds[id], n.ToInt32() - 1, n.ToString(), w,h,bpp,colors,l, iconHandle,imgbytes,rawData);
icons.Add(info);
}
}
return true;
}, IntPtr.Zero);
}
return icons;
} // ImportIcons
private static List<IconReader> ImportIco(string iconFilePath, int[] byIndex, int[] bySize, bool rawData)
{
byte[] bytes;
try {bytes = File.ReadAllBytes(iconFilePath);} catch {
throw new ArgumentException("Unable to read '"+iconFilePath+"' file.");
}
if (bytes.Length < 6) // corrupted
throw new ArgumentException("'"+iconFilePath+"' is not a valid ICO file.");
var signature = new byte[4];
Array.Copy(bytes,0, signature,0,4);
if (BitConverter.ToInt32(signature,0) != 0x10000) // basic validation
throw new ArgumentException("'"+iconFilePath+"' is not a valid ICO file.");
var icons = new List<IconReader>();
var direntry = new byte[16];
var total = new byte[2];
Array.Copy(bytes,4, total,0,2);
int dirend = 16 * BitConverter.ToInt16(total,0); // dirlen * amount
ICONDIRENTRY entry;
for (int i = 6, k = 0; i < dirend; i += 16, k++)
{
if (byIndex != null && !Array.Exists(byIndex, el => el >= 0 && el == k)) continue;
Array.Copy(bytes,i, direntry,0,16);
GCHandle enthandle = GCHandle.Alloc(direntry, GCHandleType.Pinned);
try { // get icon resource structure
entry = Marshal.PtrToStructure<ICONDIRENTRY>(enthandle.AddrOfPinnedObject());
} catch {continue;}
finally {enthandle.Free();}
var w = entry.bWidth == 0 ? 256 : entry.bWidth;
var h = entry.bHeight == 0 ? 256 : entry.bHeight;
if (bySize != null && !Array.Exists(bySize, el => el >= 0 && el == w)) continue;
var bpp = entry.wBitCount;
var col = entry.bColorCount == 0 && bpp == 8 ? 256 : entry.bColorCount;
var l = entry.dwBytesInRes;
var imgbytes = new byte[l];
Array.Copy(bytes,entry.dwImageOffset, imgbytes,0,l);
//overhead - var imgbytes = bytes.Skip(entry.dwImageOffset-1).Take(l).ToArray();
var iconHandle = CreateIconFromResourceEx(imgbytes,l,true,0x00030000,w,h,0);
var info = new IconReader(0,"",k,(k+1).ToString(), w,h,bpp,col,l, iconHandle,imgbytes,rawData);
icons.Add(info);
}
return icons;
} // ImportIco
private static bool ExportImages(List<IconReader> iconInfo, string outputPath, string type, int[] bySize=null, int[] byIndex=null, Color? background=null)
{
string[] imgType = {"png","jpeg","tiff","gif","bmp","emf","exif"}; // available formats
type = type.ToLower();
if (type == "jpg") type = "jpeg";
else if (type == "tif") type = "tiff";
if (!Array.Exists(imgType, el => el == type)) return false; // input validation
imgType = new[] {"jpeg","bmp","gif"}; // no alpha channel list
if (background == null) background = Color.Transparent;
if (Array.Exists(imgType, el => el == type) && background == Color.Transparent) background = Color.White;
foreach (var icon in iconInfo)
{
if (icon.IconData == null || icon.IconData.GetType() != typeof(IconReader))
icon.IconData = Icon.FromHandle(icon.Handle);
if (icon.IconData == null) continue; // test for image data
var ii = icon.GroupIndex;
// filters
if (byIndex != null && byIndex.Length != 0 && !Array.Exists(byIndex, el => el == ii)) continue;
if (bySize != null && bySize.Length != 0 && !Array.Exists(bySize, el => el == icon.Width)) continue;
// resolve file name
var icofile = outputPath.Replace("%index%",string.Format("{0}%sz%%color%",ii));
icofile = Regex.Replace(icofile,@"\.ico$",@"."+type);
if (icon.Colors == 0)
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}",icon.Width));
else
icofile = icofile.Replace("%sz%%color%",string.Format("-{0}-{1}",icon.Width,icon.Colors));
Icon oIcon = (Icon)icon.IconData;
Bitmap bitmap = oIcon.ToBitmap();
// Replace black (primarily) or any background
if (Array.Exists(imgType, el => el == type) && background != Color.Transparent)
{
int w = icon.Width;
int h = icon.Height;
var destRect = new Rectangle(0,0,w,h);
Bitmap destImage = new Bitmap(w,h); //,PixelFormat.Format32bppPArgb
destImage.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);
/////destImage.MakeTransparent();
var graphics = Graphics.FromImage(destImage);
graphics.Clear((Color)background);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
var wrapMode = new ImageAttributes();
wrapMode.SetWrapMode(WrapMode.TileFlipXY);
graphics.DrawImage(bitmap, destRect, 0, 0, w, h, GraphicsUnit.Pixel, wrapMode);
wrapMode.Dispose();
graphics.Dispose();
bitmap = destImage;
}
var ifmt = (ImageFormat)typeof(ImageFormat)
.GetProperty(type, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase)
.GetValue(type,null);
try {
bitmap.Save(icofile,ifmt);
} finally {
bitmap.Dispose();
}
}
iconInfo.ForEach(i => i.Dispose()); // cleanup
return true;
} // ExportImages
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 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) return; // idType: 1 - ICO, 2 - CUR
var entrySize = Marshal.SizeOf<GRPICONDIRENTRY>();
ptr += 2;
var count = Marshal.ReadInt16(ptr);
ptr += 2;
for (var i = 0; i < count; i++)
{
// restore ICONDIR
var entry = Marshal.PtrToStructure<GRPICONDIRENTRY>(ptr);
ptr += entrySize;
entries[entry.nId] = entry;
// GroupId, is it a string or an id?
groupIndices[entry.nId] = index;
if (name.ToInt64() > ushort.MaxValue)
{
groupIds[entry.nId] = Marshal.PtrToStringAuto(name);
}
else
{
groupIds[entry.nId] = "#" + name.ToInt32();
}
}
} // ExtractIconGroupEntries
private static byte[] GetImageBytes(IntPtr module, IntPtr name, IntPtr type)
{
// https://devblogs.microsoft.com/oldnewthing/?p=7083
var hres = FindResource(module, name, type);
if (hres == IntPtr.Zero) return null;
var size = SizeofResource(module, hres);
if (size == 0) return null;
var res = LoadResource(module, hres);
if (res == IntPtr.Zero) return null;
var ptr = LockResource(res);
if (ptr == IntPtr.Zero) return null;
byte[] imgbytes = new byte[size];
Marshal.Copy(ptr, imgbytes, 0, size);
return imgbytes;
} // GetImageBytes
private static string ResolveInputURI(string iconFilePath) // experimental
{
if (string.IsNullOrEmpty(iconFilePath)) return null;
if (!Regex.IsMatch(iconFilePath,@"^https?://",RegexOptions.IgnoreCase))
{
// resolve aliases; can add more
switch (iconFilePath.ToLower())
{
case "%shell32": return @"C:\Windows\System32\shell32.dll";
case "%imageres": return @"C:\Windows\System32\imageres.dll";
case "%ddo": return @"C:\Windows\System32\ddores.dll";
}
return iconFilePath;
}
var target = Path.GetTempPath() + Path.GetFileName(iconFilePath);
try
{
using (var webclient = new HttpClient())
{
webclient.Timeout = TimeSpan.FromMilliseconds(4500); // can parameterize
using (var response = webclient.GetStreamAsync(iconFilePath))
{
using (var fs = new FileStream(target, FileMode.OpenOrCreate))
{
response.Result.CopyTo(fs);
//if (!File.Exists(target)) target = null;
}
}
}
} catch {target = null;} //if (File.Exists(target)) File.Delete(target);
return target;
} // ResolveInputURI
// Generic helpers
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 IntPtr CreateIconFromResourceEx(byte[] 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;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct ICONDIRENTRY
{
public byte bWidth;
public byte bHeight;
public byte bColorCount;
public byte bReserved;
public short wPlanes;
public short wBitCount;
public int dwBytesInRes;
public int dwImageOffset;
}
} // END class IconReader
"@ # IconReader
# Compilation
$loadlib = if ($PSVersionTable.PSEdition -eq 'Core') { 'System','System.Management.Automation','System.Collections','System.Linq','System.Text.RegularExpressions','System.Drawing.Common','System.Drawing.Primitives','System.Console','System.Net.Http'
} else {'System.Drawing','System.Net.Http'}
Add-Type -TypeDefinition $IconReader -ReferencedAssemblies $loadlib -IgnoreWarnings
# Powershell session references
Add-Type -AssemblyName System.Drawing
'''
SYNOPSIS : Icon Reader for Win32
DESCRIPTION : A utility class that extracts the list of all icons handles (all sizes, all color counts) for a binary file like DLL, EXE, or ICO.
UTILITIES : GetTotal, ImportIcons, ExportIcons
FEATURES : Export: by size and index, multisize icons, split icon groups by size, various formats including ICO,PNG,JPG,WEBP,TIF; PIL for none-ICO images
VERSION : 2.0
LASTUPDATE : 2026-Sep-6
STATUS : Demo as Python learning track; tested with 3.14.7
LINK : https://en.wikipedia.org/wiki/ICO_(file_format)
NOTES : Only builtin sizes can be exported, th's no native way to resize
BACKLOG :
ISSUES : PIL sucks: bmp, no transparency, no antialising; gif, no antialising
'''
import pathlib,struct,os,re
import ctypes
from ctypes import wintypes
try:
from PIL import Image # external package :-(
pilpkg = True
except ImportError: pilpkg = False
USHORT_MAX = ctypes.c_uint16(-1).value # 65535
class IconReader:
@property
def Handle(self): return self.__handle # for .NET
@property
def Index(self): return self.__index
@property
def GroupIndex(self): return self.__groupIndex
@property
def Id(self): return self.__id
@property
def GroupId(self): return self.__groupId
@property
def Width(self): return self.__width
@property
def Height(self): return self.__height
@property
def Bpp(self): return self.__bpp
@property
def Colors(self): return self.__colors
@property
def ImgLength(self): return self.__imgLength
def __init__(self,groupIndex,groupId,index,iid,width,height,bpp,colors,ilen,handle,imgdata=None):
self.__handle = handle # pri dyn ID for icon in .NET; useless in Python
self.__index = index # global index, inc multi sizing
self.__groupIndex = groupIndex # EXE,DLL: icon group (multi-size icon) index; ICO: always 0
self.__id = iid # for EXE,DLL
self.__groupId = groupId # for EXE,DLL
self.__width = width # 256 if width == 0 else width
self.__height = height # 256 if height == 0 else height
self.__bpp = bpp
self.__colors = colors
self.__imgLength = ilen
self.Icon = imgdata # icon image/object storage
# Utilities
@staticmethod
def GetTotal(iconFilePath) -> int:
if os.name != 'nt': return 0
iconFilePath = IconReader.__resolve_inputuri(iconFilePath)
if iconFilePath == '' or iconFilePath is None: return 0
#if pathlib.Path(iconFilePath).exists() == False: return 0
# FileNotFoundError: No such file or directory
class_ = IconReader # work around: surrogate "self"
icount = 0
if pathlib.Path(iconFilePath).suffix.lower() == '.ico':
try:
with open(iconFilePath, 'rb') as f:
data = f.read(6)
if len(data) < 6: return 0
isignature,icount = struct.unpack(class_.__ICOHEADER, data[0:6])
if isignature != 0x10000: return 0
except:
print('Error reading file.')
return None
else:
handle = class_.__load_library(class_,iconFilePath)
if handle != 0 and handle is not None:
# enumerator callback
def lpEnumFunc(m,t,n,lp): nonlocal icount; icount += 1; return True
class_.__enum_resource_names(class_,handle,class_.__RT_GROUP_ICON,lpEnumFunc)
class_.__kernel32.FreeLibrary(ctypes.c_ulong(handle))
return icount
# END GetTotal
@staticmethod
def ImportIcons(iconFilePath, byIndex=[], bySize=[]):
if os.name != 'nt': return None
class_ = IconReader # work around: surrogate "self"
iconFilePath = class_.__resolve_inputuri(iconFilePath)
if iconFilePath == '' or iconFilePath is None: return None
#if pathlib.Path(iconFilePath).exists() == False: return None
if byIndex is not None and len(byIndex) > 0:
byIndex = set([n for n in byIndex if type(n).__name__ == 'int' and n > -1]) # or type(n).__name__ == 'str'
if len(byIndex) == 0: byIndex = [-1000] # stop value
if bySize is not None and len(bySize) > 0:
bySize = set([n for n in bySize if type(n).__name__ == 'int' and n > -1])
if len(bySize) == 0: bySize = [-1000] # stop value
if pathlib.Path(iconFilePath).suffix.lower() == '.ico':
return class_.__import_ico(class_, iconFilePath, byIndex, bySize)
icons = [] # output
handle = class_.__load_library(class_,iconFilePath)
if handle != 0 and handle is not None:
#icons = class_.__extract_icons(class_, handle, byIndex, bySize)
#class_.__kernel32.FreeLibrary(ctypes.c_ulong(handle))
try: icons = class_.__extract_icons(class_, handle, byIndex, bySize)
finally: class_.__kernel32.FreeLibrary(ctypes.c_ulong(handle))
return icons
# END ImportIcons
@staticmethod
def ExportIcons(iconFilePath, iconInfo=None, outputPath=None, byIndex=[], bySize=[], split=False, itype='ico', background='#ffffff'):
if os.name != 'nt': return False
if iconInfo is not None and type(iconInfo).__name__ != 'IconReader': return False
class_ = IconReader
iconFilePath = class_.__resolve_inputuri(iconFilePath)
if iconInfo is None and (iconFilePath == '' or iconFilePath is None): return False
if (outputPath == '' or outputPath is None) and (iconFilePath == '' or iconFilePath is None): return False
src = str(pathlib.Path(iconFilePath).resolve().suffix)[1:].lower() # exe,dll,ico
if src == 'ico' and not split and (bySize is None or len(bySize) == 0): split = True
if iconInfo is None or len(iconInfo) == 0: iconInfo = class_.ImportIcons(iconFilePath,byIndex,bySize)
if iconInfo is None or len(iconInfo) == 0: return False
# Resolve output file name and create a template
if outputPath == '' or outputPath is None:
sfx = r'-extract-%index%.ico' if src == 'ico' else r'-%index%.ico'
outputPath = str(pathlib.Path(iconFilePath).resolve().with_suffix('')) + sfx
else:
fn = pathlib.Path(iconFilePath).resolve().stem
outputPath = r'{0}\{1}-%index%.ico'.format(pathlib.Path(outputPath).resolve(), fn)
if re.match(r'c:\\(windows.+|program.+|[^\\]+)$',outputPath,re.I) is not None:
print('WARNING: System location specified as output. Export may fail. Make sure that you have write access.')
# Validate input filters
if byIndex is not None and len(byIndex) > 0:
byIndex = sorted(
list(set([n for n in byIndex if type(n).__name__ == 'int' and n > -1])), reverse=True
)
if bySize is not None and len(bySize) > 0:
bySize = sorted(
list(set([n for n in bySize if type(n).__name__ == 'int' and n > 15 and n < 257])), reverse=True
)
# experimental; png,jpg,...
if itype.lower() != 'ico':
return class_.__export_image(class_, iconInfo, outputPath, itype, byIndex, bySize,background)
# Group by GroupIndex first; extract indices
gi_list = set([n.GroupIndex for n in iconInfo if n.Icon is not None])
for i in gi_list: # foreach index of icon resource as a group of sizes
if len(byIndex) != 0 and i not in byIndex: continue # filter
iconGroup = [n for n in iconInfo if i == n.GroupIndex] # select a group by index
series = len(iconGroup) if split else 1 # mode selector
for s in range(series):
icoWork = [iconGroup[s]] if split else iconGroup # select a chunk to work with
# resolve output file name
# PROBLEM: size may be wrong because of filtering (below); TEST!!!
icofile = outputPath.replace('%index%',f'{str(i)}%sz%%color%')
if split:
if icoWork[0].Colors == 0:
icofile = icofile.replace('%sz%%color%',f'-{icoWork[0].Width}')
else:
icofile = icofile.replace('%sz%%color%',f'-{icoWork[0].Width}-{icoWork[0].Colors}')
else:
icofile = icofile.replace('%sz%%color%','')
try: fhandle = open(icofile,'wb')
except:
print(f"ERROR: Failed to create file '{icofile}'. Check filepath, write access, file lock and try again.")
#if pathlib.Path(icofile).exists(): os.remove(icofile)
continue
inew = False # no icons to export
memstream = bytearray() # memory buffer
memstream.extend(struct.pack('@BBHH',0,0,1,len(icoWork))) # ICO file header
offset = 6 + (16 * len(icoWork))
# foreach size in icon (group of sizes)
#natives = set([n.Width for n in icoWork]) # builtin sizes
for icocount,icon in enumerate(icoWork):
#if icon.Icon is None: continue # already filtered in gi_list
if len(bySize) != 0 and icon.Width not in bySize: continue # filter
w = 0 if icon.Width == 256 else icon.Width
h = 0 if icon.Height == 256 else icon.Height
entry = struct.pack('@BBBBHHII',w,h,0,0,0,32,icon.ImgLength,offset)
memstream.extend(entry)
offset += icon.ImgLength
inew = True
for icon in icoWork:
#if icon.Icon is not None: # already filtered in gi_list
if len(bySize) == 0 or icon.Width in bySize: memstream.extend(icon.Icon)
icocount += 1 # because it starts with 0
if inew and icocount != len(icoWork):
# if a filter happend update icon amount value with actual
memstream[4],memstream[5] = struct.pack('@H',icocount)
if inew: fhandle.write(memstream) # flush buffer to a file
# Cleanup
fhandle.close()
memstream.clear()
icoWork.clear()
if not inew: os.remove(icofile)
else: print(f"Icon '{icofile}' successfully exported.")
iconGroup.clear()
if len(gi_list) > 0: return True
else: return False
# END ExportIcons
# Icon helpers
def __extract_icons(self, handle, byIndex=[], bySize=[]):
icons = [] # output
entries = {} # directory entry table
groupIndices = {} # for DLL,EXE
groupIds = {} # for DLL,EXE
groupIndex = 0 # batch index
def lpEnumFunc(m,t,n,lp): # enumerator callback
nonlocal groupIndex, entries, groupIndices, groupIds
isIndex = byIndex is not None and len(byIndex) != 0
#iNum = [n for n in byIndex if type(n).__name__ == 'int'] if isIndex else []
#if isIndex and len(iNum) != 0 and groupIndex not in iNum:
if isIndex and groupIndex not in byIndex:
groupIndex += 1
return True
#experimental; not very useful option
'''if int(n) > USHORT_MAX:
name = ctypes.cast(n, ctypes.c_wchar_p).value # alfabet string
else:
name = str(n) # decimal string
iStr = [n for n in byIndex if type(n).__name__ == 'str'] if isIndex else []
if isIndex and len(iStr) != 0 and name not in iStr:
groupIndex += 1
return True'''
try:
self.__extract_icon_groups(self,handle, n, t, groupIndex, entries, groupIndices, groupIds)
groupIndex += 1
except: pass # do nothing
return True
if self.__enum_resource_names(self,handle,self.__RT_GROUP_ICON,lpEnumFunc):
def lpEnumFunc(m,t,n,lp): # enumerator callback
nonlocal icons, groupIndices, groupIds
if len(entries) == 0 or int(n) not in entries: return True
imgbytes = self.__get_imgbytes(self,handle,n,t) # th's no ImageOffset in RESDIR
if len(imgbytes) > 0:
iconHandle = self.__user32.CreateIconFromResourceEx(imgbytes, len(imgbytes), True, 0x00030000, 0, 0, 0)
if iconHandle != 0 and iconHandle is not None:
idx = int(n)
w,h = entries[idx]['bWidth'],entries[idx]['bHeight']
l = entries[idx]['dwBytesInRes']
bpp = entries[idx]['wBitCount']
colors = entries[idx]['bColorCount']
if colors == 0 and bpp == 8: colors = 256 # normalize
if 0 == w: w = 256 # normalize
if 0 == h: h = 256 # normalize
if bySize is None or len(bySize) == 0 or w in bySize:
info = IconReader(groupIndices[idx], groupIds[idx], int(n) - 1, str(n), w,h,bpp,colors,l, iconHandle,imgbytes)
icons.append(info)
return True
self.__enum_resource_names(self,handle, self.__RT_ICON, lpEnumFunc)
return icons
# END __extract_icons
# experimental; requires PIL package, sucks
def __export_image(self, iconInfo, outputPath, itype, byIndex, bySize, background):
if not pilpkg:
print('ERROR: PIL package not installed. Installation command is "python -m pip install pillow".')
return False
import io
# normalize itype and background
itype = itype.lower() # bmp,gif are unreliable
if itype not in ['png','jpg','jpeg','tif','tiff','webp','bmp','gif']: return False
if itype == 'jpg': itype = 'jpeg'
elif itype == 'tif': itype = 'tiff'
if background is None or background == '': background = '#ffffff'
bmphdr = struct.pack('@BBHH',0,0,1,1) # image header
for icon in iconInfo:
if icon.Icon == None or len(icon.Icon) == 0: continue # test for image data
ii = icon.GroupIndex
if len(byIndex) != 0 and ii not in byIndex: continue # filter
# for resizing; experimental
#iconGroup = [n for n in iconInfo if ii == n['GroupIndex']] # select a group by index
#natives = set([n['Width'] for n in iconGroup]) # builtin sizes
if len(bySize) != 0 and icon.Width not in bySize: continue # filter
# resolve file name
icofile = outputPath.replace('%index%',f'{str(ii)}%sz%%color%')
icofile = re.sub(r'\.ico$',f'.{itype}',icofile)
if icon.Colors == 0:
icofile = icofile.replace('%sz%%color%',f'-{icon.Width}')
else:
icofile = icofile.replace('%sz%%color%',f'-{icon.Width}-{icon.Colors}')
w = 0 if icon.Width == 256 else icon.Width
h = 0 if icon.Height == 256 else icon.Height
direntry = struct.pack('@BBBBHHII', w, h, 0, 0, 0, 32, icon.ImgLength, 22)
inMemoryICO = io.BytesIO(bmphdr + direntry + icon.Icon)
img = Image.open(inMemoryICO)
if itype in ['jpeg','bmp']:
bg = Image.new('RGBA',(icon.Width,icon.Height),background)
if itype == 'jpeg': img = Image.alpha_composite(bg,img).convert('RGB')
else:
bg.putalpha(255) # adds(255)/removes(0) antialiasing
img = Image.alpha_composite(bg,img) # this adds antialiasing
#elif itype in ['gif']: # TODO add antialiasing; how???
params = {'quality':100}
if itype in ['jpeg','bmp']: params['transparency'] = 0
img.save(icofile, **params)
img.close()
inMemoryICO.close()
'''temporarily disabled
try:
img = Image.open(inMemoryICO)
img.save(icofile,itype)
print(f"Icon '{icofile}' successfully exported.")
#except Exception as e:
# print(e)
except PIL.UnidentifiedImageError:
print(f"ERROR: Failed to open image.")
except OSError:
print(f"ERROR: Failed to create file '{icofile}'. Check filepath, write access, file lock and try again.")
finally:
if img != None: img.close()
inMemoryICO.close()'''
return True
# END __export_image
def __import_ico(self, iconFilePath, byIndex=[], bySize=[]):
try:
with open(iconFilePath, 'rb') as f:
data = f.read()
if len(data) < 6: return None
isignature,icount = struct.unpack(self.__ICOHEADER, data[0:6])
if isignature != 0x10000 or icount == 0: return None
except:
print('Error reading file.')
return None
ENTRYFORMAT = '@BBBBHHII'
icons = [] # output icon storage
#entrysize = struct.calcsize(ENTRYFORMAT) # must be 16
diroffset = range(6,16*icount,16)
isIndex = byIndex is not None and len(byIndex) > 0
isSize = bySize is not None and len(bySize) > 0
for k,i in enumerate(diroffset):
if isIndex and k not in byIndex: continue
w,h,colors,_,_,bpp,l,dataoffset = struct.unpack(ENTRYFORMAT, data[i:i+16])
if 0 == w: w = 256 # normalize
if 0 == h: h = 256 # normalize
if isSize and w not in bySize: continue
imgbytes = data[dataoffset:(dataoffset+l)]
iconHandle = self.__user32.CreateIconFromResourceEx(imgbytes, l, True, 0x0030000, w, h, 0)
info = IconReader(0, "", k, k+1, w,h,bpp,colors,l, iconHandle,imgbytes)
icons.append(info)
return icons
# END __import_ico
def __extract_icon_groups(self, module, name, itype, index, entries, groupIndices, groupIds):
hres = self.__find_resource(self, module, name, itype)
if hres == 0 or hres is None: return
resource = self.__load_resource(self, module, hres)
if resource == 0 or resource is None: return
ptr = self.__lock_resource(self, resource)
if ptr == 0 or ptr is None: return
# GRPICONDIR
ptr += 2 # idReserved
idtype = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_uint16)).contents.value
if idtype != 1: return # idType: 1 - ICO, 2 - CUR
ENTRYFORMAT = '@BBBBhhIH'
entrySize = struct.calcsize(ENTRYFORMAT) # must be 14
ptr += 2
count = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_uint16)).contents.value
ptr += 2
for i in range(count):
# restore ICONDIR
edata = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_byte * entrySize)).contents # raw entry
ptr += entrySize
values = struct.unpack(ENTRYFORMAT, edata)
entry = dict(zip(self.__GRPICONDIRENTRY, values))
##if entry['bWidth'] == 0: entry['bWidth'] = 256 # normalize later
##if entry['bHeight'] == 0: entry['bHeight'] = 256
entries[entry['nId']] = entry
# GroupId, is it a string or an id?
groupIndices[entry['nId']] = index
if int(name) > USHORT_MAX:
groupIds[entry['nId']] = ctypes.cast(name, ctypes.c_wchar_p).value # alfabet string
else:
groupIds[entry['nId']] = f'#{name}' # decimal string
# END __extract_icon_groups
def __get_imgbytes(self, module, name, itype):
# https://devblogs.microsoft.com/oldnewthing/?p=7083
hres = self.__find_resource(self, module, name, itype)
if hres == 0 or hres is None: return 0
size = self.__sizeof_resource(self, module, hres)
if size == 0: return 0
resource = self.__load_resource(self, module, hres)
if resource == 0 or resource is None: return 0
ptr = self.__lock_resource(self, resource)
if ptr == 0 or ptr is None: return 0
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_byte * size)).contents # why "POINTER" is deprecated here???
# END __get_imgbytes
# Generic helpers
def __resolve_inputuri(filepath):
if filepath is None or not filepath or type(filepath).__name__ != 'str': return None
if not filepath.lower().startswith('http'):
# resolve aliases; can add more
match filepath.lower():
case '%shell32': filepath = r'C:\Windows\System32\shell32.dll'
case '%imageres': filepath = r'C:\Windows\System32\imageres.dll'
case '%ddo': filepath = r'C:\windows\System32\ddores.dll'
return filepath
import urllib.request,tempfile
target = '\\'.join([tempfile.gettempdir(), pathlib.Path(filepath).name])
try:
with urllib.request.urlopen(filepath,timeout=3.5) as response, open(target, 'wb') as out_file:
out_file.write(response.read())
#if not pathlib.Path(target).exists(): target = None
except:
#if pathlib.Path(target).exists(): os.remove(target)
target = None
return target
def __load_library(self, lpFileName):
LoadLibraryExW = self.__kernel32.LoadLibraryExW
LoadLibraryExW.argtypes = [wintypes.LPCWSTR, wintypes.HANDLE, wintypes.DWORD]
LoadLibraryExW.restype = wintypes.HMODULE
flags = self.__LOAD_LIBRARY_AS_DATAFILE | self.__LOAD_LIBRARY_AS_IMAGE_RESOURCE
return LoadLibraryExW(lpFileName, 0, flags)
def __enum_resource_names(self, handle, lpType, lpEnumFunc):
ENUMFUNC = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HMODULE, wintypes.LPVOID, wintypes.LPVOID, wintypes.LPARAM)
enum_func = ENUMFUNC(lpEnumFunc)
enum_func.restype = ctypes.c_bool
EnumResourceNames = self.__kernel32.EnumResourceNamesW
EnumResourceNames.argtypes = [wintypes.HMODULE, wintypes.LPVOID, ENUMFUNC, wintypes.LPARAM]
EnumResourceNames.restype = ctypes.c_bool
return EnumResourceNames(handle, lpType, enum_func, 0)
def __find_resource(self, hModule, lpName, lpType):
FindResource = self.__kernel32.FindResourceW
FindResource.argtypes = [wintypes.HMODULE, wintypes.LPVOID, wintypes.LPVOID]
FindResource.restype = wintypes.HRSRC
return FindResource(hModule, lpName, lpType)
def __sizeof_resource(self, hModule, hResInfo):
SizeofResource = self.__kernel32.SizeofResource
SizeofResource.argtypes = [wintypes.HMODULE, wintypes.HRSRC]
SizeofResource.restype = wintypes.DWORD
return SizeofResource(hModule, hResInfo)
def __load_resource(self, hModule, hResInfo):
LoadResource = self.__kernel32.LoadResource
LoadResource.argtypes = [wintypes.HMODULE, wintypes.HRSRC]
LoadResource.restype = wintypes.HGLOBAL
return LoadResource(hModule, hResInfo)
def __lock_resource(self, hResData):
LockResource = self.__kernel32.LockResource
LockResource.argtypes = [wintypes.HGLOBAL]
LockResource.restype = ctypes.c_void_p
return LockResource(hResData)
__user32 = ctypes.WinDLL(r'C:\Windows\System32\user32.dll')
__kernel32 = ctypes.WinDLL(r'C:\Windows\System32\kernel32.dll')
__LOAD_LIBRARY_AS_DATAFILE = 0x2
__LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x20
__RT_ICON = 3
__RT_GROUP_ICON = __RT_ICON + 11
__GRPICONDIRENTRY = ["bWidth","bHeight","bColorCount","bReserved","wPlanes","wBitCount","dwBytesInRes","nId"]
#__ICONDIRENTRY = ["bWidth","bHeight","bColorCount","bReserved","wPlanes","wBitCount","dwBytesInRes","dwImageOffset"]
__ICOHEADER = '@IH'
# END class IconReader
icons = IconReader.ImportIcons(r'C:\windows\system32\WindowsPowerShell\v1.0\PowerShell.exe')
fmtR = '{0:>{1}}'
fmtL = '{0:<{1}}'
for i in icons:
print(
fmtR.format(i.Handle,11),
fmtR.format(i.Index,3),
fmtR.format(i.Width,4),
fmtR.format(i.Bpp,3),
fmtR.format(i.Colors,3),
fmtR.format(i.ImgLength,8),
fmtR.format(i.GroupIndex,3),
fmtL.format(i.GroupId,13)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment