I found some research about using the DIB format on the clipboard, which suggests to do it differently.

This implements that change, and potentially solves #348, BUG-2892 and more (maybe SUPPORT-135, #172, #365)
This commit is contained in:
Robin Krom 2022-02-01 22:36:16 +01:00
parent 8ce4735aad
commit b94d9139f2
No known key found for this signature in database
GPG key ID: BCC01364F1371490
4 changed files with 96 additions and 26 deletions

View file

@ -175,7 +175,7 @@ EndSelection:<<<<<<<4
try try
{ {
// For BUG-1935 this was changed from looping ourselfs, or letting MS retry... // For BUG-1935 this was changed from looping ourselves, or letting MS retry...
Clipboard.SetDataObject(ido, copy, 15, 200); Clipboard.SetDataObject(ido, copy, 15, 200);
} }
catch (Exception clipboardSetException) catch (Exception clipboardSetException)
@ -866,19 +866,13 @@ EndSelection:<<<<<<<4
{ {
if (CoreConfig.ClipboardFormats.Contains(ClipboardFormat.DIB)) if (CoreConfig.ClipboardFormats.Contains(ClipboardFormat.DIB))
{ {
using (MemoryStream tmpBmpStream = new MemoryStream()) // Create the stream for the clipboard
{ dibStream = new MemoryStream();
// Save image as BMP var dibBytes = DibHelper.ConvertToDib(imageToSave);
SurfaceOutputSettings bmpOutputSettings = new SurfaceOutputSettings(OutputFormat.bmp, 100, false); dibStream.Write(dibBytes,0, dibBytes.Length);
ImageOutput.SaveToStream(imageToSave, null, tmpBmpStream, bmpOutputSettings);
dibStream = new MemoryStream();
// Copy the source, but skip the "BITMAPFILEHEADER" which has a size of 14
dibStream.Write(tmpBmpStream.GetBuffer(), BITMAPFILEHEADER_LENGTH, (int) tmpBmpStream.Length - BITMAPFILEHEADER_LENGTH);
}
// Set the DIB to the clipboard DataObject // Set the DIB to the clipboard DataObject
dataObject.SetData(DataFormats.Dib, true, dibStream); dataObject.SetData(DataFormats.Dib, false, dibStream);
} }
} }
catch (Exception dibEx) catch (Exception dibEx)

View file

@ -658,16 +658,6 @@ namespace Greenshot.Base.Core
{ {
WebRequestReadWriteTimeout = 100; WebRequestReadWriteTimeout = 100;
} }
// Workaround for the Windows 11 clipboard issue found here: https://github.com/greenshot/greenshot/issues/348
if (WindowsVersion.IsWindows11OrLater)
{
// If the format DIB is used, remove it and replace it with BITMAP.
if (ClipboardFormats.Contains(ClipboardFormat.DIB))
{
ClipboardFormats = ClipboardFormats.Where(cf => cf != ClipboardFormat.DIB).Append(ClipboardFormat.BITMAP).ToList();
}
}
} }
/// <summary> /// <summary>

View file

