Делаем скриншот ASP.NET страницы на сервере

Был вопрос на форумах MSDN, откуда и возникла идея статьи. Хотя пока в своей практике не довелось видеть случаев где подобное могло бы понадобиться. Но всё же, раз это кому-нибудь было нужно, думаю стоит показать кам можно такое реализовать. Начнём с создания простого шаблонного проекта ASP.NET 4, которая предлагает нам Visual Studio 2010 по умолчанию. Добавим на страницу Default.aspx кнопку и рисунок.
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" 
  AutoEventWireup="true" CodeBehind="Default.aspx.cs" 
  Inherits="WebPageScreenShot._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
  <h2>
    Welcome to ASP.NET!
  </h2>
  <p>
    To learn more about ASP.NET visit <a href="http://www.asp.net" 
    title="ASP.NET Website">
      www.asp.net</a>.
  </p>
  <p>
    You can also find <a href="http://go.microsoft.com/fwlink/?
    LinkID=152368&amp;clcid=0x409"
      title="MSDN ASP.NET Docs">documentation on ASP.NET at MSDN</a>.
  </p>
  <asp:Button ID="DrawImageButton" runat="server" 
   Text="Сохранить как рисунок." OnClick="DrawImageButton_Click" />
  <asp:Image ID="DrawPageImage" runat="server" />
</asp:Content>
Далее, инкапсулируем код который будет создавать изображение страницы в отдельный класс и назовём его WebPageToImage. Основная идея генерации рисунка, заключается в использовании класса WebBrowser из WinForms (не забываем добавить в проект ссылку на сборку System.Windows.Forms.dll) , который и будет получать нашу разметку, в виде Html страницы, и сохранять его как рисунок с помощью метода DrawToBitmap(). А так как WebBrowser это всего-навсего обёртка вокруг IE, то необходимо, чтобы страница отображалась коректно в браузере Internet Explorer. Код нашего класса приведён ниже.
namespace WebPageScreenShot
{
  public class WebPageToImage
  {
    private string url;
    private string filePath = string.Empty;
    public WebPageToImage(string url, string path)
    {
      this.url = url;
      this.filePath = path;
    }
    public void Generate()
    {
      //Запускаем компонент браузера в отдельном потоке, так как это обязательно,
      //в текущем потоке ASP.NET он просто не запустится.
      Thread thread = new Thread(() =>
      {
        WebBrowser browser = new WebBrowser { ScrollBarsEnabled = false };
        browser.Navigate(url);
        browser.DocumentCompleted += WebBrowser_DocumentCompleted;
        while (browser.ReadyState != WebBrowserReadyState.Complete)
        {
          Application.DoEvents();
        }
        //Не забываем своевременно освободить системные ресурсы,
        //так как WebBrowser интенсивно их потребляет.
        browser.Dispose();
      });
      thread.SetApartmentState(ApartmentState.STA);
      thread.Start();
      //Блокируем вызывающий поток, до завершения
      //текущего, хотя это и не лучшая идея.
      thread.Join();
    }
    private void WebBrowser_DocumentCompleted(object sender, 
      WebBrowserDocumentCompletedEventArgs e)
    {
      WebBrowser browser = (WebBrowser)sender;
      browser.ClientSize = new Size(browser.Document.Body.ScrollRectangle.Width, 
        browser.Document.Body.ScrollRectangle.Bottom);
      browser.ScrollBarsEnabled = false;
      Bitmap image = new Bitmap(browser.Document.Body.ScrollRectangle.Width, 
        browser.Document.Body.ScrollRectangle.Bottom);
      browser.BringToFront();
      browser.DrawToBitmap(image, browser.Bounds);
      SaveImage(image, filePath);
    }
    private void SaveImage(Bitmap image, string path)
    {
      //Создаём массив объектов с параметрами для передачи кодировщику.
      //Используем только один параметр, для данного случая.
      EncoderParameters encoderParameters = new EncoderParameters(1);
      //Единственному элементу присваиваем новый параметр.
      encoderParameters.Param[0] = 
        new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
      //Получаем объект кодировщика с нужными нам свойствами.
      ImageCodecInfo imageCodec = ImageCodecInfo.GetImageDecoders()
        .Where(c => c.FormatID == ImageFormat.Jpeg.Guid).First();
      //Сохраняем изображение.
      image.Save(path, imageCodec, encoderParameters);
    }
  }
}
Осталось добавить код страницы, чтобы всё заработало.
namespace WebPageScreenShot
{
  public partial class _Default : System.Web.UI.Page
  {
    private bool drawPageToImage = false;
    private string fileName;
    protected void Page_Load(object sender, EventArgs e)
    {
    }
    protected void DrawImageButton_Click(object sender, EventArgs e)
    {
      fileName = Guid.NewGuid().ToString();
      drawPageToImage = true;
      DrawPageImage.ImageUrl = "~/Images/" + fileName + ".jpg";
    }
    protected override void Render(HtmlTextWriter writer)
    {
      if (drawPageToImage)
      {
        StringBuilder stringBuilder = new StringBuilder();
        StringWriter stringWriter = new StringWriter(stringBuilder);
        HtmlTextWriter htmlTextWriter = new HtmlTextWriter(stringWriter);
        base.Render(htmlTextWriter);
        //Получаем разметку в виде строки.
        string htmlString = stringBuilder.ToString();
        //Пишем разметку в новый файл расположенный в корневой директории. Это важно, 
        //так как пути к css и другим файлам относительны, и если файл 
        //разместить в другом месте, страница будет отображаться неправильно
        // и скриншот будет не тот.
        File.WriteAllText(Server.MapPath("~/") + fileName + ".htm", htmlString);
        writer.Write(htmlString);
        string htmlPageUrl = Request.Url.GetLeftPart(UriPartial.Authority) 
          + ResolveUrl("~/" + fileName + ".htm");
        //Создаём объект нашего класса генератора.
        WebPageToImage webSiteToImage = 
          new WebPageToImage(htmlPageUrl, Server.MapPath("~/Images/" + fileName + ".jpg"));
        //Генерируем изображение.
        webSiteToImage.Generate();
        //Перемещаем файл разметки в новое хранилище, или можно даже удалить его.
        Directory.Move(Server.MapPath("~/") + fileName + ".htm", 
          Server.MapPath("~/Pages/") + fileName + ".htm");
      }
      else
      {
        base.Render(writer);
      }
    }
  }
}
А почему нужно было заморачиваться и писать разметку в отдельный файл, а потом его запрашивать? Ведь можно было запрашивать страницу напрямую из кода? Дело в том, что кода будет выполнена аутентификация и авторизация, а на странице будут отображены данные зависящие от текущей сессии, то данный вариант наиболее легковесный. Иначе нужно было бы заново выполнить вход и делать разделение данных текущей сессии, чтобы WebBrowser отображал идентичную той странице страницу, которая открыта у пользователя в данный момент. А это очень непросто и нереализуемо, если не используется БД для хранения сеанса пользователя. Ну а если нет необходимости в высшеперечисленном, то не нужно заниматься шаманством, а можно запрашивать страницу напрямую, и всё будет намного проще.