Icon Files in c# – Part 1. Reading icon files

I’ve written my own icon file reader and icon writer in c# (Windows Forms – it reads the icons into System.Drawing.Icon instances), and since the samples I found on it were sparse and mostly buggy, I’m sharing mine.

Again, the source, for this and a whole bunch of other stuff, is here: RomyView.zip

Firstly, don’t trust all code you find online to actually do the job properly. My code is based in principle on an explanation I found a while back on Raymond Chen’s blog, The Old New Thing. (Sorry, I don’t have the exact URL.) I have read his blog every day for several years, and it often contains extremely useful snippets of code. They’re all in C or C++ and my icon reader code looks nothing like his samples, but the reason I say it is based on his explanation is that he makes it very clear, when icons are in PNG format, which supports a max size of 256×256, since the dimensions are each stored as a byte in c#, the maximum of which can be 255, the magic value of zero is used to indicate 256. Before I read his article, I had downloaded and attempted to use the code from two CodeProject articles, both of which crashed when they encountered 256×256 PNG icons.

Though some of my code may well depend on other parts of my solution (especially async extension methods), there are only three files relevant in my solution. I’m not going to explain this in any detail – that shouldn’t be necessary. Firstly, I define the structures necessary:

  1. using System.IO;
  2.  
  3. namespace Romy.Core
  4. {
  5.     internal struct IconHeader
  6.     {
  7.         public IconHeader(BinaryReader reader)
  8.             : this()
  9.         {
  10.             Reserved = reader.ReadInt16();
  11.             Type = reader.ReadInt16();
  12.             Count = reader.ReadInt16();
  13.         }
  14.  
  15.         public short Reserved { get; set; }
  16.  
  17.         public short Type { get; set; }
  18.  
  19.         public short Count { get; set; }
  20.  
  21.         public void Save(BinaryWriter writer)
  22.         {
  23.             writer.Write(Reserved);
  24.             writer.Write(Type);
  25.             writer.Write(Count);
  26.         }
  27.     }
  28.  
  29.     internal struct IconEntry
  30.     {
  31.         /// <summary>This constructor should be called by an IconFileReader.
  32.         /// The IconFileWriter uses a constructor that passes in all values
  33.         /// explicitly.</summary>
  34.         public IconEntry(BinaryReader reader)
  35.             : this()
  36.         {
  37.             Width = reader.ReadByte();
  38.             Height = reader.ReadByte();
  39.             ColorCount = reader.ReadByte();
  40.             Reserved = reader.ReadByte();
  41.             Planes = reader.ReadInt16();
  42.             BitCount = reader.ReadInt16();
  43.             BytesInRes = reader.ReadInt32();
  44.             ImageOffset = reader.ReadInt32();
  45.         }
  46.  
  47.         public byte Width { get; set; }
  48.  
  49.         public byte Height { get; set; }
  50.  
  51.         public byte ColorCount { get; set; }
  52.  
  53.         public byte Reserved { get; set; }
  54.  
  55.         public short Planes { get; set; }
  56.  
  57.         public short BitCount { get; set; }
  58.  
  59.         public int BytesInRes { get; set; }
  60.  
  61.         public int ImageOffset { get; set; }
  62.  
  63.         public void Save(BinaryWriter writer)
  64.         {
  65.             writer.Write(Width);
  66.             writer.Write(Height);
  67.             writer.Write(ColorCount);
  68.             writer.Write(Reserved);
  69.             writer.Write(Planes);
  70.             writer.Write(BitCount);
  71.             writer.Write(BytesInRes);
  72.             writer.Write(ImageOffset);
  73.         }
  74.     }
  75. }

 

A quirk of my implementation (which I shall explain more further on) causes my icons to “lose” the original bit depth. Thus in my reader, the Images property exposed is a collection of a simple interface type shown below. This interface stores the Icon itself, and a variable used to store the bit depth that was lost.

  1. namespace Romy.Core
  2. {
  3.     /// <summary>The collection item for the <see cref=”IconFileReader.Images”/> property.</summary>
  4.     /// <remarks>I made this an interface so that it can easily be extended at the UI level. The
  5.     /// application adds a Modified property (to an interface that inherits this one) to keep track
  6.     /// of user edits.</remarks>
  7.     public interface IIconData
  8.     {
  9.         int BitDepth { get; set; }
  10.  
  11.         System.Drawing.Icon Icon { get; set; }
  12.     }
  13. }

 

