Click here to Skip to main content
15,309,756 members
Articles / Programming Languages / C#
Article
Posted 22 Aug 2015

Tagged as

Stats

36.9K views
44 bookmarked

Take screenshots of websites for different screen sizes

Rate me:
Please Sign up or sign in to vote.
4.95/5 (26 votes)
22 Aug 2015CPOL7 min read
How to take screenshots of websites and emulate different devices and screen sizes in C#.

Introduction

A common requirement for modern applications is to take a screenshot of a website after a user has entered an URL. There's 2 principal use cases: 

  • the client wants to take a screenshot and save the image for later comparison (think web testing or approval of content before publication)
  • the client wants to show the website image next to a link (think portfolio)

At first this may sound like a simple requirement, but given that nowadays we have multiple devices (desktops, tablets, phones...) and that a well-respected (i.e. responsive) website will look different on each of them, there is an additional level of complexity.

I've therefore decided to write a C# library to answer this problem. I've published the source code on GitHub and you can also download and install the NuGet package Web.Screenshot from nuget.org.

In this article, I'm first going to go through the library API, so you know how to use it. Then, I'll go through the code to explain its implementation. 

Using Web.Screenshot NuGet package

Installing Web.Screenshot NuGet package

To fastest way to see what Web.Screenshot has to offer is to create a new Console project in Visual Studio. Click on File > New > Project and then select Console Application:

01 - Create new console application.png

Then, right click on References and click on Manage NuGet Packages...

1020939/02_-_Manager_NuGet_packages.png

Search for Web.Screenshot and select the package:

1020939/03_-_Select_package.png

Then click Install.

Once the installation is completed, you should see a new assembly listed in references called: Alx.Web.Screenshot

Using Web.Screenshot API

The main class is a static class called Screenshot which you can find in Alx.Web namespace. It offers 2 main methods (with some overrides):

  • Take(): takes a screenshot of a website and return an Image result
  • Save(): saves the website screenshot as an image file on the disk

Here are the overrides available:

C#
public static Image Take(string url, Devices device = Devices.Desktop)

public static Image Take(string url, Size size)

public static void Save(string url, string path, ImageFormat format, Devices device = Devices.Desktop)

public static void Save(string url, string path, ImageFormat format, Size size)

The Image, Size and ImageFormat classes are the built-in .NET classes available from System.Drawing.

Devices is an enum defined in Alx.Web and its values are:  PhonePortrait, PhoneLandscape, TabletPortrait, TabletLandscape, Desktop.

Let's write some code now. In programme.cs, update the main method to the following code:

C#
static void Main(string[] args)
{
    Console.WriteLine("Application started");
    const string url = "http://www.bbc.co.uk/news";

    var device = Devices.Desktop;
    var path = String.Format(@"C:\Temp\website-{0}.png", device);
    Alx.Web.Screenshot.Save(url, path, ImageFormat.Png, device);

    Console.WriteLine("Saved " + path);

    device = Devices.TabletLandscape;
    path = String.Format(@"C:\Temp\website-{0}.png", device);
    Alx.Web.Screenshot.Save(url, path, ImageFormat.Png, device);

    Console.WriteLine("Saved " + path);

    device = Devices.PhonePortrait;
    path = String.Format(@"C:\Temp\website-{0}.png", device);
    Alx.Web.Screenshot.Save(url, path, ImageFormat.Png, device);

    Console.WriteLine("Saved " + path);

    Console.WriteLine("Press [Enter] to exit...");
    Console.ReadLine();
}

The code is pretty much self-explanatory: it will save 3 screenshots of the BBC news website in 3 different formats: Desktop, Tablet (landscape) and Phone (portrait). The files will be saved in C:\Temp, but you may want to use a different folder. Here I save the files using the PNG format, but you could use JPG or any other image format defined in System.Drawing

Here's the front page for today (21st Aug 2015):

Desktop

1020939/website-Desktop.png

Tablet

1020939/website-TabletLandscape.png

Phone

1020939/website-PhonePortrait.png

Using Web.Screenshot, it should now be easy to take screenshots of any websites. 

If you are interested in how Web.Screenshot is actually implemented, then read on. Web.Screenshot's source code is also publicly available on GitHub, so feel free to contribute and/or branch to add more functionality. 

Web.Screenshot implementation

Now, let's look at Web.Screenshot source code. As already mentionned, you can download Web.Screenshot code from GitHub. 

Let's start with the easy one: Devices.cs

C#
public enum Devices
{
    [Size(375, 667)]    // Screen size for iPhone 6
    PhonePortrait,

    [Size(667, 375)]    // Screen size for iPhone 6
    PhoneLandscape,

    [Size(600, 960)]   // Screen size for Google Nexus 7
    TabletPortrait,

    [Size(960, 600)]   // Screen size for Google Nexus 7
    TabletLandscape,

    [Size(1920, 1080)]  // Screen size for Dell XPS12
    Desktop
}

