Dynamic image retrieval with .axd

A lot of my work these days deals with images in web controls; I had an earlier post completed about pulling down portions of an image through JavaScript to piece together a very wide image client-side, but then directions shifted and I never got around to posting it. For the current control we need to request many different images and I'm now using a more efficient method which I'd like to share here.

The 'old' way of doing it was to simply use <img src="page.aspx?id=6" mce_src="page.aspx?id=6"> which wrote the image to the Page's Response context. The 'new' way looks almost identical on the client side <img src="getImage.axd?id=6" mce_src="getImage.axd?id=6"> and there is a only slightly more work required on the server. If you think about the overhead (i.e. many steps required) to parse even an empty aspx, I think it's obvious that axd is the way to go.

There are actually a few built-in axd extensions for ASP.NET 2.0 including WebResource and Trace; they simply redirect to some built-in handler that ships with the framework. The first can be used to retrieve static resources embedded in your control's assembly. However I want to retrieve a dynamically generated image so we'll need to roll our own.

I've already developed the classes that will generate data and a Bitmap object, so there are just two steps remaining.

Step 1: Update the web.config file with the re-direct for the .axd request

     <httpHandlers>
      <add verb="GET" path="getCampaign.axd" 
              type="GanttControl.ImageRequestHandler, GanttControl"/>
    </httpHandlers>

Step 2: Wrap my existing classes into one that implements IHttpHandler.

     class ImageRequestHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            string id = context.Request.QueryString["id"];

            if (id == null || id.Length == 0)
            {
                throw new ArgumentException("An 'id' parameter must be 
                                                           specified");
            }

            CampaignFactory campaignFactory = new CampaignFactory();
            GanttBarFactory factory = new GanttBarFactory(
                                  campaignFactory.CreateWithOneTrend());
            Bitmap bitmap = factory.Create(200, 2);

            context.Response.ContentType = "image/png";
            bitmap.Save(context.Response.OutputStream, ImageFormat.Png);
        }
     }

It all looks good until you run it and see the following exception in your Output window.

 A first chance exception of type 'System.NotSupportedException' 
   occurred in System.Web.dll

A first chance exception of type 'System.Runtime.InteropServices.
   ExternalException' occurred in System.Drawing.dll

Groan. A quick test shows that the Bitmap can be output succesfully as JPEG and GIF, so why is PNG dying? The exception message from System.Drawing is as follows:

 "A generic error occurred in GDI+."

Thank you, Platform team. Thank you sooooo much :) The stack trace doesn't give us any more clues.

    at System.Drawing.Image.Save(Stream stream, ImageCodecInfo encoder, 
        EncoderParameters encoderParams)
   at System.Drawing.Image.Save(Stream stream, ImageFormat format)
   ...

So there's something very strange going on with PNG files. But why? We're only writing to a stream and writing it to a file works perfectly.

Oh, right. Streams aren't the same as files.

A quick check in the debugger of HttpContext.Response.OutputStream shows that it does not support CanRead and CanSeek. Apparently the PNG exporter needs that where GIF and JPEG don't. It's strange, because I remember compressing with libPng and you just passed it data row-by-row or the whole raw chunk at once, but who knows what the GDI+ implementation looks like.

Regardless, the simplest solution is to write it to a MemoryStream first, and then to the OutputStream.

         context.Response.ContentType = "image/png";
        MemoryStream memoryStream = new MemoryStream();

        bitmap.Save(memoryStream, ImageFormat.Png);
        memoryStream.WriteTo(context.Response.OutputStream);