ICO files are old but still widely used. One unique aspect of the ICO format is that it can contain multiple images of different sizes. The format itself is straightforward, yet free tools to create an ICO file from a list of images are scarce. Let's build one in .NET!
The ICO format specification is available on Wikipedia. The code below is a simple implementation of the specification. ICO stores numeric fields using little-endian encoding, so the sample writes multi-byte values explicitly in little-endian form. It supports PNG images only and does not cover all features of the format.
Create a console application and add the ImageSharp package. The ImageSharp library handles converting images to PNG and reading their dimensions.
Shell
dotnet new console
dotnet add package SixLabors.ImageSharp
C#
using System.Buffers.Binary;
using SixLabors.ImageSharp;
string[] inputFiles = [ "img_1.png", "img_2.png" ];
string outputFile = "output.ico";
await CreateIcon(inputFiles, outputFile);
// Spec https://en.wikipedia.org/wiki/ICO_(file_format)
static async Task CreateIcon(string[] inputFiles, string outputFile)
{
// Convert images to png and extract image dimensions
var pngs = new (byte[] Data, int Width, int Height)[inputFiles.Length];
for (int i = 0; i < inputFiles.Length; i++)
{
using var image = await Image.LoadAsync(inputFiles[i]);
var pngStream = new MemoryStream();
image.SaveAsPng(pngStream);
pngs[i] = (pngStream.ToArray(), image.Width, image.Height);
}
// Create the ico file
await using var output = File.OpenWrite(outputFile);
// Write header
// 0-1 reserved
output.WriteByte(0);
output.WriteByte(0);
// 2-3 image type, 1 = icon, 2 = cursor
WriteInt16LittleEndian(output, 1);
// 4-5 number of images
WriteInt16LittleEndian(output, inputFiles.Length);
// image data offset
// ico header (6 bytes) + image directory (16 bytes per image)
long offset = 6 + (16 * inputFiles.Length);
// Write image directory
foreach (var png in pngs)
{
// Convert img to png
var dataLength = png.Data.Length;
// 0 image width
output.WriteByte((byte)(png.Width >= 256 ? 0 : png.Width));
// 1 image height
output.WriteByte((byte)(png.Height >= 256 ? 0 : png.Height));
// 2 number of colors, 0 if the image does not use a color palette
output.WriteByte(0);
// 3 reserved
output.WriteByte(0);
// 4-5 color planes
WriteInt16LittleEndian(output, 0);
// 6-7 bits per pixel, png use 4 bytes per pixel (argb)
WriteInt16LittleEndian(output, 32);
// 8-11 size of image data
WriteUInt32LittleEndian(output, checked((uint)dataLength));
// 12-15 offset of image data
WriteUInt32LittleEndian(output, checked((uint)offset));
offset += dataLength;
}
// Write image data
// png data must contain the whole png data file
foreach (var png in pngs)
{
await output.WriteAsync(png.Data);
}
}
static void WriteInt16LittleEndian(Stream stream, int value)
{
Span<byte> buffer = stackalloc byte[sizeof(short)];
BinaryPrimitives.WriteInt16LittleEndian(buffer, checked((short)value));
stream.Write(buffer);
}
static void WriteUInt32LittleEndian(Stream stream, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
stream.Write(buffer);
}
Do you have a question or a suggestion about this post? Contact me!