C# WinForms Out of Memory Exception on Bitmap Clone - c#

I've written a small "watermark" program to add a custom watermark to an image. There are two watermarks a white one and a black one. The watermark is always placed on the left bottom side of the image. I clone that region of the image to determine which watermark should be placed based on that spot (the black watermark on light regions and the white watermark on dark regions).
When I use the application (either in debug or normal) on my machine - no problem. All images are processed and watermarks are added on the correct location.
However, on the client machine, the program breaks on all images throwing a OutOfMemory Exception on the clone part.
I know, ussually, the OutOfMemory Exception will also be thrown when I specify a region out of bounds, but since the function is working like a charm on my machine I can't imagine that is the case. Besides that, the program doesn't break after a few tries, it breaks on all attempts to clone.
Doesn't matter if there is text (DrawString method) or not. It breaks on the Clone.
The images being processed are big, but not "huge" (6016 x 4000 pixels at most) but even with smaller images (3264 x 2448 pixels) the client will break.
Variables:
bmOriginal : original bitmap
processImage : original image (pictureBox) - bmOriginal is a bitmap copy of this
watermarkText : textbox for extra information below watermark
black and white : pictureboxes containing the watermark images
watermarkCombo : combobox for selecting automatic, white or black (automatic fails)
Code:
using (Graphics gWatermark = Graphics.FromImage(bmOriginal))
{
gWatermark.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
System.Drawing.SolidBrush drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Black);
// position watermark - watermark should be 10% of the image height
int watermarkHeight = (int)(processImage.Image.Height * 0.1);
int watermarkPadding = (int)(watermarkHeight * 0.1); // not completely true, but asume watermark is square
Rectangle watermarkArea = new Rectangle(watermarkPadding, processImage.Image.Height - (watermarkPadding + (watermarkText.Text.Length == 0 ? 0 : watermarkPadding) + watermarkHeight), watermarkHeight, watermarkHeight);
// determine color watermark
bmWatermark = (Bitmap)black.Image;
if (watermarkCombo.SelectedIndex == 0)
{
using (Bitmap watermarkClone = bmOriginal.Clone(watermarkArea, bmOriginal.PixelFormat))
{
var pixels = Pixels(watermarkClone);
if (pixels.Average((Func<Color, decimal>)Intensity) < 110) // human eye adoption; normal threshold should be 128
{
bmWatermark = (Bitmap)white.Image;
drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
}
}
}
else if (watermarkCombo.SelectedIndex == 1)
{
bmWatermark = (Bitmap)white.Image;
drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
}
// draw the watermark
gWatermark.DrawImage(bmWatermark, watermarkArea.X, watermarkArea.Y, watermarkArea.Width, watermarkArea.Height);
// draw the text (if needed)
if (watermarkText.Text.Length > 0)
{
System.Drawing.Font drawFont = new System.Drawing.Font("Tahoma", (float)watermarkPadding);
gWatermark.DrawString(watermarkText.Text, drawFont, drawBrush, watermarkPadding, bmOriginal.Height - (watermarkPadding * 2));
}
}
bmOriginal.Save(System.IO.Path.Combine(diWatermarked.FullName, fileName), System.Drawing.Imaging.ImageFormat.Jpeg);
line of error: using (Bitmap watermarkClone = bmOriginal.Clone(watermarkArea, bmOriginal.PixelFormat))
Now the big question: how do I get rid of that OutOfMemory exception.. anyone an idea?
EDIT When I choose not to determine the color of the watermark automatically and just add a watermark (let's say the white one) the program functions normally. I've seen the stack trace in an error log (on the catch of the function I output the exception and -if any- inner exceptions).
I know a lot of the OOM exceptions using the Clone function occur when you specify a region out of bounds; but that is not the case here.
When I look at my memory when using the app in debug mode, I start at 5.36 Gb program started and a normalized 5.39 Gb (with a max spike of 5.42 Gb) when running the execution I mentioned, it's not guzzling memory like crazy.
The code I use the determine the average "color" (it's from someone on StackOverflow - I just copied that from some other answer, can't find the link though);
// functions used to determine watermark color
private static decimal ComponentAverage(decimal a, decimal b)
{
return Math.Min(a, b) + Math.Abs(a - b) / 2M;
}
private static decimal Intensity(Color color)
{
decimal result = color.A;
result = ComponentAverage(result, color.R);
result = ComponentAverage(result, color.G);
result = ComponentAverage(result, color.B);
return result;
}
private static IEnumerable<Color> Pixels(Bitmap bitmap)
{
for (int x = 0; x < bitmap.Width; x++)
for (int y = 0; y < bitmap.Height; y++)
yield return bitmap.GetPixel(x, y);
}
SOURCE There is a test project here: http://hotpepper.nu/oomtestapp.zip

Leon, I've changed your code you've uploaded to not lock any resources.
I've notices that if I keep your application open, I cannot delete the output folders because some files are in use. This usually means that you DID NOT release all file handles, basically it is always the last file.
I couldn't reproduce the out of memory issue on my computer before and after my changes, seems to be an issue with very big files maybe?
Ok anyways, I found that you use ImageBox to load the white and black resource and to load the image from disc. This is not needed at all, intead use the resources directly
Bitmap white = OomTestApp.Properties.Resources.white;
Bitmap black = OomTestApp.Properties.Resources.black;
Then to load an image from disc, simply use Bitmap.FromFile
I have added some lines to release your resources properly .Dispose where no using blocks are used.
And I also removed the Clone() call because it is absolutely not needed I think, because you are just computing the pixels of the original image and you do not draw something into that image at that point. So what was the need to have the image cloned?
Here is the full code (starting after you created the folders)
if (errors == 0)
{
this.Height = 323;
goButton.Enabled = false;
stopButton.Enabled = true;
Bitmap white = OomTestApp.Properties.Resources.white;
Bitmap black = OomTestApp.Properties.Resources.black;
Bitmap bmWatermark = black;
Bitmap processImage = null;
progressBar1.Maximum = filesToProcess.Count;
foreach (string handleFile in filesToProcess)
{
string fileName = System.IO.Path.GetFileName(handleFile);
fileNameLabel.Text = "File: " + System.IO.Path.GetFileName(handleFile);
try
{
// create backup if checked
if (diOriginal != null)
{
System.IO.File.Move(handleFile, System.IO.Path.Combine(diOriginal.FullName, fileName));
processImage = (Bitmap)Bitmap.FromFile(System.IO.Path.Combine(diOriginal.FullName, fileName));
}
else
{
processImage = (Bitmap)Bitmap.FromFile(handleFile);
}
double aspectRatio = (double)processImage.Width / (double)processImage.Height;
using (Graphics gWatermark = Graphics.FromImage(processImage))
{
gWatermark.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
System.Drawing.SolidBrush drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Black);
// position watermark - watermark should be 10% of the image height
int watermarkHeight = (int)(processImage.Height * 0.1);
int watermarkPadding = (int)(watermarkHeight * 0.1); // not completely true, but asume watermark is square
// calculate rectangle. if there is extra text, add extra padding below
Rectangle watermarkArea = new Rectangle(watermarkPadding, processImage.Height - (watermarkPadding + (watermarkText.Text.Length == 0 ? 0 : watermarkPadding) + watermarkHeight), watermarkHeight, watermarkHeight);
// determine color watermark
bmWatermark = black;
if (watermarkCombo.SelectedIndex == 0)
{
/*using (Bitmap watermarkClone = processImage.Clone(watermarkArea, processImage.PixelFormat))
{*/
var pixels = Pixels(processImage);
if (pixels.Average((Func<Color, decimal>)Intensity) < 110) // human eye adoption; normal threshold should be 128
{
bmWatermark = white;
drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
}
//}
}
else if (watermarkCombo.SelectedIndex == 1)
{
bmWatermark = white;
drawBrush = new System.Drawing.SolidBrush(System.Drawing.Color.White);
}
// draw the watermark
gWatermark.DrawImage(bmWatermark, watermarkArea.X, watermarkArea.Y, watermarkArea.Width, watermarkArea.Height);
// draw the text (if needed)
if (watermarkText.Text.Length > 0)
{
System.Drawing.Font drawFont = new System.Drawing.Font("Tahoma", (float)watermarkPadding);
gWatermark.DrawString(watermarkText.Text, drawFont, drawBrush, watermarkPadding, processImage.Height - (watermarkPadding * 2));
drawFont.Dispose();
}
// disposing resources
drawBrush.Dispose();
}
// save the watermarked file
processImage.Save(System.IO.Path.Combine(diWatermarked.FullName, fileName), System.Drawing.Imaging.ImageFormat.Jpeg);
// stop button pressed?
Application.DoEvents();
if (stopProcess) break;
// update exection progress
progressBar1.Value++;
percentLabel.Text = ((int)((progressBar1.Value * 100) / filesToProcess.Count)).ToString() + "%";
fileCountLabel.Text = "File " + progressBar1.Value.ToString() + "/" + filesToProcess.Count.ToString();
}
catch (Exception ex)
{
try
{
using (System.IO.StreamWriter sw = new System.IO.StreamWriter(System.IO.Path.Combine(folderText.Text, "errorlog.txt"), true))
{
sw.WriteLine("File: " + fileName);
while (ex != null)
{
sw.WriteLine("Message: " + ex.Message);
sw.WriteLine(ex.StackTrace);
sw.WriteLine(ex.Source);
ex = ex.InnerException;
}
sw.WriteLine();
}
}
catch
{
// nothing to do - it already failed
}
errors++;
}
finally
{
if (processImage != null) processImage.Dispose();
}
}
// dispose resources
white.Dispose();
black.Dispose();
bmWatermark.Dispose();
if (!stopProcess)
{
// set status to complete
fileCountLabel.Text = "File " + filesToProcess.Count.ToString() + "/" + filesToProcess.Count.ToString();
percentLabel.Text = "100%";
fileNameLabel.Text = "Completed...";
}
else
{
fileNameLabel.Text = "Aborted...";
}
fileNameLabel.Text += errors.ToString() + " error(s) encountered";
// defaults to screen
progressBar1.Value = progressBar1.Maximum;
stopProcess = false;
goButton.Enabled = true;
stopButton.Enabled = false;

Well, I didn't read everything on the page, but I wonder if anyone mentioned the "stride" of the Bitmap? Basically, your bitmap has to be a multiple of 4 bytes.
I had this problem taking apart a grid to make tiles. The last tile would error because the Bitmap was not evenly divisible by 4.
The stride is the width of a single row of pixels (a scan line),
rounded up to a four-byte boundary. If the stride is positive, the
bitmap is top-down. If the stride is negative, the bitmap is
bottom-up.
https://softwarebydefault.com/2013/04/11/bitmap-color-balance/

Related

Check if Clipboard.GetImage() is transparent in C#

I think I have a simple problem that seems very hard for me to figure out - how to check if an image I get from Clipboard.GetImage() uses transparency. If it does then I will show it in a PictureBox with the transparent background.
I copy the picture directly from the application - e.g. the browser or one of the Windows image viewers. Pasting the picture to e.g. Word will get the transparent background.
I am using this code:
// Check if the picture in clipboard has any transparency (alpha channel != 255)
Bitmap img = new Bitmap(Clipboard.GetImage());
for (int y = 0; y < img.Height; ++y)
{
for (int x = 0; x < img.Width; ++x)
{
if (img.GetPixel(x, y).A != 255)
{
Debug.WriteLine("Picture is transparent - set breakpoint here");
}
}
}
...
// Show the picture with transparent background - this works fine
img.MakeTransparent(img.GetPixel(0,0));
myPictureBox.Image = (Image)img;
I am trying with various pictures found on the net and I can copy/paste those pictures with the transparent background so I know they are transparent but no pictures will trigger the Debug.WriteLine and all values equals 255?
Though I recognize this has been asked before then I must be doing something wrong since this simple example does not work? Also they are old so maybe there is a new and better way? I have tried to find other solutions besides these:
Detecting if a PNG image file is a Transparent image?
Check to see if image is transparent
.. and more also not from StackOverflow. I have seen both really simple solutions and horrofying complex ones - but still none of them seems to work.
is this because the clipboard object cannot see the transparency or .. ?
I ended up this solution, based on the comment from #Jeff, CopyTransparentImages. This will get the (real?) image from the clipboard (which will also include the alpha channel) and then I will check if the image contains any transparency afterwards. If it does then I will make the image background color transparent, according to my original question, before I show it in a PictureBox.
// Get the image formats from clipboard and check if it is transparent
Image imgCopy = GetImageFromClipboard();
bool isClipboardImageTransparent = IsImageTransparent(imgCopy);
if (isClipboardImageTransparent)
{
...
}
// Get the real image from clipboard (this supports the alpha channel)
private Image GetImageFromClipboard()
{
if (Clipboard.GetDataObject() == null) return null;
if (Clipboard.GetDataObject().GetDataPresent(DataFormats.Dib))
{
// Sometimes getting the image data fails and results in a "System.NullReferenceException" error - probably because clipboard handling also can be messy and complex
byte[] dib;
try
{
dib = ((System.IO.MemoryStream)Clipboard.GetData(DataFormats.Dib)).ToArray();
}
catch (Exception ex)
{
return Clipboard.ContainsImage() ? Clipboard.GetImage() : null;
}
var width = BitConverter.ToInt32(dib, 4);
var height = BitConverter.ToInt32(dib, 8);
var bpp = BitConverter.ToInt16(dib, 14);
if (bpp == 32)
{
var gch = GCHandle.Alloc(dib, GCHandleType.Pinned);
Bitmap bmp = null;
try
{
var ptr = new IntPtr((long)gch.AddrOfPinnedObject() + 40);
bmp = new Bitmap(width, height, width * 4, System.Drawing.Imaging.PixelFormat.Format32bppArgb, ptr);
return new Bitmap(bmp);
}
finally
{
gch.Free();
if (bmp != null) bmp.Dispose();
}
}
}
return Clipboard.ContainsImage() ? Clipboard.GetImage() : null;
}
// Check if the image contains any transparency
private static bool IsImageTransparent(Image image)
{
Bitmap img = new Bitmap(image);
for (int y = 0; y < img.Height; ++y)
{
for (int x = 0; x < img.Width; ++x)
{
if (img.GetPixel(x, y).A != 255)
{
return true;
}
}
}
return false;
}
At least this is working for me :-)

