diff --git a/docs/source/api.rst b/docs/source/api.rst index a385d56..a566783 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -2,466 +2,72 @@ MSS API ======= -Classes -======= - -macOS ------ - -.. module:: mss.darwin - -.. attribute:: CFUNCTIONS - - .. versionadded:: 6.1.0 - -.. function:: cgfloat - -.. class:: CGPoint - -.. class:: CGSize - -.. class:: CGRect - -.. class:: MSS - - .. attribute:: core - - .. attribute:: max_displays - -GNU/Linux ---------- - -.. module:: mss.linux - -Factory function to return the appropriate backend implementation. - -.. function:: mss(backend="default", **kwargs) - - :keyword str backend: Backend name ("default", "xlib", "xgetimage", or "xshmgetimage"). - :keyword display: Display name (e.g., ":0.0") for the X server. Default is taken from the :envvar:`DISPLAY` environment variable. - :type display: str or None - :param kwargs: Additional arguments passed to the backend MSS class. - :rtype: :class:`mss.base.MSSBase` - :return: Backend-specific MSS instance. - - Factory returning a proper MSS class instance for GNU/Linux. - The backend parameter selects the implementation: - - - "default" or "xshmgetimage": XCB-based backend using XShmGetImage (default, with automatic fallback to XGetImage) - - "xgetimage": XCB-based backend using XGetImage - - "xlib": Traditional Xlib-based backend retained for environments without working XCB libraries - - .. versionadded:: 10.2.0 - The :py:attr:`backend` attribute. - -.. function:: MSS(*args, **kwargs) - - Alias for :func:`mss` for backward compatibility. - - .. versionadded:: 10.2.0 - - -Xlib Backend -^^^^^^^^^^^^ - -.. versionadded:: 10.2.0 -.. module:: mss.linux.xlib - -Legacy Xlib-based backend, kept as a fallback when XCB is unavailable. - -.. attribute:: CFUNCTIONS - - .. versionadded:: 6.1.0 - -.. attribute:: PLAINMASK - -.. attribute:: ZPIXMAP - -.. class:: Display - - Structure that serves as the connection to the X server, and that contains all the information about that X server. - -.. class:: XErrorEvent - - XErrorEvent to debug eventual errors. - -.. class:: XFixesCursorImage - - Cursor structure - -.. class:: XImage - - Description of an image as it exists in the client's memory. - -.. class:: XRRCrtcInfo - - Structure that contains CRTC information. - -.. class:: XRRModeInfo - -.. class:: XRRScreenResources - - Structure that contains arrays of XIDs that point to the available outputs and associated CRTCs. +Core Package +============ -.. class:: XWindowAttributes +.. automodule:: mss - Attributes for the specified window. - -.. class:: MSS - - .. method:: close() - - Clean-up method. - - .. versionadded:: 8.0.0 - - -XGetImage Backend -^^^^^^^^^^^^^^^^^ - -.. versionadded:: 10.2.0 -.. module:: mss.linux.xgetimage - -XCB-based backend using XGetImage protocol. - -.. class:: MSS - - XCB implementation using XGetImage for screenshot capture. - - -XShmGetImage Backend -^^^^^^^^^^^^^^^^^^^^ - -.. versionadded:: 10.2.0 -.. module:: mss.linux.xshmgetimage - -XCB-based backend using XShmGetImage protocol with shared memory. - -.. class:: ShmStatus - - Enum describing the availability of the X11 MIT-SHM extension used by the backend. - - .. attribute:: UNKNOWN - - Initial state before any capture confirms availability or failure. - - .. attribute:: AVAILABLE - - Shared-memory capture works and will continue to be used. - - .. attribute:: UNAVAILABLE - - Shared-memory capture failed; MSS will use XGetImage. - -.. class:: MSS - - XCB implementation using XShmGetImage for screenshot capture. - Falls back to XGetImage if shared memory extension is unavailable. - - .. attribute:: shm_status - - Current shared-memory availability, using :class:`mss.linux.xshmgetimage.ShmStatus`. - - .. attribute:: shm_fallback_reason - - Optional string describing why the backend fell back to XGetImage when MIT-SHM is unavailable. - -Windows -------- - -.. module:: mss.windows - -.. attribute:: CAPTUREBLT - -.. attribute:: CFUNCTIONS - - .. versionadded:: 6.1.0 - -.. attribute:: DIB_RGB_COLORS - -.. attribute:: SRCCOPY - -.. class:: BITMAPINFOHEADER - -.. class:: BITMAPINFO - -.. attribute:: MONITORNUMPROC - - .. versionadded:: 6.1.0 - -.. class:: MSS - - .. attribute:: gdi32 - - .. attribute:: user32 - -Methods -======= +Screenshot Objects +================== -.. module:: mss.base +.. automodule:: mss.screenshot -.. attribute:: lock +Base Classes +============ - .. versionadded:: 6.0.0 +.. automodule:: mss.base -.. class:: MSSBase +Tools +===== - The parent's class for every OS implementation. +.. automodule:: mss.tools - .. attribute:: cls_image - - .. attribute:: compression_level - - PNG compression level used when saving the screenshot data into a file (see :py:func:`zlib.compress()` for details). - - .. versionadded:: 3.2.0 - - .. attribute:: with_cursor - - Include the mouse cursor in screenshots. - - .. versionadded:: 8.0.0 - - .. method:: __init__(compression_level=6, display=None, max_displays=32, with_cursor=False) - - :type compression_level: int - :param compression_level: PNG compression level. - :type display: bytes, str or None - :param display: The display to use. Only effective on GNU/Linux. - :type max_displays: int - :param max_displays: Maximum number of displays. Only effective on macOS. - :type with_cursor: bool - :param with_cursor: Include the mouse cursor in screenshots. - - .. versionadded:: 8.0.0 - ``compression_level``, ``display``, ``max_displays``, and ``with_cursor``, keyword arguments. - - .. method:: close() - - Clean-up method. - - .. versionadded:: 4.0.0 - - .. method:: grab(region) - - :param dict monitor: region's coordinates. - :rtype: :class:`ScreenShot` - - Retrieve screen pixels for a given *region*. - Subclasses need to implement this. - - .. note:: - - *monitor* can be a ``tuple`` like ``PIL.Image.grab()`` accepts, - it will be converted to the appropriate ``dict``. - - .. method:: save([mon=1], [output='mon-{mon}.png'], [callback=None]) - - :param int mon: the monitor's number. - :param str output: the output's file name. - :type callback: callable or None - :param callback: callback called before saving the screenshot to a file. Takes the *output* argument as parameter. - :rtype: iterable - :return: Created file(s). - - Grab a screenshot and save it to a file. - The *output* parameter can take several keywords to customize the filename: - - - ``{mon}``: the monitor number - - ``{top}``: the screenshot y-coordinate of the upper-left corner - - ``{left}``: the screenshot x-coordinate of the upper-left corner - - ``{width}``: the screenshot's width - - ``{height}``: the screenshot's height - - ``{date}``: the current date using the default formatter - - As it is using the :py:func:`format()` function, you can specify formatting options like ``{date:%Y-%m-%s}``. - - .. warning:: On Windows, the default date format may result with a filename containing ':' which is not allowed:: - - IOerror: [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png' - - To fix this, you must provide a custom date formatting. - - .. method:: shot() - - :return str: The created file. - - Helper to save the screenshot of the first monitor, by default. - You can pass the same arguments as for :meth:`save()`. - - .. versionadded:: 3.0.0 - -.. class:: ScreenShot - - Screenshot object. - - .. note:: - - A better name would have been *Image*, but to prevent collisions - with ``PIL.Image``, it has been decided to use *ScreenShot*. - - .. classmethod:: from_size(cls, data, width, height) - - :param bytearray data: raw BGRA pixels retrieved by ctypes - OS independent implementations. - :param int width: the monitor's width. - :param int height: the monitor's height. - :rtype: :class:`ScreenShot` - - Instantiate a new class given only screenshot's data and size. - - .. method:: pixel(coord_x, coord_y) - - :param int coord_x: The x coordinate. - :param int coord_y: The y coordinate. - :rtype: tuple(int, int, int) - - Get the pixel value at the given position. - - .. versionadded:: 3.0.0 - -.. module:: mss.tools - -.. method:: to_png(data, size, level=6, output=None) - - :param bytes data: RGBRGB...RGB data. - :param tuple size: The (width, height) pair. - :param int level: PNG compression level. - :param str output: output's file name. - :raises ScreenShotError: On error when writing *data* to *output*. - :raises zlib.error: On bad compression *level*. - - Dump data to the image file. Pure Python PNG implementation. - If *output* is ``None``, create no file but return the whole PNG data. - - .. versionadded:: 3.0.0 - - .. versionchanged:: 3.2.0 - - The *level* keyword argument to control the PNG compression level. - - -Properties +Exceptions ========== -.. class:: mss.base.MSSBase - - .. attribute:: monitors - - Positions of all monitors. - If the monitor has rotation, you have to deal with it - inside this method. - - This method has to fill ``self._monitors`` with all information - and use it as a cache: - - - ``self._monitors[0]`` is a dict of all monitors together - - ``self._monitors[N]`` is a dict of the monitor N (with N > 0) - - Each monitor is a dict with: - - - ``left``: the x-coordinate of the upper-left corner - - ``top``: the y-coordinate of the upper-left corner - - ``width``: the width - - ``height``: the height - - Subclasses need to implement this. - - :rtype: list[dict[str, int]] - -.. class:: mss.base.ScreenShot - - .. attribute:: __array_interface__() - - Numpy array interface support. It uses raw data in BGRA form. - - :rtype: dict[str, Any] - - .. attribute:: bgra - - BGRA values from the BGRA raw pixels. - - :rtype: bytes - - .. versionadded:: 3.2.0 - - .. attribute:: height +.. automodule:: mss.exception - The screenshot's height. +Data Models +=========== - :rtype: int +.. automodule:: mss.models - .. attribute:: left +Platform Backends +================= - The screenshot's left coordinate. +macOS Backend +------------- - :rtype: int +.. automodule:: mss.darwin - .. attribute:: pixels +GNU/Linux Dispatcher +-------------------- - List of row tuples that contain RGB tuples. +.. automodule:: mss.linux - :rtype: list[tuple(tuple(int, int, int), ...)] +GNU/Linux Xlib Backend +---------------------- - .. attribute:: pos +.. automodule:: mss.linux.xlib - The screenshot's coordinates. +GNU/Linux XCB Base +------------------ - :rtype: :py:func:`collections.namedtuple()` +.. automodule:: mss.linux.base - .. attribute:: rgb +GNU/Linux XCB XGetImage Backend +------------------------------- - Computed RGB values from the BGRA raw pixels. +.. automodule:: mss.linux.xgetimage - :rtype: bytes +GNU/Linux XCB XShmGetImage Backend +---------------------------------- - .. versionadded:: 3.0.0 +.. automodule:: mss.linux.xshmgetimage - .. attribute:: size - - The screenshot's size. - - :rtype: :py:func:`collections.namedtuple()` - - .. attribute:: top - - The screenshot's top coordinate. - - :rtype: int - - .. attribute:: width - - The screenshot's width. - - :rtype: int - - -Exception -========= - -.. module:: mss.exception - -.. exception:: ScreenShotError - - Base class for MSS exceptions. - - .. attribute:: details - - On GNU/Linux, and if the error comes from the XServer, it contains XError details. - This is an empty dict by default. - - For XErrors, you can find information on `Using the Default Error Handlers `_. - - :rtype: dict[str, Any] - - .. versionadded:: 3.3.0 - - -Factory -======= +Windows Backend +--------------- -.. module:: mss.factory +.. automodule:: mss.windows -.. function:: mss() - Factory function to instance the appropriate MSS class. diff --git a/docs/source/conf.py b/docs/source/conf.py index a0d9b99..9f49912 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,11 +6,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) +import ctypes + import mss # -- General configuration ------------------------------------------------ extensions = [ + "sphinx.ext.autodoc", "sphinx_copybutton", "sphinx.ext.intersphinx", "sphinx_new_tab_link", @@ -29,6 +32,16 @@ release = "latest" language = "en" todo_include_todos = True +autodoc_member_order = "bysource" +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, +} + +# Monkey-patch WINFUNCTYPE into ctypes, so that we can import +# mss.windows while building the documentation. +ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE # type:ignore[attr-defined] # -- Options for HTML output ---------------------------------------------- diff --git a/docs/source/developers.rst b/docs/source/developers.rst index 1d4636f..160e0d6 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -29,7 +29,7 @@ You will need `pytest `_:: How to Test? ------------ -Launch the test suit:: +Launch the test suite:: $ python -m pytest diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 21ae18f..ecac4b4 100644 --- a/src/mss/__init__.py +++ b/src/mss/__init__.py @@ -1,11 +1,10 @@ +# This module is maintained by Mickaël Schoentgen . +# +# You can always get the latest version of this module at: +# https://github.com/BoboTiG/python-mss +# If that URL should fail, try contacting the author. """An ultra fast cross-platform multiple screenshots module in pure python using ctypes. - -This module is maintained by Mickaël Schoentgen . - -You can always get the latest version of this module at: - https://github.com/BoboTiG/python-mss -If that URL should fail, try contacting the author. """ from mss.exception import ScreenShotError diff --git a/src/mss/base.py b/src/mss/base.py index 2abfcbd..1a984d1 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -1,6 +1,5 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. -""" +# This is part of the MSS Python's module. +# Source: https://github.com/BoboTiG/python-mss. from __future__ import annotations @@ -35,15 +34,28 @@ UTC = timezone.utc +#: Global lock protecting access to platform screenshot calls. +#: +#: .. versionadded:: 6.0.0 lock = Lock() OPAQUE = 255 class MSSBase(metaclass=ABCMeta): - """This class will be overloaded by a system specific one.""" + """Base class for all Multiple ScreenShots implementations. - __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"} + :param backend: Backend selector, for platforms with multiple backends. + :param compression_level: PNG compression level. + :param with_cursor: Include the mouse cursor in screenshots. + :param display: X11 display name (GNU/Linux only). + :param max_displays: Maximum number of displays to enumerate (macOS only). + + .. versionadded:: 8.0.0 + ``compression_level``, ``display``, ``max_displays``, and ``with_cursor`` keyword arguments. + """ + + __slots__ = {"_closed", "_monitors", "cls_image", "compression_level", "with_cursor"} def __init__( self, @@ -58,9 +70,17 @@ def __init__( max_displays: int = 32, # noqa: ARG002 ) -> None: self.cls_image: type[ScreenShot] = ScreenShot + #: PNG compression level used when saving the screenshot data into a file + #: (see :py:func:`zlib.compress()` for details). + #: + #: .. versionadded:: 3.2.0 self.compression_level = compression_level + #: Include the mouse cursor in screenshots. + #: + #: .. versionadded:: 8.0.0 self.with_cursor = with_cursor self._monitors: Monitors = [] + self._closed = False # If there isn't a factory that removed the "backend" argument, make sure that it was set to "default". # Factories that do backend-specific dispatch should remove that argument. if backend != "default": @@ -91,17 +111,43 @@ def _monitors_impl(self) -> None: It must populate self._monitors. """ - def close(self) -> None: # noqa:B027 - """Clean-up.""" + def _close_impl(self) -> None: # noqa:B027 + """Clean up. + + This will be called at most once. + """ + # It's not necessary for subclasses to implement this if they have nothing to clean up. + + def close(self) -> None: + """Clean up. + + This releases resources that MSS may be using. Once the MSS + object is closed, it may not be used again. + + It is safe to call this multiple times; multiple calls have no + effect. + + Rather than use :py:meth:`close` explicitly, we recommend you + use the MSS object as a context manager:: + + with mss.mss() as sct: + ... + """ + with lock: + if self._closed: + return + self._close_impl() + self._closed = True def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot: """Retrieve screen pixels for a given monitor. - Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts. + Note: *monitor* can be a tuple like the one + py:meth:`PIL.ImageGrab.grab` accepts: `(left, top, right, bottom)` :param monitor: The coordinates and size of the box to capture. See :meth:`monitors ` for object details. - :return :class:`ScreenShot `. + :returns: Screenshot of the requested region. """ # Convert PIL bbox style if isinstance(monitor, tuple): @@ -126,16 +172,16 @@ def monitors(self) -> Monitors: This method has to fill self._monitors with all information and use it as a cache: - self._monitors[0] is a dict of all monitors together - self._monitors[N] is a dict of the monitor N (with N > 0) + + - self._monitors[0] is a dict of all monitors together + - self._monitors[N] is a dict of the monitor N (with N > 0) Each monitor is a dict with: - { - 'left': the x-coordinate of the upper-left corner, - 'top': the y-coordinate of the upper-left corner, - 'width': the width, - 'height': the height - } + + - ``left``: the x-coordinate of the upper-left corner + - ``top``: the y-coordinate of the upper-left corner + - ``width``: the width + - ``height``: the height """ if not self._monitors: with lock: @@ -153,28 +199,13 @@ def save( ) -> Iterator[str]: """Grab a screenshot and save it to a file. - :param int mon: The monitor to screenshot (default=0). - -1: grab one screenshot of all monitors - 0: grab one screenshot by monitor - N: grab the screenshot of the monitor N - - :param str output: The output filename. - - It can take several keywords to customize the filename: - - `{mon}`: the monitor number - - `{top}`: the screenshot y-coordinate of the upper-left corner - - `{left}`: the screenshot x-coordinate of the upper-left corner - - `{width}`: the screenshot's width - - `{height}`: the screenshot's height - - `{date}`: the current date using the default formatter - - As it is using the `format()` function, you can specify - formatting options like `{date:%Y-%m-%s}`. - - :param callable callback: Callback called before saving the - screenshot to a file. Take the `output` argument as parameter. - - :return generator: Created file(s). + :param int mon: The monitor to screenshot (default=0). ``-1`` grabs all + monitors, ``0`` grabs each monitor, and ``N`` grabs monitor ``N``. + :param str output: The output filename. Keywords: ``{mon}``, ``{top}``, + ``{left}``, ``{width}``, ``{height}``, ``{date}``. + :param callable callback: Called before saving the screenshot; receives + the ``output`` argument. + :return: Created file(s). """ monitors = self.monitors if not monitors: diff --git a/src/mss/darwin.py b/src/mss/darwin.py index 5d723b9..77fe50b 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -1,5 +1,7 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. +"""macOS CoreGraphics backend for MSS. + +Uses the CoreGraphics APIs to capture windows and enumerates up to +``max_displays`` active displays. """ from __future__ import annotations @@ -18,7 +20,7 @@ if TYPE_CHECKING: # pragma: nocover from mss.models import CFunctions, Monitor -__all__ = ("MSS",) +__all__ = ("IMAGE_OPTIONS", "MSS") BACKENDS = ["default"] @@ -27,8 +29,9 @@ kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816 kCGWindowImageNominalResolution = 1 << 4 # noqa: N816 kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816 -# Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information) -IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution +#: 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 def cgfloat() -> type[c_double | c_float]: @@ -92,14 +95,22 @@ def __repr__(self) -> str: class MSS(MSSBase): """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.base.MSSBase` + Lists other parameters. """ __slots__ = {"core", "max_displays"} def __init__(self, /, **kwargs: Any) -> None: - """MacOS initialisations.""" super().__init__(**kwargs) + #: Maximum number of displays to handle. self.max_displays = kwargs.get("max_displays", 32) self._init_library() @@ -117,6 +128,7 @@ def _init_library(self) -> None: if not coregraphics: msg = "No CoreGraphics library found." raise ScreenShotError(msg) + # :meta:private: self.core = ctypes.cdll.LoadLibrary(coregraphics) def _set_cfunctions(self) -> None: diff --git a/src/mss/exception.py b/src/mss/exception.py index 7fdf211..5c8b08f 100644 --- a/src/mss/exception.py +++ b/src/mss/exception.py @@ -1,6 +1,5 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. -""" +# This is part of the MSS Python's module. +# Source: https://github.com/BoboTiG/python-mss. from __future__ import annotations @@ -12,4 +11,11 @@ class ScreenShotError(Exception): def __init__(self, message: str, /, *, details: dict[str, Any] | None = None) -> None: super().__init__(message) + #: On GNU/Linux, and if the error comes from the XServer, it contains XError details. + #: This is an empty dict by default. + #: + #: For XErrors, you can find information on + #: `Using the Default Error Handlers `_. + #: + #: .. versionadded:: 3.3.0 self.details = details or {} diff --git a/src/mss/factory.py b/src/mss/factory.py index 933310d..e9bf40a 100644 --- a/src/mss/factory.py +++ b/src/mss/factory.py @@ -1,6 +1,5 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. -""" +# This is part of the MSS Python's module. +# Source: https://github.com/BoboTiG/python-mss. import platform from typing import Any @@ -18,6 +17,15 @@ def mss(**kwargs: Any) -> MSSBase: It then proxies its arguments to the class for instantiation. + + .. seealso:: + - :class:`mss.darwin.MSS` + - :class:`mss.linux.MSS` + - :class:`mss.windows.MSS` + - :func:`mss.linux.mss` + - :class:`mss.linux.xshmgetimage.MSS` + - :class:`mss.linux.xgetimage.MSS` + - :class:`mss.linux.xlib.MSS` """ os_ = platform.system().lower() diff --git a/src/mss/linux/__init__.py b/src/mss/linux/__init__.py index 8426abf..e98424b 100644 --- a/src/mss/linux/__init__.py +++ b/src/mss/linux/__init__.py @@ -1,3 +1,5 @@ +"""GNU/Linux backend dispatcher for X11 screenshot implementations.""" + from typing import Any from mss.base import MSSBase @@ -7,14 +9,35 @@ def mss(backend: str = "default", **kwargs: Any) -> MSSBase: - """Factory returning a proper MSS class instance. + """Return a backend-specific MSS implementation for GNU/Linux. + + Selects and instantiates the appropriate X11 backend based on the + ``backend`` parameter. + + :param backend: Backend selector. Valid values: + + - ``"default"`` or ``"xshmgetimage"`` (default): XCB-based backend + using XShmGetImage with automatic fallback to XGetImage when MIT-SHM + is unavailable; see :py:class:`mss.linux.xshmgetimage.MSS`. + - ``"xgetimage"``: XCB-based backend using XGetImage; + see :py:class:`mss.linux.xgetimage.MSS`. + - ``"xlib"``: Legacy Xlib-based backend retained for environments + without working XCB libraries; see :py:class:`mss.linux.xlib.MSS`. - It examines the options provided, and chooses the most adapted MSS - class to take screenshots. It then proxies its arguments to the - class for instantiation. + .. versionadded:: 10.2.0 Prior to this version, the + :class:`mss.linux.xlib.MSS` implementation was the only available + backend. - Currently, the only option used is the "backend" flag. Future - versions will look at other options as well. + :param display: Optional keyword argument. Specifies an X11 display + string to connect to. The default is taken from the environment + variable :envvar:`DISPLAY`. + :type display: str | bytes | None + :param kwargs: Additional keyword arguments passed to the backend class. + :returns: An MSS backend implementation. + + .. versionadded:: 10.2.0 Prior to this version, this didn't exist: + the :func:`mss.linux.MSS` was a class equivalent to the current + :class:`mss.linux.xlib.MSS` implementation. """ backend = backend.lower() if backend == "xlib": @@ -39,4 +62,9 @@ class for instantiation. # Alias in upper-case for backward compatibility. This is a supported name in the docs. def MSS(*args, **kwargs) -> MSSBase: # type: ignore[no-untyped-def] # noqa: N802, ANN002, ANN003 + """Alias for :func:`mss.linux.mss.mss` for backward compatibility. + + .. versionchanged:: 10.2.0 Prior to this version, this was a class. + .. deprecated:: 10.2.0 Use :func:`mss.linux.mss` instead. + """ return mss(*args, **kwargs) diff --git a/src/mss/linux/base.py b/src/mss/linux/base.py index 489863c..f9921d5 100644 --- a/src/mss/linux/base.py +++ b/src/mss/linux/base.py @@ -23,25 +23,28 @@ class MSSXCBBase(MSSBase): """Base class for XCB-based screenshot implementations. - This class provides common XCB initialization and monitor detection logic - that can be shared across different XCB screenshot methods (XGetImage, - XShmGetImage, XComposite, etc.). + Provides common XCB initialization and monitor detection logic that can be + shared across different XCB screenshot methods (``XGetImage``, + ``XShmGetImage``, ``XComposite``, etc.). + + :param display: Optional keyword argument. + Specifies an X11 display string to connect to. The default is + taken from the environment variable :envvar:`DISPLAY`. + :type display: str | bytes | None + + .. seealso:: + :py:class:`mss.base.MSSBase` + Lists other parameters. """ def __init__(self, /, **kwargs: Any) -> None: # noqa: PLR0912 - """Initialize an XCB connection and validate the display configuration. - - Args: - **kwargs: Keyword arguments, including optional 'display' for X11 display string. - - Raises: - ScreenShotError: If the display configuration is not supported. - """ super().__init__(**kwargs) display = kwargs.get("display", b"") if not display: display = None + elif isinstance(display, str): + display = display.encode("utf-8") self.conn: xcb.Connection | None self.conn, pref_screen_num = xcb.connect(display) @@ -120,14 +123,19 @@ def __init__(self, /, **kwargs: Any) -> None: # noqa: PLR0912 msg = "Only visuals with BGRx ordering are supported" raise ScreenShotError(msg) - def close(self) -> None: + def _close_impl(self) -> None: """Close the XCB connection.""" if self.conn is not None: xcb.disconnect(self.conn) self.conn = None def _monitors_impl(self) -> None: - """Get positions of monitors. It will populate self._monitors.""" + """Populate monitor geometry information. + + Detects and appends monitor rectangles to ``self._monitors``. The first + entry represents the entire X11 root screen; subsequent entries, when + available, represent individual monitors reported by XRandR. + """ if self.conn is None: msg = "Cannot identify monitors while the connection is closed" raise ScreenShotError(msg) @@ -187,6 +195,11 @@ def _monitors_impl(self) -> None: # style is. def _cursor_impl_check_xfixes(self) -> bool: + """Check XFixes availability and version. + + :returns: ``True`` if the server provides XFixes with a compatible + version, otherwise ``False``. + """ if self.conn is None: msg = "Cannot take screenshot while the connection is closed" raise ScreenShotError(msg) @@ -201,7 +214,12 @@ def _cursor_impl_check_xfixes(self) -> bool: return (reply.major_version, reply.minor_version) >= (2, 0) def _cursor_impl(self) -> ScreenShot: - """Retrieve all cursor data. Pixels have to be RGBx.""" + """Capture the current cursor image. + + Pixels are returned in BGRA ordering. + + :returns: A screenshot object containing the cursor image and region. + """ if self.conn is None: msg = "Cannot take screenshot while the connection is closed" @@ -229,10 +247,14 @@ def _cursor_impl(self) -> ScreenShot: return self.cls_image(data, region) def _grab_impl_xgetimage(self, monitor: Monitor, /) -> ScreenShot: - """Retrieve all pixels from a monitor using GetImage. + """Retrieve pixels from a monitor using ``GetImage``. + + Used by the XGetImage backend and by the XShmGetImage backend in + fallback mode. - This is used by the XGetImage backend, and also the XShmGetImage - backend in fallback mode. + :param monitor: Monitor rectangle specifying ``left``, ``top``, + ``width``, and ``height`` to capture. + :returns: A screenshot object containing the captured region. """ if self.conn is None: diff --git a/src/mss/linux/xgetimage.py b/src/mss/linux/xgetimage.py index a41368c..ba3cb44 100644 --- a/src/mss/linux/xgetimage.py +++ b/src/mss/linux/xgetimage.py @@ -1,3 +1,14 @@ +"""XCB-based backend using the XGetImage request. + +This backend issues XCB ``GetImage`` requests and supports the RandR and +XFixes extensions when available for monitor enumeration and cursor capture. + +This backend will work on any X connection, but is slower than the xshmgetimage +backend. + +.. versionadded:: 10.2.0 +""" + from mss.models import Monitor from mss.screenshot import ScreenShot @@ -5,12 +16,11 @@ class MSS(MSSXCBBase): - """Multiple ScreenShots implementation for GNU/Linux. + """XCB backend using XGetImage requests on GNU/Linux. - This implementation is based on XCB, using the GetImage request. - It can optionally use some extensions: - * RandR: Enumerate individual monitors' sizes. - * XFixes: Including the cursor. + .. seealso:: + :py:class:`mss.linux.base.MSSXCBBase` + Lists constructor parameters. """ def _grab_impl(self, monitor: Monitor) -> ScreenShot: diff --git a/src/mss/linux/xlib.py b/src/mss/linux/xlib.py index 6b8208f..2419d59 100644 --- a/src/mss/linux/xlib.py +++ b/src/mss/linux/xlib.py @@ -1,5 +1,11 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. +"""Legacy Xlib-based GNU/Linux backend. + +This backend talks to X11 via Xlib and the Xrandr extension, and is retained +as a fallback when XCB backends are unavailable. Cursor capture uses XFixes +when available. + +.. versionadded:: 10.2.0 Prior to this version, this was available as + ``mss.linux.MSS``. """ from __future__ import annotations @@ -379,12 +385,20 @@ def _validate_x11( class MSS(MSSBase): """Multiple ScreenShots implementation for GNU/Linux. It uses intensively the Xlib and its Xrandr extension. + + :param display: Optional keyword argument. + Specifies an X11 display string to connect to. The default is + taken from the environment variable :envvar:`DISPLAY`. + :type display: str | bytes | None + + .. seealso:: + :py:class:`mss.base.MSSBase` + Lists other parameters. """ __slots__ = {"_handles", "xfixes", "xlib", "xrandr"} def __init__(self, /, **kwargs: Any) -> None: - """GNU/Linux initialisations.""" super().__init__(**kwargs) # Available thread-specific variables @@ -412,15 +426,18 @@ def __init__(self, /, **kwargs: Any) -> None: if not _X11: msg = "No X11 library found." raise ScreenShotError(msg) + #: :meta private: self.xlib = cdll.LoadLibrary(_X11) if not _XRANDR: msg = "No Xrandr extension found." raise ScreenShotError(msg) + #: :meta private: self.xrandr = cdll.LoadLibrary(_XRANDR) if self.with_cursor: if _XFIXES: + #: :meta private: self.xfixes = cdll.LoadLibrary(_XFIXES) else: self.with_cursor = False @@ -441,11 +458,12 @@ def __init__(self, /, **kwargs: Any) -> None: self._handles.drawable = self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display) - def close(self) -> None: + def _close_impl(self) -> None: # Clean-up if self._handles.display: - with lock: - self.xlib.XCloseDisplay(self._handles.display) + # We don't grab the lock, since MSSBase.close is holding + # it for us. + self.xlib.XCloseDisplay(self._handles.display) self._handles.display = None self._handles.drawable = None self._handles.root = None diff --git a/src/mss/linux/xshmgetimage.py b/src/mss/linux/xshmgetimage.py index 981f211..9858552 100644 --- a/src/mss/linux/xshmgetimage.py +++ b/src/mss/linux/xshmgetimage.py @@ -1,3 +1,16 @@ +"""XCB backend using MIT-SHM XShmGetImage with automatic fallback. + +This is the fastest Linux backend available, and will work in most common +cases. However, it will not work over remote X connections, such as over ssh. + +This implementation prefers shared-memory captures for performance and will +fall back to XGetImage when the MIT-SHM extension is unavailable or fails at +runtime. The fallback reason is exposed via ``shm_fallback_reason`` to aid +debugging. + +.. versionadded:: 10.2.0 +""" + from __future__ import annotations import enum @@ -17,18 +30,19 @@ class ShmStatus(enum.Enum): + """Availability of the MIT-SHM extension for this backend.""" + UNKNOWN = enum.auto() # Constructor says SHM *should* work, but we haven't seen a real GetImage succeed yet. AVAILABLE = enum.auto() # We've successfully used XShmGetImage at least once. UNAVAILABLE = enum.auto() # We know SHM GetImage is unusable; always use XGetImage. class MSS(MSSXCBBase): - """Multiple ScreenShots implementation for GNU/Linux. + """XCB backend using XShmGetImage with an automatic XGetImage fallback. - This implementation is based on XCB, using the ShmGetImage request. - If ShmGetImage fails, then this will fall back to using GetImage. - In that event, the reason for the fallback will be recorded in the - shm_fallback_reason attribute as a string, for debugging purposes. + .. seealso:: + :py:class:`mss.linux.base.MSSXCBBase` + Lists constructor parameters. """ def __init__(self, /, **kwargs: Any) -> None: @@ -43,7 +57,11 @@ def __init__(self, /, **kwargs: Any) -> None: # isn't available. The factory in linux/__init__.py could then catch that and switch to XGetImage. # The conditions under which the attach will succeed but the xcb_shm_get_image will fail are extremely # rare, and I haven't yet found any that also will work with xcb_get_image. + #: Whether we can use the MIT-SHM extensions for this connection. + #: This will not be ``AVAILABLE`` until at least one capture has succeeded. + #: It may be set to ``UNAVAILABLE`` sooner. self.shm_status: ShmStatus = self._setup_shm() + #: If MIT-SHM is unavailable, the reason why (for debugging purposes). self.shm_fallback_reason: str | None = None def _shm_report_issue(self, msg: str, *args: Any) -> None: @@ -114,9 +132,9 @@ def _setup_shm(self) -> ShmStatus: # noqa: PLR0911 return ShmStatus.UNKNOWN - def close(self) -> None: + def _close_impl(self) -> None: self._shutdown_shm() - super().close() + super()._close_impl() def _shutdown_shm(self) -> None: # It would be nice to also try to tell the server to detach the shmseg, but we might be in an error path diff --git a/src/mss/models.py b/src/mss/models.py index 665a41b..7b51553 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -1,6 +1,5 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. -""" +# This is part of the MSS Python's module. +# Source: https://github.com/BoboTiG/python-mss. from typing import Any, NamedTuple @@ -14,10 +13,14 @@ class Pos(NamedTuple): + #: The horizontal X coordinate of the position. left: int + #: The vertical Y coordinate of the position. top: int class Size(NamedTuple): + #: The horizontal X width. width: int + #: The vertical Y height. height: int diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 5bcf654..9443aba 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -1,6 +1,5 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. -""" +# This is part of the MSS Python's module. +# Source: https://github.com/BoboTiG/python-mss. from __future__ import annotations @@ -30,23 +29,34 @@ def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = #: Bytearray of the raw BGRA pixels retrieved by ctypes #: OS independent implementations. - self.raw = data + self.raw: bytearray = data #: NamedTuple of the screenshot coordinates. - self.pos = Pos(monitor["left"], monitor["top"]) + self.pos: Pos = Pos(monitor["left"], monitor["top"]) #: NamedTuple of the screenshot size. - self.size = Size(monitor["width"], monitor["height"]) if size is None else size + self.size: Size = Size(monitor["width"], monitor["height"]) if size is None else size def __repr__(self) -> str: return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>" @property def __array_interface__(self) -> dict[str, Any]: - """Numpy array interface support. - It uses raw data in BGRA form. + """NumPy array interface support. - See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html + This is used by NumPy, many SciPy projects, CuPy, PyTorch (via + ``torch.from_numpy``), TensorFlow (via ``tf.convert_to_tensor``), + JAX (via ``jax.numpy.asarray``), Pandas, scikit-learn, Matplotlib, + some OpenCV functions, and others. This allows you to pass a + :class:`ScreenShot` instance directly to these libraries without + needing to convert it first. + + This is in HWC order, with 4 channels (BGRA). + + .. seealso:: + + https://numpy.org/doc/stable/reference/arrays.interface.html + The NumPy array interface protocol specification """ return { "version": 3, @@ -63,33 +73,49 @@ def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, / @property def bgra(self) -> bytes: - """BGRA values from the BGRA raw pixels.""" - return bytes(self.raw) + """BGRx values from the BGRx raw pixels. - @property - def height(self) -> int: - """Convenient accessor to the height size.""" - return self.size.height + The format is a bytes object with BGRxBGRx... sequence. A specific + pixel can be accessed as + ``bgra[(y * width + x) * 4:(y * width + x) * 4 + 4].`` - @property - def left(self) -> int: - """Convenient accessor to the left position.""" - return self.pos.left + .. note:: + While the name is ``bgra``, the alpha channel may or may not be + valid. + """ + return bytes(self.raw) @property def pixels(self) -> Pixels: - """:return list: RGB tuples.""" + """RGB tuples. + + The format is a list of rows. Each row is a list of pixels. + Each pixel is a tuple of (R, G, B). + """ if not self.__pixels: rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4]) self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) return self.__pixels + def pixel(self, coord_x: int, coord_y: int) -> Pixel: + """Return the pixel value at a given position. + + :returns: A tuple of (R, G, B) values. + """ + try: + return self.pixels[coord_y][coord_x] + except IndexError as exc: + msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." + raise ScreenShotError(msg) from exc + @property def rgb(self) -> bytes: """Compute RGB values from the BGRA raw pixels. - :return bytes: RGB pixels. + The format is a bytes object with BGRBGR... sequence. A specific + pixel can be accessed as + ``rgb[(y * width + x) * 3:(y * width + x) * 3 + 3]``. """ if not self.__rgb: rgb = bytearray(self.height * self.width * 3) @@ -106,20 +132,17 @@ def top(self) -> int: """Convenient accessor to the top position.""" return self.pos.top + @property + def left(self) -> int: + """Convenient accessor to the left position.""" + return self.pos.left + @property def width(self) -> int: """Convenient accessor to the width size.""" return self.size.width - def pixel(self, coord_x: int, coord_y: int) -> Pixel: - """Returns the pixel value at a given position. - - :param int coord_x: The x coordinate. - :param int coord_y: The y coordinate. - :return tuple: The pixel value as (R, G, B). - """ - try: - return self.pixels[coord_y][coord_x] - except IndexError as exc: - msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." - raise ScreenShotError(msg) from exc + @property + def height(self) -> int: + """Convenient accessor to the height size.""" + return self.size.height diff --git a/src/mss/tools.py b/src/mss/tools.py index 9eb8b6f..ff90635 100644 --- a/src/mss/tools.py +++ b/src/mss/tools.py @@ -1,6 +1,5 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. -""" +# This is part of the MSS Python's module. +# Source: https://github.com/BoboTiG/python-mss. from __future__ import annotations @@ -21,6 +20,11 @@ def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: Pat :param tuple size: The (width, height) pair. :param int level: PNG compression level. :param str output: Output file name. + + .. versionadded:: 3.0.0 + + .. versionchanged:: 3.2.0 + Added the ``level`` keyword argument to control the PNG compression level. """ pack = struct.pack crc32 = zlib.crc32 diff --git a/src/mss/windows.py b/src/mss/windows.py index b84daa3..1aacc2e 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -1,5 +1,6 @@ -"""This is part of the MSS Python's module. -Source: https://github.com/BoboTiG/python-mss. +"""Windows GDI-based backend for MSS. + +Uses user32/gdi32 APIs to capture the desktop and enumerate monitors. """ from __future__ import annotations @@ -93,12 +94,19 @@ class BITMAPINFO(Structure): class MSS(MSSBase): - """Multiple ScreenShots implementation for Microsoft Windows.""" + """Multiple ScreenShots implementation for Microsoft Windows. + + This has no Windows-specific constructor parameters. + + .. seealso:: + + :py:class:`mss.base.MSSBase` + Lists constructor parameters. + """ __slots__ = {"_handles", "gdi32", "user32"} def __init__(self, /, **kwargs: Any) -> None: - """Windows initialisations.""" super().__init__(**kwargs) self.user32 = ctypes.WinDLL("user32") @@ -122,7 +130,7 @@ def __init__(self, /, **kwargs: Any) -> None: bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3] self._handles.bmi = bmi - def close(self) -> None: + def _close_impl(self) -> None: # Clean-up if self._handles.bmp: self.gdi32.DeleteObject(self._handles.bmp)