c# - La copia da e negli Appunti perde la trasparenza dell'immagine



wpf bitmap (1)

Gli Appunti di Windows, per impostazione predefinita, non supportano la trasparenza, ma è possibile mettere insieme il contenuto negli Appunti in molti tipi per assicurarsi che la maggior parte delle applicazioni trovi un tipo in esso utilizzabile. Purtroppo, il tipo più comune (che Windows stesso sembra utilizzare) è davvero sporco e inaffidabile. Ho scritto una grande spiegazione a riguardo here .

Presumo che tu l'abbia letto prima di continuare con la mia risposta qui, perché contiene le informazioni di base richieste per la parte successiva.

Ora, il modo più pulito di inserire un'immagine negli Appunti con supporto per la trasparenza è un flusso PNG, ma non garantirà che tutte le applicazioni possano incollarlo. Gimp supporta l'incollaggio PNG, e apparentemente lo stesso fanno i programmi MS Office più recenti, ma Google Chrome, ad esempio, non lo fa e accetterà solo il tipo DIB disordinato dettagliato nella risposta a cui mi sono collegato. D'altra parte, Gimp non accetterà DIB come trasparente, perché i suoi creatori hanno effettivamente seguito le specifiche del formato e si sono resi conto che il formato era inaffidabile (come chiaramente dimostrato da quella domanda che ho collegato).

A causa del pasticcio DIB, purtroppo, la cosa migliore da fare è semplicemente inserirla nel maggior numero possibile di tipi generalmente supportati, tra cui PNG, DIB e l'immagine normale.

PNG e DIB sono entrambi inseriti negli Appunti allo stesso modo: inserendoli nel DataObject come MemoryStream e quindi dando agli appunti l'istruzione "copia" quando lo si mette effettivamente.

Gran parte di questo è semplice, ma quello DIB è un po 'più complesso. Nota che la parte seguente contiene un paio di riferimenti ai miei set di strumenti. Il GetImageData è disponibile in questa risposta , il BuildImage è disponibile here e quelli ArrayUtils sono riportati di seguito.

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

Ora, per quanto riguarda ottenere un'immagine dagli appunti, ho notato che a quanto pare c'è una differenza nel comportamento tra .Net 3.5 e quelli successivi, che sembrano effettivamente utilizzare quel DIB. Data questa differenza e data la scarsa affidabilità del formato DIB, ti consigliamo di controllare manualmente tutti i tipi, preferibilmente a partire dal formato PNG completamente affidabile.

È possibile ottenere il DataObject dagli Appunti con questo codice:

DataObject retrievedData = Clipboard.GetDataObject() as DataObject;

La funzione CloneImage utilizzata qui è fondamentalmente solo la combinazione dei miei set di strumenti GetImageData e BuildImage , garantendo che venga creata una nuova immagine senza risorse di backup che potrebbero incasinare; gli oggetti immagine sono noti per causare arresti anomali quando sono basati su uno Stream che viene quindi eliminato. Una versione compatta e ottimizzata è stata pubblicata qui, in una domanda che vale la pena leggere sull'argomento del perché questa clonazione è così importante.

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

Poiché BitConverter richiede sempre quel controllo stupido sull'endianness del sistema, ho ottenuto il mio ReadIntFromByteArray e WriteIntToByteArray in una classe ArrayUtils :

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

Ho cercato di copiare un'immagine PNG trasparente negli Appunti e preservarne la trasparenza per incollarla in un programma specifico che la supporta.

Ho già provato molte soluzioni ma lo sfondo è sempre diventato grigio in un modo o nell'altro.

Quindi ho provato a copiare la stessa immagine usando Chrome e incollandola nel programma e ha funzionato. Ha conservato la trasparenza. Quindi ho provato a ottenere l'immagine dagli Appunti che avevo copiato usando Chrome e impostare di nuovo l'immagine, aspettandomi che la trasparenza fosse ancora lì - ma no, la trasparenza non è stata preservata anche se ho appena preso l'immagine dagli appunti e l'ho impostata ancora.

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

Stesso problema anche se uso System.Windows.Forms.Clipboard o provo a ottenere e impostare DataObject anziché l'immagine.





clipboard