/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2012 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: http://getgreenshot.org/ * The Greenshot project is hosted on Sourceforge: http://sourceforge.net/projects/greenshot/ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Windows.Forms; using Greenshot.Plugin; using GreenshotPlugin.UnmanagedHelpers; using System.Diagnostics; using Greenshot.IniFile; namespace GreenshotPlugin.Core { /// /// This Class is used to pass details about the capture around. /// The time the Capture was taken and the Title of the window (or a region of) that is captured /// public class CaptureDetails : ICaptureDetails { private string title; public string Title { get {return title;} set {title = value;} } private string filename; public string Filename { get {return filename;} set {filename = value;} } private DateTime dateTime; public DateTime DateTime { get {return dateTime;} set {dateTime = value;} } private float dpiX; public float DpiX { get { return dpiX; } set { dpiX = value; } } private float dpiY; public float DpiY { get { return dpiY; } set { dpiY = value; } } private Dictionary metaData = new Dictionary(); public Dictionary MetaData { get {return metaData;} } public void AddMetaData(string key, string value) { if (metaData.ContainsKey(key)) { metaData[key] = value; } else { metaData.Add(key, value); } } private CaptureMode captureMode; public CaptureMode CaptureMode { get {return captureMode;} set {captureMode = value;} } private List captureDestinations = new List(); public List CaptureDestinations { get {return captureDestinations;} set {captureDestinations = value;} } public void ClearDestinations() { captureDestinations.Clear(); } public void RemoveDestination(IDestination destination) { if (captureDestinations.Contains(destination)) { captureDestinations.Remove(destination); } } public void AddDestination(IDestination captureDestination) { if (!captureDestinations.Contains(captureDestination)) { captureDestinations.Add(captureDestination); } } public bool HasDestination(string designation) { foreach(IDestination destination in captureDestinations) { if (designation.Equals(destination.Designation)) { return true; } } return false; } public CaptureDetails() { dateTime = DateTime.Now; } } /// /// This class is used to pass an instance of the "Capture" around /// Having the Bitmap, eventually the Windows Title and cursor all together. /// public class Capture : IDisposable, ICapture { private static log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(Capture)); private List elements = new List(); private Rectangle screenBounds; /// /// Get/Set the Screenbounds /// public Rectangle ScreenBounds { get { if (screenBounds == null) { screenBounds = WindowCapture.GetScreenBounds(); } return screenBounds; } set {screenBounds = value;} } private Image image; /// /// Get/Set the Image /// public Image Image { get {return image;} set { if (image != null) { image.Dispose(); } image = value; if (value != null) { if (value.PixelFormat.Equals(PixelFormat.Format8bppIndexed) || value.PixelFormat.Equals(PixelFormat.Format1bppIndexed) || value.PixelFormat.Equals(PixelFormat.Format4bppIndexed)) { LOG.Debug("Converting Bitmap to PixelFormat.Format32bppArgb as we don't support: " + value.PixelFormat); try { // Default Bitmap PixelFormat is Format32bppArgb this.image = new Bitmap(value); } finally { // Always dispose, even when a exception occured value.Dispose(); } } LOG.Debug("Image is set with the following specifications: " + image.Width + "," + image.Height + " - " + image.PixelFormat); } else { LOG.Debug("Image is removed."); } } } public void NullImage() { image = null; } private Icon cursor; /// /// Get/Set the image for the Cursor /// public Icon Cursor { get {return cursor;} set { if (cursor != null) { cursor.Dispose(); } cursor = (Icon)value.Clone(); } } private bool cursorVisible = false; /// /// Set if the cursor is visible /// public bool CursorVisible { get {return cursorVisible;} set {cursorVisible = value;} } private Point cursorLocation = Point.Empty; /// /// Get/Set the CursorLocation /// public Point CursorLocation { get {return cursorLocation;} set {cursorLocation = value;} } private Point location = Point.Empty; /// /// Get/set the Location /// public Point Location { get {return location;} set {location = value;} } private CaptureDetails captureDetails; /// /// Get/set the CaptureDetails /// public ICaptureDetails CaptureDetails { get {return captureDetails;} set {captureDetails = (CaptureDetails)value;} } /// /// Default Constructor /// public Capture() { screenBounds = WindowCapture.GetScreenBounds(); captureDetails = new CaptureDetails(); } /// /// Constructor with Image /// Note: the supplied bitmap can be disposed immediately or when constructor is called. /// /// Image public Capture(Image newImage) : this() { this.Image = newImage; } /// /// Destructor /// ~Capture() { Dispose(false); } /// /// The public accessible Dispose /// Will call the GarbageCollector to SuppressFinalize, preventing being cleaned twice /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// This Dispose is called from the Dispose and the Destructor. /// When disposing==true all non-managed resources should be freed too! /// /// protected virtual void Dispose(bool disposing) { if (disposing) { if (image != null) { image.Dispose(); } if (cursor != null) { cursor.Dispose(); } } image = null; cursor = null; } /// /// Crops the capture to the specified rectangle (with Bitmap coordinates!) /// /// Rectangle with bitmap coordinates public bool Crop(Rectangle cropRectangle) { LOG.Debug("Cropping to: " + cropRectangle.ToString()); if (ImageHelper.Crop(ref image, ref cropRectangle)) { location = cropRectangle.Location; // Change mouse location according to the cropRegtangle (including screenbounds) offset MoveMouseLocation(-cropRectangle.Location.X, -cropRectangle.Location.Y); // Move all the elements MoveElements(-cropRectangle.Location.X, -cropRectangle.Location.Y); // Remove invisible elements List newElements = new List(); foreach(ICaptureElement captureElement in elements) { if (captureElement.Bounds.IntersectsWith(cropRectangle)) { newElements.Add(captureElement); } } elements = newElements; return true; } return false; } /// /// Apply a translate to the mouse location. /// e.g. needed for crop /// /// x coordinates to move the mouse /// y coordinates to move the mouse public void MoveMouseLocation(int x, int y) { cursorLocation.Offset(x, y); } /// /// Apply a translate to the elements /// e.g. needed for crop /// /// x coordinates to move the elements /// y coordinates to move the elements public void MoveElements(int x, int y) { MoveElements(elements, x, y); } private void MoveElements(List listOfElements, int x, int y) { foreach(ICaptureElement childElement in listOfElements) { Rectangle bounds = childElement.Bounds; bounds.Offset(x, y); childElement.Bounds = bounds; MoveElements(childElement.Children, x, y); } } /// /// Add a new element to the capture /// /// CaptureElement public void AddElement(ICaptureElement element) { int match = elements.IndexOf(element); if (match >= 0) { if (elements[match].Children.Count < element.Children.Count) { elements.RemoveAt(match); elements.Add(element); } } else { elements.Add(element); } } /// /// Returns a list of rectangles which represent object that are on the capture /// public List Elements { get { return elements; } set { elements = value; } } } /// /// A class representing an element in the capture /// public class CaptureElement : ICaptureElement { public CaptureElement(Rectangle bounds) { Bounds = bounds; } public CaptureElement(string name) { Name = name; } public CaptureElement(string name, Rectangle bounds) { Name = name; Bounds = bounds; } private List children = new List(); public List Children { get { return children; } set { children = value; } } public string Name { get; set; } public Rectangle Bounds { get; set; } // CaptureElements are regarded equal if their bounds are equal. this should be sufficient. public override bool Equals(object obj) { bool ret = false; if (obj != null && GetType().Equals(obj.GetType())) { CaptureElement other = obj as CaptureElement; if (Bounds.Equals(other.Bounds)) { ret = true; } } return ret; } public override int GetHashCode() { return Bounds.GetHashCode(); } } /// /// The Window Capture code /// public class WindowCapture { private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(WindowCapture)); private static CoreConfiguration conf = IniConfig.GetIniSection(); private WindowCapture() { } /// /// Get the bounds of all screens combined. /// /// A Rectangle of the bounds of the entire display area. public static Rectangle GetScreenBounds() { int left = 0, top = 0, bottom = 0, right = 0; foreach (Screen screen in Screen.AllScreens) { left = Math.Min(left, screen.Bounds.X); top = Math.Min(top, screen.Bounds.Y); int screenAbsRight = screen.Bounds.X + screen.Bounds.Width; int screenAbsBottom = screen.Bounds.Y + screen.Bounds.Height; right = Math.Max(right, screenAbsRight); bottom = Math.Max(bottom, screenAbsBottom); } return new Rectangle(left, top, (right + Math.Abs(left)), (bottom + Math.Abs(top))); } /// /// Retrieves the cursor location safely, accounting for DPI settings in Vista/Windows 7 /// Point with cursor location public static Point GetCursorLocation() { if (Environment.OSVersion.Version.Major >= 6) { POINT cursorLocation; if (User32.GetPhysicalCursorPos(out cursorLocation)) { return new Point(cursorLocation.X, cursorLocation.Y); } else { Win32Error error = Win32.GetLastErrorCode(); LOG.ErrorFormat("Error retrieving PhysicalCursorPos : {0}", Win32.GetMessage(error)); } } return new Point(Cursor.Position.X, Cursor.Position.Y); } /// /// This method will capture the current Cursor by using User32 Code /// /// A Capture Object with the Mouse Cursor information in it. public static ICapture CaptureCursor(ICapture capture) { LOG.Debug("Capturing the mouse cursor."); if (capture == null) { capture = new Capture(); } int x,y; IntPtr hicon; CursorInfo cursorInfo = new CursorInfo(); IconInfo iconInfo; cursorInfo.cbSize = Marshal.SizeOf(cursorInfo); if (User32.GetCursorInfo(out cursorInfo)) { if (cursorInfo.flags == User32.CURSOR_SHOWING) { hicon = User32.CopyIcon(cursorInfo.hCursor); if (User32.GetIconInfo(hicon, out iconInfo)) { Point cursorLocation = GetCursorLocation(); // Allign cursor location to Bitmap coordinates (instead of Screen coordinates) x = cursorLocation.X - iconInfo.xHotspot - capture.ScreenBounds.X; y = cursorLocation.Y - iconInfo.yHotspot - capture.ScreenBounds.Y; // Set the location capture.CursorLocation = new Point(x, y); using (Icon icon = Icon.FromHandle(hicon)) { capture.Cursor = icon; } if (iconInfo.hbmMask != IntPtr.Zero) { GDI32.DeleteObject(iconInfo.hbmMask); } if (iconInfo.hbmColor != IntPtr.Zero) { GDI32.DeleteObject(iconInfo.hbmColor); } } User32.DestroyIcon(hicon); } } return capture; } /// /// This method will call the CaptureRectangle with the screenbounds, therefor Capturing the whole screen. /// /// A Capture Object with the Screen as an Image public static ICapture CaptureScreen(ICapture capture) { if (capture == null) { capture = new Capture(); } return CaptureRectangle(capture, capture.ScreenBounds); } /// /// Helper method to create an exception that might explain what is wrong while capturing /// /// string with current method /// ICapture /// Rectangle of what we want to capture /// private static Exception CreateCaptureException(string method, Rectangle captureBounds) { Exception exceptionToThrow = User32.CreateWin32Exception(method); if (!captureBounds.IsEmpty) { exceptionToThrow.Data.Add("Height", captureBounds.Height); exceptionToThrow.Data.Add("Width", captureBounds.Width); } return exceptionToThrow; } /// /// Helper method to check if it is allowed to capture the process using DWM /// /// Process owning the window /// true if it's allowed public static bool isDWMAllowed(Process process) { if (process != null) { if (conf.NoDWMCaptureForProduct != null && conf.NoDWMCaptureForProduct.Count > 0) { try { string productName = process.MainModule.FileVersionInfo.ProductName; if (productName != null && conf.NoDWMCaptureForProduct.Contains(productName.ToLower())) { return false; } } catch (Exception ex) { LOG.Warn(ex.Message); } } } return true; } /// /// Helper method to check if it is allowed to capture the process using GDI /// /// Process owning the window /// true if it's allowed public static bool isGDIAllowed(Process process) { if (process != null) { if (conf.NoGDICaptureForProduct != null && conf.NoGDICaptureForProduct.Count > 0) { try { string productName = process.MainModule.FileVersionInfo.ProductName; if (productName != null && conf.NoGDICaptureForProduct.Contains(productName.ToLower())) { return false; } } catch (Exception ex) { LOG.Warn(ex.Message); } } } return true; } /// /// This method will use User32 code to capture the specified captureBounds from the screen /// /// ICapture where the captured Bitmap will be stored /// Rectangle with the bounds to capture /// A Capture Object with a part of the Screen as an Image public static ICapture CaptureRectangle(ICapture capture, Rectangle captureBounds) { if (capture == null) { capture = new Capture(); } capture.Image = CaptureRectangle(captureBounds); capture.Location = captureBounds.Location; ((Bitmap)capture.Image).SetResolution(capture.CaptureDetails.DpiX, capture.CaptureDetails.DpiY); if (capture.Image == null) { return null; } return capture; } /// /// This method will use User32 code to capture the specified captureBounds from the screen /// /// Rectangle with the bounds to capture /// Bitmap which is captured from the screen at the location specified by the captureBounds public static Bitmap CaptureRectangle(Rectangle captureBounds) { Bitmap returnBitmap = null; if (captureBounds.Height <= 0 || captureBounds.Width <= 0) { LOG.Warn("Nothing to capture, ignoring!"); return null; } else { LOG.Debug("CaptureRectangle Called!"); } // .NET GDI+ Solution, according to some post this has a GDI+ leak... // See http://connect.microsoft.com/VisualStudio/feedback/details/344752/gdi-object-leak-when-calling-graphics-copyfromscreen // Bitmap capturedBitmap = new Bitmap(captureBounds.Width, captureBounds.Height); // using (Graphics graphics = Graphics.FromImage(capturedBitmap)) { // graphics.CopyFromScreen(captureBounds.Location, Point.Empty, captureBounds.Size, CopyPixelOperation.CaptureBlt); // } // capture.Image = capturedBitmap; // capture.Location = captureBounds.Location; // "P/Invoke" Solution for capturing the screen IntPtr hWndDesktop = User32.GetDesktopWindow(); // get te hDC of the target window IntPtr hDCDesktop = User32.GetWindowDC(hWndDesktop); // Make sure the last error is set to 0 Win32.SetLastError(0); // create a device context we can copy to IntPtr hDCDest = GDI32.CreateCompatibleDC(hDCDesktop); // Check if the device context is there, if not throw an error with as much info as possible! if (hDCDest == IntPtr.Zero) { // Get Exception before the error is lost Exception exceptionToThrow = CreateCaptureException("CreateCompatibleDC", captureBounds); // Cleanup User32.ReleaseDC(hWndDesktop, hDCDesktop); // throw exception throw exceptionToThrow; } // Create BitmapInfoHeader for CreateDIBSection BitmapInfoHeader bmi = new BitmapInfoHeader(captureBounds.Width, captureBounds.Height, 24); // Make sure the last error is set to 0 Win32.SetLastError(0); // create a bitmap we can copy it to, using GetDeviceCaps to get the width/height IntPtr bits0; // not used for our purposes. It returns a pointer to the raw bits that make up the bitmap. IntPtr hDIBSection = GDI32.CreateDIBSection(hDCDesktop, ref bmi, BitmapInfoHeader.DIB_RGB_COLORS, out bits0, IntPtr.Zero, 0); if (hDIBSection == IntPtr.Zero) { // Get Exception before the error is lost Exception exceptionToThrow = CreateCaptureException("CreateDIBSection", captureBounds); exceptionToThrow.Data.Add("hdcDest", hDCDest.ToInt32()); exceptionToThrow.Data.Add("hdcSrc", hDCDesktop.ToInt32()); // clean up GDI32.DeleteDC(hDCDest); User32.ReleaseDC(hWndDesktop, hDCDesktop); // Throw so people can report the problem throw exceptionToThrow; } else { // select the bitmap object and store the old handle IntPtr hOldObject = GDI32.SelectObject(hDCDest, hDIBSection); // bitblt over (make copy) GDI32.BitBlt(hDCDest, 0, 0, captureBounds.Width, captureBounds.Height, hDCDesktop, captureBounds.X, captureBounds.Y, CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt); // restore selection (old handle) GDI32.SelectObject(hDCDest, hOldObject); // clean up GDI32.DeleteDC(hDCDest); User32.ReleaseDC(hWndDesktop, hDCDesktop); // get a .NET image object for it // A suggestion for the "A generic error occurred in GDI+." E_FAIL/0×80004005 error is to re-try... bool success = false; ExternalException exception = null; for(int i = 0; i < 3; i++) { try { // assign image to Capture, the image will be disposed there.. returnBitmap = Bitmap.FromHbitmap(hDIBSection); success = true; break; } catch (ExternalException ee) { LOG.Warn("Problem getting bitmap at try " + i + " : ", ee); exception = ee; } } if (!success) { LOG.Error("Still couldn't create Bitmap!"); throw exception; } // free up the Bitmap object GDI32.DeleteObject(hDIBSection); } return returnBitmap; } } }