In this note we are considering the image extraction from ICO files using the Win32 API.

The ICO file is a container which contains images of a different size and color depth. Icon class allows to load only one image from the ICO file. When the image size is set the algorithm tries to select a more appropriated image from the ICO file.

To extract a specific image from the ICO file CreateIconFromResourceEx function is used.

All that we need is to provide the proper resource bits from the ICO file. To find these bits let’s look at the structure of the ICO file.

The ICO file starts with the fixed header:

public struct ICONDIR
{
    public ushort idReserved;          // Reserved (must be 0)
    public ushort idType;              // Resource Type (1 for icons)
    public ushort idCount;             // The total number of images
    public ICONDIRENTRY[] idEntries;   // An entry for each image
};

We need only idCount field and to know that the header size is 6 bytes (3 times ushort).

Each entry has the following structure:

public struct ICONDIRENTRY
{
    public byte bWidth;          // Width of the image
    public byte bHeight;         // Height of the image
    public byte bColorCount;     // Number of colors in the image (0 if >= 8bpp)
    public byte bReserved;       // Reserved (must be 0)
    public ushort wPlanes;       // Color Planes
    public ushort wBitCount;     // Bits per pixel
    public uint dwBytesInRes;    // The total size of the image
    public uint dwImageOffset;   // The position of the image
};

The size of this stucture is 16 bytes. We need dwBytesInRes and dwImageOffset fields.

These structures are followed by the images data. From this data we need to extract image bits and pass them to CreateIconFromResourceEx function.

Suppose that we read the entire ICO file to the buffer:

using var stream = File.OpenRead(fileName);
var length = (int)stream.Length;

var array = ArrayPool<byte>.Shared.Rent(length);
try
{
    var buffer = array.AsSpan(0, length);
    stream.ReadExactly(buffer);

    ExtractFrames(buffer, outputDirectory);
}
finally
{
    ArrayPool<byte>.Shared.Return(array);
}

void ExtractFrames(Span<byte> buffer, string outputDirectory)
{
    var framesCount = GetFramesCount(buffer);

    for (var i = 0; i < framesCount; i++)
    {
        using var bitmap = GetFrame(buffer, i);
        bitmap.Save(Path.Combine(outputDirectory, $"{i}.png"), System.Drawing.Imaging.ImageFormat.Png);
    }
}

To get the total number of images we need to read a couple of bytes from the buffer (see the ICONDIR structure):

int GetFramesCount(Span<byte> buffer)
{
    return BitConverter.ToUInt16(buffer.Slice(sizeof(ushort) * 2, sizeof(ushort)));
}

Since each directory entry has the same size (16 bytes), we can get a buffer for any entry. From this buffer we can extract dwBytesInRes and dwImageOffset fields (see the ICONDIRENTRY structure).

The final version of GetFrame method is the following:

Bitmap GetFrame(Span<byte> buffer, int frameId)
{
    const int ICON_DIR_SIZE = 6;
    const int ICON_DIR_ENTRY_SIZE = 16;

    var entryBuffer = buffer
        .Slice(ICON_DIR_SIZE)
        .Slice(ICON_DIR_ENTRY_SIZE * frameId, ICON_DIR_ENTRY_SIZE);

    int pos = BitConverter.ToInt32(entryBuffer.Slice(ICON_DIR_ENTRY_SIZE - sizeof(uint), sizeof(uint)));
    int length = BitConverter.ToInt32(entryBuffer.Slice(ICON_DIR_ENTRY_SIZE - sizeof(uint) * 2, sizeof(uint)));

    using var handle = Windows.Win32.PInvoke.CreateIconFromResourceEx(
        buffer.Slice(pos, length),
        new Windows.Win32.Foundation.BOOL(true),
        0x00030000,
        0,
        0,
        0);

    using var icon = Icon.FromHandle(handle.DangerousGetHandle());

    return icon.ToBitmap();
}

I use C#/Win32 P/Invoke Source Generator so we need to add the reference to the NuGet package and add NativeMethods.txt with CreateIconFromResourceEx function. Also this method uses System.Drawing.Common. We need to add the reference to it too. The sample code is available here.

Don’t forget that System.Drawing.Common package works only on Windows! Of course, in this case cross-platforming is pointless, since we use the system function CreateIconFromResourceEx. But it’s just a friendly reminder.

There is another way to extract images from ICO files without using Windows-specific functions. We’ll consider it in the next note.