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.