Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
267 views
in Technique[技术] by (71.8m points)

c# - Copying From and To Clipboard loses image transparency

I've been trying to copy a transparent PNG image to clipboard and preserve its transparency to paste it into a specific program that supports it.

I tried many solutions already but the background always ended up gray in one way or another.

So I tried copying the same image using Chrome and pasting it into the program and it worked. It preserved transparency. So then I tried Getting the image from the Clipboard that I had copied using Chrome and Set the image again, expecting the transparency to still be there - but no, transparency was not preserved even though I just took the image from the clipboard and set it again.

var img = Clipboard.GetImage(); // copied using Chrome and transparency is preserved
Clipboard.SetImage(img); // transparency lost

Same issue even if I use the System.Windows.Forms.Clipboard or try getting and setting the DataObject instead of the Image.

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The Windows clipboard, by default, does not support transparency, but you can put content on the clipboard in many types together to make sure most applications find some type in it that they can use. Sadly, the most common type, DeviceIndependentBitmap (which Windows itself seems to use) is a really dirty and unreliable one. I wrote a big rant explanation about that here.

I'll assume you have read through that before continuing with my answer here, because it contains the background information required for the next part.

Now, the cleanest way of putting an image on the clipboard with transparency support is a PNG stream, but it won't guarantee that all applications can paste it. Gimp supports PNG paste, and apparently so do the newer MS Office programs, but Google Chrome, for example, doesn't, and will only accept the messy DIB type detailed in the answer I linked to. On the other hand, Gimp will not accept DIB as having transparency, because its creators actually followed the format's specifications, and realized that the format was unreliable (as clearly demonstrated by that question I linked).

Because of the DIB mess, sadly, the best thing to do is simply to put it in there in as many generally-supported types as you can, including PNG, DIB and the normal Image.

PNG and DIB are both put on the clipboard in the same way: by putting them in the DataObject as MemoryStream, and then giving the clipboard the "copy" instruction when actually putting it on.

Most of this is straightforward, but the DIB one is a bit more complex. Note that the following part contains a couple of references to my own toolsets. The GetImageData one can be found in this answer, the BuildImage one can be found here, and the ArrayUtils ones are given below.

These toolsets all use System.Drawing, though. You'll have to figure out for yourself exactly how to do the same things in WPF.

/// <summary>
/// Copies the given image to the clipboard as PNG, DIB and standard Bitmap format.
/// </summary>
/// <param name="image">Image to put on the clipboard.</param>
/// <param name="imageNoTr">Optional specifically nontransparent version of the image to put on the clipboard.</param>
/// <param name="data">Clipboard data object to put the image into. Might already contain other stuff. Leave null to create a new one.</param>
public static void SetClipboardImage(Bitmap image, Bitmap imageNoTr, DataObject data)
{
    Clipboard.Clear();
    if (data == null)
        data = new DataObject();
    if (imageNoTr == null)
        imageNoTr = image;
    using (MemoryStream pngMemStream = new MemoryStream())
    using (MemoryStream dibMemStream = new MemoryStream())
    {
        // As standard bitmap, without transparency support
        data.SetData(DataFormats.Bitmap, true, imageNoTr);
        // As PNG. Gimp will prefer this over the other two.
        image.Save(pngMemStream, ImageFormat.Png);
        data.SetData("PNG", false, pngMemStream);
        // As DIB. This is (wrongly) accepted as ARGB by many applications.
        Byte[] dibData = ConvertToDib(image);
        dibMemStream.Write(dibData, 0, dibData.Length);
        data.SetData(DataFormats.Dib, false, dibMemStream);
        // The 'copy=true' argument means the MemoryStreams can be safely disposed after the operation.
        Clipboard.SetDataObject(data, true);
    }
}
    
/// <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;
    Int32 width = image.Width;
    Int32 height = image.Height;
    // Ensure image is 32bppARGB by painting it on a new 32bppARGB image.
    using (Bitmap bm32b = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb))
    {
        using (Graphics gr = Graphics.FromImage(bm32b))
            gr.DrawImage(image, new Rectangle(0, 0, bm32b.Width, bm32b.Height));
        // Bitmap format has its lines reversed.
        bm32b.RotateFlip(RotateFlipType.Rotate180FlipX);
        Int32 stride;
        bm32bData = ImageUtils.GetImageData(bm32b, out stride);
    }
    // BITMAPINFOHEADER struct for DIB.
    Int32 hdrSize = 0x28;
    Byte[] fullImage = new Byte[hdrSize + 12 + bm32bData.Length];
    //Int32 biSize;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x00, 4, true, (UInt32)hdrSize);
    //Int32 biWidth;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x04, 4, true, (UInt32)width);
    //Int32 biHeight;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x08, 4, true, (UInt32)height);
    //Int16 biPlanes;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x0C, 2, true, 1);
    //Int16 biBitCount;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x0E, 2, true, 32);
    //BITMAPCOMPRESSION biCompression = BITMAPCOMPRESSION.BITFIELDS;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x10, 4, true, 3);
    //Int32 biSizeImage;
    ArrayUtils.WriteIntToByteArray(fullImage, 0x14, 4, true, (UInt32)bm32bData.Length);
    // These are all 0. Since .net clears new arrays, don't bother writing them.
    //Int32 biXPelsPerMeter = 0;
    //Int32 biYPelsPerMeter = 0;
    //Int32 biClrUsed = 0;
    //Int32 biClrImportant = 0;

    // The aforementioned "BITFIELDS": colour masks applied to the Int32 pixel value to get the R, G and B values.
    ArrayUtils.WriteIntToByteArray(fullImage, hdrSize + 0, 4, true, 0x00FF0000);
    ArrayUtils.WriteIntToByteArray(fullImage, hdrSize + 4, 4, true, 0x0000FF00);
    ArrayUtils.WriteIntToByteArray(fullImage, hdrSize + 8, 4, true, 0x000000FF);
    Array.Copy(bm32bData, 0, fullImage, hdrSize + 12, bm32bData.Length);
    return fullImage;
}

