91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
149 lines
4.4 KiB
Python
149 lines
4.4 KiB
Python
"""Cross-platform screen capture and info."""
|
|
|
|
import base64
|
|
import io
|
|
import os
|
|
import platform
|
|
from typing import Tuple
|
|
|
|
from PIL import Image
|
|
|
|
|
|
def _get_display_scale() -> float:
|
|
"""Detect the OS display scale factor (DPI scaling).
|
|
|
|
Returns the ratio of physical pixels to logical pixels:
|
|
- macOS Retina: typically 2.0
|
|
- Windows 150% DPI: 1.5
|
|
- Linux HiDPI (Wayland/X11): reads GDK_SCALE or QT_SCALE_FACTOR
|
|
- Falls back to 1.0 on failure or unknown platforms.
|
|
"""
|
|
system = platform.system()
|
|
|
|
# macOS: use AppKit to query the main screen backing scale factor
|
|
if system == "Darwin":
|
|
try:
|
|
from AppKit import NSScreen # type: ignore[import-untyped]
|
|
|
|
return float(NSScreen.mainScreen().backingScaleFactor())
|
|
except Exception:
|
|
return 1.0
|
|
|
|
# Windows: use shcore to get the monitor scale factor
|
|
if system == "Windows":
|
|
try:
|
|
import ctypes
|
|
|
|
return ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100.0 # type: ignore[attr-defined]
|
|
except Exception:
|
|
return 1.0
|
|
|
|
# Linux: check common env vars set by Wayland/X11 compositors and toolkits
|
|
if system == "Linux":
|
|
try:
|
|
gdk = os.environ.get("GDK_SCALE")
|
|
if gdk:
|
|
return float(gdk)
|
|
qt = os.environ.get("QT_SCALE_FACTOR")
|
|
if qt:
|
|
return float(qt)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return 1.0
|
|
|
|
return 1.0
|
|
|
|
|
|
def get_display_scale() -> float:
|
|
"""Return the OS display scale factor (public API).
|
|
|
|
This is a convenience wrapper around _get_display_scale() for use by
|
|
external code (e.g. MCP servers) that need to know the current DPI scale.
|
|
"""
|
|
return _get_display_scale()
|
|
|
|
|
|
def screenshot() -> Image.Image:
|
|
"""Capture a screenshot of all monitors and return a PIL Image.
|
|
|
|
The returned image is normalized to logical pixels so that its coordinate
|
|
space matches pynput mouse input (which operates in logical coordinates).
|
|
|
|
Tries PIL.ImageGrab first (works on Windows and macOS).
|
|
Falls back to mss if ImageGrab fails (better Linux / multi-monitor support).
|
|
"""
|
|
# Try PIL.ImageGrab (Windows + macOS)
|
|
try:
|
|
from PIL import ImageGrab
|
|
|
|
img = ImageGrab.grab(all_screens=True)
|
|
if isinstance(img, Image.Image):
|
|
scale = _get_display_scale()
|
|
if scale > 1.0:
|
|
img = img.resize(
|
|
(int(img.width / scale), int(img.height / scale)),
|
|
Image.LANCZOS,
|
|
)
|
|
return img
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: mss (cross-platform, optional dependency)
|
|
try:
|
|
import mss # type: ignore[import-untyped]
|
|
import mss.tools # type: ignore[import-untyped]
|
|
|
|
with mss.mss() as sct:
|
|
monitor = sct.monitors[0] # combined virtual desktop
|
|
sct_img = sct.grab(monitor)
|
|
img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
|
|
scale = _get_display_scale()
|
|
if scale > 1.0:
|
|
img = img.resize(
|
|
(int(img.width / scale), int(img.height / scale)),
|
|
Image.LANCZOS,
|
|
)
|
|
return img
|
|
except Exception as e:
|
|
raise RuntimeError(f"screenshot failed: {e}") from e
|
|
|
|
|
|
def screenshot_bytes(format: str = "PNG") -> bytes:
|
|
"""Return screenshot as raw bytes in the specified image format."""
|
|
img = screenshot()
|
|
buf = io.BytesIO()
|
|
img.save(buf, format=format)
|
|
return buf.getvalue()
|
|
|
|
|
|
def screenshot_b64(format: str = "PNG") -> str:
|
|
"""Return screenshot as a base64-encoded string."""
|
|
return base64.b64encode(screenshot_bytes(format)).decode()
|
|
|
|
|
|
def screen_size() -> Tuple[int, int]:
|
|
"""Return the total virtual desktop size as (width, height)."""
|
|
img = screenshot()
|
|
return img.size
|
|
|
|
|
|
def cursor_position() -> Tuple[int, int]:
|
|
"""Return the current cursor position as (x, y).
|
|
|
|
Tries win32gui first on Windows (more reliable with DPI scaling).
|
|
Falls back to pynput if win32 is unavailable (cross-platform fallback).
|
|
"""
|
|
try:
|
|
import win32gui # type: ignore[import-untyped]
|
|
|
|
pos = win32gui.GetCursorPos()
|
|
return int(pos[0]), int(pos[1])
|
|
except Exception:
|
|
pass
|
|
|
|
from pynput.mouse import Controller as _MouseController
|
|
|
|
c = _MouseController()
|
|
x, y = c.position
|
|
return int(x), int(y)
|