Click here to Skip to main content
15,881,248 members
Articles / Multimedia / GDI

HTML to Image in C#

Rate me:
Please Sign up or sign in to vote.
4.90/5 (38 votes)
13 Feb 2010CPOL2 min read 194.8K   121   46
Capture an HTML document as an image.

Introduction

In this article, I will show you how to capture an HTML document as an image using a WebBrowser object and the IViewObject.Draw method, which according to MSDN draws a representation of an object onto the specified device context. Before we get started, I just want to mention that the obtained results were identical to those obtained using commercial libraries, so I hope this will be useful to someone.

The IViewObject interface

The very first thing that we must do is to define the IViewObject interface.

C#
[ComVisible(true), ComImport()]
[GuidAttribute("0000010d-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
public interface IViewObject
{
    [return: MarshalAs(UnmanagedType.I4)]
    [PreserveSig]
    int Draw(
        [MarshalAs(UnmanagedType.U4)] UInt32 dwDrawAspect,
        int lindex,
        IntPtr pvAspect,
        [In] IntPtr ptd,
        IntPtr hdcTargetDev,
        IntPtr hdcDraw,
        [MarshalAs(UnmanagedType.Struct)] ref Rectangle lprcBounds,
        [MarshalAs(UnmanagedType.Struct)] ref Rectangle lprcWBounds,
        IntPtr pfnContinue,
        [MarshalAs(UnmanagedType.U4)] UInt32 dwContinue);
    [PreserveSig]
    int GetColorSet([In, MarshalAs(UnmanagedType.U4)] int dwDrawAspect, 
       int lindex, IntPtr pvAspect,[In] IntPtr ptd, 
       IntPtr hicTargetDev, [Out] IntPtr ppColorSet);
    [PreserveSig]
    int Freeze([In, MarshalAs(UnmanagedType.U4)] int dwDrawAspect, 
                    int lindex, IntPtr pvAspect, [Out] IntPtr pdwFreeze);
    [PreserveSig]
    int Unfreeze([In, MarshalAs(UnmanagedType.U4)] int dwFreeze);
    void SetAdvise([In, MarshalAs(UnmanagedType.U4)] int aspects, 
      [In, MarshalAs(UnmanagedType.U4)] int advf, 
      [In, MarshalAs(UnmanagedType.Interface)] IAdviseSink pAdvSink);
    void GetAdvise([In, Out, MarshalAs(UnmanagedType.LPArray)] int[] paspects, 
      [In, Out, MarshalAs(UnmanagedType.LPArray)] int[] advf, 
      [In, Out, MarshalAs(UnmanagedType.LPArray)] IAdviseSink[] pAdvSink);
}

Below is a summary description of the parameters that the Draw method takes (this is the only method we will use):

  • UInt32 dwDrawAspect - specifies the aspect to be drawn. Valid values are taken from the DVASPECT and DVASPECT2 enumerations. In this example, I'm using DVASPECT.CONTENT so the value passed is 1.
  • int lindex - portion of the object that is of interest for the draw operation. Currently, only -1 is supported.
  • IntPtr pvAspect - pointer to the additional information.
  • IntPtr ptd - describes the device for which the object is to be rendered. We will render for the default target device, so the value passed will be IntPtr.Zero.
  • IntPtr hdcTargetDev - information context for the target device indicated by the ptd parameter.
  • IntPtr hdcDraw - device context on which to draw.
  • ref Rectangle lprcBounds - the size of the captured image.
  • ref Rectangle lprcWBounds - the region of the WebBrowser object that we want to be captured.
  • IntPtr pfnContinue - pointer to a callback function (not used here).
  • UInt32 dwContinue - value to pass as a parameter to the function (not used here).

The HtmlCapture class

Now that we have defined our IViewObject interface, it is time to move on and create a class that will be used to capture a web page as an image.

C#
public class HtmlCapture
{
    private WebBrowser web;
    private Timer tready;
    private Rectangle screen;
    private Size? imgsize=null; 

    //an event that triggers when the html document is captured
    public delegate void HtmlCaptureEvent(object sender, 
                         Uri url, Bitmap image);
    public event HtmlCaptureEvent HtmlImageCapture;
    
    //class constructor
    public HtmlCapture() 
    {
       //initialise the webbrowser and the timer
       web = new WebBrowser();
       tready = new Timer();
       tready.Interval = 2000;
       screen = Screen.PrimaryScreen.Bounds;
       //set the webbrowser width and hight
       web.Width = screen.Width;
       web.Height = screen.Height;
       //suppress script errors and hide scroll bars
       web.ScriptErrorsSuppressed = true;
       web.ScrollBarsEnabled = false;
       //attached events
       web.Navigating += 
         new WebBrowserNavigatingEventHandler(web_Navigating);
       web.DocumentCompleted += new 
         WebBrowserDocumentCompletedEventHandler(web_DocumentCompleted);
       tready.Tick += new EventHandler(tready_Tick);
    }
         
    #region Public methods
    public void Create(string url)
    {
        imgsize = null;
        web.Navigate(url);
    }

    public void Create(string url,Size imgsz) 
    {
        this.imgsize = imgsz;
        web.Navigate(url);
    }
    #endregion 
         
    #region Events
    void web_DocumentCompleted(object sender, 
             WebBrowserDocumentCompletedEventArgs e)
    {
        //start the timer
        tready.Start();
    }

    void web_Navigating(object sender, WebBrowserNavigatingEventArgs e)
    {
        //stop the timer   
        tready.Stop();
    }

    void tready_Tick(object sender, EventArgs e)
    {
        //stop the timer
        tready.Stop();
        
        //capture html as an image
        //...
    }
    #endregion
}

As you can see, I'm using a Timer object to determine if the HTML document is fully loaded and can be captured. The reason I'm doing this is because an HTML document can trigger the DocumentCompleted event multiple times. After the document is fully loaded, the tready_Tick method is called.

C#
void tready_Tick(object sender, EventArgs e)
{
    //stop the timer
    tready.Stop();
    //get the size of the document's body
    Rectangle body = web.Document.Body.ScrollRectangle;

    //check if the document width/height is greater than screen width/height
    Rectangle docRectangle = new Rectangle()
    {
       Location=new Point(0,0),
        Size=new Size(body.Width > screen.Width ? body.Width : screen.Width,
         body.Height > screen.Height ? body.Height : screen.Height)
    };
    //set the width and height of the WebBrowser object
    web.Width = docRectangle.Width;
    web.Height = docRectangle.Height;
    
    //if the imgsize is null, the size of the image will 
    //be the same as the size of webbrowser object
    //otherwise  set the image size to imgsize
    Rectangle imgRectangle;
    if (imgsize == null)
        imgRectangle = docRectangle;
    else
        imgRectangle = new Rectangle()
        {
            Location=new Point(0,0),
            Size =imgsize.Value
        };
    //create a bitmap object 
    Bitmap bitmap = new Bitmap(imgRectangle.Width,imgRectangle.Height);
    //get the viewobject of the WebBrowser
    IViewObject ivo = web.Document.DomDocument as IViewObject;
     
    using (Graphics g = Graphics.FromImage(bitmap))
    {
        //get the handle to the device context and draw
        IntPtr hdc = g.GetHdc();
        ivo.Draw(1, -1, IntPtr.Zero, IntPtr.Zero,
                 IntPtr.Zero, hdc, ref imgRectangle, 
                 ref docRectangle, IntPtr.Zero, 0);
        g.ReleaseHdc(hdc);
    }
     //invoke the HtmlImageCapture event
    HtmlImageCapture(this, web.Url, bitmap);
}

Using the code

HtmlCapture has an overloaded method named Create. If you use the Create(string url) method, the size of the image will be the same as the size of the HTML document. If you want to create a thumbnail image of the HTML document, use Create(string url,Size imgsz).

C#
private void button2_Click(object sender, EventArgs e)
{
    HtmlCapture hc = new HtmlCapture();
    hc.HtmlImageCapture += 
       new HtmlCapture.HtmlCaptureEvent(hc_HtmlImageCapture);
    hc.Create("http://www.codeproject.com");
    //or
    hc.Create("http://www.codeproject.com",new Size(200,300)); 
}

void hc_HtmlImageCapture(object sender, Uri url, Bitmap image)
{
    image.Save("C:/"+ url.Authority+ ".bmp");
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionVery good Pin
lyfy721230-Dec-11 14:27
lyfy721230-Dec-11 14:27 
QuestionCannot compile Pin
CyberZeus27-Nov-11 21:42
CyberZeus27-Nov-11 21:42 
AnswerRe: Cannot compile Pin
CyberZeus28-Nov-11 2:50
CyberZeus28-Nov-11 2:50 
Questioncapturing only part of the web page Pin
Member 816805621-Oct-11 13:24
Member 816805621-Oct-11 13:24 
GeneralMy vote of 1 Pin
Member 789596113-Jul-11 0:16
Member 789596113-Jul-11 0:16 
QuestionWorks on all sites except CODEPROJECT.COM Pin
hienchu8-Jul-11 22:55
hienchu8-Jul-11 22:55 
GeneralMy vote of 5 Pin
Samuel Cherinet27-May-11 3:45
Samuel Cherinet27-May-11 3:45 
QuestionAmazing! Pin
vnmatt15-Oct-10 3:04
vnmatt15-Oct-10 3:04 
I have been working on the web browser control recently and what a mess! I don't know about you, but it took me a lot of time to get the screenshots working. This code has helped tremendously, but I do have some issues still that I would really appreciate your input on. The thing is that although this code is great for 99% of cases, it has one downfall; it creates a new web browser and navigates to the specified url. Why is this an issue? Well, I am working on software automation at the moment where data is input via the web browser control to a web page and then we detect if there were any errors (validation labels) and want to grab the screen as evidence. You see where I am going with this?

I am also the author behind the MBG Extensions Library (http://www.codeproject.com/KB/dotnet/MBGExtensionsLibrary.aspx) and want to create an extension method using your code.

Looking forward to hearing from you. Smile | :)
AnswerRe: Amazing! Pin
vnmatt19-Oct-10 19:45
vnmatt19-Oct-10 19:45 
GeneralDoesn't seem to work on a page with JScript or ActiveX Pin
Steve Dunn22-Jun-10 7:27
Steve Dunn22-Jun-10 7:27 
GeneralRe: Doesn't seem to work on a page with JScript or ActiveX Pin
Member 440130929-Dec-10 5:43
Member 440130929-Dec-10 5:43 
GeneralRunning the code as a windows service Pin
Izidor8-Jun-10 20:08
Izidor8-Jun-10 20:08 
GeneralRe: Running the code as a windows service Pin
cxfksword13-Jun-10 7:36
cxfksword13-Jun-10 7:36 
GeneralRe: Running the code as a windows service Pin
freelaxy17-Sep-10 10:39
freelaxy17-Sep-10 10:39 
GeneralDoes not work in ASP.NET Pin
yannlh28-Feb-10 15:37
yannlh28-Feb-10 15:37 
GeneralRe: Does not work in ASP.NET Pin
DadajiIn28-Jun-12 23:26
DadajiIn28-Jun-12 23:26 
QuestionSomething of strange [modified] Pin
lucio.menci24-Feb-10 4:59
lucio.menci24-Feb-10 4:59 
AnswerRe: Something of strange Pin
mariscn25-Feb-10 8:59
mariscn25-Feb-10 8:59 
QuestionNeed source code, please. Pin
Jerold Anderson20-Feb-10 20:52
Jerold Anderson20-Feb-10 20:52 
GeneralOutput quality [modified] Pin
JPaula18-Feb-10 8:57
JPaula18-Feb-10 8:57 
GeneralRe: Output quality Pin
mariscn20-Feb-10 7:49
mariscn20-Feb-10 7:49 
GeneralVery cool Pin
Sacha Barber18-Feb-10 0:11
Sacha Barber18-Feb-10 0:11 
GeneralRe: Very cool Pin
mariscn20-Feb-10 7:50
mariscn20-Feb-10 7:50 
GeneralSource Code Pin
David Goudie15-Feb-10 18:00
David Goudie15-Feb-10 18:00 
GeneralExcellant Pin
NeoPunk13-Feb-10 20:28
NeoPunk13-Feb-10 20:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.