pythonpython-3.xwindowsclipboardtransparency

Copy image to clipboard and preserve transparency


I'm trying to achieve the same thing that modern web browsers like Firefox and Chrome allow you to do. When you right click a transparent image on the web and then select "Copy Image", the image is then copied to your clipboard. So you can later paste it for example to Discord chat. And the transparency is preserved.

I want to do the same thing in Python 3. I want to be able to copy image stored on my computer (for example in .png format) to windows clipboard using python script and then paste it to Discord chat with the transparency preserved.

Attempt #1

I found this post featuring the code below. But as I see in the code, the image is converted to RGB only and the alpha channel is lost.

from io import BytesIO
import win32clipboard
from PIL import Image

def send_to_clipboard(clip_type, data):
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardData(clip_type, data)
    win32clipboard.CloseClipboard()

image = Image.open("test.png")

output = BytesIO()
image.convert("RGB").save(output, "BMP")
data = output.getvalue()[14:]
output.close()

send_to_clipboard(win32clipboard.CF_DIB, data)

Result I get by using the code from the post mentioned above:
alpha channel lost

Desired result:
desired result

I also tried saving the image as png like this: image.save(output, "PNG").
But that didn't work, discord crashed when I tried pasting the image to chat.

Attempt #2

Next I tried using CF_HDROP, hoping that discord desktop app would recognize it.

import ctypes
import pythoncom
import win32clipboard
from ctypes import wintypes

class DROPFILES(ctypes.Structure):
    _fields_ = [("pFiles", wintypes.DWORD),
                ("pt", wintypes.POINT),
                ("fNC", wintypes.BOOL),
                ("fWide", wintypes.BOOL)]

path = r"D:\Visual Studio Code Projects\clipboard-test\test.png"

offset = ctypes.sizeof(DROPFILES)
size = offset + (len(path) + 1) * ctypes.sizeof(ctypes.c_wchar) + 1
buffer = (ctypes.c_char * size)()
df = DROPFILES.from_buffer(buffer)
df.pFiles = offset
df.fWide = True

wchars = (ctypes.c_wchar * (len(path) + 1)).from_buffer(buffer, offset)
wchars.value = path

stg = pythoncom.STGMEDIUM()
stg.set(pythoncom.TYMED_HGLOBAL, buffer)

win32clipboard.OpenClipboard()

try:
    win32clipboard.EmptyClipboard()
    win32clipboard.SetClipboardData(win32clipboard.CF_HDROP, stg.data)
finally:
    win32clipboard.CloseClipboard()

Windows file explorer was able to recognize the data that I stored inside the clipboard, but discord wasn't. So no visible result there.

Attempt #3

At last I found about CF_HTML or html clipboard format.

import win32clipboard

class HTMLClipboard:
    def __init__(self):
        win32clipboard.OpenClipboard()
        self.format = win32clipboard.RegisterClipboardFormat("HTML Format")
        self.headers = "Version:0.9\r\n" +\
            "StartHTML:00000000\r\n" +\
            "EndHTML:00000000\r\n" +\
            "StartFragment:00000000\r\n" +\
            "EndFragment:00000000\r\n"
    def _insertHeaders(self, data):
        data = self.headers + data
        hStartHtml = data.find("StartHTML")
        startHtml = str(data.find("<html>"))
        data = data[:hStartHtml + 18 - len(startHtml)] + startHtml + data[hStartHtml + 19:]
        hEndHtml = data.find("EndHTML")
        endHtml = str(len(data) - 1)
        data = data[:hEndHtml + 16 - len(endHtml)] + endHtml + data[hEndHtml + 17:]
        hStartFragment = data.find("StartFragment")
        startFragment = str(data.find("<!--StartFragment-->") + 20)
        data = data[:hStartFragment + 22 - len(startFragment)] + startFragment + data[hStartFragment + 23:]
        hEndFragment = data.find("EndFragment")
        endFragment = str(data.find("<!--EndFragment-->") + 1)
        data = data[:hEndFragment + 20 - len(endFragment)] + endFragment + data[hEndFragment + 21:]
        return data
    def write(self, html):
        data = "<html>\r\n" +\
            "<body>\r\n" +\
            "<!--StartFragment-->" +\
            html + "" +\
            "<!--EndFragment-->\r\n" +\
            "</body>\r\n" +\
            "</html>"
        data = self._insertHeaders(data)
        win32clipboard.SetClipboardData(self.format, data.encode("ascii"))
    def read(self):
        pass
    def close(self):
        win32clipboard.CloseClipboard()
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        self.close()


with HTMLClipboard() as clip:
    clip.write("<img class='lnXdpd' alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png' srcset='/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png 1x, /images/branding/googlelogo/2x/googlelogo_color_272x92dp.png 2x' data-atf='1' width='272' height='92'>")

Discord again showed no visible result. But strange thing occurred, I copied image from the web using Microsoft Edge (the new one, based on Chromium) to clipboard and tried rewriting only the html format part using my code and it did still work.

So I am guessing that either I am still forgetting something, some clipboard format that I am not setting but the browsers do, or discord doesn't use the html clipboard format at all to import images from clipboard.

I even tried using all the clipboard formats from the attempts above together but with no visible result except the one with the transparency lost (black background).

I really don't know how the web browsers do the thing. Any help would be appreciated.


Solution

  • After I found out my original answer doesn't work for most images, I did some research and constructed a working solution. Unfortunately, it has a few drawbacks:

    The solution utilizes pywin32 and is as follows:

    import os
    import win32clipboard as clp
    
    file_path = 'test.png'
    
    clp.OpenClipboard()
    clp.EmptyClipboard()
    
    # This works for Discord, but not for Paint.NET:
    wide_path = os.path.abspath(file_path).encode('utf-16-le') + b'\0'
    clp.SetClipboardData(clp.RegisterClipboardFormat('FileNameW'), wide_path)
    
    # This works for Paint.NET, but not for Discord:
    file = open(file_path, 'rb')
    clp.SetClipboardData(clp.RegisterClipboardFormat('image/png'), file.read())
    
    clp.CloseClipboard()