Quite self-explanatory again: there's 5 values, for 3 different devices, some of them in 2 different positions (landscape and portrait). 

You will notice the Size attribute which specifies the width and the height of the screen for each value. 

Here is the code for SizeAttribute.cs:

C#
public class SizeAttribute : Attribute
{
    public int Width { get; set; }
    public int Height { get; set; }

    public Size Size
    {
        get { return new Size(Width, Height); }
    }

    public SizeAttribute(int width, int height)
    {
        Width = width;
        Height = height;
    }
}

I've created an extension method to read Enum's attributes. It's in EnumExtensions.cs:

C#
public static class EnumHelper
{
    public static T GetAttribute<T>(this Enum enumVal) where T : Attribute
    {
        var type = enumVal.GetType();
        var memInfo = type.GetMember(enumVal.ToString());
        var attributes = memInfo[0].GetCustomAttributes(typeof(T), false);
        return (attributes.Length > 0) ? (T)attributes[0] : null;
    }
}

And the static Screenshot class has this method to read the screen size from the Enum value:

C#
public static Size GetSize(Devices device)
{
    var attribute = device.GetAttribute<SizeAttribute>();

    if (attribute == null)
    {
        throw new DeviceSizeException(device);
    }

    return attribute.Size;
}

Now that we've got that in place, let's take a look at the Take() methods. Remember, the Take() methods takes the website screenshot and return a System.Drawing.Image object. The code is also in Screenshot.cs:

C#
public static Image Take(string url, Devices device = Devices.Desktop)
{
    var size = GetSize(device);
    return Take(url, size);
}

public static Image Take(string url, Size size)
{
    var result = Capture(url, size);
    return result;
}

Nothing too complicate so far. Both methods will end up calling Capture() with 2 arguments: the website url and the device screen's size. Now, let's take a look at the Capture() method, this is where the fun begins:  

