How to Fix a Opacity Bug with Drawtobitmap on Webbrowser Control

How to fix a opacity bug with DrawToBitmap on WebBrowser Control?

I'm looking for a built-in solution to take a screenshot from posted
URL in the question without HIDDEN TEXT. In other words a solution to
respect opacity.

The following code does just that: respects CSS opacity. Amongst other things, it uses a Metafile object and OleDraw API to render the web page's image.

The test HTML:

<!DOCTYPE html>
<body style='background-color: grey'>
<div style='background-color: blue; opacity: 0.2; color: yellow'>This is a text</div>
</body>

The output:

opacity

The code (console app):

using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Console_21697048
{
// http://stackoverflow.com/q/21697048/1768303

class Program
{
const string HTML = "<!DOCTYPE html><body style='background-color: grey'><div style='background-color: blue; opacity: 0.2; color: yellow'>This is a text</div></body>";
const string FILE_NAME = "webpage.png";
readonly static Size IMAGE_SIZE = new Size(320, 200);

// Main
static void Main(string[] args)
{
try
{
// enable HTML5 etc (assuming we're running IE9+)
SetFeatureBrowserFeature("FEATURE_BROWSER_EMULATION", 9000);
// force software rendering
SetFeatureBrowserFeature("FEATURE_IVIEWOBJECTDRAW_DMLT9_WITH_GDI", 1);
SetFeatureBrowserFeature("FEATURE_GPU_RENDERING", 0);

using (var apartment = new MessageLoopApartment())
{
// create WebBrowser on a seprate thread with its own message loop
var webBrowser = apartment.Invoke(() => new WebBrowser());

// navigate and wait for the result
apartment.Invoke(() =>
{
var pageLoadedTcs = new TaskCompletionSource<bool>();
webBrowser.DocumentCompleted += (s, e) =>
pageLoadedTcs.TrySetResult(true);

webBrowser.DocumentText = HTML;
return pageLoadedTcs.Task;
}).Wait();

// save the picture
apartment.Invoke(() =>
{
webBrowser.Size = IMAGE_SIZE;
var rectangle = new Rectangle(0, 0, webBrowser.Width, webBrowser.Height);

// get reference DC
using (var screenGraphics = webBrowser.CreateGraphics())
{
var screenHdc = screenGraphics.GetHdc();
// create a metafile
using (var metafile = new Metafile(screenHdc, rectangle, MetafileFrameUnit.Pixel))
{
using (var graphics = Graphics.FromImage(metafile))
{
var hdc = graphics.GetHdc();
var rect = new Rectangle(0, 0, 320, 50);
OleDraw(webBrowser.ActiveXInstance, DVASPECT_CONTENT, hdc, ref rectangle);
graphics.ReleaseHdc(hdc);
}
// save the metafile as bitmap
metafile.Save(FILE_NAME, ImageFormat.Png);
}
screenGraphics.ReleaseHdc(screenHdc);
}
});

// dispose of webBrowser
apartment.Invoke(() => webBrowser.Dispose());
webBrowser = null;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}

// interop
const uint DVASPECT_CONTENT = 1;

[DllImport("ole32.dll", PreserveSig = false)]
static extern void OleDraw(
[MarshalAs(UnmanagedType.IUnknown)] object pUnk,
uint dwAspect,
IntPtr hdcDraw,
[In] ref System.Drawing.Rectangle lprcBounds);

// WebBrowser Feature Control
// http://msdn.microsoft.com/en-us/library/ie/ee330733(v=vs.85).aspx
static void SetFeatureBrowserFeature(string feature, uint value)
{
if (LicenseManager.UsageMode != LicenseUsageMode.Runtime)
return;
var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\" + feature,
appName, value, RegistryValueKind.DWord);
}
}

// MessageLoopApartment
// more info: http://stackoverflow.com/a/21808747/1768303

public class MessageLoopApartment : IDisposable
{
Thread _thread; // the STA thread

TaskScheduler _taskScheduler; // the STA thread's task scheduler

public TaskScheduler TaskScheduler { get { return _taskScheduler; } }

/// <summary>MessageLoopApartment constructor</summary>
public MessageLoopApartment()
{
var tcs = new TaskCompletionSource<TaskScheduler>();

// start an STA thread and gets a task scheduler
_thread = new Thread(startArg =>
{
EventHandler idleHandler = null;

idleHandler = (s, e) =>
{
// handle Application.Idle just once
Application.Idle -= idleHandler;
// return the task scheduler
tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
};

// handle Application.Idle just once
// to make sure we're inside the message loop
// and SynchronizationContext has been correctly installed
Application.Idle += idleHandler;
Application.Run();
});

_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
_taskScheduler = tcs.Task.Result;
}

/// <summary>shutdown the STA thread</summary>
public void Dispose()
{
if (_taskScheduler != null)
{
var taskScheduler = _taskScheduler;
_taskScheduler = null;

// execute Application.ExitThread() on the STA thread
Task.Factory.StartNew(
() => Application.ExitThread(),
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler).Wait();

_thread.Join();
_thread = null;
}
}

/// <summary>Task.Factory.StartNew wrappers</summary>
public void Invoke(Action action)
{
Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
}

public TResult Invoke<TResult>(Func<TResult> action)
{
return Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
}
}
}

How to do Control.DrawToBitmap with transparency support?

EDIT 2: opacity on embedded browser

With CEF (Chromium Embedded Framework from Google) you should do as follows. This may or may not apply to the default .NET Internet Explorer control.

body
{
background-color: transparent;
}

To overlay this you have to render to a buffer that was cleared. That buffer IS NOT the final rendered raster but an intermediate in-memory buffer that retains VERY MUCH the alpha channel. As long as the buffer is cleared (filled with transparent pixels), you grab the buffer with the full ARGB32 channels and overlay it correctly, you should get what you want.

Best is, you try as far as you can go, then take a screenshot and post a new question specifying that you are using an embedded browser. Also specify the browser version because it will change from one machine to the other: Options for embedding Chromium instead of IE WebBrowser control with WPF/C#


The Bitmap might contain alpha channel information, but not all file formats support it. You should save the image as a *.PNG with ARGB32 lossless mode. A simple *.BMP file will not suffice.

Look at this answer to know not just how to save as PNG but also how to actually get the transparency right:

c# Bitmap.Save transparancy doesn't save in png

To grab transparency from the web page you can try this UGLY, CUMBERSOME, HORRIBLE technique that has tons of problems:

Use a unique color for the body background, like #ffe000. Render the page, grab it as before.

The transparent parts will show the body's background showing through.

At this point you can filter the image by setting its TransparencyKey to the color of the background -OR- by substituting all pixels of that color with a fully transparent pixel color (just change the alpha channel's value from 255 to 0).

Why this is horrible? Because the results will be ugly:

  • fonts are smoothed and will have shades of that color, so you can't get them partially transparent, they will retain opacity and look horrible much like gifs looked back in the ole'90 (they are called rendering artifacts).
  • if you use pretty CSS3 stuff like border-radius or box-shadow the same artifacts as above arise: border pixels with shades (anti-aliasing) will look horrible.

Ugly Example here.

So how can you remove these artifacts? You can't.
I highly recommend a completely different approach to your problem.

By the way, what do you want to achieve? Are you using CEF or other embedded browser technology that you want to overlay somehow?
Maybe there are other ways I can help you with....

EDIT: why the ffff <div> does not show

You set the style to:

background-color: transparent;  opacity:0;

Now, the background of the <div> will be transparent and show what is behind (the <body>'s background-color). When you set opacity instead, you say to make the whole <div> (contained images and texts included) transparent. Setting it to 0 means totally invisible.

WebBrowser.DrawToBitmap() or other methods?

The Control.DrawToBitmap doesn't always work so I resorted to the following native API calls that provide more consistent results:

The Utilities class. Call Utilities.CaptureWindow(Control.Handle) to capture a specific control:

public static class Utilities
{
public static Image CaptureScreen()
{
return CaptureWindow(User32.GetDesktopWindow());
}

public static Image CaptureWindow(IntPtr handle)
{

IntPtr hdcSrc = User32.GetWindowDC(handle);

RECT windowRect = new RECT();
User32.GetWindowRect(handle, ref windowRect);

int width = windowRect.right - windowRect.left;
int height = windowRect.bottom - windowRect.top;

IntPtr hdcDest = Gdi32.CreateCompatibleDC(hdcSrc);
IntPtr hBitmap = Gdi32.CreateCompatibleBitmap(hdcSrc, width, height);

IntPtr hOld = Gdi32.SelectObject(hdcDest, hBitmap);
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, 0, 0, ApiConstants.SRCCOPY);
Gdi32.SelectObject(hdcDest, hOld);
Gdi32.DeleteDC(hdcDest);
User32.ReleaseDC(handle, hdcSrc);

Image image = Image.FromHbitmap(hBitmap);
Gdi32.DeleteObject(hBitmap);

return image;
}
}

The Gdi32 class:

public class Gdi32
{
[DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr hObject, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hObjectSource, int nXSrc, int nYSrc, int dwRop);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int nWidth, int nHeight);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll")]
public static extern bool DeleteDC(IntPtr hDC);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
}

The User32 class:

public static class User32
{
[DllImport("user32.dll")]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowRect(IntPtr hWnd, ref RECT rect);
[DllImport("user32.dll")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
}

The constants used:

    public const int SRCCOPY = 13369376;

The structs used:

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}

A friendly Control extension method:

public static class ControlExtensions
{
public static Image DrawToImage(this Control control)
{
return Utilities.CaptureWindow(control.Handle);
}
}

This is a code snippet from my CC.Utilities project and I specifically wrote it to take screenshots from the WebBrowser control.

WebBrowser.DrawtoBitmap() generating blank images for few sites consistently

DrawToBitmap has limitations and dont always work as expected. Try instead work with native GDI+

Here is example

Converting WebBrowser.Document To A Bitmap?

http://www.bryancook.net/2006/03/screen-capture-for-invisible-windows.html

and here:

http://www.codeproject.com/KB/graphics/screen_capturing.aspx

I believe you should get the handle of your WebBrowser control and save it's content as image like suggested in those links.

Get bitmap from hidden WebBrowser control (IWebBrowser2 interface aka MSHTML )

Ended up grabbing the pixels from the window using Windows GDI function BitBlt.
This works okay, but expect to also get the window border + minimize and maximize boxes in your image in case its the main application window.

Solution for that is to create a child window of the main window, run the browser in that window and grab the pixels from that window as well.

Also found that using the browser's "render to a DC" method from multiple isolated processes doesn't scale in a linear way: the per process CPU usage figure increases significantly the more processes you start.
With the BitBlt method this doesn't happen.

And in both cases problems occur when the browser runs in the main window and that gets minimized. In that case the system decides nothing needs to be drawn and the resulting screenshot bitmap will remain empty.



Related Topics



Leave a reply



Submit