Generate an Image of a Web Page

I have some WinForms applications that would benefit from having images and thumbnails (sometimes called thumbshots) of internet web pages and so I decided yesterday to have a look at how to approach this. As you would expect, I went Googling. C# web page thumbnail, C# web site thumbnail, and others give plenty of links about how to make "websites of thumbnails" rather than "thumbnails of websites"...

Along the way, I found Display Web Page ThumbShots without Hosting Images which discusses two sites in particular (thumbshots.org and alexa.com) which provide web capabilites for thumbnails by serving up images. An example this can be seen at Search CSS which combines Google results with images from thumbshots.org, which I think is a cool piece of application integration.

If you want to write a web application, these services may be very useful for you. They might also be useful if you want to to write certain types of windows applications (for example, smart client apps on PocketPC where storage space is at a premium). However, they really don't meet my needs because I want to generate the images myself.

My next thought was to see if I could use Internet Explorer to get the images I needed. IE is provided as a component by the WebBrowser control. To add this control to your project, simply select "Add/Remove Items" from the Toolbox context menu and check "Microsoft Web Browser" on the "COM Components" tab of the "Customize Toolbox" dialog.

Once added, the control can be treated as any normal control and placed on a form. Unfortunately, the WebBrowser control does not provide an Image property (life just isn't that simple). The challenge is to obtain the rendered image. Usefully, the WebBrowser control does provide the CreateGraphics() method. This is useful because, in turn, the System.Drawing.Graphics class provides the GetHdc() method which returns the handle to the device context. System.Drawing (aka GDI+) is a powerful library that does an excellent job of encapsulating the GDI32 library, but it is not comprehensive and with a device context handle we can call the Win32 API directly to achieve what we need.

The following code snippet is for the event triggered by the WebBrowser control when document completes after a call to the Navigate() method, which isn't shown. Also not shown is the GDI32 class which contains DllImport static methods and enumerations. These can be obtained easily from pinvoke.net.

C#

private void OnDocumentComplete(object sender, AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
    using (Graphics srcGraphics = this.axWebBrowser1.CreateGraphics())
    {
          using (Graphics destGraphics = this.pictureBox1.CreateGraphics())
          {
              IntPtr hdcDest = destGraphics.GetHdc();
              IntPtr hdcSrc = srcGraphics.GetHdc();
              GDI32.BitBlt(
                    hdcDest,
                    0, 0,
                    this.axWebBrowser1.ClientRectangle.Width, this.axWebBrowser1.ClientRectangle.Height,
                    hdcSrc,
                    0, 0,
                    (int) GDI32.TernaryRasterOperations.SRCCOPY
                    );
              srcGraphics.ReleaseHdc(hdcSrc);
              destGraphics.ReleaseHdc(hdcDest);
          }
    }
}

VB.NET

Private Sub OnDocumentComplete(ByVal sender As Object, ByVal e As AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent)
  ' Using
  Dim srcGraphics As Graphics = Me.axWebBrowser1.CreateGraphics
  Try
    ' Using
    Dim destGraphics As Graphics = Me.pictureBox1.CreateGraphics
    Try
      Dim hdcDest As IntPtr = destGraphics.GetHdc
      Dim hdcSrc As IntPtr = srcGraphics.GetHdc
      GDI32.BitBlt(hdcDest, 0, 0, Me.axWebBrowser1.ClientRectangle.Width, Me.axWebBrowser1.ClientRectangle.Height, hdcSrc, 0, 0, CType(GDI32.TernaryRasterOperations.SRCCOPY, Integer))
      srcGraphics.ReleaseHdc(hdcSrc)
      destGraphics.ReleaseHdc(hdcDest)
    Finally
      CType(destGraphics, IDisposable).Dispose()
    End Try
  Finally
    CType(srcGraphics, IDisposable).Dispose()
  End Try
End Sub

The code above copies the rendered image from the WebBrowser to the PictureBox. However, there is a problem. The problem is that the BitBlt operation will only copy the visible client area of the WebBrowser. This means that part if the WebBrowser window is not visible (by being off-screen, for example) then it will not be copied. If it is obscured by another window then the copy will part of the other window. The workaround for this issue is to use interfaces in the Microsoft.mshtml library as this provides a mean of having the control draw onto a device context of your choosing. To do this, click "Add Reference" on your project, and select "Microsoft.mshtml" from the .NET tab.

There is a gotcha, which I found on Ryan Farber's weblog, which means that you need to describe the IHTMLElementRender interface:

C#

[Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
    ComVisible(true),
    ComImport]
interface IHTMLElementRender
{
    void DrawToDC([In] IntPtr hDC);
    void SetDocumentPrinter([In, MarshalAs(UnmanagedType.BStr)] string bstrPrinterName, [In] IntPtr hDC);
};

VB.NET

<Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComVisible(True), ComImport()> _
Interface IHTMLElementRender

  Sub DrawToDC(  <In()> _
ByVal hDC As IntPtr)

  Sub SetDocumentPrinter(  <In(), MarshalAs(UnmanagedType.BStr)> _
ByVal bstrPrinterName As String,  <In()> _
ByVal hDC As IntPtr)
End Interface

Once you have done this, then the event handler can be amended to:

C#

private void OnDocumentComplete(object sender, AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
    IHTMLDocument2 document = (IHTMLDocument2) this.axWebBrowser1.Document;
    if (document != null)
    {
          IHTMLElement element = (IHTMLElement) document.body;
          if (element != null)
          {
              IHTMLElementRender render = (IHTMLElementRender) element;
              if (render != null)
              {
                    using (Graphics graphics = this.pictureBox1.CreateGraphics())
                    {
                        IntPtr hdc = graphics.GetHdc();
                        render.DrawToDC(hdc);
                    }
              }
          }
    }
}

VB.NET

Private Sub OnDocumentComplete(ByVal sender As Object, ByVal e As AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent)
  Dim document As IHTMLDocument2 = CType(Me.axWebBrowser1.Document, IHTMLDocument2)
  If Not (document Is Nothing) Then
    Dim element As IHTMLElement = CType(document.body, IHTMLElement)
    If Not (element Is Nothing) Then
      Dim render As IHTMLElementRender = CType(element, IHTMLElementRender)
      If Not (render Is Nothing) Then
        ' Using
        Dim graphics As Graphics = Me.pictureBox1.CreateGraphics
        Try
          Dim hdc As IntPtr = graphics.GetHdc
          render.DrawToDC(hdc)
        Finally
          CType(graphics, IDisposable).Dispose()
        End Try
      End If
    End If
  End If
End Sub

Now the copy operation performs better. If the WebBrowser control is obscured in some fashion then you still get the original copy. There is, unfortunately, a further problem which is that the copy made is not persistent. This means that if the PictureBox which contains the copy is obscured in some fashion, then the copy needs to be made a second time. Furthermore, the copied image is not available via the Image property of the PictureBox which means that you cannot call the Save() method, for example, to save the image to disk. In order to set the Image property, we need to create and set a persistent bitmap. To do this we need to resort to the Win32 API again and change the code:

C#

private void OnDocumentComplete(object sender, AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
    IHTMLDocument2 document = (IHTMLDocument2) this.axWebBrowser1.Document;
    if (document != null)
    {
          IHTMLElement element = (IHTMLElement) document.body;
          if (element != null)
          {
              IHTMLElementRender render = (IHTMLElementRender) element;
              if (render != null)
              {
                    using (Graphics graphics = this.pictureBox1.CreateGraphics())
                    {
                        IntPtr hdcDestination = graphics.GetHdc();
                        render.DrawToDC(hdcDestination);
                        IntPtr hdcMemory = GDI32.CreateCompatibleDC(hdcDestination);
                        IntPtr bitmap = GDI32.CreateCompatibleBitmap(
                              hdcDestination, 
                              this.axWebBrowser1.ClientRectangle.Width, this.axWebBrowser1.ClientRectangle.Height
                              );
             
                        if (bitmap != IntPtr.Zero)
                        {
                              IntPtr hOld = (IntPtr) GDI32.SelectObject(hdcMemory, bitmap);
                              GDI32.BitBlt(
                                  hdcMemory,
                                  0, 0,
                                  this.axWebBrowser1.ClientRectangle.Width, this.axWebBrowser1.ClientRectangle.Height,
                                  hdcDestination,
                                  0, 0,
                                  (int) GDI32.TernaryRasterOperations.SRCCOPY
                                  );
                              GDI32.SelectObject(hdcMemory, hOld);
                              GDI32.DeleteDC(hdcMemory);
                              graphics.ReleaseHdc(hdcDestination);
         
                              this.pictureBox.Image = Image.FromHbitmap(bitmap);
                        }
                    }
              }
          }
    }
}

VB.NET

Private Sub OnDocumentComplete(ByVal sender As Object, ByVal e As AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent)
  Dim document As IHTMLDocument2 = CType(Me.axWebBrowser1.Document, IHTMLDocument2)
  If Not (document Is Nothing) Then
    Dim element As IHTMLElement = CType(document.body, IHTMLElement)
    If Not (element Is Nothing) Then
      Dim render As IHTMLElementRender = CType(element, IHTMLElementRender)
      If Not (render Is Nothing) Then
        ' Using
        Dim graphics As Graphics = Me.pictureBox1.CreateGraphics
        Try
          Dim hdcDestination As IntPtr = graphics.GetHdc
          render.DrawToDC(hdcDestination)
          Dim hdcMemory As IntPtr = GDI32.CreateCompatibleDC(hdcDestination)
          Dim bitmap As IntPtr = GDI32.CreateCompatibleBitmap(hdcDestination, Me.axWebBrowser1.ClientRectangle.Width, Me.axWebBrowser1.ClientRectangle.Height)
          If Not (bitmap = IntPtr.Zero) Then
            Dim hOld As IntPtr = CType(GDI32.SelectObject(hdcMemory, bitmap), IntPtr)
            GDI32.BitBlt(hdcMemory, 0, 0, Me.axWebBrowser1.ClientRectangle.Width, Me.axWebBrowser1.ClientRectangle.Height, hdcDestination, 0, 0, CType(GDI32.TernaryRasterOperations.SRCCOPY, Integer))
            GDI32.SelectObject(hdcMemory, hOld)
            GDI32.DeleteDC(hdcMemory)
            graphics.ReleaseHdc(hdcDestination)
            Me.pictureBox.Image = Image.FromHbitmap(bitmap)
          End If
        Finally
          CType(graphics, IDisposable).Dispose()
        End Try
      End If
    End If
  End If
End Sub

I'm not going to go into detail about the GDI operations (as they are so well documented elsewhere) and I have sacrified good practice like exception handling for clarity, but the code above will set a persistent Image of the web page loaded in the WebBrowser control which can be saved or manipulated with the GDI+ library. For example, the following code sample takes the Image created above and scales it to a thumbnail PictureBox:

C#

private void DrawThumbnail()
{
    using (Graphics graphics = this.thumbnailPictureBox.CreateGraphics())
    {
          graphics.DrawImage(this.pictureBox1.Image, this.thumbnailPictureBox.ClientRectangle);
    }
}

VB.NET

Private Sub DrawThumbnail()
  ' Using
  Dim graphics As Graphics = Me.thumbnailPictureBox.CreateGraphics
  Try
    graphics.DrawImage(Me.pictureBox1.Image, Me.thumbnailPictureBox.ClientRectangle)
  Finally
    CType(graphics, IDisposable).Dispose()
  End Try
End Sub

You might also like...

Comments

Alan Dean I am a UK-based professional software developer. I am also active in the development user community.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“In order to understand recursion, one must first understand recursion.”