Thursday, December 15, 2011

How to use WPF in a web service?


Using WPF in a web service might seem bizarre, but consider the following scenarios:
  1. You need to burn in a watermark into images requested from web application,
  2. You want to apply a red-eye reduction process to photographs takes from a mobile application, or
  3. You want to apply a Jason Bourne night effect to images in a web mapping application.
We recently implemented something similar to the third scenario (see blog, source, live app). A client web application written in JavaScript, Flex or Silverlight could apply an image rendering effects by parsing an image’s url through a proxy service. The proxy service, using WPF, would apply a bitmap effect and return a modified image. This technique adds the power of WPF to thin client at the cost of minimal network latency.
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.

3 comments:

  1. Great article! To answer your question, RenderTargetBitmap always uses the software renderer, which may be to advantage on most servers without gpus.

    Microsoft 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.

    ReplyDelete
  2. Hi Jeremiah, thanks for the clarification and tip.

    ReplyDelete
  3. can you please send me code at aajay78@hotmail.com

    ReplyDelete