Friday, August 06, 2004
« Web Project Code Templates...er, Project... | Main | I Figured We'd Configure the .Config »

I ran into something today that had me stumped for a little bit.  I'm developing a website that streams images down to the client.  These images originate from a SQL Server database and are context sensitive as well as user-definable.  I don't yet have my image caching implemented, but that doesn't really play into this discussion.

I have set up an HttpHandler mapped to my custom extension (.dsix) which gets intercepted by aspnet_isapi to be processed (can't wait for Whidbey in which we'll get handlers out of the box for the .asix extension - active server image files :-)  My HttpHandler resolves some HttpContext sensitive details about the request and then proceeds to acquire the image bits from the database.

Now before anyone jumps down my throat, these images are very small (6k-8k) and yes, I'm storing them in memory on the server.  I'm not concerned about this as a matter of fact because 1) my site has very few users (5-15) and 2) there is only 1 image per user in memory.  Should the site get to be more demanding I'll refactor and reevaluate the storage.

Ok, now to the issue...

I created a SqlDataReader object to read my values from the database and could successfully load the image from the database with the following code:

if ( !dr.IsDBNull(USERSTATE_PORTRAIT) ) {
  
long bufferLength = dr.GetBytes(USERSTATE_PORTRAIT, 0, null
, 0, 0);
  
byte[] buffer = new byte
[bufferLength];
   dr.GetBytes(USERSTATE_PORTRAIT, 0, buffer, 0, (int)bufferLength);

  
using ( MemoryStream ms = new MemoryStream(buffer) ) {
      _portrait = 
Bitmap.FromStream(ms) as Bitmap;
  
}
}

This information was later cached.  When it came time to render the image, I used the following code, where res is a reference to the HttpResponse:

user.Portrait.Save(res.OutputStream, user.Portrait.RawFormat);

I had stored a PNG file in the database.  Upon making the call to Image.Save, I would receive a System.Runtime.InteropServices.ExternalException.  The message was 'A generic error occurred in GDI+'.  Well thanks!  At least is wasn't a specific error!

I did a little digging and found an interesting article in Microsoft support about how Bitmaps need access to their originating stream, file, etc.  It turns out that GDI+ may arbitrarily choose to discard the memory occupied by a bitmap, only to rehydrate it when needed later.  For this reason, the source stream or file gets locked.  Aha! I wish I had understood that behavior several years ago when I really needed it.  Well, as it turns out, I don't still have the MemoryStream from which the image was created just hanging around when it comes time to render the image.  So what I have to do is make a copy of the image, and dispose of the original:

...
using ( MemoryStream ms = new MemoryStream(buffer) ) {
   // see Q814675 - PRB: Bitmap and Image Constructor Dependencies for details
  
Bitmap orig = Bitmap.FromStream(ms) as Bitmap;
   _portrait = getPortraitImage(orig);
   orig.Dispose();
}
...

private Bitmap getPortraitImage(Bitmap original) {
   Bitmap ret = new Bitmap(original.Width, original.Height, PixelFormat.Format24bppRgb);
   // copy the original image into the new one
  
using ( Graphics g = Graphics.FromImage(ret) ) {
      g.DrawImage(original, 0, 0);
   }
   return ret;
}

Now, in reality, my actual code is a little different than what I've shown - I've trimmed this down a bit for simplicity.  What you'd probably want to do that I've not shown is check the color depth of the original image and create an image of the same type rather than defaulting to a 24bit image.  Now the HttpHandler might look something like this:

user.Portrait.Save(res.OutputStream, ImageFormat.Jpeg);

Works like a champ!

Friday, August 06, 2004 5:53:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback