Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Tests/test_imagegrab.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ 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, match="unable to get device context for handle"):
ImageGrab.grab(window=-1)
with pytest.raises(OSError, match="screen grab failed"):
ImageGrab.grab(window=0)

def test_grabclipboard(self) -> None:
if sys.platform == "darwin":
subprocess.call(["screencapture", "-cx"])
Expand Down
7 changes: 6 additions & 1 deletion docs/reference/ImageGrab.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ or the clipboard to a PIL image memory.

.. versionadded:: 1.1.3

.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None)
.. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None)

Take a snapshot of the screen. The pixels inside the bounding box are returned as
an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted,
Expand Down Expand Up @@ -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 window:
HWND, to capture a single window. Windows only.

.. versionadded:: 11.2.0
:return: An image

.. py:function:: grabclipboard()
Expand Down
9 changes: 9 additions & 0 deletions docs/releasenotes/11.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")

Specify window in ImageGrab on Windows
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the
HWND::

from PIL import ImageGrab
ImageGrab.grab(window=hwnd)

Check for MozJPEG
^^^^^^^^^^^^^^^^^

Expand Down
11 changes: 10 additions & 1 deletion src/PIL/ImageGrab.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,17 @@

from . import Image

TYPE_CHECKING = False
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,
window: int | ImageWin.HWND | None = None,
) -> Image.Image:
im: Image.Image
if xdisplay is None:
Expand All @@ -51,8 +56,12 @@ def grab(
return im_resized
return im
elif sys.platform == "win32":
if window is not None:
all_screens = -1
offset, size, data = Image.core.grabscreen_win32(
include_layered_windows, all_screens
include_layered_windows,
all_screens,
int(window) if window is not None else 0,
)
im = Image.frombytes(
"RGB",
Expand Down
56 changes: 47 additions & 9 deletions src/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -286,29 +286,42 @@
/* -------------------------------------------------------------------- */
/* Windows screen grabber */

typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE);
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;
HWND wnd;
DWORD rop;
PyObject *buffer;
HANDLE dpiAwareness;
HANDLE dpiAwareness = NULL;
HMODULE user32;
Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function;
Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function;

if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) {
if (!PyArg_ParseTuple(
args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd
)) {
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 = GetDC(wnd);
if (screen == NULL) {
PyErr_SetString(PyExc_OSError, "unable to get device context for handle");
return NULL;
}
} else {
screen = CreateDC("DISPLAY", NULL, NULL, NULL);
}
screen_copy = CreateCompatibleDC(screen);

// added in Windows 10 (1607)
Expand All @@ -317,15 +330,28 @@
SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext
)GetProcAddress(user32, "SetThreadDpiAwarenessContext");
if (SetThreadDpiAwarenessContext_function != NULL) {
GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext
)GetProcAddress(user32, "GetWindowDpiAwarenessContext");
if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) {
dpiAwareness = GetWindowDpiAwarenessContext_function(wnd);
}
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3)
dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE)-3);
dpiAwareness = SetThreadDpiAwarenessContext_function(
dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness
);
}

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;
if (GetClientRect(wnd, &rect)) {
width = rect.right;
height = rect.bottom;

Check warning on line 353 in src/display.c

View check run for this annotation

Codecov / codecov/patch

src/display.c#L352-L353

Added lines #L352 - L353 were not covered by tests
}
} else {
width = GetDeviceCaps(screen, HORZRES);
height = GetDeviceCaps(screen, VERTRES);
Expand All @@ -337,6 +363,10 @@

FreeLibrary(user32);

if (width == -1) {
goto error;
}

bitmap = CreateCompatibleBitmap(screen, width, height);
if (!bitmap) {
goto error;
Expand Down Expand Up @@ -382,15 +412,23 @@

DeleteObject(bitmap);
DeleteDC(screen_copy);
DeleteDC(screen);
if (screens == -1) {
ReleaseDC(wnd, screen);

Check warning on line 416 in src/display.c

View check run for this annotation

Codecov / codecov/patch

src/display.c#L416

Added line #L416 was not covered by tests
} else {
DeleteDC(screen);
}

return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer);

error:
PyErr_SetString(PyExc_OSError, "screen grab failed");

DeleteDC(screen_copy);
DeleteDC(screen);
if (screens == -1) {
ReleaseDC(wnd, screen);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added this for the error: branch but not for the success branch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've updated the commit to add it or success as well.

} else {
DeleteDC(screen);

Check warning on line 430 in src/display.c

View check run for this annotation

Codecov / codecov/patch

src/display.c#L430

Added line #L430 was not covered by tests
}

return NULL;
}
Expand Down
Loading