Convert HBITMAP to .ICO file

There is surprisingly little documentation or example code about the .ICO format. There are easy to use windows API functions to load icons from files and resources but only with limited options. Recently I have had to work with this format in detail and here I will give a code example of how to save a HBITMAP as an .ico file. I will only consider 32bit bitmaps\icons for now.

There is a detailed article on MSDN that gives a lot of background information, the only problem it was written in 1995, and has no information about how to deal with 32bit icons. But it still gives important details about the internal structure of the format and is worth looking at.

The key difference with 32bit icons is the XOR map is not added to the entry. Also the color table is not 3 components(RGB) it is 4 components to include the 8bit alpha channel.

All Code is freely provided, no guarantees or warranties about its quality, use at your own risk. In my next post I will show how to read the .ico format.


First we need to define some structures, these are the same as those used in the MSDN article

// These next two structs represent how the icon information is stored
// in an ICO file.
typedef struct
  BYTE  bWidth;               // Width of the image
  BYTE  bHeight;              // Height of the image (times 2)
  BYTE  bColorCount;          // Number of colors in image (0 if >=8bpp)
  BYTE  bReserved;            // Reserved
  WORD  wPlanes;              // Color Planes
  WORD  wBitCount;            // Bits per pixel
  DWORD  dwBytesInRes;         // how many bytes in this resource?
  DWORD  dwImageOffset;        // where in the file is this image
typedef struct 
  WORD      idReserved;   // Reserved
  WORD      idType;       // resource type (1 for icons)
  WORD      idCount;      // how many images?
  ICONDIRENTRY  idEntries[1]; // the entries for each image
typedef struct
  UINT      Width, Height, Colors; // Width, Height and bpp
  LPBYTE      lpBits;                // ptr to DIB bits
  DWORD      dwNumBytes;            // how many bytes?
  LPBITMAPINFO  lpbi;                  // ptr to header
  LPBYTE      lpXOR;                 // ptr to XOR image bits
  LPBYTE      lpAND;                 // ptr to AND image bits

Next we need a function that converts the HBITMAP into a ICONIMAGE structure, making sure the DIB is 32bit

// How wide, in bytes, would this many bits be, DWORD aligned?
#define WIDTHBYTES(bits)      ((((bits) + 31)>>5)<<2)
ICONIMAGE CreateIconImageFromBitmap(HBITMAP bmp)
  info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  //Get the size of the bitmap
  HDC hdc = GetDC(NULL);  
  GetDIBits(hdc, bmp, 0, 0, NULL, &info, DIB_RGB_COLORS);
  //allocate space for pixels
  DWORD sz = info.bmiHeader.biWidth * info.bmiHeader.biHeight * 4;
  BYTE* pPixels = (BYTE*)malloc(sizeof(BYTE)*sz);
  memset(pPixels, 0, sz);  
  //get pixel data in 32bit format
  info.bmiHeader.biSize = sizeof(info.bmiHeader);
  info.bmiHeader.biBitCount = 32;
  info.bmiHeader.biCompression = BI_RGB;
  info.bmiHeader.biHeight = (info.bmiHeader.biHeight < 0) ? (-info.bmiHeader.biHeight) : (info.bmiHeader.biHeight);  // correct the bottom-up ordering of lines
  GetDIBits(hdc, bmp, 0, info.bmiHeader.biHeight, (LPVOID)pPixels, &info, DIB_RGB_COLORS);  
  LONG width = info.bmiHeader.biWidth;
  LONG height = info.bmiHeader.biHeight;
  LONG imgSz = (info.bmiHeader.biWidth*info.bmiHeader.biHeight*(info.bmiHeader.biBitCount/8));
  ico.Colors = info.bmiHeader.biBitCount;
  ico.Width = width;
  ico.Height = height;
  DWORD xorSz = 0; //not used in 32bit icons
  DWORD andSz = ico.Height * WIDTHBYTES( ico.Width );
  ico.dwNumBytes = sizeof( BITMAPINFOHEADER )
          + imgSz
          + xorSz  // XOR mask
                    + andSz; // AND mask
  ico.lpBits = (LPBYTE)malloc(ico.dwNumBytes);
  memset(ico.lpBits, 0, ico.dwNumBytes);
  // Copy the bits
    memcpy(ico.lpBits, &info.bmiHeader, sizeof(BITMAPINFOHEADER));
  memcpy(ico.lpBits + sizeof(BITMAPINFOHEADER), pPixels, imgSz);
    // Adjust internal pointers/variables for new image
    ico.lpbi = (LPBITMAPINFO)(ico.lpBits);    
  ico.lpXOR = ico.lpBits + sizeof(BITMAPINFOHEADER) + imgSz;
    ico.lpAND = ico.lpXOR + xorSz;
  if(xorSz > 0)
    memcpy( ico.lpXOR, ico.lpBits + sizeof(BITMAPINFOHEADER), xorSz );
  memset( ico.lpAND, 0x0, andSz );
  //fix bmp header for icon height, x2 required
  ico.lpbi->bmiHeader.biHeight *= 2;   
  return ico;


