From 5728662c7f93997f9d0e4d588e44437d45d880da Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 9 May 2020 09:29:40 +0200 Subject: [PATCH 1/3] add support for CF_DIBV5, CF_HDROP, and 'PNG' in ImageGrab.grabclipboard() on win32 --- Tests/test_imagegrab.py | 25 ++++++++++++++++- src/PIL/ImageGrab.py | 24 ++++++++++++---- src/display.c | 62 +++++++++++++++++++++++++++++++---------- 3 files changed, 90 insertions(+), 21 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 82e746fda..9b5084acf 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -4,7 +4,7 @@ import sys import pytest from PIL import Image, ImageGrab -from .helper import assert_image +from .helper import assert_image, assert_image_equal_tofile class TestImageGrab: @@ -71,3 +71,26 @@ $bmp = New-Object Drawing.Bitmap 200, 200 im = ImageGrab.grabclipboard() assert_image(im, im.mode, im.size) + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_file(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') + p.communicate() + + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.gif") + + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grabclipboard_png(self): + p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + p.stdin.write( + rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") +$ms = new-object System.IO.MemoryStream(, $bytes) +[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") +[Windows.Forms.Clipboard]::SetData("PNG", $ms)""" + ) + p.communicate() + + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, "Tests/images/hopper.png") diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 39d5e23c7..9fd8b1610 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -93,12 +93,24 @@ def grabclipboard(): os.unlink(filepath) return im elif sys.platform == "win32": - data = Image.core.grabclipboard_win32() - if isinstance(data, bytes): - from . import BmpImagePlugin - import io + import io - return BmpImagePlugin.DibImageFile(io.BytesIO(data)) - return data + fmt, data = Image.core.grabclipboard_win32() + if isinstance(data, str): + if fmt == "file": + with open(data, "rb") as f: + im = Image.open(io.BytesIO(f.read())) + return im + if isinstance(data, bytes): + data = io.BytesIO(data) + if fmt == "png": + from . import PngImagePlugin + + return PngImagePlugin.PngImageFile(data) + elif fmt == "DIB": + from . import BmpImagePlugin + + return BmpImagePlugin.DibImageFile(data) + return None else: raise NotImplementedError("ImageGrab.grabclipboard() is macOS and Windows only") diff --git a/src/display.c b/src/display.c index 9a337d1c0..0d52ca2cf 100644 --- a/src/display.c +++ b/src/display.c @@ -32,6 +32,7 @@ #ifdef _WIN32 +#include #include "ImDib.h" #if SIZEOF_VOID_P == 8 @@ -473,33 +474,66 @@ PyObject* PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) { int clip; - HANDLE handle; + HANDLE handle = NULL; int size; void* data; PyObject* result; + UINT format; + UINT formats[] = { CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0 }; + LPCSTR format_names[] = { "DIB", "DIB", "file", "png", NULL }; - clip = OpenClipboard(NULL); - /* FIXME: check error status */ - - handle = GetClipboardData(CF_DIB); - if (!handle) { - /* FIXME: add CF_HDROP support to allow cut-and-paste from - the explorer */ - CloseClipboard(); - Py_INCREF(Py_None); - return Py_None; + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } + + // find best format as set by clipboard owner + format = 0; + while (!handle && (format = EnumClipboardFormats(format))) { + for (UINT i = 0; formats[i] != 0; i++) { + if (format == formats[i]) { + handle = GetClipboardData(format); + format = i; + break; + } + } + } + + if (!handle) { + CloseClipboard(); + return Py_BuildValue("zO", NULL, Py_None); + } + + if (formats[format] == CF_HDROP) { + LPDROPFILES files = (LPDROPFILES)GlobalLock(handle); + size = GlobalSize(handle); + + if (files->fWide) { + LPCWSTR filename = (LPCWSTR)(((char*)files) + files->pFiles); + size = wcsnlen(filename, (size - files->pFiles) / 2); + result = Py_BuildValue("zu#", "file", filename, size); + } + else { + LPCSTR filename = (LPCSTR)(((char*)files) + files->pFiles); + size = strnlen(filename, size - files->pFiles); + result = Py_BuildValue("zs#", "file", filename, size); + } + + GlobalUnlock(handle); + CloseClipboard(); + + return result; } - size = GlobalSize(handle); data = GlobalLock(handle); + size = GlobalSize(handle); result = PyBytes_FromStringAndSize(data, size); GlobalUnlock(handle); - CloseClipboard(); - return result; + return Py_BuildValue("zN", format_names[format], result); } /* -------------------------------------------------------------------- */ From 1656edaf4176eea36b3acac0f8410b2a204493a3 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 9 May 2020 10:40:10 +0200 Subject: [PATCH 2/3] fix docs compliance for CF_HDROP --- Tests/test_imagegrab.py | 4 +++- src/PIL/ImageGrab.py | 19 ++++++++++++------- src/display.c | 22 ---------------------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 9b5084acf..23eee2445 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,3 +1,4 @@ +import os import subprocess import sys @@ -79,7 +80,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200 p.communicate() im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, "Tests/images/hopper.gif") + assert len(im) == 1 + assert os.path.samefile(im[0], "Tests/images/hopper.gif") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_png(self): diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 9fd8b1610..cb685b1ed 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -93,15 +93,20 @@ def grabclipboard(): os.unlink(filepath) return im elif sys.platform == "win32": - import io - fmt, data = Image.core.grabclipboard_win32() - if isinstance(data, str): - if fmt == "file": - with open(data, "rb") as f: - im = Image.open(io.BytesIO(f.read())) - return im + if fmt == "file": # CF_HDROP + import struct + + o = struct.unpack_from("I", data)[0] + if data[16] != 0: + files = data[o:].decode("utf-16le").split("\0") + return files[: files.index("")] + else: + files = data[o:].decode("mbcs").split("\0") + return files[: files.index("")] if isinstance(data, bytes): + import io + data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin diff --git a/src/display.c b/src/display.c index 0d52ca2cf..c8d3086e3 100644 --- a/src/display.c +++ b/src/display.c @@ -32,7 +32,6 @@ #ifdef _WIN32 -#include #include "ImDib.h" #if SIZEOF_VOID_P == 8 @@ -504,27 +503,6 @@ PyImaging_GrabClipboardWin32(PyObject* self, PyObject* args) return Py_BuildValue("zO", NULL, Py_None); } - if (formats[format] == CF_HDROP) { - LPDROPFILES files = (LPDROPFILES)GlobalLock(handle); - size = GlobalSize(handle); - - if (files->fWide) { - LPCWSTR filename = (LPCWSTR)(((char*)files) + files->pFiles); - size = wcsnlen(filename, (size - files->pFiles) / 2); - result = Py_BuildValue("zu#", "file", filename, size); - } - else { - LPCSTR filename = (LPCSTR)(((char*)files) + files->pFiles); - size = strnlen(filename, size - files->pFiles); - result = Py_BuildValue("zs#", "file", filename, size); - } - - GlobalUnlock(handle); - CloseClipboard(); - - return result; - } - data = GlobalLock(handle); size = GlobalSize(handle); From d2e23e386b7be9775aba20471067caac0f217d46 Mon Sep 17 00:00:00 2001 From: nulano Date: Thu, 28 May 2020 12:07:53 +0100 Subject: [PATCH 3/3] simplify code Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index cb685b1ed..d16dd08f6 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -100,10 +100,9 @@ def grabclipboard(): o = struct.unpack_from("I", data)[0] if data[16] != 0: files = data[o:].decode("utf-16le").split("\0") - return files[: files.index("")] else: files = data[o:].decode("mbcs").split("\0") - return files[: files.index("")] + return files[: files.index("")] if isinstance(data, bytes): import io