In this note we are considering the image extraction from ICO files in pure C# (without Win32 or any other dependencies).
The ICO file is a container which contains images of a different size and color depth.
The file header has the following data:
Offset | Size | Description |
---|---|---|
0 | 2 | Reserved; it must be 0 |
2 | 2 | The type: it muse be 1 for icons |
4 | 2 | The number of images in the file |
Then the array of ico directory entities follows:
Offset | Size | Description |
---|---|---|
0 | 1 | The image width; 0 value is used for 256 |
1 | 1 | The image height; 0 value is used for 256 |
2 | 1 | The number of colors in the palette; 0 if the image doesn’t use a palette. |
3 | 1 | Reserved. Must be 0 |
4 | 2 | The number of planes |
6 | 2 | The number of bits per pixel |
8 | 4 | The size of the image data (in bytes) |
12 | 4 | The offset of the image data from the beginning of the file |
To obtain the position and size of the image data we need to parse the last two values (entryBuffer - the byte array of the whole ICO file):
// extract the position and length of the content
int pos = BitConverter.ToInt32(entryBuffer[(ICON_DIR_ENTRY_SIZE - sizeof(uint))..]);
int length = BitConverter.ToInt32(entryBuffer[(ICON_DIR_ENTRY_SIZE - sizeof(uint) * 2)..]);
The image can be in two formats: PNG or bitmap. In the case of PNG we can simply extract data by the offset and save to the file:
// check the PNG signature
if (IsPng(contentBuffer))
{
// save the PNG file
using var outputStream = File.Create(Path.Combine(outputDirectory, "image.png"));
outputStream.Write(contentBuffer);
}
// Checks if the buffer contains PNG image
bool IsPng(Span<byte> buffer)
{
long PngSignature = BitConverter.ToInt64(new byte[] {
(byte)0x89,
(byte)0x50,
(byte)0x4e,
(byte)0x47,
(byte)0x0d,
(byte)0x0a,
(byte)0x1a,
(byte)0x0a
});
return buffer.Length > sizeof(long)
&& BitConverter.ToInt64(buffer[..sizeof(long)]) == PngSignature;
}
The bitmap case is more complex. First of all, the bitmap is stored without a file header. That means we can’t save it as it is. The image consists of two pixel arrays - the original image and the transparency bitmask. The icon image is their product. It’s important for indexed bitmaps (bit count <= 8), less important for 16/24 bit bitmaps (without a bitmask we lose the transparency) and it doesn’t matter for 32 bit bitmaps (some tools even add a solid bitmask to these images; but that’s wrong). And the last (but not the least) - it’s height. The height of the image is twice as large (due to the bitmask).
As an ICO bitmap doesn’t have a file header, the image data starts with the bitmap header. BITMAPINFOHEADER is used:
Offset | Size | Description |
---|---|---|
0 | 4 | The header size in bytes; it equals 40. |
4 | 4 | The bitmap width (in pixels) |
8 | 4 | The bitmap height (in pixels) |
12 | 2 | The number of planes for the target device; it must be 1 |
14 | 2 | The number of bits per pixel |
16 | 4 | The image compression method; for icons BI_RGB (0) or BI_BITFIELDS (3) is used |
20 | 4 | The size of the image (in bytes); it can be set to 0 for uncompressed RGB bitmaps |
24 | 4 | The image horizontal resolution (in pixel per meter) |
28 | 4 | The image vertical resolution (in pixel per meter) |
32 | 4 | The number of colors in the palette; if 0 it’s calculated in the following way: 1 « BitCount |
36 | 4 | The number of important colors is used, or 0 when every color is important |
This code loads the required information from the header:
// note: we divide height by 2
Height = BitConverter.ToInt32(buffer[BITMAP_HEADER_HEIGHT..]) / 2;
Width = BitConverter.ToInt32(buffer[BITMAP_HEADER_WIDTH..]);
BitCount = BitConverter.ToInt16(buffer[BITMAP_HEADER_BIT_COUNT..]);
// Check the color count
var colorCount = BitConverter.ToInt32(buffer[BITMAP_HEADER_COLORS..]);
For indexed bitmaps (bit count <= 8) the color table follows the bitmap header. The color table is an array of uint
values. The total number of these values is set by colorCount
from the header or in the following way:
if (colorCount == 0)
{
colorCount = BitCount <= 8 ? (1 << BitCount) : 0;
}
The color table is only used for indexed bitmaps (bit count <= 8); otherwise this section is omitted.
To parse the color table the following code is used:
var colorTableLength = colorCount * sizeof(uint);
var colorTable = colorTableLength == 0
? []
: ParseColorTable(buffer.Slice(BITMAP_INFO_HEADER_SIZE, colorTableLength));
uint[] ParseColorTable(Span<byte> buffer)
{
var colorTableLength = buffer.Length / sizeof(uint);
var pos = 0;
var colorTable = new uint[colorTableLength];
for (var i = 0; i < colorTable.Length; i++)
{
var b = buffer[pos++];
var g = buffer[pos++];
var r = buffer[pos++];
var a = buffer[pos++];
colorTable[i] = (uint)
(0xFFFFFFFF & (a << 24 | r << 16 | g << 8 | b));
}
return colorTable;
}
The structure of the pixel array depends on the bit count property. For indexed bitmap you need to read data bit by bit and then get the value from the color table. 24 (32 bit) bitmap stores its pixels as 3 (4) bytes sequence.
The following code shows how to parse a pixel array for 32 or 24 bits bitmaps (the case of 16 bit bitmap is omitted):
var pixels = ParsePixels(Width, Height, BitCount == 32, buffer[BITMAP_INFO_HEADER_SIZE..]);
uint[] ParsePixels(int width, int height, bool hasAlpha, Span<byte> pixelBuffer)
{
var pixels = new uint[width * height];
var pos = 0;
for (var y = height; y > 0; y--)
{
for (var x = 0; x < width; x++)
{
var b = pixelBuffer[pos++]; // blue
var g = pixelBuffer[pos++]; // green
var r = pixelBuffer[pos++]; // red
if (hasAlpha)
{
var a = pixelBuffer[pos++]; // alpha
pixels[width * (y - 1) + x] = (uint)
(0xFFFFFFFF & (a << 24 | r << 16 | g << 8 | b));
}
else
{
pixels[width * (y - 1) + x] = (uint)
(0xFFFFFFFF & ((r + b + g == 0 ? 0 : 255) << 24 | r << 16 | g << 8 | b));
}
}
}
return pixels;
}
In case of indexed images a new concept is introduced: the row length of the pixel array must be aligned to 4 bytes. The row width is calculated in the following way:
int GetStride(int width, int bitCount)
{
return (((width * bitCount) + 31) & ~31) >> 3;
}
To calculate the distance to the end of the row you can use the following function:
int GetPaddingBytes(int width, int bitCount)
{
return GetStride(width, bitCount) - Convert.ToInt32(Math.Ceiling(width * bitCount / 8.0));
}
The following code loads values from the buffer bit by bit:
void ReadBits(int width, int height, int bitCount, Span<byte> buffer, Span<byte> data)
{
var pos = 0;
var x = 0;
var y = height;
var paddingBytes = GetPaddingBytes(width, bitCount);
var b = buffer[pos++];
var bPos = 0;
do
{
var mask = (1 << bitCount) - 1;
var result = (b >> (8 - bPos - bitCount)) & mask;
data[width * (y - 1) + x] = (byte)result;
if (x < width - 1)
{
x++;
}
else
{
x = 0;
y--;
pos += paddingBytes;
bPos = 8;
}
if (bPos + bitCount >= 8 && y > 0)
{
b = buffer[pos++];
bPos = 0;
}
else
{
bPos += bitCount;
}
}
while (y > 0);
}
To convert this raw data into an array of pixels the following code is used:
T[] ParseIndexedPixels<T>(int width, int height, int bitCount, IList<T> valueTable, Span<byte> pixelBuffer)
{
var rawDataSize = width * height;
var rawData = ArrayPool<byte>.Shared.Rent(rawDataSize);
var pixels = new T[rawDataSize];
try
{
ReadBits(width, height, bitCount, pixelBuffer, rawData.AsSpan(0, rawDataSize));
for (var i = 0; i < pixels.Length; i++)
{
pixels[i] = valueTable[rawData[i]];
}
}
finally
{
ArrayPool<byte>.Shared.Return(rawData);
}
return pixels;
}
This code is generic because we can use it to parse a pixel array or a bitmask:
// we previously loaded colorTable
var pixels = ParseIndexedPixels(Width, Height, BitCount, colorTable, buffer[(BITMAP_INFO_HEADER_SIZE + colorTableLength)..]);
var bitmask = ParseIndexedPixels(Width, Height, 1, [true, false], buffer[^(GetStride(Width, 1) * Height)..]);
The bitmask is located at the end of the image. We need to get the stride and multiple it to the image height to calculate its length.
At this point we have loaded our bitmap from the ico file. The next step is to save it as 32 bit bitmap.
To do it properly we need to:
- Generate a bitmap file header
- Change a bitmap header to BITMAPV5HEADER to support 32 bit bitmaps
- Apply a bitmask to the pixel array
Consider the following Save
function:
void Save(Stream stream) {
const int BI_BITFIELDS = 0x0003;
var pixelBufferLength = Width * Height * sizeof(uint);
var imageLength = FILE_HEADER_SIZE + BITMAP_V5_HEADER_SIZE + pixelBufferLength;
// rent the array for the file
var buffer = ArrayPool<byte>.Shared.Rent(imageLength);
try
{
//... generate a bitmap file header
//... generate a bitmap header
//... generate a new pixel array
// save an image into the stream
stream.Write(buffer.AsSpan(0, imageLength));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
We have created the buffer byte array to work with the new image data.
The bitmap file header has the following structure:
Offset | Size | Description |
---|---|---|
0 | 2 | The bitmap signature; we use BM |
2 | 4 | The image size: FILE_HEADER_SIZE + BITMAP_V5_HEADER_SIZE + pixelBufferLength |
6 | 2 | Reserved; can be 0 |
8 | 2 | Reserved; can be 0 |
10 | 4 | The offset of the pixel array from the beginning of the file. We save image as 32 bit bitmap so our file contains only a file header, a bitmap header and a pixel array. Therefore, the pixel array offset is FILE_HEADER_SIZE + BITMAP_V5_HEADER_SIZE |
The following code generates the bitmap file header:
// bitmap file header
var headerBuffer = buffer.AsSpan(0, FILE_HEADER_SIZE);
headerBuffer.Clear();
// write the file signature
headerBuffer[0] = (byte)'B';
headerBuffer[1] = (byte)'M';
// write the full file size: add the header size and subtract the bitmask length
BitConverter.TryWriteBytes(
headerBuffer[FILE_HEADER_SIZE..],
imageLength);
// write the pixel array position: file header + bitmap header
BitConverter.TryWriteBytes(
headerBuffer[FILE_HEADER_PIXELS..],
FILE_HEADER_SIZE + BITMAP_V5_HEADER_SIZE);
BITMAPV5HEADER extends BITMAPINFOHEADER. We only need a few fields:
Offset | Size | Description |
---|---|---|
40 | 4 | Color mask that specifies the red component of each pixel (0x00FF0000) |
44 | 4 | Color mask that specifies the green component of each pixel (0x00FF00) |
48 | 4 | Color mask that specifies the blue component of each pixel (0x000000FF) |
52 | 4 | Color mask that specifies the alpha component of each pixel (0xFF000000) |
Also we need to set compression to BI_BITFIELDS
(0x0003) and the bitmap header size to 124:
// bitmap header
var bitmapHeaderBuffer = buffer.AsSpan(FILE_HEADER_SIZE, BITMAP_V5_HEADER_SIZE);
bitmapHeaderBuffer.Clear();
// write the image parameters
BitConverter.TryWriteBytes(bitmapHeaderBuffer, BITMAP_V5_HEADER_SIZE);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_WIDTH..], Width);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_HEIGHT..], Height);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_COLOR_PANES..], 1);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_BIT_COUNT..], 32);
// write the bitfields masks
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_COMPRESSION..], BI_BITFIELDS);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_RED_MASK..], (uint)0x00ff0000);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_GREEN_MASK..], (uint)0x00ff00);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_BLUE_MASK..], (uint)0x000000ff);
BitConverter.TryWriteBytes(bitmapHeaderBuffer[BITMAP_HEADER_ALPHA_MASK..], (uint)0xff000000);
To complete our Save
function we need to implement the pixel array generation. The algorithm is the following: if the bitmask is true
- set a pixel value; otherwise, set a transparent pixel. For 32 bit bitmaps the bitmask is ignored:
// write pixel array
WritePixels(
Width,
Height,
pixels,
BitCount == 32 ? [] : bitmask,
buffer.AsSpan(FILE_HEADER_SIZE + BITMAP_V5_HEADER_SIZE, pixelBufferLength));
void WritePixels(int width, int height, uint[] pixels, bool[] bitmask, Span<byte> buffer)
{
var hasAlpha = bitmask.Length == 0;
buffer.Clear();
var pos = 0;
var mask = (1u << 8) - 1;
for (var y = height; y > 0; y--)
{
for (var x = 0; x < width; x++)
{
// check the bitmask for the transparency for non-32 bit bitmaps
var color = hasAlpha || bitmask[width * (y - 1) + x]
? pixels[width * (y - 1) + x]
: 0x00000000;
buffer[pos++] = (byte)(color & mask);
buffer[pos++] = (byte)((color >> 8) & mask);
buffer[pos++] = (byte)((color >> 16) & mask);
// use the native transparency for 32 bit bitmaps or the bitmask value
buffer[pos++] = hasAlpha
? (byte)((color >> 24) & mask)
: bitmask[width * (y - 1) + x] ? (byte)0xFF : (byte)00;
}
}
}
The Save
function is completed. It saves the icon image as 32 bit bitmap (with transparency). It doesn’t require any additional dependency so it can run on any OS that is supported by .NET.
The full source code is available here.
Useful links: