Download Source Code: FileIconExtractor.zip - 86.71KB
| First text from a series of articles on Icon Extraction. Implementation of a base FileIcon class, that loads an ICO file and represents in memory the internal structure of a group of icons. Individually select embedded icon images and save them into new ICO files, with single images. | ||
Overview
You may sometimes need to use icons for your applications, and instead of creating new images from scratch, you may try to see what is freely available, on the Internet or stored on your local computer. Icons are small images stored in ICO files, embedded in executable files as resources, archives in icon libraries or concatenated into bitmap image bars.
We'll focus here only on ICO files, and we'll gradually cover all other possible places where icons can "hide". An ICO file is actually a group of at least one icon image. Icon images stored in a ICO file have different sizes and a maximum number of colors, which gives you the image type. Each icon image is stored as a bitmap.
Now, the icon image support in .NET should be available, you say. Unfortunately, the System.Drawing.Icon class maps only to one icon image, and doesn't know how to handle image group files. When you load a ICO file into a Icon object, only the first image from the group is picked up and loaded. All others are simply ignored.
This is also how some image editors, with limited capabilities and support for composite images, act. Load a ICO file in Microsoft Paint, and you get access only to one icon. There may be more, but Paint doesn't know how to handle this. You need a more advanced editor or Visual Studio (and not one of the Express editions!), to get the actual composition of a ICO file.
File managers, like Windows Explorer, will also represent a ICO file with the first icon image from that group of icons. Your browser will also pick-up one single icon from the ICO file. Which one? you may ask. It depends. Some applications try to locate the icon with the best definition, or most appropriate size for that view. For instance, in Windows Explorer it makes sense to show a 16x16 icon for List or Detail View, and a 32x32 icon for List View.
Here are the eight icon images embedded in the icon group file PDXFile.ico, that comes with the demo project (and is copyright by Adobe):
| colors \ size: | 16x16 | 32x32 | 48x48 |
| 16 | |||
| 256 | |||
| 16777216 |
Look how your browser chose to render PDXFile.ico:
.
Surprinsingly enough, our tests show that Internet Explorer and Firefox choose different
images to display. Explorer chose the 32x32, 16 colors image, while Firefox picked-up
the 16x16, 16777216 colors image. Different resolutions and, more important, different
sizes! Hard for us to determine a width and height, to specify in our HTML IMG tag.
We specified 32 and 32, so the Firefox 16x16 image will be zoomed...
Unfortunately, there is no way to specify an image from a group of icons to a browser, or other external application. You need to extract that image into a separate ICO file. This would be a new group of icons with one single image, and there will be no confusion which actual image will be later chosen by a browser, Window Explorer or other program.
Icon File Format
Icon images have usually the same width and height. Values for their size and the maximum number of colors are rather standard, power of 2. Standard values for the width and height are: 16x16, 32x32, 48x48, 64x64, 96x95. Standard number of maximum colors are: 2 (for monochrome), 16, 256 and 16777216 (also a power of 2).
As we said, the .NET System.Drawing.Icon class has limited capabilities for icon groups, so we'll get back to the old Win32 data structures, and find out there are several possible representations for a raw icon group and its embedded images.
Each ICO file starts with a header, that can fit into a MEMICONDIR structure. It is followed by a number of fixed-length MEMICONDIRENTRY or FILEICONDIRENTRY structures, one for each icon image stored in the file. After this metadata storage area comes actual bitmap data, as continuous areas of bytes, one for each stored icon image, in the same order as their previous header entries.
While MEMICONDIR.wCount gives you the number of images, and all header structures have fixed length, it's very easy to find the offset to the first data storage area. Combine this information with the image offset value and the number of bytes, given by dwImageOffset and dwBytesInRes, from an entry header, and there is no problem to locate anything in the file.
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct MEMICONDIR
{
public ushort wReserved;
public ushort wType;
public ushort wCount;
public MEMICONDIRENTRY arEntries;
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct MEMICONDIRENTRY
{
public byte bWidth;
public byte bHeight;
public byte bColorCount;
public byte bReserved;
public ushort wPlanes;
public ushort wBitCount;
public uint dwBytesInRes;
public ushort wId;
}
[StructLayout(LayoutKind.Sequential)]
public struct FILEICONDIRENTRY
{
public byte bWidth;
public byte bHeight;
public byte bColorCount;
public byte bReserved;
public ushort wPlanes;
public ushort wBitCount;
public uint dwBytesInRes;
public uint dwImageOffset;
}Why two different types of structures for an image header, that look so similar? The difference is in the last field, because MEMICONDIRENTRY.wId and FILEICONDIRENTRY.dwImageOffset hold different information.
MEMICONDIRENTRY should be used only when the icon group is stored as a resource, in an executable file. In this case, wId holds the ID (name or identification numer) of the resource. When you load icon images from ICO files, or save them in ICO files, use FILEICONDIRENTRY. Remark that dwImageOffset is somehow redundant, as long as you already have other fields that give you the location and offset of each icon image.
FileIcon Class
In .NET, we created a FileIcon class, used to represent both a group of icons or an icon image, based on the IsGroup read-only property. Icons property will return an empty array of images for an icon image, while the FILEICONDIRENTRY structure, Width and Height, will not be used only for icon groups.
This is acceptable, as long as icons are very small images and do not take a lot of memory. You always instantiate a FileIcon with the constructor that takes just a file name as parameter. The other constructor, with file name and binary reader, is called from the first constructor for each embedded image. We didn't use more protective accessors, because we intend to later use FileIcon as a base class, for icons extracted from other kinds of repositories.
/// <summary>
/// Constructor of a group of icons
/// </summary>
/// <param name="filename"></param>
public FileIcon(string filename)
{
_filename = filename;
// loads all file content in intermediate buffer
using (FileStream stream = new FileStream(
filename, FileMode.Open, FileAccess.Read))
{
_buffer = new byte[stream.Length];
stream.Read(_buffer, 0, _buffer.Length);
}
// creates objects for each embedded icon image from the group
using (BinaryReader reader
= new BinaryReader(new MemoryStream(_buffer)))
{
reader.ReadUInt16();
reader.ReadUInt16();
_icons = new FileIcon[reader.ReadUInt16()];
// reads header info for each icon image
for (int i = 0; i < _icons.Length; i++)
_icons[i] = new FileIcon(_filename, reader);
// reads actual content data for each icon image
for (int i = 0; i < _icons.Length; i++)
reader.Read(_icons[i]._buffer, 0,
_icons[i]._buffer.Length);
}
}
/// <summary>
/// Constructor of an embedded icon
/// </summary>
/// <param name="filename"></param>
/// <param name="reader"></param>
public FileIcon(string filename, BinaryReader reader)
{
_filename = filename;
_fi.bWidth = reader.ReadByte();
_fi.bHeight = reader.ReadByte();
_fi.bColorCount = reader.ReadByte();
_fi.bReserved = reader.ReadByte();
_fi.wPlanes = reader.ReadUInt16();
_fi.wBitCount = reader.ReadUInt16();
_fi.dwBytesInRes = reader.ReadUInt32();
_fi.dwImageOffset = reader.ReadUInt32();
_colors = (long)Math.Pow(2, _fi.wBitCount == 32
? (double)24 : (double)_fi.wBitCount);
_buffer = new byte[_fi.dwBytesInRes];
}You can now call Save on a group of icon or a single icon image. In second case, you actually extract an icon image from a group, and save it separately, into a new ICO file. Save calls internally some Write methods, for a file stream. First Write method is saving all data headers and data, for a group or image. Second method overload is called for each icon image header. Last method overload is saving actual icon image data, internally stored in a _buffer:
/// <summary>
/// Write current icon content to a stream of bytes
/// </summary>
/// <param name="stream"></param>
protected void Write(Stream stream)
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
// writes group header info
writer.Write((ushort)0);
writer.Write((ushort)1);
writer.Write((ushort)(_icons == null ? 1 : _icons.Length));
// writes header info
if (_icons == null)
Write(writer, 3 * Marshal.SizeOf(typeof(ushort))
+ Marshal.SizeOf(typeof(FILEICONDIRENTRY)));
else
for (int i = 0; i < _icons.Length; i++)
{
int offset = 3 * Marshal.SizeOf(typeof(ushort))
+ _icons.Length
* Marshal.SizeOf(typeof(FILEICONDIRENTRY));
for (int j = 0; j < i; j++)
offset += _icons[j]._buffer.Length;
_icons[i].Write(writer, offset);
}
// writes actual icon image data
if (_icons == null)
Write(writer);
else
for (int i = 0; i < _icons.Length; i++)
_icons[i].Write(writer);
}
}
/// <summary>
/// Write the directory entry/image data to the stream.
/// </summary>
protected virtual void Write(BinaryWriter writer, int offset)
{
writer.Write(_fi.bWidth);
writer.Write(_fi.bHeight);
writer.Write(_fi.bColorCount);
writer.Write(_fi.bReserved);
writer.Write(_fi.wPlanes);
writer.Write(_fi.wBitCount);
writer.Write(_fi.dwBytesInRes);
writer.Write((uint)offset);
}
/// <summary>
/// Write the directory entry/image data to the stream.
/// </summary>
unsafe protected void Write(BinaryWriter writer)
{
fixed (byte* buffer = &_buffer[0])
{
BITMAPINFO* bitmapinfo = (BITMAPINFO*)buffer;
uint size = bitmapinfo->bmiHeader.biSizeImage;
bitmapinfo->bmiHeader.biSizeImage = 0;
writer.Write(_buffer);
bitmapinfo->bmiHeader.biSizeImage = size;
}
}Extract All Images from Icon Files
Our project demo comes with several ICO files in the bin\Debug directory. The Main method will simply look for all ICO files in this folder, load them in FileIcon objects, and extract each embedded icon in a separate file, in a new folder with name derived from the original icon group file name:
// For each icon file in execution directory
foreach (FileInfo file in new DirectoryInfo(
Directory.GetCurrentDirectory()).GetFiles("*.ico"))
{
// (Re)create icon extraction folder
// = icon file name + "_files" suffix
string dir = Path.Combine(
Directory.GetCurrentDirectory(), file.Name + "_files");
if (Directory.Exists(dir))
Directory.Delete(dir, true);
Directory.CreateDirectory(dir);
// Save group icon itself
FileIcon icon = new FileIcon(file.FullName);
Debug.WriteLine(file.Name);
icon.Save(Path.Combine(dir, file.Name));
// Save individual icons from group
foreach (FileIcon subicon in icon.Icons)
{
Debug.WriteLine('\t'
+ Path.GetFileName(subicon.FileName));
subicon.Save(Path.Combine(
dir, Path.GetFileName(subicon.FileName)));
}
}You can imagine other use case scenarios, and look only for icons with specific resolution and size. What is important, you have now a simple method to extract and save individual icon images from a ICO file.