The ICO File format (icon/icons)

In this post we will discuss the rather straight forward icon file format (from here on referred to as ICO file format), what icons files are, how they are structured, and most importantly how we can read them, and thus construct and save them as well. This post should give a thorough understanding of the file format and how it works, allowing you to work with ICO files yourself. Most code examples are taken from ico.h, from the open source headers library, which can load icons and cursors to binary data.

The ICO file format is a rather simple file format primarily used for icons in Windows. An icon file can contain several images of various sizes, formats, and color depths to ensure maximum compatibility for all supported platforms and contexts. ICO (from here on icon) files are primarily used for application and file icons in windows, website icons or windows cursors (in the form of the CUR file format, which is identical to the icon file format).

File structure

struct ICON {
    ICONDIR header;
    ICONDIRENTRY entries[];
    u08 entryData[];
}

Icon files a structured as a header with metadata about the icon file itself, called the icon directory, followed by metadata for each of the icons in the file, called directory entries (in arbitratry order, IE. the order of the icon data is not guarenteed to be the same as the order of the directory), finally followed by the binary data for the directory entries themsleves.

Icon files are all stored in little-endian byte order.

struct ICONDIR {
    u08 reserved[2]; // Must always be 0
    u16 type;
    u16 numberOfImages;
};

The icon directory starts with two reserved bytes, these should always be set to 0x00, however most parsers/viewers will simply skip the initial two bytes of the directory, ignoring them, though a strict parser should generate a warning/error when it encounters reserved bytes that are not 0x00. Following the reserved bytes is a 16-bit unsigned integer, type, which specifies what type of icon file this is, the only two valid values for this field are 0x01 and 0x02, where 0x01 specifies that this is a regular icon file, and 0x02 that this file is a cursor file, and instead contains cursor entries.

Please note that the ICONDIR is the same for both icon and cursor files.

struct ICONDIRENTRY {
    u08 width;
    u08 height;
    u08 colors;
    u08 reserved; // Must always be 0
    u16 colorPlanes;
    u16 bitsPerPixel;
    u32 size;
    u32 offset;
};

Each entry contains information about the icon itself, starting with its width and height, given the nature of icons the images are usually square, though this is not a requirement. It is important to note that icons support images of up to 256 by 256 pixels, so a width and/or height of 0 actually specifies 256.

colors indicate how many colors are used in the image, and thus the size of the color palette, and is set to 0 if a color palette is not used (color palettes are used for bitmaps with less than 16 bits per pixel). Likewise colorPlanes is used in conjunction with colors and is set to 0 if a pelatte is not used, 1 if it is.

bitsPerPixel specifies how many bits are used per pixel, if less than 16 bits per pixel is used the image will be using a color palette, together with colorPlanes the maximum number of colors in the palette can be calculated by 2^(colorPlanes * bitsPerPixel).

size tells us the size (in bytes) of the images data and offset gives us the offset (from the start of the file) where the image data is

Reading ICO files

Given the simple nature of the ICO format icon files are relatively straight forward to read, this can be done as follows in C, likewise this exaple can be easily adpated to instead write the icon files to disk:

FILE* icon = fopen("file.ico", "rb");

// Read the header
ICONDIR icondir;
fread(&icondir, sizeof(ICONDIR), 1, icon);

// Allocate an ICONDIRENTRY for each icon and read all entries at once
ICONDIRENTRY* entries = malloc(sizeof(ICONDIRENTRY) * icondir.numberOfImages);
fread(entries, sizeof(ICONDIRENTRY), icondir.numberOfImages, icon);

// Iterate over all entries and read their data
unsigned char** imageData = malloc(sizeof(unsigned char*), icondir.numberOfImages);
for(int i = 0; i < iconDir.numberOfImages; i++) {
    imageData[i] = malloc(entries[i].Size);
    fseek(icon, entries[i].offset, SEEK_SET);
    fread(&imageData[i], sizeof(unsigned char), entries[i].size, icon);
}
fclose(icon);

// Do something with the icons and their data...