And finally the function that takes a HBITMAP and a destination filepath for the target .ico file.
This function takes an array of HBITMAP’s of bmpsSz length and writes them all to a single .ico file. (there is no validation of the file path, so make sure it can be written to first and add a .ico extension)


void ConvertBitmapToICO(HBITMAP* bmps, int bmpsSz, TCHAR* destPath)
  int entryCount = bmpsSz;
  //create icons from bitmaps
  ICONIMAGE* icons = (ICONIMAGE*)malloc(sizeof(ICONIMAGE) * entryCount);
  memset(icons, 0, sizeof(ICONIMAGE) * entryCount);
  for(int i=0;i<bmpsSz;i++)
    icons[i] = CreateIconImageFromBitmap(bmps[i]);
  ICONDIR icoDir;
  icoDir.idReserved = 0;
  icoDir.idType = 1;
  icoDir.idCount = entryCount;
  FILE* fp = _tfopen(destPath,_T("wb"));
  fwrite(&icoDir.idReserved, sizeof(WORD), 1, fp);
  fwrite(&icoDir.idType, sizeof(WORD), 1, fp);
  fwrite(&icoDir.idCount, sizeof(WORD), 1, fp);
  long entriesPos = ftell(fp);
  int entrySz = 16;
  ICONDIRENTRY* entries = (ICONDIRENTRY*)malloc(sizeof(ICONDIRENTRY) * entryCount);
  memset(entries, 0, sizeof(ICONDIRENTRY) * entryCount);
  for(int i=0;i<entrySz * entryCount;i++)
    fwrite(&entrySz,sizeof(BYTE),1, fp);
  //fseek(fp,entriesPos + entrySz * entryCount,SEEK_SET);
  for(int i=0;i<entryCount;i++)
    //seek to imge offset  
    entries[i].dwImageOffset = ftell(fp);
    entries[i].bWidth = icons[i].Width;
    entries[i].bHeight = icons[i].Height;
    entries[i].bColorCount = icons[i].lpbi->bmiHeader.biClrUsed;
    entries[i].wBitCount = icons[i].Colors;
    entries[i].wPlanes = 1;
    icons[i].lpbi->bmiHeader.biSizeImage = 0;
    //write icon image data    
    fwrite( icons[i].lpBits, icons[i].dwNumBytes, 1, fp);
    entries[i].dwBytesInRes = ftell(fp) - entries[i].dwImageOffset;
  //fix entries
  fseek(fp, entriesPos, SEEK_SET);
  for(int i=0;i<entryCount;i++)
    fwrite(&entries[i].bWidth,sizeof(BYTE), 1, fp);
    fwrite(&entries[i].bHeight,sizeof(BYTE), 1, fp);
    fwrite(&entries[i].bColorCount,sizeof(BYTE), 1, fp);
    fwrite(&entries[i].bReserved,sizeof(BYTE), 1, fp);
    fwrite(&entries[i].wPlanes,sizeof(WORD), 1, fp);
    fwrite(&entries[i].wBitCount,sizeof(WORD), 1, fp);
    fwrite(&entries[i].dwBytesInRes,sizeof(DWORD), 1, fp);
    fwrite(&entries[i].dwImageOffset,sizeof(DWORD), 1, fp);
  for(int i=0;i<bmpsSz;i++)

All Code is freely provided, no guarantees or warranties about its quality, use at your own risk.




Comments (1)

  1. 8:25 am, July 21, 2011Chris Jean 

    It’s awesome that you posted this not too long ago. I’ve been working on a PHP library to create ICO files. In all the documentation I found, I never saw a good explanation of the opacity mask format. With your code, I was able to finally understand it.


Pingbacks (2)

  1. 2:58 am, June 6, 2011Favicon of chironexsoftware.comRead .ICO format.