├── src ├── mss │ ├── py.typed │ ├── models.py │ ├── exception.py │ ├── linux │ │ ├── xgetimage.py │ │ ├── __init__.py │ │ ├── xcb.py │ │ └── xshmgetimage.py │ ├── __init__.py │ ├── factory.py │ ├── tools.py │ ├── __main__.py │ ├── screenshot.py │ ├── darwin.py │ ├── windows.py │ └── base.py ├── tests │ ├── __init__.py │ ├── third_party │ │ ├── __init__.py │ │ ├── test_numpy.py │ │ └── test_pil.py │ ├── res │ │ └── monitor-1024x768.raw.zip │ ├── test_bgra_to_rgb.py │ ├── test_cls_image.py │ ├── test_find_monitors.py │ ├── test_get_pixels.py │ ├── test_tools.py │ ├── test_issue_220.py │ ├── bench_general.py │ ├── bench_bgra2rgb.py │ ├── test_macos.py │ ├── test_save.py │ ├── test_windows.py │ ├── conftest.py │ ├── test_leaks.py │ ├── test_setup.py │ ├── test_xcb.py │ ├── test_implementation.py │ └── test_gnu_linux.py └── xcbproto │ └── README.md ├── .well-known └── funding-manifest-urls ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── release.yml │ └── tests.yml ├── docs ├── icon.png └── source │ ├── examples │ ├── linux_display_keyword.py │ ├── part_of_screen.py │ ├── linux_xshm_backend.py │ ├── callback.py │ ├── pil.py │ ├── custom_cls_image.py │ ├── from_pil_tuple.py │ ├── pil_pixels.py │ ├── part_of_screen_monitor_2.py │ ├── opencv_numpy.py │ ├── fps_multiprocessing.py │ └── fps.py │ ├── installation.rst │ ├── support.rst │ ├── api.rst │ ├── developers.rst │ ├── conf.py │ ├── index.rst │ ├── where.rst │ ├── usage.rst │ └── examples.rst ├── .gitignore ├── .readthedocs.yml ├── check.sh ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTORS.md ├── .devcontainer └── devcontainer.json ├── LICENSE.txt ├── README.md ├── CODE_OF_CONDUCT.md ├── pyproject.toml ├── CHANGES.md └── demos └── tinytv-stream-simple.py /src/mss/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/third_party/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://www.tiger-222.fr/funding.json 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: BoboTiG 2 | polar: tiger-222 3 | issuehunt: BoboTiG 4 | -------------------------------------------------------------------------------- /docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/HEAD/docs/icon.png -------------------------------------------------------------------------------- /src/tests/res/monitor-1024x768.raw.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/HEAD/src/tests/res/monitor-1024x768.raw.zip -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Changes proposed in this PR 2 | 3 | - Fixes # 4 | - ... 5 | - ... 6 | 7 | It is **very** important to keep up to date tests and documentation. 8 | 9 | - [ ] Tests added/updated 10 | - [ ] Documentation updated 11 | 12 | Is your code right? 13 | 14 | - [ ] `./check.sh` passed 15 | -------------------------------------------------------------------------------- /docs/source/examples/linux_display_keyword.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Usage example with a specific display. 5 | """ 6 | 7 | import mss 8 | 9 | with mss.mss(display=":0.0") as sct: 10 | for filename in sct.save(): 11 | print(filename) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files 2 | .coverage 3 | *.doctree 4 | .DS_Store 5 | *.orig 6 | *.jpg 7 | /*.png 8 | *.png.old 9 | *.pickle 10 | *.pyc 11 | 12 | # Folders 13 | build/ 14 | .cache/ 15 | dist/ 16 | docs_out/ 17 | *.egg-info/ 18 | .idea/ 19 | .pytest_cache/ 20 | docs/output/ 21 | .mypy_cache/ 22 | __pycache__/ 23 | ruff_cache/ 24 | venv/ 25 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-24.04 5 | tools: 6 | python: "3.13" 7 | 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | fail_on_warning: true 11 | 12 | formats: 13 | - htmlzip 14 | - epub 15 | - pdf 16 | 17 | python: 18 | install: 19 | - method: pip 20 | path: . 21 | extra_requirements: 22 | - docs 23 | -------------------------------------------------------------------------------- /check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Small script to ensure quality checks pass before submitting a commit/PR. 4 | # 5 | set -eu 6 | 7 | python -m ruff format docs src 8 | python -m ruff check --fix --unsafe-fixes docs src 9 | 10 | # "--platform win32" to not fail on ctypes.windll (it does not affect the overall check on other OSes) 11 | python -m mypy --platform win32 src docs/source/examples 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # GitHub Actions 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | labels: 9 | - dependencies 10 | - QA/CI 11 | 12 | # Python requirements 13 | - package-ecosystem: pip 14 | directory: / 15 | schedule: 16 | interval: weekly 17 | assignees: 18 | - BoboTiG 19 | labels: 20 | - dependencies 21 | -------------------------------------------------------------------------------- /src/tests/third_party/test_numpy.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from collections.abc import Callable 6 | 7 | import pytest 8 | 9 | from mss.base import MSSBase 10 | 11 | np = pytest.importorskip("numpy", reason="Numpy module not available.") 12 | 13 | 14 | def test_numpy(mss_impl: Callable[..., MSSBase]) -> None: 15 | box = {"top": 0, "left": 0, "width": 10, "height": 10} 16 | with mss_impl() as sct: 17 | img = np.array(sct.grab(box)) 18 | assert len(img) == 10 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "charliermarsh.ruff", 7 | "ms-python.mypy-type-checker", 8 | "ms-python.python", 9 | "ms-python.vscode-pylance", 10 | "ms-python.vscode-python-envs", 11 | ], 12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 13 | "unwantedRecommendations": [] 14 | } 15 | -------------------------------------------------------------------------------- /docs/source/examples/part_of_screen.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Example to capture part of the screen. 5 | """ 6 | 7 | import mss 8 | import mss.tools 9 | 10 | with mss.mss() as sct: 11 | # The screen part to capture 12 | monitor = {"top": 160, "left": 160, "width": 160, "height": 135} 13 | output = "sct-{top}x{left}_{width}x{height}.png".format(**monitor) 14 | 15 | # Grab the data 16 | sct_img = sct.grab(monitor) 17 | 18 | # Save to the picture file 19 | mss.tools.to_png(sct_img.rgb, sct_img.size, output=output) 20 | print(output) 21 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | Recommended Way 8 | =============== 9 | 10 | Quite simple:: 11 | 12 | $ python -m pip install -U --user mss 13 | 14 | Conda Package 15 | ------------- 16 | 17 | The module is also available from Conda:: 18 | 19 | $ conda install -c conda-forge python-mss 20 | 21 | From Sources 22 | ============ 23 | 24 | Alternatively, you can get a copy of the module from GitHub:: 25 | 26 | $ git clone https://github.com/BoboTiG/python-mss.git 27 | $ cd python-mss 28 | 29 | 30 | And then:: 31 | 32 | $ python setup.py install --user 33 | -------------------------------------------------------------------------------- /src/tests/test_bgra_to_rgb.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import pytest 6 | 7 | from mss.base import ScreenShot 8 | 9 | 10 | def test_bad_length() -> None: 11 | data = bytearray(b"789c626001000000ffff030000060005") 12 | image = ScreenShot.from_size(data, 1024, 768) 13 | with pytest.raises(ValueError, match="attempt to assign"): 14 | _ = image.rgb 15 | 16 | 17 | def test_good_types(raw: bytes) -> None: 18 | image = ScreenShot.from_size(bytearray(raw), 1024, 768) 19 | assert isinstance(image.raw, bytearray) 20 | assert isinstance(image.rgb, bytes) 21 | -------------------------------------------------------------------------------- /docs/source/examples/linux_xshm_backend.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Select the XShmGetImage backend explicitly and inspect its status. 5 | """ 6 | 7 | from mss.linux.xshmgetimage import MSS as mss 8 | 9 | with mss() as sct: 10 | screenshot = sct.grab(sct.monitors[1]) 11 | print(f"Captured screenshot dimensions: {screenshot.size.width}x{screenshot.size.height}") 12 | 13 | print(f"shm_status: {sct.shm_status.name}") 14 | if sct.shm_fallback_reason: 15 | print(f"Falling back to XGetImage because: {sct.shm_fallback_reason}") 16 | else: 17 | print("MIT-SHM capture active.") 18 | -------------------------------------------------------------------------------- /docs/source/examples/callback.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Screenshot of the monitor 1, with callback. 5 | """ 6 | 7 | from pathlib import Path 8 | 9 | import mss 10 | 11 | 12 | def on_exists(fname: str) -> None: 13 | """Callback example when we try to overwrite an existing screenshot.""" 14 | file = Path(fname) 15 | if file.is_file(): 16 | newfile = file.with_name(f"{file.name}.old") 17 | print(f"{fname} → {newfile}") 18 | file.rename(newfile) 19 | 20 | 21 | with mss.mss() as sct: 22 | filename = sct.shot(output="mon-{mon}.png", callback=on_exists) 23 | print(filename) 24 | -------------------------------------------------------------------------------- /src/mss/models.py: -------------------------------------------------------------------------------- 1 | # This is part of the MSS Python's module. 2 | # Source: https://github.com/BoboTiG/python-mss. 3 | 4 | from typing import Any, NamedTuple 5 | 6 | Monitor = dict[str, int] 7 | Monitors = list[Monitor] 8 | 9 | Pixel = tuple[int, int, int] 10 | Pixels = list[tuple[Pixel, ...]] 11 | 12 | CFunctions = dict[str, tuple[str, list[Any], Any]] 13 | 14 | 15 | class Pos(NamedTuple): 16 | #: The horizontal X coordinate of the position. 17 | left: int 18 | #: The vertical Y coordinate of the position. 19 | top: int 20 | 21 | 22 | class Size(NamedTuple): 23 | #: The horizontal X width. 24 | width: int 25 | #: The vertical Y height. 26 | height: int 27 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | The full list can be found here: https://github.com/BoboTiG/python-mss/graphs/contributors 4 | 5 | That document is mostly useful for users without a GitHub account (sorted alphabetically): 6 | 7 | - [bubulle](http://indexerror.net/user/bubulle) 8 | - Windows: efficiency of MSS.get_pixels() 9 | - [Condé 'Eownis' Titouan](https://titouan.co) 10 | - MacOS X tester 11 | - [David Becker](https://davide.me) 12 | - Mac: Take into account extra black pixels added when screen with is not divisible by 16 13 | - [Oros](https://ecirtam.net) 14 | - GNU/Linux tester 15 | - [yoch](http://indexerror.net/user/yoch) 16 | - Windows: efficiency of `MSS.get_pixels()` 17 | - Wagoun 18 | - equipment loan (Macbook Pro) 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "off", // We'll use Mypy instead of the built-in Pyright 3 | "python.testing.pytestEnabled": true, 4 | "python.testing.unittestEnabled": false, 5 | "ruff.enable": true, 6 | 7 | "languageToolLinter.languageTool.ignoredWordsInWorkspace": [ 8 | "bgra", 9 | "ctypes", 10 | "eownis", 11 | "memoization", 12 | "noop", 13 | "numpy", 14 | "oros", 15 | "pylint", 16 | "pypy", 17 | "python-mss", 18 | "pythonista", 19 | "sdist", 20 | "sourcery", 21 | "tk", 22 | "tkinter", 23 | "xlib", 24 | "xrandr", 25 | "xserver", 26 | "zlib" 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | General information: 2 | 3 | * OS name: _Debian GNU/Linux_ 4 | * OS version: __sid__ 5 | * OS architecture: _64 bits_ 6 | * Resolutions: 7 | * Monitor 1: _800x600_ 8 | * Monitor 2: _1920x1080_ 9 | * Python version: _3.6.4_ 10 | * MSS version: __3.2.0__ 11 | 12 | 13 | For GNU/Linux users: 14 | 15 | * Display server protocol and version, if known: __X server__ 16 | * Desktop Environment: _XFCE 4_ 17 | * Composite Window Manager name and version: __Cairo v1.14.6__ 18 | 19 | 20 | ### Description of the warning/error 21 | 22 | A description of the issue with optional steps to reproduce. 23 | 24 | ### Full message 25 | 26 | Copy and paste here the entire warning/error message. 27 | 28 | ### Other details 29 | 30 | More information, if you think it is needed. 31 | 32 | -------------------------------------------------------------------------------- /docs/source/examples/pil.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | PIL example using frombytes(). 5 | """ 6 | 7 | from PIL import Image 8 | 9 | import mss 10 | 11 | with mss.mss() as sct: 12 | # Get rid of the first, as it represents the "All in One" monitor: 13 | for num, monitor in enumerate(sct.monitors[1:], 1): 14 | # Get raw pixels from the screen 15 | sct_img = sct.grab(monitor) 16 | 17 | # Create the Image 18 | img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") 19 | # The same, but less efficient: 20 | # img = Image.frombytes('RGB', sct_img.size, sct_img.rgb) 21 | 22 | # And save it! 23 | output = f"monitor-{num}.png" 24 | img.save(output) 25 | print(output) 26 | -------------------------------------------------------------------------------- /src/mss/exception.py: -------------------------------------------------------------------------------- 1 | # This is part of the MSS Python's module. 2 | # Source: https://github.com/BoboTiG/python-mss. 3 | 4 | from __future__ import annotations 5 | 6 | from typing import Any 7 | 8 | 9 | class ScreenShotError(Exception): 10 | """Error handling class.""" 11 | 12 | def __init__(self, message: str, /, *, details: dict[str, Any] | None = None) -> None: 13 | super().__init__(message) 14 | #: On GNU/Linux, and if the error comes from the XServer, it contains XError details. 15 | #: This is an empty dict by default. 16 | #: 17 | #: For XErrors, you can find information on 18 | #: `Using the Default Error Handlers `_. 19 | #: 20 | #: .. versionadded:: 3.3.0 21 | self.details = details or {} 22 | -------------------------------------------------------------------------------- /src/tests/test_cls_image.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from collections.abc import Callable 6 | from typing import Any 7 | 8 | from mss.base import MSSBase 9 | from mss.models import Monitor 10 | 11 | 12 | class SimpleScreenShot: 13 | def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: 14 | self.raw = bytes(data) 15 | self.monitor = monitor 16 | 17 | 18 | def test_custom_cls_image(mss_impl: Callable[..., MSSBase]) -> None: 19 | with mss_impl() as sct: 20 | sct.cls_image = SimpleScreenShot # type: ignore[assignment] 21 | mon1 = sct.monitors[1] 22 | image = sct.grab(mon1) 23 | assert isinstance(image, SimpleScreenShot) 24 | assert isinstance(image.raw, bytes) 25 | assert isinstance(image.monitor, dict) 26 | -------------------------------------------------------------------------------- /docs/source/examples/custom_cls_image.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Screenshot of the monitor 1, using a custom class to handle the data. 5 | """ 6 | 7 | from typing import Any 8 | 9 | import mss 10 | from mss.models import Monitor 11 | from mss.screenshot import ScreenShot 12 | 13 | 14 | class SimpleScreenShot(ScreenShot): 15 | """Define your own custom method to deal with screenshot raw data. 16 | Of course, you can inherit from the ScreenShot class and change 17 | or add new methods. 18 | """ 19 | 20 | def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None: 21 | self.data = data 22 | self.monitor = monitor 23 | 24 | 25 | with mss.mss() as sct: 26 | sct.cls_image = SimpleScreenShot 27 | image = sct.grab(sct.monitors[1]) 28 | # ... 29 | -------------------------------------------------------------------------------- /docs/source/examples/from_pil_tuple.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Use PIL bbox style and percent values. 5 | """ 6 | 7 | import mss 8 | import mss.tools 9 | 10 | with mss.mss() as sct: 11 | # Use the 1st monitor 12 | monitor = sct.monitors[1] 13 | 14 | # Capture a bbox using percent values 15 | left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left 16 | top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top 17 | right = left + 400 # 400px width 18 | lower = top + 400 # 400px height 19 | bbox = (left, top, right, lower) 20 | 21 | # Grab the picture 22 | # Using PIL would be something like: 23 | # im = ImageGrab(bbox=bbox) 24 | im = sct.grab(bbox) 25 | 26 | # Save it! 27 | mss.tools.to_png(im.rgb, im.size, output="screenshot.png") 28 | -------------------------------------------------------------------------------- /docs/source/examples/pil_pixels.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | PIL examples to play with pixels. 5 | """ 6 | 7 | from PIL import Image 8 | 9 | import mss 10 | 11 | with mss.mss() as sct: 12 | # Get a screenshot of the 1st monitor 13 | sct_img = sct.grab(sct.monitors[1]) 14 | 15 | # Create an Image 16 | img = Image.new("RGB", sct_img.size) 17 | 18 | # Best solution: create a list(tuple(R, G, B), ...) for putdata() 19 | pixels = zip(sct_img.raw[2::4], sct_img.raw[1::4], sct_img.raw[::4]) 20 | img.putdata(list(pixels)) 21 | 22 | # But you can set individual pixels too (slower) 23 | """ 24 | pixels = img.load() 25 | for x in range(sct_img.width): 26 | for y in range(sct_img.height): 27 | pixels[x, y] = sct_img.pixel(x, y) 28 | """ 29 | 30 | # Show it! 31 | img.show() 32 | -------------------------------------------------------------------------------- /docs/source/support.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Support 3 | ======= 4 | 5 | Feel free to try MSS on a system we had not tested, and let us know by creating an `issue `_. 6 | 7 | - OS: GNU/Linux, macOS, and Windows 8 | - Python: 3.9 and newer 9 | 10 | 11 | Future 12 | ====== 13 | 14 | - Support `ReactOS `_ and other systems on OS stability or available hardware. 15 | - Any idea? 16 | 17 | 18 | Others 19 | ====== 20 | 21 | Tested successfully on Pypy 5.1.0 on Windows, but speed is terrible. 22 | 23 | 24 | Abandoned 25 | ========= 26 | 27 | - Python 2.6 (2016-10-08) 28 | - Python 2.7 (2019-01-31) 29 | - Python 3.0 (2016-10-08) 30 | - Python 3.1 (2016-10-08) 31 | - Python 3.2 (2016-10-08) 32 | - Python 3.3 (2017-12-05) 33 | - Python 3.4 (2018-03-19) 34 | - Python 3.5 (2022-10-27) 35 | - Python 3.6 (2022-10-27) 36 | - Python 3.7 (2023-04-09) 37 | - Python 3.8 (2024-11-14) 38 | -------------------------------------------------------------------------------- /src/mss/linux/xgetimage.py: -------------------------------------------------------------------------------- 1 | """XCB-based backend using the XGetImage request. 2 | 3 | This backend issues XCB ``GetImage`` requests and supports the RandR and 4 | XFixes extensions when available for monitor enumeration and cursor capture. 5 | 6 | This backend will work on any X connection, but is slower than the xshmgetimage 7 | backend. 8 | 9 | .. versionadded:: 10.2.0 10 | """ 11 | 12 | from mss.models import Monitor 13 | from mss.screenshot import ScreenShot 14 | 15 | from .base import MSSXCBBase 16 | 17 | 18 | class MSS(MSSXCBBase): 19 | """XCB backend using XGetImage requests on GNU/Linux. 20 | 21 | .. seealso:: 22 | :py:class:`mss.linux.base.MSSXCBBase` 23 | Lists constructor parameters. 24 | """ 25 | 26 | def _grab_impl(self, monitor: Monitor) -> ScreenShot: 27 | """Retrieve all pixels from a monitor. Pixels have to be RGBX.""" 28 | return super()._grab_impl_xgetimage(monitor) 29 | -------------------------------------------------------------------------------- /docs/source/examples/part_of_screen_monitor_2.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Example to capture part of the screen of the monitor 2. 5 | """ 6 | 7 | import mss 8 | import mss.tools 9 | 10 | with mss.mss() as sct: 11 | # Get information of monitor 2 12 | monitor_number = 2 13 | mon = sct.monitors[monitor_number] 14 | 15 | # The screen part to capture 16 | monitor = { 17 | "top": mon["top"] + 100, # 100px from the top 18 | "left": mon["left"] + 100, # 100px from the left 19 | "width": 160, 20 | "height": 135, 21 | "mon": monitor_number, 22 | } 23 | output = "sct-mon{mon}_{top}x{left}_{width}x{height}.png".format(**monitor) 24 | 25 | # Grab the data 26 | sct_img = sct.grab(monitor) 27 | 28 | # Save to the picture file 29 | mss.tools.to_png(sct_img.rgb, sct_img.size, output=output) 30 | print(output) 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | /* We use python:3 because it's based on Debian, which has libxcb-errors0 available. The default universal image is 3 | * based on Ubuntu, which doesn't. */ 4 | "image": "mcr.microsoft.com/devcontainers/python:3", 5 | "features": { 6 | "ghcr.io/devcontainers-extra/features/apt-get-packages:1": { 7 | "packages": [ 8 | /* Needed for MSS generally */ 9 | "libxfixes3", 10 | /* Needed for testing */ 11 | "xvfb", "xauth", 12 | /* Improves error messages */ 13 | "libxcb-errors0", 14 | /* We include the gdb stuff to troubleshoot when ctypes stuff goes off the rails. */ 15 | "debuginfod", "gdb", 16 | /* GitHub checks out the repo with git-lfs configured. */ 17 | "git-lfs" 18 | ], 19 | "preserve_apt_list": true 20 | } 21 | }, 22 | "postCreateCommand": "echo set debuginfod enabled on | sudo tee /etc/gdb/gdbinit.d/debuginfod.gdb" 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | - name: Install Python 16 | uses: actions/setup-python@v6 17 | with: 18 | python-version: "3.x" 19 | cache: pip 20 | - name: Install build dependencies 21 | run: | 22 | python -m pip install -U pip 23 | python -m pip install -e '.[dev]' 24 | - name: Build 25 | run: python -m build 26 | - name: Check 27 | run: twine check --strict dist/* 28 | - name: What will we publish? 29 | run: ls -l dist 30 | - name: Publish 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.PYPI_API_TOKEN }} 35 | skip_existing: true 36 | print_hash: true 37 | -------------------------------------------------------------------------------- /src/mss/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is maintained by Mickaël Schoentgen . 2 | # 3 | # You can always get the latest version of this module at: 4 | # https://github.com/BoboTiG/python-mss 5 | # If that URL should fail, try contacting the author. 6 | """An ultra fast cross-platform multiple screenshots module in pure python 7 | using ctypes. 8 | """ 9 | 10 | from mss.exception import ScreenShotError 11 | from mss.factory import mss 12 | 13 | __version__ = "10.2.0.dev0" 14 | __author__ = "Mickaël Schoentgen" 15 | __date__ = "2013-2025" 16 | __copyright__ = f""" 17 | Copyright (c) {__date__}, {__author__} 18 | 19 | Permission to use, copy, modify, and distribute this software and its 20 | documentation for any purpose and without fee or royalty is hereby 21 | granted, provided that the above copyright notice appear in all copies 22 | and that both that copyright notice and this permission notice appear 23 | in supporting documentation or portions thereof, including 24 | modifications, that you make. 25 | """ 26 | __all__ = ("ScreenShotError", "mss") 27 | -------------------------------------------------------------------------------- /docs/source/examples/opencv_numpy.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | OpenCV/Numpy example. 5 | """ 6 | 7 | import time 8 | 9 | import cv2 10 | import numpy as np 11 | 12 | import mss 13 | 14 | with mss.mss() as sct: 15 | # Part of the screen to capture 16 | monitor = {"top": 40, "left": 0, "width": 800, "height": 640} 17 | 18 | while "Screen capturing": 19 | last_time = time.time() 20 | 21 | # Get raw pixels from the screen, save it to a Numpy array 22 | img = np.array(sct.grab(monitor)) 23 | 24 | # Display the picture 25 | cv2.imshow("OpenCV/Numpy normal", img) 26 | 27 | # Display the picture in grayscale 28 | # cv2.imshow('OpenCV/Numpy grayscale', 29 | # cv2.cvtColor(img, cv2.COLOR_BGRA2GRAY)) 30 | 31 | print(f"fps: {1 / (time.time() - last_time)}") 32 | 33 | # Press "q" to quit 34 | if cv2.waitKey(25) & 0xFF == ord("q"): 35 | cv2.destroyAllWindows() 36 | break 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2013-2025, Mickaël 'Tiger-222' Schoentgen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/tests/test_find_monitors.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from collections.abc import Callable 6 | 7 | from mss.base import MSSBase 8 | 9 | 10 | def test_get_monitors(mss_impl: Callable[..., MSSBase]) -> None: 11 | with mss_impl() as sct: 12 | assert sct.monitors 13 | 14 | 15 | def test_keys_aio(mss_impl: Callable[..., MSSBase]) -> None: 16 | with mss_impl() as sct: 17 | all_monitors = sct.monitors[0] 18 | assert "top" in all_monitors 19 | assert "left" in all_monitors 20 | assert "height" in all_monitors 21 | assert "width" in all_monitors 22 | 23 | 24 | def test_keys_monitor_1(mss_impl: Callable[..., MSSBase]) -> None: 25 | with mss_impl() as sct: 26 | mon1 = sct.monitors[1] 27 | assert "top" in mon1 28 | assert "left" in mon1 29 | assert "height" in mon1 30 | assert "width" in mon1 31 | 32 | 33 | def test_dimensions(mss_impl: Callable[..., MSSBase]) -> None: 34 | with mss_impl() as sct: 35 | mon = sct.monitors[1] 36 | assert mon["width"] > 0 37 | assert mon["height"] > 0 38 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | MSS API 3 | ======= 4 | 5 | Core Package 6 | ============ 7 | 8 | .. automodule:: mss 9 | 10 | Screenshot Objects 11 | ================== 12 | 13 | .. automodule:: mss.screenshot 14 | 15 | Base Classes 16 | ============ 17 | 18 | .. automodule:: mss.base 19 | 20 | Tools 21 | ===== 22 | 23 | .. automodule:: mss.tools 24 | 25 | Exceptions 26 | ========== 27 | 28 | .. automodule:: mss.exception 29 | 30 | Data Models 31 | =========== 32 | 33 | .. automodule:: mss.models 34 | 35 | Platform Backends 36 | ================= 37 | 38 | macOS Backend 39 | ------------- 40 | 41 | .. automodule:: mss.darwin 42 | 43 | GNU/Linux Dispatcher 44 | -------------------- 45 | 46 | .. automodule:: mss.linux 47 | 48 | GNU/Linux Xlib Backend 49 | ---------------------- 50 | 51 | .. automodule:: mss.linux.xlib 52 | 53 | GNU/Linux XCB Base 54 | ------------------ 55 | 56 | .. automodule:: mss.linux.base 57 | 58 | GNU/Linux XCB XGetImage Backend 59 | ------------------------------- 60 | 61 | .. automodule:: mss.linux.xgetimage 62 | 63 | GNU/Linux XCB XShmGetImage Backend 64 | ---------------------------------- 65 | 66 | .. automodule:: mss.linux.xshmgetimage 67 | 68 | Windows Backend 69 | --------------- 70 | 71 | .. automodule:: mss.windows 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/source/examples/fps_multiprocessing.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Example using the multiprocessing module to speed-up screen capture. 5 | https://github.com/pythonlessons/TensorFlow-object-detection-tutorial 6 | """ 7 | 8 | from multiprocessing import Process, Queue 9 | 10 | import mss 11 | import mss.tools 12 | 13 | 14 | def grab(queue: Queue) -> None: 15 | rect = {"top": 0, "left": 0, "width": 600, "height": 800} 16 | 17 | with mss.mss() as sct: 18 | for _ in range(1_000): 19 | queue.put(sct.grab(rect)) 20 | 21 | # Tell the other worker to stop 22 | queue.put(None) 23 | 24 | 25 | def save(queue: Queue) -> None: 26 | number = 0 27 | output = "screenshots/file_{}.png" 28 | to_png = mss.tools.to_png 29 | 30 | while "there are screenshots": 31 | img = queue.get() 32 | if img is None: 33 | break 34 | 35 | to_png(img.rgb, img.size, output=output.format(number)) 36 | number += 1 37 | 38 | 39 | if __name__ == "__main__": 40 | # The screenshots queue 41 | queue: Queue = Queue() 42 | 43 | # 2 processes: one for grabbing and one for saving PNG files 44 | Process(target=grab, args=(queue,)).start() 45 | Process(target=save, args=(queue,)).start() 46 | -------------------------------------------------------------------------------- /src/mss/factory.py: -------------------------------------------------------------------------------- 1 | # This is part of the MSS Python's module. 2 | # Source: https://github.com/BoboTiG/python-mss. 3 | 4 | import platform 5 | from typing import Any 6 | 7 | from mss.base import MSSBase 8 | from mss.exception import ScreenShotError 9 | 10 | 11 | def mss(**kwargs: Any) -> MSSBase: 12 | """Factory returning a proper MSS class instance. 13 | 14 | It detects the platform we are running on 15 | and chooses the most adapted mss_class to take 16 | screenshots. 17 | 18 | It then proxies its arguments to the class for 19 | instantiation. 20 | 21 | .. seealso:: 22 | - :class:`mss.darwin.MSS` 23 | - :class:`mss.linux.MSS` 24 | - :class:`mss.windows.MSS` 25 | - :func:`mss.linux.mss` 26 | - :class:`mss.linux.xshmgetimage.MSS` 27 | - :class:`mss.linux.xgetimage.MSS` 28 | - :class:`mss.linux.xlib.MSS` 29 | """ 30 | os_ = platform.system().lower() 31 | 32 | if os_ == "darwin": 33 | from mss import darwin # noqa: PLC0415 34 | 35 | return darwin.MSS(**kwargs) 36 | 37 | if os_ == "linux": 38 | from mss import linux # noqa: PLC0415 39 | 40 | # Linux has its own factory to choose the backend. 41 | return linux.mss(**kwargs) 42 | 43 | if os_ == "windows": 44 | from mss import windows # noqa: PLC0415 45 | 46 | return windows.MSS(**kwargs) 47 | 48 | msg = f"System {os_!r} not (yet?) implemented." 49 | raise ScreenShotError(msg) 50 | -------------------------------------------------------------------------------- /docs/source/developers.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | 3 | ========== 4 | Developers 5 | ========== 6 | 7 | Setup 8 | ===== 9 | 10 | 1. You need to fork the `GitHub repository `_. 11 | 2. Create you own branch. 12 | 3. Be sure to add/update tests and documentation within your patch. 13 | 14 | 15 | Testing 16 | ======= 17 | 18 | Dependency 19 | ---------- 20 | 21 | You will need `pytest `_:: 22 | 23 | $ python -m venv venv 24 | $ . venv/bin/activate 25 | $ python -m pip install -U pip 26 | $ python -m pip install -e '.[tests]' 27 | 28 | 29 | How to Test? 30 | ------------ 31 | 32 | Launch the test suite:: 33 | 34 | $ python -m pytest 35 | 36 | 37 | Code Quality 38 | ============ 39 | 40 | To ensure the code quality is correct enough:: 41 | 42 | $ python -m pip install -e '.[dev]' 43 | $ ./check.sh 44 | 45 | 46 | Documentation 47 | ============= 48 | 49 | To build the documentation, simply type:: 50 | 51 | $ python -m pip install -e '.[docs]' 52 | $ sphinx-build -d docs docs/source docs_out --color -W -bhtml 53 | 54 | 55 | XCB Code Generator 56 | ================== 57 | 58 | .. versionadded:: 10.2.0 59 | 60 | The GNU/Linux XCB backends rely on generated ctypes bindings. If you need to 61 | add new XCB requests or types, do **not** edit ``src/mss/linux/xcbgen.py`` by 62 | hand. Instead, follow the workflow described in ``src/xcbproto/README.md``, 63 | which explains how to update ``gen_xcb_to_py.py`` and regenerate the bindings. 64 | -------------------------------------------------------------------------------- /docs/source/examples/fps.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | Simple naive benchmark to compare with: 5 | https://pythonprogramming.net/game-frames-open-cv-python-plays-gta-v/ 6 | """ 7 | 8 | import time 9 | 10 | import cv2 11 | import numpy as np 12 | from PIL import ImageGrab 13 | 14 | import mss 15 | 16 | 17 | def screen_record() -> int: 18 | # 800x600 windowed mode 19 | mon = (0, 40, 800, 640) 20 | 21 | title = "[PIL.ImageGrab] FPS benchmark" 22 | fps = 0 23 | last_time = time.time() 24 | 25 | while time.time() - last_time < 1: 26 | img = np.asarray(ImageGrab.grab(bbox=mon)) 27 | fps += 1 28 | 29 | cv2.imshow(title, cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) 30 | if cv2.waitKey(25) & 0xFF == ord("q"): 31 | cv2.destroyAllWindows() 32 | break 33 | 34 | return fps 35 | 36 | 37 | def screen_record_efficient() -> int: 38 | # 800x600 windowed mode 39 | mon = {"top": 40, "left": 0, "width": 800, "height": 640} 40 | 41 | title = "[MSS] FPS benchmark" 42 | fps = 0 43 | sct = mss.mss() 44 | last_time = time.time() 45 | 46 | while time.time() - last_time < 1: 47 | img = np.asarray(sct.grab(mon)) 48 | fps += 1 49 | 50 | cv2.imshow(title, img) 51 | if cv2.waitKey(25) & 0xFF == ord("q"): 52 | cv2.destroyAllWindows() 53 | break 54 | 55 | return fps 56 | 57 | 58 | print("PIL:", screen_record()) 59 | print("MSS:", screen_record_efficient()) 60 | -------------------------------------------------------------------------------- /src/tests/test_get_pixels.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import itertools 6 | from collections.abc import Callable 7 | 8 | import pytest 9 | 10 | from mss.base import MSSBase, ScreenShot 11 | from mss.exception import ScreenShotError 12 | 13 | 14 | def test_grab_monitor(mss_impl: Callable[..., MSSBase]) -> None: 15 | with mss_impl() as sct: 16 | for mon in sct.monitors: 17 | image = sct.grab(mon) 18 | assert isinstance(image, ScreenShot) 19 | assert isinstance(image.raw, bytearray) 20 | assert isinstance(image.rgb, bytes) 21 | 22 | 23 | def test_grab_part_of_screen(mss_impl: Callable[..., MSSBase]) -> None: 24 | with mss_impl() as sct: 25 | for width, height in itertools.product(range(1, 42), range(1, 42)): 26 | monitor = {"top": 160, "left": 160, "width": width, "height": height} 27 | image = sct.grab(monitor) 28 | 29 | assert image.top == 160 30 | assert image.left == 160 31 | assert image.width == width 32 | assert image.height == height 33 | 34 | 35 | def test_get_pixel(raw: bytes) -> None: 36 | image = ScreenShot.from_size(bytearray(raw), 1024, 768) 37 | assert image.width == 1024 38 | assert image.height == 768 39 | assert len(image.pixels) == 768 40 | assert len(image.pixels[0]) == 1024 41 | 42 | assert image.pixel(0, 0) == (135, 152, 192) 43 | assert image.pixel(image.width // 2, image.height // 2) == (0, 0, 0) 44 | assert image.pixel(image.width - 1, image.height - 1) == (135, 152, 192) 45 | 46 | with pytest.raises(ScreenShotError): 47 | image.pixel(image.width + 1, 12) 48 | -------------------------------------------------------------------------------- /src/tests/test_tools.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import io 8 | from pathlib import Path 9 | from typing import TYPE_CHECKING 10 | 11 | import pytest 12 | 13 | from mss.tools import to_png 14 | 15 | if TYPE_CHECKING: 16 | from collections.abc import Callable 17 | 18 | from mss.base import MSSBase 19 | 20 | WIDTH = 10 21 | HEIGHT = 10 22 | 23 | 24 | def assert_is_valid_png(*, raw: bytes | None = None, file: Path | None = None) -> None: 25 | Image = pytest.importorskip("PIL.Image", reason="PIL module not available.") # noqa: N806 26 | 27 | assert bool(Image.open(io.BytesIO(raw) if raw is not None else file).tobytes()) 28 | try: 29 | Image.open(io.BytesIO(raw) if raw is not None else file).verify() 30 | except Exception: # noqa: BLE001 31 | pytest.fail(reason="invalid PNG data") 32 | 33 | 34 | def test_bad_compression_level(mss_impl: Callable[..., MSSBase]) -> None: 35 | with mss_impl(compression_level=42) as sct, pytest.raises(Exception, match="Bad compression level"): 36 | sct.shot() 37 | 38 | 39 | @pytest.mark.parametrize("level", range(10)) 40 | def test_compression_level(level: int) -> None: 41 | data = b"rgb" * WIDTH * HEIGHT 42 | raw = to_png(data, (WIDTH, HEIGHT), level=level) 43 | assert isinstance(raw, bytes) 44 | assert_is_valid_png(raw=raw) 45 | 46 | 47 | def test_output_file() -> None: 48 | data = b"rgb" * WIDTH * HEIGHT 49 | output = Path(f"{WIDTH}x{HEIGHT}.png") 50 | to_png(data, (WIDTH, HEIGHT), output=output) 51 | assert output.is_file() 52 | assert_is_valid_png(file=output) 53 | 54 | 55 | def test_output_raw_bytes() -> None: 56 | data = b"rgb" * WIDTH * HEIGHT 57 | raw = to_png(data, (WIDTH, HEIGHT)) 58 | assert isinstance(raw, bytes) 59 | assert_is_valid_png(raw=raw) 60 | -------------------------------------------------------------------------------- /src/tests/test_issue_220.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from functools import partial 6 | 7 | import pytest 8 | 9 | import mss 10 | 11 | tkinter = pytest.importorskip("tkinter") 12 | 13 | 14 | @pytest.fixture 15 | def root() -> tkinter.Tk: # type: ignore[name-defined] 16 | try: 17 | master = tkinter.Tk() 18 | except RuntimeError: 19 | pytest.skip(reason="tk.h version (8.5) doesn't match libtk.a version (8.6)") 20 | 21 | try: 22 | yield master 23 | finally: 24 | master.destroy() 25 | 26 | 27 | def take_screenshot(*, backend: str) -> None: 28 | region = {"top": 370, "left": 1090, "width": 80, "height": 390} 29 | with mss.mss(backend=backend) as sct: 30 | sct.grab(region) 31 | 32 | 33 | def create_top_level_win(master: tkinter.Tk, backend: str) -> None: # type: ignore[name-defined] 34 | top_level_win = tkinter.Toplevel(master) 35 | 36 | take_screenshot_btn = tkinter.Button( 37 | top_level_win, text="Take screenshot", command=partial(take_screenshot, backend=backend) 38 | ) 39 | take_screenshot_btn.pack() 40 | 41 | take_screenshot_btn.invoke() 42 | master.update_idletasks() 43 | master.update() 44 | 45 | top_level_win.destroy() 46 | master.update_idletasks() 47 | master.update() 48 | 49 | 50 | def test_regression(root: tkinter.Tk, capsys: pytest.CaptureFixture, backend: str) -> None: # type: ignore[name-defined] 51 | btn = tkinter.Button(root, text="Open TopLevel", command=lambda: create_top_level_win(root, backend)) 52 | btn.pack() 53 | 54 | # First screenshot: it works 55 | btn.invoke() 56 | 57 | # Second screenshot: it should work too 58 | btn.invoke() 59 | 60 | # Check there were no exceptions 61 | captured = capsys.readouterr() 62 | assert not captured.out 63 | assert not captured.err 64 | -------------------------------------------------------------------------------- /src/tests/bench_general.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | 2018-03-19. 5 | 6 | Original means MSS 3.1.2. 7 | Patched means MSS 3.2.0. 8 | 9 | 10 | GNU/Linux Original Patched Gain % 11 | grab 2618 2738 +4.58 12 | access_rgb 1083 1128 +4.15 13 | output 324 322 ------ 14 | save 320 319 ------ 15 | 16 | macOS 17 | grab 524 526 ------ 18 | access_rgb 396 406 +2.52 19 | output 194 195 ------ 20 | save 193 194 ------ 21 | 22 | Windows 23 | grab 1280 2498 +95.16 24 | access_rgb 574 712 +24.04 25 | output 139 188 +35.25 26 | """ 27 | 28 | from __future__ import annotations 29 | 30 | from time import time 31 | from typing import TYPE_CHECKING 32 | 33 | import mss 34 | import mss.tools 35 | 36 | if TYPE_CHECKING: # pragma: nocover 37 | from collections.abc import Callable 38 | 39 | from mss.base import MSSBase 40 | from mss.screenshot import ScreenShot 41 | 42 | 43 | def grab(sct: MSSBase) -> ScreenShot: 44 | monitor = {"top": 144, "left": 80, "width": 1397, "height": 782} 45 | return sct.grab(monitor) 46 | 47 | 48 | def access_rgb(sct: MSSBase) -> bytes: 49 | im = grab(sct) 50 | return im.rgb 51 | 52 | 53 | def output(sct: MSSBase, filename: str | None = None) -> None: 54 | rgb = access_rgb(sct) 55 | mss.tools.to_png(rgb, (1397, 782), output=filename) 56 | 57 | 58 | def save(sct: MSSBase) -> None: 59 | output(sct, filename="screenshot.png") 60 | 61 | 62 | def benchmark(func: Callable) -> None: 63 | count = 0 64 | start = time() 65 | 66 | with mss.mss() as sct: 67 | while (time() - start) % 60 < 10: 68 | count += 1 69 | func(sct) 70 | 71 | print(func.__name__, count) 72 | 73 | 74 | benchmark(grab) 75 | benchmark(access_rgb) 76 | benchmark(output) 77 | benchmark(save) 78 | -------------------------------------------------------------------------------- /src/xcbproto/README.md: -------------------------------------------------------------------------------- 1 | # xcbproto Directory 2 | 3 | This directory contains the tooling and protocol definitions used to generate Python bindings for XCB (X C Binding). 4 | 5 | ## Overview 6 | 7 | - **`gen_xcb_to_py.py`**: Code generator that produces Python/ctypes bindings from XCB protocol XML files. 8 | - **`*.xml`**: Protocol definition files vendored from the upstream [xcbproto](https://gitlab.freedesktop.org/xorg/proto/xcbproto) repository. These describe the X11 core protocol and extensions (RandR, Render, XFixes, etc.). 9 | 10 | ## Workflow 11 | 12 | The generator is a **maintainer tool**, not part of the normal build process: 13 | 14 | 1. When the project needs new XCB requests or types, a maintainer edits the configuration in `gen_xcb_to_py.py` (see `TYPES` and `REQUESTS` dictionaries near the top). 15 | 2. The maintainer runs the generator: 16 | 17 | ```bash 18 | python src/xcbproto/gen_xcb_to_py.py 19 | ``` 20 | 21 | 3. The generator reads the XML protocol definitions and emits `xcbgen.py`. 22 | 4. The maintainer ensures that this worked correctly, and moves the file to `src/mss/linux/xcbgen.py`. 23 | 5. The generated `xcbgen.py` is committed to version control and distributed with the package, so end users never need to run the generator. 24 | 25 | ## Protocol XML Files 26 | 27 | The `*.xml` files are **unmodified copies** from the upstream xcbproto project. They define the wire protocol and data structures used by libxcb. Do not edit these files. 28 | 29 | ## Why Generate Code? 30 | 31 | The XCB C library exposes thousands of protocol elements. Rather than hand-write ctypes bindings for every structure and request, we auto-generate only the subset we actually use. This keeps the codebase lean while ensuring the bindings exactly match the upstream protocol definitions. 32 | 33 | ## Dependencies 34 | 35 | - **lxml**: Required to parse the XML protocol definitions. 36 | - **Python 3.12+**: The generator uses modern Python features. 37 | 38 | Note that end users do **not** need lxml; it's only required if you're regenerating the bindings. 39 | -------------------------------------------------------------------------------- /src/tests/bench_bgra2rgb.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | 4 | 2018-03-19. 5 | 6 | Maximum screenshots in 1 second by computing BGRA raw values to RGB. 7 | 8 | 9 | GNU/Linux 10 | pil_frombytes 139 11 | mss_rgb 119 12 | pil_frombytes_rgb 51 13 | numpy_flip 31 14 | numpy_slice 29 15 | 16 | macOS 17 | pil_frombytes 209 18 | mss_rgb 174 19 | pil_frombytes_rgb 113 20 | numpy_flip 39 21 | numpy_slice 36 22 | 23 | Windows 24 | pil_frombytes 81 25 | mss_rgb 66 26 | pil_frombytes_rgb 42 27 | numpy_flip 25 28 | numpy_slice 22 29 | """ 30 | 31 | import time 32 | 33 | import numpy as np 34 | from PIL import Image 35 | 36 | import mss 37 | from mss.screenshot import ScreenShot 38 | 39 | 40 | def mss_rgb(im: ScreenShot) -> bytes: 41 | return im.rgb 42 | 43 | 44 | def numpy_flip(im: ScreenShot) -> bytes: 45 | frame = np.array(im, dtype=np.uint8) 46 | return np.flip(frame[:, :, :3], 2).tobytes() 47 | 48 | 49 | def numpy_slice(im: ScreenShot) -> bytes: 50 | return np.array(im, dtype=np.uint8)[..., [2, 1, 0]].tobytes() 51 | 52 | 53 | def pil_frombytes_rgb(im: ScreenShot) -> bytes: 54 | return Image.frombytes("RGB", im.size, im.rgb).tobytes() 55 | 56 | 57 | def pil_frombytes(im: ScreenShot) -> bytes: 58 | return Image.frombytes("RGB", im.size, im.bgra, "raw", "BGRX").tobytes() 59 | 60 | 61 | def benchmark() -> None: 62 | with mss.mss() as sct: 63 | im = sct.grab(sct.monitors[0]) 64 | for func in ( 65 | pil_frombytes, 66 | mss_rgb, 67 | pil_frombytes_rgb, 68 | numpy_flip, 69 | numpy_slice, 70 | ): 71 | count = 0 72 | start = time.time() 73 | while (time.time() - start) <= 1: 74 | func(im) 75 | im._ScreenShot__rgb = None # type: ignore[attr-defined] 76 | count += 1 77 | print(func.__name__.ljust(17), count) 78 | 79 | 80 | benchmark() 81 | -------------------------------------------------------------------------------- /src/tests/third_party/test_pil.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import itertools 6 | from collections.abc import Callable 7 | from pathlib import Path 8 | 9 | import pytest 10 | 11 | from mss.base import MSSBase 12 | 13 | Image = pytest.importorskip("PIL.Image", reason="PIL module not available.") 14 | 15 | 16 | def test_pil(mss_impl: Callable[..., MSSBase]) -> None: 17 | width, height = 16, 16 18 | box = {"top": 0, "left": 0, "width": width, "height": height} 19 | with mss_impl() as sct: 20 | sct_img = sct.grab(box) 21 | 22 | img = Image.frombytes("RGB", sct_img.size, sct_img.rgb) 23 | assert img.mode == "RGB" 24 | assert img.size == sct_img.size 25 | 26 | for x, y in itertools.product(range(width), range(height)): 27 | assert img.getpixel((x, y)) == sct_img.pixel(x, y) 28 | 29 | output = Path("box.png") 30 | img.save(output) 31 | assert output.is_file() 32 | 33 | 34 | def test_pil_bgra(mss_impl: Callable[..., MSSBase]) -> None: 35 | width, height = 16, 16 36 | box = {"top": 0, "left": 0, "width": width, "height": height} 37 | with mss_impl() as sct: 38 | sct_img = sct.grab(box) 39 | 40 | img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") 41 | assert img.mode == "RGB" 42 | assert img.size == sct_img.size 43 | 44 | for x, y in itertools.product(range(width), range(height)): 45 | assert img.getpixel((x, y)) == sct_img.pixel(x, y) 46 | 47 | output = Path("box-bgra.png") 48 | img.save(output) 49 | assert output.is_file() 50 | 51 | 52 | def test_pil_not_16_rounded(mss_impl: Callable[..., MSSBase]) -> None: 53 | width, height = 10, 10 54 | box = {"top": 0, "left": 0, "width": width, "height": height} 55 | with mss_impl() as sct: 56 | sct_img = sct.grab(box) 57 | 58 | img = Image.frombytes("RGB", sct_img.size, sct_img.rgb) 59 | assert img.mode == "RGB" 60 | assert img.size == sct_img.size 61 | 62 | for x, y in itertools.product(range(width), range(height)): 63 | assert img.getpixel((x, y)) == sct_img.pixel(x, y) 64 | 65 | output = Path("box.png") 66 | img.save(output) 67 | assert output.is_file() 68 | -------------------------------------------------------------------------------- /src/mss/tools.py: -------------------------------------------------------------------------------- 1 | # This is part of the MSS Python's module. 2 | # Source: https://github.com/BoboTiG/python-mss. 3 | 4 | from __future__ import annotations 5 | 6 | import os 7 | import struct 8 | import zlib 9 | from typing import TYPE_CHECKING 10 | 11 | if TYPE_CHECKING: 12 | from pathlib import Path 13 | 14 | 15 | def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: Path | str | None = None) -> bytes | None: 16 | """Dump data to a PNG file. If `output` is `None`, create no file but return 17 | the whole PNG data. 18 | 19 | :param bytes data: RGBRGB...RGB data. 20 | :param tuple size: The (width, height) pair. 21 | :param int level: PNG compression level (see :py:func:`zlib.compress()` for details). 22 | :param str output: Output file name. 23 | 24 | .. versionadded:: 3.0.0 25 | 26 | .. versionchanged:: 3.2.0 27 | Added the ``level`` keyword argument to control the PNG compression level. 28 | """ 29 | pack = struct.pack 30 | crc32 = zlib.crc32 31 | 32 | width, height = size 33 | line = width * 3 34 | png_filter = pack(">B", 0) 35 | scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)]) 36 | 37 | magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10) 38 | 39 | # Header: size, marker, data, CRC32 40 | ihdr = [b"", b"IHDR", b"", b""] 41 | ihdr[2] = pack(">2I5B", width, height, 8, 2, 0, 0, 0) 42 | ihdr[3] = pack(">I", crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF) 43 | ihdr[0] = pack(">I", len(ihdr[2])) 44 | 45 | # Data: size, marker, data, CRC32 46 | idat = [b"", b"IDAT", zlib.compress(scanlines, level), b""] 47 | idat[3] = pack(">I", crc32(b"".join(idat[1:3])) & 0xFFFFFFFF) 48 | idat[0] = pack(">I", len(idat[2])) 49 | 50 | # Footer: size, marker, None, CRC32 51 | iend = [b"", b"IEND", b"", b""] 52 | iend[3] = pack(">I", crc32(iend[1]) & 0xFFFFFFFF) 53 | iend[0] = pack(">I", len(iend[2])) 54 | 55 | if not output: 56 | # Returns raw bytes of the whole PNG data 57 | return magic + b"".join(ihdr + idat + iend) 58 | 59 | with open(output, "wb") as fileh: # noqa: PTH123 60 | fileh.write(magic) 61 | fileh.write(b"".join(ihdr)) 62 | fileh.write(b"".join(idat)) 63 | fileh.write(b"".join(iend)) 64 | 65 | # Force write of file to disk 66 | fileh.flush() 67 | os.fsync(fileh.fileno()) 68 | 69 | return None 70 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Lets prevent misses, and import the module to get the proper version. 2 | # So that the version in only defined once across the whole code base: 3 | # src/mss/__init__.py 4 | import sys 5 | from pathlib import Path 6 | 7 | sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) 8 | 9 | import ctypes 10 | 11 | import mss 12 | 13 | # -- General configuration ------------------------------------------------ 14 | 15 | extensions = [ 16 | "sphinx.ext.autodoc", 17 | "sphinx_copybutton", 18 | "sphinx.ext.intersphinx", 19 | "sphinx_new_tab_link", 20 | ] 21 | templates_path = ["_templates"] 22 | source_suffix = {".rst": "restructuredtext"} 23 | master_doc = "index" 24 | new_tab_link_show_external_link_icon = True 25 | 26 | # General information about the project. 27 | project = "Python MSS" 28 | copyright = f"{mss.__date__}, {mss.__author__} & contributors" # noqa:A001 29 | author = mss.__author__ 30 | version = mss.__version__ 31 | 32 | release = "latest" 33 | language = "en" 34 | todo_include_todos = True 35 | autodoc_member_order = "bysource" 36 | autodoc_default_options = { 37 | "members": True, 38 | "undoc-members": True, 39 | "show-inheritance": True, 40 | } 41 | 42 | # Monkey-patch WINFUNCTYPE into ctypes, so that we can import 43 | # mss.windows while building the documentation. 44 | ctypes.WINFUNCTYPE = ctypes.CFUNCTYPE # type:ignore[attr-defined] 45 | 46 | 47 | # -- Options for HTML output ---------------------------------------------- 48 | 49 | html_theme = "shibuya" 50 | html_theme_options = { 51 | "accent_color": "lime", 52 | "globaltoc_expand_depth": 1, 53 | "toctree_titles_only": False, 54 | } 55 | html_favicon = "../icon.png" 56 | html_context = { 57 | "source_type": "github", 58 | "source_user": "BoboTiG", 59 | "source_repo": "python-mss", 60 | "source_docs_path": "/docs/source/", 61 | "source_version": "main", 62 | } 63 | htmlhelp_basename = "PythonMSSdoc" 64 | 65 | 66 | # -- Options for Epub output ---------------------------------------------- 67 | 68 | # Bibliographic Dublin Core info. 69 | epub_title = project 70 | epub_author = author 71 | epub_publisher = author 72 | epub_copyright = copyright 73 | 74 | # A list of files that should not be packed into the epub file. 75 | epub_exclude_files = ["search.html"] 76 | 77 | 78 | # ---------------------------------------------- 79 | 80 | # Example configuration for intersphinx: refer to the Python standard library. 81 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python MSS 2 | 3 | [![PyPI version](https://badge.fury.io/py/mss.svg)](https://badge.fury.io/py/mss) 4 | [![Anaconda version](https://anaconda.org/conda-forge/python-mss/badges/version.svg)](https://anaconda.org/conda-forge/python-mss) 5 | [![Tests workflow](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml) 6 | [![Downloads](https://static.pepy.tech/personalized-badge/mss?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/mss) 7 | 8 | > [!TIP] 9 | > Become **my boss** to help me work on this awesome software, and make the world better: 10 | > 11 | > [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/mschoentgen) 12 | 13 | ```python 14 | from mss import mss 15 | 16 | # The simplest use, save a screenshot of the 1st monitor 17 | with mss() as sct: 18 | sct.shot() 19 | ``` 20 | 21 | An ultra-fast cross-platform multiple screenshots module in pure python using ctypes. 22 | 23 | - **Python 3.9+**, PEP8 compliant, no dependency, thread-safe; 24 | - very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file; 25 | - but you can use PIL and benefit from all its formats (or add yours directly); 26 | - integrate well with Numpy and OpenCV; 27 | - it could be easily embedded into games and other software which require fast and platform optimized methods to grab screenshots (like AI, Computer Vision); 28 | - get the [source code on GitHub](https://github.com/BoboTiG/python-mss); 29 | - learn with a [bunch of examples](https://python-mss.readthedocs.io/examples.html); 30 | - you can [report a bug](https://github.com/BoboTiG/python-mss/issues); 31 | - need some help? Use the tag *python-mss* on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-mss); 32 | - and there is a [complete, and beautiful, documentation](https://python-mss.readthedocs.io) :) 33 | - **MSS** stands for Multiple ScreenShots; 34 | 35 | 36 | ## Installation 37 | 38 | You can install it with pip: 39 | 40 | ```shell 41 | python -m pip install -U --user mss 42 | ``` 43 | 44 | Or you can install it with Conda: 45 | 46 | ```shell 47 | conda install -c conda-forge python-mss 48 | ``` 49 | 50 | In case of scaling and high DPI issues for external monitors: some packages (e.g. `mouseinfo` / `pyautogui` / `pyscreeze`) incorrectly call `SetProcessDpiAware()` during import process. To prevent that, import `mss` first. 51 | -------------------------------------------------------------------------------- /src/tests/test_macos.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import ctypes.util 6 | import platform 7 | from unittest.mock import patch 8 | 9 | import pytest 10 | 11 | import mss 12 | from mss.exception import ScreenShotError 13 | 14 | if platform.system().lower() != "darwin": 15 | pytestmark = pytest.mark.skip 16 | 17 | import mss.darwin 18 | 19 | 20 | def test_repr() -> None: 21 | # CGPoint 22 | point = mss.darwin.CGPoint(2.0, 1.0) 23 | ref1 = mss.darwin.CGPoint() 24 | ref1.x = 2.0 25 | ref1.y = 1.0 26 | assert repr(point) == repr(ref1) 27 | 28 | # CGSize 29 | size = mss.darwin.CGSize(2.0, 1.0) 30 | ref2 = mss.darwin.CGSize() 31 | ref2.width = 2.0 32 | ref2.height = 1.0 33 | assert repr(size) == repr(ref2) 34 | 35 | # CGRect 36 | rect = mss.darwin.CGRect(point, size) 37 | ref3 = mss.darwin.CGRect() 38 | ref3.origin.x = 2.0 39 | ref3.origin.y = 1.0 40 | ref3.size.width = 2.0 41 | ref3.size.height = 1.0 42 | assert repr(rect) == repr(ref3) 43 | 44 | 45 | def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: 46 | # No `CoreGraphics` library 47 | version = float(".".join(platform.mac_ver()[0].split(".")[:2])) 48 | 49 | if version < 10.16: 50 | monkeypatch.setattr(ctypes.util, "find_library", lambda _: None) 51 | with pytest.raises(ScreenShotError): 52 | mss.mss() 53 | monkeypatch.undo() 54 | 55 | with mss.mss() as sct: 56 | assert isinstance(sct, mss.darwin.MSS) # For Mypy 57 | 58 | # Test monitor's rotation 59 | original = sct.monitors[1] 60 | monkeypatch.setattr(sct.core, "CGDisplayRotation", lambda _: -90.0) 61 | sct._monitors = [] 62 | modified = sct.monitors[1] 63 | assert original["width"] == modified["height"] 64 | assert original["height"] == modified["width"] 65 | monkeypatch.undo() 66 | 67 | # Test bad data retrieval 68 | monkeypatch.setattr(sct.core, "CGWindowListCreateImage", lambda *_: None) 69 | with pytest.raises(ScreenShotError): 70 | sct.grab(sct.monitors[1]) 71 | 72 | 73 | def test_scaling_on() -> None: 74 | """Screnshots are taken at the nominal resolution by default, but scaling can be turned on manually.""" 75 | # Grab a 1x1 screenshot 76 | region = {"top": 0, "left": 0, "width": 1, "height": 1} 77 | 78 | with mss.mss() as sct: 79 | # Nominal resolution, i.e.: scaling is off 80 | assert sct.grab(region).size[0] == 1 81 | 82 | # Retina resolution, i.e.: scaling is on 83 | with patch.object(mss.darwin, "IMAGE_OPTIONS", 0): 84 | assert sct.grab(region).size[0] in {1, 2} # 1 on the CI, 2 for all other the world 85 | -------------------------------------------------------------------------------- /src/tests/test_save.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from collections.abc import Callable 6 | from datetime import datetime 7 | from pathlib import Path 8 | 9 | import pytest 10 | 11 | from mss.base import MSSBase 12 | 13 | try: 14 | from datetime import UTC 15 | except ImportError: 16 | # Python < 3.11 17 | from datetime import timezone 18 | 19 | UTC = timezone.utc 20 | 21 | 22 | def test_at_least_2_monitors(mss_impl: Callable[..., MSSBase]) -> None: 23 | with mss_impl() as sct: 24 | assert list(sct.save(mon=0)) 25 | 26 | 27 | def test_files_exist(mss_impl: Callable[..., MSSBase]) -> None: 28 | with mss_impl() as sct: 29 | for filename in sct.save(): 30 | assert Path(filename).is_file() 31 | 32 | assert Path(sct.shot()).is_file() 33 | 34 | sct.shot(mon=-1, output="fullscreen.png") 35 | assert Path("fullscreen.png").is_file() 36 | 37 | 38 | def test_callback(mss_impl: Callable[..., MSSBase]) -> None: 39 | def on_exists(fname: str) -> None: 40 | file = Path(fname) 41 | if Path(file).is_file(): 42 | file.rename(f"{file.name}.old") 43 | 44 | with mss_impl() as sct: 45 | filename = sct.shot(mon=0, output="mon0.png", callback=on_exists) 46 | assert Path(filename).is_file() 47 | 48 | filename = sct.shot(output="mon1.png", callback=on_exists) 49 | assert Path(filename).is_file() 50 | 51 | 52 | def test_output_format_simple(mss_impl: Callable[..., MSSBase]) -> None: 53 | with mss_impl() as sct: 54 | filename = sct.shot(mon=1, output="mon-{mon}.png") 55 | assert filename == "mon-1.png" 56 | assert Path(filename).is_file() 57 | 58 | 59 | def test_output_format_positions_and_sizes(mss_impl: Callable[..., MSSBase]) -> None: 60 | fmt = "sct-{top}x{left}_{width}x{height}.png" 61 | with mss_impl() as sct: 62 | filename = sct.shot(mon=1, output=fmt) 63 | assert filename == fmt.format(**sct.monitors[1]) 64 | assert Path(filename).is_file() 65 | 66 | 67 | def test_output_format_date_simple(mss_impl: Callable[..., MSSBase]) -> None: 68 | fmt = "sct_{mon}-{date}.png" 69 | with mss_impl() as sct: 70 | try: 71 | filename = sct.shot(mon=1, output=fmt) 72 | assert Path(filename).is_file() 73 | except OSError: 74 | # [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png' 75 | pytest.mark.xfail("Default date format contains ':' which is not allowed.") 76 | 77 | 78 | def test_output_format_date_custom(mss_impl: Callable[..., MSSBase]) -> None: 79 | fmt = "sct_{date:%Y-%m-%d}.png" 80 | with mss_impl() as sct: 81 | filename = sct.shot(mon=1, output=fmt) 82 | assert filename == fmt.format(date=datetime.now(tz=UTC)) 83 | assert Path(filename).is_file() 84 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Python MSS's documentation! 2 | ====================================== 3 | 4 | |PyPI Version| 5 | |PyPI Status| 6 | |PyPI Python Versions| 7 | |GitHub Build Status| 8 | |GitHub License| 9 | 10 | |Patreon| 11 | 12 | .. code-block:: python 13 | 14 | from mss import mss 15 | 16 | # The simplest use, save a screenshot of the 1st monitor 17 | with mss() as sct: 18 | sct.shot() 19 | 20 | 21 | An ultra fast cross-platform multiple screenshots module in pure python using ctypes. 22 | 23 | - **Python 3.9+**, :pep:`8` compliant, no dependency, thread-safe; 24 | - very basic, it will grab one screenshot by monitor or a screenshot of all monitors and save it to a PNG file; 25 | - but you can use PIL and benefit from all its formats (or add yours directly); 26 | - integrate well with Numpy and OpenCV; 27 | - it could be easily embedded into games and other software which require fast and platform optimized methods to grab screenshots (like AI, Computer Vision); 28 | - get the `source code on GitHub `_; 29 | - learn with a `bunch of examples `_; 30 | - you can `report a bug `_; 31 | - need some help? Use the tag *python-mss* on `Stack Overflow `_; 32 | - **MSS** stands for Multiple ScreenShots; 33 | 34 | +-------------------------+ 35 | | Content | 36 | +-------------------------+ 37 | |.. toctree:: | 38 | | :maxdepth: 1 | 39 | | | 40 | | installation | 41 | | usage | 42 | | examples | 43 | | support | 44 | | api | 45 | | developers | 46 | | where | 47 | +-------------------------+ 48 | 49 | Indices and tables 50 | ================== 51 | 52 | * :ref:`genindex` 53 | * :ref:`search` 54 | 55 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/mss.svg 56 | :target: https://pypi.python.org/pypi/mss/ 57 | .. |PyPI Status| image:: https://img.shields.io/pypi/status/mss.svg 58 | :target: https://pypi.python.org/pypi/mss/ 59 | .. |PyPI Python Versions| image:: https://img.shields.io/pypi/pyversions/mss.svg 60 | :target: https://pypi.python.org/pypi/mss/ 61 | .. |Github Build Status| image:: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml/badge.svg?branch=main 62 | :target: https://github.com/BoboTiG/python-mss/actions/workflows/tests.yml 63 | .. |GitHub License| image:: https://img.shields.io/github/license/BoboTiG/python-mss.svg 64 | :target: https://github.com/BoboTiG/python-mss/blob/main/LICENSE.txt 65 | .. |Patreon| image:: https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white 66 | :target: https://www.patreon.com/mschoentgen 67 | -------------------------------------------------------------------------------- /src/mss/linux/__init__.py: -------------------------------------------------------------------------------- 1 | """GNU/Linux backend dispatcher for X11 screenshot implementations.""" 2 | 3 | from typing import Any 4 | 5 | from mss.base import MSSBase 6 | from mss.exception import ScreenShotError 7 | 8 | BACKENDS = ["default", "xlib", "xgetimage", "xshmgetimage"] 9 | 10 | 11 | def mss(backend: str = "default", **kwargs: Any) -> MSSBase: 12 | """Return a backend-specific MSS implementation for GNU/Linux. 13 | 14 | Selects and instantiates the appropriate X11 backend based on the 15 | ``backend`` parameter. 16 | 17 | :param backend: Backend selector. Valid values: 18 | 19 | - ``"default"`` or ``"xshmgetimage"`` (default): XCB-based backend 20 | using XShmGetImage with automatic fallback to XGetImage when MIT-SHM 21 | is unavailable; see :py:class:`mss.linux.xshmgetimage.MSS`. 22 | - ``"xgetimage"``: XCB-based backend using XGetImage; 23 | see :py:class:`mss.linux.xgetimage.MSS`. 24 | - ``"xlib"``: Legacy Xlib-based backend retained for environments 25 | without working XCB libraries; see :py:class:`mss.linux.xlib.MSS`. 26 | 27 | .. versionadded:: 10.2.0 Prior to this version, the 28 | :class:`mss.linux.xlib.MSS` implementation was the only available 29 | backend. 30 | 31 | :param display: Optional keyword argument. Specifies an X11 display 32 | string to connect to. The default is taken from the environment 33 | variable :envvar:`DISPLAY`. 34 | :type display: str | bytes | None 35 | :param kwargs: Additional keyword arguments passed to the backend class. 36 | :returns: An MSS backend implementation. 37 | 38 | .. versionadded:: 10.2.0 Prior to this version, this didn't exist: 39 | the :func:`mss.linux.MSS` was a class equivalent to the current 40 | :class:`mss.linux.xlib.MSS` implementation. 41 | """ 42 | backend = backend.lower() 43 | if backend == "xlib": 44 | from . import xlib # noqa: PLC0415 45 | 46 | return xlib.MSS(**kwargs) 47 | if backend == "xgetimage": 48 | from . import xgetimage # noqa: PLC0415 49 | 50 | # Note that the xshmgetimage backend will automatically fall back to XGetImage calls if XShmGetImage isn't 51 | # available. The only reason to use the xgetimage backend is if the user already knows that XShmGetImage 52 | # isn't going to be supported. 53 | return xgetimage.MSS(**kwargs) 54 | if backend in {"default", "xshmgetimage"}: 55 | from . import xshmgetimage # noqa: PLC0415 56 | 57 | return xshmgetimage.MSS(**kwargs) 58 | assert backend not in BACKENDS # noqa: S101 59 | msg = f"Backend {backend!r} not (yet?) implemented." 60 | raise ScreenShotError(msg) 61 | 62 | 63 | # Alias in upper-case for backward compatibility. This is a supported name in the docs. 64 | def MSS(*args, **kwargs) -> MSSBase: # type: ignore[no-untyped-def] # noqa: N802, ANN002, ANN003 65 | """Alias for :func:`mss.linux.mss.mss` for backward compatibility. 66 | 67 | .. versionchanged:: 10.2.0 Prior to this version, this was a class. 68 | .. deprecated:: 10.2.0 Use :func:`mss.linux.mss` instead. 69 | """ 70 | return mss(*args, **kwargs) 71 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | concurrency: 12 | group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name != 'pull_request' && github.sha || '' }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | quality: 17 | name: Quality 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: actions/setup-python@v6 22 | with: 23 | python-version: "3.x" 24 | cache: pip 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install -U pip 28 | python -m pip install -e '.[dev]' 29 | - name: Check 30 | run: ./check.sh 31 | 32 | documentation: 33 | name: Documentation 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v6 37 | - uses: actions/setup-python@v6 38 | with: 39 | python-version: "3.x" 40 | cache: pip 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install -U pip 44 | python -m pip install -e '.[docs]' 45 | - name: Build 46 | run: | 47 | sphinx-build -d docs docs/source docs_out --color -W -bhtml 48 | 49 | tests: 50 | name: "${{ matrix.os.emoji }} ${{ matrix.python.name }}" 51 | runs-on: ${{ matrix.os.runs-on }} 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | os: 56 | - emoji: 🐧 57 | runs-on: [ubuntu-latest] 58 | - emoji: 🍎 59 | runs-on: [macos-latest] 60 | - emoji: 🪟 61 | runs-on: [windows-latest] 62 | python: 63 | - name: CPython 3.9 64 | runs-on: "3.9" 65 | - name: CPython 3.10 66 | runs-on: "3.10" 67 | - name: CPython 3.11 68 | runs-on: "3.11" 69 | - name: CPython 3.12 70 | runs-on: "3.12" 71 | - name: CPython 3.13 72 | runs-on: "3.13" 73 | - name: CPython 3.14 74 | runs-on: "3.14-dev" 75 | steps: 76 | - uses: actions/checkout@v6 77 | - uses: actions/setup-python@v6 78 | with: 79 | python-version: ${{ matrix.python.runs-on }} 80 | cache: pip 81 | check-latest: true 82 | - name: Install dependencies 83 | run: | 84 | python -m pip install -U pip 85 | python -m pip install -e '.[dev,tests]' 86 | - name: Tests (GNU/Linux) 87 | if: matrix.os.emoji == '🐧' 88 | run: xvfb-run python -m pytest 89 | - name: Tests (macOS, Windows) 90 | if: matrix.os.emoji != '🐧' 91 | run: python -m pytest 92 | 93 | automerge: 94 | name: Automerge 95 | runs-on: ubuntu-latest 96 | needs: [documentation, quality, tests] 97 | if: ${{ github.actor == 'dependabot[bot]' }} 98 | steps: 99 | - name: Automerge 100 | run: gh pr merge --auto --rebase "$PR_URL" 101 | env: 102 | PR_URL: ${{github.event.pull_request.html_url}} 103 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 104 | -------------------------------------------------------------------------------- /docs/source/where.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Who Uses it? 3 | ============ 4 | 5 | This is a non exhaustive list where MSS is integrated or has inspired. 6 | Do not hesitate to `say Hello! `_ if you are using MSS too. 7 | 8 | - Nvidia; 9 | - `Airtest `_, a cross-platform UI automation framework for aames and apps; 10 | - `Automation Framework `_, a Batmans utility; 11 | - `DeepEye `_, a deep vision-based software library for autonomous and advanced driver-assistance systems; 12 | - `Diablo 4 Loot Filter `_; 13 | - `DoomPy `_ (Autonomous Anti-Demonic Combat Algorithms); 14 | - `Europilot `_, a self-driving algorithm using Euro Truck Simulator (ETS2); 15 | - `Flexx Python UI toolkit `_; 16 | - `Go Review Partner `_, a tool to help analyse and review your game of go (weiqi, baduk) using strong bots; 17 | - `Gradient Sampler `_, sample blender gradients from anything on the screen; 18 | - `gym-mupen64plus `_, an OpenAI Gym environment wrapper for the Mupen64Plus N64 emulator; 19 | - `NativeShot `_ (Mozilla Firefox module); 20 | - `NCTU Scratch and Python, 2017 Spring `_ (Python course); 21 | - `normcap `_, OCR powered screen-capture tool to capture information instead of images; 22 | - `Open Source Self Driving Car Initiative `_; 23 | - `OSRS Bot COLOR (OSBC) `_, a lightweight desktop client for controlling and monitoring color-based automation scripts (bots) for OSRS and private server alternatives; 24 | - `Philips Hue Lights Ambiance `_; 25 | - `Pombo `_, a thief recovery software; 26 | - `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; 27 | - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; 28 | - `ScreenCapLibrary `_, a Robot Framework test library for capturing screenshots and video recording; 29 | - `ScreenVivid `_, an open source cross-platform screen recorder for everyone ; 30 | - `Self-Driving-Car-3D-Simulator-With-CNN `_; 31 | - `Serpent.AI `_, a Game Agent Framework; 32 | - `Star Wars - The Old Republic: Galactic StarFighter `_ parser; 33 | - `Stitch `_, a Python Remote Administration Tool (RAT); 34 | - `TensorKart `_, a self-driving MarioKart with TensorFlow; 35 | - `videostream_censor `_, a real time video recording censor ; 36 | - `wow-fishing-bot `_, a fishing bot for World of Warcraft that uses template matching from OpenCV; 37 | - `Zelda Bowling AI `_; 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@tiger-222.fr. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/tests/test_windows.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import threading 8 | 9 | import pytest 10 | 11 | import mss 12 | from mss.exception import ScreenShotError 13 | 14 | try: 15 | import mss.windows 16 | except ImportError: 17 | pytestmark = pytest.mark.skip 18 | 19 | 20 | def test_implementation(monkeypatch: pytest.MonkeyPatch) -> None: 21 | # Test bad data retrieval 22 | with mss.mss() as sct: 23 | assert isinstance(sct, mss.windows.MSS) # For Mypy 24 | 25 | monkeypatch.setattr(sct.gdi32, "GetDIBits", lambda *_: 0) 26 | with pytest.raises(ScreenShotError): 27 | sct.shot() 28 | 29 | 30 | def test_region_caching() -> None: 31 | """The region to grab is cached, ensure this is well-done.""" 32 | with mss.mss() as sct: 33 | assert isinstance(sct, mss.windows.MSS) # For Mypy 34 | 35 | # Grab the area 1 36 | region1 = {"top": 0, "left": 0, "width": 200, "height": 200} 37 | sct.grab(region1) 38 | bmp1 = id(sct._handles.bmp) 39 | 40 | # Grab the area 2, the cached BMP is used 41 | # Same sizes but different positions 42 | region2 = {"top": 200, "left": 200, "width": 200, "height": 200} 43 | sct.grab(region2) 44 | bmp2 = id(sct._handles.bmp) 45 | assert bmp1 == bmp2 46 | 47 | # Grab the area 2 again, the cached BMP is used 48 | sct.grab(region2) 49 | assert bmp2 == id(sct._handles.bmp) 50 | 51 | 52 | def test_region_not_caching() -> None: 53 | """The region to grab is not bad cached previous grab.""" 54 | grab1 = mss.mss() 55 | grab2 = mss.mss() 56 | 57 | assert isinstance(grab1, mss.windows.MSS) # For Mypy 58 | assert isinstance(grab2, mss.windows.MSS) # For Mypy 59 | 60 | region1 = {"top": 0, "left": 0, "width": 100, "height": 100} 61 | region2 = {"top": 0, "left": 0, "width": 50, "height": 1} 62 | grab1.grab(region1) 63 | bmp1 = id(grab1._handles.bmp) 64 | grab2.grab(region2) 65 | bmp2 = id(grab2._handles.bmp) 66 | assert bmp1 != bmp2 67 | 68 | # Grab the area 1, is not bad cached BMP previous grab the area 2 69 | grab1.grab(region1) 70 | bmp1 = id(grab1._handles.bmp) 71 | assert bmp1 != bmp2 72 | 73 | 74 | def run_child_thread(loops: int) -> None: 75 | for _ in range(loops): 76 | with mss.mss() as sct: # New sct for every loop 77 | sct.grab(sct.monitors[1]) 78 | 79 | 80 | def test_thread_safety() -> None: 81 | """Thread safety test for issue #150. 82 | 83 | The following code will throw a ScreenShotError exception if thread-safety is not guaranteed. 84 | """ 85 | # Let thread 1 finished ahead of thread 2 86 | thread1 = threading.Thread(target=run_child_thread, args=(30,)) 87 | thread2 = threading.Thread(target=run_child_thread, args=(50,)) 88 | thread1.start() 89 | thread2.start() 90 | thread1.join() 91 | thread2.join() 92 | 93 | 94 | def run_child_thread_bbox(loops: int, bbox: tuple[int, int, int, int]) -> None: 95 | with mss.mss() as sct: # One sct for all loops 96 | for _ in range(loops): 97 | sct.grab(bbox) 98 | 99 | 100 | def test_thread_safety_regions() -> None: 101 | """Thread safety test for different regions. 102 | 103 | The following code will throw a ScreenShotError exception if thread-safety is not guaranteed. 104 | """ 105 | thread1 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 100, 100))) 106 | thread2 = threading.Thread(target=run_child_thread_bbox, args=(100, (0, 0, 50, 1))) 107 | thread1.start() 108 | thread2.start() 109 | thread1.join() 110 | thread2.join() 111 | -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import os 6 | from collections.abc import Callable, Generator 7 | from hashlib import sha256 8 | from pathlib import Path 9 | from platform import system 10 | from zipfile import ZipFile 11 | 12 | import pytest 13 | 14 | from mss import mss 15 | from mss.base import MSSBase 16 | from mss.linux import xcb, xlib 17 | 18 | 19 | @pytest.fixture(autouse=True) 20 | def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: 21 | """Fail on warning.""" 22 | yield 23 | 24 | warnings = [f"{warning.filename}:{warning.lineno} {warning.message}" for warning in recwarn] 25 | for warning in warnings: 26 | print(warning) 27 | assert not warnings 28 | 29 | 30 | def purge_files() -> None: 31 | """Remove all generated files from previous runs.""" 32 | for file in Path().glob("*.png"): 33 | print(f"Deleting {file} ...") 34 | file.unlink() 35 | 36 | for file in Path().glob("*.png.old"): 37 | print(f"Deleting {file} ...") 38 | file.unlink() 39 | 40 | 41 | @pytest.fixture(scope="module", autouse=True) 42 | def _before_tests() -> None: 43 | purge_files() 44 | 45 | 46 | @pytest.fixture(autouse=True) 47 | def no_xlib_errors(request: pytest.FixtureRequest) -> None: 48 | system() == "Linux" and ("backend" not in request.fixturenames or request.getfixturevalue("backend") == "xlib") 49 | assert not xlib._ERROR 50 | 51 | 52 | @pytest.fixture(autouse=True) 53 | def reset_xcb_libraries(request: pytest.FixtureRequest) -> Generator[None]: 54 | # We need to test this before we yield, since the backend isn't available afterwards. 55 | xcb_should_reset = system() == "Linux" and ( 56 | "backend" not in request.fixturenames or request.getfixturevalue("backend") == "xcb" 57 | ) 58 | yield None 59 | if xcb_should_reset: 60 | xcb.LIB.reset() 61 | 62 | 63 | @pytest.fixture(scope="session") 64 | def raw() -> bytes: 65 | file = Path(__file__).parent / "res" / "monitor-1024x768.raw.zip" 66 | with ZipFile(file) as fh: 67 | data = fh.read(file.with_suffix("").name) 68 | 69 | assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd" 70 | return data 71 | 72 | 73 | @pytest.fixture(params=["xlib", "xgetimage", "xshmgetimage"] if system() == "Linux" else ["default"]) 74 | def backend(request: pytest.FixtureRequest) -> str: 75 | return request.param 76 | 77 | 78 | @pytest.fixture 79 | def mss_impl(backend: str) -> Callable[..., MSSBase]: 80 | # We can't just use partial here, since it will read $DISPLAY at the wrong time. This can cause problems, 81 | # depending on just how the fixtures get run. 82 | return lambda *args, **kwargs: mss(*args, display=os.getenv("DISPLAY"), backend=backend, **kwargs) 83 | 84 | 85 | @pytest.fixture(autouse=True, scope="session") 86 | def inhibit_x11_resets() -> Generator[None, None, None]: 87 | """Ensure that an X11 connection is open during the test session. 88 | 89 | Under X11, when the last client disconnects, the server resets. If 90 | a new client tries to connect before the reset is complete, it may fail. 91 | Since we often run the tests under Xvfb, they're frequently the only 92 | clients. Since our tests run in rapid succession, this combination 93 | can lead to intermittent failures. 94 | 95 | To avoid this, we open a connection at the start of the test session 96 | and keep it open until the end. 97 | """ 98 | if system() != "Linux": 99 | yield 100 | return 101 | 102 | conn, _ = xcb.connect() 103 | try: 104 | yield 105 | finally: 106 | # Some tests may have reset xcb.LIB, so make sure it's currently initialized. 107 | xcb.initialize() 108 | xcb.disconnect(conn) 109 | -------------------------------------------------------------------------------- /src/mss/__main__.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import os.path 6 | import platform 7 | import sys 8 | from argparse import ArgumentError, ArgumentParser 9 | 10 | from mss import __version__ 11 | from mss.exception import ScreenShotError 12 | from mss.factory import mss 13 | from mss.tools import to_png 14 | 15 | 16 | def _backend_cli_choices() -> list[str]: 17 | os_name = platform.system().lower() 18 | if os_name == "darwin": 19 | from mss import darwin # noqa: PLC0415 20 | 21 | return list(darwin.BACKENDS) 22 | if os_name == "linux": 23 | from mss import linux # noqa: PLC0415 24 | 25 | return list(linux.BACKENDS) 26 | if os_name == "windows": 27 | from mss import windows # noqa: PLC0415 28 | 29 | return list(windows.BACKENDS) 30 | return ["default"] 31 | 32 | 33 | def main(*args: str) -> int: 34 | """Main logic.""" 35 | backend_choices = _backend_cli_choices() 36 | 37 | cli_args = ArgumentParser(prog="mss", exit_on_error=False) 38 | cli_args.add_argument( 39 | "-c", 40 | "--coordinates", 41 | default="", 42 | type=str, 43 | help="the part of the screen to capture: top, left, width, height", 44 | ) 45 | cli_args.add_argument( 46 | "-l", 47 | "--level", 48 | default=6, 49 | type=int, 50 | choices=list(range(10)), 51 | help="the PNG compression level", 52 | ) 53 | cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screenshot") 54 | cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name") 55 | cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor") 56 | cli_args.add_argument( 57 | "-q", 58 | "--quiet", 59 | default=False, 60 | action="store_true", 61 | help="do not print created files", 62 | ) 63 | cli_args.add_argument( 64 | "-b", "--backend", default="default", choices=backend_choices, help="platform-specific backend to use" 65 | ) 66 | cli_args.add_argument("-v", "--version", action="version", version=__version__) 67 | 68 | try: 69 | options = cli_args.parse_args(args or None) 70 | except ArgumentError as e: 71 | # By default, parse_args will print and the error and exit. We 72 | # return instead of exiting, to make unit testing easier. 73 | cli_args.print_usage(sys.stderr) 74 | print(f"{cli_args.prog}: error: {e}", file=sys.stderr) 75 | return 2 76 | kwargs = {"mon": options.monitor, "output": options.output} 77 | if options.coordinates: 78 | try: 79 | top, left, width, height = options.coordinates.split(",") 80 | except ValueError: 81 | print("Coordinates syntax: top, left, width, height") 82 | return 2 83 | 84 | kwargs["mon"] = { 85 | "top": int(top), 86 | "left": int(left), 87 | "width": int(width), 88 | "height": int(height), 89 | } 90 | if options.output == "monitor-{mon}.png": 91 | kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png" 92 | 93 | try: 94 | with mss(with_cursor=options.with_cursor, backend=options.backend) as sct: 95 | if options.coordinates: 96 | output = kwargs["output"].format(**kwargs["mon"]) 97 | sct_img = sct.grab(kwargs["mon"]) 98 | to_png(sct_img.rgb, sct_img.size, level=options.level, output=output) 99 | if not options.quiet: 100 | print(os.path.realpath(output)) 101 | else: 102 | for file_name in sct.save(**kwargs): 103 | if not options.quiet: 104 | print(os.path.realpath(file_name)) 105 | return 0 106 | except ScreenShotError: 107 | if options.quiet: 108 | return 1 109 | raise 110 | 111 | 112 | if __name__ == "__main__": # pragma: nocover 113 | try: 114 | sys.exit(main()) 115 | except ScreenShotError as exc: 116 | print("[ERROR]", exc) 117 | sys.exit(1) 118 | -------------------------------------------------------------------------------- /src/tests/test_leaks.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import ctypes 6 | import os 7 | import platform 8 | import subprocess 9 | from collections.abc import Callable 10 | 11 | import pytest 12 | 13 | import mss 14 | 15 | OS = platform.system().lower() 16 | PID = os.getpid() 17 | 18 | 19 | def get_opened_socket() -> int: 20 | """GNU/Linux: a way to get the opened sockets count. 21 | It will be used to check X server connections are well closed. 22 | """ 23 | output = subprocess.check_output(["lsof", "-a", "-U", "-Ff", f"-p{PID}"]) 24 | # The first line will be "p{PID}". The remaining lines start with "f", one per open socket. 25 | return len([line for line in output.splitlines() if line.startswith(b"f")]) 26 | 27 | 28 | def get_handles() -> int: 29 | """Windows: a way to get the GDI handles count. 30 | It will be used to check the handles count is not growing, showing resource leaks. 31 | """ 32 | PROCESS_QUERY_INFORMATION = 0x400 # noqa:N806 33 | GR_GDIOBJECTS = 0 # noqa:N806 34 | h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, PID) 35 | return ctypes.windll.user32.GetGuiResources(h, GR_GDIOBJECTS) 36 | 37 | 38 | @pytest.fixture 39 | def monitor_func() -> Callable[[], int]: 40 | """OS specific function to check resources in use.""" 41 | return get_opened_socket if OS == "linux" else get_handles 42 | 43 | 44 | def bound_instance_without_cm(*, backend: str) -> None: 45 | # Will always leak 46 | sct = mss.mss(backend=backend) 47 | sct.shot() 48 | 49 | 50 | def bound_instance_without_cm_but_use_close(*, backend: str) -> None: 51 | sct = mss.mss(backend=backend) 52 | sct.shot() 53 | sct.close() 54 | # Calling .close() twice should be possible 55 | sct.close() 56 | 57 | 58 | def unbound_instance_without_cm(*, backend: str) -> None: 59 | # Will always leak 60 | mss.mss(backend=backend).shot() 61 | 62 | 63 | def with_context_manager(*, backend: str) -> None: 64 | with mss.mss(backend=backend) as sct: 65 | sct.shot() 66 | 67 | 68 | def regression_issue_128(*, backend: str) -> None: 69 | """Regression test for issue #128: areas overlap.""" 70 | with mss.mss(backend=backend) as sct: 71 | area1 = {"top": 50, "left": 7, "width": 400, "height": 320, "mon": 1} 72 | sct.grab(area1) 73 | area2 = {"top": 200, "left": 200, "width": 320, "height": 320, "mon": 1} 74 | sct.grab(area2) 75 | 76 | 77 | def regression_issue_135(*, backend: str) -> None: 78 | """Regression test for issue #135: multiple areas.""" 79 | with mss.mss(backend=backend) as sct: 80 | bounding_box_notes = {"top": 0, "left": 0, "width": 100, "height": 100} 81 | sct.grab(bounding_box_notes) 82 | bounding_box_test = {"top": 220, "left": 220, "width": 100, "height": 100} 83 | sct.grab(bounding_box_test) 84 | bounding_box_score = {"top": 110, "left": 110, "width": 100, "height": 100} 85 | sct.grab(bounding_box_score) 86 | 87 | 88 | def regression_issue_210(*, backend: str) -> None: 89 | """Regression test for issue #210: multiple X servers.""" 90 | pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") 91 | 92 | with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(backend=backend): 93 | pass 94 | 95 | with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(backend=backend): 96 | pass 97 | 98 | 99 | @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") 100 | @pytest.mark.parametrize( 101 | "func", 102 | [ 103 | # bound_instance_without_cm, 104 | bound_instance_without_cm_but_use_close, 105 | # unbound_instance_without_cm, 106 | with_context_manager, 107 | regression_issue_128, 108 | regression_issue_135, 109 | regression_issue_210, 110 | ], 111 | ) 112 | def test_resource_leaks(func: Callable[..., None], monitor_func: Callable[[], int], backend: str) -> None: 113 | """Check for resource leaks with different use cases.""" 114 | # Warm-up 115 | func(backend=backend) 116 | 117 | original_resources = monitor_func() 118 | allocated_resources = 0 119 | 120 | for _ in range(5): 121 | func(backend=backend) 122 | new_resources = monitor_func() 123 | allocated_resources = max(allocated_resources, new_resources) 124 | 125 | assert allocated_resources <= original_resources 126 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Import 6 | ====== 7 | 8 | MSS can be used as simply as:: 9 | 10 | from mss import mss 11 | 12 | Or import the good one based on your operating system:: 13 | 14 | # GNU/Linux 15 | from mss.linux import MSS as mss 16 | 17 | # macOS 18 | from mss.darwin import MSS as mss 19 | 20 | # Microsoft Windows 21 | from mss.windows import MSS as mss 22 | 23 | On GNU/Linux you can also import a specific backend (see :ref:`backends`) 24 | directly when you need a particular implementation, for example:: 25 | 26 | from mss.linux.xshmgetimage import MSS as mss 27 | 28 | 29 | Instance 30 | ======== 31 | 32 | So the module can be used as simply as:: 33 | 34 | with mss() as sct: 35 | # ... 36 | 37 | Intensive Use 38 | ============= 39 | 40 | If you plan to integrate MSS inside your own module or software, pay attention to using it wisely. 41 | 42 | This is a bad usage:: 43 | 44 | for _ in range(100): 45 | with mss() as sct: 46 | sct.shot() 47 | 48 | This is a much better usage, memory efficient:: 49 | 50 | with mss() as sct: 51 | for _ in range(100): 52 | sct.shot() 53 | 54 | Also, it is a good thing to save the MSS instance inside an attribute of your class and calling it when needed. 55 | 56 | 57 | .. _backends: 58 | 59 | Backends 60 | -------- 61 | 62 | Some platforms have multiple ways to take screenshots. In MSS, these are known as *backends*. The :py:func:`mss` functions will normally autodetect which one is appropriate for your situation, but you can override this if you want. For instance, you may know that your specific situation requires a particular backend. 63 | 64 | If you want to choose a particular backend, you can use the :py:attr:`backend` keyword to :py:func:`mss`:: 65 | 66 | with mss(backend="xgetimage") as sct: 67 | ... 68 | 69 | Alternatively, you can also directly import the backend you want to use:: 70 | 71 | from mss.linux.xgetimage import MSS as mss 72 | 73 | Currently, only the GNU/Linux implementation has multiple backends. These are described in their own section below. 74 | 75 | 76 | GNU/Linux 77 | --------- 78 | 79 | Display 80 | ^^^^^^^ 81 | 82 | On GNU/Linux, the default display is taken from the :envvar:`DISPLAY` environment variable. You can instead specify which display to use (useful for distant screenshots via SSH) using the ``display`` keyword: 83 | 84 | .. literalinclude:: examples/linux_display_keyword.py 85 | :lines: 7- 86 | 87 | 88 | Backends 89 | ^^^^^^^^ 90 | 91 | The GNU/Linux implementation has multiple backends (see :ref:`backends`), or ways it can take screenshots. The :py:func:`mss.mss` and :py:func:`mss.linux.mss` functions will normally autodetect which one is appropriate, but you can override this if you want. 92 | 93 | There are three available backends. 94 | 95 | :py:mod:`xshmgetimage` (default) 96 | The fastest backend, based on :c:func:`xcb_shm_get_image`. It is roughly three times faster than :py:mod:`xgetimage` 97 | and is used automatically. When the MIT-SHM extension is unavailable (for example on remote SSH displays), it 98 | transparently falls back to :py:mod:`xgetimage` so you can always request it safely. 99 | 100 | :py:mod:`xgetimage` 101 | A highly-compatible, but slower, backend based on :c:func:`xcb_get_image`. Use this explicitly only when you know 102 | that :py:mod:`xshmgetimage` cannot operate in your environment. 103 | 104 | :py:mod:`xlib` 105 | The legacy backend powered by :c:func:`XGetImage`. It is kept solely for systems where XCB libraries are 106 | unavailable and no new features are being added to it. 107 | 108 | 109 | Command Line 110 | ============ 111 | 112 | You can use ``mss`` via the CLI:: 113 | 114 | mss --help 115 | 116 | Or via direct call from Python:: 117 | 118 | $ python -m mss --help 119 | usage: mss [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}] [-m MONITOR] 120 | [-o OUTPUT] [--with-cursor] [-q] [-b BACKEND] [-v] 121 | 122 | options: 123 | -h, --help show this help message and exit 124 | -c COORDINATES, --coordinates COORDINATES 125 | the part of the screen to capture: top, left, width, height 126 | -l {0,1,2,3,4,5,6,7,8,9}, --level {0,1,2,3,4,5,6,7,8,9} 127 | the PNG compression level 128 | -m MONITOR, --monitor MONITOR 129 | the monitor to screenshot 130 | -o OUTPUT, --output OUTPUT 131 | the output file name 132 | -b, --backend BACKEND 133 | platform-specific backend to use 134 | (Linux: default/xlib/xgetimage/xshmgetimage; macOS/Windows: default) 135 | --with-cursor include the cursor 136 | -q, --quiet do not print created files 137 | -v, --version show program's version number and exit 138 | 139 | .. versionadded:: 3.1.1 140 | 141 | .. versionadded:: 8.0.0 142 | ``--with-cursor`` to include the cursor in screenshots. 143 | 144 | .. versionadded:: 10.2.0 145 | ``--backend`` to force selecting the backend to use. 146 | -------------------------------------------------------------------------------- /src/mss/screenshot.py: -------------------------------------------------------------------------------- 1 | # This is part of the MSS Python's module. 2 | # Source: https://github.com/BoboTiG/python-mss. 3 | 4 | from __future__ import annotations 5 | 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from mss.exception import ScreenShotError 9 | from mss.models import Monitor, Pixel, Pixels, Pos, Size 10 | 11 | if TYPE_CHECKING: # pragma: nocover 12 | from collections.abc import Iterator 13 | 14 | 15 | class ScreenShot: 16 | """Screenshot object. 17 | 18 | .. note:: 19 | 20 | A better name would have been *Image*, but to prevent collisions 21 | with PIL.Image, it has been decided to use *ScreenShot*. 22 | """ 23 | 24 | __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} 25 | 26 | def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = None) -> None: 27 | self.__pixels: Pixels | None = None 28 | self.__rgb: bytes | None = None 29 | 30 | #: Bytearray of the raw BGRA pixels retrieved by ctypes 31 | #: OS independent implementations. 32 | self.raw: bytearray = data 33 | 34 | #: NamedTuple of the screenshot coordinates. 35 | self.pos: Pos = Pos(monitor["left"], monitor["top"]) 36 | 37 | #: NamedTuple of the screenshot size. 38 | self.size: Size = Size(monitor["width"], monitor["height"]) if size is None else size 39 | 40 | def __repr__(self) -> str: 41 | return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>" 42 | 43 | @property 44 | def __array_interface__(self) -> dict[str, Any]: 45 | """NumPy array interface support. 46 | 47 | This is used by NumPy, many SciPy projects, CuPy, PyTorch (via 48 | ``torch.from_numpy``), TensorFlow (via ``tf.convert_to_tensor``), 49 | JAX (via ``jax.numpy.asarray``), Pandas, scikit-learn, Matplotlib, 50 | some OpenCV functions, and others. This allows you to pass a 51 | :class:`ScreenShot` instance directly to these libraries without 52 | needing to convert it first. 53 | 54 | This is in HWC order, with 4 channels (BGRA). 55 | 56 | .. seealso:: 57 | 58 | https://numpy.org/doc/stable/reference/arrays.interface.html 59 | The NumPy array interface protocol specification 60 | """ 61 | return { 62 | "version": 3, 63 | "shape": (self.height, self.width, 4), 64 | "typestr": "|u1", 65 | "data": self.raw, 66 | } 67 | 68 | @classmethod 69 | def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, /) -> ScreenShot: 70 | """Instantiate a new class given only screenshot's data and size.""" 71 | monitor = {"left": 0, "top": 0, "width": width, "height": height} 72 | return cls(data, monitor) 73 | 74 | @property 75 | def bgra(self) -> bytes: 76 | """BGRx values from the BGRx raw pixels. 77 | 78 | The format is a bytes object with BGRxBGRx... sequence. A specific 79 | pixel can be accessed as 80 | ``bgra[(y * width + x) * 4:(y * width + x) * 4 + 4].`` 81 | 82 | .. note:: 83 | While the name is ``bgra``, the alpha channel may or may not be 84 | valid. 85 | """ 86 | return bytes(self.raw) 87 | 88 | @property 89 | def pixels(self) -> Pixels: 90 | """RGB tuples. 91 | 92 | The format is a list of rows. Each row is a list of pixels. 93 | Each pixel is a tuple of (R, G, B). 94 | """ 95 | if not self.__pixels: 96 | rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4]) 97 | self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) 98 | 99 | return self.__pixels 100 | 101 | def pixel(self, coord_x: int, coord_y: int) -> Pixel: 102 | """Return the pixel value at a given position. 103 | 104 | :returns: A tuple of (R, G, B) values. 105 | """ 106 | try: 107 | return self.pixels[coord_y][coord_x] 108 | except IndexError as exc: 109 | msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." 110 | raise ScreenShotError(msg) from exc 111 | 112 | @property 113 | def rgb(self) -> bytes: 114 | """Compute RGB values from the BGRA raw pixels. 115 | 116 | The format is a bytes object with BGRBGR... sequence. A specific 117 | pixel can be accessed as 118 | ``rgb[(y * width + x) * 3:(y * width + x) * 3 + 3]``. 119 | """ 120 | if not self.__rgb: 121 | rgb = bytearray(self.height * self.width * 3) 122 | raw = self.raw 123 | rgb[::3] = raw[2::4] 124 | rgb[1::3] = raw[1::4] 125 | rgb[2::3] = raw[::4] 126 | self.__rgb = bytes(rgb) 127 | 128 | return self.__rgb 129 | 130 | @property 131 | def top(self) -> int: 132 | """Convenient accessor to the top position.""" 133 | return self.pos.top 134 | 135 | @property 136 | def left(self) -> int: 137 | """Convenient accessor to the left position.""" 138 | return self.pos.left 139 | 140 | @property 141 | def width(self) -> int: 142 | """Convenient accessor to the width size.""" 143 | return self.size.width 144 | 145 | @property 146 | def height(self) -> int: 147 | """Convenient accessor to the height size.""" 148 | return self.size.height 149 | -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Examples 3 | ======== 4 | 5 | Basics 6 | ====== 7 | 8 | One screenshot per monitor 9 | -------------------------- 10 | :: 11 | 12 | for filename in sct.save(): 13 | print(filename) 14 | 15 | Screenshot of the monitor 1 16 | --------------------------- 17 | :: 18 | 19 | filename = sct.shot() 20 | print(filename) 21 | 22 | A screenshot to grab them all 23 | ----------------------------- 24 | :: 25 | 26 | filename = sct.shot(mon=-1, output='fullscreen.png') 27 | print(filename) 28 | 29 | Callback 30 | -------- 31 | 32 | Screenshot of the monitor 1 with a callback: 33 | 34 | .. literalinclude:: examples/callback.py 35 | :lines: 7- 36 | 37 | 38 | Part of the screen 39 | ------------------ 40 | 41 | You can capture only a part of the screen: 42 | 43 | .. literalinclude:: examples/part_of_screen.py 44 | :lines: 7- 45 | 46 | .. versionadded:: 3.0.0 47 | 48 | 49 | Part of the screen of the 2nd monitor 50 | ------------------------------------- 51 | 52 | This is an example of capturing some part of the screen of the monitor 2: 53 | 54 | .. literalinclude:: examples/part_of_screen_monitor_2.py 55 | :lines: 7- 56 | 57 | .. versionadded:: 3.0.0 58 | 59 | 60 | Use PIL bbox style and percent values 61 | ------------------------------------- 62 | 63 | You can use the same value as you would do with ``PIL.ImageGrab(bbox=tuple(...))``. 64 | This is an example that uses it, but also using percentage values: 65 | 66 | .. literalinclude:: examples/from_pil_tuple.py 67 | :lines: 7- 68 | 69 | .. versionadded:: 3.1.0 70 | 71 | PNG Compression 72 | --------------- 73 | 74 | You can tweak the PNG compression level (see :py:func:`zlib.compress()` for details):: 75 | 76 | sct.compression_level = 2 77 | 78 | .. versionadded:: 3.2.0 79 | 80 | Get PNG bytes, no file output 81 | ----------------------------- 82 | 83 | You can get the bytes of the PNG image: 84 | :: 85 | 86 | with mss.mss() as sct: 87 | # The monitor or screen part to capture 88 | monitor = sct.monitors[1] # or a region 89 | 90 | # Grab the data 91 | sct_img = sct.grab(monitor) 92 | 93 | # Generate the PNG 94 | png = mss.tools.to_png(sct_img.rgb, sct_img.size) 95 | 96 | Advanced 97 | ======== 98 | 99 | You can handle data using a custom class: 100 | 101 | .. literalinclude:: examples/custom_cls_image.py 102 | :lines: 7- 103 | 104 | .. versionadded:: 3.1.0 105 | 106 | GNU/Linux XShm backend 107 | ---------------------- 108 | 109 | Select the XShmGetImage backend explicitly and inspect whether it is active or 110 | falling back to XGetImage: 111 | 112 | .. literalinclude:: examples/linux_xshm_backend.py 113 | :lines: 7- 114 | 115 | .. versionadded:: 10.2.0 116 | 117 | PIL 118 | === 119 | 120 | You can use the Python Image Library (aka Pillow) to do whatever you want with raw pixels. 121 | This is an example using `frombytes() `_: 122 | 123 | .. literalinclude:: examples/pil.py 124 | :lines: 7- 125 | 126 | .. versionadded:: 3.0.0 127 | 128 | Playing with pixels 129 | ------------------- 130 | 131 | This is an example using `putdata() `_: 132 | 133 | .. literalinclude:: examples/pil_pixels.py 134 | :lines: 7- 135 | 136 | .. versionadded:: 3.0.0 137 | 138 | OpenCV/Numpy 139 | ============ 140 | 141 | See how fast you can record the screen. 142 | You can easily view a HD movie with VLC and see it too in the OpenCV window. 143 | And with __no__ lag please. 144 | 145 | .. literalinclude:: examples/opencv_numpy.py 146 | :lines: 7- 147 | 148 | .. versionadded:: 3.0.0 149 | 150 | FPS 151 | === 152 | 153 | Benchmark 154 | --------- 155 | 156 | Simple naive benchmark to compare with `Reading game frames in Python with OpenCV - Python Plays GTA V `_: 157 | 158 | .. literalinclude:: examples/fps.py 159 | :lines: 8- 160 | 161 | .. versionadded:: 3.0.0 162 | 163 | Multiprocessing 164 | --------------- 165 | 166 | Performances can be improved by delegating the PNG file creation to a specific worker. 167 | This is a simple example using the :py:mod:`multiprocessing` inspired by the `TensorFlow Object Detection Introduction `_ project: 168 | 169 | .. literalinclude:: examples/fps_multiprocessing.py 170 | :lines: 8- 171 | 172 | .. versionadded:: 5.0.0 173 | 174 | 175 | BGRA to RGB 176 | =========== 177 | 178 | Different possibilities to convert raw BGRA values to RGB:: 179 | 180 | def mss_rgb(im): 181 | """ Better than Numpy versions, but slower than Pillow. """ 182 | return im.rgb 183 | 184 | 185 | def numpy_flip(im): 186 | """ Most efficient Numpy version as of now. """ 187 | frame = numpy.array(im, dtype=numpy.uint8) 188 | return numpy.flip(frame[:, :, :3], 2).tobytes() 189 | 190 | 191 | def numpy_slice(im): 192 | """ Slow Numpy version. """ 193 | return numpy.array(im, dtype=numpy.uint8)[..., [2, 1, 0]].tobytes() 194 | 195 | 196 | def pil_frombytes(im): 197 | """ Efficient Pillow version. """ 198 | return Image.frombytes('RGB', im.size, im.bgra, 'raw', 'BGRX').tobytes() 199 | 200 | 201 | with mss.mss() as sct: 202 | im = sct.grab(sct.monitors[1]) 203 | rgb = pil_frombytes(im) 204 | ... 205 | 206 | .. versionadded:: 3.2.0 207 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mss" 7 | description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes." 8 | readme = "README.md" 9 | requires-python = ">= 3.9" 10 | authors = [ 11 | { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, 12 | ] 13 | maintainers = [ 14 | { name = "Mickaël Schoentgen", email="contact@tiger-222.fr" }, 15 | ] 16 | license = { file = "LICENSE.txt" } 17 | classifiers = [ 18 | "Development Status :: 5 - Production/Stable", 19 | "Environment :: MacOS X", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: Education", 22 | "Intended Audience :: End Users/Desktop", 23 | "Intended Audience :: Information Technology", 24 | "Intended Audience :: Science/Research", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: MacOS", 27 | "Operating System :: Microsoft :: Windows", 28 | "Operating System :: Unix", 29 | "Programming Language :: Python :: Implementation :: CPython", 30 | "Programming Language :: Python :: Implementation :: PyPy", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Programming Language :: Python :: 3.13", 39 | "Programming Language :: Python :: 3.14", 40 | "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", 41 | "Topic :: Software Development :: Libraries", 42 | ] 43 | keywords = [ 44 | "BitBlt", 45 | "ctypes", 46 | "EnumDisplayMonitors", 47 | "CGGetActiveDisplayList", 48 | "CGImageGetBitsPerPixel", 49 | "monitor", 50 | "screen", 51 | "screenshot", 52 | "screencapture", 53 | "screengrab", 54 | "XGetImage", 55 | "XGetWindowAttributes", 56 | "XRRGetScreenResourcesCurrent", 57 | ] 58 | dynamic = ["version"] 59 | 60 | [project.urls] 61 | Homepage = "https://github.com/BoboTiG/python-mss" 62 | Documentation = "https://python-mss.readthedocs.io" 63 | Changelog = "https://github.com/BoboTiG/python-mss/blob/main/CHANGELOG.md" 64 | Source = "https://github.com/BoboTiG/python-mss" 65 | Sponsor = "https://github.com/sponsors/BoboTiG" 66 | Tracker = "https://github.com/BoboTiG/python-mss/issues" 67 | "Released Versions" = "https://github.com/BoboTiG/python-mss/releases" 68 | 69 | [project.scripts] 70 | mss = "mss.__main__:main" 71 | 72 | [project.optional-dependencies] 73 | dev = [ 74 | "build==1.3.0", 75 | "lxml==6.0.2", 76 | "mypy==1.19.1", 77 | "ruff==0.14.10", 78 | "twine==6.2.0", 79 | ] 80 | docs = [ 81 | "shibuya==2025.10.21", 82 | "sphinx==8.2.3", 83 | "sphinx-copybutton==0.5.2", 84 | "sphinx-new-tab-link==0.8.0", 85 | ] 86 | tests = [ 87 | "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", 88 | "pillow==11.3.0 ; sys_platform == 'linux' and python_version == '3.13'", 89 | "pytest==8.4.2", 90 | "pytest-cov==7.0.0", 91 | "pytest-rerunfailures==16.0.1", 92 | "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", 93 | ] 94 | 95 | [tool.hatch.version] 96 | path = "src/mss/__init__.py" 97 | 98 | [tool.hatch.build] 99 | skip-excluded-dirs = true 100 | 101 | [tool.hatch.build.targets.sdist] 102 | only-include = [ 103 | "CHANGELOG.md", 104 | "CHANGES.md", 105 | "CONTRIBUTORS.md", 106 | "docs/source", 107 | "src", 108 | ] 109 | 110 | [tool.hatch.build.targets.wheel] 111 | packages = [ 112 | "src/mss", 113 | ] 114 | 115 | [tool.mypy] 116 | # Ensure we know what we do 117 | warn_redundant_casts = true 118 | warn_unused_ignores = true 119 | warn_unused_configs = true 120 | 121 | # Imports management 122 | ignore_missing_imports = true 123 | follow_imports = "skip" 124 | 125 | # Ensure full coverage 126 | disallow_untyped_defs = true 127 | disallow_incomplete_defs = true 128 | disallow_untyped_calls = true 129 | 130 | # Restrict dynamic typing (a little) 131 | # e.g. `x: List[Any]` or x: List` 132 | # disallow_any_generics = true 133 | 134 | strict_equality = true 135 | 136 | [tool.pytest.ini_options] 137 | pythonpath = "src" 138 | markers = ["without_libraries"] 139 | addopts = """ 140 | --showlocals 141 | --strict-markers 142 | -r fE 143 | -v 144 | --cov=src/mss 145 | --cov-report=term-missing:skip-covered 146 | """ 147 | 148 | [tool.ruff] 149 | exclude = [ 150 | ".git", 151 | ".mypy_cache", 152 | ".pytest_cache", 153 | ".ruff_cache", 154 | "venv", 155 | ] 156 | line-length = 120 157 | indent-width = 4 158 | target-version = "py39" 159 | 160 | [tool.ruff.format] 161 | quote-style = "double" 162 | indent-style = "space" 163 | skip-magic-trailing-comma = false 164 | line-ending = "auto" 165 | 166 | [tool.ruff.lint] 167 | fixable = ["ALL"] 168 | extend-select = ["ALL"] 169 | ignore = [ 170 | "ANN401", # typing.Any 171 | "C90", # complexity 172 | "COM812", # conflict 173 | "D", # TODO 174 | "ISC001", # conflict 175 | "T201", # `print()` 176 | ] 177 | 178 | [tool.ruff.lint.per-file-ignores] 179 | "docs/source/*" = [ 180 | "ERA001", # commented code 181 | "INP001", # file `xxx` is part of an implicit namespace package 182 | "N811", # importing constant (MSS) as non-constant (mss) 183 | ] 184 | "src/tests/*" = [ 185 | "FBT001", # boolean-typed positional argument in function definition 186 | "PLR2004", # magic value used in comparison 187 | "S101", # use of `assert` detected 188 | "S602", # `subprocess` call with `shell=True` 189 | "S603", # `subprocess` call: check for execution of untrusted input 190 | "S607", # `subprocess` call without explicit paths 191 | "SLF001", # private member accessed 192 | ] 193 | 194 | [tool.ruff.per-file-target-version] 195 | "src/xcbproto/*" = "py312" -------------------------------------------------------------------------------- /src/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | import platform 6 | import tarfile 7 | from subprocess import STDOUT, check_call, check_output 8 | from zipfile import ZipFile 9 | 10 | import pytest 11 | 12 | from mss import __version__ 13 | 14 | if platform.system().lower() != "linux": 15 | pytestmark = pytest.mark.skip 16 | 17 | pytest.importorskip("build") 18 | pytest.importorskip("twine") 19 | 20 | SDIST = ["python", "-m", "build", "--sdist"] 21 | WHEEL = ["python", "-m", "build", "--wheel"] 22 | CHECK = ["twine", "check", "--strict"] 23 | 24 | 25 | def test_sdist() -> None: 26 | output = check_output(SDIST, stderr=STDOUT, text=True) 27 | file = f"mss-{__version__}.tar.gz" 28 | assert f"Successfully built {file}" in output 29 | assert "warning" not in output.lower() 30 | 31 | check_call([*CHECK, f"dist/{file}"]) 32 | 33 | with tarfile.open(f"dist/{file}", mode="r:gz") as fh: 34 | files = sorted(fh.getnames()) 35 | 36 | assert files == [ 37 | f"mss-{__version__}/.gitignore", 38 | f"mss-{__version__}/CHANGELOG.md", 39 | f"mss-{__version__}/CHANGES.md", 40 | f"mss-{__version__}/CONTRIBUTORS.md", 41 | f"mss-{__version__}/LICENSE.txt", 42 | f"mss-{__version__}/PKG-INFO", 43 | f"mss-{__version__}/README.md", 44 | f"mss-{__version__}/docs/source/api.rst", 45 | f"mss-{__version__}/docs/source/conf.py", 46 | f"mss-{__version__}/docs/source/developers.rst", 47 | f"mss-{__version__}/docs/source/examples.rst", 48 | f"mss-{__version__}/docs/source/examples/callback.py", 49 | f"mss-{__version__}/docs/source/examples/custom_cls_image.py", 50 | f"mss-{__version__}/docs/source/examples/fps.py", 51 | f"mss-{__version__}/docs/source/examples/fps_multiprocessing.py", 52 | f"mss-{__version__}/docs/source/examples/from_pil_tuple.py", 53 | f"mss-{__version__}/docs/source/examples/linux_display_keyword.py", 54 | f"mss-{__version__}/docs/source/examples/linux_xshm_backend.py", 55 | f"mss-{__version__}/docs/source/examples/opencv_numpy.py", 56 | f"mss-{__version__}/docs/source/examples/part_of_screen.py", 57 | f"mss-{__version__}/docs/source/examples/part_of_screen_monitor_2.py", 58 | f"mss-{__version__}/docs/source/examples/pil.py", 59 | f"mss-{__version__}/docs/source/examples/pil_pixels.py", 60 | f"mss-{__version__}/docs/source/index.rst", 61 | f"mss-{__version__}/docs/source/installation.rst", 62 | f"mss-{__version__}/docs/source/support.rst", 63 | f"mss-{__version__}/docs/source/usage.rst", 64 | f"mss-{__version__}/docs/source/where.rst", 65 | f"mss-{__version__}/pyproject.toml", 66 | f"mss-{__version__}/src/mss/__init__.py", 67 | f"mss-{__version__}/src/mss/__main__.py", 68 | f"mss-{__version__}/src/mss/base.py", 69 | f"mss-{__version__}/src/mss/darwin.py", 70 | f"mss-{__version__}/src/mss/exception.py", 71 | f"mss-{__version__}/src/mss/factory.py", 72 | f"mss-{__version__}/src/mss/linux/__init__.py", 73 | f"mss-{__version__}/src/mss/linux/base.py", 74 | f"mss-{__version__}/src/mss/linux/xcb.py", 75 | f"mss-{__version__}/src/mss/linux/xcbgen.py", 76 | f"mss-{__version__}/src/mss/linux/xcbhelpers.py", 77 | f"mss-{__version__}/src/mss/linux/xgetimage.py", 78 | f"mss-{__version__}/src/mss/linux/xlib.py", 79 | f"mss-{__version__}/src/mss/linux/xshmgetimage.py", 80 | f"mss-{__version__}/src/mss/models.py", 81 | f"mss-{__version__}/src/mss/py.typed", 82 | f"mss-{__version__}/src/mss/screenshot.py", 83 | f"mss-{__version__}/src/mss/tools.py", 84 | f"mss-{__version__}/src/mss/windows.py", 85 | f"mss-{__version__}/src/tests/__init__.py", 86 | f"mss-{__version__}/src/tests/bench_bgra2rgb.py", 87 | f"mss-{__version__}/src/tests/bench_general.py", 88 | f"mss-{__version__}/src/tests/conftest.py", 89 | f"mss-{__version__}/src/tests/res/monitor-1024x768.raw.zip", 90 | f"mss-{__version__}/src/tests/test_bgra_to_rgb.py", 91 | f"mss-{__version__}/src/tests/test_cls_image.py", 92 | f"mss-{__version__}/src/tests/test_find_monitors.py", 93 | f"mss-{__version__}/src/tests/test_get_pixels.py", 94 | f"mss-{__version__}/src/tests/test_gnu_linux.py", 95 | f"mss-{__version__}/src/tests/test_implementation.py", 96 | f"mss-{__version__}/src/tests/test_issue_220.py", 97 | f"mss-{__version__}/src/tests/test_leaks.py", 98 | f"mss-{__version__}/src/tests/test_macos.py", 99 | f"mss-{__version__}/src/tests/test_save.py", 100 | f"mss-{__version__}/src/tests/test_setup.py", 101 | f"mss-{__version__}/src/tests/test_tools.py", 102 | f"mss-{__version__}/src/tests/test_windows.py", 103 | f"mss-{__version__}/src/tests/test_xcb.py", 104 | f"mss-{__version__}/src/tests/third_party/__init__.py", 105 | f"mss-{__version__}/src/tests/third_party/test_numpy.py", 106 | f"mss-{__version__}/src/tests/third_party/test_pil.py", 107 | f"mss-{__version__}/src/xcbproto/README.md", 108 | f"mss-{__version__}/src/xcbproto/gen_xcb_to_py.py", 109 | f"mss-{__version__}/src/xcbproto/randr.xml", 110 | f"mss-{__version__}/src/xcbproto/render.xml", 111 | f"mss-{__version__}/src/xcbproto/shm.xml", 112 | f"mss-{__version__}/src/xcbproto/xfixes.xml", 113 | f"mss-{__version__}/src/xcbproto/xproto.xml", 114 | ] 115 | 116 | 117 | def test_wheel() -> None: 118 | output = check_output(WHEEL, stderr=STDOUT, text=True) 119 | file = f"mss-{__version__}-py3-none-any.whl" 120 | assert f"Successfully built {file}" in output 121 | assert "warning" not in output.lower() 122 | 123 | check_call([*CHECK, f"dist/{file}"]) 124 | 125 | with ZipFile(f"dist/{file}") as fh: 126 | files = sorted(fh.namelist()) 127 | 128 | assert files == [ 129 | f"mss-{__version__}.dist-info/METADATA", 130 | f"mss-{__version__}.dist-info/RECORD", 131 | f"mss-{__version__}.dist-info/WHEEL", 132 | f"mss-{__version__}.dist-info/entry_points.txt", 133 | f"mss-{__version__}.dist-info/licenses/LICENSE.txt", 134 | "mss/__init__.py", 135 | "mss/__main__.py", 136 | "mss/base.py", 137 | "mss/darwin.py", 138 | "mss/exception.py", 139 | "mss/factory.py", 140 | "mss/linux/__init__.py", 141 | "mss/linux/base.py", 142 | "mss/linux/xcb.py", 143 | "mss/linux/xcbgen.py", 144 | "mss/linux/xcbhelpers.py", 145 | "mss/linux/xgetimage.py", 146 | "mss/linux/xlib.py", 147 | "mss/linux/xshmgetimage.py", 148 | "mss/models.py", 149 | "mss/py.typed", 150 | "mss/screenshot.py", 151 | "mss/tools.py", 152 | "mss/windows.py", 153 | ] 154 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Technical Changes 2 | 3 | ## 10.1.1 (2025-xx-xx) 4 | 5 | ### linux/__init__.py 6 | - Added an ``mss()`` factory to select between the different GNU/Linux backends. 7 | 8 | ### linux/xlib.py 9 | - Moved the legacy Xlib backend into the ``mss.linux.xlib`` module to be used as a fallback implementation. 10 | 11 | ### linux/xgetimage.py 12 | - Added an XCB-based backend that mirrors XGetImage semantics. 13 | 14 | ### linux/xshmgetimage.py 15 | - Added an XCB backend powered by XShmGetImage with ``shm_status`` and ``shm_fallback_reason`` attributes for diagnostics. 16 | 17 | ## 10.1.0 (2025-08-16) 18 | 19 | ### darwin.py 20 | - Added `IMAGE_OPTIONS` 21 | - Added `kCGWindowImageBoundsIgnoreFraming` 22 | - Added `kCGWindowImageNominalResolution` 23 | - Added `kCGWindowImageShouldBeOpaque` 24 | 25 | ## 10.0.0 (2024-11-14) 26 | 27 | ### base.py 28 | - Added `OPAQUE` 29 | 30 | ### darwin.py 31 | - Added `MAC_VERSION_CATALINA` 32 | 33 | ### linux.py 34 | - Added `BITS_PER_PIXELS_32` 35 | - Added `SUPPORTED_BITS_PER_PIXELS` 36 | 37 | ## 9.0.0 (2023-04-18) 38 | 39 | ### linux.py 40 | - Removed `XEvent` class. Use `XErrorEvent` instead. 41 | 42 | ### windows.py 43 | - Added `MSS.close()` method 44 | - Removed `MSS.bmp` attribute 45 | - Removed `MSS.memdc` attribute 46 | 47 | ## 8.0.3 (2023-04-15) 48 | 49 | ### linux.py 50 | - Added `XErrorEvent` class (old `Event` class is just an alias now, and will be removed in v9.0.0) 51 | 52 | ## 8.0.0 (2023-04-09) 53 | 54 | ### base.py 55 | - Added `compression_level=6` keyword argument to `MSS.__init__()` 56 | - Added `display=None` keyword argument to `MSS.__init__()` 57 | - Added `max_displays=32` keyword argument to `MSS.__init__()` 58 | - Added `with_cursor=False` keyword argument to `MSS.__init__()` 59 | - Added `MSS.with_cursor` attribute 60 | 61 | ### linux.py 62 | - Added `MSS.close()` 63 | - Moved `MSS.__init__()` keyword arguments handling to the base class 64 | - Renamed `error_handler()` function to `_error_handler()` 65 | - Renamed `validate()` function to `__validate()` 66 | - Renamed `MSS.has_extension()` method to `_is_extension_enabled()` 67 | - Removed `ERROR` namespace 68 | - Removed `MSS.drawable` attribute 69 | - Removed `MSS.root` attribute 70 | - Removed `MSS.get_error_details()` method. Use `ScreenShotError.details` attribute instead. 71 | 72 | ## 6.1.0 (2020-10-31) 73 | 74 | ### darwin.py 75 | - Added `CFUNCTIONS` 76 | 77 | ### linux.py 78 | - Added `CFUNCTIONS` 79 | 80 | ### windows.py 81 | - Added `CFUNCTIONS` 82 | - Added `MONITORNUMPROC` 83 | - Removed `MSS.monitorenumproc`. Use `MONITORNUMPROC` instead. 84 | 85 | ## 6.0.0 (2020-06-30) 86 | 87 | ### base.py 88 | - Added `lock` 89 | - Added `MSS._grab_impl()` (abstract method) 90 | - Added `MSS._monitors_impl()` (abstract method) 91 | - `MSS.grab()` is no more an abstract method 92 | - `MSS.monitors` is no more an abstract property 93 | 94 | ### darwin.py 95 | - Renamed `MSS.grab()` to `MSS._grab_impl()` 96 | - Renamed `MSS.monitors` to `MSS._monitors_impl()` 97 | 98 | ### linux.py 99 | - Added `MSS.has_extension()` 100 | - Removed `MSS.display` 101 | - Renamed `MSS.grab()` to `MSS._grab_impl()` 102 | - Renamed `MSS.monitors` to `MSS._monitors_impl()` 103 | 104 | ### windows.py 105 | - Removed `MSS._lock` 106 | - Renamed `MSS.srcdc_dict` to `MSS._srcdc_dict` 107 | - Renamed `MSS.grab()` to `MSS._grab_impl()` 108 | - Renamed `MSS.monitors` to `MSS._monitors_impl()` 109 | 110 | ## 5.1.0 (2020-04-30) 111 | 112 | ### base.py 113 | - Renamed back `MSSMixin` class to `MSSBase` 114 | - `MSSBase` is now derived from `abc.ABCMeta` 115 | - `MSSBase.monitor` is now an abstract property 116 | - `MSSBase.grab()` is now an abstract method 117 | 118 | ### windows.py 119 | - Replaced `MSS.srcdc` with `MSS.srcdc_dict` 120 | 121 | ## 5.0.0 (2019-12-31) 122 | 123 | ### darwin.py 124 | - Added `MSS.__slots__` 125 | 126 | ### linux.py 127 | - Added `MSS.__slots__` 128 | - Deleted `MSS.close()` 129 | - Deleted `LAST_ERROR` constant. Use `ERROR` namespace instead, specially the `ERROR.details` attribute. 130 | 131 | ### models.py 132 | - Added `Monitor` 133 | - Added `Monitors` 134 | - Added `Pixel` 135 | - Added `Pixels` 136 | - Added `Pos` 137 | - Added `Size` 138 | 139 | ### screenshot.py 140 | - Added `ScreenShot.__slots__` 141 | - Removed `Pos`. Use `models.Pos` instead. 142 | - Removed `Size`. Use `models.Size` instead. 143 | 144 | ### windows.py 145 | - Added `MSS.__slots__` 146 | - Deleted `MSS.close()` 147 | 148 | ## 4.0.1 (2019-01-26) 149 | 150 | ### linux.py 151 | - Removed use of `MSS.xlib.XDefaultScreen()` 152 | 4.0.0 (2019-01-11) 153 | 154 | ### base.py 155 | - Renamed `MSSBase` class to `MSSMixin` 156 | 157 | ### linux.py 158 | - Renamed `MSS.__del__()` method to `MSS.close()` 159 | - Deleted `MSS.last_error` attribute. Use `LAST_ERROR` constant instead. 160 | - Added `validate()` function 161 | - Added `MSS.get_error_details()` method 162 | 163 | ### windows.py 164 | - Renamed `MSS.__exit__()` method to `MSS.close()` 165 | 166 | ## 3.3.0 (2018-09-04) 167 | 168 | ### exception.py 169 | - Added `details` attribute to `ScreenShotError` exception. Empty dict by default. 170 | 171 | ### linux.py 172 | - Added `error_handler()` function 173 | 174 | ## 3.2.1 (2018-05-21) 175 | 176 | ### windows.py 177 | - Removed `MSS.scale_factor` property 178 | - Removed `MSS.scale()` method 179 | 180 | ## 3.2.0 (2018-03-22) 181 | 182 | ### base.py 183 | - Added `MSSBase.compression_level` attribute 184 | 185 | ### linux.py 186 | - Added `MSS.drawable` attribute 187 | 188 | ### screenshot.py 189 | - Added `Screenshot.bgra` attribute 190 | 191 | ### tools.py 192 | - Changed signature of `to_png(data, size, output=None)` to `to_png(data, size, level=6, output=None)`. `level` is the Zlib compression level. 193 | 194 | ## 3.1.2 (2018-01-05) 195 | 196 | ### tools.py 197 | - Changed signature of `to_png(data, size, output)` to `to_png(data, size, output=None)`. If `output` is `None`, the raw PNG bytes will be returned. 198 | 199 | ## 3.1.1 (2017-11-27) 200 | 201 | ### \_\_main\_\_.py 202 | - Added `args` argument to `main()` 203 | 204 | ### base.py 205 | - Moved `ScreenShot` class to `screenshot.py` 206 | 207 | ### darwin.py 208 | - Added `CGPoint.__repr__()` function 209 | - Added `CGRect.__repr__()` function 210 | - Added `CGSize.__repr__()` function 211 | - Removed `get_infinity()` function 212 | 213 | ### windows.py 214 | - Added `MSS.scale()` method 215 | - Added `MSS.scale_factor` property 216 | 217 | ## 3.0.0 (2017-07-06) 218 | 219 | ### base.py 220 | - Added the `ScreenShot` class containing data for a given screenshot (support the Numpy array interface [`ScreenShot.__array_interface__`]) 221 | - Added `shot()` method to `MSSBase`. It takes the same arguments as the `save()` method. 222 | - Renamed `get_pixels` to `grab`. It now returns a `ScreenShot` object. 223 | - Moved `to_png` method to `tools.py`. It is now a simple function. 224 | - Removed `enum_display_monitors()` method. Use `monitors` property instead. 225 | - Removed `monitors` attribute. Use `monitors` property instead. 226 | - Removed `width` attribute. Use `ScreenShot.size[0]` attribute or `ScreenShot.width` property instead. 227 | - Removed `height` attribute. Use `ScreenShot.size[1]` attribute or `ScreenShot.height` property instead. 228 | - Removed `image`. Use the `ScreenShot.raw` attribute or `ScreenShot.rgb` property instead. 229 | - Removed `bgra_to_rgb()` method. Use `ScreenShot.rgb` property instead. 230 | 231 | ### darwin.py 232 | - Removed `_crop_width()` method. Screenshots are now using the width set by the OS (rounded to 16). 233 | 234 | ### exception.py 235 | - Renamed `ScreenshotError` class to `ScreenShotError` 236 | 237 | ### tools.py 238 | - Changed signature of `to_png(data, monitor, output)` to `to_png(data, size, output)` where `size` is a `tuple(width, height)` 239 | -------------------------------------------------------------------------------- /src/mss/linux/xcb.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from ctypes import _Pointer, c_int 4 | 5 | from . import xcbgen 6 | 7 | # We import these just so they're re-exported to our users. 8 | # ruff: noqa: F401, TC001 9 | from .xcbgen import ( 10 | RANDR_MAJOR_VERSION, 11 | RANDR_MINOR_VERSION, 12 | RENDER_MAJOR_VERSION, 13 | RENDER_MINOR_VERSION, 14 | SHM_MAJOR_VERSION, 15 | SHM_MINOR_VERSION, 16 | XFIXES_MAJOR_VERSION, 17 | XFIXES_MINOR_VERSION, 18 | Atom, 19 | BackingStore, 20 | Colormap, 21 | Depth, 22 | DepthIterator, 23 | Drawable, 24 | Format, 25 | GetGeometryReply, 26 | GetImageReply, 27 | GetPropertyReply, 28 | ImageFormat, 29 | ImageOrder, 30 | Keycode, 31 | Pixmap, 32 | RandrCrtc, 33 | RandrGetCrtcInfoReply, 34 | RandrGetScreenResourcesCurrentReply, 35 | RandrGetScreenResourcesReply, 36 | RandrMode, 37 | RandrModeInfo, 38 | RandrOutput, 39 | RandrQueryVersionReply, 40 | RandrSetConfig, 41 | RenderDirectformat, 42 | RenderPictdepth, 43 | RenderPictdepthIterator, 44 | RenderPictformat, 45 | RenderPictforminfo, 46 | RenderPictscreen, 47 | RenderPictscreenIterator, 48 | RenderPictType, 49 | RenderPictvisual, 50 | RenderQueryPictFormatsReply, 51 | RenderQueryVersionReply, 52 | RenderSubPixel, 53 | Screen, 54 | ScreenIterator, 55 | Setup, 56 | SetupIterator, 57 | ShmCreateSegmentReply, 58 | ShmGetImageReply, 59 | ShmQueryVersionReply, 60 | ShmSeg, 61 | Timestamp, 62 | VisualClass, 63 | Visualid, 64 | Visualtype, 65 | Window, 66 | XfixesGetCursorImageReply, 67 | XfixesQueryVersionReply, 68 | depth_visuals, 69 | get_geometry, 70 | get_image, 71 | get_image_data, 72 | get_property, 73 | get_property_value, 74 | no_operation, 75 | randr_get_crtc_info, 76 | randr_get_crtc_info_outputs, 77 | randr_get_crtc_info_possible, 78 | randr_get_screen_resources, 79 | randr_get_screen_resources_crtcs, 80 | randr_get_screen_resources_current, 81 | randr_get_screen_resources_current_crtcs, 82 | randr_get_screen_resources_current_modes, 83 | randr_get_screen_resources_current_names, 84 | randr_get_screen_resources_current_outputs, 85 | randr_get_screen_resources_modes, 86 | randr_get_screen_resources_names, 87 | randr_get_screen_resources_outputs, 88 | randr_query_version, 89 | render_pictdepth_visuals, 90 | render_pictscreen_depths, 91 | render_query_pict_formats, 92 | render_query_pict_formats_formats, 93 | render_query_pict_formats_screens, 94 | render_query_pict_formats_subpixels, 95 | render_query_version, 96 | screen_allowed_depths, 97 | setup_pixmap_formats, 98 | setup_roots, 99 | setup_vendor, 100 | shm_attach_fd, 101 | shm_create_segment, 102 | shm_create_segment_reply_fds, 103 | shm_detach, 104 | shm_get_image, 105 | shm_query_version, 106 | xfixes_get_cursor_image, 107 | xfixes_get_cursor_image_cursor_image, 108 | xfixes_query_version, 109 | ) 110 | 111 | # These are also here to re-export. 112 | from .xcbhelpers import LIB, XID, Connection, QueryExtensionReply, XcbExtension, XError 113 | 114 | XCB_CONN_ERROR = 1 115 | XCB_CONN_CLOSED_EXT_NOTSUPPORTED = 2 116 | XCB_CONN_CLOSED_MEM_INSUFFICIENT = 3 117 | XCB_CONN_CLOSED_REQ_LEN_EXCEED = 4 118 | XCB_CONN_CLOSED_PARSE_ERR = 5 119 | XCB_CONN_CLOSED_INVALID_SCREEN = 6 120 | XCB_CONN_CLOSED_FDPASSING_FAILED = 7 121 | 122 | # I don't know of error descriptions for the XCB connection errors being accessible through a library (a la strerror), 123 | # and the ones in xcb.h's comments aren't too great, so I wrote these. 124 | XCB_CONN_ERRMSG = { 125 | XCB_CONN_ERROR: "connection lost or could not be established", 126 | XCB_CONN_CLOSED_EXT_NOTSUPPORTED: "extension not supported", 127 | XCB_CONN_CLOSED_MEM_INSUFFICIENT: "memory exhausted", 128 | XCB_CONN_CLOSED_REQ_LEN_EXCEED: "request length longer than server accepts", 129 | XCB_CONN_CLOSED_PARSE_ERR: "display is unset or invalid (check $DISPLAY)", 130 | XCB_CONN_CLOSED_INVALID_SCREEN: "server does not have a screen matching the requested display", 131 | XCB_CONN_CLOSED_FDPASSING_FAILED: "could not pass file descriptor", 132 | } 133 | 134 | 135 | #### High-level XCB function wrappers 136 | 137 | 138 | def get_extension_data( 139 | xcb_conn: Connection | _Pointer[Connection], ext: XcbExtension | _Pointer[XcbExtension] 140 | ) -> QueryExtensionReply: 141 | """Get extension data for the given extension. 142 | 143 | Returns the extension data, which includes whether the extension is present 144 | and its opcode information. 145 | """ 146 | reply_p = LIB.xcb.xcb_get_extension_data(xcb_conn, ext) 147 | return reply_p.contents 148 | 149 | 150 | def prefetch_extension_data( 151 | xcb_conn: Connection | _Pointer[Connection], ext: XcbExtension | _Pointer[XcbExtension] 152 | ) -> None: 153 | """Prefetch extension data for the given extension. 154 | 155 | This is a performance hint to XCB to fetch the extension data 156 | asynchronously. 157 | """ 158 | LIB.xcb.xcb_prefetch_extension_data(xcb_conn, ext) 159 | 160 | 161 | def generate_id(xcb_conn: Connection | _Pointer[Connection]) -> XID: 162 | """Generate a new unique X resource ID. 163 | 164 | Returns an XID that can be used to create new X resources. 165 | """ 166 | return LIB.xcb.xcb_generate_id(xcb_conn) 167 | 168 | 169 | def get_setup(xcb_conn: Connection | _Pointer[Connection]) -> Setup: 170 | """Get the connection setup information. 171 | 172 | Returns the setup structure containing information about the X server, 173 | including available screens, pixmap formats, etc. 174 | """ 175 | setup_p = LIB.xcb.xcb_get_setup(xcb_conn) 176 | return setup_p.contents 177 | 178 | 179 | # Connection management 180 | 181 | 182 | def initialize() -> None: 183 | LIB.initialize(callbacks=[xcbgen.initialize]) 184 | 185 | 186 | def connect(display: str | bytes | None = None) -> tuple[Connection, int]: 187 | if isinstance(display, str): 188 | display = display.encode("utf-8") 189 | 190 | initialize() 191 | pref_screen_num = c_int() 192 | conn_p = LIB.xcb.xcb_connect(display, pref_screen_num) 193 | 194 | # We still get a connection object even if the connection fails. 195 | conn_err = LIB.xcb.xcb_connection_has_error(conn_p) 196 | if conn_err != 0: 197 | # XCB won't free its connection structures until we disconnect, even in the event of an error. 198 | LIB.xcb.xcb_disconnect(conn_p) 199 | msg = "Cannot connect to display: " 200 | conn_errmsg = XCB_CONN_ERRMSG.get(conn_err) 201 | if conn_errmsg: 202 | msg += conn_errmsg 203 | else: 204 | msg += f"error code {conn_err}" 205 | raise XError(msg) 206 | 207 | # Prefetch extension data for all extensions we support to populate XCB's internal cache. 208 | prefetch_extension_data(conn_p, LIB.randr_id) 209 | prefetch_extension_data(conn_p, LIB.render_id) 210 | prefetch_extension_data(conn_p, LIB.shm_id) 211 | prefetch_extension_data(conn_p, LIB.xfixes_id) 212 | 213 | return conn_p.contents, pref_screen_num.value 214 | 215 | 216 | def disconnect(conn: Connection) -> None: 217 | conn_err = LIB.xcb.xcb_connection_has_error(conn) 218 | # XCB won't free its connection structures until we disconnect, even in the event of an error. 219 | LIB.xcb.xcb_disconnect(conn) 220 | if conn_err != 0: 221 | msg = "Connection to X server closed: " 222 | conn_errmsg = XCB_CONN_ERRMSG.get(conn_err) 223 | if conn_errmsg: 224 | msg += conn_errmsg 225 | else: 226 | msg += f"error code {conn_err}" 227 | raise XError(msg) 228 | -------------------------------------------------------------------------------- /src/tests/test_xcb.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import gc 4 | from ctypes import ( 5 | POINTER, 6 | Structure, 7 | addressof, 8 | c_int, 9 | c_void_p, 10 | cast, 11 | pointer, 12 | sizeof, 13 | ) 14 | from types import SimpleNamespace 15 | from typing import Any, Callable 16 | from unittest.mock import Mock 17 | from weakref import finalize 18 | 19 | import pytest 20 | 21 | from mss.exception import ScreenShotError 22 | from mss.linux import base, xcb, xgetimage 23 | from mss.linux.xcbhelpers import ( 24 | XcbExtension, 25 | array_from_xcb, 26 | depends_on, 27 | list_from_xcb, 28 | ) 29 | 30 | 31 | def _force_gc() -> None: 32 | gc.collect() 33 | gc.collect() 34 | 35 | 36 | class _Placeholder: 37 | """Trivial class to test weakrefs""" 38 | 39 | 40 | def test_depends_on_defers_parent_teardown_until_child_collected() -> None: 41 | parent = _Placeholder() 42 | child = _Placeholder() 43 | finalizer_calls: list[str] = [] 44 | finalize(parent, lambda: finalizer_calls.append("parent")) 45 | 46 | depends_on(child, parent) 47 | 48 | del parent 49 | _force_gc() 50 | assert finalizer_calls == [] 51 | 52 | del child 53 | _force_gc() 54 | assert finalizer_calls == ["parent"] 55 | 56 | 57 | def test_ctypes_scalar_finalizer_runs_when_object_collected() -> None: 58 | callback = Mock() 59 | 60 | foo = c_int(42) 61 | finalize(foo, callback) 62 | del foo 63 | _force_gc() 64 | 65 | callback.assert_called_once() 66 | 67 | 68 | class FakeCEntry(Structure): 69 | _fields_ = (("value", c_int),) 70 | 71 | 72 | class FakeParentContainer: 73 | def __init__(self, values: list[int]) -> None: 74 | self.count = len(values) 75 | array_type = FakeCEntry * self.count 76 | self.buffer = array_type(*(FakeCEntry(v) for v in values)) 77 | self.pointer = cast(self.buffer, POINTER(FakeCEntry)) 78 | 79 | 80 | class FakeIterator: 81 | def __init__(self, parent: FakeParentContainer) -> None: 82 | self.parent = parent 83 | self.data = parent.pointer 84 | self.rem = parent.count 85 | 86 | @staticmethod 87 | def next(iterator: FakeIterator) -> None: 88 | iterator.rem -= 1 89 | if iterator.rem == 0: 90 | return 91 | current_address = addressof(iterator.data.contents) 92 | next_address = current_address + sizeof(FakeCEntry) 93 | iterator.data = cast(c_void_p(next_address), POINTER(FakeCEntry)) 94 | 95 | 96 | def test_list_from_xcb_keeps_parent_alive_until_items_drop() -> None: 97 | parent = FakeParentContainer([1, 2, 3]) 98 | callback = Mock() 99 | finalize(parent, callback) 100 | 101 | items = list_from_xcb(FakeIterator, FakeIterator.next, parent) # type: ignore[arg-type] 102 | assert [item.value for item in items] == [1, 2, 3] 103 | 104 | del parent 105 | _force_gc() 106 | callback.assert_not_called() 107 | 108 | item = items[0] 109 | assert isinstance(item, FakeCEntry) 110 | 111 | del items 112 | _force_gc() 113 | callback.assert_not_called() 114 | 115 | del item 116 | _force_gc() 117 | callback.assert_called_once() 118 | 119 | 120 | def test_array_from_xcb_keeps_parent_alive_until_array_gone() -> None: 121 | parent = _Placeholder() 122 | callback = Mock() 123 | finalize(parent, callback) 124 | 125 | values = [FakeCEntry(1), FakeCEntry(2)] 126 | array_type = FakeCEntry * len(values) 127 | buffer = array_type(*values) 128 | 129 | def pointer_func(_parent: _Placeholder) -> Any: 130 | return cast(buffer, POINTER(FakeCEntry)) 131 | 132 | def length_func(_parent: _Placeholder) -> int: 133 | return len(values) 134 | 135 | array = array_from_xcb(pointer_func, length_func, parent) # type: ignore[arg-type] 136 | assert [entry.value for entry in array] == [1, 2] 137 | 138 | del parent 139 | _force_gc() 140 | callback.assert_not_called() 141 | 142 | item = array[0] 143 | assert isinstance(item, FakeCEntry) 144 | 145 | del array 146 | _force_gc() 147 | callback.assert_not_called() 148 | 149 | del item 150 | _force_gc() 151 | callback.assert_called_once() 152 | 153 | 154 | class _VisualValidationHarness: 155 | """Test utility that supplies deterministic XCB setup data.""" 156 | 157 | def __init__(self, monkeypatch: pytest.MonkeyPatch) -> None: 158 | self._monkeypatch = monkeypatch 159 | self.setup = xcb.Setup() 160 | self.screen = xcb.Screen() 161 | self.format = xcb.Format() 162 | self.depth = xcb.Depth() 163 | self.visual = xcb.Visualtype() 164 | self._setup_ptr = pointer(self.setup) 165 | self.connection = xcb.Connection() 166 | 167 | fake_lib = SimpleNamespace( 168 | xcb=SimpleNamespace( 169 | xcb_prefetch_extension_data=lambda *_args, **_kwargs: None, 170 | xcb_get_setup=lambda _conn: self._setup_ptr, 171 | ), 172 | randr_id=XcbExtension(), 173 | xfixes_id=XcbExtension(), 174 | ) 175 | self._monkeypatch.setattr(xcb, "LIB", fake_lib) 176 | self._monkeypatch.setattr(xcb, "connect", lambda _display=None: (self.connection, 0)) 177 | self._monkeypatch.setattr(xcb, "disconnect", lambda _conn: None) 178 | self._monkeypatch.setattr(xcb, "setup_roots", self._setup_roots) 179 | self._monkeypatch.setattr(xcb, "setup_pixmap_formats", self._setup_pixmap_formats) 180 | self._monkeypatch.setattr(xcb, "screen_allowed_depths", self._screen_allowed_depths) 181 | self._monkeypatch.setattr(xcb, "depth_visuals", self._depth_visuals) 182 | 183 | self.reset() 184 | 185 | def reset(self) -> None: 186 | self.setup.image_byte_order = xcb.ImageOrder.LSBFirst 187 | self.screen.root = xcb.Window(1) 188 | self.screen.root_depth = 32 189 | visual_id = 0x1234 190 | self.screen.root_visual = xcb.Visualid(visual_id) 191 | 192 | self.format.depth = self.screen.root_depth 193 | self.format.bits_per_pixel = base.SUPPORTED_BITS_PER_PIXEL 194 | self.format.scanline_pad = base.SUPPORTED_BITS_PER_PIXEL 195 | 196 | self.depth.depth = self.screen.root_depth 197 | 198 | self.visual.visual_id = xcb.Visualid(visual_id) 199 | self.visual.class_ = xcb.VisualClass.TrueColor 200 | self.visual.red_mask = base.SUPPORTED_RED_MASK 201 | self.visual.green_mask = base.SUPPORTED_GREEN_MASK 202 | self.visual.blue_mask = base.SUPPORTED_BLUE_MASK 203 | 204 | self.screens = [self.screen] 205 | self.pixmap_formats = [self.format] 206 | self.depths = [self.depth] 207 | self.visuals = [self.visual] 208 | 209 | def _setup_roots(self, _setup: xcb.Setup) -> list[xcb.Screen]: 210 | return self.screens 211 | 212 | def _setup_pixmap_formats(self, _setup: xcb.Setup) -> list[xcb.Format]: 213 | return self.pixmap_formats 214 | 215 | def _screen_allowed_depths(self, _screen: xcb.Screen) -> list[xcb.Depth]: 216 | return self.depths 217 | 218 | def _depth_visuals(self, _depth: xcb.Depth) -> list[xcb.Visualtype]: 219 | return self.visuals 220 | 221 | 222 | @pytest.fixture 223 | def visual_validation_env(monkeypatch: pytest.MonkeyPatch) -> _VisualValidationHarness: 224 | return _VisualValidationHarness(monkeypatch) 225 | 226 | 227 | def test_xgetimage_visual_validation_accepts_default_setup(visual_validation_env: _VisualValidationHarness) -> None: 228 | visual_validation_env.reset() 229 | mss_instance = xgetimage.MSS() 230 | try: 231 | assert isinstance(mss_instance, xgetimage.MSS) 232 | finally: 233 | mss_instance.close() 234 | 235 | 236 | @pytest.mark.parametrize( 237 | ("mutator", "message"), 238 | [ 239 | (lambda env: setattr(env.setup, "image_byte_order", xcb.ImageOrder.MSBFirst), "LSB-First"), 240 | (lambda env: setattr(env.screen, "root_depth", 16), "color depth 24 or 32"), 241 | (lambda env: setattr(env, "pixmap_formats", []), "supported formats"), 242 | (lambda env: setattr(env.format, "bits_per_pixel", 16), "32 bpp"), 243 | (lambda env: setattr(env.format, "scanline_pad", 16), "scanline padding"), 244 | (lambda env: setattr(env, "depths", []), "supported depths"), 245 | (lambda env: setattr(env, "visuals", []), "supported visuals"), 246 | (lambda env: setattr(env.visual, "class_", xcb.VisualClass.StaticGray), "TrueColor"), 247 | (lambda env: setattr(env.visual, "red_mask", 0), "BGRx ordering"), 248 | ], 249 | ) 250 | def test_xgetimage_visual_validation_failures( 251 | visual_validation_env: _VisualValidationHarness, 252 | mutator: Callable[[_VisualValidationHarness], None], 253 | message: str, 254 | ) -> None: 255 | mutator(visual_validation_env) 256 | with pytest.raises(ScreenShotError, match=message): 257 | xgetimage.MSS() 258 | -------------------------------------------------------------------------------- /src/mss/darwin.py: -------------------------------------------------------------------------------- 1 | """macOS CoreGraphics backend for MSS. 2 | 3 | Uses the CoreGraphics APIs to capture windows and enumerates up to 4 | ``max_displays`` active displays. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import ctypes 10 | import ctypes.util 11 | import sys 12 | from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p 13 | from platform import mac_ver 14 | from typing import TYPE_CHECKING, Any 15 | 16 | from mss.base import MSSBase 17 | from mss.exception import ScreenShotError 18 | from mss.screenshot import ScreenShot, Size 19 | 20 | if TYPE_CHECKING: # pragma: nocover 21 | from mss.models import CFunctions, Monitor 22 | 23 | __all__ = ("IMAGE_OPTIONS", "MSS") 24 | 25 | BACKENDS = ["default"] 26 | 27 | MAC_VERSION_CATALINA = 10.16 28 | 29 | kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816 30 | kCGWindowImageNominalResolution = 1 << 4 # noqa: N816 31 | kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816 32 | #: For advanced users: as a note, you can set ``IMAGE_OPTIONS = 0`` to turn on scaling; see issue #257 for more 33 | #: information. 34 | IMAGE_OPTIONS: int = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution 35 | 36 | 37 | def cgfloat() -> type[c_double | c_float]: 38 | """Get the appropriate value for a float.""" 39 | return c_double if sys.maxsize > 2**32 else c_float 40 | 41 | 42 | class CGPoint(Structure): 43 | """Structure that contains coordinates of a rectangle.""" 44 | 45 | _fields_ = (("x", cgfloat()), ("y", cgfloat())) 46 | 47 | def __repr__(self) -> str: 48 | return f"{type(self).__name__}(left={self.x} top={self.y})" 49 | 50 | 51 | class CGSize(Structure): 52 | """Structure that contains dimensions of an rectangle.""" 53 | 54 | _fields_ = (("width", cgfloat()), ("height", cgfloat())) 55 | 56 | def __repr__(self) -> str: 57 | return f"{type(self).__name__}(width={self.width} height={self.height})" 58 | 59 | 60 | class CGRect(Structure): 61 | """Structure that contains information about a rectangle.""" 62 | 63 | _fields_ = (("origin", CGPoint), ("size", CGSize)) 64 | 65 | def __repr__(self) -> str: 66 | return f"{type(self).__name__}<{self.origin} {self.size}>" 67 | 68 | 69 | # C functions that will be initialised later. 70 | # 71 | # Available attr: core. 72 | # 73 | # Note: keep it sorted by cfunction. 74 | CFUNCTIONS: CFunctions = { 75 | # Syntax: cfunction: (attr, argtypes, restype) 76 | "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), 77 | "CGDisplayBounds": ("core", [c_uint32], CGRect), 78 | "CGDisplayRotation": ("core", [c_uint32], c_float), 79 | "CFDataGetBytePtr": ("core", [c_void_p], c_void_p), 80 | "CFDataGetLength": ("core", [c_void_p], c_uint64), 81 | "CFRelease": ("core", [c_void_p], c_void_p), 82 | "CGDataProviderRelease": ("core", [c_void_p], c_void_p), 83 | "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32), 84 | "CGImageGetBitsPerPixel": ("core", [c_void_p], int), 85 | "CGImageGetBytesPerRow": ("core", [c_void_p], int), 86 | "CGImageGetDataProvider": ("core", [c_void_p], c_void_p), 87 | "CGImageGetHeight": ("core", [c_void_p], int), 88 | "CGImageGetWidth": ("core", [c_void_p], int), 89 | "CGRectStandardize": ("core", [CGRect], CGRect), 90 | "CGRectUnion": ("core", [CGRect, CGRect], CGRect), 91 | "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p), 92 | } 93 | 94 | 95 | class MSS(MSSBase): 96 | """Multiple ScreenShots implementation for macOS. 97 | It uses intensively the CoreGraphics library. 98 | 99 | :param max_displays: maximum number of displays to handle (default: 32). 100 | :type max_displays: int 101 | 102 | .. seealso:: 103 | 104 | :py:class:`mss.base.MSSBase` 105 | Lists other parameters. 106 | """ 107 | 108 | __slots__ = {"core", "max_displays"} 109 | 110 | def __init__(self, /, **kwargs: Any) -> None: 111 | super().__init__(**kwargs) 112 | 113 | #: Maximum number of displays to handle. 114 | self.max_displays = kwargs.get("max_displays", 32) 115 | 116 | self._init_library() 117 | self._set_cfunctions() 118 | 119 | def _init_library(self) -> None: 120 | """Load the CoreGraphics library.""" 121 | version = float(".".join(mac_ver()[0].split(".")[:2])) 122 | if version < MAC_VERSION_CATALINA: 123 | coregraphics = ctypes.util.find_library("CoreGraphics") 124 | else: 125 | # macOS Big Sur and newer 126 | coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" 127 | 128 | if not coregraphics: 129 | msg = "No CoreGraphics library found." 130 | raise ScreenShotError(msg) 131 | # :meta:private: 132 | self.core = ctypes.cdll.LoadLibrary(coregraphics) 133 | 134 | def _set_cfunctions(self) -> None: 135 | """Set all ctypes functions and attach them to attributes.""" 136 | cfactory = self._cfactory 137 | attrs = {"core": self.core} 138 | for func, (attr, argtypes, restype) in CFUNCTIONS.items(): 139 | cfactory(attrs[attr], func, argtypes, restype) 140 | 141 | def _monitors_impl(self) -> None: 142 | """Get positions of monitors. It will populate self._monitors.""" 143 | int_ = int 144 | core = self.core 145 | 146 | # All monitors 147 | # We need to update the value with every single monitor found 148 | # using CGRectUnion. Else we will end with infinite values. 149 | all_monitors = CGRect() 150 | self._monitors.append({}) 151 | 152 | # Each monitor 153 | display_count = c_uint32(0) 154 | active_displays = (c_uint32 * self.max_displays)() 155 | core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count)) 156 | for idx in range(display_count.value): 157 | display = active_displays[idx] 158 | rect = core.CGDisplayBounds(display) 159 | rect = core.CGRectStandardize(rect) 160 | width, height = rect.size.width, rect.size.height 161 | 162 | # 0.0: normal 163 | # 90.0: right 164 | # -90.0: left 165 | if core.CGDisplayRotation(display) in {90.0, -90.0}: 166 | width, height = height, width 167 | 168 | self._monitors.append( 169 | { 170 | "left": int_(rect.origin.x), 171 | "top": int_(rect.origin.y), 172 | "width": int_(width), 173 | "height": int_(height), 174 | }, 175 | ) 176 | 177 | # Update AiO monitor's values 178 | all_monitors = core.CGRectUnion(all_monitors, rect) 179 | 180 | # Set the AiO monitor's values 181 | self._monitors[0] = { 182 | "left": int_(all_monitors.origin.x), 183 | "top": int_(all_monitors.origin.y), 184 | "width": int_(all_monitors.size.width), 185 | "height": int_(all_monitors.size.height), 186 | } 187 | 188 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 189 | """Retrieve all pixels from a monitor. Pixels have to be RGB.""" 190 | core = self.core 191 | rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) 192 | 193 | image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) 194 | if not image_ref: 195 | msg = "CoreGraphics.CGWindowListCreateImage() failed." 196 | raise ScreenShotError(msg) 197 | 198 | width = core.CGImageGetWidth(image_ref) 199 | height = core.CGImageGetHeight(image_ref) 200 | prov = copy_data = None 201 | try: 202 | prov = core.CGImageGetDataProvider(image_ref) 203 | copy_data = core.CGDataProviderCopyData(prov) 204 | data_ref = core.CFDataGetBytePtr(copy_data) 205 | buf_len = core.CFDataGetLength(copy_data) 206 | raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len)) 207 | data = bytearray(raw.contents) 208 | 209 | # Remove padding per row 210 | bytes_per_row = core.CGImageGetBytesPerRow(image_ref) 211 | bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref) 212 | bytes_per_pixel = (bytes_per_pixel + 7) // 8 213 | 214 | if bytes_per_pixel * width != bytes_per_row: 215 | cropped = bytearray() 216 | for row in range(height): 217 | start = row * bytes_per_row 218 | end = start + width * bytes_per_pixel 219 | cropped.extend(data[start:end]) 220 | data = cropped 221 | finally: 222 | if prov: 223 | core.CGDataProviderRelease(prov) 224 | if copy_data: 225 | core.CFRelease(copy_data) 226 | 227 | return self.cls_image(data, monitor, size=Size(width, height)) 228 | 229 | def _cursor_impl(self) -> ScreenShot | None: 230 | """Retrieve all cursor data. Pixels have to be RGB.""" 231 | return None 232 | -------------------------------------------------------------------------------- /src/mss/windows.py: -------------------------------------------------------------------------------- 1 | """Windows GDI-based backend for MSS. 2 | 3 | Uses user32/gdi32 APIs to capture the desktop and enumerate monitors. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import ctypes 9 | import sys 10 | from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p 11 | from ctypes.wintypes import ( 12 | BOOL, 13 | DOUBLE, 14 | DWORD, 15 | HBITMAP, 16 | HDC, 17 | HGDIOBJ, 18 | HWND, 19 | INT, 20 | LONG, 21 | LPARAM, 22 | LPRECT, 23 | RECT, 24 | UINT, 25 | WORD, 26 | ) 27 | from threading import local 28 | from typing import TYPE_CHECKING, Any 29 | 30 | from mss.base import MSSBase 31 | from mss.exception import ScreenShotError 32 | 33 | if TYPE_CHECKING: # pragma: nocover 34 | from mss.models import CFunctions, Monitor 35 | from mss.screenshot import ScreenShot 36 | 37 | __all__ = ("MSS",) 38 | 39 | BACKENDS = ["default"] 40 | 41 | 42 | CAPTUREBLT = 0x40000000 43 | DIB_RGB_COLORS = 0 44 | SRCCOPY = 0x00CC0020 45 | 46 | 47 | class BITMAPINFOHEADER(Structure): 48 | """Information about the dimensions and color format of a DIB.""" 49 | 50 | _fields_ = ( 51 | ("biSize", DWORD), 52 | ("biWidth", LONG), 53 | ("biHeight", LONG), 54 | ("biPlanes", WORD), 55 | ("biBitCount", WORD), 56 | ("biCompression", DWORD), 57 | ("biSizeImage", DWORD), 58 | ("biXPelsPerMeter", LONG), 59 | ("biYPelsPerMeter", LONG), 60 | ("biClrUsed", DWORD), 61 | ("biClrImportant", DWORD), 62 | ) 63 | 64 | 65 | class BITMAPINFO(Structure): 66 | """Structure that defines the dimensions and color information for a DIB.""" 67 | 68 | _fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)) 69 | 70 | 71 | MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE) 72 | 73 | 74 | # C functions that will be initialised later. 75 | # 76 | # Available attr: gdi32, user32. 77 | # 78 | # Note: keep it sorted by cfunction. 79 | CFUNCTIONS: CFunctions = { 80 | # Syntax: cfunction: (attr, argtypes, restype) 81 | "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), 82 | "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), 83 | "CreateCompatibleDC": ("gdi32", [HDC], HDC), 84 | "DeleteDC": ("gdi32", [HDC], HDC), 85 | "DeleteObject": ("gdi32", [HGDIOBJ], INT), 86 | "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), 87 | "GetDeviceCaps": ("gdi32", [HWND, INT], INT), 88 | "GetDIBits": ("gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL), 89 | "GetSystemMetrics": ("user32", [INT], INT), 90 | "GetWindowDC": ("user32", [HWND], HDC), 91 | "ReleaseDC": ("user32", [HWND, HDC], c_int), 92 | "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), 93 | } 94 | 95 | 96 | class MSS(MSSBase): 97 | """Multiple ScreenShots implementation for Microsoft Windows. 98 | 99 | This has no Windows-specific constructor parameters. 100 | 101 | .. seealso:: 102 | 103 | :py:class:`mss.base.MSSBase` 104 | Lists constructor parameters. 105 | """ 106 | 107 | __slots__ = {"_handles", "gdi32", "user32"} 108 | 109 | def __init__(self, /, **kwargs: Any) -> None: 110 | super().__init__(**kwargs) 111 | 112 | self.user32 = ctypes.WinDLL("user32") 113 | self.gdi32 = ctypes.WinDLL("gdi32") 114 | self._set_cfunctions() 115 | self._set_dpi_awareness() 116 | 117 | # Available thread-specific variables 118 | self._handles = local() 119 | self._handles.region_width_height = (0, 0) 120 | self._handles.bmp = None 121 | self._handles.srcdc = self.user32.GetWindowDC(0) 122 | self._handles.memdc = self.gdi32.CreateCompatibleDC(self._handles.srcdc) 123 | 124 | bmi = BITMAPINFO() 125 | bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) 126 | bmi.bmiHeader.biPlanes = 1 # Always 1 127 | bmi.bmiHeader.biBitCount = 32 # See grab.__doc__ [2] 128 | bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression) 129 | bmi.bmiHeader.biClrUsed = 0 # See grab.__doc__ [3] 130 | bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3] 131 | self._handles.bmi = bmi 132 | 133 | def _close_impl(self) -> None: 134 | # Clean-up 135 | if self._handles.bmp: 136 | self.gdi32.DeleteObject(self._handles.bmp) 137 | self._handles.bmp = None 138 | 139 | if self._handles.memdc: 140 | self.gdi32.DeleteDC(self._handles.memdc) 141 | self._handles.memdc = None 142 | 143 | if self._handles.srcdc: 144 | self.user32.ReleaseDC(0, self._handles.srcdc) 145 | self._handles.srcdc = None 146 | 147 | def _set_cfunctions(self) -> None: 148 | """Set all ctypes functions and attach them to attributes.""" 149 | cfactory = self._cfactory 150 | attrs = { 151 | "gdi32": self.gdi32, 152 | "user32": self.user32, 153 | } 154 | for func, (attr, argtypes, restype) in CFUNCTIONS.items(): 155 | cfactory(attrs[attr], func, argtypes, restype) 156 | 157 | def _set_dpi_awareness(self) -> None: 158 | """Set DPI awareness to capture full screen on Hi-DPI monitors.""" 159 | version = sys.getwindowsversion()[:2] 160 | if version >= (6, 3): 161 | # Windows 8.1+ 162 | # Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means: 163 | # per monitor DPI aware. This app checks for the DPI when it is 164 | # created and adjusts the scale factor whenever the DPI changes. 165 | # These applications are not automatically scaled by the system. 166 | ctypes.windll.shcore.SetProcessDpiAwareness(2) 167 | elif (6, 0) <= version < (6, 3): 168 | # Windows Vista, 7, 8, and Server 2012 169 | self.user32.SetProcessDPIAware() 170 | 171 | def _monitors_impl(self) -> None: 172 | """Get positions of monitors. It will populate self._monitors.""" 173 | int_ = int 174 | user32 = self.user32 175 | get_system_metrics = user32.GetSystemMetrics 176 | 177 | # All monitors 178 | self._monitors.append( 179 | { 180 | "left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN 181 | "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN 182 | "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN 183 | "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN 184 | }, 185 | ) 186 | 187 | # Each monitor 188 | def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int: 189 | """Callback for monitorenumproc() function, it will return 190 | a RECT with appropriate values. 191 | """ 192 | rct = rect.contents 193 | self._monitors.append( 194 | { 195 | "left": int_(rct.left), 196 | "top": int_(rct.top), 197 | "width": int_(rct.right) - int_(rct.left), 198 | "height": int_(rct.bottom) - int_(rct.top), 199 | }, 200 | ) 201 | return 1 202 | 203 | callback = MONITORNUMPROC(_callback) 204 | user32.EnumDisplayMonitors(0, 0, callback, 0) 205 | 206 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 207 | """Retrieve all pixels from a monitor. Pixels have to be RGB. 208 | 209 | In the code, there are a few interesting things: 210 | 211 | [1] bmi.bmiHeader.biHeight = -height 212 | 213 | A bottom-up DIB is specified by setting the height to a 214 | positive number, while a top-down DIB is specified by 215 | setting the height to a negative number. 216 | https://msdn.microsoft.com/en-us/library/ms787796.aspx 217 | https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx 218 | 219 | 220 | [2] bmi.bmiHeader.biBitCount = 32 221 | image_data = create_string_buffer(height * width * 4) 222 | 223 | We grab the image in RGBX mode, so that each word is 32bit 224 | and we have no striding. 225 | Inspired by https://github.com/zoofIO/flexx 226 | 227 | 228 | [3] bmi.bmiHeader.biClrUsed = 0 229 | bmi.bmiHeader.biClrImportant = 0 230 | 231 | When biClrUsed and biClrImportant are set to zero, there 232 | is "no" color table, so we can read the pixels of the bitmap 233 | retrieved by gdi32.GetDIBits() as a sequence of RGB values. 234 | Thanks to http://stackoverflow.com/a/3688682 235 | """ 236 | srcdc, memdc = self._handles.srcdc, self._handles.memdc 237 | gdi = self.gdi32 238 | width, height = monitor["width"], monitor["height"] 239 | 240 | if self._handles.region_width_height != (width, height): 241 | self._handles.region_width_height = (width, height) 242 | self._handles.bmi.bmiHeader.biWidth = width 243 | self._handles.bmi.bmiHeader.biHeight = -height # Why minus? [1] 244 | self._handles.data = ctypes.create_string_buffer(width * height * 4) # [2] 245 | if self._handles.bmp: 246 | gdi.DeleteObject(self._handles.bmp) 247 | self._handles.bmp = gdi.CreateCompatibleBitmap(srcdc, width, height) 248 | gdi.SelectObject(memdc, self._handles.bmp) 249 | 250 | gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT) 251 | bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS) 252 | if bits != height: 253 | msg = "gdi32.GetDIBits() failed." 254 | raise ScreenShotError(msg) 255 | 256 | return self.cls_image(bytearray(self._handles.data), monitor) 257 | 258 | def _cursor_impl(self) -> ScreenShot | None: 259 | """Retrieve all cursor data. Pixels have to be RGB.""" 260 | return None 261 | -------------------------------------------------------------------------------- /demos/tinytv-stream-simple.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # You're the type of person who likes to understand how things work under the hood. You want to see a simple example 4 | # of how to stream video to a TinyTV. This is that example! 5 | # 6 | # There's a more advanced example, tinytv-stream.py, that has more features and better performance. But this simple 7 | # demo is easier to understand, because it does everything in a straightforward way, without any complicated features. 8 | # 9 | # Wait, what's a TinyTV? It's a tiny retro-style TV, about 5cm tall. You can put videos on it, or stream video to it 10 | # over USB. Advanced users can even reprogram its firmware! You can find out more about it at https://tinytv.us/ 11 | # 12 | # You may want to read at least the docstring at the top of tinytv-stream.py, since it gives you some details about 13 | # setting up permissions on Linux to connect to your TinyTV. 14 | # 15 | # We use three libraries that don't come with Python: PySerial, Pillow, and (of course) MSS. You'll need to install 16 | # those with "pip install pyserial pillow mss". Normally, you'll want to install these into a venv; if you don't know 17 | # about those, there are lots of great tutorials online. 18 | 19 | from __future__ import annotations 20 | 21 | import io 22 | import sys 23 | import time 24 | 25 | import serial 26 | from PIL import Image 27 | 28 | import mss 29 | 30 | 31 | def main() -> None: 32 | # The TinyTV gets streaming input over its USB connection by emulating an old-style serial port. We can send our 33 | # video to that serial port, in the format that the TinyTV expects. 34 | # 35 | # The advanced demo can find the correct device name by looking at the USB IDs of the devices. In this simple 36 | # demo, we just ask the user to supply it. 37 | if len(sys.argv) != 2: # noqa: PLR2004 38 | print( 39 | f"Usage: {sys.argv[0]} DEVICE\n" 40 | "where DEVICE is something like /dev/ttyACM0 or COM3.\n" 41 | 'Use "python3 -m serial.tools.list_ports -v" to list your available devices.' 42 | ) 43 | sys.exit(2) 44 | device = sys.argv[1] 45 | 46 | # Open the serial port. It's usually best to use the serial port in a "with:" block like this, to make sure it's 47 | # cleaned up when you're done with it. 48 | with serial.Serial(device, timeout=1, write_timeout=1) as ser: 49 | # The TinyTV might have sent something to the serial port earlier, such as to a program that it was talking to 50 | # that crashed without reading it. If that happens, these messages will still be in the device's input 51 | # buffer, waiting to be read. We'll just delete anything waiting to be read, to get a fresh start. 52 | ser.reset_input_buffer() 53 | 54 | # Let's find out what type of TinyTV this is. The TinyTV has a special command to get that. 55 | ser.write(b'{"GET":"tvType"}') 56 | tvtype_response = ser.readline() 57 | print("Received response:", tvtype_response.strip()) 58 | 59 | # The response is usually something like {"tvType":TinyTV2}. Normally, you'd want to use json.loads to parse 60 | # JSON. But this isn't correct JSON (there's no quotes around the TV type), so we can't do that. 61 | # 62 | # But we still need to know the TV type, so we can figure out the screen size. We'll just see if the response 63 | # mentions the right type. 64 | if b"TinyTV2" in tvtype_response: 65 | tinytv_size = (210, 135) 66 | elif b"TinyTVKit" in tvtype_response: 67 | tinytv_size = (96, 64) 68 | elif b"TinyTVMini" in tvtype_response: 69 | tinytv_size = (64, 64) 70 | else: 71 | print("This doesn't seem to be a supported type of TinyTV.") 72 | sys.exit(1) 73 | print("Detected TinyTV with screen size", tinytv_size) 74 | 75 | # We're ready to start taking screenshots and sending them to the TinyTV! Let's start by creating an MSS 76 | # object. Like the serial object, we use a "with:" block to make sure that it can clean up after we're done 77 | # with it. 78 | # 79 | # Note that we use the same MSS object the whole time. We don't try to keep creating a new MSS object each 80 | # time we take a new screenshot. That's because the MSS object has a lot of stuff that it sets up and 81 | # remembers, and creating a new MSS object each time would mean that it has to repeat that setup constantly. 82 | with mss.mss() as sct: 83 | # It's time to get the monitor that we're going to capture. In this demo, we just capture the first 84 | # monitor. (We could also use monitors[0] for all the monitors combined.) 85 | monitor = sct.monitors[1] 86 | print("Monitor:", monitor) 87 | 88 | # The rest of this will run forever, until we get an error or the user presses Ctrl-C. Let's record our 89 | # starting time and count frames, so we can report FPS at the end. 90 | start_time = time.perf_counter() 91 | frame_count = 0 92 | try: 93 | while True: 94 | # First, we get a screenshot. MSS makes this easy! 95 | screenshot = sct.grab(monitor) 96 | 97 | # The next step is to resize the image to fit the TinyTV's screen. There's a great image 98 | # manipulation library called PIL, or Pillow, that can do that. Let's transfer the raw pixels in 99 | # the ScreenShot object into a PIL Image. 100 | original_image = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX") 101 | 102 | # Now, we can resize it. The resize method may stretch the image to make it match the TinyTV's 103 | # screen; the advanced demo gives other options. Using a reducing gap is optional, but speeds up 104 | # the resize significantly. 105 | scaled_image = original_image.resize(tinytv_size, reducing_gap=3.0) 106 | 107 | # The TinyTV wants its image frames in JPEG format. PIL can save an image to a JPEG file, but we 108 | # want the JPEG data as a bunch of bytes we can transmit to the TinyTV. Python provides 109 | # io.BytesIO to make something that pretends to be a file to PIL, but lets you just get the bytes 110 | # that PIL writes. 111 | with io.BytesIO() as fh: 112 | scaled_image.save(fh, format="JPEG") 113 | jpeg_bytes = fh.getvalue() 114 | 115 | # We're ready to send the frame to the TinyTV! First, though, this is a good time to look for any 116 | # error messages that the TinyTV has sent us. In today's firmware, anything the TinyTV sends us 117 | # is always an error message; it doesn't send us anything normally. (Of course, this might change 118 | # in later firmware versions, so we may need to change this someday.) 119 | if ser.in_waiting != 0: 120 | # There is indeed an error message. Let's read it and show it to the user. 121 | incoming_data = ser.read(ser.in_waiting) 122 | print(f"Error from TinyTV: {incoming_data!r}") 123 | sys.exit(1) 124 | 125 | # The TinyTV wants us to send a command to tell it that we're about to send it a new video frame. 126 | # We also need to tell it how many bytes of JPEG data we're going to send. The command we send 127 | # looks like {"FRAME":12345}. 128 | delimiter = b'{"FRAME":%i}' % len(jpeg_bytes) 129 | ser.write(delimiter) 130 | 131 | # Now that we've written the command delimiter, we're ready to write the JPEG data. 132 | ser.write(jpeg_bytes) 133 | 134 | # Once we've written the frame, update our counter. 135 | frame_count += 1 136 | 137 | # Now we loop! This program will keep running forever, or until you press Ctrl-C. 138 | 139 | finally: 140 | # When the loop exits, report our stats. 141 | end_time = time.perf_counter() 142 | run_time = end_time - start_time 143 | print("Frame count:", frame_count) 144 | print("Time (secs):", run_time) 145 | if run_time > 0: 146 | print("FPS:", frame_count / run_time) 147 | 148 | 149 | # Thanks for reading this far! Let's talk about some improvements; these all appear in the advanced version. 150 | # 151 | # * Right now, the user has to figure out the right device name for the TinyTV's serial port and supply it on the 152 | # command line. The advanced version can find the right device automatically by looking at the USB IDs of the 153 | # connected devices. 154 | # 155 | # * There are a lot of things the user might want to do differently, such as choosing which monitor to capture, or 156 | # changing the JPEG quality (which can affect how fast the TinyTV can process it). The advanced version uses 157 | # argparse to provide command-line options for these things. 158 | # 159 | # * The advanced program shows a status line with things like the current FPS. 160 | # 161 | # * Programs of any significant size have a lot of common things you usually want to think about, like organization 162 | # into separate functions and classes, error handling, logging, and so forth. In this simple demo, we didn't worry 163 | # about those, but they're important for a real program. 164 | # 165 | # * Here's the biggest difference, though. In the program above, we do a lot of things one at a time. First we take 166 | # a screenshot, then we resize it, then we send it to the TinyTV. 167 | # 168 | # We could overlap these, though: while we're sending one screenshot to the TinyTV, we could be preparing the next 169 | # one. This can speed up the program from about 15 fps to about 25 fps, which is about as fast as the TinyTV can 170 | # run! 171 | # 172 | # This is called a pipeline. While it's tough to coordinate, just like it's harder to coordinate a group of people 173 | # working together than to do everything yourself, it also can be much faster. A lot of the code in the advanced 174 | # version is actually about managing the pipeline. 175 | # 176 | # Using a pipeline isn't always helpful: you have to understand which operations the system can run in parallel, and 177 | # how Python itself coordinates threads. That said, I do find that many times, if I'm using MSS to capture video, 178 | # it does benefit from pipelining these three stages: taking a screenshot, processing it, and sending it somewhere 179 | # else (like a web server or an AVI file). 180 | 181 | if __name__ == "__main__": 182 | main() 183 | -------------------------------------------------------------------------------- /src/mss/linux/xshmgetimage.py: -------------------------------------------------------------------------------- 1 | """XCB backend using MIT-SHM XShmGetImage with automatic fallback. 2 | 3 | This is the fastest Linux backend available, and will work in most common 4 | cases. However, it will not work over remote X connections, such as over ssh. 5 | 6 | This implementation prefers shared-memory captures for performance and will 7 | fall back to XGetImage when the MIT-SHM extension is unavailable or fails at 8 | runtime. The fallback reason is exposed via ``shm_fallback_reason`` to aid 9 | debugging. 10 | 11 | .. versionadded:: 10.2.0 12 | """ 13 | 14 | from __future__ import annotations 15 | 16 | import enum 17 | import os 18 | from mmap import PROT_READ, mmap # type: ignore[attr-defined] 19 | from typing import TYPE_CHECKING, Any 20 | 21 | from mss.exception import ScreenShotError 22 | from mss.linux import xcb 23 | from mss.linux.xcbhelpers import LIB, XProtoError 24 | 25 | from .base import ALL_PLANES, MSSXCBBase 26 | 27 | if TYPE_CHECKING: 28 | from mss.models import Monitor 29 | from mss.screenshot import ScreenShot 30 | 31 | 32 | class ShmStatus(enum.Enum): 33 | """Availability of the MIT-SHM extension for this backend.""" 34 | 35 | UNKNOWN = enum.auto() # Constructor says SHM *should* work, but we haven't seen a real GetImage succeed yet. 36 | AVAILABLE = enum.auto() # We've successfully used XShmGetImage at least once. 37 | UNAVAILABLE = enum.auto() # We know SHM GetImage is unusable; always use XGetImage. 38 | 39 | 40 | class MSS(MSSXCBBase): 41 | """XCB backend using XShmGetImage with an automatic XGetImage fallback. 42 | 43 | .. seealso:: 44 | :py:class:`mss.linux.base.MSSXCBBase` 45 | Lists constructor parameters. 46 | """ 47 | 48 | def __init__(self, /, **kwargs: Any) -> None: 49 | super().__init__(**kwargs) 50 | 51 | # These are the objects we need to clean up when we shut down. They are created in _setup_shm. 52 | self._memfd: int | None = None 53 | self._buf: mmap | None = None 54 | self._shmseg: xcb.ShmSeg | None = None 55 | 56 | # Rather than trying to track the shm_status, we may be able to raise an exception in __init__ if XShmGetImage 57 | # isn't available. The factory in linux/__init__.py could then catch that and switch to XGetImage. 58 | # The conditions under which the attach will succeed but the xcb_shm_get_image will fail are extremely 59 | # rare, and I haven't yet found any that also will work with xcb_get_image. 60 | #: Whether we can use the MIT-SHM extensions for this connection. 61 | #: This will not be ``AVAILABLE`` until at least one capture has succeeded. 62 | #: It may be set to ``UNAVAILABLE`` sooner. 63 | self.shm_status: ShmStatus = self._setup_shm() 64 | #: If MIT-SHM is unavailable, the reason why (for debugging purposes). 65 | self.shm_fallback_reason: str | None = None 66 | 67 | def _shm_report_issue(self, msg: str, *args: Any) -> None: 68 | """Debugging hook for troubleshooting MIT-SHM issues. 69 | 70 | This will be called whenever MIT-SHM is disabled. The optional 71 | arguments are not well-defined; exceptions are common. 72 | """ 73 | full_msg = msg 74 | if args: 75 | full_msg += " | " + ", ".join(str(arg) for arg in args) 76 | self.shm_fallback_reason = full_msg 77 | 78 | def _setup_shm(self) -> ShmStatus: # noqa: PLR0911 79 | assert self.conn is not None # noqa: S101 80 | 81 | try: 82 | shm_ext_data = xcb.get_extension_data(self.conn, LIB.shm_id) 83 | if not shm_ext_data.present: 84 | self._shm_report_issue("MIT-SHM extension not present") 85 | return ShmStatus.UNAVAILABLE 86 | 87 | # We use the FD-based version of ShmGetImage, so we require the extension to be at least 1.2. 88 | shm_version_data = xcb.shm_query_version(self.conn) 89 | shm_version = (shm_version_data.major_version, shm_version_data.minor_version) 90 | if shm_version < (1, 2): 91 | self._shm_report_issue("MIT-SHM version too old", shm_version) 92 | return ShmStatus.UNAVAILABLE 93 | 94 | # We allocate something large enough for the root, so we don't have to reallocate each time the window is 95 | # resized. 96 | self._bufsize = self.pref_screen.width_in_pixels * self.pref_screen.height_in_pixels * 4 97 | 98 | if not hasattr(os, "memfd_create"): 99 | self._shm_report_issue("os.memfd_create not available") 100 | return ShmStatus.UNAVAILABLE 101 | try: 102 | self._memfd = os.memfd_create("mss-shm-buf", flags=os.MFD_CLOEXEC) # type: ignore[attr-defined] 103 | except OSError as e: 104 | self._shm_report_issue("memfd_create failed", e) 105 | self._shutdown_shm() 106 | return ShmStatus.UNAVAILABLE 107 | os.ftruncate(self._memfd, self._bufsize) 108 | 109 | try: 110 | self._buf = mmap(self._memfd, self._bufsize, prot=PROT_READ) # type: ignore[call-arg] 111 | except OSError as e: 112 | self._shm_report_issue("mmap failed", e) 113 | self._shutdown_shm() 114 | return ShmStatus.UNAVAILABLE 115 | 116 | self._shmseg = xcb.ShmSeg(xcb.generate_id(self.conn).value) 117 | try: 118 | # This will normally be what raises an exception if you're on a remote connection. 119 | # XCB will close _memfd, on success or on failure. 120 | try: 121 | xcb.shm_attach_fd(self.conn, self._shmseg, self._memfd, read_only=False) 122 | finally: 123 | self._memfd = None 124 | except xcb.XError as e: 125 | self._shm_report_issue("Cannot attach MIT-SHM segment", e) 126 | self._shutdown_shm() 127 | return ShmStatus.UNAVAILABLE 128 | 129 | except Exception: 130 | self._shutdown_shm() 131 | raise 132 | 133 | return ShmStatus.UNKNOWN 134 | 135 | def _close_impl(self) -> None: 136 | self._shutdown_shm() 137 | super()._close_impl() 138 | 139 | def _shutdown_shm(self) -> None: 140 | # It would be nice to also try to tell the server to detach the shmseg, but we might be in an error path 141 | # and don't know if that's possible. It's not like we'll leak a lot of them on the same connection anyway. 142 | # This can be called in the path of partial initialization. 143 | if self._buf is not None: 144 | self._buf.close() 145 | self._buf = None 146 | if self._memfd is not None: 147 | os.close(self._memfd) 148 | self._memfd = None 149 | 150 | def _grab_impl_xshmgetimage(self, monitor: Monitor) -> ScreenShot: 151 | if self.conn is None: 152 | msg = "Cannot take screenshot while the connection is closed" 153 | raise ScreenShotError(msg) 154 | assert self._buf is not None # noqa: S101 155 | assert self._shmseg is not None # noqa: S101 156 | 157 | required_size = monitor["width"] * monitor["height"] * 4 158 | if required_size > self._bufsize: 159 | # This is temporary. The permanent fix will depend on how 160 | # issue https://github.com/BoboTiG/python-mss/issues/432 is resolved. 161 | msg = ( 162 | "Requested capture size exceeds the allocated buffer. If you have resized the screen, " 163 | "please recreate your MSS object." 164 | ) 165 | raise ScreenShotError(msg) 166 | 167 | img_reply = xcb.shm_get_image( 168 | self.conn, 169 | self.drawable.value, 170 | monitor["left"], 171 | monitor["top"], 172 | monitor["width"], 173 | monitor["height"], 174 | ALL_PLANES, 175 | xcb.ImageFormat.ZPixmap, 176 | self._shmseg, 177 | 0, 178 | ) 179 | 180 | if img_reply.depth != self.drawable_depth or img_reply.visual.value != self.drawable_visual_id: 181 | # This should never happen; a window can't change its visual. 182 | msg = ( 183 | "Server returned an image with a depth or visual different than it initially reported: " 184 | f"expected {self.drawable_depth},{hex(self.drawable_visual_id)}, " 185 | f"got {img_reply.depth},{hex(img_reply.visual.value)}" 186 | ) 187 | raise ScreenShotError(msg) 188 | 189 | # Snapshot the buffer into new bytearray. 190 | new_size = monitor["width"] * monitor["height"] * 4 191 | # Slicing the memoryview creates a new memoryview that points to the relevant subregion. Making this and 192 | # then copying it into a fresh bytearray is much faster than slicing the mmap object. 193 | img_mv = memoryview(self._buf)[:new_size] 194 | img_data = bytearray(img_mv) 195 | 196 | return self.cls_image(img_data, monitor) 197 | 198 | def _grab_impl(self, monitor: Monitor) -> ScreenShot: 199 | """Retrieve all pixels from a monitor. Pixels have to be RGBX.""" 200 | if self.shm_status == ShmStatus.UNAVAILABLE: 201 | return super()._grab_impl_xgetimage(monitor) 202 | 203 | # The usual path is just the next few lines. 204 | try: 205 | rv = self._grab_impl_xshmgetimage(monitor) 206 | self.shm_status = ShmStatus.AVAILABLE 207 | except XProtoError as e: 208 | if self.shm_status != ShmStatus.UNKNOWN: 209 | # We know XShmGetImage works, because it worked earlier. Reraise the error. 210 | raise 211 | 212 | # Should we engage the fallback path? In almost all cases, if XShmGetImage failed at this stage (after 213 | # all our testing in __init__), XGetImage will also fail. This could mean that the user sent an 214 | # out-of-bounds request. In more exotic situations, some rare X servers disallow screen capture 215 | # altogether: security-hardened servers, for instance, or some XPrint servers. But let's make sure, by 216 | # testing the same request through XGetImage. 217 | try: 218 | rv = super()._grab_impl_xgetimage(monitor) 219 | except XProtoError: # noqa: TRY203 220 | # The XGetImage also failed, so we don't know anything about whether XShmGetImage is usable. Maybe 221 | # the user sent an out-of-bounds request. Maybe it's a security-hardened server. We're not sure what 222 | # the problem is. So, if XGetImage failed, we re-raise that error (the one from XShmGetImage will be 223 | # attached as __context__), but we won't update the shm_status yet. (Technically, our except:raise 224 | # clause here is redundant; it's just for clarity, to hold this comment.) 225 | raise 226 | 227 | # Using XShmGetImage failed, and using XGetImage worked. Use XGetImage in the future. 228 | self._shm_report_issue("MIT-SHM GetImage failed", e) 229 | self.shm_status = ShmStatus.UNAVAILABLE 230 | self._shutdown_shm() 231 | 232 | return rv 233 | -------------------------------------------------------------------------------- /src/tests/test_implementation.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import os 8 | import platform 9 | import sys 10 | import threading 11 | import time 12 | from datetime import datetime 13 | from pathlib import Path 14 | from typing import TYPE_CHECKING 15 | from unittest.mock import Mock, patch 16 | 17 | import pytest 18 | 19 | import mss 20 | from mss.__main__ import main as entry_point 21 | from mss.base import MSSBase 22 | from mss.exception import ScreenShotError 23 | from mss.screenshot import ScreenShot 24 | 25 | if TYPE_CHECKING: # pragma: nocover 26 | from collections.abc import Callable 27 | 28 | from mss.models import Monitor 29 | 30 | try: 31 | from datetime import UTC 32 | except ImportError: 33 | # Python < 3.11 34 | from datetime import timezone 35 | 36 | UTC = timezone.utc 37 | 38 | 39 | class MSS0(MSSBase): 40 | """Nothing implemented.""" 41 | 42 | 43 | class MSS1(MSSBase): 44 | """Only `grab()` implemented.""" 45 | 46 | def grab(self, monitor: Monitor) -> None: # type: ignore[override] 47 | pass 48 | 49 | 50 | class MSS2(MSSBase): 51 | """Only `monitor` implemented.""" 52 | 53 | @property 54 | def monitors(self) -> list: 55 | return [] 56 | 57 | 58 | @pytest.mark.parametrize("cls", [MSS0, MSS1, MSS2]) 59 | def test_incomplete_class(cls: type[MSSBase]) -> None: 60 | with pytest.raises(TypeError): 61 | cls() 62 | 63 | 64 | def test_bad_monitor(mss_impl: Callable[..., MSSBase]) -> None: 65 | with mss_impl() as sct, pytest.raises(ScreenShotError): 66 | sct.shot(mon=222) 67 | 68 | 69 | def test_repr(mss_impl: Callable[..., MSSBase]) -> None: 70 | box = {"top": 0, "left": 0, "width": 10, "height": 10} 71 | expected_box = {"top": 0, "left": 0, "width": 10, "height": 10} 72 | with mss_impl() as sct: 73 | img = sct.grab(box) 74 | ref = ScreenShot(bytearray(b"42"), expected_box) 75 | assert repr(img) == repr(ref) 76 | 77 | 78 | def test_factory_no_backend() -> None: 79 | with mss.mss() as sct: 80 | assert isinstance(sct, MSSBase) 81 | 82 | 83 | def test_factory_current_system(backend: str) -> None: 84 | with mss.mss(backend=backend) as sct: 85 | assert isinstance(sct, MSSBase) 86 | 87 | 88 | def test_factory_unknown_system(backend: str, monkeypatch: pytest.MonkeyPatch) -> None: 89 | monkeypatch.setattr(platform, "system", lambda: "Chuck Norris") 90 | with pytest.raises(ScreenShotError) as exc: 91 | mss.mss(backend=backend) 92 | monkeypatch.undo() 93 | 94 | error = exc.value.args[0] 95 | assert error == "System 'chuck norris' not (yet?) implemented." 96 | 97 | 98 | @pytest.fixture 99 | def reset_sys_argv(monkeypatch: pytest.MonkeyPatch) -> None: 100 | monkeypatch.setattr(sys, "argv", []) 101 | 102 | 103 | @pytest.mark.usefixtures("reset_sys_argv") 104 | @pytest.mark.parametrize("with_cursor", [False, True]) 105 | class TestEntryPoint: 106 | """CLI entry-point scenarios split into focused tests.""" 107 | 108 | @staticmethod 109 | def _run_main(with_cursor: bool, *args: str, ret: int = 0) -> None: 110 | if with_cursor: 111 | args = (*args, "--with-cursor") 112 | assert entry_point(*args) == ret 113 | 114 | def test_no_arguments(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 115 | self._run_main(with_cursor) 116 | captured = capsys.readouterr() 117 | for mon, line in enumerate(captured.out.splitlines(), 1): 118 | filename = Path(f"monitor-{mon}.png") 119 | assert line.endswith(filename.name) 120 | assert filename.is_file() 121 | filename.unlink() 122 | 123 | def test_monitor_option_and_quiet(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 124 | file = Path("monitor-1.png") 125 | filename: Path | None = None 126 | for opt in ("-m", "--monitor"): 127 | self._run_main(with_cursor, opt, "1") 128 | captured = capsys.readouterr() 129 | assert captured.out.endswith(f"{file.name}\n") 130 | filename = Path(captured.out.rstrip()) 131 | assert filename.is_file() 132 | filename.unlink() 133 | 134 | assert filename is not None 135 | for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): 136 | self._run_main(with_cursor, *opts) 137 | captured = capsys.readouterr() 138 | assert not captured.out 139 | assert filename.is_file() 140 | filename.unlink() 141 | 142 | def test_custom_output_pattern(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 143 | fmt = "sct-{mon}-{width}x{height}.png" 144 | for opt in ("-o", "--out"): 145 | self._run_main(with_cursor, opt, fmt) 146 | captured = capsys.readouterr() 147 | with mss.mss(display=os.getenv("DISPLAY")) as sct: 148 | for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): 149 | filename = Path(fmt.format(mon=mon, **monitor)) 150 | assert line.endswith(filename.name) 151 | assert filename.is_file() 152 | filename.unlink() 153 | 154 | def test_output_pattern_with_date(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 155 | fmt = "sct_{mon}-{date:%Y-%m-%d}.png" 156 | for opt in ("-o", "--out"): 157 | self._run_main(with_cursor, "-m 1", opt, fmt) 158 | filename = Path(fmt.format(mon=1, date=datetime.now(tz=UTC))) 159 | captured = capsys.readouterr() 160 | assert captured.out.endswith(f"{filename}\n") 161 | assert filename.is_file() 162 | filename.unlink() 163 | 164 | def test_coordinates_capture(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 165 | coordinates = "2,12,40,67" 166 | filename = Path("sct-2x12_40x67.png") 167 | for opt in ("-c", "--coordinates"): 168 | self._run_main(with_cursor, opt, coordinates) 169 | captured = capsys.readouterr() 170 | assert captured.out.endswith(f"{filename}\n") 171 | assert filename.is_file() 172 | filename.unlink() 173 | 174 | def test_invalid_coordinates(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 175 | coordinates = "2,12,40" 176 | for opt in ("-c", "--coordinates"): 177 | self._run_main(with_cursor, opt, coordinates, ret=2) 178 | captured = capsys.readouterr() 179 | assert captured.out == "Coordinates syntax: top, left, width, height\n" 180 | 181 | def test_backend_option(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 182 | backend = "default" 183 | for opt in ("-b", "--backend"): 184 | self._run_main(with_cursor, opt, backend, "-m1") 185 | captured = capsys.readouterr() 186 | filename = Path(captured.out.rstrip()) 187 | assert filename.is_file() 188 | filename.unlink() 189 | 190 | def test_invalid_backend_option(self, with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 191 | backend = "chuck_norris" 192 | for opt in ("-b", "--backend"): 193 | self._run_main(with_cursor, opt, backend, "-m1", ret=2) 194 | captured = capsys.readouterr() 195 | assert "argument -b/--backend: invalid choice: 'chuck_norris' (choose from" in captured.err 196 | 197 | 198 | @patch.object(sys, "argv", new=[]) # Prevent side effects while testing 199 | @patch("mss.base.MSSBase.monitors", new=[]) 200 | @pytest.mark.parametrize("quiet", [False, True]) 201 | def test_entry_point_error(quiet: bool, capsys: pytest.CaptureFixture) -> None: 202 | def main(*args: str) -> int: 203 | if quiet: 204 | args = (*args, "--quiet") 205 | return entry_point(*args) 206 | 207 | if quiet: 208 | assert main() == 1 209 | captured = capsys.readouterr() 210 | assert not captured.out 211 | assert not captured.err 212 | else: 213 | with pytest.raises(ScreenShotError): 214 | main() 215 | 216 | 217 | def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None: 218 | # Make sure to fail if arguments are not handled 219 | with ( 220 | patch("mss.factory.mss", new=Mock(side_effect=RuntimeError("Boom!"))), 221 | patch.object(sys, "argv", ["mss", "--help"]), 222 | pytest.raises(SystemExit) as exc, 223 | ): 224 | entry_point() 225 | assert exc.value.code == 0 226 | 227 | captured = capsys.readouterr() 228 | assert not captured.err 229 | assert "usage: mss" in captured.out 230 | 231 | 232 | def test_grab_with_tuple(mss_impl: Callable[..., MSSBase]) -> None: 233 | left = 100 234 | top = 100 235 | right = 500 236 | lower = 500 237 | width = right - left # 400px width 238 | height = lower - top # 400px height 239 | 240 | with mss_impl() as sct: 241 | # PIL like 242 | box = (left, top, right, lower) 243 | im = sct.grab(box) 244 | assert im.size == (width, height) 245 | 246 | # MSS like 247 | box2 = {"left": left, "top": top, "width": width, "height": height} 248 | im2 = sct.grab(box2) 249 | assert im.size == im2.size 250 | assert im.pos == im2.pos 251 | assert im.rgb == im2.rgb 252 | 253 | 254 | def test_grab_with_invalid_tuple(mss_impl: Callable[..., MSSBase]) -> None: 255 | with mss_impl() as sct: 256 | # Remember that rect tuples are PIL-style: (left, top, right, bottom) 257 | 258 | # Negative top 259 | negative_box = (100, -100, 500, 500) 260 | with pytest.raises(ScreenShotError): 261 | sct.grab(negative_box) 262 | 263 | # Negative left 264 | negative_box = (-100, 100, 500, 500) 265 | with pytest.raises(ScreenShotError): 266 | sct.grab(negative_box) 267 | 268 | # Negative width (but right > 0) 269 | negative_box = (100, 100, 50, 500) 270 | with pytest.raises(ScreenShotError): 271 | sct.grab(negative_box) 272 | 273 | # Negative height (but bottom > 0) 274 | negative_box = (100, 100, 500, 50) 275 | with pytest.raises(ScreenShotError): 276 | sct.grab(negative_box) 277 | 278 | 279 | def test_grab_with_tuple_percents(mss_impl: Callable[..., MSSBase]) -> None: 280 | with mss_impl() as sct: 281 | monitor = sct.monitors[1] 282 | left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left 283 | top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top 284 | right = left + 500 # 500px 285 | lower = top + 500 # 500px 286 | width = right - left 287 | height = lower - top 288 | 289 | # PIL like 290 | box = (left, top, right, lower) 291 | im = sct.grab(box) 292 | assert im.size == (width, height) 293 | 294 | # MSS like 295 | box2 = {"left": left, "top": top, "width": width, "height": height} 296 | im2 = sct.grab(box2) 297 | assert im.size == im2.size 298 | assert im.pos == im2.pos 299 | assert im.rgb == im2.rgb 300 | 301 | 302 | def test_thread_safety(backend: str) -> None: 303 | """Regression test for issue #169.""" 304 | 305 | def record(check: dict) -> None: 306 | """Record for one second.""" 307 | start_time = time.time() 308 | while time.time() - start_time < 1: 309 | with mss.mss(backend=backend) as sct: 310 | sct.grab(sct.monitors[1]) 311 | 312 | check[threading.current_thread()] = True 313 | 314 | checkpoint: dict = {} 315 | t1 = threading.Thread(target=record, args=(checkpoint,)) 316 | t2 = threading.Thread(target=record, args=(checkpoint,)) 317 | 318 | t1.start() 319 | time.sleep(0.5) 320 | t2.start() 321 | 322 | t1.join() 323 | t2.join() 324 | 325 | assert len(checkpoint) == 2 326 | -------------------------------------------------------------------------------- /src/mss/base.py: -------------------------------------------------------------------------------- 1 | # This is part of the MSS Python's module. 2 | # Source: https://github.com/BoboTiG/python-mss. 3 | 4 | from __future__ import annotations 5 | 6 | from abc import ABCMeta, abstractmethod 7 | from datetime import datetime 8 | from threading import Lock 9 | from typing import TYPE_CHECKING, Any 10 | 11 | from mss.exception import ScreenShotError 12 | from mss.screenshot import ScreenShot 13 | from mss.tools import to_png 14 | 15 | if TYPE_CHECKING: # pragma: nocover 16 | from collections.abc import Callable, Iterator 17 | 18 | from mss.models import Monitor, Monitors 19 | 20 | # Prior to 3.11, Python didn't have the Self type. typing_extensions does, but we don't want to depend on it. 21 | try: 22 | from typing import Self 23 | except ImportError: # pragma: nocover 24 | try: 25 | from typing_extensions import Self 26 | except ImportError: # pragma: nocover 27 | Self = Any # type: ignore[assignment] 28 | 29 | try: 30 | from datetime import UTC 31 | except ImportError: # pragma: nocover 32 | # Python < 3.11 33 | from datetime import timezone 34 | 35 | UTC = timezone.utc 36 | 37 | #: Global lock protecting access to platform screenshot calls. 38 | #: 39 | #: .. versionadded:: 6.0.0 40 | lock = Lock() 41 | 42 | OPAQUE = 255 43 | 44 | 45 | class MSSBase(metaclass=ABCMeta): 46 | """Base class for all Multiple ScreenShots implementations. 47 | 48 | :param backend: Backend selector, for platforms with multiple backends. 49 | :param compression_level: PNG compression level. 50 | :param with_cursor: Include the mouse cursor in screenshots. 51 | :param display: X11 display name (GNU/Linux only). 52 | :param max_displays: Maximum number of displays to enumerate (macOS only). 53 | 54 | .. versionadded:: 8.0.0 55 | ``compression_level``, ``display``, ``max_displays``, and ``with_cursor`` keyword arguments. 56 | """ 57 | 58 | __slots__ = {"_closed", "_monitors", "cls_image", "compression_level", "with_cursor"} 59 | 60 | def __init__( 61 | self, 62 | /, 63 | *, 64 | backend: str = "default", 65 | compression_level: int = 6, 66 | with_cursor: bool = False, 67 | # Linux only 68 | display: bytes | str | None = None, # noqa: ARG002 69 | # Mac only 70 | max_displays: int = 32, # noqa: ARG002 71 | ) -> None: 72 | self.cls_image: type[ScreenShot] = ScreenShot 73 | #: PNG compression level used when saving the screenshot data into a file 74 | #: (see :py:func:`zlib.compress()` for details). 75 | #: 76 | #: .. versionadded:: 3.2.0 77 | self.compression_level = compression_level 78 | #: Include the mouse cursor in screenshots. 79 | #: 80 | #: .. versionadded:: 8.0.0 81 | self.with_cursor = with_cursor 82 | self._monitors: Monitors = [] 83 | self._closed = False 84 | # If there isn't a factory that removed the "backend" argument, make sure that it was set to "default". 85 | # Factories that do backend-specific dispatch should remove that argument. 86 | if backend != "default": 87 | msg = 'The only valid backend on this platform is "default".' 88 | raise ScreenShotError(msg) 89 | 90 | def __enter__(self) -> Self: 91 | """For the cool call `with MSS() as mss:`.""" 92 | return self 93 | 94 | def __exit__(self, *_: object) -> None: 95 | """For the cool call `with MSS() as mss:`.""" 96 | self.close() 97 | 98 | @abstractmethod 99 | def _cursor_impl(self) -> ScreenShot | None: 100 | """Retrieve all cursor data. Pixels have to be RGB.""" 101 | 102 | @abstractmethod 103 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 104 | """Retrieve all pixels from a monitor. Pixels have to be RGB. 105 | That method has to be run using a threading lock. 106 | """ 107 | 108 | @abstractmethod 109 | def _monitors_impl(self) -> None: 110 | """Get positions of monitors (has to be run using a threading lock). 111 | It must populate self._monitors. 112 | """ 113 | 114 | def _close_impl(self) -> None: # noqa:B027 115 | """Clean up. 116 | 117 | This will be called at most once. 118 | """ 119 | # It's not necessary for subclasses to implement this if they have nothing to clean up. 120 | 121 | def close(self) -> None: 122 | """Clean up. 123 | 124 | This releases resources that MSS may be using. Once the MSS 125 | object is closed, it may not be used again. 126 | 127 | It is safe to call this multiple times; multiple calls have no 128 | effect. 129 | 130 | Rather than use :py:meth:`close` explicitly, we recommend you 131 | use the MSS object as a context manager:: 132 | 133 | with mss.mss() as sct: 134 | ... 135 | """ 136 | with lock: 137 | if self._closed: 138 | return 139 | self._close_impl() 140 | self._closed = True 141 | 142 | def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot: 143 | """Retrieve screen pixels for a given monitor. 144 | 145 | Note: ``monitor`` can be a tuple like the one 146 | :py:meth:`PIL.ImageGrab.grab` accepts: ``(left, top, right, bottom)`` 147 | 148 | :param monitor: The coordinates and size of the box to capture. 149 | See :meth:`monitors ` for object details. 150 | :returns: Screenshot of the requested region. 151 | """ 152 | # Convert PIL bbox style 153 | if isinstance(monitor, tuple): 154 | monitor = { 155 | "left": monitor[0], 156 | "top": monitor[1], 157 | "width": monitor[2] - monitor[0], 158 | "height": monitor[3] - monitor[1], 159 | } 160 | 161 | if monitor["left"] < 0 or monitor["top"] < 0: 162 | msg = f"Region has negative coordinates: {monitor!r}" 163 | raise ScreenShotError(msg) 164 | if monitor["width"] <= 0 or monitor["height"] <= 0: 165 | msg = f"Region has zero or negative size: {monitor!r}" 166 | raise ScreenShotError(msg) 167 | 168 | with lock: 169 | screenshot = self._grab_impl(monitor) 170 | if self.with_cursor and (cursor := self._cursor_impl()): 171 | return self._merge(screenshot, cursor) 172 | return screenshot 173 | 174 | @property 175 | def monitors(self) -> Monitors: 176 | """Get positions of all monitors. 177 | If the monitor has rotation, you have to deal with it 178 | inside this method. 179 | 180 | This method has to fill ``self._monitors`` with all information 181 | and use it as a cache: 182 | 183 | - ``self._monitors[0]`` is a dict of all monitors together 184 | - ``self._monitors[N]`` is a dict of the monitor N (with N > 0) 185 | 186 | Each monitor is a dict with: 187 | 188 | - ``left``: the x-coordinate of the upper-left corner 189 | - ``top``: the y-coordinate of the upper-left corner 190 | - ``width``: the width 191 | - ``height``: the height 192 | """ 193 | if not self._monitors: 194 | with lock: 195 | self._monitors_impl() 196 | 197 | return self._monitors 198 | 199 | def save( 200 | self, 201 | /, 202 | *, 203 | mon: int = 0, 204 | output: str = "monitor-{mon}.png", 205 | callback: Callable[[str], None] | None = None, 206 | ) -> Iterator[str]: 207 | """Grab a screenshot and save it to a file. 208 | 209 | :param int mon: The monitor to screenshot (default=0). ``-1`` grabs all 210 | monitors, ``0`` grabs each monitor, and ``N`` grabs monitor ``N``. 211 | :param str output: The output filename. Keywords: ``{mon}``, ``{top}``, 212 | ``{left}``, ``{width}``, ``{height}``, ``{date}``. 213 | :param callable callback: Called before saving the screenshot; receives 214 | the ``output`` argument. 215 | :return: Created file(s). 216 | """ 217 | monitors = self.monitors 218 | if not monitors: 219 | msg = "No monitor found." 220 | raise ScreenShotError(msg) 221 | 222 | if mon == 0: 223 | # One screenshot by monitor 224 | for idx, monitor in enumerate(monitors[1:], 1): 225 | fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor) 226 | if callable(callback): 227 | callback(fname) 228 | sct = self.grab(monitor) 229 | to_png(sct.rgb, sct.size, level=self.compression_level, output=fname) 230 | yield fname 231 | else: 232 | # A screenshot of all monitors together or 233 | # a screenshot of the monitor N. 234 | mon = 0 if mon == -1 else mon 235 | try: 236 | monitor = monitors[mon] 237 | except IndexError as exc: 238 | msg = f"Monitor {mon!r} does not exist." 239 | raise ScreenShotError(msg) from exc 240 | 241 | output = output.format(mon=mon, date=datetime.now(UTC) if "{date" in output else None, **monitor) 242 | if callable(callback): 243 | callback(output) 244 | sct = self.grab(monitor) 245 | to_png(sct.rgb, sct.size, level=self.compression_level, output=output) 246 | yield output 247 | 248 | def shot(self, /, **kwargs: Any) -> str: 249 | """Helper to save the screenshot of the 1st monitor, by default. 250 | You can pass the same arguments as for :meth:`save`. 251 | """ 252 | kwargs["mon"] = kwargs.get("mon", 1) 253 | return next(self.save(**kwargs)) 254 | 255 | @staticmethod 256 | def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: 257 | """Create composite image by blending screenshot and mouse cursor.""" 258 | (cx, cy), (cw, ch) = cursor.pos, cursor.size 259 | (x, y), (w, h) = screenshot.pos, screenshot.size 260 | 261 | cx2, cy2 = cx + cw, cy + ch 262 | x2, y2 = x + w, y + h 263 | 264 | overlap = cx < x2 and cx2 > x and cy < y2 and cy2 > y 265 | if not overlap: 266 | return screenshot 267 | 268 | screen_raw = screenshot.raw 269 | cursor_raw = cursor.raw 270 | 271 | cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4 272 | cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4 273 | start_count_y = -cy if cy < 0 else 0 274 | start_count_x = -cx if cx < 0 else 0 275 | stop_count_y = ch * 4 - max(cy2, 0) 276 | stop_count_x = cw * 4 - max(cx2, 0) 277 | rgb = range(3) 278 | 279 | for count_y in range(start_count_y, stop_count_y, 4): 280 | pos_s = (count_y + cy) * w + cx 281 | pos_c = count_y * cw 282 | 283 | for count_x in range(start_count_x, stop_count_x, 4): 284 | spos = pos_s + count_x 285 | cpos = pos_c + count_x 286 | alpha = cursor_raw[cpos + 3] 287 | 288 | if not alpha: 289 | continue 290 | 291 | if alpha == OPAQUE: 292 | screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3] 293 | else: 294 | alpha2 = alpha / 255 295 | for i in rgb: 296 | screen_raw[spos + i] = int(cursor_raw[cpos + i] * alpha2 + screen_raw[spos + i] * (1 - alpha2)) 297 | 298 | return screenshot 299 | 300 | @staticmethod 301 | def _cfactory( 302 | attr: Any, 303 | func: str, 304 | argtypes: list[Any], 305 | restype: Any, 306 | /, 307 | errcheck: Callable | None = None, 308 | ) -> None: 309 | """Factory to create a ctypes function and automatically manage errors.""" 310 | meth = getattr(attr, func) 311 | meth.argtypes = argtypes 312 | meth.restype = restype 313 | if errcheck: 314 | meth.errcheck = errcheck 315 | -------------------------------------------------------------------------------- /src/tests/test_gnu_linux.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import ctypes.util 8 | import platform 9 | from ctypes import CFUNCTYPE, POINTER, _Pointer, c_int 10 | from typing import TYPE_CHECKING, Any 11 | from unittest.mock import Mock, NonCallableMock, patch 12 | 13 | import pytest 14 | 15 | import mss 16 | import mss.linux 17 | import mss.linux.xcb 18 | import mss.linux.xlib 19 | from mss.base import MSSBase 20 | from mss.exception import ScreenShotError 21 | 22 | if TYPE_CHECKING: 23 | from collections.abc import Generator 24 | 25 | pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") 26 | 27 | PYPY = platform.python_implementation() == "PyPy" 28 | 29 | WIDTH = 200 30 | HEIGHT = 200 31 | DEPTH = 24 32 | 33 | 34 | def spy_and_patch(monkeypatch: pytest.MonkeyPatch, obj: Any, name: str) -> Mock: 35 | """Replace obj.name with a call-through mock and return the mock.""" 36 | real = getattr(obj, name) 37 | spy = Mock(wraps=real) 38 | monkeypatch.setattr(obj, name, spy, raising=False) 39 | return spy 40 | 41 | 42 | @pytest.fixture(autouse=True) 43 | def without_libraries(monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest) -> Generator[None]: 44 | marker = request.node.get_closest_marker("without_libraries") 45 | if marker is None: 46 | yield None 47 | return 48 | skip_find = frozenset(marker.args) 49 | old_find_library = ctypes.util.find_library 50 | 51 | def new_find_library(name: str, *args: list, **kwargs: dict[str, Any]) -> str | None: 52 | if name in skip_find: 53 | return None 54 | return old_find_library(name, *args, **kwargs) 55 | 56 | # We use a context here so other fixtures or the test itself can use .undo. 57 | with monkeypatch.context() as mp: 58 | mp.setattr(ctypes.util, "find_library", new_find_library) 59 | yield None 60 | 61 | 62 | @pytest.fixture 63 | def display() -> Generator: 64 | with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: 65 | yield vdisplay.new_display_var 66 | 67 | 68 | def test_default_backend(display: str) -> None: 69 | with mss.mss(display=display) as sct: 70 | assert isinstance(sct, MSSBase) 71 | 72 | 73 | @pytest.mark.skipif(PYPY, reason="Failure on PyPy") 74 | def test_factory_systems(monkeypatch: pytest.MonkeyPatch, backend: str) -> None: 75 | """Here, we are testing all systems. 76 | 77 | Too hard to maintain the test for all platforms, 78 | so test only on GNU/Linux. 79 | """ 80 | # GNU/Linux 81 | monkeypatch.setattr(platform, "system", lambda: "LINUX") 82 | with mss.mss(backend=backend) as sct: 83 | assert isinstance(sct, MSSBase) 84 | monkeypatch.undo() 85 | 86 | # macOS 87 | monkeypatch.setattr(platform, "system", lambda: "Darwin") 88 | # ValueError on macOS Big Sur 89 | with pytest.raises((ScreenShotError, ValueError)), mss.mss(backend=backend): 90 | pass 91 | monkeypatch.undo() 92 | 93 | # Windows 94 | monkeypatch.setattr(platform, "system", lambda: "wInDoWs") 95 | with pytest.raises(ImportError, match="cannot import name 'WINFUNCTYPE'"), mss.mss(backend=backend): 96 | pass 97 | 98 | 99 | def test_arg_display(display: str, backend: str, monkeypatch: pytest.MonkeyPatch) -> None: 100 | # Good value 101 | with mss.mss(display=display, backend=backend): 102 | pass 103 | 104 | # Bad `display` (missing ":" in front of the number) 105 | with pytest.raises(ScreenShotError), mss.mss(display="0", backend=backend): 106 | pass 107 | 108 | # Invalid `display` that is not trivially distinguishable. 109 | with pytest.raises(ScreenShotError), mss.mss(display=":INVALID", backend=backend): 110 | pass 111 | 112 | # No `DISPLAY` in envars 113 | # The monkeypatch implementation of delenv seems to interact badly with some other uses of setenv, so we use a 114 | # monkeypatch context to isolate it a bit. 115 | with monkeypatch.context() as mp: 116 | mp.delenv("DISPLAY") 117 | with pytest.raises(ScreenShotError), mss.mss(backend=backend): 118 | pass 119 | 120 | 121 | def test_xerror_without_details() -> None: 122 | # Opening an invalid display with the Xlib backend will create an XError instance, but since there was no 123 | # XErrorEvent, then the details won't be filled in. Generate one. 124 | with pytest.raises(ScreenShotError) as excinfo, mss.mss(display=":INVALID"): 125 | pass 126 | 127 | exc = excinfo.value 128 | # Ensure it has no details. 129 | assert not exc.details 130 | # Ensure it can be stringified. 131 | str(exc) 132 | 133 | 134 | @pytest.mark.without_libraries("xcb") 135 | @patch("mss.linux.xlib._X11", new=None) 136 | def test_no_xlib_library(backend: str) -> None: 137 | with pytest.raises(ScreenShotError), mss.mss(backend=backend): 138 | pass 139 | 140 | 141 | @pytest.mark.without_libraries("xcb-randr") 142 | @patch("mss.linux.xlib._XRANDR", new=None) 143 | def test_no_xrandr_extension(backend: str) -> None: 144 | with pytest.raises(ScreenShotError), mss.mss(backend=backend): 145 | pass 146 | 147 | 148 | @patch("mss.linux.xlib.MSS._is_extension_enabled", new=Mock(return_value=False)) 149 | def test_xrandr_extension_exists_but_is_not_enabled(display: str) -> None: 150 | with pytest.raises(ScreenShotError), mss.mss(display=display, backend="xlib"): 151 | pass 152 | 153 | 154 | def test_unsupported_depth(backend: str) -> None: 155 | # 8-bit is normally PseudoColor. If the order of testing the display support changes, this might raise a 156 | # different message; just change the match= accordingly. 157 | with ( 158 | pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=8) as vdisplay, 159 | pytest.raises(ScreenShotError, match=r"\b8\b"), 160 | mss.mss(display=vdisplay.new_display_var, backend=backend) as sct, 161 | ): 162 | sct.grab(sct.monitors[1]) 163 | 164 | # 16-bit is normally TrueColor, but still just 16 bits. 165 | with ( 166 | pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=16) as vdisplay, 167 | pytest.raises(ScreenShotError, match=r"\b16\b"), 168 | mss.mss(display=vdisplay.new_display_var, backend=backend) as sct, 169 | ): 170 | sct.grab(sct.monitors[1]) 171 | 172 | 173 | def test__is_extension_enabled_unknown_name(display: str) -> None: 174 | with mss.mss(display=display, backend="xlib") as sct: 175 | assert isinstance(sct, mss.linux.xlib.MSS) # For Mypy 176 | assert not sct._is_extension_enabled("NOEXT") 177 | 178 | 179 | def test_fast_function_for_monitor_details_retrieval(display: str, monkeypatch: pytest.MonkeyPatch) -> None: 180 | with mss.mss(display=display, backend="xlib") as sct: 181 | assert isinstance(sct, mss.linux.xlib.MSS) # For Mypy 182 | assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") 183 | fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent") 184 | slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources") 185 | screenshot_with_fast_fn = sct.grab(sct.monitors[1]) 186 | 187 | fast_spy.assert_called() 188 | slow_spy.assert_not_called() 189 | 190 | assert set(screenshot_with_fast_fn.rgb) == {0} 191 | 192 | 193 | def test_client_missing_fast_function_for_monitor_details_retrieval( 194 | display: str, monkeypatch: pytest.MonkeyPatch 195 | ) -> None: 196 | with mss.mss(display=display, backend="xlib") as sct: 197 | assert isinstance(sct, mss.linux.xlib.MSS) # For Mypy 198 | assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") 199 | # Even though we're going to delete it, we'll still create a fast spy, to make sure that it isn't somehow 200 | # getting accessed through a path we hadn't considered. 201 | fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent") 202 | slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources") 203 | # If we just delete sct.xrandr.XRRGetScreenResourcesCurrent, it will get recreated automatically by ctypes 204 | # the next time it's accessed. A Mock will remember that the attribute was explicitly deleted and hide it. 205 | mock_xrandr = NonCallableMock(wraps=sct.xrandr) 206 | del mock_xrandr.XRRGetScreenResourcesCurrent 207 | monkeypatch.setattr(sct, "xrandr", mock_xrandr) 208 | assert not hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") 209 | screenshot_with_slow_fn = sct.grab(sct.monitors[1]) 210 | 211 | fast_spy.assert_not_called() 212 | slow_spy.assert_called() 213 | 214 | assert set(screenshot_with_slow_fn.rgb) == {0} 215 | 216 | 217 | def test_server_missing_fast_function_for_monitor_details_retrieval( 218 | display: str, monkeypatch: pytest.MonkeyPatch 219 | ) -> None: 220 | fake_xrrqueryversion_type = CFUNCTYPE( 221 | c_int, # Status 222 | POINTER(mss.linux.xlib.Display), # Display* 223 | POINTER(c_int), # int* major 224 | POINTER(c_int), # int* minor 225 | ) 226 | 227 | @fake_xrrqueryversion_type 228 | def fake_xrrqueryversion(_dpy: _Pointer, major_p: _Pointer, minor_p: _Pointer) -> int: 229 | major_p[0] = 1 230 | minor_p[0] = 2 231 | return 1 232 | 233 | with mss.mss(display=display, backend="xlib") as sct: 234 | assert isinstance(sct, mss.linux.xlib.MSS) # For Mypy 235 | monkeypatch.setattr(sct.xrandr, "XRRQueryVersion", fake_xrrqueryversion) 236 | fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent") 237 | slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources") 238 | screenshot_with_slow_fn = sct.grab(sct.monitors[1]) 239 | 240 | fast_spy.assert_not_called() 241 | slow_spy.assert_called() 242 | 243 | assert set(screenshot_with_slow_fn.rgb) == {0} 244 | 245 | 246 | def test_with_cursor(display: str, backend: str) -> None: 247 | with mss.mss(display=display, backend=backend) as sct: 248 | assert not hasattr(sct, "xfixes") 249 | assert not sct.with_cursor 250 | screenshot_without_cursor = sct.grab(sct.monitors[1]) 251 | 252 | # 1 color: black 253 | assert set(screenshot_without_cursor.rgb) == {0} 254 | 255 | with mss.mss(display=display, backend=backend, with_cursor=True) as sct: 256 | if backend == "xlib": 257 | assert hasattr(sct, "xfixes") 258 | assert sct.with_cursor 259 | screenshot_with_cursor = sct.grab(sct.monitors[1]) 260 | 261 | # 2 colors: black & white (default cursor is a white cross) 262 | assert set(screenshot_with_cursor.rgb) == {0, 255} 263 | 264 | 265 | @patch("mss.linux.xlib._XFIXES", new=None) 266 | def test_with_cursor_but_not_xfixes_extension_found(display: str) -> None: 267 | with mss.mss(display=display, backend="xlib", with_cursor=True) as sct: 268 | assert not hasattr(sct, "xfixes") 269 | assert not sct.with_cursor 270 | 271 | 272 | def test_with_cursor_failure(display: str) -> None: 273 | with mss.mss(display=display, backend="xlib", with_cursor=True) as sct: 274 | assert isinstance(sct, mss.linux.xlib.MSS) # For Mypy 275 | with ( 276 | patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None), 277 | pytest.raises(ScreenShotError), 278 | ): 279 | sct.grab(sct.monitors[1]) 280 | 281 | 282 | def test_shm_available() -> None: 283 | """Verify that the xshmgetimage backend doesn't always fallback. 284 | 285 | Since this backend does an automatic fallback for certain types of 286 | anticipated issues, that could cause some failures to be masked. 287 | Ensure this isn't happening. 288 | """ 289 | with ( 290 | pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay, 291 | mss.mss(display=vdisplay.new_display_var, backend="xshmgetimage") as sct, 292 | ): 293 | assert isinstance(sct, mss.linux.xshmgetimage.MSS) # For Mypy 294 | # The status currently isn't established as final until a grab succeeds. 295 | sct.grab(sct.monitors[0]) 296 | assert sct.shm_status == mss.linux.xshmgetimage.ShmStatus.AVAILABLE 297 | 298 | 299 | def test_shm_fallback() -> None: 300 | """Verify that the xshmgetimage backend falls back if MIT-SHM fails. 301 | 302 | The most common case when a fallback is needed is with a TCP 303 | connection, such as the one used with ssh relaying. By using 304 | DISPLAY=localhost:99 instead of DISPLAY=:99, we connect over TCP 305 | instead of a local-domain socket. This is sufficient to prevent 306 | MIT-SHM from completing its setup: the extension is available, but 307 | won't be able to attach a shared memory segment. 308 | """ 309 | with ( 310 | pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH, extra_args=["-listen", "tcp"]) as vdisplay, 311 | mss.mss(display=f"localhost{vdisplay.new_display_var}", backend="xshmgetimage") as sct, 312 | ): 313 | assert isinstance(sct, mss.linux.xshmgetimage.MSS) # For Mypy 314 | # Ensure that the grab call completes without exception. 315 | sct.grab(sct.monitors[0]) 316 | # Ensure that it really did have to fall back; otherwise, we'd need to change how we test this case. 317 | assert sct.shm_status == mss.linux.xshmgetimage.ShmStatus.UNAVAILABLE 318 | --------------------------------------------------------------------------------