Say goodbye to GDI on the web
Posted on January 22, 2009
I've been playing around with Windows 7 over the past couple of weeks. It's really impressive that Microsoft have seemingly managed to get all of the nice frilly bits of Vista into an OS that is as fast and usable in day-to-day life as XP. One of features that's new is Libraries which present a customised view of their content, which means a Library of documents can look quite different to a Library of music. Ars Technica have a nice article which cover the basics and gives an overview of how Libraries work.
What particularly caught my eye was how smartly a Library of photos is presented. The initial thumbnail preview of each library contains three photos arranged in a 3D 'stack' with a nice border and some shadows.
I began to think about how I could mimic this kind of functionality with my own picture gallery. The system currently uses GDI+ to resize and manipulate images on-the-fly which are then cached to disk. The whole process is surprisingly decent in terms of performance and resource usage, but GDI+ has a lot of limitations when you want to do more than just the basics. Adobe Flash could be suitable, but then again I think I'd rather stick pins in my eyes.
Windows Presentation Foundation is for all intents and purposes, Microsofts replacement for GDI and GDI+. It's powering Silverlight and a whole host of applications in Windows 7, and gives .NET developers a nice easy way to do some very cool things without having to grow a beard and learn Direct3D. Through Silverlight, WPF is becoming more commonplace on the web but for me that's one of the biggest drawbacks - the need for a browser plug-in and all of the problems that come along for the ride (platform compatibility and client resource usage for example). For things like video playback and animation it's unavoidable, at least until HTML5. But for something as simple as a picture album preview...?
So I did a bit of digging and I was pleased to discover that everything you can do in Silverlight, you can do in regular .NET code, without writing a single line of XAML or needing to use the Silverlight browser plug-in. After a few hours I had some code doing roughly what I wanted to achieve, and this is the end result:
It's the Windows 7 Libraries look on the web! I made heavy use of two excellent articles on the subject of using WPF on the web, which you can read here and here. The following is a brief overview of the important bits and a few issues I ran into, although most of it is covered in the two articles linked to.
As described in the latter of those two articles, calling WPF in a non-conventional way (i.e. outside of a WinForms app or Silverlight) creates a minor headache in terms of threads and memory usage. You have to be quite explicit about terminating threads that some of the WPF classes create. I ended up adopting an old-school 'belt and braces' approach to tidying up objects since I didn't want to rely on garbage collection and run the risk of a server meltdown.
Once you have the relevant WPF assemblies referenced by your project (PesentationCore, PresentationFramework and WindowsCore) you can start working with some WPF controls. The process to generate a static image like the one above should be something like:
1. Create a root control which you can place other controls into
2. Build up the image by adding controls with effects, transformations, etc
3. Render a 'screenshot' of the root control with all of its children and save to a suitable stream
When I started working on this I was completely new to WPF and XAML. It's worth reading up on the various WPF controls and how best to use them if you're in the same boat, and being able to work back from some functional XAML is quite useful since 99% of the samples on the web are written in XAML. The following is a very cut-down overview of the code.
//setup the drawing area
Canvas myCanvas = new Canvas();
myCanvas.Background = Brushes.Transparent;
//load up the specified image
System.Windows.Controls.Image baseImage =
new System.Windows.Controls.Image();
byte[] imageBytes;
BitmapSource imageSource = CreateImage(imageBytes, loadWidth, 0);
imageBytes = null;
//configure image control
baseImage.Width = MaxDimension;
baseImage.Height = newHeight;
/* ... */
//explicitly set the canvas dimensions since it
//can't be trusted to calculate its own
myCanvas.Width = calculatedWidth;
myCanvas.Height = calculatedHeight;
//ensure child controls layout
myCanvas.Measure(new Size(myCanvas.Width, myCanvas.Height));
myCanvas.Arrange(new Rect(new Size(myCanvas.Width, myCanvas.Height)));
byte[] output;
JpegBitmapEncoder jpgEncoder = null;
RenderTargetBitmap renderedCanvas = null;
//render the canvas as a bitmap
renderedCanvas = new RenderTargetBitmap(Width, Height, 96, 96,
PixelFormats.Default);
renderedCanvas.Render(rootCanvas);
jpgEncoder = new JpegBitmapEncoder();
jpgEncoder.Frames.Add(BitmapFrame.Create(renderedCanvas));
//save the output from the encoder to something useful
using (MemoryStream ms = new MemoryStream())
{
jpgEncoder.Save(ms);
output = ms.ToArray();
}
//be very specific about clean up
if (jpgEncoder != null && jpgEncoder.Dispatcher.Thread.IsAlive)
jpgEncoder.Dispatcher.InvokeShutdown();
if (renderedCanvas != null && renderedCanvas.Dispatcher.Thread.IsAlive)
renderedCanvas.Dispatcher.InvokeShutdown();
jpgEncoder = null;
renderedCanvas = null;
The next step is to actually call the WPF code from the web. This isn't as quite as easy as I'd have hoped since you need to create a separate single-threaded apartment thread to use WPF controls. I did this in a very similar way to Shawn Rosewarne in his article.
[STAThread]
private byte[] GetImageStack()
{
try
{
Thread imageStackworker = new Thread(
new ThreadStart(BuildImageStack));
imageStackworker.SetApartmentState(ApartmentState.STA);
imageStackworker.Name = "GetImageStack";
imageStackworker.Start();
imageStackworker.Join();
}
catch (Exception ex)
{
return NiknakV2.Business.Gallery.Drawing.ImageError.
RenderAsByteArray(ex.Message);
}
return _ImageStack;
}
private void BuildImageStack()
{
try
{
NiknakV2.Business.Gallery.Drawing.ImageStack myStack =
new NiknakV2.Business.Gallery.Drawing.ImageStack(this.FullName);
/* ... */
//work some magic
_ImageStack = myStack.Render();
_ImageStackInError = myStack.InError;
}
catch (Exception ex)
{
_ImageStackInError = true;
_ImageStack = NiknakV2.Business.Gallery.Drawing.ImageError.
RenderAsByteArray(ex.Message);
}
}
That's all there is to it really. The most irritating 'feature' of using WPF in this way is that the Canvas control won't set its ActualWidth and ActualHeight properties based on the space that its child controls occupy. In my case I ended up having to use trigonometry to calculate how much extra space my skew transformations were taking up and explicitly set the Canvas dimensions. It's slightly more convoluted than writing XAML but as a pure code alternative, I like it. The performance is strong once the WPF assemblies have loaded, with a few hundred high-res photos being parsed in just a few seconds, even on basic hardware. In a server environment all of the rendering is done in software on the CPU so caching is quite important, but no more important than caching anything done with GDI.
In terms of replicating the Windows 7 Libraries look, I found that supporting portrait images was the only real issue. It's not a problem technically, I just didn't like the look of the mixed orientation images. I also found that getting the images, borders, skews and shadows in the right place, the right size and at the right angle to be quite fiddly. Although it looks right at 100px or so, when it scales up things start to get slightly out of position despite calculating all of the positions and sizes from the overall maximum size specified.
The code is running in full swing in the picture gallery so be sure to have a look. You can also download a slightly more comprehensive code sample but be aware it's not intended to be functioning code, just an example.
I'm quite excited about future uses for WPF on the web. What I've done so far has really only just scratched the surface of what's possible...
Permalink