/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2013 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 Greenshot.IniFile; using Greenshot.Plugin; using GreenshotPlugin.UnmanagedHelpers; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System.Windows.Forms; 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 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 image = new Bitmap(value); } finally { // Always dispose, even when a exception occured value.Dispose(); } } LOG.DebugFormat("Image is set with the following specifications: {0} - {1}", image.Size, 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() { 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 CoreConfiguration conf = IniConfig.GetIniSection(); /// /// Used to cleanup the unmanged resource in the iconInfo for the CaptureCursor method /// /// /// [DllImport("gdi32", SetLastError = true)] private static extern bool DeleteObject(IntPtr hObject); 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, relative to the origin of the monitor setup (i.e. negative coordinates are /// possible in multiscreen setups) 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); } /// /// Retrieves the cursor location safely, accounting for DPI settings in Vista/Windows 7. This implementation /// can conveniently be used when the cursor location is needed to deal with a fullscreen bitmap. /// Point with cursor location, relative to the top left corner of the monitor setup (which itself might /// actually not be on any screen) public static Point GetCursorLocationRelativeToScreenBounds() { return GetLocationRelativeToScreenBounds(GetCursorLocation()); } /// /// Converts locationRelativeToScreenOrigin to be relative to top left corner of all screen bounds, which might /// be different in multiscreen setups. This implementation /// can conveniently be used when the cursor location is needed to deal with a fullscreen bitmap. /// /// /// public static Point GetLocationRelativeToScreenBounds(Point locationRelativeToScreenOrigin) { Point ret = locationRelativeToScreenOrigin; Rectangle bounds = GetScreenBounds(); ret.Offset(-bounds.X, -bounds.Y); return ret; } /// /// 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; CursorInfo cursorInfo = new CursorInfo(); IconInfo iconInfo; cursorInfo.cbSize = Marshal.SizeOf(cursorInfo); if (User32.GetCursorInfo(out cursorInfo)) { if (cursorInfo.flags == User32.CURSOR_SHOWING) { using (SafeIconHandle safeIcon = User32.CopyIcon(cursorInfo.hCursor)) { if (User32.GetIconInfo(safeIcon, 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(safeIcon.DangerousGetHandle())) { capture.Cursor = icon; } if (iconInfo.hbmMask != IntPtr.Zero) { DeleteObject(iconInfo.hbmMask); } if (iconInfo.hbmColor != IntPtr.Zero) { DeleteObject(iconInfo.hbmColor); } } } } } 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; if (capture.CaptureDetails != null) { ((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; using (SafeWindowDCHandle desktopDCHandle = SafeWindowDCHandle.fromDesktop()) { if (desktopDCHandle.IsInvalid) { // Get Exception before the error is lost Exception exceptionToThrow = CreateCaptureException("desktopDCHandle", captureBounds); // throw exception throw exceptionToThrow; } // create a device context we can copy to using (SafeCompatibleDCHandle safeCompatibleDCHandle = GDI32.CreateCompatibleDC(desktopDCHandle)) { // Check if the device context is there, if not throw an error with as much info as possible! if (safeCompatibleDCHandle.IsInvalid) { // Get Exception before the error is lost Exception exceptionToThrow = CreateCaptureException("CreateCompatibleDC", captureBounds); // 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. using (SafeDibSectionHandle safeDibSectionHandle = GDI32.CreateDIBSection(desktopDCHandle, ref bmi, BitmapInfoHeader.DIB_RGB_COLORS, out bits0, IntPtr.Zero, 0)) { if (safeDibSectionHandle.IsInvalid) { // Get Exception before the error is lost Exception exceptionToThrow = CreateCaptureException("CreateDIBSection", captureBounds); exceptionToThrow.Data.Add("hdcDest", safeCompatibleDCHandle.DangerousGetHandle().ToInt32()); exceptionToThrow.Data.Add("hdcSrc", desktopDCHandle.DangerousGetHandle().ToInt32()); // Throw so people can report the problem throw exceptionToThrow; } else { // select the bitmap object and store the old handle using (SafeSelectObjectHandle selectObject = safeCompatibleDCHandle.SelectObject(safeDibSectionHandle)) { // bitblt over (make copy) GDI32.BitBlt(safeCompatibleDCHandle, 0, 0, captureBounds.Width, captureBounds.Height, desktopDCHandle, captureBounds.X, captureBounds.Y, CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt); } // 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 { // Collect all screens inside this capture List screensInsideCapture = new List(); foreach (Screen screen in Screen.AllScreens) { if (screen.Bounds.IntersectsWith(captureBounds)) { screensInsideCapture.Add(screen); } } // Check all all screens are of an equal size bool offscreenContent = false; using (Region captureRegion = new Region(captureBounds)) { // Exclude every visible part foreach (Screen screen in screensInsideCapture) { captureRegion.Exclude(screen.Bounds); } // If the region is not empty, we have "offscreenContent" using (Graphics screenGraphics = Graphics.FromHwnd(User32.GetDesktopWindow())) { offscreenContent = !captureRegion.IsEmpty(screenGraphics); } } // Check if we need to have a transparent background, needed for offscreen content if (offscreenContent) { using (Bitmap tmpBitmap = Bitmap.FromHbitmap(safeDibSectionHandle.DangerousGetHandle())) { // Create a new bitmap which has a transparent background returnBitmap = ImageHelper.CreateEmpty(tmpBitmap.Width, tmpBitmap.Height, PixelFormat.Format32bppArgb, Color.Transparent, tmpBitmap.HorizontalResolution, tmpBitmap.VerticalResolution); // Content will be copied here using (Graphics graphics = Graphics.FromImage(returnBitmap)) { // For all screens copy the content to the new bitmap foreach (Screen screen in Screen.AllScreens) { Rectangle screenBounds = screen.Bounds; // Make sure the bounds are offsetted to the capture bounds screenBounds.Offset(-captureBounds.X, -captureBounds.Y); graphics.DrawImage(tmpBitmap, screenBounds, screenBounds.X, screenBounds.Y, screenBounds.Width, screenBounds.Height, GraphicsUnit.Pixel); } } } } else { // All screens, which are inside the capture, are of equal size // assign image to Capture, the image will be disposed there.. returnBitmap = Bitmap.FromHbitmap(safeDibSectionHandle.DangerousGetHandle()); } // We got through the capture without exception 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; } } } } } return returnBitmap; } } }