Skip to content

Instantly share code, notes, and snippets.

@jankurianski
Last active August 29, 2015 14:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jankurianski/44b6e2b86e1b137a9ffd to your computer and use it in GitHub Desktop.
Save jankurianski/44b6e2b86e1b137a9ffd to your computer and use it in GitHub Desktop.
CefSharp offscreen renderer
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CefSharp;
using CefSharp.Internals;
/// <summary>
/// An offscreen Chrome browser that renders the web page into
/// a Bitmap.
///
/// Call LoadUrl() to start loading a web page.
/// Listen to the Loaded event to check when it is finished, and to retrieve the bitmap.
/// </summary>
public class ChromeOffScreen : IDisposable
{
ChromeOffscreenAdapter cefAdapter;
public ChromeOffScreen()
{
if (!ChromeProcess.IsInitialized)
throw new InvalidOperationException("ChromeProcess.Initialize() first");
}
public void Initialise()
{
this.cefAdapter = new ChromeOffscreenAdapter();
cefAdapter.BrowserInitialized += cefAdapter_BrowserInitialized;
cefAdapter.FrameLoadEnd += cefAdapter_FrameLoadEnd;
cefAdapter.LoadError += cefAdapter_LoadError;
cefAdapter.NewBitmap += cefAdapter_NewBitmap;
cefAdapter.ConsoleMessage += cefAdapter_ConsoleMessage;
this.Size = new System.Drawing.Size(500, 500);
}
/// <summary>It is critical to pass a delegate that will rethrow the exception in your
/// thread (not the Chrome thread that fires the Loaded/NewBitmap events),
/// to avoid your entire process shutting down.</summary>
public Action<Exception> ExceptionHandler { get; set; }
public Action BrowserInitialised { get; set; }
/// <summary>THIS IS INVOKED BY ANOTHER THREAD.
/// Fired when the URL is loaded.
/// Check Error property to see if the load was successful, or an error occured.</summary>
public Action<string> Loaded { get; set; }
/// <summary>THIS IS INVOKED BY ANOTHER THREAD.
/// Fired when a bitmap is generated.</summary>
public Action NewBitmap { get; set; }
public Action<ConsoleMessageEventArgs> ConsoleMessage { get; set; }
public System.Drawing.Size Size
{
get { return new System.Drawing.Size(cefAdapter.Width, cefAdapter.Height); }
set
{
cefAdapter.Width = value.Width;
cefAdapter.Height = value.Height;
cefAdapter.managedCefBrowserAdapter.WasResized();
}
}
/// <summary>Reset the zoom level of Chrome back to default.
/// This is necessary if another Chrome control has already zoomed
/// in the process lifetime, because Chrome remembers zoom levels
/// per domain.</summary>
public void ResetZoom()
{
cefAdapter.ZoomLevel = 0.0;
}
void DoThreadedWork(Action action)
{
try {
action();
} catch (Exception e) {
ExceptionHandler(e);
}
}
void cefAdapter_BrowserInitialized(object sender, EventArgs e)
{
DoThreadedWork(() => {
BrowserInitialised();
});
}
void cefAdapter_NewBitmap(object sender, EventArgs e)
{
DoThreadedWork(() => {
if (NewBitmap != null)
NewBitmap();
});
}
void cefAdapter_ConsoleMessage(object sender, ConsoleMessageEventArgs e)
{
DoThreadedWork(() => {
if (ConsoleMessage != null)
ConsoleMessage(e);
});
}
public void Dispose()
{
if (cefAdapter != null) {
cefAdapter.Dispose();
cefAdapter = null;
}
}
public string Version { get { return String.Format("Chromium: {0}, CEF: {1}, CefSharp: {2}", Cef.ChromiumVersion, Cef.CefVersion, Cef.CefSharpVersion); } }
public void LoadUrl(string url)
{
if (ExceptionHandler == null)
throw new ApplicationException("ExceptionHandler must be installed.");
this.LastError = null;
cefAdapter.Load(url);
}
public void LoadHtml(string html, string url)
{
if (ExceptionHandler == null)
throw new ApplicationException("ExceptionHandler must be installed.");
this.LastError = null;
cefAdapter.LoadHtml(html, url);
}
public bool HasScreenshot { get { return cefAdapter.Bmp != null; } }
/// <summary>Return the browser screenshot as a Bitmap.
/// It is your responsibility to Dispose of this.</summary>
public Bitmap Screenshot()
{
if (cefAdapter.Bmp == null)
throw new InvalidOperationException();
return new Bitmap(cefAdapter.Bmp);
}
public void ExecuteScriptAsync(string script)
{
cefAdapter.ExecuteScriptAsync(script);
}
void cefAdapter_LoadError(object sender, LoadErrorEventArgs e)
{
DoThreadedWork(() => {
this.LastError = e.ErrorText;
if (Loaded != null)
Loaded(e.FailedUrl);
});
}
void cefAdapter_FrameLoadEnd(object sender, FrameLoadEndEventArgs e)
{
DoThreadedWork(() => {
this.LastError = null;
if (Loaded != null)
Loaded(e.Url);
});
}
public string LastError { get; private set; }
/// <summary>
/// Our interface into Chrome.
/// Implements lots of unnecessary interface methods.
/// Avoids callers having to worry about CefSharp quirks.
/// </summary>
private class ChromeOffscreenAdapter : IRenderWebBrowser
{
public ManagedCefBrowserAdapter managedCefBrowserAdapter;
public Bitmap Bmp;
public event EventHandler NewBitmap;
public ChromeOffscreenAdapter()
{
Cef.AddDisposable(this);
this.managedCefBrowserAdapter = new ManagedCefBrowserAdapter(this);
managedCefBrowserAdapter.CreateOffscreenBrowser(new BrowserSettings());
}
public event EventHandler<LoadErrorEventArgs> LoadError;
public event EventHandler<FrameLoadStartEventArgs> FrameLoadStart;
public event EventHandler<FrameLoadEndEventArgs> FrameLoadEnd;
public event EventHandler<ConsoleMessageEventArgs> ConsoleMessage;
public event EventHandler BrowserInitialized;
public event EventHandler<StatusMessageEventArgs> StatusMessage;
/// <summary>The bitmap buffer is 32 BPP.</summary>
public int BytesPerPixel
{
get { return 4; }
}
public void ClearBitmap(BitmapInfo bitmapInfo)
{
if (Bmp != null)
Bmp.Dispose();
this.Bmp = null;
}
public int Width
{
get;
set;
}
public int Height
{
get;
set;
}
public void InvokeRenderAsync(BitmapInfo bitmapInfo)
{
SetBitmap(bitmapInfo);
}
public void SetBitmap(BitmapInfo bitmapInfo)
{
ClearBitmap(bitmapInfo);
lock (bitmapInfo.BitmapLock) {
var stride = bitmapInfo.Width * BytesPerPixel;
this.Bmp = BitmapSourceToBitmap2(Imaging.CreateBitmapSourceFromMemorySection(bitmapInfo.FileMappingHandle,
bitmapInfo.Width, bitmapInfo.Height, PixelFormats.Bgra32, stride, 0));
if (NewBitmap != null)
NewBitmap(this, EventArgs.Empty);
}
}
/// <summary>http://stackoverflow.com/a/5709472/450141</summary>
public static System.Drawing.Bitmap BitmapSourceToBitmap2(BitmapSource srs)
{
Bitmap bmp = new Bitmap(
srs.PixelWidth,
srs.PixelHeight,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
BitmapData data = bmp.LockBits(
new Rectangle(System.Drawing.Point.Empty, bmp.Size),
ImageLockMode.WriteOnly,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
srs.CopyPixels(
Int32Rect.Empty,
data.Scan0,
data.Height * data.Stride,
data.Stride);
bmp.UnlockBits(data);
return bmp;
}
public void SetCursor(IntPtr cursor)
{
}
public void SetPopupIsOpen(bool show)
{
}
public void SetPopupSizeAndPosition(int width, int height, int x, int y)
{
}
public IDictionary<string, object> BoundObjects { get; private set; }
public IJsDialogHandler JsDialogHandler { get; set; }
public IDialogHandler DialogHandler { get; set; }
public IDownloadHandler DownloadHandler { get; set; }
public IKeyboardHandler KeyboardHandler { get; set; }
public ILifeSpanHandler LifeSpanHandler { get; set; }
public void OnConsoleMessage(string message, string source, int line)
{
if (ConsoleMessage != null)
ConsoleMessage(this, new ConsoleMessageEventArgs(message, source, line));
}
public void OnFrameLoadEnd(string url, bool isMainFrame, int httpStatusCode)
{
if (FrameLoadEnd != null)
FrameLoadEnd(this, new FrameLoadEndEventArgs(url, isMainFrame, httpStatusCode));
}
public void OnFrameLoadStart(string url, bool isMainFrame)
{
if (FrameLoadStart != null)
FrameLoadStart(this, new FrameLoadStartEventArgs(url, isMainFrame));
}
public void OnInitialized()
{
this.IsBrowserInitialized = true;
if (BrowserInitialized != null)
BrowserInitialized(this, EventArgs.Empty);
}
public void OnLoadError(string url, CefErrorCode errorCode, string errorText)
{
if (LoadError != null)
LoadError(this, new LoadErrorEventArgs(url, errorCode, errorText));
}
public void OnTakeFocus(bool next)
{
throw new NotImplementedException();
}
public void OnStatusMessage(string value)
{
if (StatusMessage != null)
StatusMessage(this, new StatusMessageEventArgs(value));
}
public void SetAddress(string address)
{
this.Address = address;
}
public void SetIsLoading(bool isloading)
{
this.IsLoading = isloading;
}
public void SetNavState(bool canGoBack, bool canGoForward, bool canReload)
{
this.CanGoBack = canGoBack;
this.CanGoForward = canGoForward;
this.CanReload = canReload;
}
public void SetTitle(string title)
{
this.Title = title;
}
public void SetTooltipText(string tooltipText)
{
this.TooltipText = tooltipText;
}
public void ShowDevTools()
{
throw new NotImplementedException();
}
public void CloseDevTools()
{
throw new NotImplementedException();
}
public string Address { get; set; }
public bool CanGoBack { get; private set; }
public bool CanGoForward { get; private set; }
public object EvaluateScript(string script, TimeSpan? timeout = null)
{
return managedCefBrowserAdapter.EvaluateScript(script, timeout ?? TimeSpan.MaxValue);
}
public void ExecuteScriptAsync(string script)
{
managedCefBrowserAdapter.ExecuteScriptAsync(script);
}
public void Find(int identifier, string searchText, bool forward, bool matchCase, bool findNext)
{
managedCefBrowserAdapter.Find(identifier, searchText, forward, matchCase, findNext);
}
public void StopFinding(bool clearSelection)
{
managedCefBrowserAdapter.StopFinding(clearSelection);
}
public bool IsBrowserInitialized { get; private set; }
public bool IsLoading { get; set; }
public void Load(string url)
{
this.Address = url;
managedCefBrowserAdapter.LoadUrl(Address);
}
public void LoadHtml(string html, string url)
{
managedCefBrowserAdapter.LoadHtml(html, url);
}
public void RegisterJsObject(string name, object objectToBind)
{
throw new NotImplementedException();
}
public IRequestHandler RequestHandler { get; set; }
public void Stop()
{
managedCefBrowserAdapter.Stop();
}
public string Title { get; set; }
public string TooltipText { get; set; }
public bool CanReload { get; private set; }
public Task<string> GetSourceAsync()
{
var taskStringVisitor = new TaskStringVisitor();
managedCefBrowserAdapter.GetSource(taskStringVisitor);
return taskStringVisitor.Task;
}
public Task<string> GetTextAsync()
{
var taskStringVisitor = new TaskStringVisitor();
managedCefBrowserAdapter.GetText(taskStringVisitor);
return taskStringVisitor.Task;
}
public bool Focus()
{
// no control to focus for offscreen browser
return false;
}
public void Dispose()
{
Cef.RemoveDisposable(this);
if (Bmp != null) {
Bmp.Dispose();
Bmp = null;
}
if (managedCefBrowserAdapter != null) {
if (!managedCefBrowserAdapter.IsDisposed)
managedCefBrowserAdapter.Dispose();
managedCefBrowserAdapter = null;
}
GC.SuppressFinalize(this);
}
public void Reload()
{
Reload(false);
}
public void Reload(bool ignoreCache)
{
managedCefBrowserAdapter.Reload(ignoreCache);
}
public void ViewSource()
{
managedCefBrowserAdapter.ViewSource();
}
public void Print()
{
managedCefBrowserAdapter.Print();
}
public void Back()
{
managedCefBrowserAdapter.GoBack();
}
public void Forward()
{
managedCefBrowserAdapter.GoForward();
}
public double ZoomLevel
{
get { return managedCefBrowserAdapter.GetZoomLevel(); }
set { managedCefBrowserAdapter.SetZoomLevel(value); }
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment