ScreenshotNeo

BlogScreenshots on your device

How to Fix Python ImageGrab Screenshots That Miss Input

Fix ImageGrab screenshots that miss clicks, keystrokes, cursors, or the wrong monitor with a platform-aware diagnostic guide and working Python code.

By the ScreenshotNeo team30 September 20267 min read

How to Fix Python ImageGrab Screenshots That Miss Input

ImageGrab does not record keyboard or mouse events. It takes a snapshot of pixels on the screen. “Miss input” can therefore mean three different problems:

  • The click or keystroke never changed the visible application state.
  • The state changed, but the saved screenshot does not show it yet.
  • The screenshot captured the wrong rectangle or monitor because input coordinates and capture coordinates use different coordinate systems.

Diagnose those cases separately. First verify that the application visibly changed, then capture a full screen, then narrow the capture to a bounding box. On Windows with multiple monitors, test all_screens=True and keep cursor coordinates, bounding-box coordinates, and the captured desktop in the same coordinate space. The official ImageGrab documentation describes screen and clipboard capture; it does not promise input-event recording or mouse-pointer inclusion.

1. Confirm what “missed input” means

  1. Perform the click or keystroke with your automation library.
  2. Wait until the application visibly reflects it.
  3. Look at the screen manually or save a full-screen image.
  4. Only then investigate bbox, monitor offsets, scaling, or platform capture support.

If the application did not change before capture, investigate focus, permissions, event delivery, overlays, and application timing. ImageGrab cannot repair an input event that was never delivered.

2. Minimal full-screen capture

Install or upgrade Pillow, then capture the entire default screen. Omitting bbox avoids most crop-coordinate mistakes.

python -m pip install --upgrade Pillow
from pathlib import Path
from PIL import ImageGrab

output = Path("screen.png")
image = ImageGrab.grab()
image.save(output)
print(f"saved {output} ({image.width}x{image.height})")

Open screen.png. If the visible result of the input is present here, the capture pipeline works and your crop or coordinate conversion is the likely cause.

3. Capture a precise region safely

bbox is a pixel rectangle in the capture coordinate system: (left, top, right, bottom). Normalize points before passing them to Pillow so dragging in any direction produces a valid rectangle.

from pathlib import Path
from PIL import ImageGrab


def normalized_bbox(first, second):
    x1, y1 = first
    x2, y2 = second
    left, right = sorted((x1, x2))
    top, bottom = sorted((y1, y2))
    if left == right or top == bottom:
        raise ValueError("capture rectangle must have non-zero width and height")
    return (left, top, right, bottom)

# Replace these with coordinates from the same desktop coordinate system.
start = (100, 100)
end = (900, 700)
bbox = normalized_bbox(start, end)

image = ImageGrab.grab(bbox=bbox)
image.save(Path("region.png"))
print(f"saved region.png: bbox={bbox}, size={image.size}")

Do not mix logical coordinates from a GUI toolkit with physical pixel coordinates from another API without converting them. Display scaling and Retina rendering can make those values differ.

4. Windows multi-monitor coordinates

On Windows, all_screens=True captures the virtual desktop. Pillow documents that the virtual desktop’s top-left can be negative when a monitor sits left of or above the primary display. The current implementation obtains a desktop offset and crops the bounding box relative to that offset; your coordinates still need to describe the same virtual desktop.

Multi-monitor virtual desktops can use negative coordinates, so normalize the bbox in one coordinate system.
Multi-monitor virtual desktops can use negative coordinates, so normalize the bbox in one coordinate system.
from PIL import ImageGrab

# Windows only: include every monitor in the virtual desktop.
image = ImageGrab.grab(all_screens=True)
image.save("all-monitors.png")
print(image.size)

When coordinates come from GetCursorPos() or mouse clicks:

  • Use the same screen-coordinate origin for both points and the capture.
  • Expect negative x or y values for monitors positioned left or above the primary display.
  • Normalize the two corners before calling grab(bbox=...).
  • Log the cursor position, bbox, monitor layout, and resulting image size.

Pillow issue #7898 reports an inaccurate bbox on a Windows 11 system with an external monitor. It is useful evidence that display configuration and coordinate conversion matter, not proof of a universal Pillow defect.

5. Layered windows, pointer, and redraw timing

Layered Windows

On Windows, include_layered_windows=True is narrowly intended to include layered windows.

Verify the visible state after input and before saving the screenshot.
Verify the visible state after input and before saving the screenshot.
from PIL import ImageGrab

image = ImageGrab.grab(include_layered_windows=True)
image.save("layered.png")

This option does not capture keyboard events, force a redraw, fix delayed application updates, or guarantee that every missing visual element appears.

Mouse pointer

The ImageGrab documentation does not promise pointer inclusion. Do not treat all_screens, include_layered_windows, or scale_down as cursor-capture flags. If the pointer itself is required, collect the OS, Pillow version, desktop session, and a minimal reproduction before choosing a platform-specific capture method.

Wait for the application to repaint

A click can be delivered before a menu, animation, or network result is painted. Wait for a visible condition when your automation framework supports it; otherwise use a short delay and verify the result with a diagnostic full-screen capture.

