|
|
"""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: |
|
|
|
|
|
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]) |
|
|
|
|
|
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 |
|
|
|