Skip to main content

window-grabber

Monitor processes

from window_capture import Match, ProcessMonitor, Target, recommended_capture_mode

with ProcessMonitor(
    [Target(process_name="example.exe"),
     Target(process_name=Match.glob("agent*.exe", ignore_case=True))],
    mode=recommended_capture_mode(),
) as monitor:
    events = monitor.poll()

recommended_capture_mode() selects an initial policy without starting workers:

Executing host Recommended mode
Windows 7/8/8.1 and Windows 10 before build 19041 controlled_desktop
Windows 10 build 19041 (2004) or later, including Windows 11 isolated
Linux isolated

Other platforms raise RuntimeError. This is an OS recommendation, not a capability probe or a guarantee that a particular window can be captured. The Windows boundary includes the cursor-exclusion API required by default capture options. Capture failures never change the selected mode. Omitting mode keeps the existing default policy; using this helper explicitly permits desktop changes on older Windows.

For RPyC, evaluate the helper and construct objects through the remote module:

wc = conn.modules["window_capture"]
monitor = wc.ProcessMonitor(
    wc.Target(process_name="example.exe"),
    mode=wc.recommended_capture_mode(),
)
try:
    events = monitor.poll()
finally:
    monitor.close()

Install the package on the remote machine and run the capture process in the target user's interactive Windows session. The helper evaluates that remote host; complete monitor-over-RPyC integration has not yet been qualified.

Use mode="isolated" for source-isolated capture without desktop changes. Controlled desktop mode manages temporary visibility, topmost placement and Windows popup separation, then restores the windows. It disables explicit foreground activation to avoid cross-process input-state changes. Add allow_resize=True only if layout-changing resizing is acceptable. Each popup has its own screenshot.

Target fields combine with AND, target lists with OR; matching windows are captured once. Names, paths, classes and window_title accept literal strings, Match.regex(...), or Match.glob(...). Patterns match the whole value; glob supports * and ?. The original flags remain available for advanced callers. See revision nine for matching, mode conflicts, shared discovery deadlines and platform limits.

A typed Python API backed by precompiled, isolated Rust helpers. The coordinator owns desktop admission and recovery records. Native acquisition and potentially blocking window operations stay outside the caller's Python process.

from window_capture import CaptureSession, Target

with CaptureSession(mode="controlled_desktop") as session:
    result = session.capture(Target(process_name="viewer.exe"))
    if result.accepted:
        result.require_image().save("viewer.png")
    else:
        print(result.reason, result.message, result.recovery)
    for event in session.events.poll():
        print(event.kind, event.reason)

All supplied selectors use AND semantics; ambiguity fails. The default total budget is 2,000 ms, including startup, queueing, fallback, encoding and restoration. Desktop changes require controlled mode or explicit advanced desktop permission. PNG encoding is timed; explicitly saving the returned image is separate file I/O. RGBA output contains immutable straight-alpha bytes and must be encoded explicitly before saving.

Process monitoring

Each visible top-level window has its own capture and change history. Owned popups are discovered and captured separately at their full size; their pixels are never composited onto the owner's screenshot. Child controls remain part of their window.

from pathlib import Path
import time
from window_capture import ChangeOptions, ProcessMonitor, Target

output = Path("captures")
output.mkdir(exist_ok=True)
with ProcessMonitor(
    Target(process_name="viewer.exe"),
    changes=ChangeOptions(channel_tolerance=3, min_changed_pixels=1),
) as monitor:
    while True:
        started = time.monotonic()
        for event in monitor.poll():
            if event.image is not None:
                handle = event.window.get("hwnd", event.window.get("x11_window_id"))
                event.image.save(output / f"{handle}-{event.result.request_id}.png")
            elif event.result is not None:
                print(event.kind, event.result.reason)
        time.sleep(max(0, 5 - (time.monotonic() - started)))

poll() returns opened, changed, geometry_changed, disappeared, capture_failed, and capture_recovered events. Repeated failures with the same reason are suppressed, while capture retries continue every poll. A changed reason emits another failure. Recovery emits metadata even when pixels are unchanged; event.image is None for recovery events. Changed images are delivered separately. See revision ten. An absent process produces no images; a newly visible popup gets its own opened event. Disappearance includes hiding and no longer matching the selector. Failed captures expose no image and preserve the last good baseline. Failed discovery raises CaptureError and preserves the previous window set.

