0

What I'm trying to do is the following:

I've got a CefSharp ChromiumWebBrowser (WPF control), and I would like to take a screenshot of the webpage in that browser. The on-screen ChromiumWebBrowser has no method for taking screenshots. But I can obtain the rendering by attaching an event handler to the OnPaint event of the browser. This way I get a Bitmap that is the screenshot. The process is based on this answer: https://stackoverflow.com/a/54236602/2190492

Now I'm creating a class CefSharpScreenshotRecorder that should be responsible for taking screenshots. It should accept a browser instance, attaches an event handler to the OnPaint event, and gets the bitmap. All the state of this process should be encapsulated in that CefSharpScreenshotRecorder class. I would like to be able to use my class asynchronously. Since we have to wait until the OnPaint event is triggered. When that event is triggered (and event handler called), a Bitmap is available in the event handler. Then this Bitmap should be the result of the asynchronous method that was originally called (like CefSharpScreenshotRecorder.TakeScreenshot(...cefBrowserInstance...). Everything must happen without blocking/lagging the UI of course.

I'm not very familiar with asynchronous programming in C#. The problem I have is that I can't find a way to make an awaitable method, that only returns on behalf of the OnPaint event handler when it is called. I don't even know if any code features exist to create this logic.

BionicCode
  • 1
  • 4
  • 28
  • 44
user2190492
  • 1,174
  • 2
  • 9
  • 37
  • 2
    How did you decide any of this had to be asynchronous? – Andy Aug 28 '19 at 15:28
  • @Andy Maybe somebody likes to design an API to be asynchronous rather than event driven. – BionicCode Aug 28 '19 at 17:18
  • 2
    Waiting for the paint event to be triggered, is litarally waiting. You have to keep a state for this process, and I wanted to encapsulate this into a separate class that can be used by simply calling an asynchronous method on that class. This doesn't block, and from outside the class you don't have to worry about states or attaching/removing event handlers. – user2190492 Aug 28 '19 at 18:07
  • @user2190492 I updated my answer to show a simpler way to get a screenshot using the `CefSharp.OffScreen` API. – BionicCode Aug 31 '19 at 04:45

2 Answers2

5

This can be achieved using TaskCompletionSource. This way you can wrap synchronous (e.g. event-driven) code into an asynchronous method without using Task.Run.

class CefSharpScreenshotRecorder
{
  private TaskCompletionSource<System.Drawing.Bitmap> TaskCompletionSource { get; set; }

  public Task<System.Drawing.Bitmap> TakeScreenshotAsync(
    ChromiumWebBrowser browserInstance, 
    TaskCreationOptions optionalTaskCreationOptions = TaskCreationOptions.None)
  {
    this.TaskCompletionSource = new TaskCompletionSource<System.Drawing.Bitmap>(optionalTaskCreationOptions);

    browserInstance.Paint += GetScreenShotOnPaint;

    // Return Task instance to make this method awaitable
    return this.TaskCompletionSource.Task;
  }

  private void GetScreenShotOnPaint(object sender, PaintEventArgs e)
  { 
    (sender as ChromiumWebBrowser).Paint -= GetScreenShotOnPaint;

    System.Drawing.Bitmap newBitmap = new Bitmap(e.Width, e.Height, 4 * e.Width, PixelFormat.Format32bppPArgb, e.Buffer);

    // Optional: save the screenshot to the hard disk "MyPictures" folder
    var screenshotDestinationPath = Path.Combine(
      Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), 
      "CefSharpBrowserScreenshot.png");
    newBitmap.Save(screenshotDestinationPath);

    // Create a copy of the bitmap, since the underlying buffer is reused by the library internals
    var bitmapCopy = new System.Drawing.Bitmap(newBitmap);

    // Set the Task.Status of the Task instance to 'RanToCompletion'
    // and return the result to the caller
    this.TaskCompletionSource.SetResult(bitmapCopy);
  }

  public BitmapImage ConvertToBitmapImage(System.Drawing.Bitmap bitmap)
  {
    using(var memoryStream = new MemoryStream())
    {
      bitmap.Save(memoryStream, ImageFormat.Png);
      memoryStream.Position = 0;

      BitmapImage bitmapImage = new BitmapImage();
      bitmapImage.BeginInit();
      bitmapImage.StreamSource = memoryStream;
      bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
      bitmapImage.EndInit();
      bitmapImage.Freeze();
    }
  }
}

Usage example (working):

MainWindow.xaml

