Using WPF in a web service might seem bizarre, but consider the following scenarios:
- You need to burn in a watermark into images requested from web application,
- You want to apply a red-eye reduction process to photographs takes from a mobile application, or
- You want to apply a Jason Bourne night effect to images in a web mapping application.
Below is a detailed description of this proxy web service.
The following class is an ASP.NET generic web handler called “invert”. When published it will be accessible as ../virtualdirectory/invert.ashx. and will perform a color negative effect on parsed images. Because our server is accommodating multiple effects, one effect per handler, the web context is handed to static method to reduce the amount of redundant code.
public class Invert : IHttpHandler { public void ProcessRequest(HttpContext context) { Shader.Process(context, ShaderType.Invert); } public bool IsReusable { get { return false; } } }The code below is performing a number of important tasks. Firstly, because this proxy is published on the internet we need to add some restrictions so that it is only used as intended. The proxy will only entertain requests from web applications published on the maps.esri.com domain and it will only process images hosted on the services.arcgisonline.com domain. If the proxy request is legitimate, the remote image is downloaded and passed to a new background thread. The key is the creation of a background thread with a single-threaded apartment which is required to use COM and hence WPF. The proxy (or IIS) process is put only hold as the WPF operation is underway. The background thread returns a new image as an array of bytes which are then passed back to the client.
public class Shader { public static void Process(HttpContext context, ShaderType effect) { // Only accept request from apps from this domain! if (!context.Request.UrlReferrer.ToString().ToLowerInvariant(). Contains("maps.esri.com")) { context.Response.StatusCode = 403; context.Response.End(); return; } // Check for query string string query = Uri.UnescapeDataString(context.Request.QueryString.ToString()); if (string.IsNullOrEmpty(query)) { context.Response.StatusCode = 403; context.Response.End(); return; } // Filter requests if (!query.ToLowerInvariant().Contains("services.arcgisonline.com")) { context.Response.StatusCode = 403; context.Response.End(); return; } // Create web request WebRequest webRequest = WebRequest.Create(new Uri(query)); webRequest.Method = context.Request.HttpMethod; // Send the request to the server WebResponse serverResponse = null; try { serverResponse = webRequest.GetResponse(); } catch (WebException webException) { context.Response.StatusCode = 500; context.Response.StatusDescription = webException.Status.ToString(); context.Response.Write(webException.Response); context.Response.End(); return; } // Exit if invalid response if (serverResponse == null) { context.Response.End(); return; } // Configure reponse context.Response.ContentType = serverResponse.ContentType; Stream stream = serverResponse.GetResponseStream(); // Read response byte[] buffer = new byte[32768]; int size = 0; int chunk; while ((chunk = stream.Read(buffer, size, buffer.Length - size)) > 0) { size += chunk; if (size != buffer.Length) { continue; } int nextByte = stream.ReadByte(); if (nextByte == -1) { break; } // Resize the buffer byte[] newBuffer = new byte[buffer.Length * 2]; Array.Copy(buffer, newBuffer, buffer.Length); newBuffer[size] = (byte)nextByte; buffer = newBuffer; size++; } serverResponse.Close(); stream.Close(); // Shrink buffer byte[] bytes = new byte[size]; Array.Copy(buffer, bytes, size); // If the request is not a JPEG image then return data if (!serverResponse.ContentType.ToLowerInvariant().Contains("image/jpg")) { context.Response.OutputStream.Write(bytes, 0, bytes.Length); context.Response.End(); return; } // Create class to send to STA backgroung thread ShaderWorker work = new ShaderWorker() { Source = bytes, ShaderEffect = effect }; // Execute background thread Thread worker = new Thread(new ThreadStart(work.Execute)); worker.SetApartmentState(ApartmentState.STA); worker.Name = "CreateImageWorker"; worker.Start(); worker.Join(); // Return processed stream context.Response.OutputStream.Write(work.Target, 0, work.Target.Length); context.Response.End(); } }
This enumerator lists available pixel shaders. These shaders (or “effects”) were downloaded from the Windows Presentation Foundation Pixel Shader Effects Library.
public enum ShaderType {
Emboss,
Invert,
Monochrome,
Mosaic,
Pixelate,
Tint
}
And lastly, this is were the magic happens. This class is executed from the main IIS thread as a single-apartment thread as explained above. The source image is loaded as a generic BitmapImage in an Image, rendered with an effect and then exported to a new byte array.public class ShaderWorker { public ShaderType ShaderEffect { private get; set; } public byte[] Source { private get; set; } public byte[] Target { get; private set; } public void Execute() { // Create bitmapimage MemoryStream m = new MemoryStream(this.Source); BitmapImage b = new BitmapImage(); b.BeginInit(); b.DecodePixelWidth = 256; b.DecodePixelHeight = 256; b.StreamSource = m; b.EndInit(); b.Freeze(); // Create image element Image i = new Image() { Source = b }; // Assign shade effect switch (this.ShaderEffect) { case ShaderType.Emboss: i.Effect = new EmbossedEffect(); break; case ShaderType.Invert: i.Effect = new InvertEffect(); break; case ShaderType.Monochrome: i.Effect = new MonochromeEffect(); break; case ShaderType.Mosaic: i.Effect = new MosaicEffect(); break; case ShaderType.Pixelate: i.Effect = new PixelateEffect(); break; case ShaderType.Tint: i.Effect = new TintEffect(); break; } // Assign element size i.Arrange( new System.Windows.Rect( new System.Windows.Size(256, 256) ) ); i.UpdateLayout(); // Render image element to a new bitmap RenderTargetBitmap r = new RenderTargetBitmap(256, 256, 96, 96, PixelFormats.Default); r.Render(i); // Create a new memory stream MemoryStream memoryStream = new MemoryStream(); // Export bitmap to the stream JpegBitmapEncoder encoder = new JpegBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(r)); encoder.Save(memoryStream); // Export the stream to a byte array byte[] bytes = memoryStream.ToArray(); memoryStream.Close(); // Shutdown rendering thread (if alive) if (r.Dispatcher.Thread.IsAlive) { r.Dispatcher.InvokeShutdown(); } // Store processed image (as a byte array) this.Target = bytes; } }In summary, this post describes the technique of using WPF in a web service to perform advanced rendering. In theory, this technique is not restricted to image processing but could also be used to generate graphics of charts, diagrams and even three dimensional scenes. During limited testing it is obvious that this technique introduces network latency, particularly when the service has been dormant. And what is still unknown is whether the rendering is performed in hardware (i.e. GPU) or software (i.e. CPU). Please comment below if you know the answer to this or know of a definitive test for GPU vs. CPU.
Great article! To answer your question, RenderTargetBitmap always uses the software renderer, which may be to advantage on most servers without gpus.
ReplyDeleteMicrosoft may tell you this isn't a technically supported scenario, but it works none the less! You may alternatively look at D2D as it supports software And hw rendering on the server, but v1 does not support shaders. vNext does but unknown if it is win8 only.
Hi Jeremiah, thanks for the clarification and tip.
ReplyDeletecan you please send me code at aajay78@hotmail.com
ReplyDelete