C# image loses transparency when loaded in clipboard even with DIBv5 conversion

429 views Asked by At

I'm trying to load an image file into the clipboard in order to then paste it in Discord with Ctrl+V. It is possible to paste a PNG into Discord and conserve the transparency, as I am able to right click to copy an image on Mozilla or Chrome and then press Ctrl+V to paste it into the Discord client. A look to the clipboard content shows that Mozilla stores DeviceIndependantBitmap and Format17 data formats after copying an image. I therefore adapted the functions from the post Copying From and To Clipboard loses image transparency to convert an Image object into DIBv5 data and load it in the clipboard under the DeviceIndependantFormat name.

However the transparency is not supported. When then pasting the image into Discord, all transparent regions are filled with a black color. Here is the script used to test the loading of a PNG in the clipboard. The DIB header is built to exactly match Mozilla's DIB header when an image is copied (with only width, height and number of pixels information changed):

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;

namespace ClipboardDIBTest
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            // Loads a PNG with transparency in the clipboard.
            SetClipboardImage(Image.FromFile("./test.png"));
        }

        /// <summary>Loads an image into the DIBv5 format in the clipboard,
        /// under the `DeviceIndependantBitmap` format.</summary>
        static void SetClipboardImage(Image image)
        {
            Clipboard.Clear();
            DataObject data = new DataObject();
            using (MemoryStream dibV5MemStream = new MemoryStream())
            {
                Byte[] dibV5Data = ConvertToDIBv5(image);
                dibV5MemStream.Write(dibV5Data, 0, dibV5Data.Length);
                data.SetData(DataFormats.Dib, false, dibV5MemStream);
                // The 'copy=true' argument means the MemoryStreams can be safely disposed after the operation.
                Clipboard.SetDataObject(data, true);
            }
        }
        
        /// <summary>Retrieves bitmap data from the image, ensuring an ARGB pixel format.</summary>
        static Byte[] GetBM32Data(Image image)
        {
            Byte[] bm32bData;
            // 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);
                bm32bData = GetImageData(bm32b);
            }

            return bm32bData;
        }

        /// <summar>Retrieves the pixel data from a bitmap.</summary>
        static byte[] GetImageData(Bitmap bmp)
        {
            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);
            IntPtr ptr = bmpData.Scan0;
            int bytes = Math.Abs(bmpData.Stride) * bmp.Height;
            byte[] data = new byte[bytes];
            Marshal.Copy(ptr, data, 0, bytes);
            bmp.UnlockBits(bmpData);
            return data;
        }

        /// <summary>Converts the image into a DIBv5 (Format17) data format.</summary>
        static Byte[] ConvertToDIBv5(Image image)
        {
            Byte[] bm32bData = GetBM32Data(image);
            Int32 width = image.Width;
            Int32 height = image.Height;

            // Header data built to match Mozilla's Format17 content when an image is copied.

            Int32 hdrSize = 124;
            Byte[] fullImage = new Byte[hdrSize + bm32bData.Length];
            //Int32 bV5Size;
            WriteIntToByteArray(fullImage, 0, 4, true, (UInt32)hdrSize);
            //Int32 bV5Width;
            WriteIntToByteArray(fullImage, 4, 4, true, (UInt32)width);
            //Int32 bV5Height;
            WriteIntToByteArray(fullImage, 8, 4, true, (UInt32)height);
            //Int16 bV5Planes;
            WriteIntToByteArray(fullImage, 12, 2, true, 1);
            //Int16 bV5BitCount;
            WriteIntToByteArray(fullImage, 14, 2, true, 32);
            //Int32 bV5Compression;
            WriteIntToByteArray(fullImage, 16, 4, true, 0);
            //Int32 biSizeImage;
            WriteIntToByteArray(fullImage, 20, 4, true, (UInt32)bm32bData.Length);
            // These are all 0. Since .net clears new arrays, don't bother writing them.
            //Int32 bV5XPelsPerMeter = 0;
            //Int32 bV5YPelsPerMeter = 0;
            //Int32 bV5ClrUsed = 0;
            //Int32 bV5ClrImportant = 0;
            //Int32 Red/Green/Blue/Alpha masks
            WriteIntToByteArray(fullImage, 40, 4, true, 0x000000FF);
            WriteIntToByteArray(fullImage, 44, 4, true, 0x0000FF00);
            WriteIntToByteArray(fullImage, 48, 4, true, 0x00FF0000);
            WriteIntToByteArray(fullImage, 52, 4, true, 0xFF000000);
            // Int32 bV5CSType : "sRGB"
            WriteIntToByteArray(fullImage, 56, 4, true, 1934772034);
            // Rest is all 0

            Array.Copy(bm32bData, 0, fullImage, hdrSize, bm32bData.Length);
            return fullImage;
        }

        static void WriteIntToByteArray(Byte[] data, Int32 startIndex, Int32 bytes, Boolean littleEndian, UInt32 value)
        {
            Int32 lastByte = bytes - 1;
            if (data.Length < startIndex + bytes)
                throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to write a " + bytes + "-byte value at offset " + startIndex + ".");
            for (Int32 index = 0; index < bytes; index++)
            {
                Int32 offs = startIndex + (littleEndian ? index : lastByte - index);
                data[offs] = (Byte)(value >> (8 * index) & 0xFF);
            }
        }

        static UInt32 ReadIntFromByteArray(Byte[] data, Int32 startIndex, Int32 bytes, Boolean littleEndian)
        {
            Int32 lastByte = bytes - 1;
            if (data.Length < startIndex + bytes)
                throw new ArgumentOutOfRangeException("startIndex", "Data array is too small to read a " + bytes + "-byte value at offset " + startIndex + ".");
            UInt32 value = 0;
            for (Int32 index = 0; index < bytes; index++)
            {
                Int32 offs = startIndex + (littleEndian ? index : lastByte - index);
                value += (UInt32)(data[offs] << (8 * index));
            }
            return value;
        }
    }
}
  • The original image file:

Original PNG image, with transparency.

  • The pasted image in Discord with Ctrl+V, after running the program:

Pasted result in Discord. Transparent regions are filled with black.

I tried the following during my debugging/retroengineering attempts:

  • Copying an image from Mozilla (with right click and copy), then reading the data stored in the clipboard under the Format17 name, and saving it into a file before loading the content of the file into the clipboard again. If loaded under the Format17 name nothing happens on Ctrl+V (Discord doesn't detect anything to paste). But if loaded under DeviceIndependantBitmap then the image is correctly pasted in Discord (with transparency).
  • Making use of the same code from the post mentioned above to load the picture in the simpler DIB format, though it gives a black background as well.

Other posts I've read:

Does anyone have an idea on why this happens and how to fix it? Thanks.

0

There are 0 answers