<Window>
  <StackPanel>
    <Button Click="TakeScreenshot_OnClick" Height="50" Content="Take Screenshot"/>
    <ChromiumWebBrowser x:Name="ChromiumWebBrowser"
                        Width="500"
                        Height="500"
                        Address="https://stackoverflow.com/a/57695630/3141792" />
    <Image x:Name="ScreenshotImage" />
  </StackPanel>
</Window>

MainWindow.xaml.cs

private async void TakeScreenshot_OnClick(object sender, RoutedEventArgs e)
{
  var cefSharpScreenshotRecorder = new CefSharpScreenshotRecorder();
  System.Drawing.Bitmap bitmap = await cefSharpScreenshotRecorder.TakeScreenshotAsync(this.ChromiumWebBrowser);

  this.ScreenshotImage.Source = cefSharpScreenshotRecorder.ConvertToBitmapImage(bitmap);
}

Edit

In case you are just interested in taking a snapshot from a web page then take a look at CefSharp.OffScreen (available via the NuGet package manager). The ChromiumWebBrowser class exposes a ScreenshotAsync method that returns a ready to use System.Drawing.Bitmap. Here is an example from the project repository on GitHub.

Example:

class CefSharpScreenshotRecorder
{
  private TaskCompletionSource<System.Drawing.Bitmap> TaskCompletionSource { get; set; }

  public async Task<System.Drawing.Bitmap> TakeScreenshotAsync(
    ChromiumWebBrowser browser, 
    string url, 
    TaskCreationOptions optionalTaskCreationOptions = TaskCreationOptions.None)
  {
    if (!string.IsNullOrEmpty(url))
    {
      throw new ArgumentException("Invalid URL", nameof(url));
    }

    this.TaskCompletionSource = new TaskCompletionSource<Bitmap>(optionalTaskCreationOptions);

    // Load the page. In the loaded event handler 
    // take the snapshot and return it asynchronously it to caller
    return await LoadPageAsync(browser, url);
  }

  private Task<System.Drawing.Bitmap> LoadPageAsync(IWebBrowser browser, string url)
  {
    browser.LoadingStateChanged += GetScreenShotOnLoadingStateChanged;

    browser.Load(url);

    // Return Task instance to make this method awaitable
    return this.TaskCompletionSource.Task;
  }

  private async void GetScreenShotOnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
  { 
    browser.LoadingStateChanged -= GetScreenShotOnLoadingStateChanged;

    System.Drawing.Bitmap screenshot = await browser.ScreenshotAsync(true);

    // Set the Task.Status of the Task instance to 'RanToCompletion'
    // and return the result to the caller
    this.TaskCompletionSource.SetResult(screenshot);
  }
}

Usage example:

