Creating ico files from multiple images in .NET

 
 
  • Gérald Barré

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!

Follow me:
Enjoy this blog?