Icon Files in c# – Part 2. Writing icon files

Welcome to part 2 of my c# icon file handling code. This one is about my IconFileWriter.

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

To be honest, I didn’t spend much time on this. I spent far more time on the part of my application that calls this code. I probably didn’t take much more than 10 minutes on the actual code presented here. One decision I made up front was to support only PNG format icons. So don’t use this code to overwrite icon files if you want to preserve the existing format. I don’t care about older formats, so I only write PNG format. If you search online, you’ll find many confused developers who can write other formats, but can’t figure out how to save icon files in PNG format. Feel free to modify the code to save in other formats if you wish.

The code uses the same structures from part 1, so I won’t repeat those. I did add async methods to save icon files. They use extension methods, defined elsewhere in the solution, that save the files asynchronously, using asynchronous IO.

Using the code is simple. In fact, I won’t even explain it. Here is an extension method that uses it to save a collection of images to an icon file:

  1. /// <summary>Save a collection of images to an icon file.</summary>
  2. /// <remarks>The images should preferably be different size representations
  3. /// of the same object. They should be in PNG format (they will be converted
  4. /// to PNG format otherwise), not larger than 256×256 (they will be scaled
  5. /// down otherwise). If the images contain more than one of the same resolution,
  6. /// only the first will be used.</remarks>
  7. /// <param name=”images”>The images to save to .ICO file.</param>
  8. /// <param name=”iconFilename”>The full path to the file, which will
  9. /// be created if it does not exist, otherwise overwritten.</param>
  10. public static void SaveIconFile(this IEnumerable<Image> images, string iconFilename)
  11. {
  12.     var writer = new IconFileWriter();
  13.     writer.Images.AddRange(images);
  14.     writer.Save(iconFilename);
  15. }

 

And here is the source for the IconFileWriter itself. (All it does is write the icon files using the predefined structures. I shouldn’t need to explain that.)

