/// <summary> /// This method will use User32 code to capture the specified captureBounds from the screen /// </summary> /// <param name="captureBounds">Rectangle with the bounds to capture</param> /// <returns>Bitmap which is captured from the screen at the location specified by the captureBounds</returns> 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 <Screen> screensInsideCapture = new List <Screen>(); 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); }
/// <summary> /// Write the images to the stream as icon /// Every image is resized to 256x256 (but the content maintains the aspect ratio) /// </summary> /// <param name="stream">Stream to write to</param> /// <param name="images">List of images</param> public static void WriteIcon(Stream stream, IList <Image> images) { var binaryWriter = new BinaryWriter(stream); // // ICONDIR structure // binaryWriter.Write((short)0); // reserved binaryWriter.Write((short)1); // image type (icon) binaryWriter.Write((short)images.Count); // number of images IList <Size> imageSizes = new List <Size>(); IList <MemoryStream> encodedImages = new List <MemoryStream>(); foreach (var image in images) { // Pick the best fit var sizes = new[] { 16, 32, 48 }; int size = 256; foreach (var possibleSize in sizes) { if (image.Width <= possibleSize && image.Height <= possibleSize) { size = possibleSize; break; } } var imageStream = new MemoryStream(); if (image.Width == size && image.Height == size) { using (var clonedImage = ImageHelper.Clone(image, PixelFormat.Format32bppArgb)) { clonedImage.Save(imageStream, ImageFormat.Png); imageSizes.Add(new Size(size, size)); } } else { // Resize to the specified size, first make sure the image is 32bpp using (var clonedImage = ImageHelper.Clone(image, PixelFormat.Format32bppArgb)) { using (var resizedImage = ImageHelper.ResizeImage(clonedImage, true, true, Color.Empty, size, size, null)) { resizedImage.Save(imageStream, ImageFormat.Png); imageSizes.Add(resizedImage.Size); } } } imageStream.Seek(0, SeekOrigin.Begin); encodedImages.Add(imageStream); } // // ICONDIRENTRY structure // const int iconDirSize = 6; const int iconDirEntrySize = 16; var offset = iconDirSize + (images.Count * iconDirEntrySize); for (int i = 0; i < images.Count; i++) { var imageSize = imageSizes[i]; // Write the width / height, 0 means 256 binaryWriter.Write(imageSize.Width == 256 ? (byte)0 : (byte)imageSize.Width); binaryWriter.Write(imageSize.Height == 256 ? (byte)0 : (byte)imageSize.Height); binaryWriter.Write((byte)0); // no pallete binaryWriter.Write((byte)0); // reserved binaryWriter.Write((short)0); // no color planes binaryWriter.Write((short)32); // 32 bpp binaryWriter.Write((int)encodedImages[i].Length); // image data length binaryWriter.Write(offset); offset += (int)encodedImages[i].Length; } binaryWriter.Flush(); // // Write image data // foreach (var encodedImage in encodedImages) { encodedImage.WriteTo(stream); encodedImage.Dispose(); } }
/// <summary> /// Saves image to stream with specified quality /// To prevent problems with GDI version of before Windows 7: /// the stream is checked if it's seekable and if needed a MemoryStream as "cache" is used. /// </summary> /// <param name="imageToSave">image to save</param> /// <param name="surface">surface for the elements, needed if the greenshot format is used</param> /// <param name="stream">Stream to save to</param> /// <param name="outputSettings">SurfaceOutputSettings</param> public static void SaveToStream(Image imageToSave, ISurface surface, Stream stream, SurfaceOutputSettings outputSettings) { bool useMemoryStream = false; MemoryStream memoryStream = null; if (outputSettings.Format == OutputFormat.greenshot && surface == null) { throw new ArgumentException("Surface needs to be set when using OutputFormat.Greenshot"); } try { ImageFormat imageFormat; switch (outputSettings.Format) { case OutputFormat.bmp: imageFormat = ImageFormat.Bmp; break; case OutputFormat.gif: imageFormat = ImageFormat.Gif; break; case OutputFormat.jpg: imageFormat = ImageFormat.Jpeg; break; case OutputFormat.tiff: imageFormat = ImageFormat.Tiff; break; case OutputFormat.ico: imageFormat = ImageFormat.Icon; break; default: imageFormat = ImageFormat.Png; break; } Log.DebugFormat("Saving image to stream with Format {0} and PixelFormat {1}", imageFormat, imageToSave.PixelFormat); // Check if we want to use a memory stream, to prevent issues with non seakable streams // The save is made to the targetStream, this is directed to either the MemoryStream or the original Stream targetStream = stream; if (!stream.CanSeek) { useMemoryStream = true; Log.Warn("Using memorystream prevent an issue with saving to a non seekable stream."); memoryStream = new MemoryStream(); targetStream = memoryStream; } if (Equals(imageFormat, ImageFormat.Jpeg)) { bool foundEncoder = false; foreach (ImageCodecInfo imageCodec in ImageCodecInfo.GetImageEncoders()) { if (imageCodec.FormatID == imageFormat.Guid) { EncoderParameters parameters = new EncoderParameters(1) { Param = { [0] = new EncoderParameter(Encoder.Quality, outputSettings.JPGQuality) } }; // Removing transparency if it's not supported in the output if (Image.IsAlphaPixelFormat(imageToSave.PixelFormat)) { Image nonAlphaImage = ImageHelper.Clone(imageToSave, PixelFormat.Format24bppRgb); AddTag(nonAlphaImage); nonAlphaImage.Save(targetStream, imageCodec, parameters); nonAlphaImage.Dispose(); } else { AddTag(imageToSave); imageToSave.Save(targetStream, imageCodec, parameters); } foundEncoder = true; break; } } if (!foundEncoder) { throw new ApplicationException("No JPG encoder found, this should not happen."); } } else if (Equals(imageFormat, ImageFormat.Icon)) { // FEATURE-916: Added Icon support IList <Image> images = new List <Image>(); images.Add(imageToSave); WriteIcon(stream, images); } else { bool needsDispose = false; // Removing transparency if it's not supported in the output if (!Equals(imageFormat, ImageFormat.Png) && Image.IsAlphaPixelFormat(imageToSave.PixelFormat)) { imageToSave = ImageHelper.Clone(imageToSave, PixelFormat.Format24bppRgb); needsDispose = true; } AddTag(imageToSave); // Added for OptiPNG bool processed = false; if (Equals(imageFormat, ImageFormat.Png) && !string.IsNullOrEmpty(CoreConfig.OptimizePNGCommand)) { processed = ProcessPngImageExternally(imageToSave, targetStream); } if (!processed) { imageToSave.Save(targetStream, imageFormat); } if (needsDispose) { imageToSave.Dispose(); } } // If we used a memory stream, we need to stream the memory stream to the original stream. if (useMemoryStream) { memoryStream.WriteTo(stream); } // Output the surface elements, size and marker to the stream if (outputSettings.Format != OutputFormat.greenshot) { return; } using (MemoryStream tmpStream = new MemoryStream()) { long bytesWritten = surface.SaveElementsToStream(tmpStream); using (BinaryWriter writer = new BinaryWriter(tmpStream)) { writer.Write(bytesWritten); Version v = Assembly.GetExecutingAssembly().GetName().Version; byte[] marker = Encoding.ASCII.GetBytes($"Greenshot{v.Major:00}.{v.Minor:00}"); writer.Write(marker); tmpStream.WriteTo(stream); } } } finally { memoryStream?.Dispose(); } }
/// <summary> /// Create an image from a surface with the settings from the output settings applied /// </summary> /// <param name="surface"></param> /// <param name="outputSettings"></param> /// <param name="imageToSave"></param> /// <returns>true if the image must be disposed</returns> public static bool CreateImageFromSurface(ISurface surface, SurfaceOutputSettings outputSettings, out Image imageToSave) { bool disposeImage = false; if (outputSettings.Format == OutputFormat.greenshot || outputSettings.SaveBackgroundOnly) { // We save the image of the surface, this should not be disposed imageToSave = surface.Image; } else { // We create the export image of the surface to save imageToSave = surface.GetImageForExport(); disposeImage = true; } // The following block of modifications should be skipped when saving the greenshot format, no effects or otherwise! if (outputSettings.Format == OutputFormat.greenshot) { return(disposeImage); } Image tmpImage; if (outputSettings.Effects != null && outputSettings.Effects.Count > 0) { // apply effects, if there are any using (Matrix matrix = new Matrix()) { tmpImage = ImageHelper.ApplyEffects(imageToSave, outputSettings.Effects, matrix); } if (tmpImage != null) { if (disposeImage) { imageToSave.Dispose(); } imageToSave = tmpImage; disposeImage = true; } } // check for color reduction, forced or automatically, only when the DisableReduceColors is false if (outputSettings.DisableReduceColors || (!CoreConfig.OutputFileAutoReduceColors && !outputSettings.ReduceColors)) { return(disposeImage); } bool isAlpha = Image.IsAlphaPixelFormat(imageToSave.PixelFormat); if (outputSettings.ReduceColors || (!isAlpha && CoreConfig.OutputFileAutoReduceColors)) { using (var quantizer = new WuQuantizer((Bitmap)imageToSave)) { int colorCount = quantizer.GetColorCount(); Log.InfoFormat("Image with format {0} has {1} colors", imageToSave.PixelFormat, colorCount); if (!outputSettings.ReduceColors && colorCount >= 256) { return(disposeImage); } try { Log.Info("Reducing colors on bitmap to 256."); tmpImage = quantizer.GetQuantizedImage(CoreConfig.OutputFileReduceColorsTo); if (disposeImage) { imageToSave.Dispose(); } imageToSave = tmpImage; // Make sure the "new" image is disposed disposeImage = true; } catch (Exception e) { Log.Warn("Error occurred while Quantizing the image, ignoring and using original. Error: ", e); } } } else if (isAlpha && !outputSettings.ReduceColors) { Log.Info("Skipping 'optional' color reduction as the image has alpha"); } return(disposeImage); }
/// <summary> /// Saves image to stream with specified quality /// To prevent problems with GDI version of before Windows 7: /// the stream is checked if it's seekable and if needed a MemoryStream as "cache" is used. /// </summary> /// <param name="imageToSave">image to save</param> /// <param name="surface">surface for the elements, needed if the greenshot format is used</param> /// <param name="stream">Stream to save to</param> /// <param name="outputSettings">SurfaceOutputSettings</param> public static void SaveToStream(Image imageToSave, ISurface surface, Stream stream, SurfaceOutputSettings outputSettings) { ImageFormat imageFormat; bool useMemoryStream = false; MemoryStream memoryStream = null; if (outputSettings.Format == OutputFormat.greenshot && surface == null) { throw new ArgumentException("Surface needs to be se when using OutputFormat.Greenshot"); } try { switch (outputSettings.Format) { case OutputFormat.bmp: imageFormat = ImageFormat.Bmp; break; case OutputFormat.gif: imageFormat = ImageFormat.Gif; break; case OutputFormat.jpg: imageFormat = ImageFormat.Jpeg; break; case OutputFormat.tiff: imageFormat = ImageFormat.Tiff; break; default: // Problem with non-seekable streams most likely doesn't happen with Windows 7 (OS Version 6.1 and later) // http://stackoverflow.com/questions/8349260/generic-gdi-error-on-one-machine-but-not-the-other if (!stream.CanSeek) { int majorVersion = Environment.OSVersion.Version.Major; int minorVersion = Environment.OSVersion.Version.Minor; if (majorVersion < 6 || (majorVersion == 6 && minorVersion == 0)) { useMemoryStream = true; LOG.Warn("Using memorystream prevent an issue with saving to a non seekable stream."); } } imageFormat = ImageFormat.Png; break; } LOG.DebugFormat("Saving image to stream with Format {0} and PixelFormat {1}", imageFormat, imageToSave.PixelFormat); // Check if we want to use a memory stream, to prevent a issue which happens with Windows before "7". // The save is made to the targetStream, this is directed to either the MemoryStream or the original Stream targetStream = stream; if (useMemoryStream) { memoryStream = new MemoryStream(); targetStream = memoryStream; } if (Equals(imageFormat, ImageFormat.Jpeg)) { bool foundEncoder = false; foreach (ImageCodecInfo imageCodec in ImageCodecInfo.GetImageEncoders()) { if (imageCodec.FormatID == imageFormat.Guid) { EncoderParameters parameters = new EncoderParameters(1); parameters.Param[0] = new EncoderParameter(Encoder.Quality, outputSettings.JPGQuality); // Removing transparency if it's not supported in the output if (Image.IsAlphaPixelFormat(imageToSave.PixelFormat)) { Image nonAlphaImage = ImageHelper.Clone(imageToSave, PixelFormat.Format24bppRgb); AddTag(nonAlphaImage); nonAlphaImage.Save(targetStream, imageCodec, parameters); nonAlphaImage.Dispose(); nonAlphaImage = null; } else { AddTag(imageToSave); imageToSave.Save(targetStream, imageCodec, parameters); } foundEncoder = true; break; } } if (!foundEncoder) { throw new ApplicationException("No JPG encoder found, this should not happen."); } } else { bool needsDispose = false; // Removing transparency if it's not supported in the output if (!Equals(imageFormat, ImageFormat.Png) && Image.IsAlphaPixelFormat(imageToSave.PixelFormat)) { imageToSave = ImageHelper.Clone(imageToSave, PixelFormat.Format24bppRgb); needsDispose = true; } AddTag(imageToSave); // Added for OptiPNG bool processed = false; if (Equals(imageFormat, ImageFormat.Png) && !string.IsNullOrEmpty(conf.OptimizePNGCommand)) { processed = ProcessPNGImageExternally(imageToSave, targetStream); } if (!processed) { imageToSave.Save(targetStream, imageFormat); } if (needsDispose) { imageToSave.Dispose(); imageToSave = null; } } // If we used a memory stream, we need to stream the memory stream to the original stream. if (useMemoryStream) { memoryStream.WriteTo(stream); } // Output the surface elements, size and marker to the stream if (outputSettings.Format == OutputFormat.greenshot) { using (MemoryStream tmpStream = new MemoryStream()) { long bytesWritten = surface.SaveElementsToStream(tmpStream); using (BinaryWriter writer = new BinaryWriter(tmpStream)) { writer.Write(bytesWritten); Version v = Assembly.GetExecutingAssembly().GetName().Version; byte[] marker = Encoding.ASCII.GetBytes(String.Format("Greenshot{0:00}.{1:00}", v.Major, v.Minor)); writer.Write(marker); tmpStream.WriteTo(stream); } } } } finally { if (memoryStream != null) { memoryStream.Dispose(); } } }