From 72e3f6826c91e3832590bd0ed6bf3f3bab40e3b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 31 Oct 2024 23:37:58 +1100 Subject: [PATCH] Allow handle to be supplied for ImageGrab.grab() on Windows --- Tests/test_imagegrab.py | 5 +++++ docs/reference/ImageGrab.rst | 5 +++++ src/PIL/ImageGrab.py | 11 ++++++++++- src/display.c | 25 ++++++++++++++++++++----- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 5cd510751e2..61d4c8ed6cf 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -57,6 +57,11 @@ def test_grab_invalid_xdisplay(self) -> None: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") + @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") + def test_grab_invalid_handle(self) -> None: + with pytest.raises(OSError): + ImageGrab.grab(handle=-1) + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index db2987eb081..7e0dab0a95e 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -39,6 +39,11 @@ or the clipboard to a PIL image memory. You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. .. versionadded:: 7.1.0 + + :param handle: + HDC, to capture a single window. Windows only. + + .. versionadded:: 11.1.0 :return: An image .. py:function:: grabclipboard() diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index e27ca7e5033..dbe2190ffb9 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -22,15 +22,20 @@ import subprocess import sys import tempfile +from typing import TYPE_CHECKING from . import Image +if TYPE_CHECKING: + from . import ImageWin + def grab( bbox: tuple[int, int, int, int] | None = None, include_layered_windows: bool = False, all_screens: bool = False, xdisplay: str | None = None, + handle: int | ImageWin.HDC | None = None, ) -> Image.Image: im: Image.Image if xdisplay is None: @@ -51,8 +56,12 @@ def grab( return im_resized return im elif sys.platform == "win32": + if handle is not None: + all_screens = -1 offset, size, data = Image.core.grabscreen_win32( - include_layered_windows, all_screens + include_layered_windows, + all_screens, + int(handle) if handle is not None else 0, ) im = Image.frombytes( "RGB", diff --git a/src/display.c b/src/display.c index b4e2e38991a..18c5b25ae51 100644 --- a/src/display.c +++ b/src/display.c @@ -320,8 +320,8 @@ typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE); PyObject * PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { - int x = 0, y = 0, width, height; - int includeLayeredWindows = 0, all_screens = 0; + int x = 0, y = 0, width = -1, height; + int includeLayeredWindows = 0, screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; @@ -331,14 +331,18 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { HMODULE user32; Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) { + if (!PyArg_ParseTuple( + args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &screen + )) { return NULL; } /* step 1: create a memory DC large enough to hold the entire screen */ - screen = CreateDC("DISPLAY", NULL, NULL, NULL); + if (screens != -1) { + screen = CreateDC("DISPLAY", NULL, NULL, NULL); + } screen_copy = CreateCompatibleDC(screen); // added in Windows 10 (1607) @@ -351,11 +355,18 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3); } - if (all_screens) { + if (screens == 1) { x = GetSystemMetrics(SM_XVIRTUALSCREEN); y = GetSystemMetrics(SM_YVIRTUALSCREEN); width = GetSystemMetrics(SM_CXVIRTUALSCREEN); height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else if (screens == -1) { + RECT rect; + HWND hwnd = WindowFromDC(screen); + if (hwnd != NULL && GetClientRect(hwnd, &rect)) { + width = rect.right; + height = rect.bottom; + } } else { width = GetDeviceCaps(screen, HORZRES); height = GetDeviceCaps(screen, VERTRES); @@ -367,6 +378,10 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) { FreeLibrary(user32); + if (width == -1) { + goto error; + } + bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) { goto error;