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 Meziantou.Framework.Imaging package. This fully managed library handles converting images to PNG and reading their dimensions.

Shell
dotnet new console
dotnet add package Meziantou.Framework.Imaging
C#
using System.Buffers.Binary;
using Meziantou.Framework.Imaging;
using Meziantou.Framework.Imaging.Formats;

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.Save(pngStream, new PngEncoder());
        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);
}

The manual implementation above is useful to understand the format. However, Meziantou.Framework.Imaging can also write ICO files directly. Add each image to an ImageCollection and save it with the IcoEncoder:

C#
using Meziantou.Framework.Imaging;
using Meziantou.Framework.Imaging.Formats;

using var icon = ImageCollection.Create(ImageCollectionKind.Representations);
foreach (var inputFile in inputFiles)
{
    using var image = await Image.LoadAsync(inputFile);
    icon.Add(image); // Add copies the image
}

// ICO cannot store metadata such as EXIF, so drop it instead of failing
icon.Save(outputFile, new IcoEncoder { MetadataHandling = MetadataHandling.DiscardUnsupported });

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?