All formats

ImageSharp

LanguageC# (.NET 8 or later)
LicenseSix Labors Split License: Apache 2.0 for open source projects and companies under $1M annual revenue, otherwise a commercial license
Repositorygithub.com/SixLabors/ImageSharp
DocumentationICO in the ImageSharp docs
Depends onnothing native (pure managed code)
Read / writeyes / yes (version 4.0 and later), BMP and PNG entries, CUR read and write with hotspots

ImageSharp reads and writes ICO and CUR from version 4.0, in pure managed code. Every icon entry becomes one frame of the image. Because ImageSharp frames share one size, smaller entries sit in the top-left corner of a canvas as large as the biggest entry; IcoFrameMetadata.EncodingWidth and EncodingHeight give the real size of each entry, and the encoder crops to them when writing.

Per-frame metadata controls storage: Compression (IconFrameCompression.Bmp or Png) and BmpBitsPerPixel for BMP entries. CUR files use CurFrameMetadata, which adds HotspotX and HotspotY. Check the license before you use it in a commercial closed-source product.

Installation#

Shell
dotnet add package SixLabors.ImageSharp

Basic usage: list the entries of an ICO file#

Image.Identify reads the directory without decoding pixels:

C#
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Ico;

ImageInfo info = Image.Identify("app.ico");
for (int i = 0; i < info.FrameCount; i++)
{
    IcoFrameMetadata m = info.FrameMetadataCollection[i].GetIcoMetadata();
    int w = m.EncodingWidth is > 0 ? m.EncodingWidth.Value : 256;  // 0 means 256
    int h = m.EncodingHeight is > 0 ? m.EncodingHeight.Value : 256;
    Console.WriteLine($"{i}: {w} x {h}, {m.Compression}, {m.BmpBitsPerPixel}");
}

How to export every icon size to PNG#

Clone each frame and crop it to the entry’s own size:

C#
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

using Image<Rgba32> icon = Image.Load<Rgba32>("app.ico");
for (int i = 0; i < icon.Frames.Count; i++)
{
    IcoFrameMetadata m = icon.Frames[i].Metadata.GetIcoMetadata();
    int w = m.EncodingWidth is > 0 ? m.EncodingWidth.Value : icon.Width;
    int h = m.EncodingHeight is > 0 ? m.EncodingHeight.Value : icon.Height;

    using Image<Rgba32> entry = icon.Frames.CloneFrame(i);
    entry.Mutate(x => x.Crop(w, h));
    entry.SaveAsPng($"icon-{w}x{h}.png");
}

How to convert a PNG to a multi-size ICO#

Add one frame per size. Each frame has the canvas size (256x256); the smaller image is drawn in the top-left corner and EncodingWidth/EncodingHeight tell the encoder how much to keep:

C#
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

using Image<Rgba32> icon = Image.Load<Rgba32>("logo.png");
icon.Mutate(x => x.Resize(256, 256));
icon.Frames.RootFrame.Metadata.GetIcoMetadata().Compression = IconFrameCompression.Png;

foreach (int size in new[] { 48, 32, 16 })
{
    using Image<Rgba32> small = icon.Clone(x => x.Resize(size, size));
    using Image<Rgba32> canvas = new(256, 256);  // transparent
    canvas.Mutate(x => x.DrawImage(small, new Point(0, 0), 1f));

    ImageFrame<Rgba32> frame = icon.Frames.AddFrame(canvas.Frames.RootFrame);
    IcoFrameMetadata m = frame.Metadata.GetIcoMetadata();
    m.Compression = IconFrameCompression.Bmp;
    m.EncodingWidth = (byte)size;
    m.EncodingHeight = (byte)size;
}

icon.Save("app.ico", new IcoEncoder());

How to read a cursor hotspot#

C#
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Cur;

using Image cursor = Image.Load("pointer.cur");
CurFrameMetadata m = cursor.Frames.RootFrame.Metadata.GetCurMetadata();
Console.WriteLine($"hotspot: {m.HotspotX}, {m.HotspotY}");

Set HotspotX and HotspotY on each frame and save with new CurEncoder() to write a cursor.

Tips#