Thursday, 16 September 2010

Using WPF to Render Bitmaps

There are a few posts around the net about this, but a lot of them focus on taking XAML controls or pages, and turning parts of them into bitmaps.

This is fine, but I want to be able to use WPF entirely off-screen in order to create rich graphical content.

What I'm going to present here is a way of doing just that, in a console application just to press home the point.

This is the image that you're going to produce - it's a little bit, well, crap - but it does the job.

wpfinabitmap

So, it's an Ellipse with a RadialGradientFill (off-centre, white-to-blue gradient stops), and a TextBlock in Arial Black/Bold font of around 40px size.

The edges around the ellipse are going to be output as transparent (open up this file in Photoshop or Paint.Net and notice the checkerboard patterns around the edge - useful for web images!

Step 1: Create the project

Open a new Visual Studio, and create a new Console Application Project.  Make sure it targets .Net 3.5.

Add references to:

  • PresentationCore
  • PresentationFramework
  • WindowsBase

These are the core WPF libraries that are going to be required.

Warning - if you're going to be repeating this code in a Windows Forms application, you should be careful - because many of the classes in WPF clash with those in the standard windows forms framework.

Step 2: Basic setup

Now, a quick google or MSDN search will show that the main class you're going to need is called RenderTargetBitmap.  This class has a method called Render, to which you pass a Visual.  Anything that can be displayed in a WPF window is a visual (in fact the window itself is) which means, in theory, that anything you can do in WPF to screen can be done to a bitmap.

To produce the image file you're going to use one of the BitmapEncoder-implementing classes (there are classes for creating PNGs, Gifs, JPGs and more).

Open up Program.cs file and add the following usings:

using System.Windows.Media.Imaging;

using System.Windows.Media;

using System.Windows.Controls;

using System.Windows.Shapes;

using System.Windows;

using System.IO;

Modify the Main() method in Program.cs so it looks like this:

  1: [STAThread]

  2: static void Main(string[] args)

  3: {

  4: 	int height, width;

  5: 

  6: 	height = 300;

  7: 	width = 400;

  8: 

  9: 	//going to place all our stuff in here - same as on a standard WPF form.

 10: 	Grid mainContainer = new Grid();

 11: 	mainContainer.HorizontalAlignment = HorizontalAlignment.Stretch;

 12: 	mainContainer.VerticalAlignment = VerticalAlignment.Stretch;

 13: 

 14: 	Ellipse e = new Ellipse();

 15: 	e.Stroke = new SolidColorBrush(Color.FromArgb(255, 0, 0, 255));

 16: 	//setup an off-centre gradient fill for a 3D effect

 17: 	//Of course - we could instead use a Viewport3D with Visual3D elements :)

 18: 	RadialGradientBrush rFill = new RadialGradientBrush();

 19: 	GradientStopCollection gradientStops = new GradientStopCollection(new GradientStop[] { 

 20: 		new GradientStop(Color.FromArgb(255, 255, 255, 255), 0.0),

 21: 		new GradientStop(Color.FromArgb(255, 60, 90, 255), 1.0)

 22: 	});

 23: 	rFill.GradientStops = gradientStops;

 24: 	rFill.GradientOrigin = new Point(0.65, 0.25);

 25: 	rFill.Center = new Point(0.5,0.5);

 26: 	rFill.RadiusX = 0.5;

 27: 	rFill.RadiusY = 0.5;

 28: 	e.Fill = rFill;

 29: 

 30: 	e.HorizontalAlignment = HorizontalAlignment.Stretch;

 31: 	e.VerticalAlignment = VerticalAlignment.Stretch;

 32: 

 33: 	mainContainer.Children.Add(e);

 34: 

 35: 	TextBlock message = new TextBlock();

 36: 	message.Text = "WPF In a Bitmap!";

 37: 	message.HorizontalAlignment = HorizontalAlignment.Stretch;

 38: 	message.TextAlignment = TextAlignment.Center;

 39: 	message.VerticalAlignment = VerticalAlignment.Stretch;

 40: 	//use margin for placement

 41: 	//could use canvas instead - but don't like the static 'Canvas.SetLeft/SetTop'

 42: 	//methods.

 43: 	message.FontFamily = new FontFamily("Arial Black");

 44:    	message.FontSize = 40.0;

 45: 	message.FontWeight = FontWeights.Bold;

 46: 	message.Margin = new Thickness(0.0, 120, 0.0, 0.0);

 47: 	mainContainer.Children.Add(message);

 48: 

 49: 	PngBitmapEncoder encoder = new PngBitmapEncoder();

 50: 	RenderTargetBitmap render = new RenderTargetBitmap(

 51: 		width, 

 52: 		height, 

 53: 		96, 

 54: 		96, 

 55: 		PixelFormats.Pbgra32);

 56: 	render.Render(mainContainer);

 57: 	encoder.Frames.Add(BitmapFrame.Create(render));

 58: 	using (Stream s = File.Open("outputfile.png", FileMode.Create))

 59: 	{

 60: 		encoder.Save(s);

 61: 	}

 62: }

Notice a few things:


1) [STAThread] is applied to the Main method as WPF requires this.  If you omit it, an exception is raised telling you as much.


2) Transparency is enabled in the output image by using a pixel format that includes an alpha channel.  Line 55 specifies 32-bit rgba - same as most people's desktop.


3) Horizontal/Vertical Alignment being set to stretch in most places - has the same effect as it would on a XAML form.


Run the app and open up outputfile.png in the output folder.


Oops...


What's that?  The image is empty?


What's happened is that, although the grid, ellipse and textblock are all present and, in theory, ready to be rendered, they haven't actually been told to lay themselves out according to their visual container.  Of course, they don't actually have one, since they are just objects in memory, so what do we do?


We have to take the place of the layout engine that is usually wired up automatically by a real WPF form; which it achieves through the use of all the events that WPF elements expose (i.e. for when controls get added or updated etc).


On line 48 (or at least between 47 and 49) you need to add this line of code:

mainContainer.Arrange(new Rect(0, 0, width, height));

Thankfully, this is recursive.


Save and run again, and this time the image should be as above.


Animation


If you're going to try and create pre-rendered animations using WPF, there are a couple of gotchas - and one outright brick wall if animated gifs are your thing.


The brick wall is that GifBitmapEncoder is not capable of producing correct animatable gifs.  While you can successively call the AddFrame method of it's Frames collection property, and create a single gif file containing all the individual frames, you can't (out of the box) add the necessary timing information and looping information that most browsers/image viewers require.  You'll get a non-looping default-speed animation in IE7, for example, but in other browsers (apparently IE8 is one of them) it just won't animate at all.


It is possible to add this functionality into the Gif encoder - but it's not a job I want to take on (see this MSDN forum post and it's answer).  In the meantime, it's still possible to use .Net and WPF to generate the intermediate gif frames on disk, and then either manually stitch the animation together with timing in a dedicated tool.


The gotchas for animating in this way are:



  1. Once a BitmapEncoder has saved a file, it cannot be used to save another; so you'll need to create a new encoder for each frame.
  2. You can re-use the RenderBitmapTarget between each frame, but...
  3. In the same way that we had to manually call the 'Arrange' method of the Grid (or whichever root container we're going to be using), we also have to manually Invalidate any controls whose visual state changes between frames.  You can achieve this, by sticking this line before the next call to the Render(Visual) method of the RenderBitmapTarget object:
mainContainer.InvalidateVisual();

Now, in theory it might be possible to use the story-boarding animation features of WPF, but since they rely on animation timers (which will not be present since there's no actual WPF app or window here) I'm not sure how effective it'll be.  My guess is that you would manually visit the timeline, explicitly setting the current position from 0 -> 1, then invalidate anything that might be affected, and then do the Invalidate/Render code.


If that were possible, then it would certainly make animating rotations etc a lot easier.


In any case - the ability to render stills using WPFs frankly rather swish rasterisation layer is, in itself, a real boon if looking to create dynamic images on a web page, for example, or even for any other scenario.  Automatic anti-aliasing and transparency, for example, being two of the biggest boons.

No comments:

Post a Comment