How to detect where the image content ends?

I receive images of the same size but with different amounts of information. Examples below (red borders are mine). The background is always white.
I am trying to detect where the information on the image ends - at what pixel height (and crop accordingly). In other words, find the first non-white pixel from the bottom.
Is there a better way to do this other than extract BitmapData out of Image object and loop through all the pixels?
Just to add a suggestion having looked over your images and your solution (below) and your method is fine but you may be able to improve efficiency.
The more you know about your image the better; you're confident the background is always white (according to your post, the code is a more generic utility but the following suggestion can still work); can you be confident on the furthest point in a non-white pixel will be found if the row is not empty?
For example; in your two pictures the furthest in non-white pixel on a row is about 60px in. If this is universally true for your data then you don't need to scan the whole line of the image, which would make your for loop:
for (int y = bitmap.Height - 1; y >= 0; y--) {
for (int x = 0; x < 60; x++) {
Color color = bitmap.GetPixel(x, y);
if (color.R != backColor.R || color.G != backColor.G || color.B != backColor.B) {
foundContentOnRow = y;
break;
}
}
}
(You could make it a parameter on the function so you can easily control it if needed).
Imagine for example that the first non-white row was 80px down. To find it currently you do 640 x 300 = 192,000 checks. If you could confidently say that you would know a row was blank within 100 pixels (an over-estimate based on the data presented) then this would be 100 * 300 = 30,000 checks per image.
If you always knew that the first 10 pixels of the image were always blank you could shave a little bit more off (say 3000 checks).
Musing on a setup where you knew that the first non-white pixel was between 10 and 60 pixels in (range of 50) you could find it at row 80 in 50 x 300 = 15,000 checks which is a good reduction.
Of course the downside about assumptions is that if things change your assumptions may not be valid, but if the data is going to remain fairly constant then it may be worthwhile, especially if you do this for a lot of images.
I've ended up using the following code to trim the image. Hopefully someone finds this useful.
class Program {
static void Main(string[] args) {
Image full = Image.FromFile("foo.png");
Image cropped = full.TrimOnBottom();
}
}
public static class ImageUtilities {
public static Image TrimOnBottom(this Image image, Color? backgroundColor = null, int margin = 30) {
var bitmap = (Bitmap)image;
int foundContentOnRow = -1;
// handle empty optional parameter
var backColor = backgroundColor ?? Color.White;
// scan the image from the bottom up, left to right
for (int y = bitmap.Height - 1; y >= 0; y--) {
for (int x = 0; x < bitmap.Width; x++) {
Color color = bitmap.GetPixel(x, y);
if (color.R != backColor.R || color.G != backColor.G || color.B != backColor.B) {
foundContentOnRow = y;
break;
}
}
// exit loop if content found
if (foundContentOnRow > -1) {
break;
}
}
if (foundContentOnRow > -1) {
int proposedHeight = foundContentOnRow + margin;
// only trim if proposed height smaller than existing image
if (proposedHeight < bitmap.Height) {
return CropImage(image, bitmap.Width, proposedHeight);
}
}
return image;
}
private static Image CropImage(Image image, int width, int height) {
Rectangle cropArea = new Rectangle(0, 0, width, height);
Bitmap bitmap = new Bitmap(image);
return bitmap.Clone(cropArea, bitmap.PixelFormat);
}
}