Comparison uses decoded RGBA pixels, independent of PNG compression. A pixel counts as changed when any channel differs by more than 3 on the 0–255 scale. The default reports even one such pixel to preserve small text and indicators. Increase min_changed_pixels to suppress isolated speckles, accepting that equally small real changes will also be ignored. Comparisons use the last reported image, so cumulative drift is detected. Use poll(sink=...) to commit comparison history only after successful batch saving. It does not use a whole-image percentage rule.

Default monitoring enforces source_isolated acquisition and returns per-window capture failures instead of raising them. Controlled desktop mode explicitly permits observed-clear desktop captures. Each window receives its own capture deadline; a full poll also includes discovery and comparison. The caller schedules polls and file writes. Defaults limit discovery to 64 windows and retained RGBA baselines to 128 MiB; transient captures, decoding buffers and caller-held images require additional memory. Wayland portals do not provide this process-enumeration API.

WGC can refuse a popup that Windows does not expose as a capturable item, including the non-Alt+Tab popup in our Server 2022 fixture. The default monitor then tries PrintWindow for that popup. A GPU-rendered popup that neither route can capture remains an explicit failure; the engine cannot promise coverage of every renderer.

Capture routes

Platform Implemented routes Capability boundaries
Modern Windows Windows Graphics Capture, PrintWindow, DXGI Desktop Duplication, GDI desktop WGC isolates a window surface and supports cursor/alpha options. DXGI and GDI require permitted desktop control and report observed visibility.
Windows 7 SP1 build target PrintWindow and GDI desktop, separate x86/x64 helpers Modern WinRT/DXGI capture imports are excluded. Physical Windows 7 runtime and GPU qualification remain release gates.
Linux X11 Composite named pixmap and controlled root-desktop capture Local X11, opaque SDR TrueColor; client and child content. Reparented window-manager decorations are refused for whole-window capture.
Linux Wayland Explicit portal window selection and a persistent PipeWire stream Requires the packaged portal helper, system PipeWire and a ScreenCast portal. Complete selected window, hidden cursor, opaque BGRx; permission is prepared separately.

Windows supports HWND/PID, executable name/path, class and title selectors. X11 supports XID/PID, executable name/path, class and title selectors where local process identity can be established. Wayland uses a source selected by the user:

from window_capture import CaptureSession, Target

with CaptureSession() as session:
    source = session.select_window()  # interactive; separate from capture's deadline
    result = session.capture(Target(portal_source=source))

Prepared sources belong to that session and native process. Closure, worker death, idle expiry or stream loss invalidates a source; capture never silently reconnects or opens another selection dialog. select_window(timeout_ms=...) can bound the interaction. prepare() probes capabilities and warms the coordinator process; individual Windows acquisition processes still create fresh devices/frame pools.

Evidence and safety

CAPTURED means the candidate met the requested policy and required restoration completed. source_isolated describes acquisition scope; it is not independent pixel verification. Desktop routes report at most no_overlap_observed, because window events and geometry checks cannot atomically lock out unrelated programs. Suspicious uniform regions produce INDETERMINATE, including legitimate content that a generic capture engine cannot explain. Rejected pixels are never exposed as an accepted image.

The implementation separates configuration, IPC, process ownership, selection, mutation, pixel acquisition, validation and encoding. Resources have scoped owners; OS-specific unsafe code stays in native boundary modules. Pull-based diagnostic events have bounded capacity and overflow counts, with no user callbacks in the request path and no screenshots in diagnostic events or object representations. Process-monitor events deliberately expose accepted new/changed images to callers.

Capture admission currently serializes native acquisitions. Windows scopes it to the session/window station/desktop. Linux conservatively shares one local-host scope across X11, XWayland and Wayland, so environment aliases cannot create competing leases. Cross-account access fails unless a shared service is supplied; remote X11 capture is not coordinated by this package.

Unconfirmed mutation or restoration leaves a durable journal and quarantines new captures. An abandoned OS lock or elapsed timeout does not clear it. See recovery and limitations before operating a quarantined desktop.

Wheels and development

End-user installation needs no Rust, Cargo, C/C++ compiler or first-use downloads:

python -m pip install --no-index --find-links=dist --only-binary=:all: window-grabber
window-grabber --probe

Each Windows wheel includes architecture-matched modern and legacy executables; the Python facade selects the legacy binary on pre-Windows-10 systems. Linux wheels include separate X11 and optional portal executables, so absent PipeWire libraries do not prevent the X11 helper from starting. The current Linux CI build baseline is Ubuntu 24.04 x86_64; these are Linux platform wheels, not a claim of compatibility with every glibc baseline. PipeWire's shared library is an explicit system runtime dependency for the portal helper.