@ -0,0 +1,85 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using Greenshot.Base.UnmanagedHelpers;
namespace Greenshot.Base.Core
{
/// <summary>
/// Though Greenshot implements the specs for the DIB image format,
/// it seems to cause a lot of issues when using the clipboard.
/// There is some research done about the DIB on the clipboard, this code was taking from:
/// <a href="https://stackoverflow.com/questions/44177115/copying-from-and-to-clipboard-loses-image-transparency">here</a>
/// </summary>
internal static class DibHelper
{
/// <summary>
/// Converts the image to Device Independent Bitmap format of type BITFIELDS.
/// This is (wrongly) accepted by many applications as containing transparency,
/// so I'm abusing it for that.
/// </summary>
/// <param name="image">Image to convert to DIB</param>
/// <returns>The image converted to DIB, in bytes.</returns>
public static byte[] ConvertToDib(Image image)
{
byte[] bm32bData;
int width = image.Width;
int height = image.Height;
// Ensure image is 32bppARGB by painting it on a new 32bppARGB image.
using (var bm32b = ImageHelper.CreateEmptyLike(image, Color.Transparent, PixelFormat.Format32bppArgb))
{
using (var graphics = Graphics.FromImage(bm32b))
{
graphics.DrawImage(image, new Rectangle(0, 0, bm32b.Width, bm32b.Height));
}
// Bitmap format has its lines reversed.
bm32b.RotateFlip(RotateFlipType.Rotate180FlipX);
bm32bData = GetImageData(bm32b, out var stride);
}
// BITMAPINFOHEADER struct for DIB.
uint hdrSize = 0x28;
var fullImage = new byte[hdrSize + 12 + bm32bData.Length];
var bitmapInfoHeader = MemoryMarshal.Cast<byte, BITMAPINFOHEADER>(fullImage.AsSpan());
bitmapInfoHeader[0].biSize = hdrSize;
bitmapInfoHeader[0].biWidth = width;
bitmapInfoHeader[0].biHeight = height;
bitmapInfoHeader[0].biPlanes = 1;
bitmapInfoHeader[0].biBitCount = 32;
bitmapInfoHeader[0].biCompression = BI_COMPRESSION.BI_BITFIELDS;
bitmapInfoHeader[0].biSizeImage = (uint)bm32bData.Length;
bitmapInfoHeader[0].biXPelsPerMeter = (int)(image.HorizontalResolution * 39.3701);
bitmapInfoHeader[0].biYPelsPerMeter = (int)(image.VerticalResolution * 39.3701);
// The aforementioned "BITFIELDS": colour masks applied to the Int32 pixel value to get the R, G and B values.
bitmapInfoHeader[0].bV5RedMask = 0x00FF0000;
bitmapInfoHeader[0].bV5GreenMask = 0x0000FF00;
bitmapInfoHeader[0].bV5BlueMask = 0x000000FF;
// These are all 0. Since .net clears new arrays, don't bother writing them.
//Int32 biClrUsed = 0;
//Int32 biClrImportant = 0;
Array.Copy(bm32bData, 0, fullImage, hdrSize + 12, bm32bData.Length);
return fullImage;
}
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <returns>The raw bytes of the image</returns>
public static byte[] GetImageData(Bitmap sourceImage, out int stride)
{
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
byte[] data = new byte[stride * sourceImage.Height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
sourceImage.UnlockBits(sourceData);
return data;
}
}
}

View file

@ -1497,16 +1497,17 @@ namespace Greenshot.Base.Core
/// </summary> /// </summary>
/// <param name="sourceImage">the source bitmap as the specifications for the new bitmap</param> /// <param name="sourceImage">the source bitmap as the specifications for the new bitmap</param>
/// <param name="backgroundColor">The color to fill with, or Color.Empty to take the default depending on the pixel format</param> /// <param name="backgroundColor">The color to fill with, or Color.Empty to take the default depending on the pixel format</param>
/// <returns></returns> /// <param name="pixelFormat">PixelFormat</param>
public static Bitmap CreateEmptyLike(Image sourceImage, Color backgroundColor) /// <returns>Bitmap</returns>
public static Bitmap CreateEmptyLike(Image sourceImage, Color backgroundColor, PixelFormat? pixelFormat = null)
{ {
PixelFormat pixelFormat = sourceImage.PixelFormat; pixelFormat ??= sourceImage.PixelFormat;
if (backgroundColor.A < 255) if (backgroundColor.A < 255)
{ {
pixelFormat = PixelFormat.Format32bppArgb; pixelFormat = PixelFormat.Format32bppArgb;
} }
return CreateEmpty(sourceImage.Width, sourceImage.Height, pixelFormat, backgroundColor, sourceImage.HorizontalResolution, sourceImage.VerticalResolution); return CreateEmpty(sourceImage.Width, sourceImage.Height, pixelFormat.Value, backgroundColor, sourceImage.HorizontalResolution, sourceImage.VerticalResolution);
} }
/// <summary> /// <summary>