Draw a fixed size bitmap image on canvas

I am creating a card game, for this i have created a custom surface view, in which images are getting load. Since images are downloaded from internet, they are of different sizes and looks visually bad on screen. I want to achieve two things here.
Load images of fixed size or resize the images dynamically.
Draw images from bottom of screen in upward direction.
For 1st point i used CreateBitmap method but getting below exception.
java.lang.OutOfMemoryError: Failed to allocate a 1915060280 byte allocation with 4194304 free bytes and 123MB until OOM error
To fixed the issue i thought of using Glide/Picasso based on this question and this, but i found out that Glide/Picasso load images only on imageview, but i don't have any imageview, i only got a custom surfaceview inside a linearlayout.
For 2nd point i used rotation of image. Following is the code of that.
public void Render(Canvas paramCanvas)
{
try
{
// paramCanvas.DrawColor(Android.Graphics.Color.Blue);
int i = 0;
Down_Card_Gap = 0;
foreach (Cards localcard in FaceDownDeck.ToList())
{
Bitmap localimage = BitmapFactory.DecodeResource(Resources, localcard.GetImageId(context));
Bitmap rotatedimage = RotateBitmap(localimage, 180);
paramCanvas.DrawBitmap(rotatedimage, (Screen_Center_X - Card_Width / 2)+Down_Card_Gap, (Screen_Height - Card_Height), null);
// paramCanvas.DrawBitmap(localimage, (Screen_Center_X - Card_Width / 2), (Screen_Center_Y - Card_Height), null);
if (i++ == 7)
{ break; }
if (Down_Card_Gap > 0)
{
Down_Card_Gap += Card_Width / 2;
}
else
{
Down_Card_Gap -= Card_Width / 2;
}
Down_Card_Gap *= -1;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
}
private Bitmap RotateBitmap(Bitmap localimage, float angle)
{
Matrix matrix = new Matrix();
matrix.PostRotate(angle);
matrix.PostScale(Card_Width, Card_Height);
Bitmap resized= Bitmap.CreateBitmap(localimage, 0, 0, localimage.Width, localimage.Height, matrix, true);
localimage.Recycle();
return resized;
}
I want to know if it is a right approach, or is there any better method achieve the functionality.
Load images of fixed size or resize the images dynamically.
About the fixed size and resize, you can refer to this, find decodeFile method:
protected Bitmap decodeFile(File f) {
try {
//decode image size
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new FileInputStream(f), null, o);
//Find the correct scale value. It should be the power of 2.
final int REQUIRED_SIZE = 150;
int width_tmp = o.outWidth, height_tmp = o.outHeight;
int scale = 1;
while (true) {
if (width_tmp / 2 < REQUIRED_SIZE || height_tmp / 2 < REQUIRED_SIZE)
break;
width_tmp /= 2;
height_tmp /= 2;
scale *= 2;
}
//decode with inSampleSize
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
} catch (FileNotFoundException e) {
}
return null;
}
You can see, it uses BitmapFactory.Options.inJustDecodeBounds= true to preload the bitmap, and scale the bitmap. Also you can refer to official document. Read this to compress bitmap's quality.
Except from this, you also need consider the picture cache.This talks about how to build an efficient memory cache for bitmaps.

How to change color of a image in C#

I am developing a Windows application in which I am getting an image (in black color) from server. I download that image and display it in my application.
Is there a way to change the color of this image (to white color) in code because I want to display a white colored image since I have a black background.
Please let me know if additional info is required from my side?
Depending on how far you want to go down this rabbit hole.
You could just convert the image to 32bit and roll your own image processing routine to convert black to white pixels.
The following is an example of how to use unsafe keyword and Pointers to achieve this fairly efficiently. Add pepper and salt to taste
-unsafe (C# Compiler Options)
Disclaimer there is other ways to do this, however YOLO
unsafe private void ConvertImage(string fromPath, string toPath)
{
using (Bitmap orig = new Bitmap(fromPath))
{
using (Bitmap clone = new Bitmap(orig.Width, orig.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb))
{
var rect = new Rectangle(0, 0, clone.Width, clone.Height);
using (Graphics gr = Graphics.FromImage(clone))
{
gr.DrawImage(orig, rect);
}
// lock the array for direct access
var bitmapData = clone.LockBits(Bounds, ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
// get the pointer
var scan0Ptr = (int*)bitmapData.Scan0;
// get the stride
var stride = bitmapData.Stride / 4;
var white = Color.White.ToArgb();
var black = Color.Black.ToArgb();
// scan all x
for (var x = rect.Left; x < rect.Right; x++)
{
var pX = scan0Ptr + x;
// scan all y
for (var y = rect.Top; y < rect.Bottom; y++)
{
if (*(pX + y * stride) == black)
{
*(pX + y * stride) = white;
}
else
{
*(pX + y * stride) = black;
}
}
}
// unlock the bitmap
clone.UnlockBits(bitmapData);
clone.Save(toPath);
}
}
}
Updated
Changed to invert the image

Selenium WebDriver C# Full Website Screenshots With ChromeDriver and FirefoxDriver

When I take screenshots with ChromeDriver I get screens with the size of my viewport.
When I take screenshots with FirefoxDriver I get what I want, which is a full screen print of a website.
ChromeDriver is declared like this:
IWebDriver driver = new ChromeDriver();
FirefoxDriver is declared like this:
IWebDriver driver = new FirefoxDriver();
Both drivers execute identical code:
driver.Manage().Window.Maximize();
driver.Navigate().GoToUrl(url);//url is a string variable
ITakesScreenshot screenshotDriver = driver as ITakesScreenshot;
Screenshot screenshot = screenshotDriver.GetScreenshot();
screenshot.SaveAsFile("c:/test.png", ImageFormat.Png);
ChromeDriver's test.png is of 1920x1099 resolution and contains only the browser viewport.
FirefoxDriver's test.png is of 1903x16559 resolution and contains the whole page.
I know that GetScreenshot() method doesn't return identical resolution sizes because it has slightly different implementations in IEDriver, FirefoxDriver, OperaDriver, ChromeDriver.
My questions are:
Why is there such difference between ChromeDriver's and FirefoxDriver's .GetScreenshot() method, even tho they use an identical interface (ITakesScreenshot)?
Is there a way to make ChromeDriver's GetScreenshot() method return the whole webpage screen instead of just the viewport?
we can't get the entire page screenshot with ChromeDriver2, we need to go for manual implementation.I have modified a method with is available in a blog which works fine with ChromeDriver.
use this method as following :
private IWebDriver _driver = new ChromeDriver(CHROME_DRIVER_PATH);
screenshot.SaveAsFile(saveFileName, ImageFormat.Jpeg);
public Bitmap GetEntereScreenshot()
{
Bitmap stitchedImage = null;
try
{
long totalwidth1 = (long)((IJavaScriptExecutor)_driver).ExecuteScript("return document.body.offsetWidth");//documentElement.scrollWidth");
long totalHeight1 = (long)((IJavaScriptExecutor)_driver).ExecuteScript("return document.body.parentNode.scrollHeight");
int totalWidth = (int)totalwidth1;
int totalHeight = (int)totalHeight1;
// Get the Size of the Viewport
long viewportWidth1 = (long)((IJavaScriptExecutor)_driver).ExecuteScript("return document.body.clientWidth");//documentElement.scrollWidth");
long viewportHeight1 = (long)((IJavaScriptExecutor)_driver).ExecuteScript("return window.innerHeight");//documentElement.scrollWidth");
int viewportWidth = (int)viewportWidth1;
int viewportHeight = (int)viewportHeight1;
// Split the Screen in multiple Rectangles
List<Rectangle> rectangles = new List<Rectangle>();
// Loop until the Total Height is reached
for (int i = 0; i < totalHeight; i += viewportHeight)
{
int newHeight = viewportHeight;
// Fix if the Height of the Element is too big
if (i + viewportHeight > totalHeight)
{
newHeight = totalHeight - i;
}
// Loop until the Total Width is reached
for (int ii = 0; ii < totalWidth; ii += viewportWidth)
{
int newWidth = viewportWidth;
// Fix if the Width of the Element is too big
if (ii + viewportWidth > totalWidth)
{
newWidth = totalWidth - ii;
}
// Create and add the Rectangle
Rectangle currRect = new Rectangle(ii, i, newWidth, newHeight);
rectangles.Add(currRect);
}
}
// Build the Image
stitchedImage = new Bitmap(totalWidth, totalHeight);
// Get all Screenshots and stitch them together
Rectangle previous = Rectangle.Empty;
foreach (var rectangle in rectangles)
{
// Calculate the Scrolling (if needed)
if (previous != Rectangle.Empty)
{
int xDiff = rectangle.Right - previous.Right;
int yDiff = rectangle.Bottom - previous.Bottom;
// Scroll
//selenium.RunScript(String.Format("window.scrollBy({0}, {1})", xDiff, yDiff));
((IJavaScriptExecutor)_driver).ExecuteScript(String.Format("window.scrollBy({0}, {1})", xDiff, yDiff));
System.Threading.Thread.Sleep(200);
}
// Take Screenshot
var screenshot = ((ITakesScreenshot)_driver).GetScreenshot();
// Build an Image out of the Screenshot
Image screenshotImage;
using (MemoryStream memStream = new MemoryStream(screenshot.AsByteArray))
{
screenshotImage = Image.FromStream(memStream);
}
// Calculate the Source Rectangle
Rectangle sourceRectangle = new Rectangle(viewportWidth - rectangle.Width, viewportHeight - rectangle.Height, rectangle.Width, rectangle.Height);
// Copy the Image
using (Graphics g = Graphics.FromImage(stitchedImage))
{
g.DrawImage(screenshotImage, rectangle, sourceRectangle, GraphicsUnit.Pixel);
}
// Set the Previous Rectangle
previous = rectangle;
}
}
catch (Exception ex)
{
// handle
}
return stitchedImage;
}
I cleaned up #Selvantharajah Roshanth's answer and added a check so that it won't try to stitch together screenshots that already fit in the viewport.
public Image GetEntireScreenshot()
{
// Get the total size of the page
var totalWidth = (int) (long) ((IJavaScriptExecutor) driver).ExecuteScript("return document.body.offsetWidth"); //documentElement.scrollWidth");
var totalHeight = (int) (long) ((IJavaScriptExecutor) driver).ExecuteScript("return document.body.parentNode.scrollHeight");
// Get the size of the viewport
var viewportWidth = (int) (long) ((IJavaScriptExecutor) driver).ExecuteScript("return document.body.clientWidth"); //documentElement.scrollWidth");
var viewportHeight = (int) (long) ((IJavaScriptExecutor) driver).ExecuteScript("return window.innerHeight"); //documentElement.scrollWidth");
// We only care about taking multiple images together if it doesn't already fit
if (totalWidth <= viewportWidth && totalHeight <= viewportHeight)
{
var screenshot = driver.TakeScreenshot();
return ScreenshotToImage(screenshot);
}
// Split the screen in multiple Rectangles
var rectangles = new List<Rectangle>();
// Loop until the totalHeight is reached
for (var y = 0; y < totalHeight; y += viewportHeight)
{
var newHeight = viewportHeight;
// Fix if the height of the element is too big
if (y + viewportHeight > totalHeight)
{
newHeight = totalHeight - y;
}
// Loop until the totalWidth is reached
for (var x = 0; x < totalWidth; x += viewportWidth)
{
var newWidth = viewportWidth;
// Fix if the Width of the Element is too big
if (x + viewportWidth > totalWidth)
{
newWidth = totalWidth - x;
}
// Create and add the Rectangle
var currRect = new Rectangle(x, y, newWidth, newHeight);
rectangles.Add(currRect);
}
}
// Build the Image
var stitchedImage = new Bitmap(totalWidth, totalHeight);
// Get all Screenshots and stitch them together
var previous = Rectangle.Empty;
foreach (var rectangle in rectangles)
{
// Calculate the scrolling (if needed)
if (previous != Rectangle.Empty)
{
var xDiff = rectangle.Right - previous.Right;
var yDiff = rectangle.Bottom - previous.Bottom;
// Scroll
((IJavaScriptExecutor) driver).ExecuteScript(String.Format("window.scrollBy({0}, {1})", xDiff, yDiff));
}
// Take Screenshot
var screenshot = driver.TakeScreenshot();
// Build an Image out of the Screenshot
var screenshotImage = ScreenshotToImage(screenshot);
// Calculate the source Rectangle
var sourceRectangle = new Rectangle(viewportWidth - rectangle.Width, viewportHeight - rectangle.Height, rectangle.Width, rectangle.Height);
// Copy the Image
using (var graphics = Graphics.FromImage(stitchedImage))
{
graphics.DrawImage(screenshotImage, rectangle, sourceRectangle, GraphicsUnit.Pixel);
}
// Set the Previous Rectangle
previous = rectangle;
}
return stitchedImage;
}
private static Image ScreenshotToImage(Screenshot screenshot)
{
Image screenshotImage;
using (var memStream = new MemoryStream(screenshot.AsByteArray))
{
screenshotImage = Image.FromStream(memStream);
}
return screenshotImage;
}
It appears as though full-screen screenshots are not yet implemented in the ChromeDriver, due to some inaccuracies in its previous implementation.
Source: https://code.google.com/p/chromedriver/issues/detail?id=294
I have recently written a Selenium based application to test an Internet Explorer UI and found that:
Taking screenshots with selenium was not as quick as using .NET, and
Selenium is unable to take screenshots when dialog boxes are present. This was a major drawback, as I needed to identify unexpected dialogs during interaction with the pages.
Investigate using the Graphics.CopyFromScreen method in System.Drawing as an alternative solution until the feature is implemented in Chrome. Once you have tried .the Net approach however, I don't think you will look back =]
I stumbled accross the same problem and ChromeDriver2 just does not support it.
So I created a little script which scrolls thru the page, takes screenshots and stitches everything together.
You can find the script in my blog post here:
http://dev.flauschig.ch/wordpress/?p=341

Categories