2017-05-25 114 views
3

我一直在試圖複製一個透明的PNG圖像剪貼板和保留它的透明將其粘貼到支持它的特定程序。複製從和到剪貼板丟失圖像的透明

我已經嘗試了很多解決方案,但背景總是以某種方式變成灰色。

所以我試圖使用Chrome複製相同的圖像,並將其粘貼到程序中,它工作。它保留了透明度。於是我嘗試從我使用Chrome複製的剪貼板上獲取圖像,並再次設置圖像,期待透明度仍然存在 - 但是,即使我只是從剪貼板中取出圖像並設置它再次。

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

同樣的問題,即使我用System.Windows.Forms.Clipboard或嘗試獲取和設置DataObject,而不是圖像。

+0

這應該是「保持透明度「。 ;) – Nyerguds

回答

7

默認情況下,Windows剪貼板不支持透明度,但您可以將剪貼板中的內容放在多種類型中,以確保大多數應用程序都能找到可供使用的類型。可悲的是,最常見的類型(Windows本身似乎使用)是一個非常骯髒和不可靠的類型。我寫了一個大的 rant關於那個here的解釋。

我假設你已經通讀了這篇文章,然後繼續我的答案,因爲它包含了下一部分所需的背景信息。

現在,在透明支持的剪貼板上放置圖像的最簡潔方式是PNG流,但它不能保證所有應用程序都可以粘貼它。 Gimp支持PNG粘貼,顯然新版MS Office程序也是如此,但Google Chrome並沒有,並且只會接受我鏈接到的答案中詳細描述的混亂的DIB類型。另一方面,Gimp不會接受DIB具有透明度,因爲它的創建者實際上遵循了格式的規範,並且意識到格式不可靠(如我所鏈接的問題所清楚表明的那樣)。

由於DIB混亂,可悲的是,最好的辦法是簡單地將它放在儘可能多的通常支持的類型中,包括PNG,DIB和普通圖像。

PNG和DIB都以相同的方式放在剪貼板上:通過將它們放在DataObject中作爲MemoryStream,然後在實際上將剪貼板放入「複製」指令時。

大部分情況都很簡單,但是DIB更復雜一些。請注意,以下部分包含一些對我自己的工具集的引用。可以找到GetImageData可以找到in this answer,BuildImage可以找到here,以及ArrayUtils可以在下面給出。

/// <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; 
} 

現在,作爲用於獲取圖像關閉剪貼板,我注意到有明顯的.Net 3.5和後來者,這似乎實際使用DIB之間的行爲差​​異。鑑於這種差異以及DIB格式的不可靠性,您需要手動檢查所有類型,最好從完全可靠的PNG格式開始。

您可以從剪貼板中使用此代碼得到DataObject

DataObject retrievedData = Clipboard.GetDataObject() as DataObject; 

這裏使用的CloneImage功能基本上只是我GetImageDataBuildImage工具集的組合,確保一個新的圖像,沒有任何後盾創建可能會搞砸的資源;已知圖像對象基於Stream然後被丟棄時會導致崩潰。它的一個壓縮和優化版本被張貼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")) 
    { 
     MemoryStream png_stream = retrievedData.GetData("PNG") as MemoryStream; 
     if (png_stream != null) 
      using (Bitmap bm = new Bitmap(png_stream)) 
       clipboardimage = ImageUtils.CloneImage(bm); 
    } 
    if (clipboardimage == null && retrievedData.GetDataPresent(DataFormats.Dib)) 
    { 
     MemoryStream dib = retrievedData.GetData(DataFormats.Dib) 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) 
      { 
       // Stride is always a multiple of 4; no need to take it into account for 32bpp. 
       for (Int32 pix = 3; pix < image.Length; pix += 4) 
       { 
        // 0 can mean transparent, but can also mean the alpha isn't filled in, so only check for non-zero alpha, 
        // which would indicate there is actual data in the alpha bytes. 
        if (image[pix] == 0) 
         continue; 
        fmt = PixelFormat.Format32bppPArgb; 
        break; 
       } 
      } 
      else 
       // Could be supported with a system that parses the colour masks, 
       // but I don't think the clipboard ever uses these anyway. 
       return null; 
     } 
     Bitmap bitmap = ImageUtils.BuildImage(image, width, height, stride, fmt, null, null); 
     // This is bmp; reverse image lines. 
     bitmap.RotateFlip(RotateFlipType.Rotate180FlipX); 
     return bitmap; 
    } 
    catch 
    { 
     return null; 
    } 
} 

因爲BitConverter,都要求對系統字節序啞檢查,我有我自己在ArrayUtilsReadIntFromByteArrayWriteIntToByteArray

public 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); 
    } 
} 

public 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; 
} 
+0

感謝您的詳細答案!我會盡快嘗試,並會報告回來。 – CryShana

+0

我剛發現實際上可以使用alpha版本的版本5 DIB也可以被支持。它具有剪貼板ID「格式17」,因爲[在舊的數字Windows剪貼板類型中它具有'17'作爲值](https://msdn.microsoft.com/en-us/library/windows/desktop/ff729168(v = vs.85)的.aspx)。 – Nyerguds

+0

嗨。這是正確的CloneImage函數:靜態位圖CloneImage(Bitmap bm){int stride;返回BuildImage(GetImageData(bm,out stride),bm.Width,bm.Height,stride,bm.PixelFormat,null,null); } –