3

I'm trying to convert a PNG image with transparent background to an opaque one having a white background, using Windows 10 Powershell.

The original source image is a graph on a website I'd like to copy and paste into an Excel sheet, but here for demonstration just use this one to reproduce: 526px-Wikipedia-logo-transparent.png

Downloading the image and reading in Powershell shows:

PS C:\Users\test\Desktop> New-Object System.Drawing.Bitmap("526px-Wikipedia-logo-transparent.png")

Tag                  :
PhysicalDimension    : {Width=526, Height=480}
Size                 : {Width=526, Height=480}
Width                : 526
Height               : 480
HorizontalResolution : 96
VerticalResolution   : 96
Flags                : 73746
RawFormat            : [ImageFormat: b96b3caf-0728-11d3-9d7b-0000f81ef32e]
PixelFormat          : Format32bppArgb
Palette              : System.Drawing.Imaging.ColorPalette
FrameDimensionsList  : {7462dc86-6180-4c7e-8e3f-ee7333a7a483}
PropertyIdList       : {769, 318, 319, 306...}
PropertyItems        : {769, 318, 319, 306...}

PixelFormat is clearly set to be alpha-channel capable.

But when I just copy the very same image in browser and read it from the clipboard in Powershell, alpha-channel capability is gone.

PS C:\Users\test\Desktop> Get-Clipboard -Format image

Tag                  :
PhysicalDimension    : {Width=526, Height=480}
Size                 : {Width=526, Height=480}
Width                : 526
Height               : 480
HorizontalResolution : 96
VerticalResolution   : 96
Flags                : 335888
RawFormat            : [ImageFormat: b96b3caa-0728-11d3-9d7b-0000f81ef32e]
PixelFormat          : Format32bppRgb
Palette              : System.Drawing.Imaging.ColorPalette
FrameDimensionsList  : {7462dc86-6180-4c7e-8e3f-ee7333a7a483}
PropertyIdList       : {}
PropertyItems        : {}

Even more, loading the image in Paint 3D and copying it to the clipboard, it doesn't make any difference if "Transparent canvas" is set to on or off - Powershell will read the clipboard everytime without transparency information. But when pasting the image into an Excel sheet, there is a difference if "Transparent canvas" was set to on or off. Thus, the clipboard must carry this information, but Powershell seems to be unable to respect it.

Do you have any idea how to enable Powershell to handle images with transparency information?

Important: Third party tools aren't allowed, a solution must use Windows standard tools.

  • This is a known issue: https://www.google.com/search?client=firefox-b-d&q=clipboard+no+transparency – Scepticalist Dec 18 '22 at 16:45
  • [This answer](https://stackoverflow.com/a/46424800/7571258) looks pretty comprehensive. Its `GetClipboardImage` function tries to read the image as PNG first. – zett42 Dec 18 '22 at 21:02

1 Answers1

0

I've adopted the C# solution from this answer for both Windows PowerShell 5.1 and PowerShell Core 7+.

I've included all dependencies of the original code (which were distributed over multiple answers), so the code runs as-is.

$refAsm = @(
    'System.Windows.Forms' 
    if( $PSVersionTable.PSVersion.Major -le 5 ) {'System.Drawing'} else { 'System.Drawing.Common', 'System.Drawing.Primitives' }
)
Add-Type -ReferencedAssemblies $refAsm -TypeDefinition @'
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;

public static class ClipboardImage {
    /// <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 = (Bitmap) bm.Clone();
        }
        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)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)ReadIntFromByteArray(header, 0x04, 4, true);
            Int32 height = (Int32)ReadIntFromByteArray(header, 0x08, 4, true);
            Int16 planes = (Int16)ReadIntFromByteArray(header, 0x0C, 2, true);
            Int16 bitCount = (Int16)ReadIntFromByteArray(header, 0x0E, 2, true);
            //Compression: 0 = RGB; 3 = BITFIELDS.
            Int32 compression = (Int32)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 = ReadIntFromByteArray(dibBytes, headerSize + 0, 4, true);
                UInt32 greenMask = ReadIntFromByteArray(dibBytes, headerSize + 4, 4, true);
                UInt32 blueMask = 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 = BuildImage(image, width, height, stride, fmt, null, null);
            // This is bmp; reverse image lines.
            bitmap.RotateFlip(RotateFlipType.Rotate180FlipX);
            return bitmap;
        }
        catch
        {
            return null;
        }
    }

    /// <summary>
    /// Creates a bitmap based on data, width, height, stride and pixel format.
    /// </summary>
    /// <param name="sourceData">Byte array of raw source data</param>
    /// <param name="width">Width of the image</param>
    /// <param name="height">Height of the image</param>
    /// <param name="stride">Scanline length inside the data</param>
    /// <param name="pixelFormat">Pixel format</param>
    /// <param name="palette">Color palette</param>
    /// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
    /// <returns>The new image</returns>
    public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
    {
        Bitmap newImage = new Bitmap(width, height, pixelFormat);
        BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
        Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
        // Compensate for possible negative stride on BMP format.
        Boolean isFlipped = stride < 0;
        stride = Math.Abs(stride);
        // Cache these to avoid unnecessary getter calls.
        Int32 targetStride = targetData.Stride;
        Int64 scan0 = targetData.Scan0.ToInt64();
        for (Int32 y = 0; y < height; y++)
            Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
        newImage.UnlockBits(targetData);
        // Fix negative stride on BMP format.
        if (isFlipped)
            newImage.RotateFlip(RotateFlipType.Rotate180FlipX);
        // For indexed images, set the palette.
        if ((pixelFormat & PixelFormat.Indexed) != 0 && palette != null)
        {
            ColorPalette pal = newImage.Palette;
            for (Int32 i = 0; i < pal.Entries.Length; i++)
            {
                if (i < palette.Length)
                    pal.Entries[i] = palette[i];
                else if (defaultColor.HasValue)
                    pal.Entries[i] = defaultColor.Value;
                else
                    break;
            }
            newImage.Palette = pal;
        }
        return newImage;
    }    

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

Usage example:

Add-Type -Assembly System.Windows.Forms

$data  = [System.Windows.Forms.Clipboard]::GetDataObject()
$image = [ClipboardImage]::GetClipboardImage( $data )
$image.Save("$PWD\clipboard.png")
zett42
  • 25,437
  • 3
  • 35
  • 72
  • 1
    Wow, what a heap of code to replace one-liner `Get-Clipboard -Format image`. But it works well, I can confirm. – Horst Meier Dec 20 '22 at 15:13