Maintainer builds use the pinned toolchains and Cargo.lock. The complete combined wheel commands live in .github/workflows/tests.yml. For local logic checks:

cargo test --locked --workspace
cargo fmt --all --check
cargo clippy --locked --workspace --all-targets -- -D warnings
python -m ruff check src tests scripts setup.py
python -m ruff format --check src tests scripts setup.py
PYTHONPATH=src python -m pytest tests -m "not windows and not linux"

CI tests installed wheels outside the source tree, with offline installation and Windows build tools removed from PATH. The desktop tests compare every pixel of independent GDI/child/OpenGL fixtures, test fallback/timeouts and concurrency, and verify restoration. Xvfb exercises installed X11 routes. Portal unit tests and installed-helper startup checks do not establish compositor interoperability.

Production qualification

Version 0.1.0 is the first production release of the documented capture routes. Unsupported operations continue to return explicit outcomes. Hosted CI does not establish physical Windows 7 GPU support, HDR/alpha accuracy, real compositor behavior, antivirus compatibility, or the proposed 99.5% success-within-two-seconds gate. Group movement of visible popups, Vulkan hooks and generic application adapters remain unavailable. No automatic privilege escalation or endpoint-policy changes are performed. Releases publish precompiled Windows wheels to PyPI; source distributions are disabled.

Opt-in renderer hooks

The experimental renderer_hook backend attaches to already-running, matching x86/x64 processes. It supports OpenGL 1.x/2.x compatibility contexts with a static executable SwapBuffers import, Direct3D 9 primary Present, and Direct3D 11 DXGI Present/Present1, plus experimental Direct3D 12 readback using a queue observed inside DXGI presentation. Each captures one client surface; select popups separately by HWND. Direct3D formats and state are deliberately restricted, including no MSAA. Direct3D 11 requires existing multithread protection and rejects partial updates. Direct3D 12 requires D3D11On12, IDXGISwapChain3 and an unambiguous observed queue; it does not guess from unrelated submissions. Vulkan readback remains unsupported.

result = capture(
    Target(hwnd=hwnd),
    options=CaptureOptions(
        allow_injection=True,
        backend_order=("renderer_hook", "print_window_full", "print_window"),
        content=ContentOptions(area="client", include_children=False),
    ),
)

The precompiled DLL is packaged with the Windows wheel. The hook is never enabled by a preset. It remains loaded and its presentation interception remains installed until the target exits, but readback requires an active request. Readback executes on the target's render thread and can stall it; the caller deadline cannot preempt that driver call. Hook errors are recorded without assuming antivirus caused them. An exclusion for Python does not establish that the DLL is permitted.

See revision sixteen for the compatibility limits, lifetime rules and tests. Hardware GPU and Windows 7 runtime qualification remain outstanding.

Use the qualification guide to measure required deployments. The runner separates failures from accepted latency and requires an independent reference to pass a scenario. Exact commit results are recorded in GitHub Actions.

Revision five defines separate-window capture and process monitoring, superseding earlier popup-composition behavior. Revision four records the underlying implementation; revision two retains the remaining intended contract and production gates. Earlier proposals remain history.

Scheduled process recording

python -m window_capture --monitor --process-name example.exe --interval-seconds 5 --output captures

Each popup is saved independently. Complete batch-* directories contain PNGs and a manifest; incomplete pending-* directories are not recordings. A failed save does not advance comparison baselines. The interval is a wait after each finished poll. Restarting records fresh baselines, so duplicate images are possible. The default 1 GiB limit covers new output per recorder instance; arrange archive retention separately. See revision six for API usage, code-review scope, GPU evidence and remaining production gates.

Crowded desktops and popup foreground capture

Enable DesktopOptions(enabled=True, separate_owned_popups=True, resize_to_fit=True) to permit temporary parent resizing when popups cannot be moved aside, and to raise an owned popup independently for its own screenshot. The complete owner group is journaled and restored. Resizing can change layout and hide content. The monitor reports different image dimensions as geometry_changed, with a fresh baseline, rather than treating them as pixel content changes.

CLI: --desktop --separate-owned-popups --resize-to-fit. See design revision eight.

External VM screenshots after a capture failure

A Windows monitor event offers an explicit desktop transaction:

if event.result is not None and not event.result.accepted:
    with event.bring_to_top(max_hold_ms=10000) as presented:
        # Your own vCenter client call; obtain a fresh VM screenshot here.
        screenshot = take_vm_screenshot()
    # Only publish after exit confirms restoration and observation checks.
    save_vm_screenshot(screenshot, guest_bounds=presented.bounds)

