From 40865fa902d660089cf88a5c87438503eb5e8291 Mon Sep 17 00:00:00 2001 From: Joel Ray Holveck Date: Sat, 13 Dec 2025 22:06:49 -0800 Subject: [PATCH 1/7] Migrate to Sphinx autodoc. Existing API docs were moved into docstrings, and Sphinx's autodoc extension is used to generate the API reference automatically. --- docs/source/api.rst | 474 +++------------------------------- docs/source/conf.py | 7 + src/mss/__init__.py | 11 +- src/mss/base.py | 71 ++--- src/mss/darwin.py | 6 +- src/mss/exception.py | 12 +- src/mss/factory.py | 5 +- src/mss/linux/__init__.py | 34 ++- src/mss/linux/xgetimage.py | 14 +- src/mss/linux/xlib.py | 7 +- src/mss/linux/xshmgetimage.py | 19 +- src/mss/screenshot.py | 5 +- src/mss/tools.py | 10 +- src/mss/windows.py | 12 +- 14 files changed, 178 insertions(+), 509 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index a385d56f..dc95dbe7 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 -======= +Factory Helpers +=============== -.. module:: mss.base +.. automodule:: mss.factory -.. attribute:: lock +Screenshot Objects +================== - .. versionadded:: 6.0.0 +.. automodule:: mss.screenshot -.. class:: MSSBase +Base Classes +============ - The parent's class for every OS implementation. +.. automodule:: mss.base - .. attribute:: cls_image +Tools +===== - .. attribute:: compression_level +.. automodule:: mss.tools - 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 - - The screenshot's height. - - :rtype: int +.. automodule:: mss.exception - .. attribute:: left +Data Models +=========== - The screenshot's left coordinate. +.. automodule:: mss.models - :rtype: int +Platform Backends +================= - .. attribute:: pixels +macOS Backend +------------- - List of row tuples that contain RGB tuples. +.. automodule:: mss.darwin - :rtype: list[tuple(tuple(int, int, int), ...)] +GNU/Linux Dispatcher +-------------------- - .. attribute:: pos +.. automodule:: mss.linux - The screenshot's coordinates. +GNU/Linux Xlib Backend +---------------------- - :rtype: :py:func:`collections.namedtuple()` +.. automodule:: mss.linux.xlib - .. attribute:: rgb +GNU/Linux XGetImage Backend +--------------------------- - Computed RGB values from the BGRA raw pixels. +.. automodule:: mss.linux.xgetimage - :rtype: bytes +GNU/Linux 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 a0d9b993..68073892 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,6 +11,7 @@ # -- General configuration ------------------------------------------------ extensions = [ + "sphinx.ext.autodoc", "sphinx_copybutton", "sphinx.ext.intersphinx", "sphinx_new_tab_link", @@ -29,6 +30,12 @@ release = "latest" language = "en" todo_include_todos = True +autodoc_member_order = "bysource" +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, +} # -- Options for HTML output ---------------------------------------------- diff --git a/src/mss/__init__.py b/src/mss/__init__.py index 21ae18f8..ecac4b41 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 2abfcbd3..74e9d41b 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,6 +34,9 @@ UTC = timezone.utc +#: Global lock protecting access to platform screenshot calls. +#: +#: .. versionadded:: 6.0.0 lock = Lock() OPAQUE = 255 @@ -57,8 +59,26 @@ def __init__( # Mac only max_displays: int = 32, # noqa: ARG002 ) -> None: + """Initialize common MSS options. + + :param backend: Backend selector (only ``"default"`` is valid on this platform). + :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. + """ 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 = [] # If there isn't a factory that removed the "backend" argument, make sure that it was set to "default". @@ -101,7 +121,7 @@ def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot: :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 +146,14 @@ 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 +171,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 5d723b98..e03be54d 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 diff --git a/src/mss/exception.py b/src/mss/exception.py index 7fdf2113..5c8b08f3 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 933310d7..e42814d9 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 diff --git a/src/mss/linux/__init__.py b/src/mss/linux/__init__.py index 8426abfd..71756740 100644 --- a/src/mss/linux/__init__.py +++ b/src/mss/linux/__init__.py @@ -1,3 +1,24 @@ +"""GNU/Linux backend dispatcher. + +This module picks the appropriate X11 backend implementation based on the +``backend`` option. Available values: + +- ``"default"`` or ``"xshmgetimage"``: XCB-based backend using XShmGetImage + with automatic fallback to XGetImage when MIT-SHM is unavailable (default) +- ``"xgetimage"``: XCB-based backend using XGetImage +- ``"xlib"``: legacy Xlib-based backend retained for environments without + working XCB libraries + +Keyword arguments are forwarded to the selected backend. The ``display`` +argument (e.g., ``":0.0"``) targets a specific X server when needed. + +.. versionadded:: 10.2.0 + The ``backend`` selector and the upper-case :func:`MSS` alias. + +The top-level :func:`mss` function proxies keyword arguments to the selected +backend class and returns an :class:`mss.base.MSSBase` implementation. +""" + from typing import Any from mss.base import MSSBase @@ -7,14 +28,15 @@ def mss(backend: str = "default", **kwargs: Any) -> MSSBase: - """Factory returning a proper MSS class instance. + """Return a backend-specific MSS implementation. - 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. + The ``backend`` flag selects the implementation: - Currently, the only option used is the "backend" flag. Future - versions will look at other options as well. + - ``"default"``/``"xshmgetimage"`` (default): XCB backend using + XShmGetImage with automatic fallback to XGetImage + - ``"xgetimage"``: XCB backend using XGetImage + - ``"xlib"``: traditional Xlib backend retained for environments without + working XCB libraries """ backend = backend.lower() if backend == "xlib": diff --git a/src/mss/linux/xgetimage.py b/src/mss/linux/xgetimage.py index a41368c6..dff729c7 100644 --- a/src/mss/linux/xgetimage.py +++ b/src/mss/linux/xgetimage.py @@ -1,3 +1,9 @@ +"""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. +""" + from mss.models import Monitor from mss.screenshot import ScreenShot @@ -5,12 +11,10 @@ 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. + Uses RandR (for monitor enumeration) and XFixes (for cursor capture) when + available. """ def _grab_impl(self, monitor: Monitor) -> ScreenShot: diff --git a/src/mss/linux/xlib.py b/src/mss/linux/xlib.py index 6b8208f0..9f9f085f 100644 --- a/src/mss/linux/xlib.py +++ b/src/mss/linux/xlib.py @@ -1,5 +1,8 @@ -"""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. """ from __future__ import annotations diff --git a/src/mss/linux/xshmgetimage.py b/src/mss/linux/xshmgetimage.py index 981f2116..384a64f6 100644 --- a/src/mss/linux/xshmgetimage.py +++ b/src/mss/linux/xshmgetimage.py @@ -1,3 +1,11 @@ +"""XCB backend using MIT-SHM XShmGetImage with automatic fallback. + +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. +""" + from __future__ import annotations import enum @@ -17,18 +25,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. + The ``shm_status`` attribute tracks whether shared memory is available, + and ``shm_fallback_reason`` records why a fallback occurred when MIT-SHM + cannot be used. """ def __init__(self, /, **kwargs: Any) -> None: diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 5bcf654b..6eaa5f44 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 diff --git a/src/mss/tools.py b/src/mss/tools.py index 9eb8b6f7..ff90635e 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 b84daa3e..0d84dd02 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -1,12 +1,13 @@ -"""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 import ctypes import sys -from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p +from ctypes import POINTER, Structure, c_int, c_void_p from ctypes.wintypes import ( BOOL, DOUBLE, @@ -23,6 +24,11 @@ UINT, WORD, ) + +try: + from ctypes import WINFUNCTYPE +except ImportError: # pragma: no cover - non-Windows doc builds + from ctypes import CFUNCTYPE as WINFUNCTYPE from threading import local from typing import TYPE_CHECKING, Any From 13e99ef540e407d271e0f201cc53aef24d98db2d Mon Sep 17 00:00:00 2001 From: Joel Ray Holveck Date: Sat, 20 Dec 2025 21:50:59 -0800 Subject: [PATCH 2/7] Move close implementations to _close_impl. Without this, autodoc was adding the close method to all the subclasses of MSSBase that implemented it. It didn't add things like grab and such, since the subclasses didn't implement it. While I'm at it, also make the close method safe to call multiple times. --- src/mss/base.py | 39 +++++++++++++++++++++++++++++------ src/mss/linux/base.py | 2 +- src/mss/linux/xlib.py | 2 +- src/mss/linux/xshmgetimage.py | 3 +-- src/mss/windows.py | 2 +- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/mss/base.py b/src/mss/base.py index 74e9d41b..867cb376 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -5,7 +5,7 @@ from abc import ABCMeta, abstractmethod from datetime import datetime -from threading import Lock +from threading import RLock from typing import TYPE_CHECKING, Any from mss.exception import ScreenShotError @@ -37,7 +37,7 @@ #: Global lock protecting access to platform screenshot calls. #: #: .. versionadded:: 6.0.0 -lock = Lock() +lock = RLock() OPAQUE = 255 @@ -45,7 +45,7 @@ class MSSBase(metaclass=ABCMeta): """This class will be overloaded by a system specific one.""" - __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"} + __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor", "_closed"} def __init__( self, @@ -81,6 +81,7 @@ def __init__( #: .. 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": @@ -111,13 +112,39 @@ 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 use 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. diff --git a/src/mss/linux/base.py b/src/mss/linux/base.py index 489863ce..8bdf35f9 100644 --- a/src/mss/linux/base.py +++ b/src/mss/linux/base.py @@ -120,7 +120,7 @@ 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) diff --git a/src/mss/linux/xlib.py b/src/mss/linux/xlib.py index 9f9f085f..75bb13e8 100644 --- a/src/mss/linux/xlib.py +++ b/src/mss/linux/xlib.py @@ -444,7 +444,7 @@ 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: diff --git a/src/mss/linux/xshmgetimage.py b/src/mss/linux/xshmgetimage.py index 384a64f6..1a3d8ab8 100644 --- a/src/mss/linux/xshmgetimage.py +++ b/src/mss/linux/xshmgetimage.py @@ -123,9 +123,8 @@ def _setup_shm(self) -> ShmStatus: # noqa: PLR0911 return ShmStatus.UNKNOWN - def close(self) -> None: + def _close_impl(self) -> None: self._shutdown_shm() - super().close() 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/windows.py b/src/mss/windows.py index 0d84dd02..34aca1b5 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -128,7 +128,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) From f888502c2348cf00f80d3671909bf9678afd298b Mon Sep 17 00:00:00 2001 From: Joel Ray Holveck Date: Sat, 20 Dec 2025 22:56:42 -0800 Subject: [PATCH 3/7] Fix bugs in previous autodoc commits. First, we previously patched ctypes.WINFUNCTYPE in any time mss.windows was imported, so that autodoc could import mss.windows. Instead, move that to just when we're running Sphinx. Second, when I added _close_impl, I accidentally deleted the super() call in xshmgetimage. This mean that it never closes connections, and exhausts the server's client limit. Finally, in a previous commit, I tested changing mss.lock from a Lock to an RLock while I was dealing with the fact that MSSBase.close holds the lock, and the xlib implementation grabs the global lock. That was not a great solution, so I backed that out. Instead, the xlib implementation just trusts that it's run under the lock. However, a better implementation might be to create a per-object lock in MSSBase to protect _closing, instead of using the course-grained global lock. --- docs/source/conf.py | 6 ++++++ src/mss/base.py | 4 ++-- src/mss/linux/xlib.py | 5 +++-- src/mss/linux/xshmgetimage.py | 1 + src/mss/windows.py | 6 +----- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 68073892..38b527e5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,6 +6,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) +import ctypes + import mss # -- General configuration ------------------------------------------------ @@ -37,6 +39,10 @@ "show-inheritance": True, } +# Monkey-patch WINFUNCTYPE into ctypes, so that we can import +# mss.windows while building the documentation. +ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE + # -- Options for HTML output ---------------------------------------------- diff --git a/src/mss/base.py b/src/mss/base.py index 867cb376..ed56b3aa 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -5,7 +5,7 @@ from abc import ABCMeta, abstractmethod from datetime import datetime -from threading import RLock +from threading import Lock from typing import TYPE_CHECKING, Any from mss.exception import ScreenShotError @@ -37,7 +37,7 @@ #: Global lock protecting access to platform screenshot calls. #: #: .. versionadded:: 6.0.0 -lock = RLock() +lock = Lock() OPAQUE = 255 diff --git a/src/mss/linux/xlib.py b/src/mss/linux/xlib.py index 75bb13e8..54c8d57d 100644 --- a/src/mss/linux/xlib.py +++ b/src/mss/linux/xlib.py @@ -447,8 +447,9 @@ def __init__(self, /, **kwargs: Any) -> 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 1a3d8ab8..a6f99c53 100644 --- a/src/mss/linux/xshmgetimage.py +++ b/src/mss/linux/xshmgetimage.py @@ -125,6 +125,7 @@ def _setup_shm(self) -> ShmStatus: # noqa: PLR0911 def _close_impl(self) -> None: self._shutdown_shm() + 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/windows.py b/src/mss/windows.py index 34aca1b5..afce5d79 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -7,7 +7,7 @@ import ctypes import sys -from ctypes import POINTER, Structure, c_int, c_void_p +from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p from ctypes.wintypes import ( BOOL, DOUBLE, @@ -25,10 +25,6 @@ WORD, ) -try: - from ctypes import WINFUNCTYPE -except ImportError: # pragma: no cover - non-Windows doc builds - from ctypes import CFUNCTYPE as WINFUNCTYPE from threading import local from typing import TYPE_CHECKING, Any From 3fae659d296279143fb89a26a3f7c0cba1f4aedb Mon Sep 17 00:00:00 2001 From: Joel Holveck Date: Sat, 20 Dec 2025 23:48:56 -0800 Subject: [PATCH 4/7] Typo fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mickaël Schoentgen --- src/mss/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mss/base.py b/src/mss/base.py index ed56b3aa..01213952 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -123,7 +123,7 @@ def close(self) -> None: """Clean up. This releases resources that MSS may be using. Once the MSS - object is closed, it may not be use used again. + object is closed, it may not be used again. It is safe to call this multiple times; multiple calls have no effect. From b3d0fd02dbe1da45fd19b1fa00980eb7a877f85b Mon Sep 17 00:00:00 2001 From: Joel Holveck Date: Sun, 21 Dec 2025 08:47:28 +0000 Subject: [PATCH 5/7] Additional doc clarifications and formatting improvements This adds extra detail to some docstrings, and brings formatting in line with Sphinx conventions. --- docs/source/api.rst | 5 --- docs/source/developers.rst | 2 +- src/mss/base.py | 2 +- src/mss/factory.py | 9 ++++ src/mss/linux/__init__.py | 57 ++++++++++++------------ src/mss/linux/base.py | 42 ++++++++++++------ src/mss/linux/xgetimage.py | 11 ++--- src/mss/linux/xlib.py | 6 +++ src/mss/linux/xshmgetimage.py | 16 ++++--- src/mss/models.py | 5 +-- src/mss/screenshot.py | 81 ++++++++++++++++++++++------------- src/mss/windows.py | 1 - 12 files changed, 145 insertions(+), 92 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index dc95dbe7..84212794 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -7,11 +7,6 @@ Core Package .. automodule:: mss -Factory Helpers -=============== - -.. automodule:: mss.factory - Screenshot Objects ================== diff --git a/docs/source/developers.rst b/docs/source/developers.rst index 1d4636f3..160e0d69 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/base.py b/src/mss/base.py index 01213952..e14fb986 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -45,7 +45,7 @@ class MSSBase(metaclass=ABCMeta): """This class will be overloaded by a system specific one.""" - __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor", "_closed"} + __slots__ = {"_closed", "_monitors", "cls_image", "compression_level", "with_cursor"} def __init__( self, diff --git a/src/mss/factory.py b/src/mss/factory.py index e42814d9..e9bf40a8 100644 --- a/src/mss/factory.py +++ b/src/mss/factory.py @@ -17,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 71756740..f0f43ddc 100644 --- a/src/mss/linux/__init__.py +++ b/src/mss/linux/__init__.py @@ -1,23 +1,4 @@ -"""GNU/Linux backend dispatcher. - -This module picks the appropriate X11 backend implementation based on the -``backend`` option. Available values: - -- ``"default"`` or ``"xshmgetimage"``: XCB-based backend using XShmGetImage - with automatic fallback to XGetImage when MIT-SHM is unavailable (default) -- ``"xgetimage"``: XCB-based backend using XGetImage -- ``"xlib"``: legacy Xlib-based backend retained for environments without - working XCB libraries - -Keyword arguments are forwarded to the selected backend. The ``display`` -argument (e.g., ``":0.0"``) targets a specific X server when needed. - -.. versionadded:: 10.2.0 - The ``backend`` selector and the upper-case :func:`MSS` alias. - -The top-level :func:`mss` function proxies keyword arguments to the selected -backend class and returns an :class:`mss.base.MSSBase` implementation. -""" +"""GNU/Linux backend dispatcher for X11 screenshot implementations.""" from typing import Any @@ -28,15 +9,32 @@ def mss(backend: str = "default", **kwargs: Any) -> MSSBase: - """Return a backend-specific MSS implementation. + """Return a backend-specific MSS implementation for GNU/Linux. + + Selects and instantiates the appropriate X11 backend based on the + ``backend`` parameter. Keyword arguments are forwarded to the selected + backend class. - The ``backend`` flag selects the implementation: + :param backend: Backend selector. Valid values: - - ``"default"``/``"xshmgetimage"`` (default): XCB backend using - XShmGetImage with automatic fallback to XGetImage - - ``"xgetimage"``: XCB backend using XGetImage - - ``"xlib"``: traditional Xlib backend retained for environments without - working XCB libraries + - ``"default"`` or ``"xshmgetimage"`` (default): XCB-based backend + using XShmGetImage with automatic fallback to XGetImage when MIT-SHM + is unavailable + - ``"xgetimage"``: XCB-based backend using XGetImage + - ``"xlib"``: Legacy Xlib-based backend retained for environments + without working XCB libraries + + .. versionadded:: 10.2.0 Prior to this version, the + :class:`mss.linux.xlib.MSS` implementation was the only available + backend. + + :param kwargs: Keyword arguments forwarded to the backend. Common options + include ``display`` (e.g., ``":0.0"``) to target a specific X server. + :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": @@ -61,4 +59,9 @@ def mss(backend: str = "default", **kwargs: Any) -> MSSBase: # 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 8bdf35f9..79e40dc9 100644 --- a/src/mss/linux/base.py +++ b/src/mss/linux/base.py @@ -23,19 +23,16 @@ 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.). """ 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. + :param kwargs: Optional keyword arguments. Recognized key ``display`` + specifies an X11 display string (bytes) to connect to. """ super().__init__(**kwargs) @@ -127,7 +124,12 @@ def _close_impl(self) -> None: 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 +189,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 +208,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 +241,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 dff729c7..ba2d405b 100644 --- a/src/mss/linux/xgetimage.py +++ b/src/mss/linux/xgetimage.py @@ -2,6 +2,11 @@ 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 @@ -11,11 +16,7 @@ class MSS(MSSXCBBase): - """XCB backend using XGetImage requests on GNU/Linux. - - Uses RandR (for monitor enumeration) and XFixes (for cursor capture) when - available. - """ + """XCB backend using XGetImage requests on GNU/Linux.""" def _grab_impl(self, monitor: Monitor) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGBX.""" diff --git a/src/mss/linux/xlib.py b/src/mss/linux/xlib.py index 54c8d57d..d27ea4a9 100644 --- a/src/mss/linux/xlib.py +++ b/src/mss/linux/xlib.py @@ -3,6 +3,9 @@ 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 @@ -415,15 +418,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 diff --git a/src/mss/linux/xshmgetimage.py b/src/mss/linux/xshmgetimage.py index a6f99c53..9db112ca 100644 --- a/src/mss/linux/xshmgetimage.py +++ b/src/mss/linux/xshmgetimage.py @@ -1,9 +1,14 @@ """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 @@ -33,12 +38,7 @@ class ShmStatus(enum.Enum): class MSS(MSSXCBBase): - """XCB backend using XShmGetImage with an automatic XGetImage fallback. - - The ``shm_status`` attribute tracks whether shared memory is available, - and ``shm_fallback_reason`` records why a fallback occurred when MIT-SHM - cannot be used. - """ + """XCB backend using XShmGetImage with an automatic XGetImage fallback.""" def __init__(self, /, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -52,7 +52,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: diff --git a/src/mss/models.py b/src/mss/models.py index 665a41bc..4435c836 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 diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 6eaa5f44..6fdb1188 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -29,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, @@ -62,33 +73,46 @@ 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) @@ -105,20 +129,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/windows.py b/src/mss/windows.py index afce5d79..cc5f4c67 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -24,7 +24,6 @@ UINT, WORD, ) - from threading import local from typing import TYPE_CHECKING, Any From 211f50c334fc3c4028cafe48135c5307768f2774 Mon Sep 17 00:00:00 2001 From: Joel Ray Holveck Date: Sun, 21 Dec 2025 16:48:19 -0800 Subject: [PATCH 6/7] Clean up the way constructor parameters are handled. Most constructor parameters weren't being documented by autodoc, since __init__ wasn't documented. If we added the __init__ docstrings to the class documentation (autoclass_source="both"), then that puts an awkward "Initialize a ScreenShot object" or similar sentence in the class constructor documentation in the rendered output. Instead, we put the __init__ parameter docs in the class docstring, and removed __init__ docstrings. Added "See also" sections to make looking up constructor parameters across __init__(**kwargs) chains a little more obvious. Added other requested changes: * Added mss.darwin.IMAGE_OPTIONS to the docs * Added docstring for mss.darwin.MSS.max_displays instance variable * Added docstrings to NamedTuple classes in mss.models * Added formatting for code samples in ScreenShot.bgra and rgb --- docs/source/api.rst | 13 +++++++++---- docs/source/conf.py | 2 +- src/mss/base.py | 25 +++++++++++++------------ src/mss/darwin.py | 18 ++++++++++++++---- src/mss/linux/__init__.py | 17 ++++++++++------- src/mss/linux/base.py | 16 +++++++++++----- src/mss/linux/xgetimage.py | 7 ++++++- src/mss/linux/xlib.py | 10 +++++++++- src/mss/linux/xshmgetimage.py | 7 ++++++- src/mss/models.py | 4 ++++ src/mss/screenshot.py | 9 ++++++--- src/mss/windows.py | 11 +++++++++-- 12 files changed, 98 insertions(+), 41 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 84212794..a566783c 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -50,13 +50,18 @@ GNU/Linux Xlib Backend .. automodule:: mss.linux.xlib -GNU/Linux XGetImage Backend ---------------------------- +GNU/Linux XCB Base +------------------ + +.. automodule:: mss.linux.base + +GNU/Linux XCB XGetImage Backend +------------------------------- .. automodule:: mss.linux.xgetimage -GNU/Linux XShmGetImage Backend ------------------------------- +GNU/Linux XCB XShmGetImage Backend +---------------------------------- .. automodule:: mss.linux.xshmgetimage diff --git a/docs/source/conf.py b/docs/source/conf.py index 38b527e5..9f49912b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,7 +41,7 @@ # Monkey-patch WINFUNCTYPE into ctypes, so that we can import # mss.windows while building the documentation. -ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE +ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE # type:ignore[attr-defined] # -- Options for HTML output ---------------------------------------------- diff --git a/src/mss/base.py b/src/mss/base.py index e14fb986..1a984d16 100644 --- a/src/mss/base.py +++ b/src/mss/base.py @@ -43,7 +43,17 @@ class MSSBase(metaclass=ABCMeta): - """This class will be overloaded by a system specific one.""" + """Base class for all Multiple ScreenShots implementations. + + :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"} @@ -59,17 +69,6 @@ def __init__( # Mac only max_displays: int = 32, # noqa: ARG002 ) -> None: - """Initialize common MSS options. - - :param backend: Backend selector (only ``"default"`` is valid on this platform). - :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. - """ 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). @@ -173,10 +172,12 @@ 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) 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 diff --git a/src/mss/darwin.py b/src/mss/darwin.py index e03be54d..77fe50b9 100644 --- a/src/mss/darwin.py +++ b/src/mss/darwin.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: # pragma: nocover from mss.models import CFunctions, Monitor -__all__ = ("MSS",) +__all__ = ("IMAGE_OPTIONS", "MSS") BACKENDS = ["default"] @@ -29,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]: @@ -94,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() @@ -119,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/linux/__init__.py b/src/mss/linux/__init__.py index f0f43ddc..e98424b7 100644 --- a/src/mss/linux/__init__.py +++ b/src/mss/linux/__init__.py @@ -12,24 +12,27 @@ def mss(backend: str = "default", **kwargs: Any) -> MSSBase: """Return a backend-specific MSS implementation for GNU/Linux. Selects and instantiates the appropriate X11 backend based on the - ``backend`` parameter. Keyword arguments are forwarded to the selected - backend class. + ``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 - - ``"xgetimage"``: XCB-based backend using XGetImage + 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 + without working XCB libraries; see :py:class:`mss.linux.xlib.MSS`. .. versionadded:: 10.2.0 Prior to this version, the :class:`mss.linux.xlib.MSS` implementation was the only available backend. - :param kwargs: Keyword arguments forwarded to the backend. Common options - include ``display`` (e.g., ``":0.0"``) to target a specific X server. + :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: diff --git a/src/mss/linux/base.py b/src/mss/linux/base.py index 79e40dc9..f9921d53 100644 --- a/src/mss/linux/base.py +++ b/src/mss/linux/base.py @@ -26,19 +26,25 @@ class MSSXCBBase(MSSBase): 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. - - :param kwargs: Optional keyword arguments. Recognized key ``display`` - specifies an X11 display string (bytes) to connect to. - """ 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) diff --git a/src/mss/linux/xgetimage.py b/src/mss/linux/xgetimage.py index ba2d405b..ba3cb44a 100644 --- a/src/mss/linux/xgetimage.py +++ b/src/mss/linux/xgetimage.py @@ -16,7 +16,12 @@ class MSS(MSSXCBBase): - """XCB backend using XGetImage requests on GNU/Linux.""" + """XCB backend using XGetImage requests on GNU/Linux. + + .. seealso:: + :py:class:`mss.linux.base.MSSXCBBase` + Lists constructor parameters. + """ def _grab_impl(self, monitor: Monitor) -> ScreenShot: """Retrieve all pixels from a monitor. Pixels have to be RGBX.""" diff --git a/src/mss/linux/xlib.py b/src/mss/linux/xlib.py index d27ea4a9..2419d591 100644 --- a/src/mss/linux/xlib.py +++ b/src/mss/linux/xlib.py @@ -385,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 diff --git a/src/mss/linux/xshmgetimage.py b/src/mss/linux/xshmgetimage.py index 9db112ca..98585529 100644 --- a/src/mss/linux/xshmgetimage.py +++ b/src/mss/linux/xshmgetimage.py @@ -38,7 +38,12 @@ class ShmStatus(enum.Enum): class MSS(MSSXCBBase): - """XCB backend using XShmGetImage with an automatic XGetImage fallback.""" + """XCB backend using XShmGetImage with an automatic XGetImage fallback. + + .. seealso:: + :py:class:`mss.linux.base.MSSXCBBase` + Lists constructor parameters. + """ def __init__(self, /, **kwargs: Any) -> None: super().__init__(**kwargs) diff --git a/src/mss/models.py b/src/mss/models.py index 4435c836..6ac580da 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -13,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 hoirizontal X width. width: int + #: The vertical Y height. height: int diff --git a/src/mss/screenshot.py b/src/mss/screenshot.py index 6fdb1188..9443aba4 100644 --- a/src/mss/screenshot.py +++ b/src/mss/screenshot.py @@ -76,10 +76,12 @@ def bgra(self) -> bytes: """BGRx values from the BGRx raw pixels. 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]. + pixel can be accessed as + ``bgra[(y * width + x) * 4:(y * width + x) * 4 + 4].`` .. note:: - While the name is `bgra`, the alpha channel may or may not be valid. + While the name is ``bgra``, the alpha channel may or may not be + valid. """ return bytes(self.raw) @@ -112,7 +114,8 @@ def rgb(self) -> bytes: """Compute RGB values from the BGRA raw 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]. + 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) diff --git a/src/mss/windows.py b/src/mss/windows.py index cc5f4c67..1aacc2e6 100644 --- a/src/mss/windows.py +++ b/src/mss/windows.py @@ -94,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") From 320b7289b656751a859238ea6a0aee313d6e1a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= Date: Mon, 22 Dec 2025 02:45:53 +0100 Subject: [PATCH 7/7] Update src/mss/models.py --- src/mss/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mss/models.py b/src/mss/models.py index 6ac580da..7b51553e 100644 --- a/src/mss/models.py +++ b/src/mss/models.py @@ -20,7 +20,7 @@ class Pos(NamedTuple): class Size(NamedTuple): - #: The hoirizontal X width. + #: The horizontal X width. width: int #: The vertical Y height. height: int