├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── MANIFEST.in ├── Makefile ├── README.md ├── appveyor.yml ├── autopy └── __init__.py ├── docs ├── Makefile ├── _static │ ├── default.css │ └── toc.js ├── _templates │ ├── layout.html │ └── localtoc.html ├── alert.rst ├── bitmap.rst ├── color.rst ├── conf.py ├── index.rst ├── key.rst ├── mouse.rst ├── screen.rst └── scripts │ ├── __init__.py │ ├── autopy │ └── sphinx.py ├── examples ├── hello_world_alert.py ├── screengrab.py └── sine_mouse_wave.py ├── pyproject.toml ├── requirements.txt ├── rustfmt.toml ├── scripts ├── build-wheels.sh ├── mac ├── travis ├── upload ├── windows-setup.md └── windows.cmd ├── setup.py └── src ├── alert.rs ├── bitmap.rs ├── color.rs ├── internal.rs ├── key.rs ├── lib.rs ├── mouse.rs └── screen.rs /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.5.1 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: Maturin actions 7 | 8 | on: 9 | push: 10 | pull_request: 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | linux: 17 | runs-on: ${{ matrix.platform.runner }} 18 | strategy: 19 | matrix: 20 | platform: 21 | - runner: ubuntu-latest 22 | target: x86_64 23 | - runner: ubuntu-latest 24 | target: x86 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.10' 30 | - name: Build wheels 31 | uses: PyO3/maturin-action@v1 32 | with: 33 | target: ${{ matrix.platform.target }} 34 | args: --release --out dist --find-interpreter 35 | sccache: 'true' 36 | before-script-linux: | 37 | yum update 38 | yum install -y libXtst-devel 39 | manylinux: auto 40 | - name: Upload wheels 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: wheels-linux-${{ matrix.platform.target }} 44 | path: dist 45 | 46 | windows: 47 | runs-on: ${{ matrix.platform.runner }} 48 | strategy: 49 | matrix: 50 | platform: 51 | - runner: windows-latest 52 | target: x64 53 | - runner: windows-latest 54 | target: x86 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions/setup-python@v5 58 | with: 59 | python-version: '3.10' 60 | architecture: ${{ matrix.platform.target }} 61 | - name: Build wheels 62 | uses: PyO3/maturin-action@v1 63 | with: 64 | target: ${{ matrix.platform.target }} 65 | args: --release --out dist --find-interpreter 66 | sccache: 'true' 67 | - name: Upload wheels 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: wheels-windows-${{ matrix.platform.target }} 71 | path: dist 72 | 73 | macos: 74 | runs-on: ${{ matrix.platform.runner }} 75 | strategy: 76 | matrix: 77 | platform: 78 | - runner: macos-latest 79 | target: x86_64 80 | - runner: macos-14 81 | target: aarch64 82 | steps: 83 | - uses: actions/checkout@v4 84 | - uses: actions/setup-python@v5 85 | with: 86 | python-version: '3.10' 87 | - name: Build wheels 88 | uses: PyO3/maturin-action@v1 89 | with: 90 | target: ${{ matrix.platform.target }} 91 | args: --release --out dist --find-interpreter 92 | sccache: 'true' 93 | - name: Upload wheels 94 | uses: actions/upload-artifact@v4 95 | with: 96 | name: wheels-macos-${{ matrix.platform.target }} 97 | path: dist 98 | 99 | sdist: 100 | runs-on: ubuntu-latest 101 | steps: 102 | - uses: actions/checkout@v4 103 | - name: Build sdist 104 | uses: PyO3/maturin-action@v1 105 | with: 106 | command: sdist 107 | args: --out dist 108 | - name: Upload sdist 109 | uses: actions/upload-artifact@v4 110 | with: 111 | name: wheels-sdist 112 | path: dist 113 | 114 | release: 115 | name: Release 116 | runs-on: ubuntu-latest 117 | if: "startsWith(github.ref, 'refs/tags/')" 118 | needs: [linux, windows, macos, sdist] 119 | steps: 120 | - uses: actions/download-artifact@v4 121 | - name: Publish to PyPI 122 | uses: PyO3/maturin-action@v1 123 | env: 124 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 125 | with: 126 | command: upload 127 | args: --non-interactive --skip-existing wheels-*/* 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | .static_storage/ 67 | .media/ 68 | local_settings.py 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | comment_width = 79 2 | error_on_line_overflow = true 3 | format_strings = true 4 | wrap_comments = true 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - name: Linux 64-bit 5 | sudo: required 6 | services: 7 | - docker 8 | env: 9 | - CIBW_SKIP=*manylinux1_i686* 10 | - name: Linux 32-bit 11 | sudo: required 12 | services: 13 | - docker 14 | env: 15 | - CIBW_SKIP=*manylinux1_x86_64* 16 | - name: macOS 17 | os: osx 18 | language: generic 19 | env: 20 | global: 21 | - TWINE_USERNAME=michael.sanders 22 | # Note: TWINE_PASSWORD is set in Travis settings. 23 | 24 | script: 25 | - "pip install cibuildwheel==1.1.0 setuptools-rust==0.10.3" 26 | - export RUST_BACKTRACE=1 27 | - export CIBW_BEFORE_BUILD="pip install setuptools-rust==0.10.3 && source ./scripts/travis" 28 | - export CIBW_SKIP=cp34-*\ $CIBW_SKIP 29 | - export CIBW_ENVIRONMENT="CI=\"$CI\" TRAVIS_BRANCH=\"$TRAVIS_BRANCH\" TRAVIS_COMMIT=\"$TRAVIS_COMMIT\" PATH=\"\$HOME/.cargo/bin:\$PATH\"" 30 | - export CIBW_TEST_COMMAND="python -c 'import autopy'" 31 | - git fetch --unshallow 32 | - cibuildwheel --output-dir wheelhouse 33 | - | 34 | if [[ ! -z "$TRAVIS_TAG" ]]; then 35 | pip install twine 36 | python -m twine upload wheelhouse/*.whl 37 | elif [[ "$TRAVIS_BRANCH" = "master" ]] && [[ -z "$TRAVIS_PULL_REQUEST_SHA" ]]; then 38 | export TWINE_PASSWORD="$TWINE_TEST_PASSWORD" 39 | pip install twine 40 | python -m twine upload wheelhouse/*.whl --repository-url https://test.pypi.org/legacy/ 41 | fi 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 4.0.1 - 2025-03-29 9 | 10 | ### Changed 11 | 12 | - Updated PyO3 to 0.23.5, Cargo to Edition 2021, and all main dependencies. 13 | - Provided maturin build CI for linux/macos/windows environments producing respective artifacts. 14 | 15 | ## 4.0.0 - 2020-02-26 16 | 17 | ### Changed 18 | 19 | - Updated bitmap color functions to accept hex value rather than tuple. 20 | 21 | ## 3.1.0 - 2020-01-17 22 | 23 | ### Added 24 | 25 | - Added constant for tab key. 26 | 27 | ### Fixed 28 | 29 | - Updated `autopilot-rs` to 0.4.0, including the following fixes: 30 | - Fixed typing of apostrophe and quote characters on Linux. 31 | 32 | ### Changed 33 | 34 | - This project has been relicensed under the MIT or Apache 2.0 license. 35 | - Updated image, libc, rand, quickcheck, pkg-config, core-foundation, 36 | core-graphics, and cocoa crates to latest versions. 37 | 38 | ## 3.0.1 - 2019-11-12 39 | 40 | ### Added 41 | 42 | - Added support for Python 3.8. 43 | - Added support for setting modifier delay in `key.tap`. 44 | 45 | ### Changed 46 | 47 | - Updated modifiers param in `key.tap` and `key.toggle` to be optional (note 48 | that this is not a breaking change). 49 | 50 | ### Fixed 51 | 52 | - Updated `autopilot-rs` to 0.3.1, including the following fixes: 53 | - Fixed issue with point scaling in `mouse.location` and `mouse.move`. 54 | - Fixed typing of "^" character on Linux. 55 | - Fixed typing of "_" character on Linux. 56 | 57 | ## 3.0.0 - 2019-09-16 58 | 59 | ### Added 60 | 61 | - Added support for missing function keys F13 – F24. 62 | 63 | ### Changed 64 | 65 | - Updated color functions to return hex value rather than tuple. 66 | - Updated `autopilot-rs` to the latest version, including the following fixes: 67 | - Updated image, libc, rand, quickcheck, pkg-config, and cocoa crates to latest 68 | versions. 69 | 70 | ### Fixed 71 | 72 | - Fixed warnings of using try operator on latest nightly. 73 | 74 | ## 2.1.0 - 2019-05-15 75 | 76 | ### Changed 77 | 78 | - Updated to the latest version of pyo3. 79 | 80 | ## 2.0.0 - 2019-05-13 81 | 82 | ### Changed 83 | 84 | - Updated for latest Python versions and `cibuildwheel`. 85 | - Updated `autopilot-rs` to the latest version, including the following fixes: 86 | - Updated image, libc, scopeguard, quickcheck, pkg-config, core-foundation, 87 | core-graphics, and cocoa crates to latest versions. 88 | 89 | ### Removed 90 | 91 | - Removed handling of unsupported image formats. AutoPy now only supports 92 | direct saving of PNG, GIF, BMP and JPEG files. 93 | 94 | ## 1.1.1 - 2018-09-26 95 | 96 | ### Fixed 97 | 98 | - Updated scale factor on x11 to be rounded to the nearest hundredth. 99 | 100 | ## 1.1.0 - 2018-09-19 101 | 102 | ### Added 103 | 104 | - Added constant for spacebar key. 105 | - Added support for passing a delay into `mouse.click`. 106 | - Added constant for tab key. 107 | - Added support for passing a delay into `key.tap`. 108 | - Added support for faster typing with `key.type_string`. 109 | 110 | ### Changed 111 | 112 | - Updated `autopilot-rs` to the latest version, including the following fixes: 113 | - Updated Cocoa and other macOS dependencies. 114 | - Updated x11 dependency. 115 | - Updated function signatures with delay parameters to be consistent. 116 | - Updated `key.tap` delay to be passed through to modifier key toggles. 117 | - Updated `mouse.smooth_move` to accept a duration. 118 | 119 | ### Fixed 120 | 121 | - Fixed compilation error on 32-bit Linux. 122 | - Fixed compilation error on 32-bit Windows. 123 | - Fixed linux arrow keycode constant definitions. 124 | - Fixed colon showing up as semicolon on Windows. 125 | - Fixed `mouse.click` to release at end of function. 126 | 127 | ## 1.0.1 - 2018-05-01 128 | 129 | ### Fixed 130 | 131 | - Fixed packaging issue on Linux. 132 | 133 | ## 1.0.0 - 2018-04-30 134 | 135 | ### Added 136 | 137 | - Initial release of new fork. 138 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "autopy" 3 | version = "4.0.1" 4 | edition = "2021" 5 | authors = ["Michael Sanders "] 6 | 7 | [lib] 8 | ## Python extension modules should be compiled as 'cdylib' 9 | crate-type = ["cdylib"] 10 | name = "autopy" 11 | 12 | [dependencies.autopilot] 13 | version = "0.4.0" 14 | 15 | [dependencies.pyo3] 16 | version = "0.23.5" 17 | 18 | [dependencies.image] 19 | version = "0.22.4" 20 | 21 | [dependencies.either] 22 | version = "1.15.0" 23 | 24 | [dependencies.libc] 25 | version = "0.2.171" 26 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018, 2019 Michael Sanders. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2018, 2019 Michael Sanders. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include Cargo.toml 3 | include LICENSE-APACHE 4 | include LICENSE-MIT 5 | include requirements.txt 6 | recursive-include src * 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: ## Build debug target for development. 3 | rustup default nightly 4 | pip install -r requirements.txt 5 | python setup.py build 6 | 7 | .PHONY: help 8 | help: ## Print help information. 9 | @awk 'BEGIN { \ 10 | FS = ":.*?## " \ 11 | } /^[a-zA-Z_-]+:.*?## / { \ 12 | printf "\033[36m%-30s\033[0m %s\n", $$1, $$2 \ 13 | }' $(MAKEFILE_LIST) 14 | 15 | .PHONY: install 16 | install: ## Install local target. 17 | pip install . 18 | 19 | .PHONY: mac 20 | mac: ## Build wheel distributions for macOS. 21 | scripts/mac 22 | 23 | .PHONY: linux 24 | linux: ## Build wheel distributions for Linux. 25 | docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/scripts/build-wheels.sh 26 | 27 | .PHONY: upload 28 | upload: ## Upload binary wheel distributions. 29 | scripts/upload 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest PyPI version](https://img.shields.io/pypi/v/autopy.svg)](https://pypi.python.org/pypi/autopy/) 2 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/autopy.svg)](https://pypi.python.org/pypi/autopy/) 3 | [![Total downloads](https://pepy.tech/badge/autopy)](https://pepy.tech/project/autopy) 4 | 5 | [![Github Build Status](https://github.com/autopilot-rs/autopy/actions/workflows/build.yaml/badge.svg)](https://github.com/autopilot-rs/autopy/actions/workflows/build.yaml) 6 | [![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/2p5xap3tv4qkwsd1?svg=true)](https://ci.appveyor.com/project/msanders/autopy) 7 | 8 | AutoPy Introduction and Tutorial 9 | ================================= 10 | 11 | ## Introduction 12 | 13 | AutoPy is a simple, cross-platform GUI automation library for Python. It 14 | includes functions for controlling the keyboard and mouse, finding colors and 15 | bitmaps on-screen, and displaying alerts. 16 | 17 | Currently supported on macOS, Windows, and X11 with the XTest extension. 18 | 19 | ## Getting Started 20 | 21 | ### Requirements 22 | 23 | * Python 3.8 and onwards (for newer releases). 24 | * Rust 1.23.0-nightly 2019-02-06 or later (unless using a binary wheel 25 | distribution). 26 | * macOS 10.6 and up. 27 | * Windows 7 and up. 28 | * X11 with the XTest extension. 29 | 30 | ### Installation 31 | 32 | First, see if a binary wheel is available for your machine by running: 33 | 34 | $ pip install -U autopy 35 | 36 | If that fails, install [rustup](https://rustup.rs) and then run: 37 | 38 | $ rustup default nightly-2019-10-05 39 | $ pip install -U setuptools-rust 40 | $ pip install -U autopy 41 | 42 | Another option is to build from the latest source on the GitHub repository: 43 | 44 | $ git clone git://github.com/autopilot-rs/autopy-rs.git 45 | $ cd autopy 46 | $ python -m venv .env && source .env/bin/activate 47 | $ maturin develop 48 | 49 | Additional possibly outdated instructions for installing from source on Windows 50 | are available [here](https://github.com/autopilot-rs/autopy/blob/master/scripts/windows-setup.md). 51 | 52 | ### Hello World 53 | 54 | The following is the source for a "hello world" script in autopy. Running this 55 | code will cause an alert dialog to appear on every major platform: 56 | 57 | ```python 58 | import autopy 59 | 60 | 61 | def hello_world(): 62 | autopy.alert.alert("Hello, world") 63 | hello_world() 64 | ``` 65 | 66 | ![Cross platform alerts](https://github.com/autopilot-rs/autopy/raw/gh-pages/alerts.png) 67 | 68 | ## Tutorials 69 | 70 | ### Controlling the Mouse 71 | 72 | AutoPy includes a number of functions for controlling the mouse. For a full 73 | list, consult the [API 74 | Reference](https://www.autopy.org/documentation/api-reference/mouse.html). E.g., 75 | to immediately "teleport" the mouse to the top left corner of the screen: 76 | 77 | >>> import autopy 78 | >>> autopy.mouse.move(0, 0) 79 | 80 | To move the mouse a bit more realistically, we could use: 81 | 82 | >>> import autopy 83 | >>> autopy.mouse.smooth_move(0, 0) 84 | 85 | Even better, we could write our own function to move the mouse across the screen 86 | as a sine wave: 87 | 88 | ```python 89 | import autopy 90 | import math 91 | import time 92 | import random 93 | import sys 94 | 95 | TWO_PI = math.pi * 2.0 96 | 97 | 98 | def sine_mouse_wave(): 99 | """ 100 | Moves the mouse in a sine wave from the left edge of 101 | the screen to the right. 102 | """ 103 | width, height = autopy.screen.size() 104 | height /= 2 105 | height -= 10 # Stay in the screen bounds. 106 | 107 | for x in range(int(width)): 108 | y = int(height * math.sin((TWO_PI * x) / width) + height) 109 | autopy.mouse.move(x, y) 110 | time.sleep(random.uniform(0.001, 0.003)) 111 | 112 | 113 | sine_mouse_wave() 114 | ``` 115 | 116 | 117 | 118 | ### Controlling the Keyboard 119 | 120 | The following will enter the keys from the string "Hello, world!" in the 121 | currently focused input at 100 WPM: 122 | 123 | ```python 124 | import autopy 125 | 126 | 127 | autopy.key.type_string("Hello, world!", wpm=100) 128 | ``` 129 | 130 | Alternatively, individual keys can be entered using the following: 131 | 132 | ```python 133 | import autopy 134 | 135 | 136 | autopy.key.tap(autopy.key.Code.TAB, [autopy.key.Modifier.META]) 137 | autopy.key.tap("w", [autopy.key.Modifier.META]) 138 | ``` 139 | 140 | ### Working with Bitmaps 141 | 142 | All of autopy's bitmap routines can be found in the module `autopy.bitmap`. A 143 | useful way to explore autopy is to use Python's built-in `help()` function, for 144 | example in `help(autopy.bitmap.Bitmap)`. AutoPy's functions are documented with 145 | descriptive docstrings, so this should show a nice overview. 146 | 147 | >>> import autopy 148 | >>> autopy.bitmap.capture_screen() 149 | 150 | 151 | This takes a screenshot of the main screen, copies it to a bitmap, displays its 152 | memory address, and then immediately destroys it. Let's do something more 153 | useful, like look at its pixel data: 154 | 155 | >>> import autopy 156 | >>> autopy.bitmap.capture_screen().get_color(0, 0) 157 | 15921906 158 | 159 | AutoPy uses a coordinate system with its origin starting at the top-left, so 160 | this should return the color of pixel at the top-left corner of the screen. The 161 | number shown looks a bit unrecognizable, but we can format it with Python's 162 | built-in `hex` function: 163 | 164 | >>> import autopy 165 | >>> hex(autopy.bitmap.capture_screen().get_color(0, 0)) 166 | '0xF2F2F2' 167 | 168 | Alternatively, we can use: 169 | 170 | 171 | >>> import autopy 172 | >>> autopy.color.hex_to_rgb(autopy.screen.get_color(0, 0)) 173 | (242, 242, 242) 174 | 175 | which converts that hex value to a tuple of `(r, g, b)` values. (Note that 176 | `autopy.screen.get_color()`, used here, is merely a more convenient and 177 | efficient version of `autopy.bitmap.capture_screen().get_color()`.) 178 | 179 | To save the screen capture to a file, we can use: 180 | 181 | >>> import autopy 182 | >>> autopy.bitmap.capture_screen().save('screengrab.png') 183 | 184 | The filetype is either parsed automatically from the filename, or given as an 185 | optional parameter. Currently only jpeg and png files are supported. 186 | 187 | >>> import autopy 188 | >>> autopy.bitmap.Bitmap.open('needle.png') 189 | 190 | 191 | Aside from analyzing a bitmap's pixel data, the main use for loading a bitmap is 192 | finding it on the screen or inside another bitmap. For example, the following 193 | prints the coordinates of the first image found in a bitmap (scanned from left 194 | to right, top to bottom): 195 | 196 | ```python 197 | import autopy 198 | 199 | 200 | def find_image_example(): 201 | needle = autopy.bitmap.Bitmap.open('needle.png') 202 | haystack = autopy.bitmap.Bitmap.open('haystack.png') 203 | 204 | pos = haystack.find_bitmap(needle) 205 | if pos: 206 | print("Found needle at: %s" % str(pos)) 207 | 208 | find_image_example() 209 | ``` 210 | 211 | It's also possible to do a bounded search by passing a tuple `((x, y), (width, 212 | height))`: 213 | 214 | ```python 215 | haystack.find_bitmap(needle, rect=((10, 10), (100, 100))) 216 | ``` 217 | 218 | ## Projects using AutoPy 219 | 220 | - [AutoPyDriverServer](https://github.com/daluu/autopydriverserver), AutoPy 221 | through WebDriver or a webdriver-compatible server. 222 | - [guibot](https://github.com/intra2net/guibot), A tool for GUI automation using 223 | a variety of computer vision and desktop control backends. 224 | - [spynner](https://github.com/kiorky/spynner), Programmatic web browsing 225 | module with AJAX support for Python. 226 | - [SUMO](https://github.com/eclipse/sumo), An open source, highly portable, 227 | microscopic and continuous road traffic simulation package designed to handle 228 | large road networks. 229 | 230 | ## API Reference 231 | 232 | Hope you enjoy using autopy! For a more in depth overview, see the [API 233 | Reference](https://www.autopy.org/documentation/api-reference/). 234 | 235 | ## Contributing 236 | 237 | If you are interested in this project, please consider contributing. Here are a 238 | few ways you can help: 239 | 240 | - [Report issues](https://github.com/autopilot-rs/autopy/issues). 241 | - Fix bugs and [submit pull requests](https://github.com/autopilot-rs/autopy/pulls). 242 | - Write, clarify, or fix documentation. 243 | - Suggest or add new features. 244 | 245 | ## License 246 | 247 | This project is licensed under either the [Apache-2.0](LICENSE-APACHE) or 248 | [MIT](LICENSE-MIT) license, at your option. 249 | 250 | Unless you explicitly state otherwise, any contribution intentionally submitted 251 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 252 | dual licensed as above, without any additional terms or conditions. 253 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | TWINE_USERNAME: michael.sanders 3 | # Note: TWINE_PASSWORD is set in Appveyor settings. 4 | matrix: 5 | # Stable 64-bit MSVC 6 | - channel: stable 7 | target: x86_64-pc-windows-msvc 8 | CIBW_SKIP: "*win32* cp27-* cp33-* cp34-*" 9 | CIBW_BEFORE_BUILD: python -m pip install setuptools setuptools-rust==0.10.3 10 | # Stable 32-bit MSVC 11 | - channel: stable 12 | target: i686-pc-windows-msvc 13 | CIBW_SKIP: "*win_amd64* cp33-* cp34-*" 14 | CIBW_BEFORE_BUILD: python -m pip install setuptools setuptools-rust==0.10.3 15 | 16 | 17 | # From https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml 18 | install: 19 | - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 20 | - rustup-init -yv --default-toolchain nightly --default-host %target% 21 | - set PATH=%PATH%;%USERPROFILE%\.cargo\bin 22 | - rustc -vV 23 | - cargo -vV 24 | - python -m pip install -U pip 25 | - python -m pip install cibuildwheel==1.1.0 setuptools setuptools-rust==0.10.3 26 | 27 | build_script: 28 | - cibuildwheel --output-dir wheelhouse 29 | - > 30 | IF "%APPVEYOR_REPO_TAG%" == "true" 31 | ( 32 | python -m pip install twine 33 | && 34 | @python -m twine upload "wheelhouse/*.whl" --username %TWINE_USERNAME% --password %TWINE_PASSWORD% 35 | ) 36 | - > 37 | IF "%APPVEYOR_REPO_BRANCH%" == "master" 38 | ( 39 | IF [%APPVEYOR_PULL_REQUEST_HEAD_COMMIT%] == [] 40 | ( 41 | python -m pip install twine 42 | && 43 | @python -m twine upload "wheelhouse/*.whl" --repository-url https://test.pypi.org/legacy/ --username %TWINE_USERNAME% --password %TWINE_TEST_PASSWORD% 44 | ) 45 | ) 46 | 47 | artifacts: 48 | - path: "wheelhouse\\*.whl" 49 | name: Wheels 50 | -------------------------------------------------------------------------------- /autopy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | AutoPy is a simple, cross-platform GUI automation library for Python. 4 | """ 5 | 6 | from .autopy import alert, bitmap, color, key, mouse, screen 7 | 8 | __author__ = "Michael Sanders" 9 | __version__ = "4.0.1" 10 | __all__ = ["alert", "bitmap", "color", "key", "mouse", "screen"] 11 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python3 -msphinx 7 | SPHINXPROJ = AutoPy 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/default.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | 6 | div.documentwrapper, div.document, div.bodywrapper { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | background-color: #1c4e63; 12 | } 13 | 14 | div.sphinxsidebar { 15 | } 16 | 17 | div.bodywrapper { 18 | background: #fff; 19 | overflow: auto; 20 | } 21 | 22 | div.body { 23 | min-height: 100%; 24 | padding-bottom: 15px; 25 | } 26 | 27 | dl.attribute .property { 28 | display: none; 29 | } 30 | 31 | dl.attribute { 32 | line-height: 100%; 33 | padding: 0; 34 | margin: 0; 35 | } 36 | 37 | .toctree-wrapper > ul { 38 | list-style-type: none; 39 | padding-left: 0; 40 | } 41 | 42 | .function > dt, .attribute > dt, .method > dt, .property > dt, .classmethod > dt, .class > dt { 43 | font-size: 16px; 44 | } 45 | 46 | code.descname, code.descclassname { 47 | font-size: inherit; 48 | } 49 | 50 | .function > dd { 51 | padding: 0; 52 | margin: 1em 0 0 0; 53 | } 54 | 55 | .attribute > dd, .method > dd, .property > dd, .classmethod > dd, .class > dd { 56 | padding: 0; 57 | margin-top: 10px; 58 | margin-left: 20px; 59 | } 60 | 61 | span.pre { 62 | font-family: Menlo, "Panic Sans", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace; 63 | font-size: 95%; 64 | } 65 | 66 | code.literal { 67 | background: transparent; 68 | font-weight: normal; 69 | } 70 | 71 | .docutils dd > ul li { 72 | margin: 0.25em 0 0 20px; 73 | list-style: square inside; 74 | } 75 | 76 | .docutils dd, .docutils dd > ul { 77 | margin-left: 0; 78 | padding-left: 0; 79 | } 80 | 81 | .section#functions > blockquote { 82 | padding: 0; 83 | margin: 0; 84 | } 85 | -------------------------------------------------------------------------------- /docs/_static/toc.js: -------------------------------------------------------------------------------- 1 | // Adopted from https://stackoverflow.com/a/12459801. 2 | $(function () { 3 | "use strict"; 4 | function headerID(title) { 5 | return "#" + title.toLowerCase().replace(/\s+/g, "-"); 6 | } 7 | 8 | function onlyText(e) { 9 | return e.clone().children().remove().end().text(); 10 | } 11 | 12 | function addSectionContents(section, list) { 13 | if (section.children("blockquote").length > 0) { 14 | section = section.find("blockquote > div").first(); 15 | } 16 | 17 | var classes = [ 18 | "class", 19 | "classmethod", 20 | "attribute", 21 | "method", 22 | "property", 23 | "function", 24 | ]; 25 | 26 | classes.forEach(function (cls) { 27 | var dl = section.children("dl." + cls); 28 | var children = dl.children("dt"); 29 | if (children.length === 0) { 30 | return; 31 | } 32 | 33 | var ul = $("
    "); 34 | children.each(function (_, child) { 35 | var n = $(child).children('.descname').clone(); 36 | var a = $("").attr("href", 37 | $(child).children('.headerlink').attr("href") 38 | ); 39 | 40 | var li = $("
  • ").append(a.append(n)); 41 | addSectionContents($(child).parent().children("dd"), li); 42 | ul.append(li); 43 | }); 44 | 45 | list.append(ul); 46 | }); 47 | } 48 | 49 | function generateContents() { 50 | var currentIndex = $('li.current > ul.simple'); 51 | $(":header > a.headerlink").each(function (_, header) { 52 | if ($(header).parents(":header").prop("tagName") === "H1") { 53 | return; 54 | } 55 | 56 | var section = $(header).parents(".section").first(); 57 | var title = onlyText($(header).parent()); 58 | var li = $("
  • "); 59 | li.append($("").attr("href", headerID(title)).text(title)); 60 | addSectionContents(section, li); 61 | currentIndex.append(li); 62 | }); 63 | } 64 | 65 | generateContents(); 66 | }); 67 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {# layout.html #} 2 | {# Import the theme's layout. #} 3 | {% extends "!layout.html" %} 4 | 5 | {% block rootrellink %} 6 | {{ shorttitle }} {{ reldelim1 }} API Reference 7 | {% endblock %} 8 | {%- set rellinks = rellinks|filternav -%} 9 | {% block relbar2 %}{% endblock %} 10 | {% block footer %} {% endblock %} 11 | 12 | {% set css_files = css_files + ['_static/default.css'] %} 13 | -------------------------------------------------------------------------------- /docs/_templates/localtoc.html: -------------------------------------------------------------------------------- 1 | {%- if display_toc %} 2 |

    {{ _('Table Of Contents') }}

    3 | {{ toc }} 4 | {%- endif %} 5 | 6 | {# From https://stackoverflow.com/a/14411680 #} 7 | 8 | -------------------------------------------------------------------------------- /docs/alert.rst: -------------------------------------------------------------------------------- 1 | alert — autopy module for displaying alerts 2 | =========================================== 3 | 4 | .. automodule:: autopy.alert 5 | 6 | Functions 7 | ----------------------------- 8 | .. automodule:: autopy.alert 9 | 10 | .. autofunction:: alert(msg: str, title: str=None, default_button: str=None, cancel_button: str=None) 11 | -------------------------------------------------------------------------------- /docs/bitmap.rst: -------------------------------------------------------------------------------- 1 | bitmap — autopy module for working with bitmaps 2 | =============================================== 3 | 4 | .. automodule:: autopy.bitmap 5 | 6 | Bitmap Object Methods 7 | ----------------------------- 8 | .. autoclass:: Bitmap 9 | :member-order: bysource 10 | 11 | .. automethod:: save(path: str, format: str=None) 12 | .. automethod:: copy_to_pasteboard() 13 | .. automethod:: point_in_bounds(x: float, y: float) -> bool 14 | .. automethod:: rect_in_bounds(rect: Tuple[Tuple[float, float], Tuple[float, float]]) -> bool 15 | .. automethod:: open(path: str) -> Bitmap 16 | .. automethod:: get_color(x: float, y: float) -> Tuple[int, int, int] 17 | .. automethod:: find_color(color: Tuple[int, int, int], tolerance: float=None, rect: Tuple[Tuple[float, float], Tuple[float, float]]=None, start_point: Tuple[float, float]=None) -> Tuple[float, float] 18 | .. automethod:: find_every_color(color: Tuple[int, int, int], tolerance: float=None, rect: Tuple[Tuple[float, float], Tuple[float, float]]=None, start_point: Tuple[float, float]=None) -> List[Tuple[float, float]] 19 | .. automethod:: count_of_color(color: Tuple[int, int, int], tolerance: float=None, rect: Tuple[Tuple[float, float], Tuple[float, float]]=None, start_point: Tuple[float, float]=None) -> int 20 | .. automethod:: find_bitmap(needle: Bitmap, tolerance: float=None, rect: Tuple[Tuple[float, float], Tuple[float, float]]=None, start_point: Tuple[float, float]=None) -> Tuple[float, float] 21 | .. automethod:: find_every_bitmap(needle: Bitmap, tolerance: float=None, rect: Tuple[Tuple[float, float], Tuple[float, float]]=None, start_point: Tuple[float, float]=None) -> [Tuple[float, float]] 22 | .. automethod:: count_of_bitmap(needle: Bitmap, tolerance: float=None, rect: Tuple[Tuple[float, float], Tuple[float, float]]=None, start_point: Tuple[float, float]=None) -> int 23 | .. automethod:: cropped(rect: Tuple[Tuple[float, float], Tuple[float, float]]) -> Bitmap 24 | .. automethod:: is_bitmap_equal(bitmap: Bitmap, tolerance: float=None) -> bool 25 | 26 | Functions 27 | ----------------------------- 28 | .. automodule:: autopy.bitmap 29 | 30 | .. autofunction:: capture_screen(rect: Tuple[Tuple[float, float], Tuple[float, float]]) -> autopy.bitmap.Bitmap 31 | -------------------------------------------------------------------------------- /docs/color.rst: -------------------------------------------------------------------------------- 1 | color — autopy module for working with colors 2 | ============================================= 3 | 4 | .. automodule:: autopy.color 5 | 6 | Functions 7 | ----------------------------- 8 | .. automodule:: autopy.color 9 | 10 | .. autofunction:: rgb_to_hex(red: int, green: int, blue: int) -> int 11 | .. autofunction:: hex_to_rgb(hex: int) -> Tuple[int, int, int] 12 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # AutoPy documentation build configuration file, created by 5 | # sphinx-quickstart on Tue May 5 21:44:11 2015. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | import shlex 19 | 20 | sys.path.append(os.path.abspath(os.path.join(__file__, ".."))) 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | sys.path.insert(0, os.path.abspath('.')) 25 | from scripts.sphinx import Sphinx 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | #'sphinx.ext.intersphinx', 38 | #'sphinx.ext.coverage', 39 | #'sphinxcontrib.fulltoc', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'AutoPy' 58 | copyright = '2018, Michael Sanders' 59 | author = 'Michael Sanders' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '1.0' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '1.0' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | #today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | #today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ['_build'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | #default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | #add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | #show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | #modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | #keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = False 113 | module ="test" 114 | 115 | 116 | # -- Options for HTML output ---------------------------------------------- 117 | 118 | # The theme to use for HTML and HTML Help pages. See the documentation for 119 | # a list of builtin themes. 120 | html_theme = 'classic' 121 | 122 | # Theme options are theme-specific and customize the look and feel of a theme 123 | # further. For a list of options available for each theme, see the 124 | # documentation. 125 | html_theme_options = { 126 | "relbarbgcolor": "#11303d" 127 | } 128 | 129 | # Add any paths that contain custom themes here, relative to this directory. 130 | #html_theme_path = [] 131 | 132 | # The name for this set of Sphinx documents. If None, it defaults to 133 | # " v documentation". 134 | #html_title = None 135 | 136 | # A shorter title for the navigation bar. Default is the same as html_title. 137 | html_short_title = "AutoPy Documentation" 138 | 139 | # The name of an image file (relative to this directory) to place at the top 140 | # of the sidebar. 141 | #html_logo = None 142 | 143 | # The name of an image file (within the static path) to use as favicon of the 144 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 145 | # pixels large. 146 | #html_favicon = None 147 | 148 | # Add any paths that contain custom static files (such as style sheets) here, 149 | # relative to this directory. They are copied after the builtin static files, 150 | # so a file named "default.css" will overwrite the builtin "default.css". 151 | html_static_path = ['_static'] 152 | 153 | # Add any extra paths that contain custom files (such as robots.txt or 154 | # .htaccess) here, relative to this directory. These files are copied 155 | # directly to the root of the documentation. 156 | #html_extra_path = [] 157 | 158 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 159 | # using the given strftime format. 160 | html_last_updated_fmt = None 161 | 162 | # If true, SmartyPants will be used to convert quotes and dashes to 163 | # typographically correct entities. 164 | #html_use_smartypants = True 165 | 166 | # Custom sidebar templates, maps document names to template names. 167 | html_sidebars = {'*': ['localtoc.html', 'relations.html', 'sourcelink.html']} 168 | 169 | # Additional templates that should be rendered to pages, maps page names to 170 | # template names. 171 | #html_additional_pages = {} 172 | 173 | # If false, no module index is generated. 174 | #html_domain_indices = True 175 | 176 | # If false, no index is generated. 177 | html_use_index = False 178 | 179 | # If true, the index is split into individual pages for each letter. 180 | #html_split_index = False 181 | 182 | # If true, links to the reST sources are added to the pages. 183 | html_show_sourcelink = False 184 | 185 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 186 | html_show_sphinx = False 187 | 188 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 189 | html_show_copyright = False 190 | 191 | # If true, an OpenSearch description file will be output, and all pages will 192 | # contain a tag referring to it. The value of this option must be the 193 | # base URL from which the finished HTML is served. 194 | #html_use_opensearch = '' 195 | 196 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 197 | #html_file_suffix = None 198 | 199 | # Language to be used for generating the HTML full-text search index. 200 | # Sphinx supports the following languages: 201 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 202 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 203 | #html_search_language = 'en' 204 | 205 | # A dictionary with options for the search language support, empty by default. 206 | # Now only 'ja' uses this config value 207 | #html_search_options = {'type': 'default'} 208 | 209 | # The name of a javascript file (relative to the configuration directory) that 210 | # implements a search results scorer. If empty, the default will be used. 211 | #html_search_scorer = 'scorer.js' 212 | 213 | # Output file base name for HTML help builder. 214 | htmlhelp_basename = 'AutoPydoc' 215 | 216 | # -- Options for LaTeX output --------------------------------------------- 217 | 218 | latex_elements = { 219 | # The paper size ('letterpaper' or 'a4paper'). 220 | #'papersize': 'letterpaper', 221 | 222 | # The font size ('10pt', '11pt' or '12pt'). 223 | #'pointsize': '10pt', 224 | 225 | # Additional stuff for the LaTeX preamble. 226 | #'preamble': '', 227 | 228 | # Latex figure (float) alignment 229 | #'figure_align': 'htbp', 230 | } 231 | 232 | # Grouping the document tree into LaTeX files. List of tuples 233 | # (source start file, target name, title, 234 | # author, documentclass [howto, manual, or own class]). 235 | latex_documents = [ 236 | (master_doc, 'AutoPy.tex', 'AutoPy Documentation', 237 | 'Michael Sanders', 'manual'), 238 | ] 239 | 240 | # The name of an image file (relative to this directory) to place at the top of 241 | # the title page. 242 | #latex_logo = None 243 | 244 | # For "manual" documents, if this is true, then toplevel headings are parts, 245 | # not chapters. 246 | #latex_use_parts = False 247 | 248 | # If true, show page references after internal links. 249 | #latex_show_pagerefs = False 250 | 251 | # If true, show URL addresses after external links. 252 | #latex_show_urls = False 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #latex_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #latex_domain_indices = True 259 | 260 | 261 | # -- Options for manual page output --------------------------------------- 262 | 263 | # One entry per manual page. List of tuples 264 | # (source start file, name, description, authors, manual section). 265 | man_pages = [ 266 | (master_doc, 'autopy', 'AutoPy Documentation', 267 | [author], 1) 268 | ] 269 | 270 | # If true, show URL addresses after external links. 271 | #man_show_urls = False 272 | 273 | 274 | # -- Options for Texinfo output ------------------------------------------- 275 | 276 | # Grouping the document tree into Texinfo files. List of tuples 277 | # (source start file, target name, title, author, 278 | # dir menu entry, description, category) 279 | texinfo_documents = [ 280 | (master_doc, 'AutoPy', 'AutoPy Documentation', 281 | author, 'AutoPy','A simple, cross-platform GUI automation toolkit for Python.', 282 | 'Miscellaneous'), 283 | ] 284 | 285 | # Documents to append as an appendix to all manuals. 286 | #texinfo_appendices = [] 287 | 288 | # If false, no module index is generated. 289 | #texinfo_domain_indices = True 290 | 291 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 292 | #texinfo_show_urls = 'footnote' 293 | 294 | # If true, do not generate a @detailmenu in the "Top" node's menu. 295 | #texinfo_no_detailmenu = False 296 | 297 | 298 | # Example configuration for intersphinx: refer to the Python standard library. 299 | intersphinx_mapping = {'https://docs.python.org/': None} 300 | 301 | def setup(app): 302 | Sphinx(app).setup() 303 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | autopy — API Reference 2 | ================================== 3 | 4 | .. automodule:: autopy 5 | 6 | 7 | Table Of Contents 8 | ------------------ 9 | .. toctree:: 10 | :titlesonly: 11 | :maxdepth: 20 12 | :glob: 13 | 14 | alert 15 | bitmap 16 | color 17 | key 18 | mouse 19 | screen 20 | -------------------------------------------------------------------------------- /docs/key.rst: -------------------------------------------------------------------------------- 1 | key — autopy module for working with the keyboard 2 | ================================================= 3 | 4 | .. currentmodule:: autopy.key 5 | .. automodule:: autopy.key 6 | 7 | Functions 8 | ----------------------------- 9 | .. automodule:: autopy.key 10 | 11 | .. autofunction:: toggle(key: Any, down: bool, modifiers: List[Modifier]=[], modifier_delay: float=None) 12 | .. autofunction:: tap(key: Any, modifiers: List[Modifier]=[], delay: float=None) 13 | .. autofunction:: type_string(string: str, wpm: float=None) 14 | 15 | 16 | Constants 17 | --------- 18 | 19 | .. py:class:: Code 20 | 21 | :code: F1 22 | :code: F2 23 | :code: F3 24 | :code: F4 25 | :code: F5 26 | :code: F6 27 | :code: F7 28 | :code: F8 29 | :code: F9 30 | :code: F10 31 | :code: F11 32 | :code: F12 33 | :code: F13 34 | :code: F14 35 | :code: F15 36 | :code: F16 37 | :code: F17 38 | :code: F18 39 | :code: F19 40 | :code: F20 41 | :code: F21 42 | :code: F22 43 | :code: F23 44 | :code: F24 45 | :code: ALT 46 | :code: BACKSPACE 47 | :code: CAPS_LOCK 48 | :code: CONTROL 49 | :code: DELETE 50 | :code: DOWN_ARROW 51 | :code: END 52 | :code: ESCAPE 53 | :code: HOME 54 | :code: LEFT_ARROW 55 | :code: META 56 | :code: PAGE_DOWN 57 | :code: PAGE_UP 58 | :code: RETURN 59 | :code: RIGHT_ARROW 60 | :code: SHIFT 61 | :code: SPACE 62 | :code: UP_ARROW 63 | 64 | .. class:: Modifier 65 | 66 | :modifier: META 67 | :modifier: ALT 68 | :modifier: CONTROL 69 | :modifier: SHIFT 70 | -------------------------------------------------------------------------------- /docs/mouse.rst: -------------------------------------------------------------------------------- 1 | mouse — autopy module for working with the mouse 2 | ================================================ 3 | 4 | .. automodule:: autopy.mouse 5 | 6 | Functions 7 | ----------------------------- 8 | .. automodule:: autopy.mouse 9 | 10 | .. autofunction:: location() -> (float, float) 11 | .. autofunction:: toggle(button: Button=None, down: bool) 12 | .. autofunction:: click(button: Button=None, delay: float=None) 13 | .. autofunction:: move(x: float, y: float) 14 | .. autofunction:: smooth_move(x: float, y: float) 15 | 16 | Constants 17 | --------- 18 | 19 | .. class:: Button 20 | 21 | :Button: LEFT 22 | :Button: RIGHT 23 | :Button: MIDDLE 24 | -------------------------------------------------------------------------------- /docs/screen.rst: -------------------------------------------------------------------------------- 1 | screen — autopy module for working with the screen 2 | =================================================== 3 | 4 | .. automodule:: autopy.screen 5 | 6 | Functions 7 | ----------------------------- 8 | .. automodule:: autopy.screen 9 | 10 | .. autofunction:: scale() -> float 11 | .. autofunction:: size() -> (float, float) 12 | .. autofunction:: is_point_visible(x: float, y: float) -> bool 13 | .. autofunction:: get_color(x: float, y: float) -> (int, int, int) 14 | -------------------------------------------------------------------------------- /docs/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from . import sphinx 2 | 3 | __all__ = ["sphinx"] 4 | -------------------------------------------------------------------------------- /docs/scripts/autopy: -------------------------------------------------------------------------------- 1 | ../../autopy -------------------------------------------------------------------------------- /docs/scripts/sphinx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import autopy 3 | import inspect 4 | import re 5 | import sys 6 | 7 | 8 | class DocstringMarkdownParser(object): 9 | """ 10 | This is a simple wrapper that lets us write human-readable docstrings as 11 | Markdown without sacrificing documentation quality. 12 | """ 13 | def __init__(self, modules: list): 14 | self.modules = modules 15 | 16 | @property 17 | def members(self) -> dict: 18 | modules = [__builtins__] + [sys.modules[name] for name in self.modules] 19 | return _getmembers(modules) 20 | 21 | @property 22 | def functions(self) -> list: 23 | return [x for x, y in self.members.items() if inspect.isfunction(y)] 24 | 25 | @property 26 | def classes(self) -> list: 27 | return [x for x, y in self.members.items() if inspect.isclass(y)] 28 | 29 | @property 30 | def tokens(self) -> list: 31 | members = inspect.getmembers(__builtins__) 32 | return [x for x, y in members if not inspect.isfunction(y) and 33 | not inspect.isclass(y)] 34 | 35 | @property 36 | def methods(self) -> list: 37 | classes = (y for x, y in self.members.items() if inspect.isclass(y)) 38 | return _getmembers(classes) 39 | 40 | @staticmethod 41 | def _mono_re(pattern: str): 42 | return re.compile(r'`(%s)`' % pattern, re.MULTILINE) 43 | 44 | @property 45 | def token_re(self) -> object: 46 | return self._mono_re(_re_keywords(self.tokens)) 47 | 48 | @property 49 | def function_re(self) -> object: 50 | return self._mono_re(_re_keywords(self.functions) + 51 | _re_keywords(x + "()" for x in self.functions)) 52 | 53 | @property 54 | def class_re(self) -> object: 55 | return self._mono_re(_re_keywords(self.classes)) 56 | 57 | @property 58 | def module_re(self) -> object: 59 | return self._mono_re(_re_keywords(self.modules)) 60 | 61 | def sub_method(self, matchobj: object) -> str: 62 | name = matchobj.group(1) 63 | if name.endswith("()"): 64 | name = name[:-2] 65 | method = self.methods.get(name) 66 | if method is None: 67 | return matchobj.group(0) 68 | 69 | fmt = r':attr:`%s`' if isinstance(method, property) else r':meth:`%s`' 70 | return fmt % name 71 | 72 | def transform_docstring(self, 73 | docstr: str, 74 | name: str, 75 | what: str, 76 | obj: object) -> str: 77 | # Tokenize keywords. 78 | docstr = self.token_re.sub(r':token:`\1`', docstr) 79 | 80 | # Automatically insert attributes for local hyperlinking. 81 | docstr = self.function_re.sub(r':func:`\1`', docstr) 82 | docstr = self.class_re.sub(r':class:`\1`', docstr) 83 | docstr = self.module_re.sub(r':module:`\1`', docstr) 84 | if what in ("method", "class", "attribute"): 85 | docstr = self._mono_re(r'[\w()]+').sub(self.sub_method, docstr) 86 | 87 | # Treat `text` as monospace (like Markdown). 88 | docstr = re.sub(r'([^:]|^)`(.*?)`', r'\1``\2``', docstr, 89 | flags=re.MULTILINE | re.DOTALL) 90 | 91 | # Automatically italicize "Exceptions:" 92 | docstr = re.sub(r'(\b)(Exceptions:)', r'\1`\2`', docstr) 93 | 94 | # Automatically format code blocks. 95 | docstr = re.sub("(\s*\n)+((\n?( |\t)(\w.*))+)", 96 | "\n::\n" + r'\2', docstr, re.MULTILINE) 97 | 98 | return docstr 99 | 100 | 101 | class Sphinx(object): 102 | def __init__(self, app): 103 | self.app = app 104 | self.modules = ["autopy.%s" % x for x in autopy.__all__] 105 | self.nodoc = set() 106 | self.parser = DocstringMarkdownParser(self.modules) 107 | 108 | def autodoc_process_docstring(self, app: str, what: str, name: str, 109 | obj: object, options: dict, lines: list): 110 | if name not in self.nodoc: 111 | if what == 'module': 112 | self.nodoc.add(name) 113 | 114 | lines[:] = self.parser.transform_docstring( 115 | "\n".join(lines), name, what, obj 116 | ).split("\n") 117 | else: 118 | del lines[:] 119 | 120 | def autodoc_process_signature(self, app: str, what: str, name: str, 121 | obj: object, options: dict, signature: str, 122 | return_annotation: str): 123 | module = inspect.getmodule(obj) 124 | if signature and module: 125 | # Remove superfluous module name from type signature. 126 | signature = re.sub(r'%s\.(\w+)' % re.escape(module.__name__), 127 | r'\1', signature) 128 | return signature, return_annotation 129 | 130 | # See http://sphinx-doc.org/templating.html#rellinks. 131 | def filternav(self, rellinks: list) -> list: 132 | # Remove useless navigation. 133 | rellinks = [x for x in rellinks if str(x[3]) != "modules"] 134 | rellinks.append(("index", "Index", "index", "index")) 135 | 136 | # Sort previous < index < next. 137 | order = {"previous": 2, "index": 1, "next": 0} 138 | rellinks = sorted(rellinks, key=lambda x: order.get(str(x[3]), -1)) 139 | return rellinks 140 | 141 | def add_jinja_filters(self, app): 142 | app.builder.templates.environment.filters['filternav'] = self.filternav 143 | 144 | def setup(self): 145 | self.app.connect('autodoc-process-docstring', 146 | self.autodoc_process_docstring) 147 | self.app.connect('autodoc-process-signature', 148 | self.autodoc_process_signature) 149 | self.app.connect('builder-inited', self.add_jinja_filters) 150 | 151 | 152 | def _re_keywords(words: list) -> str: 153 | return "|".join(map(re.escape, words)) 154 | 155 | 156 | def _getmembers(objects: list) -> dict: 157 | return dict(sum((inspect.getmembers(x) for x in objects), [])) 158 | -------------------------------------------------------------------------------- /examples/hello_world_alert.py: -------------------------------------------------------------------------------- 1 | import autopy 2 | 3 | autopy.alert.alert("Hello, world!") 4 | -------------------------------------------------------------------------------- /examples/screengrab.py: -------------------------------------------------------------------------------- 1 | import autopy 2 | 3 | autopy.bitmap.capture_screen().save("screenshot.png") 4 | -------------------------------------------------------------------------------- /examples/sine_mouse_wave.py: -------------------------------------------------------------------------------- 1 | import autopy 2 | import math 3 | import random 4 | import time 5 | 6 | TWO_PI = math.pi * 2.0 7 | 8 | 9 | def sine_mouse_wave(): 10 | """ 11 | Moves mouse in a sine wave from the left edge of the screen to the right. 12 | """ 13 | width, height = autopy.screen.size() 14 | height /= 2 15 | height -= 10 # Stay in the screen bounds. 16 | 17 | for x in range(int(width)): 18 | y = round(height * math.sin((TWO_PI * x) / width) + height) 19 | autopy.mouse.move(float(x), float(y)) 20 | time.sleep(random.uniform(0.001, 0.003)) 21 | 22 | 23 | sine_mouse_wave() 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.5,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "autopy" 7 | requires-python = ">=3.8" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | dynamic = ["version"] 14 | [tool.maturin] 15 | bindings = "pyo3" 16 | features = ["pyo3/extension-module"] 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools-rust 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | normalize_comments = true 2 | -------------------------------------------------------------------------------- /scripts/build-wheels.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Modified from 3 | # https://github.com/PyO3/setuptools-rust/blob/5adb24/example/build-wheels.sh 4 | # https://pyo3.github.io/pyo3/guide/distribution.html#binary-wheel-distribution 5 | set -e -x 6 | 7 | yum install -y gpg libXtst libXtst-devel libXext libXext-devel 8 | 9 | mkdir ~/rust-installer 10 | curl -sL https://static.rust-lang.org/rustup.sh -o ~/rust-installer/rustup.sh 11 | sh ~/rust-installer/rustup.sh --prefix=~/rust --channel=nightly -y --disable-sudo 12 | export PATH="$HOME/rust/bin:$PATH" 13 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$HOME/rust/lib" 14 | 15 | # Compile wheels 16 | for PYBIN in /opt/python/cp{27,35,36,37}*/bin; do 17 | export PYTHON_SYS_EXECUTABLE="$PYBIN/python" 18 | export PYTHON_LIB=$(${PYBIN}/python -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))") 19 | export LIBRARY_PATH="$LIBRARY_PATH:$PYTHON_LIB" 20 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$PYTHON_LIB" 21 | "${PYBIN}/pip" install -U setuptools setuptools-rust wheel 22 | "${PYBIN}/pip" wheel /io/ -w /io/dist/ 23 | done 24 | 25 | # Bundle external shared libraries into the wheels 26 | for whl in /io/dist/*.whl; do 27 | auditwheel repair "$whl" -w /io/dist/ 28 | done 29 | 30 | # Install packages and test 31 | for PYBIN in /opt/python/cp{27,35,36,37}*/bin/; do 32 | "${PYBIN}/pip" install autopy --no-index -f /io/dist/ 33 | "${PYBIN}/python" -c 'import autopy' 34 | done 35 | -------------------------------------------------------------------------------- /scripts/mac: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | 4 | build_wheel() { 5 | set -x 6 | python --version 7 | pip install -U pip wheel 8 | pip install -U -r requirements.txt 9 | python setup.py clean 10 | python setup.py bdist_wheel 11 | pip uninstall -y autopy 12 | pip install autopy --no-index -f dist 13 | pushd /tmp 14 | python -c "import autopy" 15 | popd 16 | set +x 17 | } 18 | 19 | eval "$(pyenv init -)" 20 | rustup default nightly 21 | 22 | pyenv shell 2.7.14 23 | build_wheel 24 | 25 | pyenv shell 3.7.0 26 | build_wheel 27 | 28 | pyenv shell 3.6.5 29 | build_wheel 30 | 31 | pyenv shell 3.5.5 32 | build_wheel 33 | -------------------------------------------------------------------------------- /scripts/travis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # From https://github.com/benfred/py-cpp-demangle/blob/13a22fd/ci/install_rust.sh 4 | # https://www.benfrederickson.com/writing-python-extensions-in-rust-using-pyo3/ 5 | if [ ! -d ~/rust-installer ]; then 6 | set -x 7 | mkdir ~/rust-installer 8 | curl --tlsv1.2 -sSf https://sh.rustup.rs -o ~/rust-installer/rustup.sh 9 | sh ~/rust-installer/rustup.sh --default-toolchain=nightly-2019-10-05 -y 10 | set +x 11 | fi 12 | 13 | if command -v yum; then 14 | yum install -y gpg libXtst libXtst-devel libXext libXext-devel 15 | fi 16 | -------------------------------------------------------------------------------- /scripts/upload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | eval "$(pyenv init -)" 4 | pyenv shell 3.6.4 5 | 6 | set -x 7 | pip install -U twine 8 | twine upload dist/autopy-*-manylinux*.whl --sign -r pypitest 9 | twine upload dist/autopy-*mac*.whl --sign -r pypitest 10 | twine upload dist/autopy-*win*.whl --sign -r pypitest 11 | -------------------------------------------------------------------------------- /scripts/windows-setup.md: -------------------------------------------------------------------------------- 1 | ## Windows Packaging Instructions 2 | 3 | The steps to get packaging up and running on Windows are difficult to keep 4 | consistent so I've documented them here. This may also be helpful for others 5 | installing from the latest source on the GitHub repository. 6 | 7 | Note: I am currently using VMware Fusion so a few steps are specific to that 8 | (e.g. shared folders). 9 | 10 | #### Initial setup 11 | 12 | 1. [Download the Windows 10 13 | ISO](https://www.microsoft.com/en-us/software-download/windows10). Use 14 | separate VMs for 32 and 64-bit versions. 15 | 2. Download and run the [Build Tools for Visual 16 | Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2017) 17 | installer. Check the "Visual C++ build tools" workload and click install. 18 | This can take some time. 19 | 3. Install 20 | [rustup](https://static.rust-lang.org/rustup/dist/i686-pc-windows-msvc/rustup-init.exe) 21 | with the default options[1](#f1). Note: Due to the issue 22 | described [here](https://github.com/rust-lang-nursery/rustup.rs/issues/763), 23 | it can significantly improve installation times to temporarily turn off 24 | Windows Defender security settings during the installation. 25 | 4. Install [Python 3.7](https://www.python.org/downloads/release/python-370/) 26 | and ensure "Add Python 3.7 to path" is checked. 27 | 5. Enable Shared Folders from VMware and share the autopy repo directory. 28 | 29 | #### Confirm Python & Rust setup 30 | 31 | 1. Open Command Prompt and run `py -3.7` to confirm Python is setup correctly. 32 | 2. Run `rustc --help` to confirm Rust is installed correctly. 33 | 34 | #### Install adjacent Python versions 35 | 36 | 1. Install [Python 3.6](https://www.python.org/downloads/release/python-366/). 37 | 2. Install [Python 3.5](https://www.python.org/downloads/release/python-356/). 38 | 3. Install [Python 2.7](https://www.python.org/downloads/release/python-2715/). 39 | 40 | For these installations, "Add Python to path" does **not** need to be checked. 41 | 42 | #### Build the Python wheel 43 | 44 | 1. Enter `pushd ` into the command prompt, open VMware Shared Folders in the 45 | file browser, drag the autopy repo directory to the prompt, and hit enter. 46 | 2. Run `call scripts/windows.cmd`. This will build and validate a binary wheel 47 | for each of the above Python versions. 48 | 3. Alternatively, if just installing from source locally, run `py -3.7 setup.py 49 | build install`, where `3.7` is your local Python version. 50 | 51 | [1]: Instructions from 52 | [here](https://doc.rust-lang.org/book/second-edition/ch01-01-installation.html#installing-rustup-on-windows). 53 | [↩](#a1) 54 | -------------------------------------------------------------------------------- /scripts/windows.cmd: -------------------------------------------------------------------------------- 1 | rustup default nightly 2 | 3 | py -3.7 setup.py clean 4 | py -3.7 -m pip install -U pip wheel 5 | py -3.7 -m pip install -r requirements.txt 6 | py -3.7 setup.py bdist_wheel 7 | py -3.7 -m pip uninstall -y autopy 8 | py -3.7 -m pip install autopy --no-index -f dist 9 | pushd %Temp% 10 | py -3.7 -c "import autopy" 11 | popd 12 | 13 | py -3.6 setup.py clean 14 | py -3.6 -m pip install -U pip wheel 15 | py -3.6 -m pip install -r requirements.txt 16 | py -3.6 setup.py bdist_wheel 17 | py -3.6 -m pip uninstall -y autopy 18 | py -3.6 -m pip install autopy --no-index -f dist 19 | pushd %Temp% 20 | py -3.6 -c "import autopy" 21 | popd 22 | 23 | py -2.7 setup.py clean 24 | py -2.7 -m pip install -U pip wheel 25 | py -2.7 -m pip install -r requirements.txt 26 | set PYTHON_SYS_EXECUTABLE="C:\Python27\python.exe" 27 | py -2.7 setup.py bdist_wheel 28 | py -2.7 -m pip uninstall -y autopy 29 | py -2.7 -m pip install autopy --no-index -f dist 30 | pushd %Temp% 31 | py -2.7 -c "import autopy" 32 | popd 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import distutils.util 3 | import os 4 | import re 5 | import subprocess 6 | from ast import literal_eval 7 | from setuptools import setup 8 | from setuptools_rust import Binding, RustExtension 9 | 10 | REPO_URL = "https://github.com/autopilot-rs/autopy" 11 | 12 | 13 | def module_attr_re(attr): 14 | return re.compile(r'__{0}__\s*=\s*(.*)'.format(attr)) 15 | 16 | 17 | def grep_attr(body, attr): 18 | return literal_eval(module_attr_re(attr).search(body).group(1)) 19 | 20 | 21 | def read_description(): 22 | with open("README.md") as f: 23 | header = "For more information, see the [GitHub Repository]" \ 24 | "({0}).".format(REPO_URL) 25 | filter_re = re.compile(r'.*\bPyPI\b.*') 26 | contents = header + "\n" + filter_re.sub("", f.read()) 27 | return contents.strip() 28 | 29 | 30 | def parse_module_metadata(): 31 | with open("autopy/__init__.py", "r") as f: 32 | body = f.read() 33 | return [grep_attr(body, attr) for attr in ("version", "author")] 34 | 35 | 36 | def strtobool(string): 37 | return bool(distutils.util.strtobool(string)) 38 | 39 | 40 | def git_rev_count(revision): 41 | return subprocess.check_output(["git", 42 | "rev-list", 43 | "--count", 44 | revision]).decode("utf-8").strip() 45 | 46 | 47 | def expand_version(version): 48 | env = os.environ 49 | is_ci = strtobool(env.get("CI", "f")) 50 | pr_sha = env.get("TRAVIS_PULL_REQUEST_SHA") or \ 51 | env.get("APPVEYOR_PULL_REQUEST_HEAD_COMMIT") 52 | branch = env.get("APPVEYOR_REPO_BRANCH") or env.get("TRAVIS_BRANCH") 53 | if is_ci and not pr_sha and branch == "master": 54 | commit = env.get("APPVEYOR_REPO_COMMIT") or env.get("TRAVIS_COMMIT") 55 | rev_count = git_rev_count(commit) 56 | return "{}.dev{}".format(version, rev_count) 57 | return version 58 | 59 | 60 | def main(): 61 | version, author = parse_module_metadata() 62 | description = "A simple, cross-platform GUI automation library for Python." 63 | setup( 64 | name='autopy', 65 | version=expand_version(version), 66 | author=author, 67 | author_email='pypi@michaelsande.rs', 68 | description=description, 69 | long_description=read_description(), 70 | long_description_content_type='text/markdown', 71 | url='https://www.autopy.org', 72 | license='MIT OR Apache-2.0', 73 | classifiers=[ 74 | 'Development Status :: 5 - Production/Stable', 75 | 'Environment :: MacOS X', 76 | 'Environment :: Win32 (MS Windows)', 77 | 'Environment :: X11 Applications', 78 | 'Intended Audience :: Developers', 79 | 'License :: OSI Approved :: Apache Software License', 80 | 'License :: OSI Approved :: MIT License', 81 | 'Natural Language :: English', 82 | 'Operating System :: MacOS', 83 | 'Operating System :: Microsoft :: Windows', 84 | 'Operating System :: POSIX :: Linux', 85 | 'Programming Language :: Rust', 86 | 'Programming Language :: Python :: 2.7', 87 | 'Programming Language :: Python :: 3.5', 88 | 'Programming Language :: Python :: 3.6', 89 | 'Programming Language :: Python :: 3.7', 90 | 'Programming Language :: Python :: 3.8' 91 | ], 92 | keywords=[ 93 | "autopy", 94 | "autopilot", 95 | "GUI", 96 | "automation", 97 | "cross-platform", 98 | "input", 99 | "simulation", 100 | ], 101 | project_urls={ 102 | "Documentation": "https://www.autopy.org/documentation/api-reference/", 103 | "Code": "https://github.com/autopilot-rs/autopy/", 104 | "Issue Tracker": "https://github.com/autopilot-rs/autopy/issues", 105 | }, 106 | platforms=["macOS", "Windows", "X11"], 107 | rust_extensions=[ 108 | RustExtension('autopy.alert', 'Cargo.toml', binding=Binding.PyO3), 109 | RustExtension('autopy.bitmap', 'Cargo.toml', binding=Binding.PyO3), 110 | RustExtension('autopy.color', 'Cargo.toml', binding=Binding.PyO3), 111 | RustExtension('autopy.key', 'Cargo.toml', binding=Binding.PyO3), 112 | RustExtension('autopy.mouse', 'Cargo.toml', binding=Binding.PyO3), 113 | RustExtension('autopy.screen', 'Cargo.toml', binding=Binding.PyO3), 114 | ], 115 | packages=['autopy'], 116 | zip_safe=False, # Rust extensions are not zip safe, like C-extensions. 117 | ) 118 | 119 | 120 | if __name__ == '__main__': 121 | main() 122 | -------------------------------------------------------------------------------- /src/alert.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use autopilot::alert::Response; 9 | use pyo3::prelude::*; 10 | 11 | /// Displays alert with the given attributes. If `cancel_button` is not given, 12 | /// only the default button is displayed. Returns `True` if the default button 13 | /// was pressed, or `False` if cancelled. Note that the arguments are keywords, 14 | /// and can be passed as named parameters (e.g., `alert(msg='bar', 15 | /// title='foo')`). 16 | /// 17 | /// NOTE: Due to limitations in the Win32 API, Windows currently replaces 18 | /// `default_button` with 'OK' and `cancel_button` (if given) with 'Cancel'. 19 | /// This may be fixed in a later release. 20 | #[pyfunction] 21 | #[pyo3(signature = (msg, title=None, default_button=None, cancel_button=None))] 22 | fn pyalert( 23 | msg: &str, 24 | title: Option<&str>, 25 | default_button: Option<&str>, 26 | cancel_button: Option<&str>, 27 | ) -> PyResult { 28 | let title = title.unwrap_or("AutoPy Alert"); 29 | let resp = autopilot::alert::alert(msg, Some(title), default_button, cancel_button); 30 | Ok(match resp { 31 | Response::Default => true, 32 | Response::Cancel => false, 33 | }) 34 | } 35 | 36 | /// This module contains functions for displaying alerts. 37 | #[pymodule] 38 | pub fn alert(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 39 | m.add("alert", wrap_pyfunction!(pyalert)(py)?)?; 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/bitmap.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use autopilot::geometry::{Point, Rect, Size}; 9 | use image::Pixel; 10 | use image::{ImageOutputFormat, ImageResult, Rgba}; 11 | use crate::internal::{rgb_to_hex, hex_to_rgb, FromImageError}; 12 | use pyo3::basic::CompareOp; 13 | use pyo3::prelude::*; 14 | use pyo3::types::PyType; 15 | use std::collections::hash_map::DefaultHasher; 16 | use std::fs::File; 17 | use std::hash::{Hash, Hasher}; 18 | use std::path::Path; 19 | 20 | #[pyclass] 21 | struct Bitmap { 22 | bitmap: autopilot::bitmap::Bitmap, 23 | } 24 | 25 | #[pymethods] 26 | impl Bitmap { 27 | fn __richcmp__(&self, other: &Bitmap, op: CompareOp) -> PyResult { 28 | match op { 29 | CompareOp::Eq => Ok(self.bitmap == other.bitmap), 30 | _ => unimplemented!(), 31 | } 32 | } 33 | 34 | fn __hash__(&self) -> PyResult { 35 | let mut s = DefaultHasher::new(); 36 | self.bitmap.hash(&mut s); 37 | Ok(s.finish() as isize) 38 | } 39 | 40 | // From 41 | // https://github.com/PyO3/pyo3/blob/97189a1/tests/test_buffer_protocol.rs#L17 42 | unsafe fn __getbuffer__(&self, view: *mut pyo3::ffi::Py_buffer, flags: libc::c_int) -> PyResult<()> { 43 | use pyo3::exceptions::PyBufferError; 44 | use pyo3::*; 45 | use std::ffi::CStr; 46 | use std::ptr; 47 | 48 | if view.is_null() { 49 | return Err(PyBufferError::new_err("View is null")); 50 | } 51 | 52 | unsafe { 53 | (*view).obj = ptr::null_mut(); 54 | } 55 | 56 | if (flags & ffi::PyBUF_WRITABLE) == ffi::PyBUF_WRITABLE { 57 | return Err(PyBufferError::new_err("Object is not writable")); 58 | } 59 | 60 | let bytes = &self.bitmap.image.raw_pixels(); 61 | 62 | unsafe { 63 | (*view).buf = bytes.as_ptr() as *mut libc::c_void; 64 | (*view).len = bytes.len() as isize; 65 | (*view).readonly = 1; 66 | (*view).itemsize = 1; 67 | 68 | (*view).format = ptr::null_mut(); 69 | if (flags & ffi::PyBUF_FORMAT) == ffi::PyBUF_FORMAT { 70 | let msg = CStr::from_bytes_with_nul(b"B\0").unwrap(); 71 | (*view).format = msg.as_ptr() as *mut _; 72 | } 73 | 74 | (*view).ndim = 1; 75 | (*view).shape = ptr::null_mut(); 76 | if (flags & ffi::PyBUF_ND) == ffi::PyBUF_ND { 77 | (*view).shape = (&((*view).len)) as *const _ as *mut _; 78 | } 79 | 80 | (*view).strides = ptr::null_mut(); 81 | if (flags & ffi::PyBUF_STRIDES) == ffi::PyBUF_STRIDES { 82 | (*view).strides = &((*view).itemsize) as *const _ as *mut _; 83 | } 84 | 85 | (*view).suboffsets = ptr::null_mut(); 86 | (*view).internal = ptr::null_mut(); 87 | } 88 | 89 | Ok(()) 90 | } 91 | 92 | /// Saves image to absolute path in the given format. The image type is 93 | /// determined from the filename if possible, unless format is given. If 94 | /// the file already exists, it will be overwritten. Currently only jpeg 95 | /// and png files are supported. 96 | /// 97 | /// Exceptions: 98 | /// - `IOError` is thrown if the file could not be saved. 99 | /// - `ValueError` is thrown if image couldn't be parsed. 100 | #[pyo3(signature = (path, format=None))] 101 | fn save(&self, path: &str, format: Option<&str>) -> PyResult<()> { 102 | let format = format 103 | .or(Path::new(path).extension().and_then(|x| x.to_str())) 104 | .unwrap_or(""); 105 | let fmt = image_output_format_from_extension(format); 106 | match fmt { 107 | AutoPyImageFormat::Unsupported => Err(pyo3::exceptions::PyValueError::new_err(format!( 108 | "Unknown format {}", 109 | format 110 | ))), 111 | _ => { 112 | let ref mut buffer = File::create(path)?; 113 | self.bitmap 114 | .image 115 | .write_to(buffer, fmt) 116 | .map_err(FromImageError::from)?; 117 | Ok(()) 118 | } 119 | } 120 | } 121 | 122 | /// Copies image to pasteboard. Currently only supported on macOS. 123 | /// 124 | /// Exceptions: 125 | /// - `IOError` is thrown if the image could not be copied. 126 | /// - `ValueError` is thrown if the image was too large or small. 127 | fn copy_to_pasteboard(&self) -> PyResult<()> { 128 | self.bitmap 129 | .copy_to_pasteboard() 130 | .map_err(FromImageError::from)?; 131 | Ok(()) 132 | } 133 | 134 | /// Returns `True` if the given point is contained in `bmp.bounds`. 135 | fn point_in_bounds(&self, x: f64, y: f64) -> PyResult { 136 | Ok(self.bitmap.bounds().is_point_visible(Point::new(x, y))) 137 | } 138 | 139 | /// Returns `True` if the given rect of the form `((x, y), (width, 140 | /// height))` is contained in `bmp.bounds`. 141 | fn rect_in_bounds(&self, rect: ((f64, f64), (f64, f64))) -> PyResult { 142 | let rect = Rect::new( 143 | Point::new((rect.0).0, (rect.0).1), 144 | Size::new((rect.1).0, (rect.1).1), 145 | ); 146 | Ok(self.bitmap.bounds().is_rect_visible(rect)) 147 | } 148 | 149 | /// Open the image located at the path specified. The image's format is 150 | /// determined from the path's file extension. 151 | #[classmethod] 152 | fn open(cls: &Bound<'_, PyType>, path: String) -> PyResult> { 153 | let image = image::open(path).map_err(FromImageError::from)?; 154 | let bmp = autopilot::bitmap::Bitmap::new(image, None); 155 | let result = Py::new(cls.py(), Bitmap { bitmap: bmp })?; 156 | Ok(result) 157 | } 158 | 159 | /// Returns hexadecimal value describing the color at a given point. 160 | /// 161 | /// Exceptions: 162 | /// - `ValueError` is thrown if the point out of bounds. 163 | fn get_color(&self, x: f64, y: f64) -> PyResult { 164 | let point = Point::new(x, y); 165 | if !self.bitmap.bounds().is_point_visible(point) { 166 | Err(pyo3::exceptions::PyValueError::new_err(format!( 167 | "Point out of bounds {}", 168 | point 169 | ))) 170 | } else { 171 | let rgb = self.bitmap.get_pixel(point); 172 | let (r, g, b, _) = rgb.channels4(); 173 | Ok(rgb_to_hex(r, g, b)) 174 | } 175 | } 176 | 177 | /// Attempts to find `color` inside `rect` of the form `((x, y), (width, 178 | /// height))` in `bmp` from the given `start_point`. Returns coordinates if 179 | /// found, or `None` if not. If `rect` is `None`, `bmp.bounds` is used 180 | /// instead. If `start_point` is `None`, the origin of `rect` is used. 181 | /// 182 | /// Tolerance is defined as a float in the range from 0 to 1, where 0 is an 183 | /// exact match and 1 matches anything. 184 | #[pyo3(signature = (color, tolerance=None, rect=None, start_point=None))] 185 | fn find_color( 186 | &self, 187 | color: u32, 188 | tolerance: Option, 189 | rect: Option<((f64, f64), (f64, f64))>, 190 | start_point: Option<(f64, f64)>, 191 | ) -> PyResult> { 192 | let split_color = hex_to_rgb(color); 193 | let rgb = Rgba([split_color.0, split_color.1, split_color.2, 255]); 194 | let rect: Option = 195 | rect.map(|r| Rect::new(Point::new((r.0).0, (r.0).1), Size::new((r.1).0, (r.1).1))); 196 | let start_point: Option = start_point.map(|p| Point::new(p.0, p.1)); 197 | if let Some(point) = self.bitmap.find_color(rgb, tolerance, rect, start_point) { 198 | Ok(Some((point.x, point.y))) 199 | } else { 200 | Ok(None) 201 | } 202 | } 203 | 204 | /// Returns list of all `(x, y)` coordinates inside `rect` in `bmp` 205 | /// matching `color` from the given `start_point`. If `rect` is `None`, 206 | /// `bmp.bounds` is used instead. If `start_point` is `None`, the origin of 207 | /// `rect` is used. 208 | #[pyo3(signature = (color, tolerance=None, rect=None, start_point=None))] 209 | fn find_every_color( 210 | &self, 211 | color: u32, 212 | tolerance: Option, 213 | rect: Option<((f64, f64), (f64, f64))>, 214 | start_point: Option<(f64, f64)>, 215 | ) -> PyResult> { 216 | let split_color = hex_to_rgb(color); 217 | let rgb = Rgba([split_color.0, split_color.1, split_color.2, 255]); 218 | let rect: Option = 219 | rect.map(|r| Rect::new(Point::new((r.0).0, (r.0).1), Size::new((r.1).0, (r.1).1))); 220 | let start_point: Option = start_point.map(|p| Point::new(p.0, p.1)); 221 | let points = self 222 | .bitmap 223 | .find_every_color(rgb, tolerance, rect, start_point) 224 | .iter() 225 | .map(|p| (p.x, p.y)) 226 | .collect(); 227 | Ok(points) 228 | } 229 | 230 | /// Returns count of color in bitmap. Functionally equivalent to: 231 | /// 232 | /// `len(find_every_color(color, tolerance, rect, start_point))` 233 | #[pyo3(signature = (color, tolerance=None, rect=None, start_point=None))] 234 | fn count_of_color( 235 | &self, 236 | color: u32, 237 | tolerance: Option, 238 | rect: Option<((f64, f64), (f64, f64))>, 239 | start_point: Option<(f64, f64)>, 240 | ) -> PyResult { 241 | let split_color = hex_to_rgb(color); 242 | let rgb = Rgba([split_color.0, split_color.1, split_color.2, 255]); 243 | let rect: Option = 244 | rect.map(|r| Rect::new(Point::new((r.0).0, (r.0).1), Size::new((r.1).0, (r.1).1))); 245 | let start_point: Option = start_point.map(|p| Point::new(p.0, p.1)); 246 | let count = self 247 | .bitmap 248 | .count_of_color(rgb, tolerance, rect, start_point); 249 | Ok(count) 250 | } 251 | 252 | /// Attempts to find `needle` inside `rect` in `bmp` from the given 253 | /// `start_point`. Returns coordinates if found, or `None` if not. If 254 | /// `rect` is `None`, `bmp.bounds` is used instead. If `start_point` is 255 | /// `None`, the origin of `rect` is used. 256 | /// 257 | /// Tolerance is defined as a float in the range from 0 to 1, where 0 is an 258 | /// exact match and 1 matches anything. 259 | #[pyo3(signature = (needle, tolerance=None, rect=None, start_point=None))] 260 | fn find_bitmap( 261 | &self, 262 | needle: &Bitmap, 263 | tolerance: Option, 264 | rect: Option<((f64, f64), (f64, f64))>, 265 | start_point: Option<(f64, f64)>, 266 | ) -> PyResult> { 267 | let rect: Option = 268 | rect.map(|r| Rect::new(Point::new((r.0).0, (r.0).1), Size::new((r.1).0, (r.1).1))); 269 | let start_point: Option = start_point.map(|p| Point::new(p.0, p.1)); 270 | if let Some(point) = self 271 | .bitmap 272 | .find_bitmap(&needle.bitmap, tolerance, rect, start_point) 273 | { 274 | Ok(Some((point.x, point.y))) 275 | } else { 276 | Ok(None) 277 | } 278 | } 279 | 280 | /// Returns list of all `(x, y)` coordinates inside `rect` in `bmp` 281 | /// matching `needle` from the given `start_point`. If `rect` is `None`, 282 | /// `bmp.bounds` is used instead. If `start_point` is `None`, the origin of 283 | /// `rect` is used. 284 | #[pyo3(signature = (needle, tolerance=None, rect=None, start_point=None))] 285 | fn find_every_bitmap( 286 | &self, 287 | needle: &Bitmap, 288 | tolerance: Option, 289 | rect: Option<((f64, f64), (f64, f64))>, 290 | start_point: Option<(f64, f64)>, 291 | ) -> PyResult> { 292 | let rect: Option = 293 | rect.map(|r| Rect::new(Point::new((r.0).0, (r.0).1), Size::new((r.1).0, (r.1).1))); 294 | let start_point: Option = start_point.map(|p| Point::new(p.0, p.1)); 295 | let points = self 296 | .bitmap 297 | .find_every_bitmap(&needle.bitmap, tolerance, rect, start_point) 298 | .iter() 299 | .map(|p| (p.x, p.y)) 300 | .collect(); 301 | Ok(points) 302 | } 303 | 304 | /// Returns count of occurrences of `needle` in `bmp`. Functionally 305 | /// equivalent to: 306 | /// 307 | /// `len(find_every_bitmap(color, tolerance, rect, start_point))` 308 | #[pyo3(signature = (needle, tolerance=None, rect=None, start_point=None))] 309 | fn count_of_bitmap( 310 | &self, 311 | needle: &Bitmap, 312 | tolerance: Option, 313 | rect: Option<((f64, f64), (f64, f64))>, 314 | start_point: Option<(f64, f64)>, 315 | ) -> PyResult { 316 | let rect: Option = 317 | rect.map(|r| Rect::new(Point::new((r.0).0, (r.0).1), Size::new((r.1).0, (r.1).1))); 318 | let start_point: Option = start_point.map(|p| Point::new(p.0, p.1)); 319 | let count = self 320 | .bitmap 321 | .count_of_bitmap(&needle.bitmap, tolerance, rect, start_point); 322 | Ok(count) 323 | } 324 | 325 | /// Returns new bitmap object created from a portion of another. 326 | /// 327 | /// Exceptions: 328 | /// - `ValueError` is thrown if the portion was out of bounds. 329 | fn cropped(&mut self, rect: ((f64, f64), (f64, f64))) -> PyResult> { 330 | let rect = Rect::new( 331 | Point::new((rect.0).0, (rect.0).1), 332 | Size::new((rect.1).0, (rect.1).1), 333 | ); 334 | let bmp = self.bitmap.cropped(rect).map_err(FromImageError::from)?; 335 | Python::with_gil(|py| { 336 | let result = Py::new(py, Bitmap { bitmap: bmp })?; 337 | Ok(result) 338 | }) 339 | } 340 | 341 | /// Returns true if bitmap is equal to receiver with the given tolerance. 342 | #[pyo3(signature = (bitmap, tolerance=None))] 343 | pub fn is_bitmap_equal(&self, bitmap: &Bitmap, tolerance: Option) -> PyResult { 344 | Ok(self.bitmap.bitmap_eq(&bitmap.bitmap, tolerance)) 345 | } 346 | 347 | #[getter(scale)] 348 | fn scale(&self) -> PyResult { 349 | Ok(self.bitmap.scale) 350 | } 351 | 352 | #[getter(width)] 353 | fn width(&self) -> PyResult { 354 | Ok(self.bitmap.size.width) 355 | } 356 | 357 | #[getter(height)] 358 | fn height(&self) -> PyResult { 359 | Ok(self.bitmap.size.height) 360 | } 361 | 362 | #[getter(size)] 363 | fn size(&self) -> PyResult<(f64, f64)> { 364 | Ok((self.bitmap.size.width, self.bitmap.size.height)) 365 | } 366 | 367 | #[getter(bounds)] 368 | fn bounds(&self) -> PyResult<((f64, f64), (f64, f64))> { 369 | let bounds = self.bitmap.bounds(); 370 | let result = ( 371 | (bounds.origin.x, bounds.origin.y), 372 | (bounds.size.width, bounds.size.height), 373 | ); 374 | Ok(result) 375 | } 376 | } 377 | 378 | /// Returns a screengrab of the given portion of the main display, or the 379 | /// entire display if `rect` is `None`. The `rect` parameter is in the form of 380 | /// `((x, y), (width, height))`. 381 | /// 382 | /// Exceptions: 383 | /// - `ValueError` is thrown if the rect is out of bounds. 384 | /// - `IOError` is thrown if the image failed to parse. 385 | #[pyfunction] 386 | #[pyo3(signature = (rect=None))] 387 | fn capture_screen(python: Python, rect: Option<((f64, f64), (f64, f64))>) -> PyResult> { 388 | let result: ImageResult = if let Some(rect) = rect { 389 | let portion = Rect::new( 390 | Point::new((rect.0).0, (rect.0).1), 391 | Size::new((rect.1).0, (rect.1).1), 392 | ); 393 | autopilot::bitmap::capture_screen_portion(portion) 394 | } else { 395 | autopilot::bitmap::capture_screen() 396 | }; 397 | let bmp = result.map_err(FromImageError::from)?; 398 | let result = Py::new(python, Bitmap { bitmap: bmp })?; 399 | Ok(result) 400 | } 401 | 402 | /// This module defines the class `Bitmap` for accessing bitmaps and searching 403 | /// for bitmaps on-screen. 404 | /// 405 | /// It also defines functions for taking screenshots of the screen. 406 | #[pymodule] 407 | pub fn bitmap(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 408 | m.add_class::()?; 409 | m.add_wrapped(wrap_pyfunction!(capture_screen))?; 410 | Ok(()) 411 | } 412 | 413 | enum AutoPyImageFormat { 414 | BMP, 415 | GIF, 416 | JPEG, 417 | PNG, 418 | Unsupported, 419 | } 420 | 421 | impl From for ImageOutputFormat { 422 | fn from(format: AutoPyImageFormat) -> ImageOutputFormat { 423 | use image::ImageOutputFormat::*; 424 | match format { 425 | AutoPyImageFormat::BMP => BMP, 426 | AutoPyImageFormat::GIF => GIF, 427 | AutoPyImageFormat::JPEG => JPEG(100), 428 | AutoPyImageFormat::PNG => PNG, 429 | AutoPyImageFormat::Unsupported => { 430 | Unsupported("This image format is unsupported by AutoPy".to_string()) 431 | } 432 | } 433 | } 434 | } 435 | 436 | impl From for AutoPyImageFormat { 437 | fn from(format: ImageOutputFormat) -> AutoPyImageFormat { 438 | use image::ImageOutputFormat::*; 439 | match format { 440 | BMP => AutoPyImageFormat::BMP, 441 | GIF => AutoPyImageFormat::GIF, 442 | JPEG(_) => AutoPyImageFormat::JPEG, 443 | PNG => AutoPyImageFormat::PNG, 444 | _ => AutoPyImageFormat::Unsupported, 445 | } 446 | } 447 | } 448 | 449 | fn image_output_format_from_extension(extension: &str) -> AutoPyImageFormat { 450 | let extension: &str = &(extension.to_lowercase()); 451 | match extension { 452 | "bmp" => AutoPyImageFormat::BMP, 453 | "gif" => AutoPyImageFormat::GIF, 454 | "jpeg" => AutoPyImageFormat::JPEG, 455 | "png" => AutoPyImageFormat::PNG, 456 | _ => AutoPyImageFormat::Unsupported, 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use crate::internal::{rgb_to_hex, hex_to_rgb}; 9 | use pyo3::prelude::*; 10 | 11 | /// Returns hexadecimal value of given RGB tuple. `r`, `g`, and `b` must be in 12 | /// the range 0 - 255. 13 | #[pyfunction] 14 | fn py_rgb_to_hex(red: u8, green: u8, blue: u8) -> PyResult { 15 | Ok(rgb_to_hex(red, green, blue)) 16 | } 17 | 18 | /// Returns a tuple `(r, g, b)` of the RGB integer values equivalent to the 19 | /// given RGB hexadecimal value. `r`, `g`, and `b` are in the range 0 - 255. 20 | #[pyfunction] 21 | fn py_hex_to_rgb(hex: u32) -> PyResult<(u8, u8, u8)> { 22 | Ok(hex_to_rgb(hex)) 23 | } 24 | 25 | /// This module provides functions for converting between the hexadecimal 26 | /// format used by autopy methods and other more readable formats (e.g., RGB 27 | /// tuples). 28 | #[pymodule] 29 | pub fn color(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { 30 | m.add("rgb_to_hex", wrap_pyfunction!(py_rgb_to_hex)(py)?)?; 31 | m.add("hex_to_rgb", wrap_pyfunction!(py_hex_to_rgb)(py)?)?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/internal.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use image::ImageError; 9 | use pyo3::prelude::*; 10 | 11 | pub struct FromImageError(ImageError); 12 | 13 | pub fn rgb_to_hex(red: u8, green: u8, blue: u8) -> u32 { 14 | ((red as u32) << 16) | ((green as u32) << 8) | blue as u32 15 | } 16 | 17 | pub fn hex_to_rgb(hex: u32) -> (u8, u8, u8) { 18 | let red: u8 = ((hex >> 16) & 0xff) as u8; 19 | let green: u8 = ((hex >> 8) & 0xff) as u8; 20 | let blue: u8 = (hex & 0xff) as u8; 21 | (red, green, blue) 22 | } 23 | 24 | impl From for FromImageError { 25 | fn from(err: ImageError) -> FromImageError { 26 | FromImageError { 0: err } 27 | } 28 | } 29 | 30 | impl From for PyErr { 31 | fn from(err: FromImageError) -> PyErr { 32 | match err.0 { 33 | ImageError::DimensionError => { 34 | pyo3::exceptions::PyValueError::new_err(format!("{}", err.0)) 35 | } 36 | _ => pyo3::exceptions::PyIOError::new_err(format!("{}", err.0)), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/key.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use either::{Either, Left, Right}; 9 | use pyo3::prelude::*; 10 | use pyo3::types::PyString; 11 | 12 | // NB: pyo3 doesn't currently support static properties for python classes, so 13 | // using a separate class as a namespace instead. 14 | #[pyclass] 15 | /// Constants used by this module in order to specify modifier flags. 16 | struct _Modifier {} 17 | 18 | #[pyclass] 19 | /// Constants used by this module in order to specify key codes. 20 | struct _Code {} 21 | 22 | #[pyclass] 23 | /// Constants used by this module in order to specify modifier flags. 24 | struct Modifier { 25 | flag: autopilot::key::Flag, 26 | } 27 | 28 | #[pyclass] 29 | /// Constants used by this module in order to specify key codes. 30 | struct Code { 31 | code: autopilot::key::KeyCode, 32 | } 33 | 34 | #[pymethods] 35 | impl _Modifier { 36 | /// Equivalent to the command key modifier on macOS, the Windows key 37 | /// modifier on Windows, or the meta key modifiers on X11. 38 | #[getter(META)] 39 | fn meta(&self) -> PyResult> { 40 | self.init_modifier_ref(autopilot::key::Flag::Meta) 41 | } 42 | #[getter(ALT)] 43 | fn alt(&self) -> PyResult> { 44 | self.init_modifier_ref(autopilot::key::Flag::Alt) 45 | } 46 | #[getter(CONTROL)] 47 | fn control(&self) -> PyResult> { 48 | self.init_modifier_ref(autopilot::key::Flag::Control) 49 | } 50 | #[getter(SHIFT)] 51 | fn shift(&self) -> PyResult> { 52 | self.init_modifier_ref(autopilot::key::Flag::Shift) 53 | } 54 | } 55 | 56 | #[pymethods] 57 | impl _Code { 58 | #[getter(F1)] 59 | fn f1(&self) -> PyResult> { 60 | self.init_code_ref(autopilot::key::KeyCode::F1) 61 | } 62 | #[getter(F2)] 63 | fn f2(&self) -> PyResult> { 64 | self.init_code_ref(autopilot::key::KeyCode::F2) 65 | } 66 | #[getter(F3)] 67 | fn f3(&self) -> PyResult> { 68 | self.init_code_ref(autopilot::key::KeyCode::F3) 69 | } 70 | #[getter(F4)] 71 | fn f4(&self) -> PyResult> { 72 | self.init_code_ref(autopilot::key::KeyCode::F4) 73 | } 74 | #[getter(F5)] 75 | fn f5(&self) -> PyResult> { 76 | self.init_code_ref(autopilot::key::KeyCode::F5) 77 | } 78 | #[getter(F6)] 79 | fn f6(&self) -> PyResult> { 80 | self.init_code_ref(autopilot::key::KeyCode::F6) 81 | } 82 | #[getter(F7)] 83 | fn f7(&self) -> PyResult> { 84 | self.init_code_ref(autopilot::key::KeyCode::F7) 85 | } 86 | #[getter(F8)] 87 | fn f8(&self) -> PyResult> { 88 | self.init_code_ref(autopilot::key::KeyCode::F8) 89 | } 90 | #[getter(F9)] 91 | fn f9(&self) -> PyResult> { 92 | self.init_code_ref(autopilot::key::KeyCode::F9) 93 | } 94 | #[getter(F10)] 95 | fn f10(&self) -> PyResult> { 96 | self.init_code_ref(autopilot::key::KeyCode::F10) 97 | } 98 | #[getter(F11)] 99 | fn f11(&self) -> PyResult> { 100 | self.init_code_ref(autopilot::key::KeyCode::F11) 101 | } 102 | #[getter(F12)] 103 | fn f12(&self) -> PyResult> { 104 | self.init_code_ref(autopilot::key::KeyCode::F12) 105 | } 106 | #[getter(F13)] 107 | fn f13(&self) -> PyResult> { 108 | self.init_code_ref(autopilot::key::KeyCode::F13) 109 | } 110 | #[getter(F14)] 111 | fn f14(&self) -> PyResult> { 112 | self.init_code_ref(autopilot::key::KeyCode::F14) 113 | } 114 | #[getter(F15)] 115 | fn f15(&self) -> PyResult> { 116 | self.init_code_ref(autopilot::key::KeyCode::F15) 117 | } 118 | #[getter(F16)] 119 | fn f16(&self) -> PyResult> { 120 | self.init_code_ref(autopilot::key::KeyCode::F16) 121 | } 122 | #[getter(F17)] 123 | fn f17(&self) -> PyResult> { 124 | self.init_code_ref(autopilot::key::KeyCode::F17) 125 | } 126 | #[getter(F18)] 127 | fn f18(&self) -> PyResult> { 128 | self.init_code_ref(autopilot::key::KeyCode::F18) 129 | } 130 | #[getter(F19)] 131 | fn f19(&self) -> PyResult> { 132 | self.init_code_ref(autopilot::key::KeyCode::F19) 133 | } 134 | #[getter(F20)] 135 | fn f20(&self) -> PyResult> { 136 | self.init_code_ref(autopilot::key::KeyCode::F20) 137 | } 138 | #[getter(F21)] 139 | fn f21(&self) -> PyResult> { 140 | self.init_code_ref(autopilot::key::KeyCode::F21) 141 | } 142 | #[getter(F22)] 143 | fn f22(&self) -> PyResult> { 144 | self.init_code_ref(autopilot::key::KeyCode::F22) 145 | } 146 | #[getter(F23)] 147 | fn f23(&self) -> PyResult> { 148 | self.init_code_ref(autopilot::key::KeyCode::F23) 149 | } 150 | #[getter(F24)] 151 | fn f24(&self) -> PyResult> { 152 | self.init_code_ref(autopilot::key::KeyCode::F24) 153 | } 154 | #[getter(LEFT_ARROW)] 155 | fn left_arrow(&self) -> PyResult> { 156 | self.init_code_ref(autopilot::key::KeyCode::LeftArrow) 157 | } 158 | #[getter(CONTROL)] 159 | fn control(&self) -> PyResult> { 160 | self.init_code_ref(autopilot::key::KeyCode::Control) 161 | } 162 | #[getter(RIGHT_ARROW)] 163 | fn right_arrow(&self) -> PyResult> { 164 | self.init_code_ref(autopilot::key::KeyCode::RightArrow) 165 | } 166 | #[getter(DOWN_ARROW)] 167 | fn down_arrow(&self) -> PyResult> { 168 | self.init_code_ref(autopilot::key::KeyCode::DownArrow) 169 | } 170 | #[getter(END)] 171 | fn end(&self) -> PyResult> { 172 | self.init_code_ref(autopilot::key::KeyCode::End) 173 | } 174 | #[getter(UP_ARROW)] 175 | fn up_arrow(&self) -> PyResult> { 176 | self.init_code_ref(autopilot::key::KeyCode::UpArrow) 177 | } 178 | #[getter(PAGE_UP)] 179 | fn page_up(&self) -> PyResult> { 180 | self.init_code_ref(autopilot::key::KeyCode::PageUp) 181 | } 182 | #[getter(ALT)] 183 | fn alt(&self) -> PyResult> { 184 | self.init_code_ref(autopilot::key::KeyCode::Alt) 185 | } 186 | #[getter(RETURN)] 187 | fn return_code(&self) -> PyResult> { 188 | self.init_code_ref(autopilot::key::KeyCode::Return) 189 | } 190 | #[getter(PAGE_DOWN)] 191 | fn page_down(&self) -> PyResult> { 192 | self.init_code_ref(autopilot::key::KeyCode::PageDown) 193 | } 194 | #[getter(DELETE)] 195 | fn delete(&self) -> PyResult> { 196 | self.init_code_ref(autopilot::key::KeyCode::Delete) 197 | } 198 | #[getter(HOME)] 199 | fn home(&self) -> PyResult> { 200 | self.init_code_ref(autopilot::key::KeyCode::Home) 201 | } 202 | #[getter(ESCAPE)] 203 | fn escape(&self) -> PyResult> { 204 | self.init_code_ref(autopilot::key::KeyCode::Escape) 205 | } 206 | #[getter(BACKSPACE)] 207 | fn backspace(&self) -> PyResult> { 208 | self.init_code_ref(autopilot::key::KeyCode::Backspace) 209 | } 210 | #[getter(SPACE)] 211 | fn space(&self) -> PyResult> { 212 | self.init_code_ref(autopilot::key::KeyCode::Space) 213 | } 214 | #[getter(META)] 215 | fn meta(&self) -> PyResult> { 216 | self.init_code_ref(autopilot::key::KeyCode::Meta) 217 | } 218 | #[getter(CAPS_LOCK)] 219 | fn caps_lock(&self) -> PyResult> { 220 | self.init_code_ref(autopilot::key::KeyCode::CapsLock) 221 | } 222 | #[getter(SHIFT)] 223 | fn shift(&self) -> PyResult> { 224 | self.init_code_ref(autopilot::key::KeyCode::Shift) 225 | } 226 | #[getter(TAB)] 227 | fn tab(&self) -> PyResult> { 228 | self.init_code_ref(autopilot::key::KeyCode::Tab) 229 | } 230 | } 231 | 232 | /// Holds down the given key or keycode if `down` is `True`, or releases it if 233 | /// not. Integer keycodes and modifiers should be taken from module constants 234 | /// (e.g., `Code.DELETE` or `Modifier.META`). If the given key is a character, 235 | /// it is automatically converted to a keycode corresponding to the current 236 | /// keyboard layout. 237 | #[pyfunction] 238 | #[pyo3(signature = (key, down, modifiers=None, modifier_delay=None))] 239 | fn toggle( 240 | py: Python<'_>, 241 | key: &Bound<'_, PyAny>, 242 | down: bool, 243 | modifiers: Option>>, 244 | modifier_delay: Option, 245 | ) -> PyResult<()> { 246 | let modifier_delay_ms: u64 = modifier_delay.map(|x| x as u64 * 1000).unwrap_or(0); 247 | if let Some(either) = py_object_to_key_code_convertible(key) { 248 | let flags: Vec<_> = modifiers 249 | .unwrap_or(Vec::new()) 250 | .iter() 251 | .map(|x| x.borrow(py).flag) 252 | .collect(); 253 | match either { 254 | Left(x) => autopilot::key::toggle(&x, down, &flags, modifier_delay_ms), 255 | Right(x) => autopilot::key::toggle(&x, down, &flags, modifier_delay_ms), 256 | }; 257 | Ok(()) 258 | } else { 259 | Err(pyo3::exceptions::PyTypeError::new_err( 260 | "Expected string or key code", 261 | )) 262 | } 263 | } 264 | 265 | /// Convenience wrapper around `toggle()` that holds down and then releases the 266 | /// given key and modifiers. 267 | #[pyfunction] 268 | #[pyo3(signature = (key, modifiers=None, delay=None, modifier_delay=None))] 269 | fn tap( 270 | py: Python<'_>, 271 | key: &Bound<'_, PyAny>, 272 | modifiers: Option>>, 273 | delay: Option, 274 | modifier_delay: Option, 275 | ) -> PyResult<()> { 276 | let delay_ms: u64 = delay.map(|x| x as u64 * 1000).unwrap_or(0); 277 | let modifier_delay_ms: u64 = modifier_delay.map(|x| x as u64 * 1000).unwrap_or(delay_ms); 278 | if let Some(either) = py_object_to_key_code_convertible(key) { 279 | let flags: Vec<_> = modifiers 280 | .unwrap_or(Vec::new()) 281 | .iter() 282 | .map(|x| x.borrow(py).flag) 283 | .collect(); 284 | match either { 285 | Left(x) => autopilot::key::tap(&x, &flags, delay_ms, modifier_delay_ms), 286 | Right(x) => autopilot::key::tap(&x, &flags, delay_ms, modifier_delay_ms), 287 | }; 288 | Ok(()) 289 | } else { 290 | Err(pyo3::exceptions::PyTypeError::new_err( 291 | "Expected string or key code", 292 | )) 293 | } 294 | } 295 | 296 | /// Attempts to simulate typing a string at the given WPM, or as fast as 297 | /// possible if the WPM is 0. 298 | #[pyfunction] 299 | #[pyo3(signature = (string, wpm=None))] 300 | fn type_string(string: &str, wpm: Option) -> PyResult<()> { 301 | autopilot::key::type_string(string, &[], wpm.unwrap_or(0.0), 0.0); 302 | Ok(()) 303 | } 304 | 305 | /// This module contains functions for controlling the keyboard. 306 | #[pymodule] 307 | pub fn key(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 308 | m.add("Modifier", Py::new(py, _Modifier {})?)?; 309 | m.add("Code", Py::new(py, _Code {})?)?; 310 | m.add_wrapped(wrap_pyfunction!(toggle))?; 311 | m.add_wrapped(wrap_pyfunction!(tap))?; 312 | m.add_wrapped(wrap_pyfunction!(type_string))?; 313 | Ok(()) 314 | } 315 | 316 | fn py_object_to_key_code_convertible( 317 | object: &Bound<'_, PyAny>, 318 | ) -> Option> { 319 | // object.extract::> 320 | if let Ok(code) = object.downcast::() { 321 | return Some(Left(autopilot::key::Code(code.borrow().code))); 322 | } else if let Ok(key) = object.downcast::() { 323 | if let Some(c) = key.to_string().chars().next() { 324 | return Some(Right(autopilot::key::Character(c))); 325 | } 326 | } 327 | None 328 | } 329 | 330 | impl _Modifier { 331 | fn init_modifier_ref(&self, flag: autopilot::key::Flag) -> PyResult> { 332 | Python::with_gil(|py| { 333 | let result = Py::new(py, Modifier { flag: flag })?; 334 | Ok(result) 335 | }) 336 | } 337 | } 338 | 339 | impl _Code { 340 | fn init_code_ref(&self, code: autopilot::key::KeyCode) -> PyResult> { 341 | Python::with_gil(|py| { 342 | let result = Py::new(py, Code { code: code })?; 343 | Ok(result) 344 | }) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | // #![feature(specialization, const_fn)] 9 | 10 | extern crate autopilot; 11 | extern crate either; 12 | extern crate image; 13 | extern crate pyo3; 14 | 15 | pub mod alert; 16 | pub mod bitmap; 17 | pub mod color; 18 | mod internal; 19 | pub mod key; 20 | pub mod mouse; 21 | pub mod screen; 22 | 23 | use pyo3::prelude::*; 24 | 25 | #[pymodule] 26 | fn autopy(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 27 | 28 | let alert_module = PyModule::new(m.py(), "alert")?; 29 | alert::alert(m.py(), &alert_module)?; 30 | m.add_submodule(&alert_module)?; 31 | 32 | let bitmap_module = PyModule::new(m.py(), "bitmap")?; 33 | bitmap::bitmap(m.py(), &bitmap_module)?; 34 | m.add_submodule(&bitmap_module)?; 35 | 36 | let color_module = PyModule::new(m.py(), "color")?; 37 | color::color(m.py(), &color_module)?; 38 | m.add_submodule(&color_module)?; 39 | 40 | let key_module = PyModule::new(m.py(), "key")?; 41 | key::key(m.py(), &key_module)?; 42 | m.add_submodule(&key_module)?; 43 | 44 | let mouse_module = PyModule::new(m.py(), "mouse")?; 45 | mouse::mouse(m.py(), &mouse_module)?; 46 | m.add_submodule(&mouse_module)?; 47 | 48 | let screen_module = PyModule::new(m.py(), "screen")?; 49 | screen::screen(m.py(), &screen_module)?; 50 | m.add_submodule(&screen_module)?; 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /src/mouse.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use autopilot::geometry::Point; 9 | use pyo3::prelude::*; 10 | 11 | struct FromMouseError(autopilot::mouse::MouseError); 12 | 13 | // NB: pyo3 doesn't currently support static properties for python classes, so 14 | // using a separate class as a namespace instead. 15 | #[pyclass] 16 | /// Constants used by this module in order to specify mouse buttons. 17 | struct _Button {} 18 | 19 | #[pyclass] 20 | /// Constants used by this module in order to specify mouse buttons. 21 | struct Button { 22 | button: autopilot::mouse::Button, 23 | } 24 | 25 | /// Moves the mouse to the given `(x, y)` coordinate. 26 | /// 27 | /// Exceptions: 28 | /// - `ValueError` is thrown if the point is out of index. 29 | #[pyfunction] 30 | fn move_py(x: f64, y: f64) -> PyResult<()> { 31 | let result = autopilot::mouse::move_to(Point::new(x, y)); 32 | result.map_err(FromMouseError::from)?; 33 | Ok(()) 34 | } 35 | 36 | #[pymethods] 37 | impl _Button { 38 | #[getter(LEFT)] 39 | fn left(&self) -> PyResult> { 40 | self.init_button_ref(autopilot::mouse::Button::Left) 41 | } 42 | 43 | #[getter(RIGHT)] 44 | fn right(&self) -> PyResult> { 45 | self.init_button_ref(autopilot::mouse::Button::Right) 46 | } 47 | 48 | #[getter(MIDDLE)] 49 | fn middle(&self) -> PyResult> { 50 | self.init_button_ref(autopilot::mouse::Button::Middle) 51 | } 52 | } 53 | 54 | /// Returns a tuple `(x, y)` of the current mouse position. 55 | #[pyfunction] 56 | fn location() -> PyResult<(f64, f64)> { 57 | let point = autopilot::mouse::location(); 58 | Ok((point.x, point.y)) 59 | } 60 | 61 | /// Holds down or releases the given mouse button in the current position. 62 | /// Button can be `LEFT`, `RIGHT`, `MIDDLE`, or `None` to default to the left 63 | /// button. 64 | #[pyfunction] 65 | #[pyo3(signature = (button=None, down=false))] 66 | fn toggle(button: Option<&Button>, down: bool) -> PyResult<()> { 67 | use autopilot::mouse::Button::*; 68 | autopilot::mouse::toggle(button.map_or(Left, |x| x.button), down); 69 | Ok(()) 70 | } 71 | 72 | /// Convenience wrapper around `toggle()` that holds down and then releases the 73 | /// given mouse button. By default, the left button is pressed. 74 | #[pyfunction] 75 | #[pyo3(signature = (button=None, delay=None))] 76 | fn click(button: Option<&Button>, delay: Option) -> PyResult<()> { 77 | let delay_ms: Option = delay.map(|x| x as u64 * 1000); 78 | use autopilot::mouse::Button::*; 79 | autopilot::mouse::click(button.map_or(Left, |x| x.button), delay_ms); 80 | Ok(()) 81 | } 82 | 83 | /// Smoothly moves the mouse to the given `(x, y)` coordinate in a straight 84 | /// line. 85 | /// 86 | /// Exceptions: 87 | /// - `ValueError` is thrown if the point is out of index. 88 | #[pyfunction] 89 | #[pyo3(signature = (x, y, duration=None))] 90 | fn smooth_move(x: f64, y: f64, duration: Option) -> PyResult<()> { 91 | let result = autopilot::mouse::smooth_move(Point::new(x, y), duration); 92 | result.map_err(FromMouseError::from)?; 93 | Ok(()) 94 | } 95 | 96 | /// This module contains functions for getting the current state of and 97 | /// controlling the mouse cursor. 98 | /// 99 | /// Unless otherwise stated, coordinates are those of a screen coordinate 100 | /// system, where the origin is at the top left. 101 | #[pymodule] 102 | pub fn mouse(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 103 | // Workaround bug where #[pyfunction(m, "move")] identifier causes error in 104 | // pyo3. 105 | m.add("move", wrap_pyfunction!(move_py)(py)?)?; 106 | m.add_wrapped(wrap_pyfunction!(location))?; 107 | m.add_wrapped(wrap_pyfunction!(toggle))?; 108 | m.add_wrapped(wrap_pyfunction!(click))?; 109 | m.add_wrapped(wrap_pyfunction!(smooth_move))?; 110 | 111 | m.add("Button", Py::new(py, _Button {})?)?; 112 | Ok(()) 113 | } 114 | 115 | impl _Button { 116 | fn init_button_ref(&self, button: autopilot::mouse::Button) -> PyResult> { 117 | Python::with_gil(|py| { 118 | let result = Py::new(py, Button { button: button })?; 119 | Ok(result) 120 | }) 121 | } 122 | } 123 | 124 | impl From for FromMouseError { 125 | fn from(err: autopilot::mouse::MouseError) -> FromMouseError { 126 | FromMouseError { 0: err } 127 | } 128 | } 129 | 130 | impl From for PyErr { 131 | fn from(err: FromMouseError) -> PyErr { 132 | use autopilot::mouse::MouseError::*; 133 | match err.0 { 134 | OutOfBounds => pyo3::exceptions::PyValueError::new_err("Point out of bounds"), 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/screen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018, 2019, 2020 Michael Sanders 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT License , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | use autopilot::geometry::Point; 9 | use crate::internal::{rgb_to_hex, FromImageError}; 10 | use pyo3::prelude::*; 11 | use crate::image::Pixel; 12 | 13 | /// Returns the scale of the main screen, i.e. how many pixels are in a point. 14 | #[pyfunction] 15 | fn scale() -> PyResult { 16 | Ok(autopilot::screen::scale()) 17 | } 18 | 19 | /// Returns a tuple `(width, height)` of the size of the main screen in points. 20 | #[pyfunction] 21 | fn size() -> PyResult<(f64, f64)> { 22 | let size = autopilot::screen::size(); 23 | Ok((size.width, size.height)) 24 | } 25 | 26 | /// Returns `True` if the given point is inside the main screen boundaries. 27 | #[pyfunction] 28 | fn is_point_visible(x: f64, y: f64) -> PyResult { 29 | Ok(autopilot::screen::is_point_visible(Point::new(x, y))) 30 | } 31 | 32 | /// Returns hexadecimal value describing the color at a given point. 33 | /// 34 | /// Functionally equivalent to: 35 | /// 36 | /// rect = ((x, y), (1, 1)) 37 | /// bitmap.capture_screen_portion(rect).get_color(0, 0) 38 | /// 39 | /// only more efficient/convenient. 40 | /// 41 | /// Exceptions: 42 | /// - `ValueError` is thrown if the point out of bounds. 43 | #[pyfunction] 44 | fn get_color(x: f64, y: f64) -> PyResult { 45 | let point = Point::new(x, y); 46 | let rgb = autopilot::screen::get_color(point).map_err(FromImageError::from)?; 47 | let (r, g, b, _) = rgb.channels4(); 48 | Ok(rgb_to_hex(r, g, b)) 49 | } 50 | 51 | /// This module contains functions for working with the screen. 52 | #[pymodule] 53 | pub fn screen(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 54 | m.add_wrapped(wrap_pyfunction!(scale))?; 55 | m.add_wrapped(wrap_pyfunction!(size))?; 56 | m.add_wrapped(wrap_pyfunction!(is_point_visible))?; 57 | m.add_wrapped(wrap_pyfunction!(get_color))?; 58 | Ok(()) 59 | } 60 | --------------------------------------------------------------------------------