Then the actual code of my IconFileReader:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Runtime.InteropServices;
  7.  
  8. namespace Romy.Core
  9. {
  10.     /// <summary>Parses a multi-image icon file (an <b>.ICO</b> file) and
  11.     /// exposes the icons contained therein as separate images.</summary>
  12.     public class IconFileReader
  13.     {
  14.         #region Fields
  15.  
  16.         private IEnumerable<IIconData> images;
  17.  
  18.         private string filename;
  19.  
  20.         #endregion Fields
  21.  
  22.         #region Constructors
  23.  
  24.         public IconFileReader(string filename)
  25.         {
  26.             Filename = filename;
  27.         }
  28.  
  29.         public IconFileReader() { }
  30.  
  31.         #endregion Constructors
  32.  
  33.         #region Properties
  34.  
  35.         public IEnumerable<IIconData> Images
  36.         {
  37.             get { return images; }
  38.         }
  39.  
  40.         public string Filename
  41.         {
  42.             get { return filename; }
  43.             set
  44.             {
  45.                 filename = value;
  46.                 ReadFile(filename);
  47.             }
  48.         }
  49.  
  50.         #endregion Properties
  51.  
  52.         #region Methods
  53.  
  54.         #region Public Methods
  55.  
  56.         public Icon FirstIcon
  57.         {
  58.             get { return images != null ? images.FirstOrDefault().Icon : null; }
  59.         }
  60.  
  61.         #endregion Public Methods
  62.  
  63.         #region Private Methods
  64.  
  65.         private static IconData BuildIconData(Stream source, IconHeader header, IconEntry entry)
  66.         {
  67.             using (var stream = new MemoryStream())
  68.             {
  69.                 using (var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, true))
  70.                 {
  71.                     /* I’m not recreating an icon file here, just one icon at a time in the icon file format.
  72.                      * Therefore write the count as 1, and the offset as sizeof(header) + sizeof(entry). */
  73.                     const short number = 1;
  74.                     var offset = Marshal.SizeOf(typeof(IconHeader)) + Marshal.SizeOf(typeof(IconEntry));
  75.  
  76.                     writer.Write(header.Reserved);
  77.                     writer.Write(header.Type);
  78.                     writer.Write(number);
  79.                     writer.Write((byte)entry.Width);
  80.                     writer.Write((byte)entry.Height);
  81.                     writer.Write(entry.ColorCount);
  82.                     writer.Write(entry.Reserved);
  83.                     writer.Write(entry.Planes);
  84.                     writer.Write(entry.BitCount);
  85.                     writer.Write(entry.BytesInRes);
  86.                     writer.Write(offset);
  87.  
  88.                     var buffer = new byte[entry.BytesInRes];
  89.                     source.Position = entry.ImageOffset;
  90.                     source.Read(buffer, 0, entry.BytesInRes);
  91.                     writer.Write(buffer);
  92.  
  93.                     /* While this shouldn’t always be necessary, the managed Icon type will throw an exception when, for example,
  94.                      * trying to create an icon from any .png image that was just loaded from the file, whether it is 256×256
  95.                      * (0x0 in the entry), or smaller, whereas this way always seems to work. */
  96.                     using (var image = Image.FromStream(stream) as Bitmap)
  97.                     {
  98.                         Icon temp = null;
  99.                         try
  100.                         {
  101.                             temp = Icon.FromHandle(image.GetHicon());
  102.  
  103.                             /* Use the dimensions we got from the GDI icon, so we
  104.                              * don’t have to worry about double-height bitmaps etc.*/
  105.                             return new IconData { Icon = new Icon(temp, temp.Width, temp.Height), BitDepth = entry.BitCount };
  106.                         }
  107.                         finally
  108.                         {
  109.                             if (temp != null)
  110.                                 NativeMethods.DestroyIcon(temp.Handle);
  111.                         }
  112.                     }
  113.                 }
  114.             }
  115.         }
  116.  
  117.         private static IEnumerable<IIconData> ReadStream(Stream stream)
  118.         {
  119.             using (var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, true))
  120.             {
  121.                 var header = new IconHeader(reader);
  122.  
  123.                 var entries = new IconEntry[header.Count];
  124.  
  125.                 for (var i = 0; i < header.Count; i++)
  126.                 {
  127.                     entries[i] = new IconEntry(reader);
  128.                 }
  129.  
  130.                 var imageData = new IconData[header.Count];
  131.  
  132.                 for (var i = 0; i < header.Count; i++)
  133.                 {
  134.                     try
  135.                     {
  136.                         imageData[i] = BuildIconData(stream, header, entries[i]);
  137.                     }
  138.                     catch { }
  139.                 }
  140.  
  141.                 /* Sort the results; by bit-depth, then size; descending, so that the
  142.                  * first icon found will be the largest resolution and highest bit depth. */
  143.                 var bitDepths = imageData.Select(b => b.BitDepth).Distinct().ToList();
  144.                 bitDepths.Sort();
  145.  
  146.                 var result = new List<IIconData>();
  147.  
  148.                 foreach (var i in bitDepths)
  149.                 {
  150.                     result.AddRange(imageData.Where(b => b.BitDepth == i).OrderBy(b => b.Icon.Width));
  151.                 }
  152.  
  153.                 result.Reverse();
  154.  
  155.                 return result;
  156.             }
  157.         }
  158.  
  159.         private void ReadFile(string filename)
  160.         {
  161.             try
  162.             {
  163.                 using (var stream = new MemoryStream(File.ReadAllBytes(filename)))
  164.                 {
  165.                     stream.Position = 0;
  166.                     images = ReadStream(stream);
  167.                 }
  168.             }
  169.             catch (FileNotFoundException) { }
  170.             catch (UnauthorizedAccessException) { }
  171.             catch (IOException) { }
  172.             catch (Exception ex) { ex.Log(); }
  173.         }
  174.  
  175.         #endregion Private Methods
  176.  
  177.         #endregion Methods
  178.  
  179.         #region Nested Classes
  180.  
  181.         private static class NativeMethods
  182.         {
  183.             [DllImportAttribute(“user32.dll”, CharSet = CharSet.Unicode)]
  184.             internal static extern bool DestroyIcon(IntPtr hIcon);
  185.         }
  186.  
  187.         #endregion Nested Classes
  188.     }
  189.  
  190.     /// <summary>The <see cref=”Romy.Core.IconFileReader.Images”/> property contains a collection of this type.</summary>
  191.     /// <remarks>When the icons are converted to managed Icon instances, the original bit depth is lost. This type is used
  192.     /// to store the original bit depth, for display.</remarks>
  193.     public class IconData : IIconData
  194.     {
  195.         #region Properties
  196.  
  197.         public Icon Icon { get; set; }
  198.  
  199.         public int BitDepth { get; set; }
  200.  
  201.         #endregion Properties
  202.     }
  203. }

 

That’s it. Nothing complicated there.

One thing I said I would expand on is the IIconData interface, and why it is needed. What I found, after reading the icon data into a stream, is that it would throw an exception if I tried to instantiate an Icon from the contents of the stream. No matter what I tried, it would fail. But, it did allow me to instantiate an Image. Once it’s an image, it’s then possible to use the Image.GetHIcon method to get an Icon instance, but at that point, though it looks identical to the original icon, the bit depth is lost. What we get is actually a 32 bit icon that looks like the original. (At that point, the icon obtained by GetHIcon must me destroyed with the DestroyIcon Windows API function. We use a copy of that icon.)

But having the interface is quite useful. From the viewer of my application, I created a derived interface, adding a modified flag, which then allowed me to add UI controls to “page” back and forth through the icon images, modifying the separate images, and saving them to a new icon afterwards. That is beyond he scope of this article, but it is in the code if you download the zip file.

Enjoy.

Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s