Metadata-Version: 2.2
Name: window-grabber
Version: 0.1.0
Summary: Deadline-bounded window capture with explicit evidence and isolated native workers
Author: window-grabber contributors
Classifier: Development Status :: 5 - Production/Stable
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: test
Requires-Dist: pytest<9,>=7.4; extra == "test"
Requires-Dist: pytest-cov<6,>=4; extra == "test"

# window-grabber

## Monitor processes

```python
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:

```python
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](docs/window_capture_design_revision_nine.md) 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.

```python
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.

```python
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](docs/window_capture_design_revision_ten.md).
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:

```python
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](docs/window_capture_design_revision_four.md) before
operating a quarantined desktop.

## Wheels and development

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

```console
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](.github/workflows/tests.yml).
For local logic checks:

```console
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.

```python
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](docs/window_capture_design_revision_sixteen.md) for the
compatibility limits, lifetime rules and tests. Hardware GPU and Windows 7 runtime
qualification remain outstanding.

Use [the qualification guide](docs/qualification.md) 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](https://github.com/Heknon/window-grabber/actions).

[Revision five](docs/window_capture_design_revision_five.md) defines separate-window
capture and process monitoring, superseding earlier popup-composition behavior.
[Revision four](docs/window_capture_design_revision_four.md) records the underlying
implementation; [revision two](docs/window_capture_design_revision_two.md) retains
the remaining intended contract and production gates. Earlier proposals remain history.

### Scheduled process recording

```console
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](docs/window_capture_design_revision_six.md)
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](docs/window_capture_design_revision_eight.md).

## External VM screenshots after a capture failure

A Windows monitor event offers an explicit desktop transaction:

```python
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](docs/releasing.md)
for version bumps, Trusted Publisher setup, Linux distribution, and failed-job retries.
