Last active
August 29, 2015 14:09
-
-
Save jankurianski/44b6e2b86e1b137a9ffd to your computer and use it in GitHub Desktop.
CefSharp offscreen renderer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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