Accessing WPF Generated Images Via WCF
As I've said time and time again, WCF is a very streamlined technology. It's not a "web service framework". Rather, it's a "communication extensibility foundation". When it comes to any type of communication, WCF is your go to card. By communication, I'm not talking about one system talking to another. No, I'm talking about any data transfer. In fact, you can easily use WCF as your entry point into the world of WPF image generation.
There are two topics here: generating images with WPF and sending images over WCF. Let’s start with the first of these.
A quick Google search for "RenderTargetBitmap" will give myriad example of using this technique. For now, here’s one very simple example (actually, RenderTargetBitmap is simple to work with anyways):
System.IO.MemoryStream stream = new System.IO.MemoryStream(); //++ this is a technique to ensure any transformations render correctly VisualBrush vb = new VisualBrush(CreateVisual(templateName, text, width, height)); DrawingVisual dv = new DrawingVisual(); using (DrawingContext dc = dv.RenderOpen()) { dc.DrawRectangle(vb, null, new Rect(new Point(0, 0), new Point(width, height))); } //+ render RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32); renderTargetBitmap.Render(dv); //+ encode BitmapEncoder bitmapEncoder = new PngBitmapEncoder(); bitmapEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap)); bitmapEncoder.Save(stream);
In this code we have some WPF elements being created (returned from the CreateVisual method), a wrapper trick to get transformation to render correctly, that being sent to a renderer, the renderer being sent to an encoder, and finally the encoder being saving the data to a stream. With this we can save the data to a file or whatever we want. It's pretty straight-forward.
For completeness, here is the WPF object tree creation:
//- $CreateVisual -// private Visual CreateVisual(String templateName, String text, Int32 width, Int32 height) { StackPanel stackPanel = new StackPanel(); stackPanel.Background = new SolidColorBrush(Color.FromRgb(0xff, 0xff, 0xff)); //+ TextBlock textBlock = new TextBlock { Text = text, FontSize = 20, Foreground = new SolidColorBrush(Color.FromRgb(0x00, 0x00, 0x00)) }; textBlock.Effect = new System.Windows.Media.Effects.DropShadowEffect { Color = Color.FromRgb(0x00, 0x00, 0x00), Direction = 320, ShadowDepth = 5, Opacity = .5 }; stackPanel.Children.Add(textBlock); stackPanel.Children.Add(new TextBox { Text = text, FontSize = 10, Foreground = new SolidColorBrush(Color.FromRgb(0x00, 0x00, 0x00)), Effect = textBlock.Effect }); stackPanel.Children.Add(new Button { Content = text, FontSize = 10, Foreground = new SolidColorBrush(Color.FromRgb(0xff, 0xff, 0x00)), Effect = textBlock.Effect }); stackPanel.Arrange(new Rect(new Point(0, 0), new Point(width, height))); //+ return stackPanel; }
If you know WPF, this is natural to you. If you don't, well, the code should be fairly self explanatory.
There's not much to the topic of WPF image generation, so let's talk about sending images over WCF.
To do this, you use a standard web-based WCF setup (i.e. using webHttpBinding). So, here's our config:
<system.serviceModel> <behaviors> <endpointBehaviors> <behavior name="imageBehavior"> <webHttp /> </behavior> </endpointBehaviors> </behaviors> <services> <service name="ImageCreator.Web.ImageService"> <endpoint address="" binding="webHttpBinding" behaviorConfiguration="imageBehavior" contract="ImageCreator.Web.IImageService" /> </service> </services> </system.serviceModel>
Here's our service host (/Image.svc):
<%@ ServiceHost Service="ImageCreator.Web.ImageService" %>
When it comes to the service and operation contract, all you need to know is that you return the image from WCF as a stream.
Let's say that we want to create images that will be used on the web. That is, we will be accessing the images with HTTP GETS (i.e. via direct URLs ).
using System; using System.ServiceModel; using System.ServiceModel.Web; //+ namespace ImageCreator.Web { [ServiceContract(Namespace = Information.Namespace.Image)] public interface IImageService { //- GetImage -// [OperationContract, WebGet(UriTemplate = "/GetImage////")] System.IO.Stream GetImage(String templateName, String text, String width, String height); } }
Here you can see that we are using the WebGet attribute to specify that the web-based WCF service will be using HTTP GET. This attribute is also used to specify the mapping between the URL and the method.
At this point, we can create our service implementation and paste our WPF code in there (the themeName parameter isn't used in this example; it's only there to give you an idea of what you can do.
public System.IO.Stream GetImage(String templateName, String text, String widthString, String heightString) { //+ validate Int32 width; Int32 height; if (!Int32.TryParse(widthString, out width)) { return null; } if (!Int32.TryParse(heightString, out height)) { return null; } //+ content-type WebOperationContext.Current.OutgoingResponse.ContentType = "image/png"; //+ System.IO.MemoryStream stream = new System.IO.MemoryStream(); //++ this is a technique to ensure any transformations render correctly VisualBrush vb = new VisualBrush(CreateVisual(templateName, text, width, height)); DrawingVisual dv = new DrawingVisual(); using (DrawingContext dc = dv.RenderOpen()) { dc.DrawRectangle(vb, null, new Rect(new Point(0, 0), new Point(width, height))); } //+ render RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32); renderTargetBitmap.Render(dv); //+ encode BitmapEncoder bitmapEncoder = new PngBitmapEncoder(); bitmapEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap)); bitmapEncoder.Save(stream); //+ seek stream.Seek(0, System.IO.SeekOrigin.Begin); //+ return stream; }
With this setup, you have a wonderful system where you can access http://www.tempuri.com/Image.svc/GetImage/skyblue/Close/75/24 and get a beautiful... error.
Why would you get an error? For the simple reason that WCF is a highly threaded monster and WPF only rendered with STA (single thread apartment). Therefore, we need to do something like the following:
public System.IO.Stream GetImage(String themeName, String text, String widthString, String heightString) { //+ validate Int32 width; Int32 height; if (!Int32.TryParse(widthString, out width)) { return null; } if (!Int32.TryParse(heightString, out height)) { return null; } //+ content-type WebOperationContext.Current.OutgoingResponse.ContentType = "image/png"; //+ System.IO.MemoryStream stream = new System.IO.MemoryStream(); System.ServiceModel.OperationContext context = System.ServiceModel.OperationContext.Current; System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(delegate { using (System.ServiceModel.OperationContextScope scope = new System.ServiceModel.OperationContextScope(context)) { //++ this is a technique to ensure any transformations render correctly VisualBrush vb = new VisualBrush(CreateVisual(themeName, text, width, height)); DrawingVisual dv = new DrawingVisual(); using (DrawingContext dc = dv.RenderOpen()) { dc.DrawRectangle(vb, null, new Rect(new Point(0, 0), new Point(width, height))); } //+ render RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32); renderTargetBitmap.Render(dv); //+ encode BitmapEncoder bitmapEncoder = new PngBitmapEncoder(); bitmapEncoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap)); bitmapEncoder.Save(stream); } })); thread.SetApartmentState(System.Threading.ApartmentState.STA); thread.Start(); thread.Join(); //+ seek stream.Seek(0, System.IO.SeekOrigin.Begin); //+ return stream; }
Now it will actually render. In fact, behold the greatness:
While this works fine, it's not idea. When you need to have mechanics stuff next to what you are actually trying to do, you're typically doing something wrong. In this case you have the threading stuff all over the place. This is like tracking mud all over the living room. The mud isn't bad, you just don't want it next to your couch.
To get around this, you can tap into the world of WCF further and create an operation behavior to run the operation as STA. Technically, we don't care about the operation behavior so much as we do the operation invoker we also need to create. We want to control the invocation of the operation. For this we create an operation invoker; the operation behavior is only there to install the invoker. Let's get to it...
An operation behavior is nothing more than a class that implements the System.ServiceModel.Description.IOperationBehavior interface. Typically it inherits from the System.Attribute class, but this isn't a strict requirement. Doing this does, however, allow the operation behavior to be applied declaratively to your operation implementation. Again, this is optional.
The only method we care about in our new operation behavior is the ApplyDispatchBehavior method. Using this method, we can install our operation invoker. Here's our behavior:
using System; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; //+ namespace Themelia.Web.Behavior { [AttributeUsage(AttributeTargets.Method)] public class STAOperationBehavior : Attribute, System.ServiceModel.Description.IOperationBehavior { //- @AddBindingParameters -// public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { //+ blank } //- @ApplyClientBehavior -// public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { //+ blank } //- @ApplyDispatchBehavior -// public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) { dispatchOperation.Invoker = new STAInvoker(dispatchOperation.Invoker); } //- @Validate -// public void Validate(OperationDescription operationDescription) { //+ blank } } }
Next, let's actually build the operation invoker this guy is installing.
An operation invoker is class which implements the System.ServiceModel.Dispatcher.IOperationInvoker interface. Among other things, the invoker allows us to hijack the WCF process and put our own logic around our operation implementation. This is a VERY useful things to do. For example, whenever I want to implement my own security on services I'll create an invoker to do authorization. If authorization is successful, I'll call the operation myself. Otherwise, I'll throw a security exception and WCF will deal with it from there.
For our purposes, we are going to use this hijacking ability to starting a new STA thread and call the operation from that thread. We are going to do this in the Invoke method.
using System; using System.Security; using System.ServiceModel.Dispatcher; //+ namespace Themelia.Web.Behavior { public class STAInvoker : System.ServiceModel.Dispatcher.IOperationInvoker { //- $InnerOperationInvoker -// private IOperationInvoker InnerOperationInvoker { get; set; } //+ //- @Ctor -// public STAInvoker(IOperationInvoker operationInvoker) { this.InnerOperationInvoker = operationInvoker; } //+ //- @AllocateInputs -// public Object[] AllocateInputs() { return InnerOperationInvoker.AllocateInputs(); } //- @Invoke -// public Object Invoke(Object instance, Object[] inputs, out Object[] outputs) { Object result = null; Object[] staOutputs = null; System.ServiceModel.OperationContext context = System.ServiceModel.OperationContext.Current; System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(delegate { using (System.ServiceModel.OperationContextScope scope = new System.ServiceModel.OperationContextScope(context)) { result = InnerOperationInvoker.Invoke(instance, inputs, out staOutputs); } })); thread.SetApartmentState(System.Threading.ApartmentState.STA); thread.Start(); thread.Join(); //+ outputs = staOutputs; //+ return result; } //- @InvokeBegin -// public IAsyncResult InvokeBegin(Object instance, Object[] inputs, AsyncCallback callback, Object state) { return InnerOperationInvoker.InvokeBegin(instance, inputs, callback, state); } //- @InvokeEnd -// public Object InvokeEnd(Object instance, out Object[] outputs, IAsyncResult result) { return InnerOperationInvoker.InvokeEnd(instance, out outputs, result); } //- @IsSynchronous -// public bool IsSynchronous { get { return InnerOperationInvoker.IsSynchronous; } } } }
If you look closely at the Invoke method, you will see that we are doing essentially the same thing with the thread that we did with the WPF code directly. Now we can remove that mud from our living room and live cleaner.
Notice, though, one major thing you do NOT want to forget: the declaration of the operation scope. If you forget to do this, you will NOT have access to your operation context. You need this because in our operation we are doing the following:
WebOperationContext.Current.OutgoingResponse.ContentType = "image/png";
This tells the person receiving the image the type of the image (otherwise they have to parse the header or just guess). Without the operation scope declaration, we lose this completely.
Anyways, as you can see form the Invoker method, the current operation context is saved and is restored once the new thread has been started.
Now, to finish the job, all we need to do is apply the attribute to our operation (or use another mechanism if you didn't make the behavior an attribute). Here's what our operation looks like now (with the original logic we had):
//- @GetImage -// [Themelia.Web.Behavior.STAOperationBehavior] public System.IO.Stream GetImage(String templateName, String text, String widthString, String heightString) { //+ stuff here }
Now when you access http://www.tempuri.com/Image.svc/GetImage/skyblue/Close/75/24, you get the actual image you wanted to generate. Thus, once again, WCF provides a very streamlined way of accessing data.
Samples for this document may be accessed from here.