Rendering an image from an embedded Web Browser (C# WPF application)

How is all started

So this week I was working on an extension for WebMatrix, Luke Sampson of http://StudioStyle.es just integrate a cool piece of code from Matt MCElheny. The news is that the studiostyle.es website now supports converting the over 1,000 themes uploaded for Visual Studio 2010 into the WebMatrix format, and hence we automatically got a very large load of themes to choose from.

Still we aspired for an even better experience, currently the WebMatrix user will have to install the ColorThemeEditor extension, go to the site, find a theme, download it, import it, and so on. Just too many steps. So Luke and some other folks (thanks Justin Beckwith and Scott Hanselman) started chatting. Luke was super fast, two hours later he announced that there is an api to the site.

By hitting:

http://studiostyl.es/api/schemes.xml I could now get a live feed of all the themes available

and for each one he provided a preview, and a download link. So I was ready to write the update to my extension.

The preview came from the following style link http://studiostyl.es//schemes/son-of-obsidian/snippet

This returns an HTML page, that looks like this:

image

and I was planning to show one per item in a WPF list view. Unfortunately the web browser control is not too happy inside a WPF life (since it’s basically a thin wrapper around native window). So instead I decided to render the browser into a bitmap and then show it on the list. I Spent several hours playing around and searching for a solution online, and couldn’t find any valid solution for my case.

When I came back to work, I chatted with Mikhail Arkhipov and he has some old class that did something very similar.

 

What doesn’t work

The WPF WebBrowser control is really just a wrapper around the WinFrom WebBrowser control. So at first I thought I’ll load the page off the screen and render it. Well it turns out that if this control is not on the screen it will never actually render. I wasn’t inclined to add it to the view as a hack, but I tried anyways. Well turns out that rendering the image using RenderTargetBitmap doesn’t work either. I guess because it’s an activeX and not a true WPF control.

So what is the solution

First I used a WinForms WebBrowser, so instead of instantiating a System.Windows.Controls.WebBrowser I used System.Windows.Forms.WebBrowser

Things already start to look better because I got the browser to render without going on the screen. I still couldn’t figure out how to render the bitmap. This where mshtml came to help.

The browsers document can be cast into IViewObject like in the code below:

 

   1: var viewObject = wb.Document.DomDocument as IViewObject;

for more info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680763(v=vs.85).aspx

 

and now rendering can happen.

 

In my code I wasn’t hitting the server directly using Navigate and in order to do verify that I actually got the site data back rather than an error page I looked for a <pre tag that I knew Luke is always including in the response and is highly unlikely in an error page.

 

Of course I could have gotten an HttpWebRequest get the response and then feed the browser with the plain html, except that this page actually has some Jquery links, and it felt way more natural to let the browser deal with it. It’s really up to the implementer to pick what suits his application.

 

I’m attaching the full code below, here is some explanations on how it works and how to use it.

How to use it:

1. Add a reference to mshtml it’s going to appear in the COM section in VS2010

2. In your code instantiate a WebBrowserUtility utility object

3. Whenever you get a uri you want to render hookup to the WebBrowserImageReady event, and call Navigate with the Uri

 

   1: private WebBrowserUtility _browserUtility;
   2:  
   3: ...
   4:  
   5: _browserUtility = new WebBrowserUtility();
   6:  
   7: _browserUtility.WebBrowserImageReady += ImageReady;
   8:  
   9: _browserUtility.Navigate(PreviewUri);

4. The event will fire back after some time with the image on the eventargs, in my case if the html text did not include “<pre”  it will return null.

5. In my case I use CroppedImage to remove the scrollbars and I just render larger than I need (see lines 41, 42), cheap and dirty tricks that works nicely in my case. It’s a bit more complex to remove the scrollbars, but there are plenty of blogs on how to achieve that.

 

A word of caution: The webbrowserutility might not call back on the event, so you might want to spin up a timer disconnect it and dispose it after a while.

 

That’s it you are ready to roll.

 

How it works:

Lines 37-43:

When you instantiate the utility a WinForms browser is generated, that is important for rendering off the screen. The WPF one simply never fires an event back when it’s ready in this case.

 

Line 45:

I made navigate a separate method to avoid timing issues in hooking up the WebBrowserImageReady event. It can only be called once! The browser is disposed after the first navigation. Of course this can be modified to make the class reusable and disposable.

 

Line 57+:

When the browser finishes downloading it fires document ready event, at that point I’m getting the Document.Text and doing a simple text search for a <pre (yes it could be better, in my case it’s a sufficient test).

 

I then create an image and “Cast” or QueryInterface really for the IViewObject. If I got it I’ll create graphics and Draw it on the bitmap (that’s the method signature defined below on line 132+).

Since this is a WPF app, I now convert it back to a WPF Image, and raise the event. If I failed in any way the event fires with a null image.

 

   1: using System;
   2: using System.Runtime.InteropServices;
   3: using System.Windows;
   4: using System.Windows.Controls;
   5: using System.Windows.Interop;
   6: using System.Windows.Media.Imaging;
   7: using mshtml;
   8:  
   9: using Forms = System.Windows.Forms;
  10: using Drawing = System.Drawing;
  11:  
  12: namespace ColorThemeManager
  13: {
  14:   internal class WebBrowserImageReadyEvengArgs : EventArgs
  15:   {
  16:     public BitmapSource Bms
  17:     {
  18:       get;
  19:       private set;
  20:     }
  21:  
  22:     public WebBrowserImageReadyEvengArgs(BitmapSource b)
  23:     {
  24:       Bms = b;
  25:     }
  26:   }
  27:  
  28:   internal class WebBrowserUtility : IDisposable
  29:   {
  30:     public Forms.WebBrowser WebBrowser { get; private set; }
  31:  
  32:     public BitmapSource BitmapSource { get; private set; }
  33:  
  34:     public event EventHandler<WebBrowserImageReadyEvengArgs>
  35:                                     WebBrowserImageReady = null;
  36:  
  37:     public WebBrowserUtility()
  38:     {
  39:       WebBrowser = new Forms.WebBrowser();
  40:       WebBrowser.DocumentCompleted += DocumentCompleted;
  41:       WebBrowser.Width = OnlineTheme.BrowserWidth;
  42:       WebBrowser.Height = OnlineTheme.BrowserHeight;
  43:     }
  44:  
  45:     public void Navigate(Uri url)
  46:     {
  47:       if (WebBrowser != null)
  48:       {
  49:         WebBrowser.Navigate(url);
  50:       }
  51:       else
  52:       {
  53:         throw new InvalidOperationException();
  54:       }
  55:     }
  56:  
  57:     private void DocumentCompleted
  58:         (object sender,
  59:          Forms.WebBrowserDocumentCompletedEventArgs e)
  60:     {
  61:       using (var wb = (Forms.WebBrowser)sender)
  62:       {
  63:         var text = wb.DocumentText ?? string.Empty;
  64:         bool validResult = text.Contains("<pre");
  65:  
  66:         if (validResult)
  67:         {
  68:           var pixelFormat = Drawing.Imaging.PixelFormat.Format24bppRgb;
  69:  
  70:           using (var bitmap = new Drawing.Bitmap(wb.Width,
  71:                                                  wb.Height,
  72:                                                  pixelFormat))
  73:           {
  74:             var viewObject = wb.Document.DomDocument as IViewObject;
  75:  
  76:             if (viewObject != null)
  77:             {
  78:               var sourceRect = new tagRECT();
  79:               sourceRect.left = 0;
  80:               sourceRect.top = 0;
  81:               sourceRect.right = wb.Width;
  82:               sourceRect.bottom = wb.Height;
  83:  
  84:               var targetRect = new tagRECT();
  85:               targetRect.left = 0;
  86:               targetRect.top = 0;
  87:               targetRect.right = wb.Width;
  88:               targetRect.bottom = wb.Height;
  89:  
  90:               using (var gr = Drawing.Graphics.FromImage(bitmap))
  91:               {
  92:                 IntPtr hdc = gr.GetHdc();
  93:  
  94:                 try
  95:                 {
  96:                   int hr = viewObject.Draw(1 /*DVASPECT_CONTENT*/,
  97:                                   (int)-1,
  98:                                   IntPtr.Zero,
  99:                                   IntPtr.Zero,
 100:                                   IntPtr.Zero,
 101:                                   hdc,
 102:                                   ref targetRect,
 103:                                   ref sourceRect,
 104:                                   IntPtr.Zero,
 105:                                   (uint)0);
 106:                 }
 107:                 finally
 108:                 {
 109:                   gr.ReleaseHdc();
 110:                 }
 111:               }
 112:  
 113:               BitmapSource = Imaging.CreateBitmapSourceFromHBitmap(
 114:                                   bitmap.GetHbitmap(),
 115:                                   IntPtr.Zero,
 116:                                   Int32Rect.Empty,
 117:                                   BitmapSizeOptions.FromEmptyOptions());
 118:             }
 119:           }
 120:         }
 121:       }
 122:  
 123:       WebBrowser = null;
 124:  
 125:       if (WebBrowserImageReady != null)
 126:       {
 127:         WebBrowserImageReady(this,
 128:             new WebBrowserImageReadyEvengArgs(BitmapSource));
 129:       }
 130:     }
 131:  
 132:     [ComVisible(true), ComImport()]
 133:     [GuidAttribute("0000010d-0000-0000-C000-000000000046")]
 134:     [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
 135:     private interface IViewObject
 136:     {
 137:       [return: MarshalAs(UnmanagedType.I4)]
 138:       [PreserveSig]
 139:       int Draw(
 140:         ////tagDVASPECT                
 141:           [MarshalAs(UnmanagedType.U4)] UInt32 dwDrawAspect,
 142:           int lindex,
 143:           IntPtr pvAspect,
 144:           [In] IntPtr ptd,
 145:         //// [MarshalAs(UnmanagedType.Struct)] ref DVTARGETDEVICE ptd,
 146:           IntPtr hdcTargetDev, IntPtr hdcDraw,
 147:           [MarshalAs(UnmanagedType.Struct)] ref tagRECT lprcBounds,
 148:           [MarshalAs(UnmanagedType.Struct)] ref tagRECT lprcWBounds,
 149:           IntPtr pfnContinue,
 150:           [MarshalAs(UnmanagedType.U4)] UInt32 dwContinue);
 151:     }
 152:  
 153:     public void Dispose()
 154:     {
 155:       if (WebBrowser != null)
 156:       {
 157:         WebBrowser.Dispose();
 158:         WebBrowser = null;
 159:       }
 160:     }
 161:   }
 162: }

 

And this it how it looks at the end

image

Come give it a spin

-> Install webmatrix from here:

http://www.microsoft.com/web/webmatrix/betafeatures.aspx

-> Open any site

-> Click on the gallery icon:

image

-> Install ColorThemeManager

-> Switch to to the Files workspace, open any file

image

-> Click on the Theme button

image

-> Notice the new fun colors in your editor

3 Comments

  • Could you use this for generating link previews too, like Google does? Pretty cool hack—it'll almost be a shame when studiostyles provides images and you don't need to use it any more :)

  • Hey Luke,

    I heard the studiostyles might be using this code, so maybe not such a shame.

    You can use it to do other things, but the caviat is that I wouldn't run it in the same process with you site for security reason. In your case where you run internal html through the browser it "might" be ok, though I would still run it in a sandbox.

    I would start a separate process, give it minimal rights and run the rendering in it. You can make it as simple as dropping html files in an input folder, and the process will render them to an output folder.

    Hope this helps

  • what does this code do
    _browserUtility.WebBrowserImageReady += ImageReady;

    here what is `ImageReady`

    there is no ImageReady function in ur code. i don't understand it.

Comments have been disabled for this content.