"""Screen capture using mss — fast, cross-platform screenshots. Usage: from src.services.ocr.screen_capture import ScreenCapture capture = ScreenCapture() img = capture.grab_primary() # full primary monitor img = capture.grab_region(100, 200, 800, 600) # x, y, w, h """ from __future__ import annotations import mss from PIL import Image class ScreenCapture: """Fast screen capture using mss library. Why per-call mss.mss(): mss uses thread-local GDI device contexts (srcdc) on Windows. When created in one thread and used from another (e.g. pywebview API call thread), it raises '_thread._local' object has no attribute 'srcdc'. Creating a fresh mss context per grab call is cheap and thread-safe. """ def __init__(self) -> None: # Pre-read monitor info (works cross-thread — just metadata) with mss.mss() as sct: self._monitors = list(sct.monitors) @staticmethod def _grab(monitor: dict) -> Image.Image: """Thread-safe grab: creates mss context per call.""" with mss.mss() as sct: raw = sct.grab(monitor) return Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX").convert("RGBA") def grab_primary(self) -> Image.Image: """Capture the entire primary monitor. Returns: PIL Image (RGBA). """ return self._grab(self._monitors[1]) # [0] = all monitors, [1] = primary def grab_region(self, x: int, y: int, width: int, height: int) -> Image.Image: """Capture a rectangular region of the screen. Args: x: Left coordinate. y: Top coordinate. width: Width in pixels. height: Height in pixels. Returns: PIL Image (RGBA). """ region = {"left": x, "top": y, "width": width, "height": height} return self._grab(region) def grab_monitor(self, monitor_index: int = 1) -> Image.Image: """Capture a specific monitor. Args: monitor_index: 1-based monitor index (1 = primary). Returns: PIL Image (RGBA). """ if monitor_index < 1 or monitor_index >= len(self._monitors): raise ValueError( f"Monitor index must be 1..{len(self._monitors) - 1}, got {monitor_index}" ) return self._grab(self._monitors[monitor_index]) @property def monitor_count(self) -> int: """Number of physical monitors (excluding virtual 'all' monitor).""" return len(self._monitors) - 1 def close(self) -> None: """No-op — mss contexts are per-call now.""" pass