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