public async Task CreateScreenShotAsync(ChromiumWebBrowser browserInstance, string url)
{
  var recorder = new CefSharpScreenshotRecorder();   
  System.Drawing.Bitmap screenshot = await recorder.TakeScreenshotAsync(browserInstance, url);
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • You can turn this into an extension method to make it slightly easier to use. Similar pattern as outlined in https://github.com/cefsharp/CefSharp/blob/cefsharp/75/CefSharp.OffScreen.Example/Program.cs#L117 is what I would use. – amaitland Aug 28 '19 at 21:18
  • @amaitland Thank you for the link. Extension methods indeed make your code look better. I agree. But in this case I think I would stay away from any static method. The advantage of encapsulating the snapshot logic in a (non-static) class is that it increases testability. When writing unit tests for a class that uses the snapshot class I can easily mock the underlying logic away (loading a web page, converting the snapshot to a bitmap, waiting for an event). Coupling code to static code reduces testability as you can't mock (replace) static dependencies (without modifying the code). – BionicCode Aug 28 '19 at 21:39
  • Is there a `WinForms` equivalent? `e.Width`, etc are not available. – TEK Aug 30 '19 at 22:49
  • @TEK Yes there is. You only have to use the WinForms library instead. Check _NuGet Package Manager_ for _"CefSharp.WinForms"_ or download it from GtiHub: [CefSharp.WinForms](https://github.com/cefsharp/CefSharp/tree/master/CefSharp.WinForms) (example: [CefSharp.WinForms.Example](https://github.com/cefsharp/CefSharp/tree/master/CefSharp.WinForms.Example). – BionicCode Aug 31 '19 at 01:18
  • @TEK If you are interested in taking a snapshot then take a look at [CefSharp.OffScreen](http://cefsharp.github.io/api/75.1.x/html/N_CefSharp_OffScreen.htm). The ' ChromiumWebBrowser' class exposes a [`ScreenshotAsync`](http://cefsharp.github.io/api/75.1.x/html/M_CefSharp_OffScreen_ChromiumWebBrowser_ScreenshotAsync.htm#!) method that returns a ready to use bitmap. `Here`(https://github.com/cefsharp/CefSharp/blob/master/CefSharp.OffScreen.Example/Program.cs) is an example. Just call the async metod to get the bitmap. Quite simple API. – BionicCode Aug 31 '19 at 03:59
  • @TEK I updated my answer to show an example. This will work with WinForms too. – BionicCode Aug 31 '19 at 04:51
  • As with the other answer, PixelFormat should be PixelFormat.Format32bppPArgb as transparency is supported. The buffer shouldn't be used outside the scope of the Paint event, it's often reused internally and could potentially even be freed. A copy should be taken. I've improved the xml doc to make these requirements clearer, relevant commit is https://github.com/cefsharp/CefSharp/commit/518b4d286d21333ef3aaa54821c8134d1c753f9f – amaitland Sep 24 '19 at 20:41
  • @amaitland Thank you very much for the valuable hint. I think having the API returning a copy of internal buffers by default would be more convenient. Also a `PaintEventArgs.BufferLength` property would be nice, especially when the client code is expected to make a copy from the buffer. Maybe you like to modify the source code. I think this would make sense. Maybe I should request this at your project repository? – BionicCode Sep 26 '19 at 12:07
  • For performance reasons unnessicary copies must be avoided at all costs. A pointer to the raw buffer is the most flexible option, there are many cases where you might say wish to generate s Writablebitmap, and copy the buffer directly into the backbuffer. You could also do the same with a GDI bitmap. Performance and flexibility are more important than convenience in this instance. I have no plans to add a buffer length property, you are welcome to submit a PR for consideration for that property. – amaitland Sep 26 '19 at 18:57
  • The PixelFormat still needs to be updated. I'd be surprised if your OffScreen example actually compiled, I don't See a URL being passed in. – amaitland Sep 26 '19 at 19:27
1

You don't really need a separate class to hold the state. You could use a local function (or an Action<object, PaintEventArgs> delegate) and the compiler will generate a class for you to hold the state, if there is any state. These hidden classes are known as closures.

public static Task<Bitmap> TakeScreenshotAsync(this ChromiumWebBrowser source)
{
    var tcs = new TaskCompletionSource<Bitmap>(
        TaskCreationOptions.RunContinuationsAsynchronously);
    source.Paint += ChromiumWebBrowser_Paint;
    return tcs.Task;

    void ChromiumWebBrowser_Paint(object sender, PaintEventArgs e)
    {
        source.Paint -= ChromiumWebBrowser_Paint;
        using (var temp = new Bitmap(e.Width, e.Height, 4 * e.Width,
            PixelFormat.Format32bppPArgb, e.Buffer))
        {
            tcs.SetResult(new Bitmap(temp));
        }
    }
}

The option TaskCreationOptions.RunContinuationsAsynchronously ensures that the continuation of the task will not run synchronously in the UI thread. Of course if you await the task without configureAwait(false) in the context of a WPF application, the continuation will then be rescheduled to run in the UI thread, since configureAwait(true) is the default.

As a general rule, I would say any usage of TaskCompletionSource should specify TaskCreationOptions.RunContinuationsAsynchronously. Personally, I think the semantics are more appropriate and less surprising with that flag. [citation]


Disclaimer: the part of the code that creates the bitmap is copied from another answer and then modified (see comments), but has not been tested.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I also prefer the succinctness of using an inline method/delegate. `CefSharp` only requires `.Net 4.5.2` and this code requires a minimum of `.Net 4.6` as a disclaimer for anyone looking to use this. You'd also need a newer `C#` compiler that supports inline methods, from memory you'd need at least `VS2017` or the `Microsoft.Net.Compilers Nuget Package`. – amaitland Sep 24 '19 at 09:03
  • A slightly more detailed review and the `PixelFormat` should be `PixelFormat.Format32bppPArgb` as the transparency is supported. The buffer shouldn't be used outside the scope of the `Paint` event, it's often reused internally and could potentially even be freed. A copy should be taken. – amaitland Sep 24 '19 at 09:22
  • @amaitland I updated the bitmap-creation code according to your recommendations. Do you think it is OK? I haven't tested it really, because I have not installed the CefSharp library. The previous code was copy-pasted from another answer. :-) – Theodor Zoulias Sep 24 '19 at 11:49
  • The answer should have a disclaimer that it's not actually been tested. In theory it looks ok, I haven't had a chance to run it myself. – amaitland Sep 24 '19 at 20:39
  • @amaitland agreed. :-) – Theodor Zoulias Sep 25 '19 at 04:43