Creating Browser Screenshots on the Server Side

In the light of the plethora of gloomy headlines in the past few weeks I have started taking screenshots of some of the major web sites online, maybe just to have an archive of how the world ended one day :-)

I started thinking there’s got to be a way to automate this. Indeed, there are a number of downloadable utilities out there to create JPEG images of web sites, but I really wanted to build this myself. After some initial online research I decided to give it a spin in C#. I came across the System.Windows.Forms.WebBrowser class which, as it turns out, can do all these magic things for me directly on the server-side without requiring a desktop application.

The caveat is that the WebBrowser does need to execute in a standalone STA thread. As a side-note, the code is a total memory-hog the way it is written now, mostly due to IE.

Check out a single-threaded version of the code right here (full download at bottom of page):

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.Windows.Forms;

namespace JToEE
{
    public class Shoot
    {
        /// <summary>
        /// Set URL for the screenshot
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// Output filename
        /// </summary>
        public string Filename { get; set; }

        /// <summary>
        /// Width of the browser window
        /// </summary>
        private int? Width { set; get; }

        /// <summary>
        /// Initial height of the browser (get re-sized 
        /// automatically if the page is longer)
        /// </summary>
        private const int DefaultHeight = 768;

        /// <summary>
        /// Default width of the browser if not specified. This
        /// class doesn't re-size the width.
        /// </summary>
        private const int DefaultWidth = 1024;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <param name="filename"></param>
        public Shoot(string url, string filename) : this()
        {
            Url = url;
            Filename = filename;
        }

        /// <summary>
        /// 
        /// </summary>        
        public Shoot()
        {
            Width = 1024;
        }

        /// <summary>
        /// Main entry method which created the screenshot
        /// </summary>
        public void CreateScreenshot()

        {
            ThreadStart ts = Run;
            Thread t = new Thread(ts);
            t.SetApartmentState(ApartmentState.STA);
            t.Start();
            t.Join();
        }

        /// <summary>
        /// Does the work in the thread
        /// </summary>
        private void Run()
        {
            using(Bitmap bitmap = CreateBitmap())
            {
                ImageCodecInfo jgpEncoder = GetEncoder(ImageFormat.Jpeg);
                var myEncoder = System.Drawing.Imaging.Encoder.Quality;
                var myEncoderParameters = new EncoderParameters(1);
                // encode jpeg at 75%
                var myEncoderParameter = new EncoderParameter(myEncoder, 75L);
                myEncoderParameters.Param[0] = myEncoderParameter;
                // save to file
                bitmap.Save(Filename, jgpEncoder, myEncoderParameters);
            }
        }

        /// <summary>
        /// Get a hold of a jpeg encoder
        /// </summary>
        /// <param name="format"></param>
        /// <returns></returns>
        private static ImageCodecInfo GetEncoder(ImageFormat format)
        {
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
            foreach (ImageCodecInfo codec in codecs)
            {
                if (codec.FormatID == format.Guid)
                {
                    return codec;
                }
            }
            return null;
        }

        /// <summary>
        /// Create a bitmap of the browser window. The bitmap's height is
        /// automatically adapted to the length of the page while the width
        /// is kept at the specified value.
        /// </summary>
        /// <returns></returns>
        private Bitmap CreateBitmap()
        {
            using (var browser = new WebBrowser())
            {
                browser.ScrollBarsEnabled = false;
                // set the initial height and width
                browser.Size = new Size(Width ?? 1024, DefaultHeight);
                browser.ScriptErrorsSuppressed = true;
                // supress creation of new windows
                browser.NewWindow += OnNewWindow;
                // navigate the browser to the URL
                browser.Navigate(Url);
                // wait for it to load
                WaitForBrowserReady(browser);
                // re-size the browser windows to the full height of the web page
                var finalHeight = browser.Document == null ? DefaultHeight :
                                      browser.Document.Body.ScrollRectangle.Height;
                // width of the bitmap
                var width = Width ?? DefaultWidth;
                // re-size the browser
                browser.Size = new Size(width, finalHeight);
                WaitForBrowserReady(browser);
                // create a bitmap                
                Console.WriteLine(string.Format("Creating bitmap of size {0}x{1}", width, finalHeight));
                var bitmap = new Bitmap(width, finalHeight);
                var rect = new Rectangle(0, 0, width, finalHeight);
                browser.DrawToBitmap(bitmap, rect);
                return bitmap;
            }
        }

        /// <summary>
        /// Waits for the browser to be ready loading the page
        /// </summary>
        /// <param name="browser"></param>
        private static void WaitForBrowserReady(WebBrowser browser)
        {
            Console.WriteLine("Waiting for browser..");
            while (browser.ReadyState != WebBrowserReadyState.Complete)
            {
                Application.DoEvents();
            }
            Console.WriteLine("Waiting ready");
        }

        /// <summary>
        /// New window delegate -- basically a popup blocker.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnNewWindow(object sender, CancelEventArgs e)
        {
            e.Cancel = true;
        }
    }
}

Download the full source code here.


Comments

Leave a Reply