"""macOS CoreGraphics backend for MSS. Uses the CoreGraphics APIs to capture windows and enumerates up to ``max_displays`` active displays. """ from __future__ import annotations import ctypes import ctypes.util import sys import warnings from ctypes import ( POINTER, Structure, c_double, c_float, c_int32, c_long, c_size_t, c_ubyte, c_uint32, c_void_p, ) from platform import mac_ver from typing import TYPE_CHECKING from mss.base import MSS as _MSS from mss.base import MSSImplementation from mss.exception import ScreenShotError from mss.screenshot import Size if TYPE_CHECKING: from typing import Any from mss.models import CFunctions, Monitor, Monitors __all__ = ("IMAGE_OPTIONS", "MSS") BACKENDS = ["default"] MAC_VERSION_CATALINA = 10.16 kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816 kCGWindowImageNominalResolution = 1 << 4 # noqa: N816 kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816 #: For advanced users: as a note, you can set ``IMAGE_OPTIONS = 0`` to turn on scaling; see issue #257 for more #: information. IMAGE_OPTIONS: int = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution class MSS(_MSS): """Deprecated macOS compatibility constructor. Use :class:`mss.MSS` instead. """ def __init__(self, /, **kwargs: Any) -> None: # TODO(jholveck): #493 Remove compatibility constructor after 10.x transition period. warnings.warn( "mss.darwin.MSS is deprecated and will be removed in 11.0; use mss.MSS instead", DeprecationWarning, stacklevel=2, ) super().__init__(**kwargs) def cgfloat() -> type[c_double | c_float]: """Get the appropriate value for a float.""" return c_double if sys.maxsize > 2**32 else c_float class CGPoint(Structure): """Structure that contains coordinates of a rectangle.""" _fields_ = (("x", cgfloat()), ("y", cgfloat())) def __repr__(self) -> str: return f"{type(self).__name__}(left={self.x} top={self.y})" class CGSize(Structure): """Structure that contains dimensions of an rectangle.""" _fields_ = (("width", cgfloat()), ("height", cgfloat())) def __repr__(self) -> str: return f"{type(self).__name__}(width={self.width} height={self.height})" class CGRect(Structure): """Structure that contains information about a rectangle.""" _fields_ = (("origin", CGPoint), ("size", CGSize)) def __repr__(self) -> str: return f"{type(self).__name__}<{self.origin} {self.size}>" # C functions that will be initialised later. # # Available attr: core. # # Note: keep it sorted by cfunction. CFUNCTIONS: CFunctions = { # Syntax: cfunction: (attr, argtypes, restype) "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), "CGDisplayBounds": ("core", [c_uint32], CGRect), "CGDisplayRotation": ("core", [c_uint32], c_double), "CFDataGetBytePtr": ("core", [c_void_p], POINTER(c_ubyte)), "CFDataGetLength": ("core", [c_void_p], c_long), "CFRelease": ("core", [c_void_p], None), "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32), "CGImageGetBitsPerPixel": ("core", [c_void_p], c_size_t), "CGImageGetBytesPerRow": ("core", [c_void_p], c_size_t), "CGImageGetDataProvider": ("core", [c_void_p], c_void_p), "CGImageGetHeight": ("core", [c_void_p], c_size_t), "CGImageGetWidth": ("core", [c_void_p], c_size_t), "CGRectStandardize": ("core", [CGRect], CGRect), "CGRectUnion": ("core", [CGRect, CGRect], CGRect), "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p), } class MSSImplDarwin(MSSImplementation): """Multiple ScreenShots implementation for macOS. It uses intensively the CoreGraphics library. :param max_displays: maximum number of displays to handle (default: 32). :type max_displays: int .. seealso:: :py:class:`mss.MSS` Lists other parameters. """ __slots__ = {"core", "max_displays"} def __init__(self, *, backend: str = "default", max_displays: int = 32) -> None: super().__init__() if backend != "default": msg = 'The only valid backend on this platform is "default".' raise ScreenShotError(msg) # max_displays is only used by monitors(), while the lock is held. #: Maximum number of displays to handle. self.max_displays = max_displays self._init_library() self._set_cfunctions() def _init_library(self) -> None: """Load the CoreGraphics library.""" version = float(".".join(mac_ver()[0].split(".")[:2])) if version < MAC_VERSION_CATALINA: coregraphics = ctypes.util.find_library("CoreGraphics") else: # macOS Big Sur and newer coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" if not coregraphics: msg = "No CoreGraphics library found." raise ScreenShotError(msg) # :meta:private: self.core = ctypes.cdll.LoadLibrary(coregraphics) def _set_cfunctions(self) -> None: """Set all ctypes functions and attach them to attributes.""" cfactory = self._cfactory attrs = {"core": self.core} for func, (attr, argtypes, restype) in CFUNCTIONS.items(): cfactory(attrs[attr], func, argtypes, restype) def monitors(self) -> Monitors: """Get positions of monitors.""" int_ = int core = self.core monitors: Monitors = [] # All monitors # We need to update the value with every single monitor found # using CGRectUnion. Else we will end with infinite values. all_monitors = CGRect() monitors.append({}) # Each monitor display_count = c_uint32(0) active_displays = (c_uint32 * self.max_displays)() core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count)) for idx in range(display_count.value): display = active_displays[idx] rect = core.CGDisplayBounds(display) rect = core.CGRectStandardize(rect) width, height = rect.size.width, rect.size.height # 0.0: normal # 90.0: right # -90.0: left if core.CGDisplayRotation(display) in {90.0, -90.0}: width, height = height, width monitors.append( { "left": int_(rect.origin.x), "top": int_(rect.origin.y), "width": int_(width), "height": int_(height), }, ) # Update AiO monitor's values all_monitors = core.CGRectUnion(all_monitors, rect) # Set the AiO monitor's values monitors[0] = { "left": int_(all_monitors.origin.x), "top": int_(all_monitors.origin.y), "width": int_(all_monitors.size.width), "height": int_(all_monitors.size.height), } return monitors def grab(self, monitor: Monitor, /) -> tuple[bytearray, Size]: """Retrieve all pixels from a monitor. Pixels have to be RGB.""" core = self.core rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) if not image_ref: msg = "CoreGraphics.CGWindowListCreateImage() failed." raise ScreenShotError(msg) width = core.CGImageGetWidth(image_ref) height = core.CGImageGetHeight(image_ref) copy_data = None try: prov = core.CGImageGetDataProvider(image_ref) copy_data = core.CGDataProviderCopyData(prov) data_ref = core.CFDataGetBytePtr(copy_data) buf_len = core.CFDataGetLength(copy_data) raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len)) data = bytearray(raw.contents) # Remove padding per row bytes_per_row = core.CGImageGetBytesPerRow(image_ref) bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref) bytes_per_pixel = (bytes_per_pixel + 7) // 8 if bytes_per_pixel * width != bytes_per_row: cropped = bytearray() for row in range(height): start = row * bytes_per_row end = start + width * bytes_per_pixel cropped.extend(data[start:end]) data = cropped finally: if copy_data: core.CFRelease(copy_data) core.CFRelease(image_ref) return data, Size(width, height) def cursor(self) -> None: """Retrieve all cursor data. Pixels have to be RGB.""" return