hackification

Homepage

...rediscover the joy of coding

Using WPF To Generate Web Images

Recently I needed to display rotated graphics within a web-page. Since there's no way to do that cross-browser using CSS, I needed to auto-generate a collection of pre-rotated images that could be displayed as CSS sprites. I've found that WPF (Windows Presentation Foundation) is great for generating batches of images suitable for use in web apps.

Let's start with an example graphic, Arrow.xaml:
<UserControl x:Class="WebImagesGenerator.Graphics.Arrow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Height="64" Width="64">
  <Canvas Width="64" Height="64">
    <Polygon Points="32,4 60,32 42,32 42,60 22,60 22,32 4,32"
      Fill="Red" Stroke="Black"
      StrokeThickness="3" StrokeLineJoin="Round" />
  </Canvas>
</UserControl>
That's just a standard XAML file, nothing fancy there.

Rendering WPF content to an image is really easy; you use the RenderTargetBitmap class. I wanted to be able to produce a strip of images, all the same except rotated, that can be used as CSS sprites. For my actual web application, I have a quick console application that generates the images as a batch. If I ever change the XAML files, I can quickly re-generate all the auto-generated images in my project just be running my console application.

Let's get started on the console application:
[STAThread]
static void Main( string[] args )
{
  var arrow = new Graphics.Arrow();

SaveStrip( arrow, GenerateSeries( 0, 360, 10 ) , "..\\..\\Testing\\arrows.png" ); }
WPF must be run in a [STAThread] context. I create an instance of my graphic, then call my SaveStrip routine. GenerateSeries simply returns an array of integers:
static int[] GenerateSeries( int start, int end, int step )
{
  var series = new List<int>();

for( var v = start; v < end; v += step ) { series.Add( v ); }

return series.ToArray(); }
The real magic happens in SaveStrip:
static void SaveStrip( FrameworkElement fe, int[] angles, string filename )
{
  var outer = new Canvas();

outer.Width = fe.Width * angles.Length; outer.Height = fe.Height;

outer.Children.Add( fe );

outer.Arrange( new Rect( 0, 0, outer.Width, outer.Height ) );

var bmp = new RenderTargetBitmap ( (int) fe.Width * angles.Length , (int) fe.Height, 96, 96, PixelFormats.Pbgra32 );

for( var i = 0; i < angles.Length; ++i ) { var transforms = new TransformGroup();

transforms.Children.Add ( new RotateTransform( angles[i], fe.Width / 2, fe.Height / 2 ) );

fe.RenderTransform = transforms; fe.SetValue( Canvas.LeftProperty, i * fe.Width );

// The following line is VITAL! outer.Arrange( new Rect( 0, 0, outer.Width, outer.Height ) );

bmp.Render( fe ); }

var encoder = new PngBitmapEncoder();

encoder.Frames.Add( BitmapFrame.Create( bmp ) );

using( var stream = File.Create( filename ) ) { encoder.Save( stream ); } }
When I first tried this technique, all my output bitmaps were completely blank. After debugging for a while, I realised that all my graphics were of zero size. You MUST call Arrange() on your elements before calling Render(), otherwise they won't have been laid out properly.

When run, this console app produces the following (reduced in size to fit):

WPF-Generated Images

Fantastic! To get a better idea of how such images might appear in a web-page, I've created a little HTML+jQuery demo to see a very quick example of the images in use:
View Demo