C#
private static Image Capture(string url, Size size)
{
    Image result = new Bitmap(size.Width, size.Height);

    var thread = new Thread(() =>
    {
        using (var browser = new WebBrowser())
        {
            browser.ScrollBarsEnabled = false;
            browser.AllowNavigation = true;
            browser.Navigate(url);
            browser.Width = size.Width;
            browser.Height = size.Height;
            browser.ScriptErrorsSuppressed = true;
            browser.DocumentCompleted += (sender,args) => DocumentCompleted(sender, args, ref result);

            while (browser.ReadyState != WebBrowserReadyState.Complete)
            {
                Application.DoEvents();
            }
        }
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    thread.Join();

    return result;
}

First thing first: WebBrowser controls cannot be instantiated if the current thread is not in a 
single-threaded apartment. To be sure to run in a single-threaded apartment, we create a new thread and we set the apartment state to ApartmentState.STA

We set a few properties on the browser control:

  • to disable scroll bars
  • to set the width and the height
  • to suppress scripts errors

We call Navigate(url) to navigate to the requested URL. 

We set the DocumentCompleted event, which will be fired when the page is loaded and the document is ready. 

Then we enter the while loop and process events until the browser state is set to Complete

Finally, we start the thread (thread.Start()), we wait for it to complete (thread.Join()) and we return the result (of type Image). The thread will complete when the browser state is set to Complete. The DocumentCompleted event would be fired just before. Here's the code for the event handler: 

C#
private static void DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e, ref Image image)
{
    var browser = sender as WebBrowser;

    if (browser == null) throw new Exception("Sender should be browser");
    if (browser.Document == null) throw new Exception("Document is missing");
    if (browser.Document.Body == null) throw new Exception("Body is missing");

    using (var bitmap = new Bitmap(browser.Width, browser.Height))
    {
        browser.DrawToBitmap(bitmap, new Rectangle(0, 0, browser.Width, browser.Height));
        image = (Image)bitmap.Clone();
    }
}

First, we perform a few sanity checks, then we create a new Bitmap of the same size as the web browser. We call browser.DrawToBitmap() to copy the content of the browser as an image into the bitmap variable. Then, we set the image variable (which was passed by reference) with a clone of the bitmap (because bitmap will be disposed after we exit the using block, and therefore wouldn't be available to the calling method). 

That's about it. Thanks to the WebControl .NET class, just a few lines of code were sufficient to write this library. 

Now that we've managed to take a screenshot of a website and stored it into an Image object, we can take a look at the Save() methods:

C#
public static void Save(string url, string path, ImageFormat format, Devices device = Devices.Desktop)
{
    var size = GetSize(device);
    Save(url, path, format, size);
}

public static void Save(string url, string path, ImageFormat format, Size size)
{
    var image = Take(url, size);

    using (var stream = new MemoryStream())
    {
        image.Save(stream, format);
        var bytes = stream.ToArray();
        File.WriteAllBytes(path, bytes);
    }
}

The Save() methods simply call the Take() method and saves the Image object to the disk in the requested image format. 

Points of Interest

If you too need to take screenshots of websites, then I hope you'll find this library useful. I hope you'll also find the possibility to take screenshots for different devices screen sizes interesting. 

From a code point of view, and depending on your experience with the .NET framework, you may find the following points interesting: 

  • The use of WebControl outside of a Windows Forms Application
  • The creation of a new thread in a single-threaded apartment
  • The use of custom attribute on Enum values
  • An example of extension methods
  • How to save an Image object as an image file on the disk

Web.Screenshot is available as a Nuget package from nuget.org. 

Web.Screenshot source code is available on GitHub. Feel free to branch and contribute to add more features.

Also, feel free to submit any issues and/or suggestions. 

Alternative Solutions

Selenium WebDriver

Another approach I've investigated before creating this library is to use Selenium.WebDriver

Selenium.WebDriver offers a lot more than simply taking screenshots of websites. Once the page is loaded, it can navigate to another URL by simulating a click on a link. It can also read content of the page, as well as set content, which can be very useful for completing forms. Also, it can emulate different browsers by creating different drivers, for instance Firefox driver or Chrome driver. 

PhantomJS

PhantomJS is another library which I've investigated. PhantomJS is a headless browser and its functionality are very similar to those of Selenium.WebDriver. PhantomJS can be scripted with JavaScript. One of its down side though is its heavy weight: 47 MB. 

So why not using them?

The reason why I've decided to create Web.Screenshot is that I've found both Selenium.WebDriver and PhantomJS to be unreliable when it comes to internal website (intranet) using Windows Authentication with Kerboros. In fact, from my reading and from my understanding I believe PhantomJS does not support Windows Authentication at all (the authentication header is missing from the request, which results in access denied error messages). 

If you've managed to get PhantomJS working with Windows Authentication (Kerboros), please let me know as I'd like to understand what setting was missing.  

Having said that, I've used both Selenium.WebDriver and PhantomJS for testing on other projects and they worked like a charm!

History

  • [22/08/2015]: first version

 

License

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

Share

About the Author

Alex Sanséau
Technical Lead
United Kingdom United Kingdom
Alex is an experienced .NET professional with strong technical skills and management
experience. Alex is specialised in Microsoft stack and web technology (C#, MVC, Azure, T-SQL, CSS, jQuery...) and he's passionate about software development best practices.

Comments and Discussions

 
QuestionNot getting any response Pin
Member 1023664123-Oct-19 23:42
MemberMember 1023664123-Oct-19 23:42 
GeneralMy vote of 5 Pin
Dmitriy Gakh13-Sep-15 8:09
professionalDmitriy Gakh13-Sep-15 8:09 
QuestionTaking screenshot of injected scripts Pin
Member 1193788926-Aug-15 6:09
MemberMember 1193788926-Aug-15 6:09 
AnswerRe: Taking screenshot of injected scripts Pin
Alex Sanséau26-Aug-15 7:59
MemberAlex Sanséau26-Aug-15 7:59 
QuestionWhy it does't work on my computer? Pin
Software .NET24-Aug-15 15:19
MemberSoftware .NET24-Aug-15 15:19 
AnswerRe: Why it does't work on my computer? Pin
Alex Sanséau25-Aug-15 5:21
MemberAlex Sanséau25-Aug-15 5:21 
GeneralRe: Why it does't work on my computer? Pin
Software .NET25-Aug-15 15:35
MemberSoftware .NET25-Aug-15 15:35 
GeneralRe: Why it does't work on my computer? Pin
Alex Sanséau25-Aug-15 23:07
MemberAlex Sanséau25-Aug-15 23:07 
GeneralRe: Why it does't work on my computer? Pin
Software .NET26-Aug-15 4:09
MemberSoftware .NET26-Aug-15 4:09 
GeneralRe: Why it does't work on my computer? Pin
Alex Sanséau26-Aug-15 7:50
MemberAlex Sanséau26-Aug-15 7:50 
QuestionWhy I got the blank Image? Pin
Software .NET24-Aug-15 14:54
MemberSoftware .NET24-Aug-15 14:54 
AnswerRe: Why I got the blank Image? Pin
Alex Sanséau25-Aug-15 5:22
MemberAlex Sanséau25-Aug-15 5:22 
QuestionI tried this to get the result but getting blank image. Pin
jaswanthb23-Aug-15 23:52
professionaljaswanthb23-Aug-15 23:52 
AnswerRe: I tried this to get the result but getting blank image. Pin
Alex Sanséau24-Aug-15 0:27
MemberAlex Sanséau24-Aug-15 0:27 
GeneralRe: I tried this to get the result but getting blank image. Pin
jaswanthb24-Aug-15 2:58
professionaljaswanthb24-Aug-15 2:58 
GeneralRe: I tried this to get the result but getting blank image. Pin
Alex Sanséau24-Aug-15 3:10
MemberAlex Sanséau24-Aug-15 3:10 
GeneralRe: I tried this to get the result but getting blank image. Pin
jaswanthb24-Aug-15 20:41
professionaljaswanthb24-Aug-15 20:41 

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.