File size: 2,694 Bytes
ce847d4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
"""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
|