BlogScreenshots on your device
Why PyWin32 Screenshot Dimensions Don’t Match Resized Windows
Learn why PyWin32 window rectangles and screenshot pixels differ, how DPI awareness changes coordinates, and how to diagnose each capture path.
Short answer: a PyWin32 window rectangle and a screenshot bitmap may be measured in different coordinate spaces. win32gui.GetWindowRect() returns window bounds through the caller’s Windows DPI-awareness context, while Pillow or another capture backend may return physical desktop pixels or crop using its own bounding-box rules. A resized window can therefore have one width in API coordinates and another width in the image.
DPI virtualization is a leading cause, but it is not proof of every mismatch. The capture route, process awareness, target-window awareness, monitor scale, and whether you captured the desktop or a crop must all be recorded before choosing a conversion.
1. What the numbers actually mean
| Measurement | What it represents | Common mistake |
|---|---|---|
GetWindowRect |
(left, top, right, bottom) bounds returned in the API caller’s coordinate context. |
Treating right and bottom as width and height. |
| Screenshot size | Bitmap dimensions in pixels produced by the capture backend. | Assuming those pixels use the same scaling as the rectangle. |
Pillow bbox |
A crop rectangle interpreted by ImageGrab.grab. |
Assuming a full-screen grab and a bbox crop have identical coordinate behavior. |
| Window client area | Content area inside borders, title bar, and frame. | Comparing client dimensions with an outer-window screenshot. |
For a rectangle, calculate dimensions explicitly:
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
width = right - left
height = bottom - top
Microsoft documents GetDpiForWindow as returning 96 DPI for a DPI-unaware window, system DPI for a system-aware window, or monitor DPI for a per-monitor-aware window. See the GetDpiForWindow API reference.
2. A reproducible diagnostic script
The following script logs the rectangle, DPI, Pillow image dimensions, and capture mode. Install dependencies with pip install pywin32 pillow.
import ctypes
import sys
import win32gui
from PIL import ImageGrab
user32 = ctypes.windll.user32
# Get the foreground window for a minimal example.
hwnd = user32.GetForegroundWindow()
if not hwnd:
raise RuntimeError("No foreground window")
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
rect_width = right - left
rect_height = bottom - top
# Available on modern Windows. It reports the target HWND's effective DPI.
try:
dpi = user32.GetDpiForWindow(hwnd)
except AttributeError:
dpi = None
# Capture the entire desktop, then inspect physical bitmap pixels.
full = ImageGrab.grab()
# Capture the rectangle as a separate experiment.
cropped = ImageGrab.grab(bbox=(left, top, right, bottom))
print({
"hwnd": hwnd,
"rect": (left, top, right, bottom),
"rect_size": (rect_width, rect_height),
"window_dpi": dpi,
"full_image_size": full.size,
"cropped_image_size": cropped.size,
"python": sys.version,
})
full.save("desktop.png")
cropped.save("window-crop.png")
Run it with the target window on the monitor whose scaling you want to inspect. Save the output together with your Windows version, display scale, Python process awareness, and Pillow version.
3. Check DPI awareness before applying any multiplier
3.1 Understand the three relevant awareness modes
- DPI-unaware: Windows can virtualize coordinates and report 96 DPI to the application.
- System-aware: coordinates are based on the system DPI selected when the session starts.
- Per-monitor-aware: the application receives the effective DPI for the monitor containing the window and can respond as the window moves.
Microsoft’s DPI awareness context guidance explains how these contexts affect scaling. The default DPI-awareness guidance also states that a programmatically selected process default must be configured before creating HWNDs; changing it after HWND creation is unsupported.
3.2 Set awareness early when your application needs it
For a new desktop process, declare awareness before creating windows. A manifest is generally preferable for a packaged application. For a small Python diagnostic, this call must happen at process startup, before any window handles are created:
import ctypes
# Windows 10 1607+: per-monitor-v2 awareness.
try:
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
except AttributeError:
# Older systems may require a manifest or SetProcessDPIAware.
ctypes.windll.user32.SetProcessDPIAware()
# Only create or discover HWNDs after the awareness choice.
Do not add this after your GUI has already created HWNDs and expect existing coordinates to be repaired. If you cannot change process awareness, keep the existing context and convert coordinates deliberately.
3.3 Convert only after measuring the source and destination spaces
A common diagnostic relationship is:
physical_pixels = logical_units * dpi / 96
This is useful only when the rectangle is known to be logical units and the bitmap is known to be physical pixels at the same monitor DPI. A capture backend may already apply scaling, use a virtual desktop origin, or return a bitmap from a different monitor. Measure first; do not assume a universal multiplier such as 1.25 or 1.5.
4. Capture routes and their edge cases
4.1 Full desktop capture
A full-screen grab returns the backend’s desktop bitmap. Compare its size with the virtual desktop and monitor arrangement, not directly with one window rectangle. On multi-monitor systems, the virtual desktop can have negative coordinates and a size that differs from the primary monitor.
4.2 Bounding-box crop
With Pillow, ImageGrab.grab(bbox=(left, top, right, bottom)) crops using the supplied coordinates. If those coordinates came from a DPI-virtualized API, the crop can be offset or have unexpected dimensions. Log both the bbox and resulting image size.
4.3 Window-specific capture
Other backends may capture client content, the outer frame, or a compositor surface. A window screenshot can therefore exclude the title bar or include shadows even when the HWND rectangle appears correct. Document the backend and whether it captures the client area.
4.4 Resizing, borders, and invisible frame margins
GetWindowRect describes the outer rectangle. Windows may include non-client borders and invisible resize margins. A screenshot that crops visible pixels can be smaller than the outer rectangle even with matching DPI. If you need client coordinates, use the client-area APIs and convert between screen and client coordinates explicitly.
4.5 Mixed-DPI monitors
Moving a window between monitors can change its effective DPI. Record the monitor containing the target at capture time. A per-monitor-aware process can receive a different DPI after the move; a system-aware or unaware process may continue using virtualized values.
5. A step-by-step troubleshooting checklist
- Print
left, top, right, bottomand calculate width and height by subtraction. - Print the actual image
width, heightafter capture; do not infer them from the requested bbox. - Record whether the image came from a full desktop grab, bbox crop, client capture, or another backend.
- Call
GetDpiForWindow(hwnd)and record the result. - Record process awareness, target-window awareness when available, Windows display scaling, monitor identity, and Windows version.
- Compare coordinates in one space. Only then test a DPI conversion.
- Repeat on one monitor at 100% scaling to separate DPI effects from backend behavior.
- If the mismatch remains, create a minimal reproduction containing the exact rectangle, backend, arguments, and resulting bitmap dimensions.
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Image is consistently 1.25× or 1.5× larger | Logical coordinates compared with physical pixels under display scaling. | Verify DPI and awareness, then convert using dpi / 96 only if both spaces are confirmed. |
| Crop is shifted on the second monitor | Virtual desktop origin, negative coordinates, or mixed-DPI virtualization. | Log monitor geometry and use a capture API that documents virtual-screen coordinates. |
| Width is correct but height differs | Outer frame versus client area, title bar, shadow, or an exclusive crop. | Identify whether the backend captures the frame or client area and compare like with like. |
Changing SetProcessDPIAware fixes one run and breaks another |
Awareness was changed after HWND creation or the backend has its own scaling. | Set awareness at process startup, before HWND creation, and retest from a clean process. |
| Rectangle values look correct but image size is unexpected | Backend-specific scaling, Retina-style capture behavior, or bbox semantics. | Inspect backend documentation and log the returned bitmap dimensions. |
GetDpiForWindow is unavailable |
Older Windows API surface or incorrect ctypes declaration. | Use a supported Windows version/API wrapper, or record the process and monitor DPI through another documented method. |
7. Reliability and performance considerations
- Cache the HWND and revalidate it; windows can be destroyed and recreated during navigation or application restarts.
- Capture after the resize has settled. A resize event followed immediately by a grab can observe an intermediate frame.
- For repeated captures, avoid unnecessary full-desktop screenshots when a backend can capture the target region directly.
- Keep diagnostic logs with the image: rectangle, DPI, awareness, monitor, backend, bbox, and bitmap size make regressions reproducible.
- On multi-monitor systems, test every monitor scale used in production.
8. Cost and architecture choices
Local PyWin32 and Pillow captures have no API request charge, but your process must handle Windows DPI contexts, window lifetime, monitor layouts, and backend differences. A hosted webpage screenshot API is a different architecture: it captures a URL rather than an HWND, so desktop-window DPI virtualization is not part of the capture path.
9. Or skip the browser setup
If the thing you need is a clean screenshot of a webpage URL rather than a local Windows window, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for parameters.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
10. FAQ
Is every mismatch caused by DPI scaling?
No. DPI virtualization is a leading explanation, but frame margins, client-area differences, virtual desktop origins, and backend-specific crop rules can produce the same symptom.
Should I always multiply the rectangle by 1.25?
No. Use a multiplier only after confirming the rectangle’s coordinate space, the bitmap’s pixel space, and the effective DPI.
What does GetDpiForWindow tell me?
It reports the effective DPI for the specified HWND according to that window’s awareness mode.
Why does a full-screen screenshot not equal my monitor resolution?
The backend may capture the virtual desktop, multiple monitors, or a scaled desktop surface. Log the returned image dimensions and monitor geometry.
Can ScreenshotNeo capture my local PyWin32 HWND?
No. ScreenshotNeo captures webpage URLs through its API. Use it when your input is a URL and use PyWin32 or another desktop backend for local windows.