import time
from PIL import ImageGrab

# Replace with a state-aware wait when possible.
time.sleep(0.25)
ImageGrab.grab().save("after-input.png")

6. macOS Retina and Pillow versions

Pillow says Retina captures are 2x by default. In Pillow 12.3.0, keyword-only scale_down=True prevents the 2x result for bbox captures.

from PIL import ImageGrab

bbox = (100, 100, 900, 700)
image = ImageGrab.grab(bbox=bbox, scale_down=True)
image.save("retina-region.png")

Check your installed version before using newer parameters:

import PIL
print(PIL.__version__)

Window capture support was added on macOS in Pillow 12.1.0 and on Windows in 11.2.1. Older installations will reject those arguments or behave differently.

7. Linux sessions and capture backends

Pillow supports Linux from version 7.1.0. With xdisplay=None, if the default X11 screen cannot return a snapshot, Pillow may fall back to an installed gnome-screenshot, grim, or spectacle. Passing xdisplay="" disables that fallback.

from PIL import ImageGrab, features

print("XCB support:", features.check_feature("xcb"))
image = ImageGrab.grab()
image.save("linux-screen.png")

Verify the display session, permissions, and installed screenshot utility. Wayland desktops and applications can differ from X11 behavior; the documented fallback is not a guarantee that every Wayland surface will be captured identically.

8. A diagnostic script you can run

import argparse
import platform
from pathlib import Path

from PIL import ImageGrab, features


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--output", default="diagnostic.png")
    parser.add_argument("--all-screens", action="store_true")
    parser.add_argument("--layered", action="store_true")
    args = parser.parse_args()

    print("OS:", platform.platform())
    print("Pillow XCB support:", features.check_feature("xcb"))

    kwargs = {}
    if args.all_screens:
        kwargs["all_screens"] = True
    if args.layered:
        kwargs["include_layered_windows"] = True

    image = ImageGrab.grab(**kwargs)
    output = Path(args.output)
    image.save(output)
    print("saved:", output.resolve())
    print("size:", image.size)


if __name__ == "__main__":
    main()
python diagnostic.py --output full.png
# Windows virtual desktop
python diagnostic.py --all-screens --output monitors.png
# Windows layered windows
python diagnostic.py --layered --output layered.png

9. Troubleshooting checklist

Symptom Likely cause Fix
Click result is absent everywhere Focus, permissions, event delivery, or application timing Verify the visible state before capture; fix automation and wait for repaint
Full screen is correct, crop is wrong Bbox coordinates use another origin, scale, or monitor Capture all screens, normalize corners, and keep coordinate systems consistent
External monitor causes an offset Virtual desktop has negative coordinates or mixed DPI scaling Use all_screens=True; log offsets and convert coordinates
Layered overlay is missing on Windows Layered window not included Try include_layered_windows=True
macOS crop is twice the expected size Retina backing pixels Use Pillow 12.3.0+ with scale_down=True for bbox captures
Linux capture is blank or errors Display/session/backend or missing utility Check XCB, display variables, permissions, and fallback utilities
Pointer is absent ImageGrab does not document pointer inclusion Use a platform-specific pointer-capable method after identifying the OS/session
Argument is rejected Pillow version is too old Check PIL.__version__ and upgrade where appropriate

10. Performance, reliability, and cost

  • Performance: Full-screen and all-monitor images consume memory proportional to pixel count. Crop after you have validated coordinates, and save an appropriate image format for your workflow.
  • Reliability: Record OS, Pillow version, monitor arrangement, scaling, session type, bbox, and image dimensions with failures. This makes display-specific bugs reproducible.
  • Timing: Prefer waiting for a UI condition over a fixed sleep. A fixed delay is only a fallback for animations or applications without observable state.
  • Permissions: macOS screen-recording permissions, Linux display access, Windows desktop isolation, and application security policies can prevent pixels from being available.
  • Cost: Pillow is a local library; the capture itself has no API request cost. Your operational costs are CPU, memory, storage, and any hosted desktop or CI runner.

11. Or skip the browser setup

If you need a screenshot of a public web page rather than the local desktop, ScreenshotNeo returns a clean PNG, JPEG, WebP, or PDF from one GET request. Its consent step accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. See the ScreenshotNeo API documentation for all options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also provides an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

12. FAQ

Does ImageGrab capture keyboard or mouse events?

No. It captures pixels. Use an input-automation library for events and verify the resulting visual state separately.

Why does a bbox work on one monitor but not another?

The monitors may have different origins, negative virtual-desktop coordinates, or display scaling. Capture all screens and keep every coordinate in one system.

Should I always enable all_screens=True?

No. Use it on Windows when you need the virtual desktop or when diagnosing monitor offsets. A default full-screen capture is simpler for a single display.

Can scale_down=True fix a missing cursor?

No. It addresses Retina bbox dimensions in Pillow 12.3.0 and later; it is not a pointer option.

How do I know whether Linux is using X11?

Inspect the session environment and check Pillow’s XCB feature support. Also verify the documented external screenshot utilities when fallback capture is involved.