Now, as for getting an image off the clipboard, I noticed there is apparently a difference in behaviour between .Net 3.5 and the later ones, which seem to actually use that DIB. Given that difference, and given how unreliable the DIB format is, you'll want to actually check manually for all types, preferably starting with the completely reliable PNG format.

You can get the DataObject from the clipboard with this code:

DataObject retrievedData = Clipboard.GetDataObject() as DataObject;

The CloneImage function used here is basically just the combination of my GetImageData and BuildImage toolsets, ensuring that a new image is created without any backing resources that might mess up; image objects are known to cause crashes when they're based on a Stream that then gets disposed. A compacted and optimised version of it was posted here, in a question well worth reading on the subject of why this cloning is so important.

/// <summary>
/// Retrieves an image from the given clipboard data object, in the order PNG, DIB, Bitmap, Image object.
/// </summary>
/// <param name="retrievedData">The clipboard data.</param>
/// <returns>The extracted image, or null if no supported image type was found.</returns>
public static Bitmap GetClipboardImage(DataObject retrievedData)
{
    Bitmap clipboardimage = null;
    // Order: try PNG, move on to try 32-bit ARGB DIB, then try the normal Bitmap and Image types.
    if (retrievedData.GetDataPresent("PNG", false))
    {
        MemoryStream png_stream = retrievedData.GetData("PNG", false) as MemoryStream;
        if (png_stream != null)
            using (Bitmap bm = new Bitmap(png_stream))
                clipboardimage = ImageUtils.CloneImage(bm);
    }
    if (clipboardimage == null && retrievedData.GetDataPresent(DataFormats.Dib, false))
    {
        MemoryStream dib = retrievedData.GetData(DataFormats.Dib, false) as MemoryStream;
        if (dib != null)
            clipboardimage = ImageFromClipboardDib(dib.ToArray());
    }
    if (clipboardimage == null && retrievedData.GetDataPresent(DataFormats.Bitmap))
        clipboardimage = new Bitmap(retrievedData.GetData(DataFormats.Bitmap) as Image);
    if (clipboardimage == null && retrievedData.GetDataPresent(typeof(Image)))
        clipboardimage = new Bitmap(retrievedData.GetData(typeof(Image)) as Image);
    return clipboardimage;
}

public static Bitmap ImageFromClipboardDib(Byte[] dibBytes)
{
    if (dibBytes == null || dibBytes.Length < 4)
        return null;
    try
    {
        Int32 headerSize = (Int32)ArrayUtils.ReadIntFromByteArray(dibBytes, 0, 4, true);
        // Only supporting 40-byte DIB from clipboard
        if (headerSize != 40)
            return null;
        Byte[] header = new Byte[40];
        Array.Copy(dibBytes, header, 40);
        Int32 imageIndex = headerSize;
        Int32 width = (Int32)ArrayUtils.ReadIntFromByteArray(header, 0x04, 4, true);
        Int32 height = (Int32)ArrayUtils.ReadIntFromByteArray(header, 0x08, 4, true);
        Int16 planes = (Int16)ArrayUtils.ReadIntFromByteArray(header, 0x0C, 2, true);
        Int16 bitCount = (Int16)ArrayUtils.ReadIntFromByteArray(header, 0x0E, 2, true);
        //Compression: 0 = RGB; 3 = BITFIELDS.
        Int32 compression = (Int32)ArrayUtils.ReadIntFromByteArray(header, 0x10, 4, true);
        // Not dealing with non-standard formats.
        if (planes != 1 || (compression != 0 && compression != 3))
            return null;
        PixelFormat fmt;
        switch (bitCount)
        {
            case 32:
                fmt = PixelFormat.Format32bppRgb;
                break;
            case 24:
                fmt = PixelFormat.Format24bppRgb;
                break;
            case 16:
                fmt = PixelFormat.Format16bppRgb555;
                break;
            default:
                return null;
        }
        if (compression == 3)
            imageIndex += 12;
        if (dibBytes.Length < imageIndex)
            return null;
        Byte[] image = new Byte[dibBytes.Length - imageIndex];
        Array.Copy(dibBytes, imageIndex, image, 0, image.Length);
        // Classic stride: fit within blocks of 4 bytes.
        Int32 stride = (((((bitCount * width) + 7) / 8) + 3) / 4) * 4;
        if (compression == 3)
        {
            UInt32 redMask = ArrayUtils.ReadIntFromByteArray(dibBytes, headerSize + 0, 4, true);
            UInt32 greenMask = ArrayUtils.ReadIntFromByteArray(dibBytes, headerSize + 4, 4, true);
            UInt32 blueMask = ArrayUtils.ReadIntFromByteArray(dibBytes, headerSize + 8, 4, true);
            // Fix for the undocumented use of 32bppARGB disguised as BITFIELDS. Despite lacking an alpha bit field,
            // the alpha bytes are still filled in, without any header indication of alpha usage.
            // Pure 32-bit RGB: check if a switch to ARGB can be made by checking for non-zero alpha.
            // Admitted, this may give a mess if the alpha bits simply aren't cleared, but why the hell wouldn't it use 24bpp then?
            if (bitCount == 32 && redMask == 0xFF0000 && greenMask == 0x00FF00 && blueMask == 0x0000FF)
            {
                // Strid

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...