OPTIMIZING DYNAMIC 8-BIT PNG’S IN C# AND ASP.NET MVC
Programmatically generating images to be served real-time over the web can be useful for many scenarios, such as CAPTCHA’s, watermarks, thumbnail generation, or complicated layouts and effects that may not be possible in a web browser or client application otherwise. In such cases, especially when dealing with a large amount of traffic it is critical to do this as efficiently as possible while reducing the amount of data sent to the client, especially when sending the output to mobile devices. Generating images in .Net is pretty easy using the System.Drawing namespace. Using the System.Drawing.Bitmap class, one can load up a background image, get a Graphics object and use it’s DrawString method to render some text and it’s DrawImage method to layer additional images. I have put together a demo application that goes through a few of the possible approaches which I will walk through.
Here is a simple example that we will use to dynamically generate an in memory Bitmap consisting of a background image, an icon layered on top, and some text centered on the background:
public Bitmap GenerateDemoImage(string path, string iconName, string text) { var background = new Bitmap(path + "background.png"); var icon = new Bitmap(path + iconName); var rect = new Rectangle(1, 1, background.Width, background.Height); var solidBrush = new SolidBrush(Color.WhiteSmoke); var font = new Font("Arial", 24, FontStyle.Bold, GraphicsUnit.Pixel); var stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; var g = Graphics.FromImage(background); g.TextRenderingHint = TextRenderingHint.AntiAlias; g.DrawString(text, font, solidBrush, rect, stringFormat); g.DrawImage(icon, 10, 12); g.Dispose(); icon.Dispose(); font.Dispose(); return background; }
The Bitmap class makes it easy to save the result in a bunch of common formats including PNG, but it doesn’t offer an easy mechanism to optimize the size of the PNG and to reduce the bit depth and amount of colors used, a process called quantization. By reducing the colors to 256 or less you can easily reduce the size of the PNG by half or more in many cases, depending on the type of image and colors used.
After some research looking for a fully managed .Net solution I came across an Octree Quantizer in the Mono port of the Paint.Net source code. Some initial testing and benchmarking showed that while this worked it wasn’t terribly efficient and the output quality wasn’t the greatest. When comparing the generated image quality to the quality output by a popular commercial image editor you quickly realize the difference was significant.
Continuing my search I ended up trying the open source library FreeImage. The FreeImage project has a .Net wrapper that makes interacting with it using native .Net Bitmaps and Streams pretty easy. The free image library has a ColorQuantize method that quantizes a high-color 24-bit bitmap to an 8-bit palette color bitmap. The method has two possible algorithms to choose from, the Xiaolin Wu color quantization algorithm and a NeuQuant neural-net quantization algorithm. The two algorithms take different approaches with a tradeoff between image quality and computational requirements.
With the introduction of Microsoft’s Windows Presentation Foundation, or WPF, some new graphics libraries were introduced, specifically the FormatConvertedBitmap in the System.Windows.Media.Imaging namespace found in PresentationCore.dll The WPF libraries originally had a bug that prevented me from using them in an IIS application but has subsequently been fixed. The WPF imaging libraries are a bit tougher to use and since my original image rendering was done using the older GDI+ based System.Drawing libraries there is some additional overhead to marshal between the different formats but if you can’t use third party libraries and need a pure “.Net” solution, then this can be useful.
The demo application I have put together dynamically generates a fake weather forecast image for illustration purposes, showing the different results of the different algorithms. The application is a simple ASP.NET MVC app that lets you choose an icon and text string, which gets passed to a controller that returns the actual PNG images for the color reducer given.
A simple interface is defined that takes the Bitmap generated in the example above and returns a MemoryStream containing the actual PNG bytes:
public interface IPngColorReducer { MemoryStream ReduceColorDepth(Bitmap bmp); }
There are four classes in the demo that implement the interface, they are OtreeManagedPngColorReducer, WPFPngColorReducer, FreeImageStandardPngColorReducer and FreeImageNeuralNetPngColorReducer.
Here is the implementation of the OctreeManagedPngColorReducer class:
public class OctreeManagedPngColorReducer : IPngColorReducer { private static readonly ImageFormat ImageFormat = ImageFormat.Png; private const int DitherLevel = 4; private const int MaxColors = 255; private const int ColorDepthBits = 8; public MemoryStream ReduceColorDepth(Bitmap bmp) { var memstream = new MemoryStream(); var quantizer = new OctreeQuantizer(MaxColors, ColorDepthBits); quantizer.DitherLevel = DitherLevel; using (Bitmap quantized = quantizer.Quantize(bmp)) { quantized.Save(memstream, ImageFormat); } memstream.Position = 0; return memstream; } }
The FreeImageStandardPngColorReducer class:
public class FreeImageStandardPngColorReducer : IPngColorReducer { public MemoryStream ReduceColorDepth(Bitmap bmp) { // convert image to a 'FreeImageAPI' image MemoryStream ms; using (var fiBitmap = FreeImageAPI.FreeImageBitmap.FromHbitmap(bmp.GetHbitmap())) { //uses the FIQ_WUQUANT Xiaolin Wu color quantization algorithm fiBitmap.ConvertColorDepth(FreeImageAPI.FREE_IMAGE_COLOR_DEPTH.FICD_08_BPP); ms = new MemoryStream(); fiBitmap.Save(ms, FreeImageAPI.FREE_IMAGE_FORMAT.FIF_PNG, FreeImageAPI.FREE_IMAGE_SAVE_FLAGS.PNG_Z_DEFAULT_COMPRESSION); } ms.Position = 0; return ms; } }
The FreeImageNeuralNetPngColorReducer class:
public class FreeImageNeuralNetPngColorReducer : IPngColorReducer { public MemoryStream ReduceColorDepth(Bitmap bmp) { MemoryStream ms; // convert image to a 'FreeImageAPI' image using ( var fiBitmap = FreeImageAPI.FreeImageBitmap.FromHbitmap(bmp.GetHbitmap())) { if (fiBitmap.ColorDepth > 24) { fiBitmap.ConvertColorDepth( FreeImageAPI.FREE_IMAGE_COLOR_DEPTH.FICD_24_BPP); } //quantize using the NeuQuant neural-net quantization algorithm fiBitmap.Quantize(FreeImageAPI.FREE_IMAGE_QUANTIZE.FIQ_NNQUANT, 256); ms = new MemoryStream(); fiBitmap.Save(ms, FreeImageAPI.FREE_IMAGE_FORMAT.FIF_PNG, FreeImageAPI.FREE_IMAGE_SAVE_FLAGS.PNG_Z_DEFAULT_COMPRESSION); } ms.Position = 0; return ms; } }
The WPFPngColorReducer has a little more going on as it to first convert the System.Drawing.Btimap to a stream to be read into the newer WPF based System.Windows.Media.Imaging.BitmapImage:
/// <summary> /// Reduces color depth using the Windows Presentation Foundation classes /// in System.Windows.Media.Imaging using a FormatConvertedBitmap /// </summary> public class WPFPngColorReducer : IPngColorReducer { public MemoryStream ReduceColorDepth(Bitmap bmp) { var ms = new MemoryStream(); bmp.Save(ms, ImageFormat.Png); var bi = new BitmapImage(); bi.BeginInit(); bi.StreamSource = ms; bi.EndInit(); var newFormatedBitmapSource = new FormatConvertedBitmap(); newFormatedBitmapSource.BeginInit(); newFormatedBitmapSource.Source = bi; var myPalette = new BitmapPalette(bi, 256); newFormatedBitmapSource.DestinationPalette = myPalette; //Set PixelFormats newFormatedBitmapSource.DestinationFormat = PixelFormats.Indexed8; newFormatedBitmapSource.EndInit(); var pngBitmapEncoder = new PngBitmapEncoder(); pngBitmapEncoder.Interlace = PngInterlaceOption.Off; pngBitmapEncoder.Frames.Add(BitmapFrame.Create(newFormatedBitmapSource)); var memstream = new MemoryStream(); pngBitmapEncoder.Save(memstream); memstream.Position = 0; return memstream; } }
Outputting the Dynamic Image from a Controller
The index view of the DemoController takes the submitted options and loops through each of the different PngColorReducer classes and uses Html.RenderPartial to render a simple control with an img tag in it. The control takes a simple model class, ImageModel as follows:
public class ImageModel { public int Id { get; set; } public string Icon { get; set; } public string Text { get; set; } }
And the content of the control, DynamicImage.ascx:
<%@ Control Language="C#" Inherits="ViewUserControl<DynamicImageDemo.Models.ImageModel>" %> <img src='<%= Url.RouteUrl(new { controller = "Image", action = "Show", Model.Id, Model.Icon, Model.Text })%>' />
The DynamicImage control passes the arguments to the url of the ImageController which does the actual image generation and returns the content in a FileStreamResult:
public class ImageController : Controller { // GET: /Image/Show/ [AcceptVerbs(HttpVerbs.Get)] [OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] public FileResult Show(ImageModel imageModel) { var imageGenerator = new ImageGenerator(); MemoryStream pngStream; using ( var bitmap = imageGenerator.GenerateDemoImage( Server.MapPath("~/Images/"), imageModel.Icon, imageModel.Text)) { IPngColorReducer colorReducer = PngColorReducerFactory.GetReducer((PngColorReducers) imageModel.Id); pngStream = colorReducer.ReduceColorDepth(bitmap); } return new FileStreamResult(pngStream, "image/png"); } }
Output Quality and Size
Here is a comparison of the image quality output by each (200% zoom):

The demo app SizeController gets the size of each of the final images in bytes and displays them. The size of each of the different reducers is as follows:
Reduced images sizes: Unreduced size was 16844 bytes. OctreeManagedPngColorReducer size was 5205 bytes. WPFPngColorReducer size was 8064 bytes. FreeImageStandardPngColorReducer size was 4557 bytes. FreeImageNeuralNetPngColorReducer size was 5828 bytes.
Performance
I profiled the performance of each of the different routines using dotTrace. After a warm up run, taking a snapshot of the Size controller shows the time taken for each of the reducers:

Conclusion
The basic testing performed shows that using the FreeImageBitmap.Quantize method with the neural-net quantization algorithm gives the best image quality with very good performance.
The source code for the demo application: download