take_vm_screenshot and save_vm_screenshot are caller-provided functions. The context manager uses the event's complete window identity, separates owned popups, temporarily makes the target topmost, and restores journaled state. It holds the same desktop lock as normal captures. allow_resize=True explicitly permits layout changes when needed. A custom coordinator must match the monitor's configuration. This operation explicitly permits desktop changes even when the monitor uses isolated mode.

The hold limit is 100–60000 ms; setup and restoration have separate bounded budgets. Context exit, caller-pipe closure, and native hold expiry initiate restoration. A crashed/hung native transaction can leave a recovery journal and quarantine; restoration errors are raised, never silently treated as success. Expiry or observed window changes also raise, even if restoration completed.

presented.bounds uses guest desktop pixels. Map these to the vCenter image using its resolution/display arrangement; do not assume a multi-monitor VM screenshot shares that origin. Success reports observation and restoration, not external image freshness, completeness, or atomic freedom from overlap. The external image never changes the monitor baseline or automatically marks capture as recovered.

With RPyC, call bring_to_top() on the remote event and keep the context open while the local caller requests the vCenter screenshot. The native hold timer remains active if that connection is lost. Full vCenter/RPyC integration is not qualified.

Capture fallback and confidence

The default Windows order is WGC, print_window_full, print_window, desktop DXGI, then desktop GDI. The default attempt limit is five; caller limits and the single shared deadline still apply. Desktop backends are eligible only when the selected policy permits them. Full-content PrintWindow uses PW_RENDERFULLCONTENT on Windows 8.1 or newer; ordinary PrintWindow remains a separate fallback. Neither method is a universal GPU-content guarantee.

WGC uses cursor control when the interface exists. Older WGC can retain its default cursor inclusion when requested, but cannot satisfy cursor exclusion; that produces CURSOR_CONTROL_UNAVAILABLE. The recommended mode therefore uses Windows 10 2004 as its boundary for default cursor-free capture.

Suspicious uniform regions trigger fallback. If later attempts also fail, the suspicious result is preserved as CONTENT_UNCERTAIN, with the attempt history and an optional diagnostic candidate image. The cause remains unknown; matching black outputs do not prove that the application intended black content. Restoration failures remain authoritative. content_evidence describes checks and suspicious regions; completeness="not_established" explicitly distinguishes these checks from proof that all application content was rendered. pixel_assurance="checked" means the configured checks ran, not reference-image verification.

Monitor baselines retain their backend and occlusion evidence. A change in capture source emits capture_source_changed with an image and starts a new baseline, even if pixels are identical. Geometry changes retain their existing event type. An uncertain result never replaces either baseline pixels or baseline source.

Release automation

Merges to main run final tests, create a version tag and GitHub release, then publish the tested Windows wheels to PyPI. See the release guide for version bumps, Trusted Publisher setup, Linux distribution, and failed-job retries.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

window_grabber-0.1.0-py3-none-win_amd64.whl (2.5 MB view details)

Uploaded Python 3Windows x86-64

window_grabber-0.1.0-py3-none-win32.whl (2.3 MB view details)

Uploaded Python 3Windows x86

File details

Details for the file window_grabber-0.1.0-py3-none-win_amd64.whl.

File metadata

File hashes

Hashes for window_grabber-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 f99c3e91885b076c716ef864609a9551c1acc719edd8a10c3658e74476df0164
MD5 4a8873c72bc45ebe3eab426e180caced
BLAKE2b-256 580fbda740c43bcb6aeebd33fb66ab66d0b505899bec5d1a88bed80f6dc6bcbe

See more details on using hashes here.

Provenance

The following attestation bundles were made for window_grabber-0.1.0-py3-none-win_amd64.whl:

Publisher: release.yml on Heknon/window-grabber

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file window_grabber-0.1.0-py3-none-win32.whl.

File metadata

  • Download URL: window_grabber-0.1.0-py3-none-win32.whl
  • Upload date:
  • Size: 2.3 MB
  • Tags: Python 3, Windows x86
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for window_grabber-0.1.0-py3-none-win32.whl
Algorithm Hash digest
SHA256 bfca7ee22153488f40f0fdefbea44b91dcbcd17d5cc73a429343431bd095796e
MD5 cdfcfc9e6473a061f8f286b88dafe33b
BLAKE2b-256 e2a550013146a7f58153424f71a8e9eb94d34633943f74caac3d1dcd06e4e9e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for window_grabber-0.1.0-py3-none-win32.whl:

Publisher: release.yml on Heknon/window-grabber

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page