Btw, you may have noticed my SaveAsync method uses a method called Stream.CopyToStreamAsync. This is not the extension method from the Parallel Extension Extras Microsoft sample project. It is my own extension method that’s very similar to the built in CopyToAsync, except it enforces a particular small buffer size. (You have it if you downloaded the zip file.)

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.IO;
  5. using System.Runtime.InteropServices;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8.  
  9. namespace Romy.Core
  10. {
  11.     /// <summary>Saves a collection of images to an icon file. Only PNG images are supported.</summary>
  12.     /// <remarks><para>
  13.     /// This is my first implementation of an icon file writer. It just does the basics; and doesn’t do any validation
  14.     /// that the images you add to the collection before trying to save are actually PNG files (but will convert other
  15.     /// images anyway).</para>
  16.     /// <para>
  17.     /// Because this will convert all images to be saved to PNG format (32-bit with alpha channel), this <i>should not
  18.     /// be used to save different pixel format images of the same resolution to the same icon file</i>. For example,
  19.     /// if you try to save two 32×32 images to the same icon file, one of which is 32BPP and one is 8BPP, only one of
  20.     /// them will be saved. (whichever one is first) This may not be the better quality image.</para></remarks>
  21.     public class IconFileWriter
  22.     {
  23.         #region Fields
  24.  
  25.         List<Image> images = new List<Image>();
  26.  
  27.         #endregion Fields
  28.  
  29.         #region Properties
  30.  
  31.         public IList<Image> Images
  32.         {
  33.             get { return images; }
  34.         }
  35.  
  36.         #endregion Properties
  37.  
  38.         #region Methods
  39.  
  40.         #region Public Methods
  41.  
  42.         public void Save(string path)
  43.         {
  44.             using (var stream = new MemoryStream())
  45.             {
  46.                 SaveToStream(stream);
  47.                 stream.Position = 0;
  48.  
  49.                 using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, (int)stream.Length, false))
  50.                 {
  51.                     stream.WriteTo(fileStream);
  52.                 }
  53.             }
  54.         }
  55.  
  56.         public async Task SaveAsync(string path)
  57.         {
  58.             using (var stream = new MemoryStream())
  59.             {
  60.                 SaveToStream(stream);
  61.                 stream.Position = 0;
  62.  
  63.                 using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, (int)stream.Length, true))
  64.                 {
  65.                     await stream.CopyToStreamAsync(fileStream);
  66.                 }
  67.             }
  68.         }
  69.  
  70.         #endregion Public Methods
  71.  
  72.         #region Private Methods
  73.  
  74.         private void SaveToStream(Stream stream)
  75.         {
  76.             using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
  77.             {
  78.                 // Remove any duplicated resolutions. Done first because this may change the image count.
  79.                 ValidateImages();
  80.  
  81.                 var imageData = new Dictionary<IconEntry, byte[]>();
  82.  
  83.                 // Write header
  84.                 new IconHeader
  85.                 {
  86.                     Reserved = 0,
  87.                     Type = 1,
  88.                     Count = Convert.ToInt16(images.Count)
  89.                 }.Save(writer);
  90.  
  91.                 // The offset of the first icon.
  92.                 var offset = Marshal.SizeOf(typeof(IconHeader)) + images.Count * Marshal.SizeOf(typeof(IconEntry));
  93.  
  94.                 // Write all the icon entries
  95.                 for (var i = 0; i < images.Count; i++)
  96.                 {
  97.                     var image = images[i] as Bitmap;
  98.  
  99.                     var data = image.ToArray(); // An extension method that saves an Image to a png-format byte array.
  100.  
  101.                     var entry = new IconEntry
  102.                     {
  103.                         Width = image.Width < 256 ? Convert.ToByte(image.Width) : (byte)0,
  104.                         Height = image.Height < 256 ? Convert.ToByte(image.Height) : (byte)0,
  105.                         ColorCount = 0,
  106.                         Reserved = 0,
  107.                         Planes = 1,
  108.                         BitCount = 32,
  109.                         BytesInRes = data.Length,
  110.                         ImageOffset = offset
  111.                     };
  112.  
  113.                     imageData[entry] = data;
  114.                     entry.Save(writer);
  115.  
  116.                     offset += data.Length;
  117.                 }
  118.  
  119.                 // Write the Icons.
  120.                 foreach (var kvp in imageData)
  121.                 {
  122.                     writer.Seek(kvp.Key.ImageOffset, SeekOrigin.Begin);
  123.                     writer.Write(kvp.Value);
  124.                 }
  125.             }
  126.         }
  127.  
  128.         private void ValidateImages()
  129.         {
  130.             var contained = new List<int>();
  131.  
  132.             var validatedImages = new List<Image>();
  133.  
  134.             // Make sure there are not multiple images of the same resolution
  135.             for (var i = 0; i < images.Count; i++)
  136.             {
  137.                 var image = images[i] as Bitmap;
  138.  
  139.                 /* Images larger than 256×256 will create invalid
  140.                  * icons, so resize any image that’s too large. */
  141.                 if (image.Width > 256 || image.Height > 256)
  142.                 {
  143.                     image = image.Resize(256, 256) as Bitmap; // Resize is an extension method.
  144.                 }
  145.  
  146.                 if (!contained.Contains(image.Width))
  147.                 {
  148.                     contained.Add(image.Width);
  149.                     validatedImages.Add(image);
  150.                 }
  151.             }
  152.  
  153.             images.Clear();
  154.             images.AddRange(validatedImages);
  155.         }
  156.  
  157.         #endregion Private Methods
  158.  
  159.         #endregion Methods
  160.     }
  161. }

Enjoy!

Update: I just noticed that the Images property on IconFileWriter is of type IList<Image>, which means the AddRange method in my example of how to use it doesn’t exist out of the box – that’s my own extension method as well, so I’ll include it here. (There are loads of my own extension methods used by my code. I’m not going to try to add all of them, or else I’ll be playing this game all day. You have them if you downloaded the zip file anyway.)

  1. /// <summary>AddRange extension on IList&lt;T&gt;.</summary>
  2. public static void AddRange<T>(this IList<T> list, IEnumerable<T> collection)
  3. {
  4.     var typedList = list as List<T>;
  5.  
  6.     if (typedList != null)
  7.     {
  8.         /* If list is actually a List<T> that was cast
  9.          * to IList<T>, call its AddRange method directly. */
  10.         typedList.AddRange(collection);
  11.     }
  12.     else
  13.     {
  14.         foreach (var item in collection)
  15.         {
  16.             list.Add(item);
  17.         }
  18.     }
  19. }
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