├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .readthedocs.yml ├── .vscode └── settings.json ├── .well-known └── funding-manifest-urls ├── CHANGELOG.md ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTORS.md ├── LICENSE.txt ├── README.md ├── check.sh ├── docs ├── icon.png └── source │ ├── api.rst │ ├── conf.py │ ├── developers.rst │ ├── examples.rst │ ├── examples │ ├── callback.py │ ├── custom_cls_image.py │ ├── fps.py │ ├── fps_multiprocessing.py │ ├── from_pil_tuple.py │ ├── linux_display_keyword.py │ ├── opencv_numpy.py │ ├── part_of_screen.py │ ├── part_of_screen_monitor_2.py │ ├── pil.py │ └── pil_pixels.py │ ├── index.rst │ ├── installation.rst │ ├── support.rst │ ├── usage.rst │ └── where.rst ├── pyproject.toml └── src ├── mss ├── __init__.py ├── __main__.py ├── base.py ├── darwin.py ├── exception.py ├── factory.py ├── linux.py ├── models.py ├── py.typed ├── screenshot.py ├── tools.py └── windows.py └── tests ├── __init__.py ├── bench_bgra2rgb.py ├── bench_general.py ├── conftest.py ├── res └── monitor-1024x768.raw.zip ├── test_bgra_to_rgb.py ├── test_cls_image.py ├── test_find_monitors.py ├── test_get_pixels.py ├── test_gnu_linux.py ├── test_implementation.py ├── test_issue_220.py ├── test_leaks.py ├── test_macos.py ├── test_save.py ├── test_setup.py ├── test_tools.py ├── test_windows.py └── third_party ├── __init__.py ├── test_numpy.py └── test_pil.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: BoboTiG 2 | polar: tiger-222 3 | issuehunt: BoboTiG 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | - [ ] PEP8 compliant 15 | - [ ] `flake8` passed 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@v4 15 | - name: Install Python 16 | uses: actions/setup-python@v5 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 | -------------------------------------------------------------------------------- /.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 | jobs: 12 | quality: 13 | name: Quality 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.x" 20 | cache: pip 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install -U pip 24 | python -m pip install -e '.[dev]' 25 | - name: Check 26 | run: ./check.sh 27 | 28 | documentation: 29 | name: Documentation 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/setup-python@v5 34 | with: 35 | python-version: "3.x" 36 | cache: pip 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install -U pip 40 | python -m pip install -e '.[docs]' 41 | - name: Build 42 | run: | 43 | sphinx-build -d docs docs/source docs_out --color -W -bhtml 44 | 45 | tests: 46 | name: "${{ matrix.os.emoji }} ${{ matrix.python.name }}" 47 | runs-on: ${{ matrix.os.runs-on }} 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | os: 52 | - emoji: 🐧 53 | runs-on: [ubuntu-latest] 54 | - emoji: 🍎 55 | runs-on: [macos-latest] 56 | - emoji: 🪟 57 | runs-on: [windows-latest] 58 | python: 59 | - name: CPython 3.9 60 | runs-on: "3.9" 61 | - name: CPython 3.10 62 | runs-on: "3.10" 63 | - name: CPython 3.11 64 | runs-on: "3.11" 65 | - name: CPython 3.12 66 | runs-on: "3.12" 67 | - name: CPython 3.13 68 | runs-on: "3.13" 69 | - name: CPython 3.14 70 | runs-on: "3.14-dev" 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: actions/setup-python@v5 74 | with: 75 | python-version: ${{ matrix.python.runs-on }} 76 | cache: pip 77 | check-latest: true 78 | - name: Install dependencies 79 | run: | 80 | python -m pip install -U pip 81 | python -m pip install -e '.[dev,tests]' 82 | - name: Tests (GNU/Linux) 83 | if: matrix.os.emoji == '🐧' 84 | run: xvfb-run python -m pytest 85 | - name: Tests (macOS, Windows) 86 | if: matrix.os.emoji != '🐧' 87 | run: python -m pytest 88 | 89 | automerge: 90 | name: Automerge 91 | runs-on: ubuntu-latest 92 | needs: [documentation, quality, tests] 93 | if: ${{ github.actor == 'dependabot[bot]' }} 94 | steps: 95 | - name: Automerge 96 | run: gh pr merge --auto --rebase "$PR_URL" 97 | env: 98 | PR_URL: ${{github.event.pull_request.html_url}} 99 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 100 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "languageToolLinter.languageTool.ignoredWordsInWorkspace": [ 3 | "bgra", 4 | "ctypes", 5 | "eownis", 6 | "memoization", 7 | "noop", 8 | "numpy", 9 | "oros", 10 | "pylint", 11 | "pypy", 12 | "python-mss", 13 | "pythonista", 14 | "sdist", 15 | "sourcery", 16 | "tk", 17 | "tkinter", 18 | "xlib", 19 | "xrandr", 20 | "xserver", 21 | "zlib" 22 | ] 23 | } -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://www.tiger-222.fr/funding.json 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | See Git checking messages for full history. 4 | 5 | ## 10.1.0.dev0 (2025-xx-xx) 6 | - Mac: up to 60% performances improvement by taking screenshots at nominal resolution (e.g. scaling is off by default). To enable back scaling, set `mss.darwin.IMAGE_OPTIONS = 0`. (#257) 7 | - :heart: contributors: @brycedrennan 8 | 9 | ## 10.0.0 (2024-11-14) 10 | - removed support for Python 3.8 11 | - added support for Python 3.14 12 | - Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251) 13 | - Linux: minor optimization when checking for a X extension status (#251) 14 | - :heart: contributors: @kianmeng, @shravanasati, @mgorny 15 | 16 | ## 9.0.2 (2024-09-01) 17 | - added support for Python 3.13 18 | - leveled up the packaging using `hatchling` 19 | - used `ruff` to lint the code base (#275) 20 | - MSS: minor optimization when using an output file format without date (#275) 21 | - MSS: fixed `Pixel` model type (#274) 22 | - CI: automated release publishing on tag creation 23 | - :heart: contributors: @Andon-Li 24 | 25 | ## 9.0.1 (2023-04-20) 26 | - CLI: fixed entry point not taking into account arguments 27 | 28 | ## 9.0.0 (2023-04-18) 29 | - Linux: add failure handling to `XOpenDisplay()` call (fixes #246) 30 | - Mac: tiny improvement in monitors finding 31 | - Windows: refactored how internal handles are stored (fixes #198) 32 | - Windows: removed side effects when leaving the context manager, resources are all freed (fixes #209) 33 | - CI: run tests via `xvfb-run` on GitHub Actions (#248) 34 | - tests: enhance `test_get_pixels.py`, and try to fix a random failure at the same time (related to #251) 35 | - tests: use `PyVirtualDisplay` instead of `xvfbwrapper` (#249) 36 | - tests: automatic rerun in case of failure (related to #251) 37 | - :heart: contributors: @mgorny, @CTPaHHuK-HEbA 38 | 39 | ## 8.0.3 (2023-04-15) 40 | - added support for Python 3.12 41 | - MSS: added PEP 561 compatibility 42 | - MSS: include more files in the sdist package (#240) 43 | - Linux: restore the original X error handler in `.close()` (#241) 44 | - Linux: fixed `XRRCrtcInfo.width`, and `XRRCrtcInfo.height`, C types 45 | - docs: use Markdown for the README, and changelogs 46 | - dev: renamed the `master` branch to `main` 47 | - dev: review the structure of the repository to fix/improve packaging issues (#243) 48 | - :heart: contributors: @mgorny, @relent95 49 | 50 | ## 8.0.2 (2023-04-09) 51 | - fixed `SetuptoolsDeprecationWarning`: Installing 'XXX' as data is deprecated, please list it in packages 52 | - CLI: fixed arguments handling 53 | 54 | ## 8.0.1 (2023-04-09) 55 | - MSS: ensure `--with-cursor`, and `with_cursor` argument & attribute, are simple NOOP on platforms not supporting the feature 56 | - CLI: do not raise a `ScreenShotError` when `-q`, or `--quiet`, is used but return ` 57 | - tests: fixed `test_entry_point()` with multiple monitors having the same resolution 58 | 59 | ## 8.0.0 (2023-04-09) 60 | - removed support for Python 3.6 61 | - removed support for Python 3.7 62 | - MSS: fixed PEP 484 prohibits implicit Optional 63 | - MSS: the whole source code was migrated to PEP 570 (Python positional-only parameters) 64 | - Linux: reset the X server error handler on exit to prevent issues with Tk/Tkinter (fixes #220) 65 | - Linux: refactored how internal handles are stored to fixed issues with multiple X servers (fixes #210) 66 | - Linux: removed side effects when leaving the context manager, resources are all freed (fixes #210) 67 | - Linux: added mouse support (related to #55) 68 | - CLI: added `--with-cursor` argument 69 | - tests: added PyPy 3.9, removed `tox`, and improved GNU/Linux coverage 70 | - :heart: contributors: @zorvios 71 | 72 | ## 7.0.1 (2022-10-27) 73 | - fixed the wheel package 74 | 75 | ## 7.0.0 (2022-10-27) 76 | - added support for Python 3.11 77 | - added support for Python 3.10 78 | - removed support for Python 3.5 79 | - MSS: modernized the code base (types, `f-string`, ran `isort` & `black`) (closes #101) 80 | - MSS: fixed several Sourcery issues 81 | - MSS: fixed typos here, and there 82 | - docs: fixed an error when building the documentation 83 | 84 | ## 6.1.0 (2020-10-31) 85 | - MSS: reworked how C functions are initialized 86 | - Mac: reduce the number of function calls 87 | - Mac: support macOS Big Sur (fixes #178) 88 | - tests: expand Python versions to 3.9 and 3.10 89 | - tests: fixed macOS interpreter not found on Travis-CI 90 | - tests: fixed `test_entry_point()` when there are several monitors 91 | 92 | ## 6.0.0 (2020-06-30) 93 | - removed usage of deprecated `license_file` option for `license_files` 94 | - fixed flake8 usage in pre-commit 95 | - the module is now available on Conda (closes #170) 96 | - MSS: the implementation is now thread-safe on all OSes (fixes #169) 97 | - Linux: better handling of the Xrandr extension (fixes #168) 98 | - tests: fixed a random bug on `test_grab_with_tuple_percents()` (fixes #142) 99 | 100 | ## 5.1.0 (2020-04-30) 101 | - produce wheels for Python 3 only 102 | - MSS: renamed again `MSSMixin` to `MSSBase`, now derived from `abc.ABCMeta` 103 | - tools: force write of file when saving a PNG file 104 | - tests: fixed tests on macOS with Retina display 105 | - Windows: fixed multi-thread safety (fixes #150) 106 | - :heart: contributors: @narumishi 107 | 108 | ## 5.0.0 (2019-12-31) 109 | - removed support for Python 2.7 110 | - MSS: improve type annotations and add CI check 111 | - MSS: use `__slots__` for better performances 112 | - MSS: better handle resources to prevent leaks 113 | - MSS: improve monitors finding 114 | - Windows: use our own instances of `GDI32` and `User32` DLLs 115 | - docs: add `project_urls` to `setup.cfg` 116 | - docs: add an example using the multiprocessing module (closes #82) 117 | - tests: added regression tests for #128 and #135 118 | - tests: move tests files into the package 119 | - :heart: contributors: @hugovk, @foone, @SergeyKalutsky 120 | 121 | ## 4.0.2 (2019-02-23) 122 | - Windows: ignore missing `SetProcessDPIAware()` on Window XP (fixes #109) 123 | - :heart: contributors: @foone 124 | 125 | ## 4.0.1 (2019-01-26) 126 | - Linux: fixed several Xlib functions signature (fixes #92) 127 | - Linux: improve monitors finding by a factor of 44 128 | 129 | ## 4.0.0 (2019-01-11) 130 | - MSS: remove use of `setup.py` for `setup.cfg` 131 | - MSS: renamed `MSSBase` to `MSSMixin` in `base.py` 132 | - MSS: refactor ctypes `argtype`, `restype` and `errcheck` setup (fixes #84) 133 | - Linux: ensure resources are freed in `grab()` 134 | - Windows: avoid unnecessary class attributes 135 | - MSS: ensure calls without context manager will not leak resources or document them (fixes #72 and #85) 136 | - MSS: fixed Flake8 C408: Unnecessary dict call - rewrite as a literal, in `exceptions.py` 137 | - MSS: fixed Flake8 I100: Import statements are in the wrong order 138 | - MSS: fixed Flake8 I201: Missing newline before sections or imports 139 | - MSS: fixed PyLint bad-super-call: Bad first argument 'Exception' given to `super()` 140 | - tests: use `tox`, enable PyPy and PyPy3, add macOS and Windows CI 141 | 142 | ## 3.3.2 (2018-11-20) 143 | - MSS: do monitor detection in MSS constructor (fixes #79) 144 | - MSS: specify compliant Python versions for pip install 145 | - tests: enable Python 3.7 146 | - tests: fixed `test_entry_point()` with multiple monitors 147 | - :heart: contributors: @hugovk, @andreasbuhr 148 | 149 | ## 3.3.1 (2018-09-22) 150 | - Linux: fixed a memory leak introduced with 7e8ae5703f0669f40532c2be917df4328bc3985e (fixes #72) 151 | - docs: add the download statistics badge 152 | 153 | ## 3.3.0 (2018-09-04) 154 | - Linux: add an error handler for the XServer to prevent interpreter crash (fixes #61) 155 | - MSS: fixed a `ResourceWarning`: unclosed file in `setup.py` 156 | - tests: fixed a `ResourceWarning`: unclosed file 157 | - docs: fixed a typo in `Screenshot.pixel()` method (thanks to @mchlnix) 158 | - big code clean-up using `black` 159 | 160 | ## 3.2.1 (2018-05-21) 161 | - Windows: enable Hi-DPI awareness 162 | - :heart: contributors: @ryanfox 163 | 164 | ## 3.2.0 (2018-03-22) 165 | - removed support for Python 3.4 166 | - MSS: add the `Screenshot.bgra` attribute 167 | - MSS: speed-up grabbing on the 3 platforms 168 | - tools: add PNG compression level control to `to_png()` 169 | - tests: add `leaks.py` and `benchmarks.py` for manual testing 170 | - docs: add an example about capturing part of the monitor 2 171 | - docs: add an example about computing BGRA values to RGB 172 | 173 | ## 3.1.2 (2018-01-05) 174 | - removed support for Python 3.3 175 | - MSS: possibility to get the whole PNG raw bytes 176 | - Windows: capture all visible window 177 | - docs: improvements and fixes (fixes #37) 178 | - CI: build the documentation 179 | 180 | ## 3.1.1 (2017-11-27) 181 | - MSS: add the `mss` entry point 182 | 183 | ## 3.1.0 (2017-11-16) 184 | - MSS: add more way of customization to the output argument of `save()` 185 | - MSS: possibility to use custom class to handle screenshot data 186 | - Mac: properly support all display scaling and resolutions (fixes #14, #19, #21, #23) 187 | - Mac: fixed memory leaks (fixes #24) 188 | - Linux: handle bad display value 189 | - Windows: take into account zoom factor for high-DPI displays (fixes #20) 190 | - docs: several fixes (fixes #22) 191 | - tests: a lot of tests added for better coverage 192 | - add the 'Say Thanks' button 193 | - :heart: contributors: @karanlyons 194 | 195 | ## 3.0.1 (2017-07-06) 196 | - fixed examples links 197 | 198 | ## 3.0.0 (2017-07-06) 199 | - big refactor, introducing the `ScreenShot` class 200 | - MSS: add Numpy array interface support to the `Screenshot` class 201 | - docs: add OpenCV/Numpy, PIL pixels, FPS 202 | 203 | ## 2.0.22 (2017-04-29) 204 | - MSS: better use of exception mechanism 205 | - Linux: use of `hasattr()` to prevent Exception on early exit 206 | - Mac: take into account extra black pixels added when screen with is not divisible by 16 (fixes #14) 207 | - docs: add an example to capture only a part of the screen 208 | - :heart: contributors: David Becker, @redodo 209 | 210 | ## 2.0.18 (2016-12-03) 211 | - change license to MIT 212 | - MSS: add type hints 213 | - MSS: remove unused code (reported by `Vulture`) 214 | - Linux: remove MSS library 215 | - Linux: insanely fast using only ctypes 216 | - Linux: skip unused monitors 217 | - Linux: use `errcheck` instead of deprecated `restype` with callable (fixes #11) 218 | - Linux: fixed security issue (reported by Bandit) 219 | - docs: add documentation (fixes #10) 220 | - tests: add tests and use Travis CI (fixes #9) 221 | - :heart: contributors: @cycomanic 222 | 223 | ## 2.0.0 (2016-06-04) 224 | - add issue and pull request templates 225 | - split the module into several files 226 | - MSS: a lot of code refactor and optimizations 227 | - MSS: rename `save_img()` to `to_png()` 228 | - MSS: `save()`: replace `screen` argument by `mon` 229 | - Mac: get rid of the `PyObjC` module, 100% ctypes 230 | - Linux: prevent segfault when `DISPLAY` is set but no X server started 231 | - Linux: prevent segfault when Xrandr is not loaded 232 | - Linux: `get_pixels()` insanely fast, use of MSS library (C code) 233 | - Windows: screenshot not correct on Windows 8 (fixes #6) 234 | 235 | ## 1.0.2 (2016-04-22) 236 | - MSS: fixed non-existent alias 237 | 238 | ## 1.0.1 (2016-04-22) 239 | - MSS: `libpng` warning (ignoring bad filter type) (fixes #7) 240 | 241 | ## 1.0.0 (2015-04-16) 242 | - Python 2.6 to 3.5 ready 243 | - MSS: code clean-up and review, no more debug information 244 | - MSS: add a shortcut to take automatically use the proper `MSS` class (fixes #5) 245 | - MSS: few optimizations into `save_img()` 246 | - Darwin: remove rotation from information returned by `enum_display_monitors()` 247 | - Linux: fixed `object has no attribute 'display' into __del__` 248 | - Linux: use of `XDestroyImage()` instead of `XFree()` 249 | - Linux: optimizations of `get_pixels()` 250 | - Windows: huge optimization of `get_pixels()` 251 | - CLI: delete `--debug` argument 252 | 253 | ## 0.1.1 (2015-04-10) 254 | - MSS: little code review 255 | - Linux: fixed monitor count 256 | - tests: remove `test-linux` binary 257 | - docs: add `doc/TESTING` 258 | - docs: remove Bonus section from README 259 | 260 | ## 0.1.0 (2015-04-10) 261 | - MSS: fixed code with `YAPF` tool 262 | - Linux: fully functional using Xrandr library 263 | - Linux: code clean-up (no more XML files to parse) 264 | - docs: better tests and examples 265 | 266 | ## 0.0.8 (2015-02-04) 267 | - MSS: filename's directory is not used when saving (fixes #3) 268 | - MSS: fixed flake8 error: E713 test for membership should be 'not in' 269 | - MSS: raise an exception for unimplemented methods 270 | - Windows: robustness to `MSSWindows.get_pixels` (fixes #4) 271 | - :heart: contributors: @sergey-vin, @thehesiod 272 | 273 | ## 0.0.7 (2014-03-20) 274 | - MSS: fixed path where screenshots are saved 275 | 276 | ## 0.0.6 (2014-03-19) 277 | - Python 3.4 ready 278 | - PEP8 compliant 279 | - MSS: review module structure to fit the "Code Like a Pythonista: Idiomatic Python" 280 | - MSS: refactoring of all `enum_display_monitors()` methods 281 | - MSS: fixed misspellings using `codespell` tool 282 | - MSS: better way to manage output filenames (callback) 283 | - MSS: several fixes here and there, code refactoring 284 | - Linux: add XFCE4 support 285 | - CLI: possibility to append `--debug` to the command line 286 | - :heart: contributors: @sametmax 287 | 288 | ## 0.0.5 (2013-11-01) 289 | - MSS: code simplified 290 | - Windows: few optimizations into `_arrange()` 291 | 292 | ## 0.0.4 (2013-10-31) 293 | - Linux: use of memoization → huge time/operations gains 294 | 295 | ## 0.0.3 (2013-10-30) 296 | - MSS: removed PNG filters 297 | - MSS: removed `ext` argument, using only PNG 298 | - MSS: do not overwrite existing image files 299 | - MSS: few optimizations into `png()` 300 | - Linux: few optimizations into `get_pixels()` 301 | 302 | ## 0.0.2 (2013-10-21) 303 | - added support for python 3 on Windows and GNU/Linux 304 | - :heart: contributors: Oros, Eownis 305 | 306 | ## 0.0.1 (2013-07-01) 307 | - first release 308 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Technical Changes 2 | 3 | ## 10.1.0 (2025-xx-xx) 4 | 5 | ### darwin.py 6 | - Added `IMAGE_OPTIONS` 7 | - Added `kCGWindowImageBoundsIgnoreFraming` 8 | - Added `kCGWindowImageNominalResolution` 9 | - Added `kCGWindowImageShouldBeOpaque` 10 | 11 | ## 10.0.0 (2024-11-14) 12 | 13 | ### base.py 14 | - Added `OPAQUE` 15 | 16 | ### darwin.py 17 | - Added `MAC_VERSION_CATALINA` 18 | 19 | ### linux.py 20 | - Added `BITS_PER_PIXELS_32` 21 | - Added `SUPPORTED_BITS_PER_PIXELS` 22 | 23 | ## 9.0.0 (2023-04-18) 24 | 25 | ### linux.py 26 | - Removed `XEvent` class. Use `XErrorEvent` instead. 27 | 28 | ### windows.py 29 | - Added `MSS.close()` method 30 | - Removed `MSS.bmp` attribute 31 | - Removed `MSS.memdc` attribute 32 | 33 | ## 8.0.3 (2023-04-15) 34 | 35 | ### linux.py 36 | - Added `XErrorEvent` class (old `Event` class is just an alias now, and will be removed in v9.0.0) 37 | 38 | ## 8.0.0 (2023-04-09) 39 | 40 | ### base.py 41 | - Added `compression_level=6` keyword argument to `MSS.__init__()` 42 | - Added `display=None` keyword argument to `MSS.__init__()` 43 | - Added `max_displays=32` keyword argument to `MSS.__init__()` 44 | - Added `with_cursor=False` keyword argument to `MSS.__init__()` 45 | - Added `MSS.with_cursor` attribute 46 | 47 | ### linux.py 48 | - Added `MSS.close()` 49 | - Moved `MSS.__init__()` keyword arguments handling to the base class 50 | - Renamed `error_handler()` function to `_error_handler()` 51 | - Renamed `validate()` function to `__validate()` 52 | - Renamed `MSS.has_extension()` method to `_is_extension_enabled()` 53 | - Removed `ERROR` namespace 54 | - Removed `MSS.drawable` attribute 55 | - Removed `MSS.root` attribute 56 | - Removed `MSS.get_error_details()` method. Use `ScreenShotError.details` attribute instead. 57 | 58 | ## 6.1.0 (2020-10-31) 59 | 60 | ### darwin.py 61 | - Added `CFUNCTIONS` 62 | 63 | ### linux.py 64 | - Added `CFUNCTIONS` 65 | 66 | ### windows.py 67 | - Added `CFUNCTIONS` 68 | - Added `MONITORNUMPROC` 69 | - Removed `MSS.monitorenumproc`. Use `MONITORNUMPROC` instead. 70 | 71 | ## 6.0.0 (2020-06-30) 72 | 73 | ### base.py 74 | - Added `lock` 75 | - Added `MSS._grab_impl()` (abstract method) 76 | - Added `MSS._monitors_impl()` (abstract method) 77 | - `MSS.grab()` is no more an abstract method 78 | - `MSS.monitors` is no more an abstract property 79 | 80 | ### darwin.py 81 | - Renamed `MSS.grab()` to `MSS._grab_impl()` 82 | - Renamed `MSS.monitors` to `MSS._monitors_impl()` 83 | 84 | ### linux.py 85 | - Added `MSS.has_extension()` 86 | - Removed `MSS.display` 87 | - Renamed `MSS.grab()` to `MSS._grab_impl()` 88 | - Renamed `MSS.monitors` to `MSS._monitors_impl()` 89 | 90 | ### windows.py 91 | - Removed `MSS._lock` 92 | - Renamed `MSS.srcdc_dict` to `MSS._srcdc_dict` 93 | - Renamed `MSS.grab()` to `MSS._grab_impl()` 94 | - Renamed `MSS.monitors` to `MSS._monitors_impl()` 95 | 96 | ## 5.1.0 (2020-04-30) 97 | 98 | ### base.py 99 | - Renamed back `MSSMixin` class to `MSSBase` 100 | - `MSSBase` is now derived from `abc.ABCMeta` 101 | - `MSSBase.monitor` is now an abstract property 102 | - `MSSBase.grab()` is now an abstract method 103 | 104 | ### windows.py 105 | - Replaced `MSS.srcdc` with `MSS.srcdc_dict` 106 | 107 | ## 5.0.0 (2019-12-31) 108 | 109 | ### darwin.py 110 | - Added `MSS.__slots__` 111 | 112 | ### linux.py 113 | - Added `MSS.__slots__` 114 | - Deleted `MSS.close()` 115 | - Deleted `LAST_ERROR` constant. Use `ERROR` namespace instead, specially the `ERROR.details` attribute. 116 | 117 | ### models.py 118 | - Added `Monitor` 119 | - Added `Monitors` 120 | - Added `Pixel` 121 | - Added `Pixels` 122 | - Added `Pos` 123 | - Added `Size` 124 | 125 | ### screenshot.py 126 | - Added `ScreenShot.__slots__` 127 | - Removed `Pos`. Use `models.Pos` instead. 128 | - Removed `Size`. Use `models.Size` instead. 129 | 130 | ### windows.py 131 | - Added `MSS.__slots__` 132 | - Deleted `MSS.close()` 133 | 134 | ## 4.0.1 (2019-01-26) 135 | 136 | ### linux.py 137 | - Removed use of `MSS.xlib.XDefaultScreen()` 138 | 4.0.0 (2019-01-11) 139 | 140 | ### base.py 141 | - Renamed `MSSBase` class to `MSSMixin` 142 | 143 | ### linux.py 144 | - Renamed `MSS.__del__()` method to `MSS.close()` 145 | - Deleted `MSS.last_error` attribute. Use `LAST_ERROR` constant instead. 146 | - Added `validate()` function 147 | - Added `MSS.get_error_details()` method 148 | 149 | ### windows.py 150 | - Renamed `MSS.__exit__()` method to `MSS.close()` 151 | 152 | ## 3.3.0 (2018-09-04) 153 | 154 | ### exception.py 155 | - Added `details` attribute to `ScreenShotError` exception. Empty dict by default. 156 | 157 | ### linux.py 158 | - Added `error_handler()` function 159 | 160 | ## 3.2.1 (2018-05-21) 161 | 162 | ### windows.py 163 | - Removed `MSS.scale_factor` property 164 | - Removed `MSS.scale()` method 165 | 166 | ## 3.2.0 (2018-03-22) 167 | 168 | ### base.py 169 | - Added `MSSBase.compression_level` attribute 170 | 171 | ### linux.py 172 | - Added `MSS.drawable` attribute 173 | 174 | ### screenshot.py 175 | - Added `Screenshot.bgra` attribute 176 | 177 | ### tools.py 178 | - Changed signature of `to_png(data, size, output=None)` to `to_png(data, size, level=6, output=None)`. `level` is the Zlib compression level. 179 | 180 | ## 3.1.2 (2018-01-05) 181 | 182 | ### tools.py 183 | - 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. 184 | 185 | ## 3.1.1 (2017-11-27) 186 | 187 | ### \_\_main\_\_.py 188 | - Added `args` argument to `main()` 189 | 190 | ### base.py 191 | - Moved `ScreenShot` class to `screenshot.py` 192 | 193 | ### darwin.py 194 | - Added `CGPoint.__repr__()` function 195 | - Added `CGRect.__repr__()` function 196 | - Added `CGSize.__repr__()` function 197 | - Removed `get_infinity()` function 198 | 199 | ### windows.py 200 | - Added `MSS.scale()` method 201 | - Added `MSS.scale_factor` property 202 | 203 | ## 3.0.0 (2017-07-06) 204 | 205 | ### base.py 206 | - Added the `ScreenShot` class containing data for a given screenshot (support the Numpy array interface [`ScreenShot.__array_interface__`]) 207 | - Added `shot()` method to `MSSBase`. It takes the same arguments as the `save()` method. 208 | - Renamed `get_pixels` to `grab`. It now returns a `ScreenShot` object. 209 | - Moved `to_png` method to `tools.py`. It is now a simple function. 210 | - Removed `enum_display_monitors()` method. Use `monitors` property instead. 211 | - Removed `monitors` attribute. Use `monitors` property instead. 212 | - Removed `width` attribute. Use `ScreenShot.size[0]` attribute or `ScreenShot.width` property instead. 213 | - Removed `height` attribute. Use `ScreenShot.size[1]` attribute or `ScreenShot.height` property instead. 214 | - Removed `image`. Use the `ScreenShot.raw` attribute or `ScreenShot.rgb` property instead. 215 | - Removed `bgra_to_rgb()` method. Use `ScreenShot.rgb` property instead. 216 | 217 | ### darwin.py 218 | - Removed `_crop_width()` method. Screenshots are now using the width set by the OS (rounded to 16). 219 | 220 | ### exception.py 221 | - Renamed `ScreenshotError` class to `ScreenShotError` 222 | 223 | ### tools.py 224 | - Changed signature of `to_png(data, monitor, output)` to `to_png(data, size, output)` where `size` is a `tuple(width, height)` 225 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/d7813b5d9794a73aaf01632955e9d98d49112d4e/docs/icon.png -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | MSS API 3 | ======= 4 | 5 | Classes 6 | ======= 7 | 8 | macOS 9 | ----- 10 | 11 | .. module:: mss.darwin 12 | 13 | .. attribute:: CFUNCTIONS 14 | 15 | .. versionadded:: 6.1.0 16 | 17 | .. function:: cgfloat 18 | 19 | .. class:: CGPoint 20 | 21 | .. class:: CGSize 22 | 23 | .. class:: CGRect 24 | 25 | .. class:: MSS 26 | 27 | .. attribute:: core 28 | 29 | .. attribute:: max_displays 30 | 31 | GNU/Linux 32 | --------- 33 | 34 | .. module:: mss.linux 35 | 36 | .. attribute:: CFUNCTIONS 37 | 38 | .. versionadded:: 6.1.0 39 | 40 | .. attribute:: PLAINMASK 41 | 42 | .. attribute:: ZPIXMAP 43 | 44 | .. class:: Display 45 | 46 | Structure that serves as the connection to the X server, and that contains all the information about that X server. 47 | 48 | .. class:: XErrorEvent 49 | 50 | XErrorEvent to debug eventual errors. 51 | 52 | .. class:: XFixesCursorImage 53 | 54 | Cursor structure 55 | 56 | .. class:: XImage 57 | 58 | Description of an image as it exists in the client's memory. 59 | 60 | .. class:: XRRCrtcInfo 61 | 62 | Structure that contains CRTC information. 63 | 64 | .. class:: XRRModeInfo 65 | 66 | .. class:: XRRScreenResources 67 | 68 | Structure that contains arrays of XIDs that point to the available outputs and associated CRTCs. 69 | 70 | .. class:: XWindowAttributes 71 | 72 | Attributes for the specified window. 73 | 74 | .. class:: MSS 75 | 76 | .. method:: close() 77 | 78 | Clean-up method. 79 | 80 | .. versionadded:: 8.0.0 81 | 82 | Windows 83 | ------- 84 | 85 | .. module:: mss.windows 86 | 87 | .. attribute:: CAPTUREBLT 88 | 89 | .. attribute:: CFUNCTIONS 90 | 91 | .. versionadded:: 6.1.0 92 | 93 | .. attribute:: DIB_RGB_COLORS 94 | 95 | .. attribute:: SRCCOPY 96 | 97 | .. class:: BITMAPINFOHEADER 98 | 99 | .. class:: BITMAPINFO 100 | 101 | .. attribute:: MONITORNUMPROC 102 | 103 | .. versionadded:: 6.1.0 104 | 105 | .. class:: MSS 106 | 107 | .. attribute:: gdi32 108 | 109 | .. attribute:: user32 110 | 111 | Methods 112 | ======= 113 | 114 | .. module:: mss.base 115 | 116 | .. attribute:: lock 117 | 118 | .. versionadded:: 6.0.0 119 | 120 | .. class:: MSSBase 121 | 122 | The parent's class for every OS implementation. 123 | 124 | .. attribute:: cls_image 125 | 126 | .. attribute:: compression_level 127 | 128 | PNG compression level used when saving the screenshot data into a file (see :py:func:`zlib.compress()` for details). 129 | 130 | .. versionadded:: 3.2.0 131 | 132 | .. attribute:: with_cursor 133 | 134 | Include the mouse cursor in screenshots. 135 | 136 | .. versionadded:: 8.0.0 137 | 138 | .. method:: __init__(compression_level=6, display=None, max_displays=32, with_cursor=False) 139 | 140 | :type compression_level: int 141 | :param compression_level: PNG compression level. 142 | :type display: bytes, str or None 143 | :param display: The display to use. Only effective on GNU/Linux. 144 | :type max_displays: int 145 | :param max_displays: Maximum number of displays. Only effective on macOS. 146 | :type with_cursor: bool 147 | :param with_cursor: Include the mouse cursor in screenshots. 148 | 149 | .. versionadded:: 8.0.0 150 | ``compression_level``, ``display``, ``max_displays``, and ``with_cursor``, keyword arguments. 151 | 152 | .. method:: close() 153 | 154 | Clean-up method. 155 | 156 | .. versionadded:: 4.0.0 157 | 158 | .. method:: grab(region) 159 | 160 | :param dict monitor: region's coordinates. 161 | :rtype: :class:`ScreenShot` 162 | 163 | Retrieve screen pixels for a given *region*. 164 | Subclasses need to implement this. 165 | 166 | .. note:: 167 | 168 | *monitor* can be a ``tuple`` like ``PIL.Image.grab()`` accepts, 169 | it will be converted to the appropriate ``dict``. 170 | 171 | .. method:: save([mon=1], [output='mon-{mon}.png'], [callback=None]) 172 | 173 | :param int mon: the monitor's number. 174 | :param str output: the output's file name. 175 | :type callback: callable or None 176 | :param callback: callback called before saving the screenshot to a file. Takes the *output* argument as parameter. 177 | :rtype: iterable 178 | :return: Created file(s). 179 | 180 | Grab a screenshot and save it to a file. 181 | The *output* parameter can take several keywords to customize the filename: 182 | 183 | - ``{mon}``: the monitor number 184 | - ``{top}``: the screenshot y-coordinate of the upper-left corner 185 | - ``{left}``: the screenshot x-coordinate of the upper-left corner 186 | - ``{width}``: the screenshot's width 187 | - ``{height}``: the screenshot's height 188 | - ``{date}``: the current date using the default formatter 189 | 190 | As it is using the :py:func:`format()` function, you can specify formatting options like ``{date:%Y-%m-%s}``. 191 | 192 | .. warning:: On Windows, the default date format may result with a filename containing ':' which is not allowed:: 193 | 194 | IOerror: [Errno 22] invalid mode ('wb') or filename: 'sct_1-2019-01-01 21:20:43.114194.png' 195 | 196 | To fix this, you must provide a custom date formatting. 197 | 198 | .. method:: shot() 199 | 200 | :return str: The created file. 201 | 202 | Helper to save the screenshot of the first monitor, by default. 203 | You can pass the same arguments as for :meth:`save()`. 204 | 205 | .. versionadded:: 3.0.0 206 | 207 | .. class:: ScreenShot 208 | 209 | Screenshot object. 210 | 211 | .. note:: 212 | 213 | A better name would have been *Image*, but to prevent collisions 214 | with ``PIL.Image``, it has been decided to use *ScreenShot*. 215 | 216 | .. classmethod:: from_size(cls, data, width, height) 217 | 218 | :param bytearray data: raw BGRA pixels retrieved by ctypes 219 | OS independent implementations. 220 | :param int width: the monitor's width. 221 | :param int height: the monitor's height. 222 | :rtype: :class:`ScreenShot` 223 | 224 | Instantiate a new class given only screenshot's data and size. 225 | 226 | .. method:: pixel(coord_x, coord_y) 227 | 228 | :param int coord_x: The x coordinate. 229 | :param int coord_y: The y coordinate. 230 | :rtype: tuple(int, int, int) 231 | 232 | Get the pixel value at the given position. 233 | 234 | .. versionadded:: 3.0.0 235 | 236 | .. module:: mss.tools 237 | 238 | .. method:: to_png(data, size, level=6, output=None) 239 | 240 | :param bytes data: RGBRGB...RGB data. 241 | :param tuple size: The (width, height) pair. 242 | :param int level: PNG compression level. 243 | :param str output: output's file name. 244 | :raises ScreenShotError: On error when writing *data* to *output*. 245 | :raises zlib.error: On bad compression *level*. 246 | 247 | Dump data to the image file. Pure Python PNG implementation. 248 | If *output* is ``None``, create no file but return the whole PNG data. 249 | 250 | .. versionadded:: 3.0.0 251 | 252 | .. versionchanged:: 3.2.0 253 | 254 | The *level* keyword argument to control the PNG compression level. 255 | 256 | 257 | Properties 258 | ========== 259 | 260 | .. class:: mss.base.MSSBase 261 | 262 | .. attribute:: monitors 263 | 264 | Positions of all monitors. 265 | If the monitor has rotation, you have to deal with it 266 | inside this method. 267 | 268 | This method has to fill ``self._monitors`` with all information 269 | and use it as a cache: 270 | 271 | - ``self._monitors[0]`` is a dict of all monitors together 272 | - ``self._monitors[N]`` is a dict of the monitor N (with N > 0) 273 | 274 | Each monitor is a dict with: 275 | 276 | - ``left``: the x-coordinate of the upper-left corner 277 | - ``top``: the y-coordinate of the upper-left corner 278 | - ``width``: the width 279 | - ``height``: the height 280 | 281 | Subclasses need to implement this. 282 | 283 | :rtype: list[dict[str, int]] 284 | 285 | .. class:: mss.base.ScreenShot 286 | 287 | .. attribute:: __array_interface__() 288 | 289 | Numpy array interface support. It uses raw data in BGRA form. 290 | 291 | :rtype: dict[str, Any] 292 | 293 | .. attribute:: bgra 294 | 295 | BGRA values from the BGRA raw pixels. 296 | 297 | :rtype: bytes 298 | 299 | .. versionadded:: 3.2.0 300 | 301 | .. attribute:: height 302 | 303 | The screenshot's height. 304 | 305 | :rtype: int 306 | 307 | .. attribute:: left 308 | 309 | The screenshot's left coordinate. 310 | 311 | :rtype: int 312 | 313 | .. attribute:: pixels 314 | 315 | List of row tuples that contain RGB tuples. 316 | 317 | :rtype: list[tuple(tuple(int, int, int), ...)] 318 | 319 | .. attribute:: pos 320 | 321 | The screenshot's coordinates. 322 | 323 | :rtype: :py:func:`collections.namedtuple()` 324 | 325 | .. attribute:: rgb 326 | 327 | Computed RGB values from the BGRA raw pixels. 328 | 329 | :rtype: bytes 330 | 331 | .. versionadded:: 3.0.0 332 | 333 | .. attribute:: size 334 | 335 | The screenshot's size. 336 | 337 | :rtype: :py:func:`collections.namedtuple()` 338 | 339 | .. attribute:: top 340 | 341 | The screenshot's top coordinate. 342 | 343 | :rtype: int 344 | 345 | .. attribute:: width 346 | 347 | The screenshot's width. 348 | 349 | :rtype: int 350 | 351 | 352 | Exception 353 | ========= 354 | 355 | .. module:: mss.exception 356 | 357 | .. exception:: ScreenShotError 358 | 359 | Base class for MSS exceptions. 360 | 361 | .. attribute:: details 362 | 363 | On GNU/Linux, and if the error comes from the XServer, it contains XError details. 364 | This is an empty dict by default. 365 | 366 | For XErrors, you can find information on `Using the Default Error Handlers `_. 367 | 368 | :rtype: dict[str, Any] 369 | 370 | .. versionadded:: 3.3.0 371 | 372 | 373 | Factory 374 | ======= 375 | 376 | .. module:: mss.factory 377 | 378 | .. function:: mss() 379 | 380 | Factory function to instance the appropriate MSS class. 381 | -------------------------------------------------------------------------------- /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 mss 10 | 11 | # -- General configuration ------------------------------------------------ 12 | 13 | extensions = [ 14 | "sphinx_copybutton", 15 | "sphinx.ext.intersphinx", 16 | "sphinx_new_tab_link", 17 | ] 18 | templates_path = ["_templates"] 19 | source_suffix = {".rst": "restructuredtext"} 20 | master_doc = "index" 21 | new_tab_link_show_external_link_icon = True 22 | 23 | # General information about the project. 24 | project = "Python MSS" 25 | copyright = f"{mss.__date__}, {mss.__author__} & contributors" # noqa:A001 26 | author = mss.__author__ 27 | version = mss.__version__ 28 | 29 | release = "latest" 30 | language = "en" 31 | todo_include_todos = True 32 | 33 | 34 | # -- Options for HTML output ---------------------------------------------- 35 | 36 | html_theme = "shibuya" 37 | html_theme_options = { 38 | "accent_color": "lime", 39 | "globaltoc_expand_depth": 1, 40 | "toctree_titles_only": False, 41 | } 42 | html_favicon = "../icon.png" 43 | html_context = { 44 | "source_type": "github", 45 | "source_user": "BoboTiG", 46 | "source_repo": "python-mss", 47 | "source_docs_path": "/docs/source/", 48 | "source_version": "main", 49 | } 50 | htmlhelp_basename = "PythonMSSdoc" 51 | 52 | 53 | # -- Options for Epub output ---------------------------------------------- 54 | 55 | # Bibliographic Dublin Core info. 56 | epub_title = project 57 | epub_author = author 58 | epub_publisher = author 59 | epub_copyright = copyright 60 | 61 | # A list of files that should not be packed into the epub file. 62 | epub_exclude_files = ["search.html"] 63 | 64 | 65 | # ---------------------------------------------- 66 | 67 | # Example configuration for intersphinx: refer to the Python standard library. 68 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 69 | -------------------------------------------------------------------------------- /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 suit:: 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 | -------------------------------------------------------------------------------- /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 | PIL 107 | === 108 | 109 | You can use the Python Image Library (aka Pillow) to do whatever you want with raw pixels. 110 | This is an example using `frombytes() `_: 111 | 112 | .. literalinclude:: examples/pil.py 113 | :lines: 7- 114 | 115 | .. versionadded:: 3.0.0 116 | 117 | Playing with pixels 118 | ------------------- 119 | 120 | This is an example using `putdata() `_: 121 | 122 | .. literalinclude:: examples/pil_pixels.py 123 | :lines: 7- 124 | 125 | .. versionadded:: 3.0.0 126 | 127 | OpenCV/Numpy 128 | ============ 129 | 130 | See how fast you can record the screen. 131 | You can easily view a HD movie with VLC and see it too in the OpenCV window. 132 | And with __no__ lag please. 133 | 134 | .. literalinclude:: examples/opencv_numpy.py 135 | :lines: 7- 136 | 137 | .. versionadded:: 3.0.0 138 | 139 | FPS 140 | === 141 | 142 | Benchmark 143 | --------- 144 | 145 | Simple naive benchmark to compare with `Reading game frames in Python with OpenCV - Python Plays GTA V `_: 146 | 147 | .. literalinclude:: examples/fps.py 148 | :lines: 8- 149 | 150 | .. versionadded:: 3.0.0 151 | 152 | Multiprocessing 153 | --------------- 154 | 155 | Performances can be improved by delegating the PNG file creation to a specific worker. 156 | This is a simple example using the :py:mod:`multiprocessing` inspired by the `TensorFlow Object Detection Introduction `_ project: 157 | 158 | .. literalinclude:: examples/fps_multiprocessing.py 159 | :lines: 8- 160 | 161 | .. versionadded:: 5.0.0 162 | 163 | 164 | BGRA to RGB 165 | =========== 166 | 167 | Different possibilities to convert raw BGRA values to RGB:: 168 | 169 | def mss_rgb(im): 170 | """ Better than Numpy versions, but slower than Pillow. """ 171 | return im.rgb 172 | 173 | 174 | def numpy_flip(im): 175 | """ Most efficient Numpy version as of now. """ 176 | frame = numpy.array(im, dtype=numpy.uint8) 177 | return numpy.flip(frame[:, :, :3], 2).tobytes() 178 | 179 | 180 | def numpy_slice(im): 181 | """ Slow Numpy version. """ 182 | return numpy.array(im, dtype=numpy.uint8)[..., [2, 1, 0]].tobytes() 183 | 184 | 185 | def pil_frombytes(im): 186 | """ Efficient Pillow version. """ 187 | return Image.frombytes('RGB', im.size, im.bgra, 'raw', 'BGRX').tobytes() 188 | 189 | 190 | with mss.mss() as sct: 191 | im = sct.grab(sct.monitors[1]) 192 | rgb = pil_frombytes(im) 193 | ... 194 | 195 | .. versionadded:: 3.2.0 196 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Import 6 | ====== 7 | 8 | So 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 | 24 | Instance 25 | ======== 26 | 27 | So the module can be used as simply as:: 28 | 29 | with mss() as sct: 30 | # ... 31 | 32 | Intensive Use 33 | ============= 34 | 35 | If you plan to integrate MSS inside your own module or software, pay attention to using it wisely. 36 | 37 | This is a bad usage:: 38 | 39 | for _ in range(100): 40 | with mss() as sct: 41 | sct.shot() 42 | 43 | This is a much better usage, memory efficient:: 44 | 45 | with mss() as sct: 46 | for _ in range(100): 47 | sct.shot() 48 | 49 | Also, it is a good thing to save the MSS instance inside an attribute of your class and calling it when needed. 50 | 51 | 52 | GNU/Linux 53 | --------- 54 | 55 | On GNU/Linux, you can specify which display to use (useful for distant screenshots via SSH):: 56 | 57 | with mss(display=":0.0") as sct: 58 | # ... 59 | 60 | A more specific example (only valid on GNU/Linux): 61 | 62 | .. literalinclude:: examples/linux_display_keyword.py 63 | :lines: 9- 64 | 65 | 66 | Command Line 67 | ============ 68 | 69 | You can use ``mss`` via the CLI:: 70 | 71 | mss --help 72 | 73 | Or via direct call from Python:: 74 | 75 | $ python -m mss --help 76 | usage: __main__.py [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}] 77 | [-m MONITOR] [-o OUTPUT] [-q] [-v] [--with-cursor] 78 | 79 | options: 80 | -h, --help show this help message and exit 81 | -c COORDINATES, --coordinates COORDINATES 82 | the part of the screen to capture: top, left, width, height 83 | -l {0,1,2,3,4,5,6,7,8,9}, --level {0,1,2,3,4,5,6,7,8,9} 84 | the PNG compression level 85 | -m MONITOR, --monitor MONITOR 86 | the monitor to screenshot 87 | -o OUTPUT, --output OUTPUT 88 | the output file name 89 | --with-cursor include the cursor 90 | -q, --quiet do not print created files 91 | -v, --version show program's version number and exit 92 | 93 | .. versionadded:: 3.1.1 94 | 95 | .. versionadded:: 8.0.0 96 | ``--with-cursor`` to include the cursor in screenshots. 97 | -------------------------------------------------------------------------------- /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 | - `Airtest `_, a cross-platform UI automation framework for aames and apps; 9 | - `Automation Framework `_, a Batmans utility; 10 | - `DeepEye `_, a deep vision-based software library for autonomous and advanced driver-assistance systems; 11 | - `DoomPy `_ (Autonomous Anti-Demonic Combat Algorithms); 12 | - `Europilot `_, a self-driving algorithm using Euro Truck Simulator (ETS2); 13 | - `Flexx Python UI toolkit `_; 14 | - `Go Review Partner `_, a tool to help analyse and review your game of go (weiqi, baduk) using strong bots; 15 | - `Gradient Sampler `_, sample blender gradients from anything on the screen; 16 | - `gym-mupen64plus `_, an OpenAI Gym environment wrapper for the Mupen64Plus N64 emulator; 17 | - `NativeShot `_ (Mozilla Firefox module); 18 | - `NCTU Scratch and Python, 2017 Spring `_ (Python course); 19 | - `normcap `_, OCR powered screen-capture tool to capture information instead of images; 20 | - `Open Source Self Driving Car Initiative `_; 21 | - `OSRS Bot COLOR (OSBC) `_, a lightweight desktop client for controlling and monitoring color-based automation scripts (bots) for OSRS and private server alternatives; 22 | - `Philips Hue Lights Ambiance `_; 23 | - `Pombo `_, a thief recovery software; 24 | - `Python-ImageSearch `_, a wrapper around OpenCV2 and PyAutoGUI to do image searching easily; 25 | - `PUBGIS `_, a map generator of your position throughout PUBG gameplay; 26 | - `ScreenCapLibrary `_, a Robot Framework test library for capturing screenshots and video recording; 27 | - `ScreenVivid `_, an open source cross-platform screen recorder for everyone ; 28 | - `Self-Driving-Car-3D-Simulator-With-CNN `_; 29 | - `Serpent.AI `_, a Game Agent Framework; 30 | - `Star Wars - The Old Republic: Galactic StarFighter `_ parser; 31 | - `Stitch `_, a Python Remote Administration Tool (RAT); 32 | - `TensorKart `_, a self-driving MarioKart with TensorFlow; 33 | - `videostream_censor `_, a real time video recording censor ; 34 | - `wow-fishing-bot `_, a fishing bot for World of Warcraft that uses template matching from OpenCV; 35 | - `Zelda Bowling AI `_; 36 | -------------------------------------------------------------------------------- /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.2.2.post1", 75 | "mypy==1.15.0", 76 | "ruff==0.11.11", 77 | "twine==6.1.0", 78 | ] 79 | docs = [ 80 | "shibuya==2025.5.30", 81 | "sphinx==8.2.3", 82 | "sphinx-copybutton==0.5.2", 83 | "sphinx-new-tab-link==0.8.0", 84 | ] 85 | tests = [ 86 | "numpy==2.2.4 ; sys_platform == 'linux' and python_version == '3.13'", 87 | "pillow==11.2.1 ; sys_platform == 'linux' and python_version == '3.13'", 88 | "pytest==8.3.5", 89 | "pytest-cov==6.1.1", 90 | "pytest-rerunfailures==15.1", 91 | "pyvirtualdisplay==3.0 ; sys_platform == 'linux'", 92 | ] 93 | 94 | [tool.hatch.version] 95 | path = "src/mss/__init__.py" 96 | 97 | [tool.hatch.build] 98 | skip-excluded-dirs = true 99 | 100 | [tool.hatch.build.targets.sdist] 101 | only-include = [ 102 | "CHANGELOG.md", 103 | "CHANGES.md", 104 | "CONTRIBUTORS.md", 105 | "docs/source", 106 | "src", 107 | ] 108 | 109 | [tool.hatch.build.targets.wheel] 110 | packages = [ 111 | "src/mss", 112 | ] 113 | 114 | [tool.mypy] 115 | # Ensure we know what we do 116 | warn_redundant_casts = true 117 | warn_unused_ignores = true 118 | warn_unused_configs = true 119 | 120 | # Imports management 121 | ignore_missing_imports = true 122 | follow_imports = "skip" 123 | 124 | # Ensure full coverage 125 | disallow_untyped_defs = true 126 | disallow_incomplete_defs = true 127 | disallow_untyped_calls = true 128 | 129 | # Restrict dynamic typing (a little) 130 | # e.g. `x: List[Any]` or x: List` 131 | # disallow_any_generics = true 132 | 133 | strict_equality = true 134 | 135 | [tool.pytest.ini_options] 136 | pythonpath = "src" 137 | addopts = """ 138 | --showlocals 139 | --strict-markers 140 | -r fE 141 | -vvv 142 | --cov=src/mss 143 | --cov-report=term-missing:skip-covered 144 | """ 145 | 146 | [tool.ruff] 147 | exclude = [ 148 | ".git", 149 | ".mypy_cache", 150 | ".pytest_cache", 151 | ".ruff_cache", 152 | "venv", 153 | ] 154 | line-length = 120 155 | indent-width = 4 156 | target-version = "py39" 157 | 158 | [tool.ruff.format] 159 | quote-style = "double" 160 | indent-style = "space" 161 | skip-magic-trailing-comma = false 162 | line-ending = "auto" 163 | 164 | [tool.ruff.lint] 165 | fixable = ["ALL"] 166 | extend-select = ["ALL"] 167 | ignore = [ 168 | "ANN401", # typing.Any 169 | "C90", # complexity 170 | "COM812", # conflict 171 | "D", # TODO 172 | "ISC001", # conflict 173 | "T201", # `print()` 174 | ] 175 | 176 | [tool.ruff.lint.per-file-ignores] 177 | "docs/source/*" = [ 178 | "ERA001", # commented code 179 | "INP001", # file `xxx` is part of an implicit namespace package 180 | ] 181 | "src/tests/*" = [ 182 | "FBT001", # boolean-typed positional argument in function definition 183 | "PLR2004", # magic value used in comparison 184 | "S101", # use of `assert` detected 185 | "S602", # `subprocess` call with `shell=True` 186 | "S603", # `subprocess` call 187 | "SLF001", # private member accessed 188 | ] 189 | -------------------------------------------------------------------------------- /src/mss/__init__.py: -------------------------------------------------------------------------------- 1 | """An ultra fast cross-platform multiple screenshots module in pure python 2 | using ctypes. 3 | 4 | This module is maintained by Mickaël Schoentgen . 5 | 6 | You can always get the latest version of this module at: 7 | https://github.com/BoboTiG/python-mss 8 | If that URL should fail, try contacting the author. 9 | """ 10 | 11 | from mss.exception import ScreenShotError 12 | from mss.factory import mss 13 | 14 | __version__ = "10.1.0.dev0" 15 | __author__ = "Mickaël Schoentgen" 16 | __date__ = "2013-2025" 17 | __copyright__ = f""" 18 | Copyright (c) {__date__}, {__author__} 19 | 20 | Permission to use, copy, modify, and distribute this software and its 21 | documentation for any purpose and without fee or royalty is hereby 22 | granted, provided that the above copyright notice appear in all copies 23 | and that both that copyright notice and this permission notice appear 24 | in supporting documentation or portions thereof, including 25 | modifications, that you make. 26 | """ 27 | __all__ = ("ScreenShotError", "mss") 28 | -------------------------------------------------------------------------------- /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 sys 7 | from argparse import ArgumentParser 8 | 9 | from mss import __version__ 10 | from mss.exception import ScreenShotError 11 | from mss.factory import mss 12 | from mss.tools import to_png 13 | 14 | 15 | def main(*args: str) -> int: 16 | """Main logic.""" 17 | cli_args = ArgumentParser(prog="mss") 18 | cli_args.add_argument( 19 | "-c", 20 | "--coordinates", 21 | default="", 22 | type=str, 23 | help="the part of the screen to capture: top, left, width, height", 24 | ) 25 | cli_args.add_argument( 26 | "-l", 27 | "--level", 28 | default=6, 29 | type=int, 30 | choices=list(range(10)), 31 | help="the PNG compression level", 32 | ) 33 | cli_args.add_argument("-m", "--monitor", default=0, type=int, help="the monitor to screenshot") 34 | cli_args.add_argument("-o", "--output", default="monitor-{mon}.png", help="the output file name") 35 | cli_args.add_argument("--with-cursor", default=False, action="store_true", help="include the cursor") 36 | cli_args.add_argument( 37 | "-q", 38 | "--quiet", 39 | default=False, 40 | action="store_true", 41 | help="do not print created files", 42 | ) 43 | cli_args.add_argument("-v", "--version", action="version", version=__version__) 44 | 45 | options = cli_args.parse_args(args or None) 46 | kwargs = {"mon": options.monitor, "output": options.output} 47 | if options.coordinates: 48 | try: 49 | top, left, width, height = options.coordinates.split(",") 50 | except ValueError: 51 | print("Coordinates syntax: top, left, width, height") 52 | return 2 53 | 54 | kwargs["mon"] = { 55 | "top": int(top), 56 | "left": int(left), 57 | "width": int(width), 58 | "height": int(height), 59 | } 60 | if options.output == "monitor-{mon}.png": 61 | kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png" 62 | 63 | try: 64 | with mss(with_cursor=options.with_cursor) as sct: 65 | if options.coordinates: 66 | output = kwargs["output"].format(**kwargs["mon"]) 67 | sct_img = sct.grab(kwargs["mon"]) 68 | to_png(sct_img.rgb, sct_img.size, level=options.level, output=output) 69 | if not options.quiet: 70 | print(os.path.realpath(output)) 71 | else: 72 | for file_name in sct.save(**kwargs): 73 | if not options.quiet: 74 | print(os.path.realpath(file_name)) 75 | return 0 76 | except ScreenShotError: 77 | if options.quiet: 78 | return 1 79 | raise 80 | 81 | 82 | if __name__ == "__main__": # pragma: nocover 83 | sys.exit(main()) 84 | -------------------------------------------------------------------------------- /src/mss/base.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 | from abc import ABCMeta, abstractmethod 8 | from datetime import datetime 9 | from threading import Lock 10 | from typing import TYPE_CHECKING, Any 11 | 12 | from mss.exception import ScreenShotError 13 | from mss.screenshot import ScreenShot 14 | from mss.tools import to_png 15 | 16 | if TYPE_CHECKING: # pragma: nocover 17 | from collections.abc import Callable, Iterator 18 | 19 | from mss.models import Monitor, Monitors 20 | 21 | try: 22 | from datetime import UTC 23 | except ImportError: # pragma: nocover 24 | # Python < 3.11 25 | from datetime import timezone 26 | 27 | UTC = timezone.utc 28 | 29 | lock = Lock() 30 | 31 | OPAQUE = 255 32 | 33 | 34 | class MSSBase(metaclass=ABCMeta): 35 | """This class will be overloaded by a system specific one.""" 36 | 37 | __slots__ = {"_monitors", "cls_image", "compression_level", "with_cursor"} 38 | 39 | def __init__( 40 | self, 41 | /, 42 | *, 43 | compression_level: int = 6, 44 | with_cursor: bool = False, 45 | # Linux only 46 | display: bytes | str | None = None, # noqa: ARG002 47 | # Mac only 48 | max_displays: int = 32, # noqa: ARG002 49 | ) -> None: 50 | self.cls_image: type[ScreenShot] = ScreenShot 51 | self.compression_level = compression_level 52 | self.with_cursor = with_cursor 53 | self._monitors: Monitors = [] 54 | 55 | def __enter__(self) -> MSSBase: # noqa:PYI034 56 | """For the cool call `with MSS() as mss:`.""" 57 | return self 58 | 59 | def __exit__(self, *_: object) -> None: 60 | """For the cool call `with MSS() as mss:`.""" 61 | self.close() 62 | 63 | @abstractmethod 64 | def _cursor_impl(self) -> ScreenShot | None: 65 | """Retrieve all cursor data. Pixels have to be RGB.""" 66 | 67 | @abstractmethod 68 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 69 | """Retrieve all pixels from a monitor. Pixels have to be RGB. 70 | That method has to be run using a threading lock. 71 | """ 72 | 73 | @abstractmethod 74 | def _monitors_impl(self) -> None: 75 | """Get positions of monitors (has to be run using a threading lock). 76 | It must populate self._monitors. 77 | """ 78 | 79 | def close(self) -> None: # noqa:B027 80 | """Clean-up.""" 81 | 82 | def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot: 83 | """Retrieve screen pixels for a given monitor. 84 | 85 | Note: *monitor* can be a tuple like the one PIL.Image.grab() accepts. 86 | 87 | :param monitor: The coordinates and size of the box to capture. 88 | See :meth:`monitors ` for object details. 89 | :return :class:`ScreenShot `. 90 | """ 91 | # Convert PIL bbox style 92 | if isinstance(monitor, tuple): 93 | monitor = { 94 | "left": monitor[0], 95 | "top": monitor[1], 96 | "width": monitor[2] - monitor[0], 97 | "height": monitor[3] - monitor[1], 98 | } 99 | 100 | with lock: 101 | screenshot = self._grab_impl(monitor) 102 | if self.with_cursor and (cursor := self._cursor_impl()): 103 | return self._merge(screenshot, cursor) 104 | return screenshot 105 | 106 | @property 107 | def monitors(self) -> Monitors: 108 | """Get positions of all monitors. 109 | If the monitor has rotation, you have to deal with it 110 | inside this method. 111 | 112 | This method has to fill self._monitors with all information 113 | and use it as a cache: 114 | self._monitors[0] is a dict of all monitors together 115 | self._monitors[N] is a dict of the monitor N (with N > 0) 116 | 117 | Each monitor is a dict with: 118 | { 119 | 'left': the x-coordinate of the upper-left corner, 120 | 'top': the y-coordinate of the upper-left corner, 121 | 'width': the width, 122 | 'height': the height 123 | } 124 | """ 125 | if not self._monitors: 126 | with lock: 127 | self._monitors_impl() 128 | 129 | return self._monitors 130 | 131 | def save( 132 | self, 133 | /, 134 | *, 135 | mon: int = 0, 136 | output: str = "monitor-{mon}.png", 137 | callback: Callable[[str], None] | None = None, 138 | ) -> Iterator[str]: 139 | """Grab a screenshot and save it to a file. 140 | 141 | :param int mon: The monitor to screenshot (default=0). 142 | -1: grab one screenshot of all monitors 143 | 0: grab one screenshot by monitor 144 | N: grab the screenshot of the monitor N 145 | 146 | :param str output: The output filename. 147 | 148 | It can take several keywords to customize the filename: 149 | - `{mon}`: the monitor number 150 | - `{top}`: the screenshot y-coordinate of the upper-left corner 151 | - `{left}`: the screenshot x-coordinate of the upper-left corner 152 | - `{width}`: the screenshot's width 153 | - `{height}`: the screenshot's height 154 | - `{date}`: the current date using the default formatter 155 | 156 | As it is using the `format()` function, you can specify 157 | formatting options like `{date:%Y-%m-%s}`. 158 | 159 | :param callable callback: Callback called before saving the 160 | screenshot to a file. Take the `output` argument as parameter. 161 | 162 | :return generator: Created file(s). 163 | """ 164 | monitors = self.monitors 165 | if not monitors: 166 | msg = "No monitor found." 167 | raise ScreenShotError(msg) 168 | 169 | if mon == 0: 170 | # One screenshot by monitor 171 | for idx, monitor in enumerate(monitors[1:], 1): 172 | fname = output.format(mon=idx, date=datetime.now(UTC) if "{date" in output else None, **monitor) 173 | if callable(callback): 174 | callback(fname) 175 | sct = self.grab(monitor) 176 | to_png(sct.rgb, sct.size, level=self.compression_level, output=fname) 177 | yield fname 178 | else: 179 | # A screenshot of all monitors together or 180 | # a screenshot of the monitor N. 181 | mon = 0 if mon == -1 else mon 182 | try: 183 | monitor = monitors[mon] 184 | except IndexError as exc: 185 | msg = f"Monitor {mon!r} does not exist." 186 | raise ScreenShotError(msg) from exc 187 | 188 | output = output.format(mon=mon, date=datetime.now(UTC) if "{date" in output else None, **monitor) 189 | if callable(callback): 190 | callback(output) 191 | sct = self.grab(monitor) 192 | to_png(sct.rgb, sct.size, level=self.compression_level, output=output) 193 | yield output 194 | 195 | def shot(self, /, **kwargs: Any) -> str: 196 | """Helper to save the screenshot of the 1st monitor, by default. 197 | You can pass the same arguments as for ``save``. 198 | """ 199 | kwargs["mon"] = kwargs.get("mon", 1) 200 | return next(self.save(**kwargs)) 201 | 202 | @staticmethod 203 | def _merge(screenshot: ScreenShot, cursor: ScreenShot, /) -> ScreenShot: 204 | """Create composite image by blending screenshot and mouse cursor.""" 205 | (cx, cy), (cw, ch) = cursor.pos, cursor.size 206 | (x, y), (w, h) = screenshot.pos, screenshot.size 207 | 208 | cx2, cy2 = cx + cw, cy + ch 209 | x2, y2 = x + w, y + h 210 | 211 | overlap = cx < x2 and cx2 > x and cy < y2 and cy2 > y 212 | if not overlap: 213 | return screenshot 214 | 215 | screen_raw = screenshot.raw 216 | cursor_raw = cursor.raw 217 | 218 | cy, cy2 = (cy - y) * 4, (cy2 - y2) * 4 219 | cx, cx2 = (cx - x) * 4, (cx2 - x2) * 4 220 | start_count_y = -cy if cy < 0 else 0 221 | start_count_x = -cx if cx < 0 else 0 222 | stop_count_y = ch * 4 - max(cy2, 0) 223 | stop_count_x = cw * 4 - max(cx2, 0) 224 | rgb = range(3) 225 | 226 | for count_y in range(start_count_y, stop_count_y, 4): 227 | pos_s = (count_y + cy) * w + cx 228 | pos_c = count_y * cw 229 | 230 | for count_x in range(start_count_x, stop_count_x, 4): 231 | spos = pos_s + count_x 232 | cpos = pos_c + count_x 233 | alpha = cursor_raw[cpos + 3] 234 | 235 | if not alpha: 236 | continue 237 | 238 | if alpha == OPAQUE: 239 | screen_raw[spos : spos + 3] = cursor_raw[cpos : cpos + 3] 240 | else: 241 | alpha2 = alpha / 255 242 | for i in rgb: 243 | screen_raw[spos + i] = int(cursor_raw[cpos + i] * alpha2 + screen_raw[spos + i] * (1 - alpha2)) 244 | 245 | return screenshot 246 | 247 | @staticmethod 248 | def _cfactory( 249 | attr: Any, 250 | func: str, 251 | argtypes: list[Any], 252 | restype: Any, 253 | /, 254 | errcheck: Callable | None = None, 255 | ) -> None: 256 | """Factory to create a ctypes function and automatically manage errors.""" 257 | meth = getattr(attr, func) 258 | meth.argtypes = argtypes 259 | meth.restype = restype 260 | if errcheck: 261 | meth.errcheck = errcheck 262 | -------------------------------------------------------------------------------- /src/mss/darwin.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 8 | import ctypes.util 9 | import sys 10 | from ctypes import POINTER, Structure, c_double, c_float, c_int32, c_ubyte, c_uint32, c_uint64, c_void_p 11 | from platform import mac_ver 12 | from typing import TYPE_CHECKING, Any 13 | 14 | from mss.base import MSSBase 15 | from mss.exception import ScreenShotError 16 | from mss.screenshot import ScreenShot, Size 17 | 18 | if TYPE_CHECKING: # pragma: nocover 19 | from mss.models import CFunctions, Monitor 20 | 21 | __all__ = ("MSS",) 22 | 23 | MAC_VERSION_CATALINA = 10.16 24 | 25 | kCGWindowImageBoundsIgnoreFraming = 1 << 0 # noqa: N816 26 | kCGWindowImageNominalResolution = 1 << 4 # noqa: N816 27 | kCGWindowImageShouldBeOpaque = 1 << 1 # noqa: N816 28 | # Note: set `IMAGE_OPTIONS = 0` to turn on scaling (see issue #257 for more information) 29 | IMAGE_OPTIONS = kCGWindowImageBoundsIgnoreFraming | kCGWindowImageShouldBeOpaque | kCGWindowImageNominalResolution 30 | 31 | 32 | def cgfloat() -> type[c_double | c_float]: 33 | """Get the appropriate value for a float.""" 34 | return c_double if sys.maxsize > 2**32 else c_float 35 | 36 | 37 | class CGPoint(Structure): 38 | """Structure that contains coordinates of a rectangle.""" 39 | 40 | _fields_ = (("x", cgfloat()), ("y", cgfloat())) 41 | 42 | def __repr__(self) -> str: 43 | return f"{type(self).__name__}(left={self.x} top={self.y})" 44 | 45 | 46 | class CGSize(Structure): 47 | """Structure that contains dimensions of an rectangle.""" 48 | 49 | _fields_ = (("width", cgfloat()), ("height", cgfloat())) 50 | 51 | def __repr__(self) -> str: 52 | return f"{type(self).__name__}(width={self.width} height={self.height})" 53 | 54 | 55 | class CGRect(Structure): 56 | """Structure that contains information about a rectangle.""" 57 | 58 | _fields_ = (("origin", CGPoint), ("size", CGSize)) 59 | 60 | def __repr__(self) -> str: 61 | return f"{type(self).__name__}<{self.origin} {self.size}>" 62 | 63 | 64 | # C functions that will be initialised later. 65 | # 66 | # Available attr: core. 67 | # 68 | # Note: keep it sorted by cfunction. 69 | CFUNCTIONS: CFunctions = { 70 | # Syntax: cfunction: (attr, argtypes, restype) 71 | "CGDataProviderCopyData": ("core", [c_void_p], c_void_p), 72 | "CGDisplayBounds": ("core", [c_uint32], CGRect), 73 | "CGDisplayRotation": ("core", [c_uint32], c_float), 74 | "CFDataGetBytePtr": ("core", [c_void_p], c_void_p), 75 | "CFDataGetLength": ("core", [c_void_p], c_uint64), 76 | "CFRelease": ("core", [c_void_p], c_void_p), 77 | "CGDataProviderRelease": ("core", [c_void_p], c_void_p), 78 | "CGGetActiveDisplayList": ("core", [c_uint32, POINTER(c_uint32), POINTER(c_uint32)], c_int32), 79 | "CGImageGetBitsPerPixel": ("core", [c_void_p], int), 80 | "CGImageGetBytesPerRow": ("core", [c_void_p], int), 81 | "CGImageGetDataProvider": ("core", [c_void_p], c_void_p), 82 | "CGImageGetHeight": ("core", [c_void_p], int), 83 | "CGImageGetWidth": ("core", [c_void_p], int), 84 | "CGRectStandardize": ("core", [CGRect], CGRect), 85 | "CGRectUnion": ("core", [CGRect, CGRect], CGRect), 86 | "CGWindowListCreateImage": ("core", [CGRect, c_uint32, c_uint32, c_uint32], c_void_p), 87 | } 88 | 89 | 90 | class MSS(MSSBase): 91 | """Multiple ScreenShots implementation for macOS. 92 | It uses intensively the CoreGraphics library. 93 | """ 94 | 95 | __slots__ = {"core", "max_displays"} 96 | 97 | def __init__(self, /, **kwargs: Any) -> None: 98 | """MacOS initialisations.""" 99 | super().__init__(**kwargs) 100 | 101 | self.max_displays = kwargs.get("max_displays", 32) 102 | 103 | self._init_library() 104 | self._set_cfunctions() 105 | 106 | def _init_library(self) -> None: 107 | """Load the CoreGraphics library.""" 108 | version = float(".".join(mac_ver()[0].split(".")[:2])) 109 | if version < MAC_VERSION_CATALINA: 110 | coregraphics = ctypes.util.find_library("CoreGraphics") 111 | else: 112 | # macOS Big Sur and newer 113 | coregraphics = "/System/Library/Frameworks/CoreGraphics.framework/Versions/Current/CoreGraphics" 114 | 115 | if not coregraphics: 116 | msg = "No CoreGraphics library found." 117 | raise ScreenShotError(msg) 118 | self.core = ctypes.cdll.LoadLibrary(coregraphics) 119 | 120 | def _set_cfunctions(self) -> None: 121 | """Set all ctypes functions and attach them to attributes.""" 122 | cfactory = self._cfactory 123 | attrs = {"core": self.core} 124 | for func, (attr, argtypes, restype) in CFUNCTIONS.items(): 125 | cfactory(attrs[attr], func, argtypes, restype) 126 | 127 | def _monitors_impl(self) -> None: 128 | """Get positions of monitors. It will populate self._monitors.""" 129 | int_ = int 130 | core = self.core 131 | 132 | # All monitors 133 | # We need to update the value with every single monitor found 134 | # using CGRectUnion. Else we will end with infinite values. 135 | all_monitors = CGRect() 136 | self._monitors.append({}) 137 | 138 | # Each monitor 139 | display_count = c_uint32(0) 140 | active_displays = (c_uint32 * self.max_displays)() 141 | core.CGGetActiveDisplayList(self.max_displays, active_displays, ctypes.byref(display_count)) 142 | for idx in range(display_count.value): 143 | display = active_displays[idx] 144 | rect = core.CGDisplayBounds(display) 145 | rect = core.CGRectStandardize(rect) 146 | width, height = rect.size.width, rect.size.height 147 | 148 | # 0.0: normal 149 | # 90.0: right 150 | # -90.0: left 151 | if core.CGDisplayRotation(display) in {90.0, -90.0}: 152 | width, height = height, width 153 | 154 | self._monitors.append( 155 | { 156 | "left": int_(rect.origin.x), 157 | "top": int_(rect.origin.y), 158 | "width": int_(width), 159 | "height": int_(height), 160 | }, 161 | ) 162 | 163 | # Update AiO monitor's values 164 | all_monitors = core.CGRectUnion(all_monitors, rect) 165 | 166 | # Set the AiO monitor's values 167 | self._monitors[0] = { 168 | "left": int_(all_monitors.origin.x), 169 | "top": int_(all_monitors.origin.y), 170 | "width": int_(all_monitors.size.width), 171 | "height": int_(all_monitors.size.height), 172 | } 173 | 174 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 175 | """Retrieve all pixels from a monitor. Pixels have to be RGB.""" 176 | core = self.core 177 | rect = CGRect((monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])) 178 | 179 | image_ref = core.CGWindowListCreateImage(rect, 1, 0, IMAGE_OPTIONS) 180 | if not image_ref: 181 | msg = "CoreGraphics.CGWindowListCreateImage() failed." 182 | raise ScreenShotError(msg) 183 | 184 | width = core.CGImageGetWidth(image_ref) 185 | height = core.CGImageGetHeight(image_ref) 186 | prov = copy_data = None 187 | try: 188 | prov = core.CGImageGetDataProvider(image_ref) 189 | copy_data = core.CGDataProviderCopyData(prov) 190 | data_ref = core.CFDataGetBytePtr(copy_data) 191 | buf_len = core.CFDataGetLength(copy_data) 192 | raw = ctypes.cast(data_ref, POINTER(c_ubyte * buf_len)) 193 | data = bytearray(raw.contents) 194 | 195 | # Remove padding per row 196 | bytes_per_row = core.CGImageGetBytesPerRow(image_ref) 197 | bytes_per_pixel = core.CGImageGetBitsPerPixel(image_ref) 198 | bytes_per_pixel = (bytes_per_pixel + 7) // 8 199 | 200 | if bytes_per_pixel * width != bytes_per_row: 201 | cropped = bytearray() 202 | for row in range(height): 203 | start = row * bytes_per_row 204 | end = start + width * bytes_per_pixel 205 | cropped.extend(data[start:end]) 206 | data = cropped 207 | finally: 208 | if prov: 209 | core.CGDataProviderRelease(prov) 210 | if copy_data: 211 | core.CFRelease(copy_data) 212 | 213 | return self.cls_image(data, monitor, size=Size(width, height)) 214 | 215 | def _cursor_impl(self) -> ScreenShot | None: 216 | """Retrieve all cursor data. Pixels have to be RGB.""" 217 | return None 218 | -------------------------------------------------------------------------------- /src/mss/exception.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 | from typing import Any 8 | 9 | 10 | class ScreenShotError(Exception): 11 | """Error handling class.""" 12 | 13 | def __init__(self, message: str, /, *, details: dict[str, Any] | None = None) -> None: 14 | super().__init__(message) 15 | self.details = details or {} 16 | -------------------------------------------------------------------------------- /src/mss/factory.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 | from typing import Any 7 | 8 | from mss.base import MSSBase 9 | from mss.exception import ScreenShotError 10 | 11 | 12 | def mss(**kwargs: Any) -> MSSBase: 13 | """Factory returning a proper MSS class instance. 14 | 15 | It detects the platform we are running on 16 | and chooses the most adapted mss_class to take 17 | screenshots. 18 | 19 | It then proxies its arguments to the class for 20 | instantiation. 21 | """ 22 | os_ = platform.system().lower() 23 | 24 | if os_ == "darwin": 25 | from mss import darwin 26 | 27 | return darwin.MSS(**kwargs) 28 | 29 | if os_ == "linux": 30 | from mss import linux 31 | 32 | return linux.MSS(**kwargs) 33 | 34 | if os_ == "windows": 35 | from mss import windows 36 | 37 | return windows.MSS(**kwargs) 38 | 39 | msg = f"System {os_!r} not (yet?) implemented." 40 | raise ScreenShotError(msg) 41 | -------------------------------------------------------------------------------- /src/mss/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 os 8 | from contextlib import suppress 9 | from ctypes import ( 10 | CFUNCTYPE, 11 | POINTER, 12 | Structure, 13 | byref, 14 | c_char_p, 15 | c_int, 16 | c_int32, 17 | c_long, 18 | c_short, 19 | c_ubyte, 20 | c_uint, 21 | c_uint32, 22 | c_ulong, 23 | c_ushort, 24 | c_void_p, 25 | cast, 26 | cdll, 27 | create_string_buffer, 28 | ) 29 | from ctypes.util import find_library 30 | from threading import current_thread, local 31 | from typing import TYPE_CHECKING, Any 32 | 33 | from mss.base import MSSBase, lock 34 | from mss.exception import ScreenShotError 35 | 36 | if TYPE_CHECKING: # pragma: nocover 37 | from mss.models import CFunctions, Monitor 38 | from mss.screenshot import ScreenShot 39 | 40 | __all__ = ("MSS",) 41 | 42 | 43 | PLAINMASK = 0x00FFFFFF 44 | ZPIXMAP = 2 45 | BITS_PER_PIXELS_32 = 32 46 | SUPPORTED_BITS_PER_PIXELS = { 47 | BITS_PER_PIXELS_32, 48 | } 49 | 50 | 51 | class Display(Structure): 52 | """Structure that serves as the connection to the X server 53 | and that contains all the information about that X server. 54 | https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831. 55 | """ 56 | 57 | 58 | class XErrorEvent(Structure): 59 | """XErrorEvent to debug eventual errors. 60 | https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html. 61 | """ 62 | 63 | _fields_ = ( 64 | ("type", c_int), 65 | ("display", POINTER(Display)), # Display the event was read from 66 | ("serial", c_ulong), # serial number of failed request 67 | ("error_code", c_ubyte), # error code of failed request 68 | ("request_code", c_ubyte), # major op-code of failed request 69 | ("minor_code", c_ubyte), # minor op-code of failed request 70 | ("resourceid", c_void_p), # resource ID 71 | ) 72 | 73 | 74 | class XFixesCursorImage(Structure): 75 | """Cursor structure. 76 | /usr/include/X11/extensions/Xfixes.h 77 | https://github.com/freedesktop/xorg-libXfixes/blob/libXfixes-6.0.0/include/X11/extensions/Xfixes.h#L96. 78 | """ 79 | 80 | _fields_ = ( 81 | ("x", c_short), 82 | ("y", c_short), 83 | ("width", c_ushort), 84 | ("height", c_ushort), 85 | ("xhot", c_ushort), 86 | ("yhot", c_ushort), 87 | ("cursor_serial", c_ulong), 88 | ("pixels", POINTER(c_ulong)), 89 | ("atom", c_ulong), 90 | ("name", c_char_p), 91 | ) 92 | 93 | 94 | class XImage(Structure): 95 | """Description of an image as it exists in the client's memory. 96 | https://tronche.com/gui/x/xlib/graphics/images.html. 97 | """ 98 | 99 | _fields_ = ( 100 | ("width", c_int), # size of image 101 | ("height", c_int), # size of image 102 | ("xoffset", c_int), # number of pixels offset in X direction 103 | ("format", c_int), # XYBitmap, XYPixmap, ZPixmap 104 | ("data", c_void_p), # pointer to image data 105 | ("byte_order", c_int), # data byte order, LSBFirst, MSBFirst 106 | ("bitmap_unit", c_int), # quant. of scanline 8, 16, 32 107 | ("bitmap_bit_order", c_int), # LSBFirst, MSBFirst 108 | ("bitmap_pad", c_int), # 8, 16, 32 either XY or ZPixmap 109 | ("depth", c_int), # depth of image 110 | ("bytes_per_line", c_int), # accelerator to next line 111 | ("bits_per_pixel", c_int), # bits per pixel (ZPixmap) 112 | ("red_mask", c_ulong), # bits in z arrangement 113 | ("green_mask", c_ulong), # bits in z arrangement 114 | ("blue_mask", c_ulong), # bits in z arrangement 115 | ) 116 | 117 | 118 | class XRRCrtcInfo(Structure): 119 | """Structure that contains CRTC information. 120 | https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L360. 121 | """ 122 | 123 | _fields_ = ( 124 | ("timestamp", c_ulong), 125 | ("x", c_int), 126 | ("y", c_int), 127 | ("width", c_uint), 128 | ("height", c_uint), 129 | ("mode", c_long), 130 | ("rotation", c_int), 131 | ("noutput", c_int), 132 | ("outputs", POINTER(c_long)), 133 | ("rotations", c_ushort), 134 | ("npossible", c_int), 135 | ("possible", POINTER(c_long)), 136 | ) 137 | 138 | 139 | class XRRModeInfo(Structure): 140 | """https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248.""" 141 | 142 | 143 | class XRRScreenResources(Structure): 144 | """Structure that contains arrays of XIDs that point to the 145 | available outputs and associated CRTCs. 146 | https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L265. 147 | """ 148 | 149 | _fields_ = ( 150 | ("timestamp", c_ulong), 151 | ("configTimestamp", c_ulong), 152 | ("ncrtc", c_int), 153 | ("crtcs", POINTER(c_long)), 154 | ("noutput", c_int), 155 | ("outputs", POINTER(c_long)), 156 | ("nmode", c_int), 157 | ("modes", POINTER(XRRModeInfo)), 158 | ) 159 | 160 | 161 | class XWindowAttributes(Structure): 162 | """Attributes for the specified window.""" 163 | 164 | _fields_ = ( 165 | ("x", c_int32), # location of window 166 | ("y", c_int32), # location of window 167 | ("width", c_int32), # width of window 168 | ("height", c_int32), # height of window 169 | ("border_width", c_int32), # border width of window 170 | ("depth", c_int32), # depth of window 171 | ("visual", c_ulong), # the associated visual structure 172 | ("root", c_ulong), # root of screen containing window 173 | ("class", c_int32), # InputOutput, InputOnly 174 | ("bit_gravity", c_int32), # one of bit gravity values 175 | ("win_gravity", c_int32), # one of the window gravity values 176 | ("backing_store", c_int32), # NotUseful, WhenMapped, Always 177 | ("backing_planes", c_ulong), # planes to be preserved if possible 178 | ("backing_pixel", c_ulong), # value to be used when restoring planes 179 | ("save_under", c_int32), # boolean, should bits under be saved? 180 | ("colormap", c_ulong), # color map to be associated with window 181 | ("mapinstalled", c_uint32), # boolean, is color map currently installed 182 | ("map_state", c_uint32), # IsUnmapped, IsUnviewable, IsViewable 183 | ("all_event_masks", c_ulong), # set of events all people have interest in 184 | ("your_event_mask", c_ulong), # my event mask 185 | ("do_not_propagate_mask", c_ulong), # set of events that should not propagate 186 | ("override_redirect", c_int32), # boolean value for override-redirect 187 | ("screen", c_ulong), # back pointer to correct screen 188 | ) 189 | 190 | 191 | _ERROR = {} 192 | _X11 = find_library("X11") 193 | _XFIXES = find_library("Xfixes") 194 | _XRANDR = find_library("Xrandr") 195 | 196 | 197 | @CFUNCTYPE(c_int, POINTER(Display), POINTER(XErrorEvent)) 198 | def _error_handler(display: Display, event: XErrorEvent) -> int: 199 | """Specifies the program's supplied error handler.""" 200 | # Get the specific error message 201 | xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type] 202 | get_error = xlib.XGetErrorText 203 | get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int] 204 | get_error.restype = c_void_p 205 | 206 | evt = event.contents 207 | error = create_string_buffer(1024) 208 | get_error(display, evt.error_code, error, len(error)) 209 | 210 | _ERROR[current_thread()] = { 211 | "error": error.value.decode("utf-8"), 212 | "error_code": evt.error_code, 213 | "minor_code": evt.minor_code, 214 | "request_code": evt.request_code, 215 | "serial": evt.serial, 216 | "type": evt.type, 217 | } 218 | 219 | return 0 220 | 221 | 222 | def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, Any]: 223 | """Validate the returned value of a C function call.""" 224 | thread = current_thread() 225 | if retval != 0 and thread not in _ERROR: 226 | return args 227 | 228 | details = _ERROR.pop(thread, {}) 229 | msg = f"{func.__name__}() failed" 230 | raise ScreenShotError(msg, details=details) 231 | 232 | 233 | # C functions that will be initialised later. 234 | # See https://tronche.com/gui/x/xlib/function-index.html for details. 235 | # 236 | # Available attr: xfixes, xlib, xrandr. 237 | # 238 | # Note: keep it sorted by cfunction. 239 | CFUNCTIONS: CFunctions = { 240 | # Syntax: cfunction: (attr, argtypes, restype) 241 | "XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p), 242 | "XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)), 243 | "XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p), 244 | "XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)), 245 | "XGetImage": ( 246 | "xlib", 247 | [POINTER(Display), POINTER(Display), c_int, c_int, c_uint, c_uint, c_ulong, c_int], 248 | POINTER(XImage), 249 | ), 250 | "XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int), 251 | "XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)), 252 | "XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint), 253 | "XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p), 254 | "XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p), 255 | "XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)), 256 | "XRRGetScreenResources": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)), 257 | "XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)), 258 | "XSetErrorHandler": ("xlib", [c_void_p], c_void_p), 259 | } 260 | 261 | 262 | class MSS(MSSBase): 263 | """Multiple ScreenShots implementation for GNU/Linux. 264 | It uses intensively the Xlib and its Xrandr extension. 265 | """ 266 | 267 | __slots__ = {"_handles", "xfixes", "xlib", "xrandr"} 268 | 269 | def __init__(self, /, **kwargs: Any) -> None: 270 | """GNU/Linux initialisations.""" 271 | super().__init__(**kwargs) 272 | 273 | # Available thread-specific variables 274 | self._handles = local() 275 | self._handles.display = None 276 | self._handles.drawable = None 277 | self._handles.original_error_handler = None 278 | self._handles.root = None 279 | 280 | display = kwargs.get("display", b"") 281 | if not display: 282 | try: 283 | display = os.environ["DISPLAY"].encode("utf-8") 284 | except KeyError: 285 | msg = "$DISPLAY not set." 286 | raise ScreenShotError(msg) from None 287 | 288 | if not isinstance(display, bytes): 289 | display = display.encode("utf-8") 290 | 291 | if b":" not in display: 292 | msg = f"Bad display value: {display!r}." 293 | raise ScreenShotError(msg) 294 | 295 | if not _X11: 296 | msg = "No X11 library found." 297 | raise ScreenShotError(msg) 298 | self.xlib = cdll.LoadLibrary(_X11) 299 | 300 | if not _XRANDR: 301 | msg = "No Xrandr extension found." 302 | raise ScreenShotError(msg) 303 | self.xrandr = cdll.LoadLibrary(_XRANDR) 304 | 305 | if self.with_cursor: 306 | if _XFIXES: 307 | self.xfixes = cdll.LoadLibrary(_XFIXES) 308 | else: 309 | self.with_cursor = False 310 | 311 | self._set_cfunctions() 312 | 313 | # Install the error handler to prevent interpreter crashes: any error will raise a ScreenShotError exception 314 | self._handles.original_error_handler = self.xlib.XSetErrorHandler(_error_handler) 315 | 316 | self._handles.display = self.xlib.XOpenDisplay(display) 317 | if not self._handles.display: 318 | msg = f"Unable to open display: {display!r}." 319 | raise ScreenShotError(msg) 320 | 321 | if not self._is_extension_enabled("RANDR"): 322 | msg = "Xrandr not enabled." 323 | raise ScreenShotError(msg) 324 | 325 | self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display) 326 | 327 | # Fix for XRRGetScreenResources and XGetImage: 328 | # expected LP_Display instance instead of LP_XWindowAttributes 329 | self._handles.drawable = cast(self._handles.root, POINTER(Display)) 330 | 331 | def close(self) -> None: 332 | # Clean-up 333 | if self._handles.display: 334 | with lock: 335 | self.xlib.XCloseDisplay(self._handles.display) 336 | self._handles.display = None 337 | self._handles.drawable = None 338 | self._handles.root = None 339 | 340 | # Remove our error handler 341 | if self._handles.original_error_handler: 342 | # It's required when exiting MSS to prevent letting `_error_handler()` as default handler. 343 | # Doing so would crash when using Tk/Tkinter, see issue #220. 344 | # Interesting technical stuff can be found here: 345 | # https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50 346 | # https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c 347 | self.xlib.XSetErrorHandler(self._handles.original_error_handler) 348 | self._handles.original_error_handler = None 349 | 350 | # Also empty the error dict 351 | _ERROR.clear() 352 | 353 | def _is_extension_enabled(self, name: str, /) -> bool: 354 | """Return True if the given *extension* is enabled on the server.""" 355 | major_opcode_return = c_int() 356 | first_event_return = c_int() 357 | first_error_return = c_int() 358 | 359 | try: 360 | with lock: 361 | self.xlib.XQueryExtension( 362 | self._handles.display, 363 | name.encode("latin1"), 364 | byref(major_opcode_return), 365 | byref(first_event_return), 366 | byref(first_error_return), 367 | ) 368 | except ScreenShotError: 369 | return False 370 | return True 371 | 372 | def _set_cfunctions(self) -> None: 373 | """Set all ctypes functions and attach them to attributes.""" 374 | cfactory = self._cfactory 375 | attrs = { 376 | "xfixes": getattr(self, "xfixes", None), 377 | "xlib": self.xlib, 378 | "xrandr": self.xrandr, 379 | } 380 | for func, (attr, argtypes, restype) in CFUNCTIONS.items(): 381 | with suppress(AttributeError): 382 | errcheck = None if func == "XSetErrorHandler" else _validate 383 | cfactory(attrs[attr], func, argtypes, restype, errcheck=errcheck) 384 | 385 | def _monitors_impl(self) -> None: 386 | """Get positions of monitors. It will populate self._monitors.""" 387 | display = self._handles.display 388 | int_ = int 389 | xrandr = self.xrandr 390 | 391 | # All monitors 392 | gwa = XWindowAttributes() 393 | self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa)) 394 | self._monitors.append( 395 | {"left": int_(gwa.x), "top": int_(gwa.y), "width": int_(gwa.width), "height": int_(gwa.height)}, 396 | ) 397 | 398 | # Each monitor 399 | # A simple benchmark calling 10 times those 2 functions: 400 | # XRRGetScreenResources(): 0.1755971429956844 s 401 | # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s 402 | # The second is faster by a factor of 44! So try to use it first. 403 | try: 404 | mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents 405 | except AttributeError: 406 | mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents 407 | 408 | crtcs = mon.crtcs 409 | for idx in range(mon.ncrtc): 410 | crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents 411 | if crtc.noutput == 0: 412 | xrandr.XRRFreeCrtcInfo(crtc) 413 | continue 414 | 415 | self._monitors.append( 416 | { 417 | "left": int_(crtc.x), 418 | "top": int_(crtc.y), 419 | "width": int_(crtc.width), 420 | "height": int_(crtc.height), 421 | }, 422 | ) 423 | xrandr.XRRFreeCrtcInfo(crtc) 424 | xrandr.XRRFreeScreenResources(mon) 425 | 426 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 427 | """Retrieve all pixels from a monitor. Pixels have to be RGB.""" 428 | ximage = self.xlib.XGetImage( 429 | self._handles.display, 430 | self._handles.drawable, 431 | monitor["left"], 432 | monitor["top"], 433 | monitor["width"], 434 | monitor["height"], 435 | PLAINMASK, 436 | ZPIXMAP, 437 | ) 438 | 439 | try: 440 | bits_per_pixel = ximage.contents.bits_per_pixel 441 | if bits_per_pixel not in SUPPORTED_BITS_PER_PIXELS: 442 | msg = f"[XImage] bits per pixel value not (yet?) implemented: {bits_per_pixel}." 443 | raise ScreenShotError(msg) 444 | 445 | raw_data = cast( 446 | ximage.contents.data, 447 | POINTER(c_ubyte * monitor["height"] * monitor["width"] * 4), 448 | ) 449 | data = bytearray(raw_data.contents) 450 | finally: 451 | # Free 452 | self.xlib.XDestroyImage(ximage) 453 | 454 | return self.cls_image(data, monitor) 455 | 456 | def _cursor_impl(self) -> ScreenShot: 457 | """Retrieve all cursor data. Pixels have to be RGB.""" 458 | # Read data of cursor/mouse-pointer 459 | ximage = self.xfixes.XFixesGetCursorImage(self._handles.display) 460 | if not (ximage and ximage.contents): 461 | msg = "Cannot read XFixesGetCursorImage()" 462 | raise ScreenShotError(msg) 463 | 464 | cursor_img: XFixesCursorImage = ximage.contents 465 | region = { 466 | "left": cursor_img.x - cursor_img.xhot, 467 | "top": cursor_img.y - cursor_img.yhot, 468 | "width": cursor_img.width, 469 | "height": cursor_img.height, 470 | } 471 | 472 | raw_data = cast(cursor_img.pixels, POINTER(c_ulong * region["height"] * region["width"])) 473 | raw = bytearray(raw_data.contents) 474 | 475 | data = bytearray(region["height"] * region["width"] * 4) 476 | data[3::4] = raw[3::8] 477 | data[2::4] = raw[2::8] 478 | data[1::4] = raw[1::8] 479 | data[::4] = raw[::8] 480 | 481 | return self.cls_image(data, region) 482 | -------------------------------------------------------------------------------- /src/mss/models.py: -------------------------------------------------------------------------------- 1 | """This is part of the MSS Python's module. 2 | Source: https://github.com/BoboTiG/python-mss. 3 | """ 4 | 5 | from typing import Any, NamedTuple 6 | 7 | Monitor = dict[str, int] 8 | Monitors = list[Monitor] 9 | 10 | Pixel = tuple[int, int, int] 11 | Pixels = list[tuple[Pixel, ...]] 12 | 13 | CFunctions = dict[str, tuple[str, list[Any], Any]] 14 | 15 | 16 | class Pos(NamedTuple): 17 | left: int 18 | top: int 19 | 20 | 21 | class Size(NamedTuple): 22 | width: int 23 | height: int 24 | -------------------------------------------------------------------------------- /src/mss/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/d7813b5d9794a73aaf01632955e9d98d49112d4e/src/mss/py.typed -------------------------------------------------------------------------------- /src/mss/screenshot.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 | from typing import TYPE_CHECKING, Any 8 | 9 | from mss.exception import ScreenShotError 10 | from mss.models import Monitor, Pixel, Pixels, Pos, Size 11 | 12 | if TYPE_CHECKING: # pragma: nocover 13 | from collections.abc import Iterator 14 | 15 | 16 | class ScreenShot: 17 | """Screenshot object. 18 | 19 | .. note:: 20 | 21 | A better name would have been *Image*, but to prevent collisions 22 | with PIL.Image, it has been decided to use *ScreenShot*. 23 | """ 24 | 25 | __slots__ = {"__pixels", "__rgb", "pos", "raw", "size"} 26 | 27 | def __init__(self, data: bytearray, monitor: Monitor, /, *, size: Size | None = None) -> None: 28 | self.__pixels: Pixels | None = None 29 | self.__rgb: bytes | None = None 30 | 31 | #: Bytearray of the raw BGRA pixels retrieved by ctypes 32 | #: OS independent implementations. 33 | self.raw = data 34 | 35 | #: NamedTuple of the screenshot coordinates. 36 | self.pos = Pos(monitor["left"], monitor["top"]) 37 | 38 | #: NamedTuple of the screenshot size. 39 | self.size = Size(monitor["width"], monitor["height"]) if size is None else size 40 | 41 | def __repr__(self) -> str: 42 | return f"<{type(self).__name__} pos={self.left},{self.top} size={self.width}x{self.height}>" 43 | 44 | @property 45 | def __array_interface__(self) -> dict[str, Any]: 46 | """Numpy array interface support. 47 | It uses raw data in BGRA form. 48 | 49 | See https://docs.scipy.org/doc/numpy/reference/arrays.interface.html 50 | """ 51 | return { 52 | "version": 3, 53 | "shape": (self.height, self.width, 4), 54 | "typestr": "|u1", 55 | "data": self.raw, 56 | } 57 | 58 | @classmethod 59 | def from_size(cls: type[ScreenShot], data: bytearray, width: int, height: int, /) -> ScreenShot: 60 | """Instantiate a new class given only screenshot's data and size.""" 61 | monitor = {"left": 0, "top": 0, "width": width, "height": height} 62 | return cls(data, monitor) 63 | 64 | @property 65 | def bgra(self) -> bytes: 66 | """BGRA values from the BGRA raw pixels.""" 67 | return bytes(self.raw) 68 | 69 | @property 70 | def height(self) -> int: 71 | """Convenient accessor to the height size.""" 72 | return self.size.height 73 | 74 | @property 75 | def left(self) -> int: 76 | """Convenient accessor to the left position.""" 77 | return self.pos.left 78 | 79 | @property 80 | def pixels(self) -> Pixels: 81 | """:return list: RGB tuples.""" 82 | if not self.__pixels: 83 | rgb_tuples: Iterator[Pixel] = zip(self.raw[2::4], self.raw[1::4], self.raw[::4]) 84 | self.__pixels = list(zip(*[iter(rgb_tuples)] * self.width)) 85 | 86 | return self.__pixels 87 | 88 | @property 89 | def rgb(self) -> bytes: 90 | """Compute RGB values from the BGRA raw pixels. 91 | 92 | :return bytes: RGB pixels. 93 | """ 94 | if not self.__rgb: 95 | rgb = bytearray(self.height * self.width * 3) 96 | raw = self.raw 97 | rgb[::3] = raw[2::4] 98 | rgb[1::3] = raw[1::4] 99 | rgb[2::3] = raw[::4] 100 | self.__rgb = bytes(rgb) 101 | 102 | return self.__rgb 103 | 104 | @property 105 | def top(self) -> int: 106 | """Convenient accessor to the top position.""" 107 | return self.pos.top 108 | 109 | @property 110 | def width(self) -> int: 111 | """Convenient accessor to the width size.""" 112 | return self.size.width 113 | 114 | def pixel(self, coord_x: int, coord_y: int) -> Pixel: 115 | """Returns the pixel value at a given position. 116 | 117 | :param int coord_x: The x coordinate. 118 | :param int coord_y: The y coordinate. 119 | :return tuple: The pixel value as (R, G, B). 120 | """ 121 | try: 122 | return self.pixels[coord_y][coord_x] 123 | except IndexError as exc: 124 | msg = f"Pixel location ({coord_x}, {coord_y}) is out of range." 125 | raise ScreenShotError(msg) from exc 126 | -------------------------------------------------------------------------------- /src/mss/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 os 8 | import struct 9 | import zlib 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from pathlib import Path 14 | 15 | 16 | def to_png(data: bytes, size: tuple[int, int], /, *, level: int = 6, output: Path | str | None = None) -> bytes | None: 17 | """Dump data to a PNG file. If `output` is `None`, create no file but return 18 | the whole PNG data. 19 | 20 | :param bytes data: RGBRGB...RGB data. 21 | :param tuple size: The (width, height) pair. 22 | :param int level: PNG compression level. 23 | :param str output: Output file name. 24 | """ 25 | pack = struct.pack 26 | crc32 = zlib.crc32 27 | 28 | width, height = size 29 | line = width * 3 30 | png_filter = pack(">B", 0) 31 | scanlines = b"".join([png_filter + data[y * line : y * line + line] for y in range(height)]) 32 | 33 | magic = pack(">8B", 137, 80, 78, 71, 13, 10, 26, 10) 34 | 35 | # Header: size, marker, data, CRC32 36 | ihdr = [b"", b"IHDR", b"", b""] 37 | ihdr[2] = pack(">2I5B", width, height, 8, 2, 0, 0, 0) 38 | ihdr[3] = pack(">I", crc32(b"".join(ihdr[1:3])) & 0xFFFFFFFF) 39 | ihdr[0] = pack(">I", len(ihdr[2])) 40 | 41 | # Data: size, marker, data, CRC32 42 | idat = [b"", b"IDAT", zlib.compress(scanlines, level), b""] 43 | idat[3] = pack(">I", crc32(b"".join(idat[1:3])) & 0xFFFFFFFF) 44 | idat[0] = pack(">I", len(idat[2])) 45 | 46 | # Footer: size, marker, None, CRC32 47 | iend = [b"", b"IEND", b"", b""] 48 | iend[3] = pack(">I", crc32(iend[1]) & 0xFFFFFFFF) 49 | iend[0] = pack(">I", len(iend[2])) 50 | 51 | if not output: 52 | # Returns raw bytes of the whole PNG data 53 | return magic + b"".join(ihdr + idat + iend) 54 | 55 | with open(output, "wb") as fileh: # noqa: PTH123 56 | fileh.write(magic) 57 | fileh.write(b"".join(ihdr)) 58 | fileh.write(b"".join(idat)) 59 | fileh.write(b"".join(iend)) 60 | 61 | # Force write of file to disk 62 | fileh.flush() 63 | os.fsync(fileh.fileno()) 64 | 65 | return None 66 | -------------------------------------------------------------------------------- /src/mss/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 ctypes 8 | import sys 9 | from ctypes import POINTER, WINFUNCTYPE, Structure, c_int, c_void_p 10 | from ctypes.wintypes import ( 11 | BOOL, 12 | DOUBLE, 13 | DWORD, 14 | HBITMAP, 15 | HDC, 16 | HGDIOBJ, 17 | HWND, 18 | INT, 19 | LONG, 20 | LPARAM, 21 | LPRECT, 22 | RECT, 23 | UINT, 24 | WORD, 25 | ) 26 | from threading import local 27 | from typing import TYPE_CHECKING, Any 28 | 29 | from mss.base import MSSBase 30 | from mss.exception import ScreenShotError 31 | 32 | if TYPE_CHECKING: # pragma: nocover 33 | from mss.models import CFunctions, Monitor 34 | from mss.screenshot import ScreenShot 35 | 36 | __all__ = ("MSS",) 37 | 38 | 39 | CAPTUREBLT = 0x40000000 40 | DIB_RGB_COLORS = 0 41 | SRCCOPY = 0x00CC0020 42 | 43 | 44 | class BITMAPINFOHEADER(Structure): 45 | """Information about the dimensions and color format of a DIB.""" 46 | 47 | _fields_ = ( 48 | ("biSize", DWORD), 49 | ("biWidth", LONG), 50 | ("biHeight", LONG), 51 | ("biPlanes", WORD), 52 | ("biBitCount", WORD), 53 | ("biCompression", DWORD), 54 | ("biSizeImage", DWORD), 55 | ("biXPelsPerMeter", LONG), 56 | ("biYPelsPerMeter", LONG), 57 | ("biClrUsed", DWORD), 58 | ("biClrImportant", DWORD), 59 | ) 60 | 61 | 62 | class BITMAPINFO(Structure): 63 | """Structure that defines the dimensions and color information for a DIB.""" 64 | 65 | _fields_ = (("bmiHeader", BITMAPINFOHEADER), ("bmiColors", DWORD * 3)) 66 | 67 | 68 | MONITORNUMPROC = WINFUNCTYPE(INT, DWORD, DWORD, POINTER(RECT), DOUBLE) 69 | 70 | 71 | # C functions that will be initialised later. 72 | # 73 | # Available attr: gdi32, user32. 74 | # 75 | # Note: keep it sorted by cfunction. 76 | CFUNCTIONS: CFunctions = { 77 | # Syntax: cfunction: (attr, argtypes, restype) 78 | "BitBlt": ("gdi32", [HDC, INT, INT, INT, INT, HDC, INT, INT, DWORD], BOOL), 79 | "CreateCompatibleBitmap": ("gdi32", [HDC, INT, INT], HBITMAP), 80 | "CreateCompatibleDC": ("gdi32", [HDC], HDC), 81 | "DeleteDC": ("gdi32", [HDC], HDC), 82 | "DeleteObject": ("gdi32", [HGDIOBJ], INT), 83 | "EnumDisplayMonitors": ("user32", [HDC, c_void_p, MONITORNUMPROC, LPARAM], BOOL), 84 | "GetDeviceCaps": ("gdi32", [HWND, INT], INT), 85 | "GetDIBits": ("gdi32", [HDC, HBITMAP, UINT, UINT, c_void_p, POINTER(BITMAPINFO), UINT], BOOL), 86 | "GetSystemMetrics": ("user32", [INT], INT), 87 | "GetWindowDC": ("user32", [HWND], HDC), 88 | "ReleaseDC": ("user32", [HWND, HDC], c_int), 89 | "SelectObject": ("gdi32", [HDC, HGDIOBJ], HGDIOBJ), 90 | } 91 | 92 | 93 | class MSS(MSSBase): 94 | """Multiple ScreenShots implementation for Microsoft Windows.""" 95 | 96 | __slots__ = {"_handles", "gdi32", "user32"} 97 | 98 | def __init__(self, /, **kwargs: Any) -> None: 99 | """Windows initialisations.""" 100 | super().__init__(**kwargs) 101 | 102 | self.user32 = ctypes.WinDLL("user32") 103 | self.gdi32 = ctypes.WinDLL("gdi32") 104 | self._set_cfunctions() 105 | self._set_dpi_awareness() 106 | 107 | # Available thread-specific variables 108 | self._handles = local() 109 | self._handles.region_width_height = (0, 0) 110 | self._handles.bmp = None 111 | self._handles.srcdc = self.user32.GetWindowDC(0) 112 | self._handles.memdc = self.gdi32.CreateCompatibleDC(self._handles.srcdc) 113 | 114 | bmi = BITMAPINFO() 115 | bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) 116 | bmi.bmiHeader.biPlanes = 1 # Always 1 117 | bmi.bmiHeader.biBitCount = 32 # See grab.__doc__ [2] 118 | bmi.bmiHeader.biCompression = 0 # 0 = BI_RGB (no compression) 119 | bmi.bmiHeader.biClrUsed = 0 # See grab.__doc__ [3] 120 | bmi.bmiHeader.biClrImportant = 0 # See grab.__doc__ [3] 121 | self._handles.bmi = bmi 122 | 123 | def close(self) -> None: 124 | # Clean-up 125 | if self._handles.bmp: 126 | self.gdi32.DeleteObject(self._handles.bmp) 127 | self._handles.bmp = None 128 | 129 | if self._handles.memdc: 130 | self.gdi32.DeleteDC(self._handles.memdc) 131 | self._handles.memdc = None 132 | 133 | if self._handles.srcdc: 134 | self.user32.ReleaseDC(0, self._handles.srcdc) 135 | self._handles.srcdc = None 136 | 137 | def _set_cfunctions(self) -> None: 138 | """Set all ctypes functions and attach them to attributes.""" 139 | cfactory = self._cfactory 140 | attrs = { 141 | "gdi32": self.gdi32, 142 | "user32": self.user32, 143 | } 144 | for func, (attr, argtypes, restype) in CFUNCTIONS.items(): 145 | cfactory(attrs[attr], func, argtypes, restype) 146 | 147 | def _set_dpi_awareness(self) -> None: 148 | """Set DPI awareness to capture full screen on Hi-DPI monitors.""" 149 | version = sys.getwindowsversion()[:2] 150 | if version >= (6, 3): 151 | # Windows 8.1+ 152 | # Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means: 153 | # per monitor DPI aware. This app checks for the DPI when it is 154 | # created and adjusts the scale factor whenever the DPI changes. 155 | # These applications are not automatically scaled by the system. 156 | ctypes.windll.shcore.SetProcessDpiAwareness(2) 157 | elif (6, 0) <= version < (6, 3): 158 | # Windows Vista, 7, 8, and Server 2012 159 | self.user32.SetProcessDPIAware() 160 | 161 | def _monitors_impl(self) -> None: 162 | """Get positions of monitors. It will populate self._monitors.""" 163 | int_ = int 164 | user32 = self.user32 165 | get_system_metrics = user32.GetSystemMetrics 166 | 167 | # All monitors 168 | self._monitors.append( 169 | { 170 | "left": int_(get_system_metrics(76)), # SM_XVIRTUALSCREEN 171 | "top": int_(get_system_metrics(77)), # SM_YVIRTUALSCREEN 172 | "width": int_(get_system_metrics(78)), # SM_CXVIRTUALSCREEN 173 | "height": int_(get_system_metrics(79)), # SM_CYVIRTUALSCREEN 174 | }, 175 | ) 176 | 177 | # Each monitor 178 | def _callback(_monitor: int, _data: HDC, rect: LPRECT, _dc: LPARAM) -> int: 179 | """Callback for monitorenumproc() function, it will return 180 | a RECT with appropriate values. 181 | """ 182 | rct = rect.contents 183 | self._monitors.append( 184 | { 185 | "left": int_(rct.left), 186 | "top": int_(rct.top), 187 | "width": int_(rct.right) - int_(rct.left), 188 | "height": int_(rct.bottom) - int_(rct.top), 189 | }, 190 | ) 191 | return 1 192 | 193 | callback = MONITORNUMPROC(_callback) 194 | user32.EnumDisplayMonitors(0, 0, callback, 0) 195 | 196 | def _grab_impl(self, monitor: Monitor, /) -> ScreenShot: 197 | """Retrieve all pixels from a monitor. Pixels have to be RGB. 198 | 199 | In the code, there are a few interesting things: 200 | 201 | [1] bmi.bmiHeader.biHeight = -height 202 | 203 | A bottom-up DIB is specified by setting the height to a 204 | positive number, while a top-down DIB is specified by 205 | setting the height to a negative number. 206 | https://msdn.microsoft.com/en-us/library/ms787796.aspx 207 | https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx 208 | 209 | 210 | [2] bmi.bmiHeader.biBitCount = 32 211 | image_data = create_string_buffer(height * width * 4) 212 | 213 | We grab the image in RGBX mode, so that each word is 32bit 214 | and we have no striding. 215 | Inspired by https://github.com/zoofIO/flexx 216 | 217 | 218 | [3] bmi.bmiHeader.biClrUsed = 0 219 | bmi.bmiHeader.biClrImportant = 0 220 | 221 | When biClrUsed and biClrImportant are set to zero, there 222 | is "no" color table, so we can read the pixels of the bitmap 223 | retrieved by gdi32.GetDIBits() as a sequence of RGB values. 224 | Thanks to http://stackoverflow.com/a/3688682 225 | """ 226 | srcdc, memdc = self._handles.srcdc, self._handles.memdc 227 | gdi = self.gdi32 228 | width, height = monitor["width"], monitor["height"] 229 | 230 | if self._handles.region_width_height != (width, height): 231 | self._handles.region_width_height = (width, height) 232 | self._handles.bmi.bmiHeader.biWidth = width 233 | self._handles.bmi.bmiHeader.biHeight = -height # Why minus? [1] 234 | self._handles.data = ctypes.create_string_buffer(width * height * 4) # [2] 235 | if self._handles.bmp: 236 | gdi.DeleteObject(self._handles.bmp) 237 | self._handles.bmp = gdi.CreateCompatibleBitmap(srcdc, width, height) 238 | gdi.SelectObject(memdc, self._handles.bmp) 239 | 240 | gdi.BitBlt(memdc, 0, 0, width, height, srcdc, monitor["left"], monitor["top"], SRCCOPY | CAPTUREBLT) 241 | bits = gdi.GetDIBits(memdc, self._handles.bmp, 0, height, self._handles.data, self._handles.bmi, DIB_RGB_COLORS) 242 | if bits != height: 243 | msg = "gdi32.GetDIBits() failed." 244 | raise ScreenShotError(msg) 245 | 246 | return self.cls_image(bytearray(self._handles.data), monitor) 247 | 248 | def _cursor_impl(self) -> ScreenShot | None: 249 | """Retrieve all cursor data. Pixels have to be RGB.""" 250 | return None 251 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/d7813b5d9794a73aaf01632955e9d98d49112d4e/src/tests/__init__.py -------------------------------------------------------------------------------- /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/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/tests/conftest.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 Generator 6 | from hashlib import sha256 7 | from pathlib import Path 8 | from zipfile import ZipFile 9 | 10 | import pytest 11 | 12 | 13 | @pytest.fixture(autouse=True) 14 | def _no_warnings(recwarn: pytest.WarningsRecorder) -> Generator: 15 | """Fail on warning.""" 16 | yield 17 | 18 | warnings = [f"{warning.filename}:{warning.lineno} {warning.message}" for warning in recwarn] 19 | for warning in warnings: 20 | print(warning) 21 | assert not warnings 22 | 23 | 24 | def purge_files() -> None: 25 | """Remove all generated files from previous runs.""" 26 | for file in Path().glob("*.png"): 27 | print(f"Deleting {file} ...") 28 | file.unlink() 29 | 30 | for file in Path().glob("*.png.old"): 31 | print(f"Deleting {file} ...") 32 | file.unlink() 33 | 34 | 35 | @pytest.fixture(scope="module", autouse=True) 36 | def _before_tests() -> None: 37 | purge_files() 38 | 39 | 40 | @pytest.fixture(scope="session") 41 | def raw() -> bytes: 42 | file = Path(__file__).parent / "res" / "monitor-1024x768.raw.zip" 43 | with ZipFile(file) as fh: 44 | data = fh.read(file.with_suffix("").name) 45 | 46 | assert sha256(data).hexdigest() == "d86ed4366d5a882cfe1345de82c87b81aef9f9bf085f4c42acb6f63f3967eccd" 47 | return data 48 | -------------------------------------------------------------------------------- /src/tests/res/monitor-1024x768.raw.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/d7813b5d9794a73aaf01632955e9d98d49112d4e/src/tests/res/monitor-1024x768.raw.zip -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | import os 6 | from typing import Any 7 | 8 | from mss import mss 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() -> None: 19 | with mss(display=os.getenv("DISPLAY")) 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 | -------------------------------------------------------------------------------- /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 | import os 6 | 7 | from mss import mss 8 | 9 | 10 | def test_get_monitors() -> None: 11 | with mss(display=os.getenv("DISPLAY")) as sct: 12 | assert sct.monitors 13 | 14 | 15 | def test_keys_aio() -> None: 16 | with mss(display=os.getenv("DISPLAY")) 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() -> None: 25 | with mss(display=os.getenv("DISPLAY")) 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() -> None: 34 | with mss(display=os.getenv("DISPLAY")) as sct: 35 | mon = sct.monitors[1] 36 | assert mon["width"] > 0 37 | assert mon["height"] > 0 38 | -------------------------------------------------------------------------------- /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 | import os 7 | 8 | import pytest 9 | 10 | from mss import mss 11 | from mss.base import ScreenShot 12 | from mss.exception import ScreenShotError 13 | 14 | 15 | def test_grab_monitor() -> None: 16 | with mss(display=os.getenv("DISPLAY")) as sct: 17 | for mon in sct.monitors: 18 | image = sct.grab(mon) 19 | assert isinstance(image, ScreenShot) 20 | assert isinstance(image.raw, bytearray) 21 | assert isinstance(image.rgb, bytes) 22 | 23 | 24 | def test_grab_part_of_screen() -> None: 25 | with mss(display=os.getenv("DISPLAY")) as sct: 26 | for width, height in itertools.product(range(1, 42), range(1, 42)): 27 | monitor = {"top": 160, "left": 160, "width": width, "height": height} 28 | image = sct.grab(monitor) 29 | 30 | assert image.top == 160 31 | assert image.left == 160 32 | assert image.width == width 33 | assert image.height == height 34 | 35 | 36 | def test_get_pixel(raw: bytes) -> None: 37 | image = ScreenShot.from_size(bytearray(raw), 1024, 768) 38 | assert image.width == 1024 39 | assert image.height == 768 40 | assert len(image.pixels) == 768 41 | assert len(image.pixels[0]) == 1024 42 | 43 | assert image.pixel(0, 0) == (135, 152, 192) 44 | assert image.pixel(image.width // 2, image.height // 2) == (0, 0, 0) 45 | assert image.pixel(image.width - 1, image.height - 1) == (135, 152, 192) 46 | 47 | with pytest.raises(ScreenShotError): 48 | image.pixel(image.width + 1, 12) 49 | -------------------------------------------------------------------------------- /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 | import platform 6 | from collections.abc import Generator 7 | from unittest.mock import Mock, patch 8 | 9 | import pytest 10 | 11 | import mss 12 | import mss.linux 13 | from mss.base import MSSBase 14 | from mss.exception import ScreenShotError 15 | 16 | pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") 17 | 18 | PYPY = platform.python_implementation() == "PyPy" 19 | 20 | WIDTH = 200 21 | HEIGHT = 200 22 | DEPTH = 24 23 | 24 | 25 | @pytest.fixture 26 | def display() -> Generator: 27 | with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: 28 | yield vdisplay.new_display_var 29 | 30 | 31 | @pytest.mark.skipif(PYPY, reason="Failure on PyPy") 32 | def test_factory_systems(monkeypatch: pytest.MonkeyPatch) -> None: 33 | """Here, we are testing all systems. 34 | 35 | Too hard to maintain the test for all platforms, 36 | so test only on GNU/Linux. 37 | """ 38 | # GNU/Linux 39 | monkeypatch.setattr(platform, "system", lambda: "LINUX") 40 | with mss.mss() as sct: 41 | assert isinstance(sct, MSSBase) 42 | monkeypatch.undo() 43 | 44 | # macOS 45 | monkeypatch.setattr(platform, "system", lambda: "Darwin") 46 | # ValueError on macOS Big Sur 47 | with pytest.raises((ScreenShotError, ValueError)), mss.mss(): 48 | pass 49 | monkeypatch.undo() 50 | 51 | # Windows 52 | monkeypatch.setattr(platform, "system", lambda: "wInDoWs") 53 | with pytest.raises(ImportError, match="cannot import name 'WINFUNCTYPE'"), mss.mss(): 54 | pass 55 | 56 | 57 | def test_arg_display(display: str, monkeypatch: pytest.MonkeyPatch) -> None: 58 | # Good value 59 | with mss.mss(display=display): 60 | pass 61 | 62 | # Bad `display` (missing ":" in front of the number) 63 | with pytest.raises(ScreenShotError), mss.mss(display="0"): 64 | pass 65 | 66 | # Invalid `display` that is not trivially distinguishable. 67 | with pytest.raises(ScreenShotError), mss.mss(display=":INVALID"): 68 | pass 69 | 70 | # No `DISPLAY` in envars 71 | monkeypatch.delenv("DISPLAY") 72 | with pytest.raises(ScreenShotError), mss.mss(): 73 | pass 74 | 75 | 76 | @pytest.mark.skipif(PYPY, reason="Failure on PyPy") 77 | def test_bad_display_structure(monkeypatch: pytest.MonkeyPatch) -> None: 78 | monkeypatch.setattr(mss.linux, "Display", lambda: None) 79 | with pytest.raises(TypeError), mss.mss(): 80 | pass 81 | 82 | 83 | @patch("mss.linux._X11", new=None) 84 | def test_no_xlib_library() -> None: 85 | with pytest.raises(ScreenShotError), mss.mss(): 86 | pass 87 | 88 | 89 | @patch("mss.linux._XRANDR", new=None) 90 | def test_no_xrandr_extension() -> None: 91 | with pytest.raises(ScreenShotError), mss.mss(): 92 | pass 93 | 94 | 95 | @patch("mss.linux.MSS._is_extension_enabled", new=Mock(return_value=False)) 96 | def test_xrandr_extension_exists_but_is_not_enabled(display: str) -> None: 97 | with pytest.raises(ScreenShotError), mss.mss(display=display): 98 | pass 99 | 100 | 101 | def test_unsupported_depth() -> None: 102 | with ( 103 | pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=8) as vdisplay, 104 | pytest.raises(ScreenShotError), 105 | mss.mss(display=vdisplay.new_display_var) as sct, 106 | ): 107 | sct.grab(sct.monitors[1]) 108 | 109 | 110 | def test_region_out_of_monitor_bounds(display: str) -> None: 111 | monitor = {"left": -30, "top": 0, "width": WIDTH, "height": HEIGHT} 112 | 113 | assert not mss.linux._ERROR 114 | 115 | with mss.mss(display=display) as sct: 116 | with pytest.raises(ScreenShotError) as exc: 117 | sct.grab(monitor) 118 | 119 | assert str(exc.value) 120 | 121 | details = exc.value.details 122 | assert details 123 | assert isinstance(details, dict) 124 | assert isinstance(details["error"], str) 125 | assert not mss.linux._ERROR 126 | 127 | assert not mss.linux._ERROR 128 | 129 | 130 | def test__is_extension_enabled_unknown_name(display: str) -> None: 131 | with mss.mss(display=display) as sct: 132 | assert isinstance(sct, mss.linux.MSS) # For Mypy 133 | assert not sct._is_extension_enabled("NOEXT") 134 | 135 | 136 | def test_missing_fast_function_for_monitor_details_retrieval(display: str) -> None: 137 | with mss.mss(display=display) as sct: 138 | assert isinstance(sct, mss.linux.MSS) # For Mypy 139 | assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") 140 | screenshot_with_fast_fn = sct.grab(sct.monitors[1]) 141 | 142 | assert set(screenshot_with_fast_fn.rgb) == {0} 143 | 144 | with mss.mss(display=display) as sct: 145 | assert isinstance(sct, mss.linux.MSS) # For Mypy 146 | assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") 147 | del sct.xrandr.XRRGetScreenResourcesCurrent 148 | screenshot_with_slow_fn = sct.grab(sct.monitors[1]) 149 | 150 | assert set(screenshot_with_slow_fn.rgb) == {0} 151 | 152 | 153 | def test_with_cursor(display: str) -> None: 154 | with mss.mss(display=display) as sct: 155 | assert not hasattr(sct, "xfixes") 156 | assert not sct.with_cursor 157 | screenshot_without_cursor = sct.grab(sct.monitors[1]) 158 | 159 | # 1 color: black 160 | assert set(screenshot_without_cursor.rgb) == {0} 161 | 162 | with mss.mss(display=display, with_cursor=True) as sct: 163 | assert hasattr(sct, "xfixes") 164 | assert sct.with_cursor 165 | screenshot_with_cursor = sct.grab(sct.monitors[1]) 166 | 167 | # 2 colors: black & white (default cursor is a white cross) 168 | assert set(screenshot_with_cursor.rgb) == {0, 255} 169 | 170 | 171 | @patch("mss.linux._XFIXES", new=None) 172 | def test_with_cursor_but_not_xfixes_extension_found(display: str) -> None: 173 | with mss.mss(display=display, with_cursor=True) as sct: 174 | assert not hasattr(sct, "xfixes") 175 | assert not sct.with_cursor 176 | 177 | 178 | def test_with_cursor_failure(display: str) -> None: 179 | with mss.mss(display=display, with_cursor=True) as sct: 180 | assert isinstance(sct, mss.linux.MSS) # For Mypy 181 | with ( 182 | patch.object(sct.xfixes, "XFixesGetCursorImage", return_value=None), 183 | pytest.raises(ScreenShotError), 184 | ): 185 | sct.grab(sct.monitors[1]) 186 | -------------------------------------------------------------------------------- /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 os.path 9 | import platform 10 | import sys 11 | from datetime import datetime 12 | from pathlib import Path 13 | from typing import TYPE_CHECKING 14 | from unittest.mock import Mock, patch 15 | 16 | import pytest 17 | 18 | import mss 19 | import mss.tools 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 mss.models import Monitor 27 | 28 | try: 29 | from datetime import UTC 30 | except ImportError: 31 | # Python < 3.11 32 | from datetime import timezone 33 | 34 | UTC = timezone.utc 35 | 36 | 37 | class MSS0(MSSBase): 38 | """Nothing implemented.""" 39 | 40 | 41 | class MSS1(MSSBase): 42 | """Only `grab()` implemented.""" 43 | 44 | def grab(self, monitor: Monitor) -> None: # type: ignore[override] 45 | pass 46 | 47 | 48 | class MSS2(MSSBase): 49 | """Only `monitor` implemented.""" 50 | 51 | @property 52 | def monitors(self) -> list: 53 | return [] 54 | 55 | 56 | @pytest.mark.parametrize("cls", [MSS0, MSS1, MSS2]) 57 | def test_incomplete_class(cls: type[MSSBase]) -> None: 58 | with pytest.raises(TypeError): 59 | cls() 60 | 61 | 62 | def test_bad_monitor() -> None: 63 | with mss.mss(display=os.getenv("DISPLAY")) as sct, pytest.raises(ScreenShotError): 64 | sct.shot(mon=222) 65 | 66 | 67 | def test_repr() -> None: 68 | box = {"top": 0, "left": 0, "width": 10, "height": 10} 69 | expected_box = {"top": 0, "left": 0, "width": 10, "height": 10} 70 | with mss.mss(display=os.getenv("DISPLAY")) as sct: 71 | img = sct.grab(box) 72 | ref = ScreenShot(bytearray(b"42"), expected_box) 73 | assert repr(img) == repr(ref) 74 | 75 | 76 | def test_factory(monkeypatch: pytest.MonkeyPatch) -> None: 77 | # Current system 78 | with mss.mss() as sct: 79 | assert isinstance(sct, MSSBase) 80 | 81 | # Unknown 82 | monkeypatch.setattr(platform, "system", lambda: "Chuck Norris") 83 | with pytest.raises(ScreenShotError) as exc: 84 | mss.mss() 85 | monkeypatch.undo() 86 | 87 | error = exc.value.args[0] 88 | assert error == "System 'chuck norris' not (yet?) implemented." 89 | 90 | 91 | @patch.object(sys, "argv", new=[]) # Prevent side effects while testing 92 | @pytest.mark.parametrize("with_cursor", [False, True]) 93 | def test_entry_point(with_cursor: bool, capsys: pytest.CaptureFixture) -> None: 94 | def main(*args: str, ret: int = 0) -> None: 95 | if with_cursor: 96 | args = (*args, "--with-cursor") 97 | assert entry_point(*args) == ret 98 | 99 | # No arguments 100 | main() 101 | captured = capsys.readouterr() 102 | for mon, line in enumerate(captured.out.splitlines(), 1): 103 | filename = Path(f"monitor-{mon}.png") 104 | assert line.endswith(filename.name) 105 | assert filename.is_file() 106 | filename.unlink() 107 | 108 | file = Path("monitor-1.png") 109 | for opt in ("-m", "--monitor"): 110 | main(opt, "1") 111 | captured = capsys.readouterr() 112 | assert captured.out.endswith(f"{file.name}\n") 113 | assert filename.is_file() 114 | filename.unlink() 115 | 116 | for opts in zip(["-m 1", "--monitor=1"], ["-q", "--quiet"]): 117 | main(*opts) 118 | captured = capsys.readouterr() 119 | assert not captured.out 120 | assert filename.is_file() 121 | filename.unlink() 122 | 123 | fmt = "sct-{mon}-{width}x{height}.png" 124 | for opt in ("-o", "--out"): 125 | main(opt, fmt) 126 | captured = capsys.readouterr() 127 | with mss.mss(display=os.getenv("DISPLAY")) as sct: 128 | for mon, (monitor, line) in enumerate(zip(sct.monitors[1:], captured.out.splitlines()), 1): 129 | filename = Path(fmt.format(mon=mon, **monitor)) 130 | assert line.endswith(filename.name) 131 | assert filename.is_file() 132 | filename.unlink() 133 | 134 | fmt = "sct_{mon}-{date:%Y-%m-%d}.png" 135 | for opt in ("-o", "--out"): 136 | main("-m 1", opt, fmt) 137 | filename = Path(fmt.format(mon=1, date=datetime.now(tz=UTC))) 138 | captured = capsys.readouterr() 139 | assert captured.out.endswith(f"{filename}\n") 140 | assert filename.is_file() 141 | filename.unlink() 142 | 143 | coordinates = "2,12,40,67" 144 | filename = Path("sct-2x12_40x67.png") 145 | for opt in ("-c", "--coordinates"): 146 | main(opt, coordinates) 147 | captured = capsys.readouterr() 148 | assert captured.out.endswith(f"{filename}\n") 149 | assert filename.is_file() 150 | filename.unlink() 151 | 152 | coordinates = "2,12,40" 153 | for opt in ("-c", "--coordinates"): 154 | main(opt, coordinates, ret=2) 155 | captured = capsys.readouterr() 156 | assert captured.out == "Coordinates syntax: top, left, width, height\n" 157 | 158 | 159 | @patch.object(sys, "argv", new=[]) # Prevent side effects while testing 160 | @patch("mss.base.MSSBase.monitors", new=[]) 161 | @pytest.mark.parametrize("quiet", [False, True]) 162 | def test_entry_point_error(quiet: bool, capsys: pytest.CaptureFixture) -> None: 163 | def main(*args: str) -> int: 164 | if quiet: 165 | args = (*args, "--quiet") 166 | return entry_point(*args) 167 | 168 | if quiet: 169 | assert main() == 1 170 | captured = capsys.readouterr() 171 | assert not captured.out 172 | assert not captured.err 173 | else: 174 | with pytest.raises(ScreenShotError): 175 | main() 176 | 177 | 178 | def test_entry_point_with_no_argument(capsys: pytest.CaptureFixture) -> None: 179 | # Make sure to fail if arguments are not handled 180 | with ( 181 | patch("mss.factory.mss", new=Mock(side_effect=RuntimeError("Boom!"))), 182 | patch.object(sys, "argv", ["mss", "--help"]), 183 | pytest.raises(SystemExit) as exc, 184 | ): 185 | entry_point() 186 | assert exc.value.code == 0 187 | 188 | captured = capsys.readouterr() 189 | assert not captured.err 190 | assert "usage: mss" in captured.out 191 | 192 | 193 | def test_grab_with_tuple() -> None: 194 | left = 100 195 | top = 100 196 | right = 500 197 | lower = 500 198 | width = right - left # 400px width 199 | height = lower - top # 400px height 200 | 201 | with mss.mss(display=os.getenv("DISPLAY")) as sct: 202 | # PIL like 203 | box = (left, top, right, lower) 204 | im = sct.grab(box) 205 | assert im.size == (width, height) 206 | 207 | # MSS like 208 | box2 = {"left": left, "top": top, "width": width, "height": height} 209 | im2 = sct.grab(box2) 210 | assert im.size == im2.size 211 | assert im.pos == im2.pos 212 | assert im.rgb == im2.rgb 213 | 214 | 215 | def test_grab_with_tuple_percents() -> None: 216 | with mss.mss(display=os.getenv("DISPLAY")) as sct: 217 | monitor = sct.monitors[1] 218 | left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left 219 | top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top 220 | right = left + 500 # 500px 221 | lower = top + 500 # 500px 222 | width = right - left 223 | height = lower - top 224 | 225 | # PIL like 226 | box = (left, top, right, lower) 227 | im = sct.grab(box) 228 | assert im.size == (width, height) 229 | 230 | # MSS like 231 | box2 = {"left": left, "top": top, "width": width, "height": height} 232 | im2 = sct.grab(box2) 233 | assert im.size == im2.size 234 | assert im.pos == im2.pos 235 | assert im.rgb == im2.rgb 236 | 237 | 238 | def test_thread_safety() -> None: 239 | """Regression test for issue #169.""" 240 | import threading 241 | import time 242 | 243 | def record(check: dict) -> None: 244 | """Record for one second.""" 245 | start_time = time.time() 246 | while time.time() - start_time < 1: 247 | with mss.mss() as sct: 248 | sct.grab(sct.monitors[1]) 249 | 250 | check[threading.current_thread()] = True 251 | 252 | checkpoint: dict = {} 253 | t1 = threading.Thread(target=record, args=(checkpoint,)) 254 | t2 = threading.Thread(target=record, args=(checkpoint,)) 255 | 256 | t1.start() 257 | time.sleep(0.5) 258 | t2.start() 259 | 260 | t1.join() 261 | t2.join() 262 | 263 | assert len(checkpoint) == 2 264 | -------------------------------------------------------------------------------- /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 | import pytest 6 | 7 | import mss 8 | 9 | tkinter = pytest.importorskip("tkinter") 10 | 11 | 12 | @pytest.fixture 13 | def root() -> tkinter.Tk: # type: ignore[name-defined] 14 | try: 15 | master = tkinter.Tk() 16 | except RuntimeError: 17 | pytest.skip(reason="tk.h version (8.5) doesn't match libtk.a version (8.6)") 18 | 19 | try: 20 | yield master 21 | finally: 22 | master.destroy() 23 | 24 | 25 | def take_screenshot() -> None: 26 | region = {"top": 370, "left": 1090, "width": 80, "height": 390} 27 | with mss.mss() as sct: 28 | sct.grab(region) 29 | 30 | 31 | def create_top_level_win(master: tkinter.Tk) -> None: # type: ignore[name-defined] 32 | top_level_win = tkinter.Toplevel(master) 33 | 34 | take_screenshot_btn = tkinter.Button(top_level_win, text="Take screenshot", command=take_screenshot) 35 | take_screenshot_btn.pack() 36 | 37 | take_screenshot_btn.invoke() 38 | master.update_idletasks() 39 | master.update() 40 | 41 | top_level_win.destroy() 42 | master.update_idletasks() 43 | master.update() 44 | 45 | 46 | def test_regression(root: tkinter.Tk, capsys: pytest.CaptureFixture) -> None: # type: ignore[name-defined] 47 | btn = tkinter.Button(root, text="Open TopLevel", command=lambda: create_top_level_win(root)) 48 | btn.pack() 49 | 50 | # First screenshot: it works 51 | btn.invoke() 52 | 53 | # Second screenshot: it should work too 54 | btn.invoke() 55 | 56 | # Check there were no exceptions 57 | captured = capsys.readouterr() 58 | assert not captured.out 59 | assert not captured.err 60 | -------------------------------------------------------------------------------- /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 os 6 | import platform 7 | from collections.abc import Callable 8 | 9 | import pytest 10 | 11 | import mss 12 | 13 | OS = platform.system().lower() 14 | PID = os.getpid() 15 | 16 | 17 | def get_opened_socket() -> int: 18 | """GNU/Linux: a way to get the opened sockets count. 19 | It will be used to check X server connections are well closed. 20 | """ 21 | import subprocess 22 | 23 | cmd = f"lsof -U | grep {PID}" 24 | output = subprocess.check_output(cmd, shell=True) 25 | return len(output.splitlines()) 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 | import ctypes 33 | 34 | PROCESS_QUERY_INFORMATION = 0x400 # noqa:N806 35 | GR_GDIOBJECTS = 0 # noqa:N806 36 | h = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, PID) 37 | return ctypes.windll.user32.GetGuiResources(h, GR_GDIOBJECTS) 38 | 39 | 40 | @pytest.fixture 41 | def monitor_func() -> Callable[[], int]: 42 | """OS specific function to check resources in use.""" 43 | return get_opened_socket if OS == "linux" else get_handles 44 | 45 | 46 | def bound_instance_without_cm() -> None: 47 | # Will always leak 48 | sct = mss.mss() 49 | sct.shot() 50 | 51 | 52 | def bound_instance_without_cm_but_use_close() -> None: 53 | sct = mss.mss() 54 | sct.shot() 55 | sct.close() 56 | # Calling .close() twice should be possible 57 | sct.close() 58 | 59 | 60 | def unbound_instance_without_cm() -> None: 61 | # Will always leak 62 | mss.mss().shot() 63 | 64 | 65 | def with_context_manager() -> None: 66 | with mss.mss() as sct: 67 | sct.shot() 68 | 69 | 70 | def regression_issue_128() -> None: 71 | """Regression test for issue #128: areas overlap.""" 72 | with mss.mss() as sct: 73 | area1 = {"top": 50, "left": 7, "width": 400, "height": 320, "mon": 1} 74 | sct.grab(area1) 75 | area2 = {"top": 200, "left": 200, "width": 320, "height": 320, "mon": 1} 76 | sct.grab(area2) 77 | 78 | 79 | def regression_issue_135() -> None: 80 | """Regression test for issue #135: multiple areas.""" 81 | with mss.mss() as sct: 82 | bounding_box_notes = {"top": 0, "left": 0, "width": 100, "height": 100} 83 | sct.grab(bounding_box_notes) 84 | bounding_box_test = {"top": 220, "left": 220, "width": 100, "height": 100} 85 | sct.grab(bounding_box_test) 86 | bounding_box_score = {"top": 110, "left": 110, "width": 100, "height": 100} 87 | sct.grab(bounding_box_score) 88 | 89 | 90 | def regression_issue_210() -> None: 91 | """Regression test for issue #210: multiple X servers.""" 92 | pyvirtualdisplay = pytest.importorskip("pyvirtualdisplay") 93 | 94 | with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(): 95 | pass 96 | 97 | with pyvirtualdisplay.Display(size=(1920, 1080), color_depth=24), mss.mss(): 98 | pass 99 | 100 | 101 | @pytest.mark.skipif(OS == "darwin", reason="No possible leak on macOS.") 102 | @pytest.mark.parametrize( 103 | "func", 104 | [ 105 | # bound_instance_without_cm, 106 | bound_instance_without_cm_but_use_close, 107 | # unbound_instance_without_cm, 108 | with_context_manager, 109 | regression_issue_128, 110 | regression_issue_135, 111 | regression_issue_210, 112 | ], 113 | ) 114 | def test_resource_leaks(func: Callable[[], None], monitor_func: Callable[[], int]) -> None: 115 | """Check for resource leaks with different use cases.""" 116 | # Warm-up 117 | func() 118 | 119 | original_resources = monitor_func() 120 | allocated_resources = 0 121 | 122 | for _ in range(5): 123 | func() 124 | new_resources = monitor_func() 125 | allocated_resources = max(allocated_resources, new_resources) 126 | 127 | assert allocated_resources <= original_resources 128 | -------------------------------------------------------------------------------- /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 | import os.path 6 | from datetime import datetime 7 | from pathlib import Path 8 | 9 | import pytest 10 | 11 | from mss import mss 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() -> None: 23 | with mss(display=os.getenv("DISPLAY")) as sct: 24 | assert list(sct.save(mon=0)) 25 | 26 | 27 | def test_files_exist() -> None: 28 | with mss(display=os.getenv("DISPLAY")) 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() -> 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(display=os.getenv("DISPLAY")) 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() -> None: 53 | with mss(display=os.getenv("DISPLAY")) 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() -> None: 60 | fmt = "sct-{top}x{left}_{width}x{height}.png" 61 | with mss(display=os.getenv("DISPLAY")) 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() -> None: 68 | fmt = "sct_{mon}-{date}.png" 69 | with mss(display=os.getenv("DISPLAY")) 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() -> None: 79 | fmt = "sct_{date:%Y-%m-%d}.png" 80 | with mss(display=os.getenv("DISPLAY")) 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 | -------------------------------------------------------------------------------- /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".split() 21 | WHEEL = "python -m build --wheel".split() 22 | CHECK = "twine check --strict".split() 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/opencv_numpy.py", 55 | f"mss-{__version__}/docs/source/examples/part_of_screen.py", 56 | f"mss-{__version__}/docs/source/examples/part_of_screen_monitor_2.py", 57 | f"mss-{__version__}/docs/source/examples/pil.py", 58 | f"mss-{__version__}/docs/source/examples/pil_pixels.py", 59 | f"mss-{__version__}/docs/source/index.rst", 60 | f"mss-{__version__}/docs/source/installation.rst", 61 | f"mss-{__version__}/docs/source/support.rst", 62 | f"mss-{__version__}/docs/source/usage.rst", 63 | f"mss-{__version__}/docs/source/where.rst", 64 | f"mss-{__version__}/pyproject.toml", 65 | f"mss-{__version__}/src/mss/__init__.py", 66 | f"mss-{__version__}/src/mss/__main__.py", 67 | f"mss-{__version__}/src/mss/base.py", 68 | f"mss-{__version__}/src/mss/darwin.py", 69 | f"mss-{__version__}/src/mss/exception.py", 70 | f"mss-{__version__}/src/mss/factory.py", 71 | f"mss-{__version__}/src/mss/linux.py", 72 | f"mss-{__version__}/src/mss/models.py", 73 | f"mss-{__version__}/src/mss/py.typed", 74 | f"mss-{__version__}/src/mss/screenshot.py", 75 | f"mss-{__version__}/src/mss/tools.py", 76 | f"mss-{__version__}/src/mss/windows.py", 77 | f"mss-{__version__}/src/tests/__init__.py", 78 | f"mss-{__version__}/src/tests/bench_bgra2rgb.py", 79 | f"mss-{__version__}/src/tests/bench_general.py", 80 | f"mss-{__version__}/src/tests/conftest.py", 81 | f"mss-{__version__}/src/tests/res/monitor-1024x768.raw.zip", 82 | f"mss-{__version__}/src/tests/test_bgra_to_rgb.py", 83 | f"mss-{__version__}/src/tests/test_cls_image.py", 84 | f"mss-{__version__}/src/tests/test_find_monitors.py", 85 | f"mss-{__version__}/src/tests/test_get_pixels.py", 86 | f"mss-{__version__}/src/tests/test_gnu_linux.py", 87 | f"mss-{__version__}/src/tests/test_implementation.py", 88 | f"mss-{__version__}/src/tests/test_issue_220.py", 89 | f"mss-{__version__}/src/tests/test_leaks.py", 90 | f"mss-{__version__}/src/tests/test_macos.py", 91 | f"mss-{__version__}/src/tests/test_save.py", 92 | f"mss-{__version__}/src/tests/test_setup.py", 93 | f"mss-{__version__}/src/tests/test_tools.py", 94 | f"mss-{__version__}/src/tests/test_windows.py", 95 | f"mss-{__version__}/src/tests/third_party/__init__.py", 96 | f"mss-{__version__}/src/tests/third_party/test_numpy.py", 97 | f"mss-{__version__}/src/tests/third_party/test_pil.py", 98 | ] 99 | 100 | 101 | def test_wheel() -> None: 102 | output = check_output(WHEEL, stderr=STDOUT, text=True) 103 | file = f"mss-{__version__}-py3-none-any.whl" 104 | assert f"Successfully built {file}" in output 105 | assert "warning" not in output.lower() 106 | 107 | check_call([*CHECK, f"dist/{file}"]) 108 | 109 | with ZipFile(f"dist/{file}") as fh: 110 | files = sorted(fh.namelist()) 111 | 112 | assert files == [ 113 | f"mss-{__version__}.dist-info/METADATA", 114 | f"mss-{__version__}.dist-info/RECORD", 115 | f"mss-{__version__}.dist-info/WHEEL", 116 | f"mss-{__version__}.dist-info/entry_points.txt", 117 | f"mss-{__version__}.dist-info/licenses/LICENSE.txt", 118 | "mss/__init__.py", 119 | "mss/__main__.py", 120 | "mss/base.py", 121 | "mss/darwin.py", 122 | "mss/exception.py", 123 | "mss/factory.py", 124 | "mss/linux.py", 125 | "mss/models.py", 126 | "mss/py.typed", 127 | "mss/screenshot.py", 128 | "mss/tools.py", 129 | "mss/windows.py", 130 | ] 131 | -------------------------------------------------------------------------------- /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 | import hashlib 6 | import os.path 7 | import zlib 8 | from pathlib import Path 9 | 10 | import pytest 11 | 12 | from mss import mss 13 | from mss.tools import to_png 14 | 15 | WIDTH = 10 16 | HEIGHT = 10 17 | MD5SUM = "ee1b645cc989cbfc48e613b395a929d3d79a922b77b9b38e46647ff6f74acef5" 18 | 19 | 20 | def test_bad_compression_level() -> None: 21 | with mss(compression_level=42, display=os.getenv("DISPLAY")) as sct, pytest.raises(zlib.error): 22 | sct.shot() 23 | 24 | 25 | def test_compression_level() -> None: 26 | data = b"rgb" * WIDTH * HEIGHT 27 | output = Path(f"{WIDTH}x{HEIGHT}.png") 28 | 29 | with mss(display=os.getenv("DISPLAY")) as sct: 30 | to_png(data, (WIDTH, HEIGHT), level=sct.compression_level, output=output) 31 | 32 | assert hashlib.sha256(output.read_bytes()).hexdigest() == MD5SUM 33 | 34 | 35 | @pytest.mark.parametrize( 36 | ("level", "checksum"), 37 | [ 38 | (0, "547191069e78eef1c5899f12c256dd549b1338e67c5cd26a7cbd1fc5a71b83aa"), 39 | (1, "841665ec73b641dfcafff5130b497f5c692ca121caeb06b1d002ad3de5c77321"), 40 | (2, "b11107163207f68f36294deb3f8e6b6a5a11399a532917bdd59d1d5f1117d4d0"), 41 | (3, "31278bad8c1c077c715ac4f3b497694a323a71a87c5ff8bdc7600a36bd8d8c96"), 42 | (4, "8f7237e1394d9ddc71fcb1fa4a2c2953087562ef6eac85d32d8154b61b287fb0"), 43 | (5, "83a55f161bad2d511b222dcd32059c9adf32c3238b65f9aa576f19bc0a6c8fec"), 44 | (6, "ee1b645cc989cbfc48e613b395a929d3d79a922b77b9b38e46647ff6f74acef5"), 45 | (7, "85f8d1b01cef926c111b194229bd6c01e2a65b18b4dd902293698e6de8f4e9ac"), 46 | (8, "85f8d1b01cef926c111b194229bd6c01e2a65b18b4dd902293698e6de8f4e9ac"), 47 | (9, "85f8d1b01cef926c111b194229bd6c01e2a65b18b4dd902293698e6de8f4e9ac"), 48 | ], 49 | ) 50 | def test_compression_levels(level: int, checksum: str) -> None: 51 | data = b"rgb" * WIDTH * HEIGHT 52 | raw = to_png(data, (WIDTH, HEIGHT), level=level) 53 | assert isinstance(raw, bytes) 54 | sha256 = hashlib.sha256(raw).hexdigest() 55 | assert sha256 == checksum 56 | 57 | 58 | def test_output_file() -> None: 59 | data = b"rgb" * WIDTH * HEIGHT 60 | output = Path(f"{WIDTH}x{HEIGHT}.png") 61 | to_png(data, (WIDTH, HEIGHT), output=output) 62 | 63 | assert output.is_file() 64 | assert hashlib.sha256(output.read_bytes()).hexdigest() == MD5SUM 65 | 66 | 67 | def test_output_raw_bytes() -> None: 68 | data = b"rgb" * WIDTH * HEIGHT 69 | raw = to_png(data, (WIDTH, HEIGHT)) 70 | assert isinstance(raw, bytes) 71 | assert hashlib.sha256(raw).hexdigest() == MD5SUM 72 | -------------------------------------------------------------------------------- /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/third_party/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoboTiG/python-mss/d7813b5d9794a73aaf01632955e9d98d49112d4e/src/tests/third_party/__init__.py -------------------------------------------------------------------------------- /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 | import os 6 | import os.path 7 | 8 | import pytest 9 | 10 | from mss import mss 11 | 12 | np = pytest.importorskip("numpy", reason="Numpy module not available.") 13 | 14 | 15 | def test_numpy() -> None: 16 | box = {"top": 0, "left": 0, "width": 10, "height": 10} 17 | with mss(display=os.getenv("DISPLAY")) as sct: 18 | img = np.array(sct.grab(box)) 19 | assert len(img) == 10 20 | -------------------------------------------------------------------------------- /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 | import os 7 | import os.path 8 | from pathlib import Path 9 | 10 | import pytest 11 | 12 | from mss import mss 13 | 14 | Image = pytest.importorskip("PIL.Image", reason="PIL module not available.") 15 | 16 | 17 | def test_pil() -> None: 18 | width, height = 16, 16 19 | box = {"top": 0, "left": 0, "width": width, "height": height} 20 | with mss(display=os.getenv("DISPLAY")) as sct: 21 | sct_img = sct.grab(box) 22 | 23 | img = Image.frombytes("RGB", sct_img.size, sct_img.rgb) 24 | assert img.mode == "RGB" 25 | assert img.size == sct_img.size 26 | 27 | for x, y in itertools.product(range(width), range(height)): 28 | assert img.getpixel((x, y)) == sct_img.pixel(x, y) 29 | 30 | output = Path("box.png") 31 | img.save(output) 32 | assert output.is_file() 33 | 34 | 35 | def test_pil_bgra() -> None: 36 | width, height = 16, 16 37 | box = {"top": 0, "left": 0, "width": width, "height": height} 38 | with mss(display=os.getenv("DISPLAY")) as sct: 39 | sct_img = sct.grab(box) 40 | 41 | img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX") 42 | assert img.mode == "RGB" 43 | assert img.size == sct_img.size 44 | 45 | for x, y in itertools.product(range(width), range(height)): 46 | assert img.getpixel((x, y)) == sct_img.pixel(x, y) 47 | 48 | output = Path("box-bgra.png") 49 | img.save(output) 50 | assert output.is_file() 51 | 52 | 53 | def test_pil_not_16_rounded() -> None: 54 | width, height = 10, 10 55 | box = {"top": 0, "left": 0, "width": width, "height": height} 56 | with mss(display=os.getenv("DISPLAY")) as sct: 57 | sct_img = sct.grab(box) 58 | 59 | img = Image.frombytes("RGB", sct_img.size, sct_img.rgb) 60 | assert img.mode == "RGB" 61 | assert img.size == sct_img.size 62 | 63 | for x, y in itertools.product(range(width), range(height)): 64 | assert img.getpixel((x, y)) == sct_img.pixel(x, y) 65 | 66 | output = Path("box.png") 67 | img.save(output) 68 | assert output.is_file() 69 | --------------------------------------------------------------------------------