├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── README.md ├── bootstrap ├── bootstrap.sh ├── linux_bootstrap.sh ├── osx_bootstrap.sh ├── tesseract.json └── win_bootstrap.sh ├── moziris ├── __init__.py ├── api │ ├── __init__.py │ ├── enums.py │ ├── errors.py │ ├── finder │ │ ├── __init__.py │ │ ├── finder.py │ │ ├── image_search.py │ │ ├── pattern.py │ │ └── text_search.py │ ├── highlight │ │ ├── __init__.py │ │ ├── highlight_circle.py │ │ ├── highlight_rectangle.py │ │ └── screen_highlight.py │ ├── keyboard │ │ ├── __init__.py │ │ ├── key.py │ │ ├── keyboard.py │ │ ├── keyboard_api.py │ │ └── keyboard_util.py │ ├── location.py │ ├── mouse │ │ ├── __init__.py │ │ ├── mouse.py │ │ ├── mouse_controller.py │ │ └── xmouse.py │ ├── os_helpers.py │ ├── rectangle.py │ ├── save_debug_image │ │ ├── __init__.py │ │ └── save_image.py │ ├── screen │ │ ├── __init__.py │ │ ├── display.py │ │ ├── region.py │ │ ├── region_utils.py │ │ ├── screen.py │ │ └── screenshot_image.py │ └── settings.py ├── base │ ├── __init__.py │ ├── target.py │ └── testcase.py ├── configuration │ ├── __init__.py │ └── config_parser.py ├── control_center │ ├── __init__.py │ ├── assets │ │ ├── asset-manifest.json │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── firefox.png │ │ │ └── notepad.png │ │ ├── index.html │ │ ├── manifest.json │ │ ├── service-worker.js │ │ └── static │ │ │ ├── css │ │ │ ├── 2.3c6b4139.chunk.css │ │ │ ├── 2.3c6b4139.chunk.css.map │ │ │ ├── main.c73dc7db.chunk.css │ │ │ └── main.c73dc7db.chunk.css.map │ │ │ ├── js │ │ │ ├── 2.d4e7d496.chunk.js │ │ │ ├── 2.d4e7d496.chunk.js.map │ │ │ ├── main.5deb8d43.chunk.js │ │ │ ├── main.5deb8d43.chunk.js.map │ │ │ ├── runtime~main.a8a9905a.js │ │ │ └── runtime~main.a8a9905a.js.map │ │ │ └── media │ │ │ ├── ZillaSlab-Bold.170246a0.woff2 │ │ │ ├── ZillaSlab-Bold.ce6daadc.woff │ │ │ ├── ZillaSlab-Regular.9144f8fb.woff2 │ │ │ ├── ZillaSlab-Regular.d4e58840.woff │ │ │ ├── ZillaSlab-SemiBold.32f7ad7d.woff2 │ │ │ ├── ZillaSlab-SemiBold.9d5384bb.woff │ │ │ ├── open-sans-v15-latin-300.521d17bc.woff │ │ │ ├── open-sans-v15-latin-300.60c86674.woff2 │ │ │ ├── open-sans-v15-latin-300italic.06bbd318.woff2 │ │ │ ├── open-sans-v15-latin-300italic.8a648ff3.woff │ │ │ ├── open-sans-v15-latin-600.1cd5320f.woff │ │ │ ├── open-sans-v15-latin-600.223a277b.woff2 │ │ │ ├── open-sans-v15-latin-600italic.318ea1ad.woff │ │ │ ├── open-sans-v15-latin-600italic.4950a720.woff2 │ │ │ ├── open-sans-v15-latin-700.623e3205.woff │ │ │ ├── open-sans-v15-latin-700.d08c09f2.woff2 │ │ │ ├── open-sans-v15-latin-700italic.72e19cbb.woff │ │ │ ├── open-sans-v15-latin-700italic.c02f5da6.woff2 │ │ │ ├── open-sans-v15-latin-800.aaeffaf2.woff2 │ │ │ ├── open-sans-v15-latin-800.c6aa0c4a.woff │ │ │ ├── open-sans-v15-latin-800italic.6b3973ff.woff2 │ │ │ ├── open-sans-v15-latin-800italic.79b58175.woff │ │ │ ├── open-sans-v15-latin-italic.987032ea.woff2 │ │ │ ├── open-sans-v15-latin-italic.db70d0b9.woff │ │ │ ├── open-sans-v15-latin-regular.bf2d0783.woff │ │ │ └── open-sans-v15-latin-regular.cffb686d.woff2 │ └── commands.py ├── email_report │ ├── __init__.py │ └── email_client.py ├── scripts │ ├── __init__.py │ ├── main.py │ └── test.py ├── test │ ├── requirements │ │ ├── flake8.txt │ │ └── tests.txt │ └── unit │ │ └── configuration │ │ ├── test_iris_config_custom.py │ │ ├── test_iris_config_default.py │ │ └── test_logger.py └── util │ ├── __init__.py │ ├── arg_parser.py │ ├── cleanup.py │ ├── json_utils.py │ ├── local_web_server.py │ ├── logger_manager.py │ ├── path_manager.py │ ├── region_utils.py │ ├── report_utils.py │ ├── run_report.py │ ├── system.py │ ├── target_loader.py │ ├── test_assert.py │ └── test_loader.py ├── setup.cfg ├── setup.py ├── targets ├── __init__.py └── sample │ ├── __init__.py │ ├── icon.png │ └── main.py ├── tests └── sample │ ├── pytest.ini │ ├── section1 │ ├── __init__.py │ └── section1_1 │ │ ├── __init__.py │ │ ├── images │ │ ├── osx │ │ │ └── test.png │ │ └── win │ │ │ └── test.png │ │ └── test_1.py │ └── section2 │ ├── __init__.py │ └── test_2.py ├── tools ├── platform_check.sh └── project_check.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | downloads/ 15 | bin/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | share/ 23 | man/ 24 | include/ 25 | tcl/ 26 | venv/ 27 | venv3/ 28 | .venv/ 29 | ENV/ 30 | *.pyc 31 | *.pyo 32 | *.un~ 33 | *.py~ 34 | .idea/ 35 | *.egg-info/ 36 | *.egg 37 | dist/ 38 | .eggs/ 39 | doc/ 40 | .coverage 41 | .installed.cfg 42 | pip-selfcheck.json 43 | .local 44 | local/ 45 | 46 | # Iris 47 | .DS_Store 48 | .gitignore 49 | *.sh.swp 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage.xml 69 | *.cover 70 | .hypothesis/ 71 | 72 | # Translations 73 | *.mo 74 | *.pot 75 | 76 | # Django stuff: 77 | *.log 78 | local_settings.py 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # celery beat schedule file 100 | celerybeat-schedule 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # dotenv 106 | .env 107 | 108 | # virtualenv 109 | .venv 110 | venv/ 111 | ENV/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | 126 | #credential file 127 | config.ini 128 | 129 | # __debug__.py (used for debugging in Pycharm) 130 | __debug__.py 131 | 132 | # Pipfile.lock (since we are using multiple platforms, differences will appear) 133 | Pipfile.lock 134 | 135 | # Visual Studio Code 136 | .vscode 137 | 138 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: '3.7.3' 4 | 5 | # Bootstrap install 6 | install: 7 | - 'bootstrap/linux_bootstrap.sh' 8 | - 'pipenv install' 9 | 10 | # Virtual display for headless run at 1920x1080 32 bit resolution 11 | before_script: 12 | - "export DISPLAY=:99.0" 13 | - "Xvfb :99 -screen 0 1920x1080x24+32 +extension GLX +extension RANDR > /dev/null 2>&1 &" 14 | 15 | with_content_shell: true 16 | 17 | # Job sections 18 | jobs: 19 | include: 20 | - stage: Lint 21 | before_script: skip 22 | install: pip install tox 23 | script: tox -e flake8 24 | - stage: Unit Tests 25 | install: pip install tox 26 | script: 27 | - tox -e config_default 28 | - tox -e config_custom 29 | - stage: Build 30 | os: linux 31 | dist: xenial 32 | script: pipenv run iris -h 33 | 34 | 35 | # Email notification 36 | notifications: 37 | email: 38 | recipients: 39 | - twalker@mozilla.com 40 | - mwobensmith@mozilla.com 41 | on_success: change 42 | on_failure: always 43 | on_cancel: always 44 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include moziris/control_center/assets/* 2 | include moziris/control_center/assets/images/* 3 | include moziris/control_center/assets/static/* 4 | include moziris/control_center/assets/static/css/* 5 | include moziris/control_center/assets/static/js/* 6 | include moziris/control_center/assets/static/media/* 7 | 8 | prune tests 9 | prune targets 10 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [packages] 7 | bugzilla = "==1.0.0" 8 | coloredlogs = "==10.0" 9 | image = "==1.5.27" 10 | iris = {path = ".",editable = true} 11 | flaky = "==3.6.1" 12 | funcy = "==1.13" 13 | gitpython = "==3.0.5" 14 | more-itertools = "==8.0.2" 15 | mozdownload = "==1.26" 16 | mozinfo = "==1.1.0" 17 | mozinstall = "==2.0.0" 18 | mozrunner = "==7.7" 19 | mozversion = "==2.2.0" 20 | mss = "==4.0.3" 21 | numpy = "==1.17.2" 22 | opencv-python = "==4.1.1.26" 23 | packaging = "==20.0" 24 | psutil = "==5.6.3" 25 | pyautogui = "==0.9.48" 26 | pygithub = "==1.43.8" 27 | pynput = "==1.5.2" 28 | pyperclip = "==1.7.0" 29 | pytesseract = "==0.3.0" 30 | pytest = "==5.1.2" 31 | python-dateutil = "==2.8.1" 32 | # Platform dependencies 33 | xlib = {platform_system = "== 'Linux'",version = "==0.21"} 34 | 35 | [dev-packages] 36 | pep8 = "*" 37 | pylint = "*" 38 | 39 | [requires] 40 | python_version = "3.7" 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moziris 2 | 3 |  4 |  5 |  6 |  7 |  8 | 9 | Mozilla Iris is a tool that uses on-screen pattern and text matching, while manipulating a machine's mouse and keyboard, to test visual and interactive states of an application. 10 | For more detailed information and troubleshooting tips, please [view our wiki](https://github.com/mozilla/iris/wiki). 11 | 12 | ## Installation 13 | 14 | ### Mac instructions: 15 | 16 | #### System Requirements 17 | 18 | - Python 3 19 | - git 20 | - [Firefox](https://www.mozilla.org/en-US/firefox/new/) 21 | 22 | #### Setup 23 | 24 | ``` 25 | git clone https://github.com/mozilla/iris 26 | # Run the Mac bootstrap script 27 | cd iris 28 | ./bootstrap/bootstrap.sh 29 | # Run this command to agree to xcode terms of service 30 | sudo xcodebuild -license accept 31 | ``` 32 | - **Restart** your Mac in order for certain libraries to be recognized 33 | - In System Preferences, go to Mission Control and change the keyboard shortcut for "Application Windows" to "-", or none 34 | - Launch Iris 35 | ``` 36 | cd iris 37 | pipenv install 38 | pipenv shell 39 | iris sample 40 | ``` 41 | 42 | ### Windows 7 / Windows 10 Professional instructions: 43 | 44 | #### System Requirements 45 | 46 | - Python 3 47 | - git 48 | - [Firefox](https://www.mozilla.org/en-US/firefox/new/) 49 | - [Powershell 3](https://www.microsoft.com/en-us/download/details.aspx?id=34595) 50 | - [.NET framework version 4.5](https://www.microsoft.com/en-us/download/details.aspx?id=30653) 51 | 52 | #### Setup 53 | 54 | ``` 55 | git clone https://github.com/mozilla/iris 56 | cd iris 57 | bootstrap\bootstrap.sh 58 | # Install project requirements and activate the virtualenv 59 | pipenv install 60 | pipenv shell 61 | # Run Iris 62 | iris sample 63 | ``` 64 | 65 | ### Ubuntu Linux 16.04 instructions: 66 | 67 | #### System Requirements 68 | 69 | - Python 3 70 | - git 71 | - [Firefox](https://www.mozilla.org/en-US/firefox/new/) 72 | - [Follow instructions below for disabling Keyring](https://github.com/mozilla/iris/wiki/Setup#disable-system-keyring) 73 | - Open Settings > Displays > "Scale for Menu and Title bars:" and verify that it is set to 1 74 | 75 | #### Setup 76 | ``` 77 | git clone https://github.com/mozilla/iris 78 | cd iris 79 | ./bootstrap/bootstrap.sh 80 | # Note: This will take around 10 minutes to download, compile, and install dependencies 81 | # Run the following commands to complete installation and launch Iris 82 | pipenv install 83 | pipenv shell 84 | iris sample 85 | ``` 86 | 87 | ## Usage 88 | 89 | The Iris project is meant to be used with your own "target" and tests. A target is basically a pytest plugin invoked by Iris, which will then gather data during the run to present in a web-based interface known as the Iris Control Center. 90 | 91 | Iris is available as a PyPI library named `moziris`. It requires system dependencies that are installed using the bootstrap script from this repo. 92 | 93 | Once your system is configured, and the setup instructions have been followed, you can test some of Iris' functionality. 94 | 95 | To invoke the "sample" target - which is just a placeholder project for demonstration purposes: 96 | ``` 97 | iris sample 98 | ``` 99 | 100 | To open the Control Center, which is the web-based UI for managing local Iris runs: 101 | ``` 102 | iris -k 103 | ``` 104 | 105 | To verify that the Iris API itself exists, without running tests, this command will move your mouse on screen: 106 | ``` 107 | api-test 108 | ``` 109 | 110 | A complete list of command-line options is available when invoking the `-h` flag. 111 | 112 | For more detailed examples, see the [project wiki](https://github.com/mozilla/iris/wiki/Command-line-examples). 113 | 114 | 115 | ## Contributing 116 | 117 | See our [project wiki](https://github.com/mozilla/iris/wiki/Developer-Workflow) for more information on contributing to Iris. 118 | -------------------------------------------------------------------------------- /bootstrap/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RED='\033[0;31m' 4 | CYAN='\033[0;36m' 5 | NC='\033[0m' # No Color 6 | 7 | # Install requirements based on platform 8 | 9 | if [[ $(whoami | grep "root") ]]; then 10 | SUDO_USER="" 11 | else 12 | SUDO_USER="sudo" 13 | fi 14 | 15 | echo -e "\n${CYAN}Installing project dependencies based on OS type ${NC} \n" 16 | 17 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 18 | echo -e "${CYAN}Bootstrapping for Linux OS ${NC} \n" 19 | echo -e "\n${RED}The following applications will be installed on your system:${NC}" 20 | echo -e "\npython3.7\npython3.7-dev\npython3-pip\ngit\nscrot\nxsel\np7zip-full" 21 | echo -e "libopencv-dev\nautoconf\nautomake\nlibtool\nautoconf-archive\npkg-config" 22 | echo -e "libpng-dev\nlibjpeg8-dev\nlibtiff5-dev\nzlib1g-dev\nlibicu-dev" 23 | echo -e "libpango1.0-dev\nlibcairo2-dev\nfirefox\nwmctrl\nxdotool\npython3.7-tk\n" 24 | read -p "Do you wish to continue? (y)es/(n)o " -n 1 -r 25 | echo 26 | if [[ $REPLY =~ ^[Yy]$ ]] 27 | then 28 | read -p "Do you wish to install libraries to enable OCR features? (y)es/(n)o " -n 1 -r 29 | echo 30 | ${SUDO_USER} $(dirname "$0")/linux_bootstrap.sh $REPLY 31 | fi 32 | elif [[ "$OSTYPE" == "darwin"* ]]; then 33 | echo -e "${CYAN}Bootstrapping for Mac OS X ${NC} \n" 34 | echo -e "\n${RED}The following applications will be installed on your system:${NC}" 35 | echo -e "\nHomebrew package management\npython3.7\np7zip\npipenv\n" 36 | echo -e "\n${RED}This script will also overwrite any symlinks to Python 3\n${NC}" 37 | read -p "Do you wish to continue? (y)es/(n)o " -n 1 -r 38 | echo 39 | if [[ $REPLY =~ ^[Yy]$ ]] 40 | then 41 | read -p "Do you wish to install libraries to enable OCR features? (y)es/(n)o " -n 1 -r 42 | echo 43 | $(dirname "$0")/osx_bootstrap.sh $REPLY 44 | fi 45 | else 46 | echo -e "${CYAN} Bootstrapping for Windows OS ${NC} \n" 47 | echo -e "${RED}Administrator password required!${NC} \n" 48 | echo -e "\n${RED}The following applications will be installed on your system:${NC}" 49 | echo -e "\nScoop package management\n7zip\nopenssh\ngit\nfirefox\nwhich\nsudo\ntesseract\npython3.7\n" 50 | read -p "Do you wish to continue? (y)es/(n)o " -n 1 -r 51 | echo 52 | if [[ $REPLY =~ ^[Yy]$ ]] 53 | then 54 | read -p "Do you wish to install libraries to enable OCR features? (y)es/(n)o " -n 1 -r 55 | echo 56 | $(dirname "$0")/win_bootstrap.sh $REPLY 57 | fi 58 | fi 59 | -------------------------------------------------------------------------------- /bootstrap/osx_bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Mac bootstrap 3 | 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | NC='\033[0m' # No Color 7 | 8 | echo -e "\n${RED}##### Starting OS X bootstrap #####${NC} \n" 9 | 10 | echo -e "${GREEN} ---> Check and install Homebrew${NC} \n" 11 | command -v brew >/dev/null 2>&1 || { echo >&2 "Installing Homebrew Now"; \ 12 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"; } 13 | 14 | install_python37 () { 15 | brew install python3 16 | brew link --overwrite python3 17 | export PATH=/usr/local/share/python:$PATH 18 | } 19 | 20 | echo -e "\n${GREEN} ---> installing/updating Python 3.7 ${NC} \n" 21 | 22 | if command -v python3 &>/dev/null; then 23 | if [[ $(python3 --version | grep "Python 3.7") =~ 3.7 ]]; then 24 | echo -e "\n${GREEN} ---> Skipping Python 3.7 install. Already installed. ${NC}\n"--version | grep "Python 3.7" 25 | elif command -v python3.7 &>/dev/null; then 26 | echo -e "\n${GREEN} ---> Verified for specific python3.7. Skipped install. Already installed. ${NC}\n"--version | grep "Python 3.7" 27 | else 28 | echo -e "\n${GREEN} ---> Installing Python 3.7 #####${NC}\n" 29 | install_python37 30 | fi 31 | else 32 | echo -e "\n${GREEN} ---> Installing Python 3.7 #####${NC}\n" 33 | install_python37 34 | fi 35 | 36 | if [[ $1 =~ ^[Yy]$ ]] 37 | then 38 | echo -e "\n${GREEN} ---> Installing Tesseract ${NC} \n" 39 | if command -v tesseract -v >/dev/null 2>&1; then 40 | echo -e "\n${GREEN} ---> Skipping Tesseract install. Already installed. ${NC}\n" 41 | echo -e "${GREEN} ---> Checking Tesseract version. ${NC}\n" 42 | if [[ $(tesseract -v | grep "tesseract 3.05") ]]; then 43 | echo -e "${RED} ---> You have Tesseract 3, removing and installing Tesseract 4.${NC}\n" 44 | brew upgrade tesseract 45 | else 46 | echo -e "${GREEN} ---> Tesseract is the correct version. ${NC}\n" 47 | fi 48 | else 49 | brew install tesseract 50 | fi 51 | fi 52 | 53 | echo -e "\n${GREEN} ---> installing/updating p7zip ${NC} \n" 54 | brew install p7zip 55 | 56 | echo -e "\n${GREEN} ---> installing/updating xquartz ${NC} \n" 57 | brew cask install xquartz 58 | 59 | echo -e "\n${GREEN} ---> installing/updating firefox ${NC} \n" 60 | if [[ $(mdfind "kMDItemKind == 'Application'" | grep Firefox.app) =~ "Firefox.app" ]]; then 61 | echo -e "\n${GREEN} ---> Skipping Firefox install. Already installed. ${NC}\n" 62 | else 63 | echo -e "\n${GREEN} ---> installing Firefox ${NC}\n" 64 | brew cask install firefox 65 | fi 66 | 67 | echo -e "\n${GREEN} ---> installing/upgrading pipenv ${NC}\n" 68 | if command -v pipenv &>/dev/null; then 69 | brew upgrade pipenv 70 | else 71 | echo -e "\n${GREEN} ---> installing pipenv ${NC}\n" 72 | brew install pipenv 73 | fi 74 | 75 | echo -e "\n${GREEN}---> Installing pyobjc library #####${NC}\n" 76 | if [[ $(pip3.7 show pyobjc | grep Name:) =~ "pyobjc" ]]; then 77 | echo -e "${GREEN} ---> Skipping pyobjc install. Already installed. ${NC}\n" 78 | else 79 | pip3.7 install -U pyobjc 80 | fi 81 | 82 | echo -e "\n${GREEN}---> Installing pyobjc-core library #####${NC}\n" 83 | if [[ $(pip3.7 show pyobjc-core | grep Name:) =~ "pyobjc-core" ]]; then 84 | echo -e "${GREEN} ---> Skipping pyobjc-core install. Already installed. ${NC}\n" 85 | else 86 | pip3.7 install -U pyobjc-core 87 | fi 88 | 89 | # Exporting settings to .bash_profile or .zshrc 90 | grep -q -F 'export LC_ALL=en_US.UTF-8' ~/.bash_profile || echo 'export LC_ALL=en_US.UTF-8' >> ~/.bash_profile 91 | grep -q -F 'export LANG=en_US.UTF-8' ~/.bash_profile || echo 'export LANG=en_US.UTF-8' >> ~/.bash_profile 92 | grep -q -F 'export LC_ALL=en_US.UTF-8' ~/.zshrc || echo 'export LC_ALL=en_US.UTF-8' >> ~/.zshrc 93 | grep -q -F 'export LANG=en_US.UTF-8' ~/.zshrc || echo 'export LANG=en_US.UTF-8' >> ~/.zshrc 94 | -------------------------------------------------------------------------------- /bootstrap/tesseract.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://github.com/UB-Mannheim/tesseract/wiki", 3 | "license": "Apache-2.0", 4 | "version": "4.1.0-elag2019", 5 | "description": "Open Source OCR Engine", 6 | "architecture": { 7 | "64bit": { 8 | "url": "https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w64-setup-v4.1.0-elag2019.exe#/dl.7z", 9 | "hash": "75b8a8b8d458a01d675f9493a67b22225cc6f71b13c20e8337a155aa0b403510" 10 | }, 11 | "32bit": { 12 | "url": "https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w32-setup-v4.1.0-elag2019.exe#/dl.7z", 13 | "hash": "082cd4b852f512cff544721df109721188667f84c383782d6828e6cba844db4b" 14 | } 15 | }, 16 | "bin": [ 17 | "ambiguous_words.exe", 18 | "classifier_tester.exe", 19 | "cntraining.exe", 20 | "combine_lang_model.exe", 21 | "combine_tessdata.exe", 22 | "dawg2wordlist.exe", 23 | "lstmeval.exe", 24 | "lstmtraining.exe", 25 | "merge_unicharsets.exe", 26 | "mftraining.exe", 27 | "set_unicharset_properties.exe", 28 | "shapeclustering.exe", 29 | "tesseract.exe", 30 | "text2image.exe", 31 | "unicharset_extractor.exe", 32 | "wordlist2dawg.exe" 33 | ], 34 | "env_set": { 35 | "TESSDATA_PREFIX": "$persist_dir\\tessdata" 36 | }, 37 | "persist": "tessdata", 38 | "notes": [ 39 | "Recognition data files can be installed via \"scoop install tesseract-languages\"", 40 | "or downloaded manually from https://github.com/tesseract-ocr/tessdata_fast" 41 | ], 42 | "suggest": { 43 | "tesseract-languages": "tesseract-languages" 44 | }, 45 | "checkver": { 46 | "url": "https://digi.bib.uni-mannheim.de/tesseract/?C=M;O=D", 47 | "regex": "tesseract-ocr-w32-setup-v([\\w.-]+)\\.exe" 48 | }, 49 | "autoupdate": { 50 | "architecture": { 51 | "64bit": { 52 | "url": "https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w64-setup-v$version.exe#/dl.7z" 53 | }, 54 | "32bit": { 55 | "url": "https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w32-setup-v$version.exe#/dl.7z" 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /bootstrap/win_bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Windows bootstrap 3 | 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | NC='\033[0m' # No Color 7 | CWD=$(powershell -Command "(Get-Location).Path") 8 | 9 | install_tesseract () { 10 | powershell -Command "scoop install "${CWD}"\bootstrap\tesseract.json" 11 | } 12 | 13 | echo -e "\n${RED}##### Starting Windows bootstrap #####${NC}\n" 14 | 15 | echo -e "${GREEN} ---> Installing Scoop package management #####${NC}\n" 16 | if command -v scoop &>/dev/null; then 17 | echo -e "${GREEN} ---> Skipping Scoop library management install. Already found in directory --> C:\Users\$(user_name)\scoop\shims\scoop${NC}\n" 18 | else 19 | powershell -Command "Set-ExecutionPolicy RemoteSigned -scope CurrentUser" 20 | powershell -Command "iex (new-object net.webclient).downloadstring('https://get.scoop.sh')" | grep 'Scoop is already installed.' &> /dev/null 21 | if [ $? != 0 ]; then 22 | echo -e "\n${RED} ---> Scoop package management installed. You need to restart the terminal and run the bootstrap.sh again.${NC}\n" 23 | echo -e "\n${RED} ---> First out of 2 restarts. Needed in order to use 'scoop' ${NC}\n" 24 | sleep 20 25 | exit 26 | fi 27 | fi 28 | 29 | powershell -Command "scoop bucket add versions" 30 | 31 | echo -e "\n${GREEN} ---> Updating Scoop package management #####${NC}\n" 32 | if [[ $(scoop status) =~ "WARN Scoop is out of date." ]]; then 33 | powershell -Command "scoop update" 34 | else 35 | echo -e "${GREEN} ---> Skipping Scoop update. Already latest version. ${NC}\n" 36 | fi 37 | 38 | echo -e "\n${GREEN}---> Installing/updating 7zip #####${NC}\n" 39 | if [[ $(scoop list | grep "7zip") =~ 7zip ]]; then 40 | echo -e "${GREEN} ---> Skipping 7zip install. Already installed. ${NC}\n" 41 | else 42 | powershell -Command "scoop install 7zip" 43 | fi 44 | 45 | powershell -Command "scoop bucket add extras" 46 | 47 | echo -e "\n${GREEN} ---> Installing Firefox #####${NC}\n" 48 | if [[ $(scoop list | grep "firefox") =~ firefox ]]; then 49 | echo -e "${GREEN} ---> Skipping firefox install. Already installed. Update Firefox ${NC}\n" 50 | powershell -Command "scoop update firefox" 51 | else 52 | powershell -Command "scoop install firefox" 53 | fi 54 | 55 | echo -e "\n${GREEN} ---> Installing Git #####${NC}\n" 56 | if [[ $(scoop list | grep "git") =~ git ]]; then 57 | echo -e "${GREEN} ---> Skipping Git install. Already installed. Update git and openssh ${NC}\n" 58 | powershell -Command "scoop update git" 59 | powershell -Command "scoop update openssh" 60 | else 61 | powershell -Command "scoop install git" 62 | powershell -Command "scoop install openssh" 63 | fi 64 | 65 | 66 | echo -e "\n${GREEN} ---> Installing which #####${NC}\n" 67 | if [[ $(scoop list | grep "which") =~ which ]]; then 68 | echo -e "${GREEN} ---> Skipping which install. Already installed. Update which ${NC}\n" 69 | powershell -Command "scoop update which" 70 | else 71 | powershell -Command "scoop install which" 72 | fi 73 | 74 | echo -e "\n${GREEN} ---> Installing sudo #####${NC}\n" 75 | if [[ $(scoop list | grep "sudo") =~ sudo ]]; then 76 | echo -e "${GREEN} ---> Skipping sudo install. Already installed. Update sudo${NC}\n" 77 | powershell -Command "scoop update sudo" 78 | else 79 | powershell -Command "scoop install sudo" 80 | fi 81 | 82 | if [[ $1 =~ ^[Yy]$ ]] 83 | then 84 | echo -e "\n${GREEN} ---> Installing Tesseract 4 ${NC} \n" 85 | if command -v tesseract &>/dev/null; then 86 | echo -e "${GREEN} ---> Tesseract already installed. ${NC}\n" 87 | echo -e "${GREEN} ---> Checking Tesseract version. ${NC}\n" 88 | if [[ $(tesseract -v | grep "tesseract 3.05") =~ 3 ]]; then 89 | echo -e "${RED} ---> You have Tesseract 3, removing and installing Tesseract 4.${NC}\n" 90 | powershell -Command "scoop uninstall tesseract" # If Scoop does not recognize tesseract3 command 91 | powershell -Command "scoop uninstall tesseract3" 92 | install_tesseract 93 | else 94 | echo -e "${GREEN} ---> Tesseract is the correct version. ${NC}\n" 95 | fi 96 | else 97 | install_tesseract 98 | fi 99 | fi 100 | 101 | echo -e "\n${GREEN} ---> installing/updating Python 3.7 #####${NC}\n" 102 | if command -v python3 &>/dev/null; then 103 | if [[ $(python3 --version | grep "Python 3.7") =~ 3.7 ]]; then 104 | echo -e "\n${GREEN} ---> Skipping Python 3.7 install. Already installed. ${NC}\n" 105 | elif command -v python3.7 &>/dev/null; then 106 | echo -e "\n${GREEN} ---> Verified for specific python3.7. Skipped install. Already installed. ${NC}\n" 107 | else 108 | echo -e "${GREEN} ---> Update to Python 3.7 . ${NC}\n" 109 | powershell -Command "scoop update python37" | grep 'bucket already exists.' &> /dev/null 110 | fi 111 | else 112 | echo -e "\n${GREEN} ---> Installing Python 3.7 #####${NC}\n" 113 | powershell -Command "scoop install python37" | grep 'bucket already exists.' &> /dev/null 114 | if [ $? != 0 ]; then 115 | echo -e "\n${RED} ---> Python 3.7 now installed. You need to restart the terminal one more time and run the bootstrap.sh again to complete the install.${NC}\n" 116 | echo -e "\n${RED} ---> Last restart needed in order to use 'python3' and install python3 dependent packages ${NC}\n" 117 | sleep 20 118 | exit 119 | fi 120 | fi 121 | 122 | echo -e "\n${GREEN} ---> installing/upgrading pipenv ${NC}\n" 123 | if command -v pipenv &>/dev/null; then 124 | powershell -Command "pip install --upgrade pip" 125 | powershell -Command "pip install --upgrade pipenv" 126 | else 127 | powershell -Command "pip install pipenv" 128 | fi 129 | -------------------------------------------------------------------------------- /moziris/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/__init__.py -------------------------------------------------------------------------------- /moziris/api/__init__.py: -------------------------------------------------------------------------------- 1 | from moziris.api.enums import ( 2 | Alignment, 3 | Button, 4 | Color, 5 | LanguageCode, 6 | Locales, 7 | OSPlatform, 8 | ) 9 | from moziris.api.errors import * 10 | from moziris.api.finder.finder import * 11 | from moziris.api.finder.pattern import Pattern 12 | from moziris.api.highlight.highlight_circle import * 13 | from moziris.api.highlight.highlight_rectangle import * 14 | from moziris.api.highlight.screen_highlight import * 15 | from moziris.api.keyboard.key import Key, KeyCode, KeyModifier 16 | from moziris.api.keyboard.keyboard_api import paste 17 | from moziris.api.keyboard.keyboard_util import ( 18 | is_lock_on, 19 | check_keyboard_state, 20 | get_active_modifiers, 21 | is_shift_character, 22 | ) 23 | from moziris.api.keyboard.keyboard import key_down, key_up, type 24 | from moziris.api.location import Location 25 | from moziris.api.mouse.mouse_controller import Mouse 26 | from moziris.api.mouse.mouse import * 27 | from moziris.api.os_helpers import * 28 | from moziris.api.rectangle import * 29 | from moziris.api.screen.display import Display, DisplayCollection 30 | from moziris.api.screen.region import Region 31 | from moziris.api.screen.region_utils import RegionUtils 32 | from moziris.api.screen.screen import * 33 | from moziris.api.settings import Settings 34 | -------------------------------------------------------------------------------- /moziris/api/enums.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import mozinfo 6 | 7 | from enum import Enum 8 | 9 | 10 | class Alignment(Enum): 11 | CENTER = "center" 12 | TOP_LEFT = "top_left" 13 | TOP_RIGHT = "top_right" 14 | BOTTOM_LEFT = "bottom_left" 15 | BOTTOM_RIGHT = "bottom_right" 16 | 17 | 18 | def _button_value(base_name, mouse_button): 19 | """Generates the value tuple for a :class:`Button` value. 20 | 21 | :param str base_name: The base name for the button. This shuld be a string 22 | like ``'kCGEventLeftMouse'``. 23 | 24 | :param int mouse_button: The mouse button ID. 25 | 26 | :return: a value tuple 27 | """ 28 | if mozinfo.os == "mac": 29 | import Quartz 30 | 31 | return ( 32 | tuple( 33 | getattr(Quartz, "%sMouse%s" % (base_name, name)) 34 | for name in ("Down", "Up", "Dragged") 35 | ), 36 | mouse_button, 37 | ) 38 | 39 | 40 | class Button(Enum): 41 | """The various buttons. 42 | """ 43 | 44 | unknown = None 45 | left = _button_value("kCGEventLeft", 0) 46 | middle = _button_value("kCGEventOther", 2) 47 | right = _button_value("kCGEventRight", 1) 48 | 49 | 50 | class Color(Enum): 51 | RED = "red" 52 | GREEN = "green" 53 | BLUE = "blue" 54 | BLACK = "black" 55 | WHITE = "white" 56 | 57 | 58 | class LanguageCode(Enum): 59 | AFRIKAANS = "afr" 60 | AMHARIC = "amh" 61 | ARABIC = "ara" 62 | ASSAMESE = "asm" 63 | AZERBAIJANI = "aze" 64 | AZERBAIJANI_CYRILIC = "aze-cyrl" 65 | BELARUSIAN = "bel" 66 | BENGALI = "ben" 67 | TIBETAN = "bod" 68 | BOSNIAN = "bos" 69 | BULGARIAN = "bul" 70 | CATALAN = "cat" 71 | CEBUANO = "ceb" 72 | CZECH = "ces" 73 | CHINESE_SIMPLIFIED = "chi-sim" 74 | CHINESE_TRADITIONAL = "chi-tra" 75 | CHEROKEE = "chr" 76 | WELSH = "cym" 77 | DANISH = "dan" 78 | DANISH_FRAKTUR = "dan-frak" 79 | GERMAN = "deu" 80 | GERMAN_FRAKTUR = "deu-frak" 81 | DZONGKHA = "dzo" 82 | MODERNGREEK = "ell" 83 | ENGLISH = "eng" 84 | MIDDLE_ENGLISH = "enm" 85 | ESPERANTO = "epo" 86 | ESTONIAN = "est" 87 | BASQUE = "eus" 88 | PERSIAN = "fas" 89 | FINNISH = "fin" 90 | FRENCH = "fra" 91 | FRANKISH = "frk" 92 | MIDDLE_FRENCH = "frm" 93 | IRISH = "gle" 94 | IRISHUNCIAL = "gle-uncial" 95 | GALICIAN = "glg" 96 | ANCIENT_GREEK = "grc" 97 | GUJARATI = "guj" 98 | HAITIAN = "hat" 99 | HEBREW = "heb" 100 | HINDI = "hin" 101 | CROATIAN = "hrv" 102 | HUNGARIAN = "hun" 103 | INUKTITUT = "iku" 104 | INDONESIAN = "ind" 105 | ICELANDIC = "isl" 106 | ITALIAN = "ita" 107 | JAVANESE = "jav" 108 | JAPANESE = "jpn" 109 | KANNADA = "kan" 110 | GEORGIAN = "kat" 111 | KAZAKH = "kaz" 112 | KHMER = "khm" 113 | KYRGYZ = "kir" 114 | KOREAN = "kor" 115 | KURDISH = "kur" 116 | LAO = "lao" 117 | LATIN = "lat" 118 | LATVIAN = "lav" 119 | LITHUANIAN = "lit" 120 | MALAYALAM = "mal" 121 | MARATHI = "mar" 122 | MACEDONIAN = "mkd" 123 | MALTESE = "mlt" 124 | MALAY = "msa" 125 | BURMESE = "mya" 126 | NEPALI = "nep" 127 | DUTCH = "nld" 128 | NORWEGIAN = "nor" 129 | ORIYA = "ori" 130 | PUNJABI = "pan" 131 | POLISH = "pol" 132 | PORTUGUESE = "por" 133 | PASHTO = "pus" 134 | ROMANIAN = "ron" 135 | RUSSIAN = "rus" 136 | SANSKRIT = "san" 137 | SINHALA = "sin" 138 | SLOVAK = "slk" 139 | SLOVAK_FRACTUR = "slk-frak" 140 | SLOVENIAN = "slv" 141 | SPANISH = "spa" 142 | ALBANIAN = "sqi" 143 | SERBIAN = "srp" 144 | SERBIAN_LATIN = "srp-latn" 145 | SWAHILI = "swa" 146 | SWEDISH = "swe" 147 | SYRIAC = "syr" 148 | TAMIL = "tam" 149 | TELUGU = "tel" 150 | TAJIK = "tgk" 151 | TAGALOG = "tgl" 152 | THAI = "tha" 153 | TIGRINYA = "tir" 154 | TURKISH = "tur" 155 | UYGHUR = "uig" 156 | UKRANIAN = "ukr" 157 | URDU = "urd" 158 | UZBEK = "uzb" 159 | UZBEK_CYRILLIC = "uzb-cyrl" 160 | VIETNAMESE = "vie" 161 | YIDDISH = "yid" 162 | 163 | 164 | class Locales(str, Enum): 165 | ARABIC = "ar" 166 | CHINESE = "zh-CN" 167 | ENGLISH = "en-US" 168 | FRENCH = "fr" 169 | GERMAN = "de" 170 | JAPANESE = "ja" 171 | KOREAN = "ko" 172 | POLISH = "pl" 173 | PORTUGUESE = "pt-PT" 174 | ROMANIAN = "ro" 175 | RUSSIAN = "ru" 176 | SPANISH = "es-ES" 177 | TURKISH = "tr" 178 | VIETNAMESE = "vi" 179 | 180 | ALL = [ 181 | ARABIC, 182 | CHINESE, 183 | ENGLISH, 184 | FRENCH, 185 | GERMAN, 186 | JAPANESE, 187 | KOREAN, 188 | POLISH, 189 | PORTUGUESE, 190 | ROMANIAN, 191 | RUSSIAN, 192 | SPANISH, 193 | TURKISH, 194 | VIETNAMESE, 195 | ] 196 | 197 | 198 | class MatchTemplateType(Enum): 199 | SINGLE = 0 200 | MULTIPLE = 1 201 | 202 | 203 | class OSPlatform(str, Enum): 204 | WINDOWS = "win" 205 | LINUX = "linux" 206 | MAC = "osx" 207 | 208 | ALL = [WINDOWS, LINUX, MAC] 209 | -------------------------------------------------------------------------------- /moziris/api/errors.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | class FindError(Exception): 7 | """Exception raised when a Location, Pattern, image or text is not found.""" 8 | 9 | def __init__(self, message): 10 | """Create an exception instance.""" 11 | Exception.__init__(self, message) 12 | 13 | 14 | class ConfigError(Exception): 15 | """Exception raised if there is unexpected behavior when manipulating config files.""" 16 | 17 | def __init__(self, message): 18 | """Create an exception instance.""" 19 | Exception.__init__(self, message) 20 | 21 | 22 | class APIHelperError(Exception): 23 | """Exception raised when an API helper returns an error.""" 24 | 25 | def __init__(self, message): 26 | """Create an exception instance.""" 27 | Exception.__init__(self, message) 28 | 29 | 30 | class EmailError(Exception): 31 | """Exception raised when an email error occurs.""" 32 | 33 | def __init__(self, message): 34 | """Create an exception instance.""" 35 | Exception.__init__(self, message) 36 | 37 | 38 | class ScreenshotError(Exception): 39 | """Exception raised when an screenshot error occurs.""" 40 | 41 | def __init__(self, message): 42 | """Create an exception instance.""" 43 | Exception.__init__(self, message) 44 | -------------------------------------------------------------------------------- /moziris/api/finder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/api/finder/__init__.py -------------------------------------------------------------------------------- /moziris/api/finder/image_search.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import datetime 7 | import logging 8 | 9 | import cv2 10 | import numpy as np 11 | 12 | 13 | try: 14 | import Image 15 | except ImportError: 16 | from PIL import Image 17 | 18 | from moziris.api.enums import MatchTemplateType 19 | from moziris.api.errors import ScreenshotError 20 | from moziris.api.finder.pattern import Pattern 21 | from moziris.api.location import Location 22 | from moziris.api.rectangle import Rectangle 23 | from moziris.api.save_debug_image.save_image import save_debug_image 24 | from moziris.api.screen.display import DisplayCollection 25 | from moziris.api.screen.screenshot_image import ScreenshotImage 26 | from moziris.api.settings import Settings 27 | 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | FIND_METHOD = cv2.TM_CCOEFF_NORMED 32 | last_image_write_time = datetime.datetime.now() 33 | 34 | 35 | def _is_pattern_size_correct(pattern, region): 36 | """validates that the pattern is inside the region.""" 37 | if region is None: 38 | return True 39 | 40 | p_width, p_height = pattern.get_size() 41 | r_width = region.width 42 | r_height = region.height 43 | is_correct = True 44 | 45 | if p_width > r_width: 46 | logger.warning( 47 | "Pattern Width (%s) greater than Region/Screenshot Width (%s)" 48 | % (p_width, r_width) 49 | ) 50 | is_correct = False 51 | if p_height > r_height: 52 | logger.warning( 53 | "Pattern Height (%s) greater than Region/Screenshot Height (%s)" 54 | % (p_height, r_height) 55 | ) 56 | is_correct = False 57 | return is_correct 58 | 59 | 60 | def match_template( 61 | pattern: Pattern, 62 | region: Rectangle = None, 63 | match_type: MatchTemplateType = MatchTemplateType.SINGLE, 64 | ): 65 | """Find a pattern in a Region or full screen 66 | 67 | :param Pattern pattern: Image details 68 | :param Region region: Region object. 69 | :param MatchTemplateType match_type: Type of match_template (single or multiple) 70 | :return: Location. 71 | """ 72 | if region is None: 73 | region = DisplayCollection[0].bounds 74 | 75 | locations_list = [] 76 | save_img_location_list = [] 77 | if not isinstance(match_type, MatchTemplateType): 78 | logger.warning( 79 | "%s should be an instance of `%s`" % (match_type, MatchTemplateType) 80 | ) 81 | return [] 82 | try: 83 | stack_image = ScreenshotImage( 84 | region=region, screen_id=_region_in_display_list(region) 85 | ) 86 | precision = pattern.similarity 87 | if precision == 0.99: 88 | logger.debug("Searching image with similarity %s" % precision) 89 | res = cv2.matchTemplate( 90 | stack_image.get_color_array(), pattern.get_color_array(), FIND_METHOD 91 | ) 92 | else: 93 | logger.debug("Searching image with similarity %s" % precision) 94 | res = cv2.matchTemplate( 95 | stack_image.get_gray_array(), pattern.get_gray_array(), FIND_METHOD 96 | ) 97 | 98 | if match_type is MatchTemplateType.SINGLE: 99 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) 100 | logger.debug("Min location %s and max location %s" % (min_val, max_val)) 101 | if max_val >= precision: 102 | locations_list.append( 103 | Location(max_loc[0] + region.x, max_loc[1] + region.y) 104 | ) 105 | save_img_location_list.append(Location(max_loc[0], max_loc[1])) 106 | elif match_type is MatchTemplateType.MULTIPLE: 107 | loc = np.where(res >= precision) 108 | for pt in zip(*loc[::-1]): 109 | save_img_location = Location(pt[0], pt[1]) 110 | location = Location(pt[0] + region.x, pt[1] + region.y) 111 | save_img_location_list.append(save_img_location) 112 | locations_list.append(location) 113 | 114 | # Limit debug image creation to one per second to avoid creating unnecessary images. 115 | global last_image_write_time 116 | next_write_time = last_image_write_time + datetime.timedelta(seconds=1) 117 | if datetime.datetime.now() > next_write_time: 118 | save_debug_image(pattern, stack_image, save_img_location_list) 119 | last_image_write_time = datetime.datetime.now() 120 | 121 | except ScreenshotError: 122 | logger.warning("Screenshot failed.") 123 | return [] 124 | 125 | return locations_list 126 | 127 | 128 | def _region_in_display_list(region=None): 129 | r_x = region.x 130 | r_y = region.y 131 | r_w = region.width 132 | r_h = region.height 133 | 134 | for index, display in enumerate(DisplayCollection): 135 | d_x = display.bounds.x 136 | d_y = display.bounds.y 137 | d_w = display.bounds.width 138 | d_h = display.bounds.height 139 | 140 | if ( 141 | r_x >= d_x 142 | and r_x - d_x + r_w <= d_w 143 | and r_y >= d_y 144 | and r_y - d_y + r_h <= d_h 145 | ): 146 | return index 147 | 148 | 149 | def image_find(pattern, timeout=None, region=None): 150 | """ Search for an image in a Region or full screen. 151 | 152 | :param Pattern pattern: Name of the searched image. 153 | :param timeout: Number as maximum waiting time in seconds. 154 | :param Region region: Region object. 155 | :return: Location. 156 | """ 157 | if not _is_pattern_size_correct(pattern, region): 158 | return None 159 | 160 | if timeout is None: 161 | timeout = Settings.auto_wait_timeout 162 | 163 | start_time = datetime.datetime.now() 164 | end_time = start_time + datetime.timedelta(seconds=timeout) 165 | 166 | while start_time < end_time: 167 | time_remaining = end_time - start_time 168 | logger.debug( 169 | "Image find: {} - {} seconds remaining".format( 170 | pattern.get_filename(), time_remaining 171 | ) 172 | ) 173 | pos = match_template(pattern, region, MatchTemplateType.SINGLE) 174 | start_time = datetime.datetime.now() 175 | 176 | if len(pos) == 1: 177 | return pos[0] 178 | return None 179 | 180 | 181 | def image_vanish( 182 | pattern: Pattern, timeout: float = None, region: Rectangle = None 183 | ) -> None or bool: 184 | """ Search if an image is NOT in a Region or full screen. 185 | 186 | :param Pattern pattern: Name of the searched image. 187 | :param timeout: Number as maximum waiting time in seconds. 188 | :param Region region: Region object. 189 | :return: Location. 190 | """ 191 | if not _is_pattern_size_correct(pattern, region): 192 | return None 193 | 194 | pattern_found = True 195 | 196 | start_time = datetime.datetime.now() 197 | end_time = start_time + datetime.timedelta(seconds=timeout) 198 | 199 | while pattern_found and start_time < end_time: 200 | time_remaining = end_time - start_time 201 | logger.debug( 202 | "Image vanish: {} - {} seconds remaining".format( 203 | pattern.get_filename(), time_remaining 204 | ) 205 | ) 206 | image_found = match_template(pattern, region, MatchTemplateType.SINGLE) 207 | if len(image_found) == 0: 208 | pattern_found = False 209 | else: 210 | pattern_found = True 211 | start_time = datetime.datetime.now() 212 | 213 | return None if pattern_found else True 214 | -------------------------------------------------------------------------------- /moziris/api/finder/text_search.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import difflib 7 | import logging 8 | 9 | import pytesseract 10 | from PIL import ImageEnhance 11 | 12 | from moziris.api.rectangle import Rectangle 13 | from moziris.api.save_debug_image.save_image import save_debug_ocr_image 14 | from moziris.api.screen.display import DisplayCollection 15 | from moziris.api.screen.screenshot_image import ScreenshotImage 16 | 17 | TRY_RESIZE_IMAGES = 2 18 | OCR_RESULT_COLUMNS_COUNT = 12 19 | WORD_PROXIMITY = 5 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | cutoffs = { 24 | "string": {"min_cutoff": 0.7, "max_cutoff": 0.9, "step": 0.1}, 25 | "digit": {"min_cutoff": 0.75, "max_cutoff": 0.9, "step": 0.05}, 26 | } 27 | 28 | digit_chars = [".", "%", ","] 29 | 30 | 31 | def _is_similar_result(result_list, x: int, y: int, pixels: int): 32 | """Checks if current result is similar to previous results based on pixel proximity.""" 33 | if len(result_list) == 0: 34 | return False 35 | 36 | for result in result_list: 37 | if (x - pixels <= result.x <= x + pixels) and ( 38 | y - pixels <= result.y <= y + pixels 39 | ): 40 | return True 41 | return False 42 | 43 | 44 | def _is_next_word(prev_word, x, y): 45 | """Checks if previous - current word are located correctly.""" 46 | if (prev_word.x + prev_word.width + 10 >= x) and (y - 5 <= prev_word.y <= y + 5): 47 | return True 48 | return False 49 | 50 | 51 | def _replace_multiple(main_string, replace_string, replace_with_string): 52 | """Replace a string with a list of substrings.""" 53 | for elem in replace_string: 54 | if elem in main_string: 55 | main_string = main_string.replace(elem, replace_with_string) 56 | 57 | return main_string 58 | 59 | 60 | def _create_rectangle_from_ocr_data(data, scale): 61 | """Generates a Rectangle object based on OCR processed data and image scale.""" 62 | x = int(int(data[6]) / (scale * (1 if scale - 1 == 0 else scale - 1))) 63 | y = int(int(data[7]) / (scale * (1 if scale - 1 == 0 else scale - 1))) 64 | width = int(int(data[8]) / (scale * (1 if scale - 1 == 0 else scale - 1))) 65 | height = int(int(data[9]) / (scale * (1 if scale - 1 == 0 else scale - 1))) 66 | return Rectangle(x, y, width, height) 67 | 68 | 69 | def _assemble_results(result_list): 70 | """Merge all Rectangle objects into one that contains them all.""" 71 | from operator import attrgetter 72 | 73 | x = min(result_list, key=attrgetter("x")).x 74 | y = min(result_list, key=attrgetter("y")).y 75 | 76 | x_max = max(result_list, key=attrgetter("x")).x 77 | width = max([x.width for x in result_list if x.x == x_max]) + x_max - x 78 | 79 | y_max = max(result_list, key=attrgetter("y")).y 80 | height = max([x.height for x in result_list if x.y == y_max]) + y_max - y 81 | return Rectangle(x, y, width, height) 82 | 83 | 84 | def _get_processed_data(image_list): 85 | """Get all OCR data from images.""" 86 | data = [] 87 | for index_image, stack_image in enumerate(image_list): 88 | for index_scale, scale in enumerate(range(1, TRY_RESIZE_IMAGES + 1)): 89 | stack_image = stack_image.resize( 90 | [stack_image.width * scale, stack_image.height * scale] 91 | ) 92 | processed_data = pytesseract.image_to_data(stack_image) 93 | for index_data, line in enumerate(processed_data.split("\n")[1:]): 94 | d = line.split() 95 | if len(d) == OCR_RESULT_COLUMNS_COUNT: 96 | d.append(scale) 97 | data.append(d) 98 | return data 99 | 100 | 101 | def _get_first_word(word, data_list): 102 | """Finds all occurrences of the first searched word.""" 103 | words_found = [] 104 | cutoff_type = ( 105 | "digit" if _replace_multiple(word, digit_chars, "").isdigit() else "string" 106 | ) 107 | for data in data_list: 108 | cutoff = cutoffs[cutoff_type]["max_cutoff"] 109 | while cutoff >= cutoffs[cutoff_type]["min_cutoff"]: 110 | if difflib.get_close_matches(word, [data[11]], cutoff=cutoff): 111 | try: 112 | vd = _create_rectangle_from_ocr_data(data, data[12]) 113 | if not _is_similar_result(words_found, vd.x, vd.y, WORD_PROXIMITY): 114 | words_found.append(vd) 115 | except ValueError: 116 | continue 117 | cutoff -= cutoffs[cutoff_type]["step"] 118 | return words_found 119 | 120 | 121 | def _text_search(text, region: Rectangle = None, multiple_search=False): 122 | """Search text in region or screen.""" 123 | if region is None: 124 | region = DisplayCollection[0].bounds 125 | 126 | logger.debug("Text find: '{}'".format(text)) 127 | img = ScreenshotImage(region=region) 128 | raw_gray_image = img.get_gray_image() 129 | enhanced_image = ImageEnhance.Contrast(img.get_gray_image()).enhance(10.0) 130 | data_list = _get_processed_data([raw_gray_image, enhanced_image]) 131 | first_word_occurrences = _get_first_word(text.split()[0], data_list) 132 | word_count = len(text.split()) 133 | 134 | if not multiple_search: 135 | if len(first_word_occurrences) == 0: 136 | first_word = [] 137 | else: 138 | if word_count == 1: 139 | first_word = [first_word_occurrences[0]] 140 | else: 141 | first_word = first_word_occurrences 142 | else: 143 | first_word = first_word_occurrences 144 | 145 | if word_count == 1: 146 | for result in first_word: 147 | result.x += region.x 148 | result.y += region.y 149 | return first_word 150 | 151 | sentence = [] 152 | for data in first_word: 153 | sentence.append([data]) 154 | 155 | for index, word in enumerate(first_word): 156 | for index_word, word_to_search in enumerate(text.split()[1:]): 157 | found = False 158 | cutoff_type = ( 159 | "digit" 160 | if _replace_multiple(word_to_search, digit_chars, "").isdigit() 161 | else "string" 162 | ) 163 | for d in data_list: 164 | if not found: 165 | cutoff = cutoffs[cutoff_type]["max_cutoff"] 166 | while cutoff >= cutoffs[cutoff_type]["min_cutoff"]: 167 | if difflib.get_close_matches( 168 | word_to_search, [d[11]], cutoff=cutoff 169 | ): 170 | try: 171 | vd = _create_rectangle_from_ocr_data(d, d[12]) 172 | if _is_next_word( 173 | sentence[index][-1], vd.x, vd.y 174 | ) and not _is_similar_result( 175 | sentence[index], vd.x, vd.y, WORD_PROXIMITY 176 | ): 177 | sentence[index].append(vd) 178 | found = True 179 | except ValueError: 180 | continue 181 | cutoff -= cutoffs[cutoff_type]["step"] 182 | final_result = [] 183 | for words in sentence: 184 | if len(words) == word_count: 185 | final_result.append(_assemble_results(words)) 186 | 187 | save_debug_ocr_image(text, img, final_result) 188 | 189 | for result in final_result: 190 | result.x += region.x 191 | result.y += region.y 192 | 193 | return final_result 194 | 195 | 196 | def text_find(text, region): 197 | return _text_search(text, region, False) 198 | 199 | 200 | def text_find_all(text, region): 201 | return _text_search(text, region, True) 202 | -------------------------------------------------------------------------------- /moziris/api/highlight/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/api/highlight/__init__.py -------------------------------------------------------------------------------- /moziris/api/highlight/highlight_circle.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | from moziris.api.enums import Color 7 | from moziris.api.settings import Settings 8 | 9 | 10 | class HighlightCircle: 11 | def __init__( 12 | self, 13 | center_x: int, 14 | center_y: int, 15 | radius: int, 16 | color: Color = None, 17 | thickness: int = None, 18 | ): 19 | 20 | if thickness is None: 21 | thickness = Settings.highlight_thickness 22 | 23 | if color is None: 24 | color = Settings.highlight_color 25 | 26 | self.center_x = center_x 27 | self.center_y = center_y 28 | self.radius = radius 29 | self.color = color 30 | self.thickness = thickness 31 | -------------------------------------------------------------------------------- /moziris/api/highlight/highlight_rectangle.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | from moziris.api.enums import Color 7 | from moziris.api.settings import Settings 8 | from moziris.api.rectangle import Rectangle 9 | 10 | 11 | class HighlightRectangle(Rectangle): 12 | def __init__( 13 | self, 14 | x: int, 15 | y: int, 16 | width: int, 17 | height: int, 18 | color: Color = None, 19 | thickness: int = None, 20 | ): 21 | Rectangle.__init__(self, x, y, width, height) 22 | 23 | if thickness is None: 24 | thickness = Settings.highlight_thickness 25 | 26 | if color is None: 27 | color = Settings.highlight_color 28 | 29 | self.color = color 30 | self.thickness = thickness 31 | -------------------------------------------------------------------------------- /moziris/api/highlight/screen_highlight.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | from tkinter import * 7 | 8 | from moziris.api.enums import Color 9 | from moziris.api.highlight.highlight_circle import HighlightCircle 10 | from moziris.api.highlight.highlight_rectangle import HighlightRectangle 11 | from moziris.api.settings import Settings 12 | from moziris.api.os_helpers import MULTI_MONITOR_AREA, OSHelper 13 | 14 | 15 | def _draw_circle(canvas, x, y, r, **kwargs): 16 | return canvas.create_oval(x - r, y - r, x + r, y + r, **kwargs) 17 | 18 | 19 | def _draw_rectangle(canvas, x, y, w, h, **kwargs): 20 | rectangle = canvas.create_rectangle(0, 0, w, h, **kwargs) 21 | canvas.move(rectangle, x, y) 22 | 23 | 24 | class ScreenHighlight(object): 25 | def draw_circle(self, circle: HighlightCircle): 26 | return self.canvas.draw_circle( 27 | circle.center_x, 28 | circle.center_y, 29 | circle.radius, 30 | outline=circle.color, 31 | width=circle.thickness, 32 | ) 33 | 34 | def draw_rectangle(self, rect: HighlightRectangle): 35 | return self.canvas.draw_rectangle( 36 | rect.x, 37 | rect.y, 38 | rect.width, 39 | rect.height, 40 | outline=rect.color, 41 | width=rect.thickness, 42 | ) 43 | 44 | def quit(self): 45 | self.root.quit() 46 | self.root.destroy() 47 | 48 | def render(self, duration=None): 49 | 50 | if duration is None: 51 | duration = Settings.highlight_duration 52 | 53 | self.root.after(duration * 1000, self.quit) 54 | self.root.mainloop() 55 | 56 | def __init__(self): 57 | self.root = Tk() 58 | self.root.overrideredirect(1) 59 | s_width = MULTI_MONITOR_AREA["width"] 60 | s_height = MULTI_MONITOR_AREA["height"] 61 | 62 | self.root.wm_attributes("-topmost", True) 63 | 64 | canvas = Canvas( 65 | self.root, 66 | width=s_width, 67 | height=s_height, 68 | borderwidth=0, 69 | highlightthickness=0, 70 | bg=Color.BLACK.value, 71 | ) 72 | canvas.grid() 73 | 74 | Canvas.draw_circle = _draw_circle 75 | Canvas.draw_rectangle = _draw_rectangle 76 | 77 | if OSHelper.is_mac(): 78 | self.root.wm_attributes("-fullscreen", 1) 79 | self.root.wm_attributes("-transparent", True) 80 | self.root.config(bg="systemTransparent") 81 | canvas.config(bg="systemTransparent") 82 | canvas.pack() 83 | 84 | if OSHelper.is_windows(): 85 | self.root.wm_attributes("-transparentcolor", Color.BLACK.value) 86 | 87 | if OSHelper.is_linux(): 88 | self.root.wait_visibility(self.root) 89 | self.root.attributes("-alpha", 0.7) 90 | self.canvas = canvas 91 | -------------------------------------------------------------------------------- /moziris/api/keyboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/api/keyboard/__init__.py -------------------------------------------------------------------------------- /moziris/api/keyboard/key.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import logging 7 | from enum import Enum 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class KeyCode: 13 | def __init__( 14 | self, label: str, value=None, x11key: str = None, reserved: bool = True 15 | ): 16 | self.label = label 17 | self.value = value 18 | self.x11key = x11key 19 | self.is_reserved = reserved 20 | 21 | def __str__(self): 22 | return self.label 23 | 24 | 25 | class Key(Enum): 26 | """Class with multiple instances of the KeyCode class.""" 27 | 28 | ADD = KeyCode("add", None, "KP_Add") 29 | ALT = KeyCode("alt", 1 << 3, "Alt_L") 30 | BACKSPACE = KeyCode("backspace", None, "BackSpace") 31 | CAPS_LOCK = KeyCode("capslock", None, "Caps_Lock") 32 | CMD = KeyCode("command", 1 << 2, "Command") 33 | CTRL = KeyCode("ctrl", 1 << 1, "Control_L") 34 | DELETE = KeyCode("del", None, "Delete") 35 | DIVIDE = KeyCode("divide", None, "KP_Divide") 36 | DOWN = KeyCode("down", None, "Down") 37 | ENTER = KeyCode("\n", None, "Return") 38 | END = KeyCode("end", None, "End") 39 | ESC = KeyCode("esc", None, "Escape") 40 | F1 = KeyCode("f1", None, "F1") 41 | F2 = KeyCode("f2", None, "F2") 42 | F3 = KeyCode("f3", None, "F3") 43 | F4 = KeyCode("f4", None, "F4") 44 | F5 = KeyCode("f5", None, "F5") 45 | F6 = KeyCode("f6", None, "F6") 46 | F7 = KeyCode("f7", None, "F7") 47 | F8 = KeyCode("f8", None, "F8") 48 | F9 = KeyCode("f9", None, "F9") 49 | F10 = KeyCode("f10", None, "F10") 50 | F11 = KeyCode("f11", None, "F11") 51 | F12 = KeyCode("f12", None, "F12") 52 | F13 = KeyCode("f13", None, "F13") 53 | F14 = KeyCode("f14", None, "F14") 54 | F15 = KeyCode("f15", None, "F15") 55 | HOME = KeyCode("home", None, "Home") 56 | INSERT = KeyCode("insert", None, "Insert") 57 | LEFT = KeyCode("left", None, "Left") 58 | META = KeyCode("winleft", 1 << 2, "Super_L") 59 | MINUS = KeyCode("subtract", None) 60 | MULTIPLY = KeyCode("multiply", None, "KP_Multiply") 61 | NUM0 = KeyCode("num0", None, "KP_0") 62 | NUM1 = KeyCode("num1", None, "KP_1") 63 | NUM2 = KeyCode("num2", None, "KP_2") 64 | NUM3 = KeyCode("num3", None, "KP_3") 65 | NUM4 = KeyCode("num4", None, "KP_4") 66 | NUM5 = KeyCode("num5", None, "KP_5") 67 | NUM6 = KeyCode("num6", None, "KP_6") 68 | NUM7 = KeyCode("num7", None, "KP_7") 69 | NUM8 = KeyCode("num8", None, "KP_8") 70 | NUM9 = KeyCode("num9", None, "KP_9") 71 | NUM_LOCK = KeyCode("numlock", None, "Num_Lock") 72 | PAGE_DOWN = KeyCode("pagedown", None, "Page_Down") 73 | PAGE_UP = KeyCode("pageup", None, "Page_Up") 74 | PAUSE = KeyCode("pause", None, "Pause") 75 | PRINT_SCREEN = KeyCode("printscreen", None, "Print") 76 | RIGHT = KeyCode("right", None, "Right") 77 | SCROLL_LOCK = KeyCode("scrolllock", None, "Scroll_Lock") 78 | SEPARATOR = KeyCode("separator", None, "KP_Separator") 79 | SHIFT = KeyCode("shift", 1 << 0, "Shift_L") 80 | SPACE = KeyCode(" ", None, "space") 81 | TAB = KeyCode("\t", None, "Tab") 82 | UP = KeyCode("up", None, "Up") 83 | WIN = KeyCode("win", 1 << 2) 84 | 85 | ACCEPT = KeyCode("accept", None) 86 | ALT_LEFT = KeyCode("altleft", None, "Alt_L") 87 | ALT_RIGHT = KeyCode("altright", None, "Alt_R") 88 | APPS = KeyCode("apps", None, "Super_L") 89 | BROWSER_BACK = KeyCode("browserback", None) 90 | BROWSER_FAVORITES = KeyCode("browserfavorites", None) 91 | BROWSER_FORWARD = KeyCode("browserforward", None) 92 | BROWSER_HOME = KeyCode("browserhome", None) 93 | BROWSER_REFRESH = KeyCode("browserrefresh", None) 94 | BROWSER_SEARCH = KeyCode("browsersearch", None) 95 | BROWSER_STOP = KeyCode("browserstop", None) 96 | CLEAR = KeyCode("clear", None) 97 | COMMAND = KeyCode("command", None) 98 | CONVERT = KeyCode("convert", None) 99 | CTRL_LEFT = KeyCode("ctrlleft", None, "Control_L") 100 | CTRL_RIGHT = KeyCode("ctrlright", None, "Control_R") 101 | DECIMAL = KeyCode("decimal", None, "KP_Decimal") 102 | EXECUTE = KeyCode("execute", None, "Execute") 103 | F16 = KeyCode("f16", None, "F16") 104 | F17 = KeyCode("f17", None, "F17") 105 | F18 = KeyCode("f18", None, "F18") 106 | F19 = KeyCode("f19", None, "F19") 107 | F20 = KeyCode("f20", None, "F20") 108 | F21 = KeyCode("f21", None, "F21") 109 | F22 = KeyCode("f22", None, "F22") 110 | F23 = KeyCode("f23", None, "F23") 111 | F24 = KeyCode("f24", None, "F24") 112 | FINAL = KeyCode("final", None) 113 | FN = KeyCode("fn", None) 114 | HANGUEL = KeyCode("hanguel", None) 115 | HANGUL = KeyCode("hangul", None) 116 | HANJA = KeyCode("hanja", None) 117 | HELP = KeyCode("help", None, "Help") 118 | JUNJA = KeyCode("junja", None) 119 | KANA = KeyCode("kana", None) 120 | KANJI = KeyCode("kanji", None) 121 | LAUNCH_APP1 = KeyCode("launchapp1", None) 122 | LAUNCH_APP2 = KeyCode("launchapp2", None) 123 | LAUNCH_MAIL = KeyCode("launchmail", None) 124 | LAUNCH_MEDIA_SELECT = KeyCode("launchmediaselect", None) 125 | MODE_CHANGE = KeyCode("modechange", None) 126 | NEXT_TRACK = KeyCode("nexttrack", None) 127 | NONCONVERT = KeyCode("nonconvert", None) 128 | OPTION = KeyCode("option", None) 129 | OPTION_LEFT = KeyCode("optionleft", None) 130 | OPTION_RIGHT = KeyCode("optionright", None) 131 | PGDN = KeyCode("pgdn", None) 132 | PGUP = KeyCode("pgup", None) 133 | PLAY_PAUSE = KeyCode("playpause", None) 134 | PREV_TRACK = KeyCode("prevtrack", None) 135 | PRINT = KeyCode("print", None, "Print") 136 | PRNT_SCRN = KeyCode("prntscrn", None, "Print") 137 | PRTSC = KeyCode("prtsc", None) 138 | PRTSCR = KeyCode("prtscr", None) 139 | RETURN = KeyCode("return", None) 140 | SELECT = KeyCode("select", None, "Select") 141 | SHIFT_LEFT = KeyCode("shiftleft", None, "Shift_L") 142 | SHIFT_RIGHT = KeyCode("shiftright", None, "Shift_R") 143 | SLEEP = KeyCode("sleep", None) 144 | STOP = KeyCode("stop", None) 145 | SUBTRACT = KeyCode("subtract", None, "KP_Subtract") 146 | VOLUME_DOWN = KeyCode("volumedown", None) 147 | VOLUME_MUTE = KeyCode("volumemute", None) 148 | VOLUME_UP = KeyCode("volumeup", None) 149 | WIN_LEFT = KeyCode("winleft", None, "Super_L") 150 | WIN_RIGHT = KeyCode("winright", None, "Super_R") 151 | YEN = KeyCode("yen", None) 152 | 153 | 154 | class KeyModifier(Enum): 155 | """Keyboard key variables.""" 156 | 157 | SHIFT = Key.SHIFT 158 | CTRL = Key.CTRL 159 | CMD = Key.CMD 160 | WIN = Key.WIN 161 | META = Key.META 162 | ALT = Key.ALT 163 | -------------------------------------------------------------------------------- /moziris/api/keyboard/keyboard_api.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import time 7 | 8 | import pyautogui 9 | import pyperclip 10 | 11 | from moziris.api.errors import FindError 12 | from moziris.api.keyboard.key import KeyModifier 13 | from moziris.api.keyboard.keyboard import type 14 | from moziris.api.os_helpers import OSHelper 15 | from moziris.api.settings import Settings 16 | 17 | DEFAULT_KEY_SHORTCUT_DELAY = 0.1 18 | pyautogui.FAILSAFE = False 19 | 20 | 21 | def paste(text: str): 22 | """ 23 | :param text: Text to be pasted. 24 | :return: None. 25 | """ 26 | 27 | pyperclip.copy(text) 28 | text_copied = False 29 | wait_scan_rate = float(Settings.wait_scan_rate) 30 | interval = 1 / wait_scan_rate 31 | max_attempts = int(Settings.auto_wait_timeout * wait_scan_rate) 32 | attempt = 0 33 | 34 | while not text_copied and attempt < max_attempts: 35 | if pyperclip.paste() == text: 36 | text_copied = True 37 | else: 38 | time.sleep(interval) 39 | attempt += 1 40 | 41 | if not text_copied: 42 | raise FindError("Paste method failed.") 43 | 44 | if OSHelper.is_mac(): 45 | type(text="v", modifier=KeyModifier.CMD) 46 | else: 47 | type(text="v", modifier=KeyModifier.CTRL) 48 | 49 | pyperclip.copy("") 50 | -------------------------------------------------------------------------------- /moziris/api/keyboard/keyboard_util.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import ctypes 5 | import logging 6 | import re 7 | import subprocess 8 | 9 | import pyautogui 10 | import pyperclip 11 | 12 | from moziris.api.keyboard.key import Key 13 | from moziris.api.os_helpers import OSHelper 14 | 15 | logger = logging.getLogger(__name__) 16 | DEFAULT_KEY_SHORTCUT_DELAY = 0.1 17 | pyautogui.FAILSAFE = False 18 | 19 | 20 | def get_clipboard(): 21 | """Return the content copied to clipboard.""" 22 | return pyperclip.paste() 23 | 24 | 25 | def shutdown_process(process_name: str): 26 | """Checks if the process name exists in the process list and close it .""" 27 | 28 | if OSHelper.is_windows(): 29 | command_str = "taskkill /IM " + process_name + ".exe" 30 | try: 31 | subprocess.Popen(command_str, shell=True, stdout=subprocess.PIPE) 32 | except subprocess.CalledProcessError: 33 | logger.error('Command failed: "%s"' % command_str) 34 | raise Exception("Unable to run Command.") 35 | elif OSHelper.is_mac() or OSHelper.is_linux(): 36 | command_str = "pkill " + process_name 37 | try: 38 | subprocess.Popen(command_str, shell=True, stdout=subprocess.PIPE) 39 | except subprocess.CalledProcessError: 40 | logger.error('Command failed: "%s"' % command_str) 41 | raise Exception("Unable to run Command.") 42 | 43 | 44 | def is_lock_on(key): 45 | """Determines if a keyboard key(CAPS LOCK, NUM LOCK or SCROLL LOCK) is ON. 46 | 47 | :param key: Keyboard key(CAPS LOCK, NUM LOCK or SCROLL LOCK). 48 | :return: TRUE if keyboard_key state is ON or FALSE if keyboard_key state is OFF. 49 | """ 50 | if OSHelper.is_windows(): 51 | hll_dll = ctypes.WinDLL("User32.dll") 52 | keyboard_code = 0 53 | if key == Key.CAPS_LOCK: 54 | keyboard_code = 0x14 55 | elif key == Key.NUM_LOCK: 56 | keyboard_code = 0x90 57 | elif key == Key.SCROLL_LOCK: 58 | keyboard_code = 0x91 59 | try: 60 | key_state = hll_dll.GetKeyState(keyboard_code) & 1 61 | except Exception: 62 | raise Exception("Unable to run Command.") 63 | if key_state == 1: 64 | return True 65 | return False 66 | 67 | elif OSHelper.is_linux() or OSHelper.is_mac(): 68 | try: 69 | cmd = subprocess.run( 70 | "xset q", shell=True, stdout=subprocess.PIPE, timeout=20 71 | ) 72 | shutdown_process("Xquartz") 73 | except subprocess.CalledProcessError as e: 74 | logger.error("Command failed: %s" % repr(e.cmd)) 75 | raise Exception("Unable to run Command.") 76 | else: 77 | processed_lock_key = key.value.label 78 | if "caps" in processed_lock_key: 79 | processed_lock_key = "Caps" 80 | elif "num" in processed_lock_key: 81 | processed_lock_key = "Num" 82 | elif "scroll" in processed_lock_key: 83 | processed_lock_key = "Scroll" 84 | stdout = cmd.stdout.decode("utf-8").split("\n") 85 | for line in stdout: 86 | if processed_lock_key in line: 87 | values = re.findall(r"\d*\D+", " ".join(line.split())) 88 | for val in values: 89 | if processed_lock_key in val and "off" in val: 90 | return False 91 | return True 92 | 93 | 94 | def check_keyboard_state(disable=False): 95 | """Check Keyboard state. 96 | 97 | Iris cannot run in case Key.CAPS_LOCK, Key.NUM_LOCK or Key.SCROLL_LOCK are pressed. 98 | """ 99 | if disable: 100 | return True 101 | 102 | key_on = False 103 | keyboard_keys = [Key.CAPS_LOCK, Key.NUM_LOCK, Key.SCROLL_LOCK] 104 | for key in keyboard_keys: 105 | try: 106 | if is_lock_on(key): 107 | logger.error( 108 | "Cannot run Iris because %s is on. Please turn it off to continue." 109 | % key.value.label.upper() 110 | ) 111 | key_on = True 112 | break 113 | except subprocess.TimeoutExpired: 114 | logger.error("Unable to invoke xset command.") 115 | logger.error( 116 | "Please fix xset on your machine, or turn off keyboard checking with -n flag." 117 | ) 118 | key_on = True 119 | break 120 | return not key_on 121 | 122 | 123 | def get_active_modifiers(key): 124 | """Gets all the active modifiers depending on the used OS. 125 | 126 | :param key: Key modifier. 127 | :return: Returns an array with all the active modifiers. 128 | """ 129 | all_modifiers = [Key.SHIFT, Key.CTRL] 130 | if OSHelper.is_mac(): 131 | all_modifiers.append(Key.CMD) 132 | elif OSHelper.is_windows(): 133 | all_modifiers.append(Key.WIN) 134 | else: 135 | all_modifiers.append(Key.META) 136 | 137 | all_modifiers.append(Key.ALT) 138 | 139 | active_modifiers = [] 140 | for item in all_modifiers: 141 | try: 142 | for key_value in key: 143 | 144 | if item == key_value.value: 145 | active_modifiers.append(item) 146 | except TypeError: 147 | if item == key.value: 148 | active_modifiers.append(item) 149 | 150 | return active_modifiers 151 | 152 | 153 | def is_shift_character(character): 154 | """ 155 | Returns True if the key character is uppercase or shifted. 156 | """ 157 | return character.isupper() or character in '~!@#$%^&*()_+{}|:"<>?' 158 | -------------------------------------------------------------------------------- /moziris/api/location.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | class Location: 7 | """Class handle single points on the screen directly by its position (x, y). It is mainly used in the actions on a 8 | region, to directly denote the click point. It contains methods, to move a point around on the screen.""" 9 | 10 | def __init__(self, x: int = 0, y: int = 0): 11 | self.x = x 12 | self.y = y 13 | 14 | def __repr__(self): 15 | return "%s(%r, %r)" % (self.__class__.__name__, self.x, self.y) 16 | 17 | def offset(self, away_x: int, away_y: int): 18 | """Return a location object which is away_x and away_y pixels away horizontally and vertically from the current 19 | location. 20 | 21 | :param away_x: Offset added to location x parameter. 22 | :param away_y: Offset added to location y parameter. 23 | :return: Location object. 24 | """ 25 | self.x += away_x 26 | self.y += away_y 27 | return self 28 | 29 | def above(self, away_y: int): 30 | """Return a location object which is away_y pixels vertically above the current location. 31 | 32 | :param away_y: Offset decreased from the location y parameter. 33 | :return: Location object. 34 | """ 35 | self.y -= away_y 36 | return self 37 | 38 | def below(self, away_y: int): 39 | """Return a location object which is away_y pixels vertically below the current location. 40 | 41 | :param away_y: Offset added to location y parameter. 42 | :return: Location object. 43 | """ 44 | self.y += away_y 45 | return self 46 | 47 | def left(self, away_x: int): 48 | """Return a location object which is away_x pixels horizontally to the left of the current location. 49 | 50 | :param away_x: Offset decreased from the location x parameter. 51 | :return: Location object. 52 | """ 53 | self.x -= away_x 54 | return self 55 | 56 | def right(self, away_x: int): 57 | """Return a location object which is away_x pixels horizontally to the right of the current location. 58 | 59 | :param away_x: Offset added to location x parameter. 60 | :return: Location object. 61 | """ 62 | self.x += away_x 63 | return self 64 | -------------------------------------------------------------------------------- /moziris/api/mouse/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/api/mouse/__init__.py -------------------------------------------------------------------------------- /moziris/api/mouse/mouse_controller.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import time 7 | 8 | from pynput.mouse import Controller as MouseController, Button 9 | 10 | from moziris.api.settings import Settings 11 | from moziris.api.location import Location 12 | 13 | 14 | def _get_point_on_line(x1, y1, x2, y2, n): 15 | """Returns the (x, y) tuple of the point that has progressed a proportion 16 | n along the line defined by the two x, y coordinates. 17 | """ 18 | x = ((x2 - x1) * n) + x1 19 | y = ((y2 - y1) * n) + y1 20 | return x, y 21 | 22 | 23 | class Mouse: 24 | def __init__(self): 25 | self.mouse = MouseController() 26 | 27 | def move(self, location: Location = None, duration: float = None): 28 | """Mouse move with tween. 29 | 30 | :param location: Location , image name or Pattern. 31 | :param duration: Speed of mouse movement from current mouse location to target. 32 | :return: None. 33 | """ 34 | 35 | if location is None: 36 | location = Location(0, 0) 37 | 38 | if duration is None: 39 | duration = Settings.move_mouse_delay 40 | 41 | def set_mouse_position(loc_x, loc_y): 42 | self.mouse.position = (int(loc_x), int(loc_y)) 43 | 44 | def smooth_move_mouse(from_x, from_y, to_x, to_y): 45 | num_steps = int(duration / 0.05) 46 | sleep_amount = 0 47 | try: 48 | sleep_amount = duration / num_steps 49 | except ZeroDivisionError: 50 | pass 51 | 52 | steps = [ 53 | _get_point_on_line(from_x, from_y, to_x, to_y, n / num_steps) 54 | for n in range(num_steps) 55 | ] 56 | 57 | steps.append((to_x, to_y)) 58 | for tween_x, tween_y in steps: 59 | tween_x = int(round(tween_x)) 60 | tween_y = int(round(tween_y)) 61 | set_mouse_position(tween_x, tween_y) 62 | time.sleep(sleep_amount) 63 | 64 | return smooth_move_mouse( 65 | self.mouse.position[0], self.mouse.position[1], location.x, location.y 66 | ) 67 | 68 | def press( 69 | self, 70 | location: Location = None, 71 | duration: float = None, 72 | button: Button = Button.left, 73 | ): 74 | """Mouse press. 75 | 76 | :param location: Mouse press location. 77 | :param duration: Speed of mouse movement from current mouse location to target. 78 | :param button: 'left','right' or 'middle'. 79 | :return: None 80 | """ 81 | self.move(location, duration) 82 | self.mouse.press(button) 83 | 84 | def release( 85 | self, 86 | location: Location = None, 87 | duration: float = None, 88 | button: Button = Button.left, 89 | ): 90 | """Mouse press. 91 | 92 | :param location: Mouse press location. 93 | :param duration: Speed of mouse movement from current mouse location to target. 94 | :param button: 'left','right' or 'middle'. 95 | :return: None 96 | """ 97 | self.move(location, duration) 98 | self.mouse.release(button) 99 | 100 | def general_click( 101 | self, 102 | location: Location = None, 103 | duration: float = None, 104 | button: Button = Button.left, 105 | clicks: int = 1, 106 | ): 107 | """General mouse click location. 108 | 109 | :param location: click location 110 | :param duration: Speed of mouse movement from current mouse location to target. 111 | :param button: 'left','right' or 'middle'. 112 | :param clicks: number of mouse clicks. 113 | :return: None. 114 | """ 115 | self.move(location, duration) 116 | self.mouse.click(button, clicks) 117 | 118 | def drag_and_drop(self, start: Location, end: Location, duration: float = None): 119 | """Mouse drag and drop. 120 | 121 | :param start: Starting location 122 | :param end: Drop location 123 | :param duration: Speed of mouse movement to the drag and drop location. 124 | :return: None. 125 | """ 126 | time.sleep(Settings.DEFAULT_UI_DELAY) 127 | self.move(start, duration) 128 | time.sleep(Settings.delay_before_mouse_down) 129 | self.mouse.press(Button.left) 130 | time.sleep(Settings.delay_before_drag) 131 | self.move(end, duration) 132 | time.sleep(Settings.delay_before_drop) 133 | self.mouse.release(Button.left) 134 | 135 | def scroll(self, dx: int = None, dy: int = None, iterations: int = 1): 136 | """Sends scroll events. 137 | 138 | :param int dx: The horizontal scroll. 139 | :param int dy: The vertical scroll. 140 | :param int iterations: Number of iterations for the scroll event. 141 | :return None. 142 | """ 143 | if dx is None: 144 | dx = Settings.mouse_scroll_step 145 | 146 | if dy is None: 147 | dy = Settings.mouse_scroll_step 148 | 149 | for i in range(iterations): 150 | self.mouse.scroll(dx, dy) 151 | time.sleep(0.5) 152 | -------------------------------------------------------------------------------- /moziris/api/mouse/xmouse.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import os 5 | 6 | from Xlib import X 7 | from Xlib.ext.xtest import fake_input 8 | from Xlib.display import Display 9 | 10 | from moziris.api.location import Location 11 | from moziris.api.keyboard.keyboard import XScreen 12 | 13 | 14 | class XMouse(XScreen): 15 | def __init__(self): 16 | self.display = Display(os.environ["DISPLAY"]) 17 | self.MOUSE_BUTTONS = { 18 | "left": 1, 19 | "middle": 2, 20 | "right": 3, 21 | 1: 1, 22 | 2: 2, 23 | 3: 3, 24 | 4: 4, 25 | 5: 5, 26 | 6: 6, 27 | 7: 7, 28 | } 29 | 30 | def click(self, location: Location, button: str): 31 | 32 | """ 33 | Performs a click 34 | 35 | :param button :'left','middle','right' 36 | :param location :x,y coordinates where to click 37 | 38 | """ 39 | 40 | assert ( 41 | button in self.MOUSE_BUTTONS.keys() 42 | ), "button argument not in ('left', 'middle', 'right', 4, 5, 6, 7)" 43 | button = self.MOUSE_BUTTONS[button] 44 | 45 | self._mouseDown(location, button) 46 | self._mouseUp(location, button) 47 | 48 | def _vertical_scroll(self, clicks: int, location: Location = None): 49 | 50 | """ 51 | Performs a vertical mouse movement 52 | 53 | :param clicks :number of clicks 54 | :param location :x,y coordinates where to click 55 | 56 | """ 57 | clicks = int(clicks) 58 | if clicks == 0: 59 | return 60 | elif clicks > 0: 61 | button = 4 # scroll up 62 | else: 63 | button = 5 # scroll down 64 | 65 | for i in range(abs(clicks)): 66 | self.click(location, button=button) 67 | 68 | def horizontal_scroll(self, clicks: int, location: Location): 69 | """ 70 | Performs a horizontal mouse movement 71 | 72 | :param clicks :number of clicks 73 | :param location :x,y coordinates where to click 74 | 75 | """ 76 | clicks = int(clicks) 77 | if clicks == 0: 78 | return 79 | elif clicks > 0: 80 | button = 7 # scroll right 81 | else: 82 | button = 6 # scroll left 83 | 84 | for i in range(abs(clicks)): 85 | self.click(location, button=button) 86 | 87 | def scroll(self, clicks: int, location: Location): 88 | """ 89 | Performs a scroll mouse movement 90 | 91 | :param clicks :number of clicks 92 | :param location :x,y coordinates where to click 93 | 94 | """ 95 | return self.vertical_scroll(clicks, location) 96 | 97 | def moveTo(self, location: Location): 98 | 99 | """ 100 | Mouse move to specific Location 101 | 102 | :param location :x,y coordinates where to click 103 | 104 | """ 105 | fake_input(self.display, X.MotionNotify, x=location.x, y=location.y) 106 | self.display.sync() 107 | 108 | def _mouseDown(self, location: Location, button: str): 109 | """ 110 | Mouse button press 111 | 112 | :param location :x,y coordinates where to click 113 | :param button 'left','middle','right' 114 | 115 | """ 116 | self.moveTo(location) 117 | assert ( 118 | button in self.MOUSE_BUTTONS.keys() 119 | ), "button argument not in ('left', 'middle', 'right', 4, 5, 6, 7)" 120 | button = self.MOUSE_BUTTONS[button] 121 | fake_input(self.display, X.ButtonPress, button) 122 | self.display.sync() 123 | 124 | def _mouseUp(self, location: Location, button: str): 125 | """ 126 | Mouse button Up 127 | 128 | :param location :x,y coordinates where to click 129 | :param button 'left','middle','right' 130 | 131 | """ 132 | self.moveTo(location) 133 | assert ( 134 | button in self.MOUSE_BUTTONS.keys() 135 | ), "button argument not in ('left', 'middle', 'right', 4, 5, 6, 7)" 136 | button = self.MOUSE_BUTTONS[button] 137 | fake_input(self.display, X.ButtonRelease, button) 138 | self.display.sync() 139 | 140 | def position(self): 141 | """Returns: 142 | (x, y) tuple of the current xy coordinates of the mouse cursor. 143 | """ 144 | coord = self.display.screen().root.query_pointer()._data 145 | position = (coord["root_x"], coord["root_y"]) 146 | return position 147 | -------------------------------------------------------------------------------- /moziris/api/os_helpers.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import logging 6 | import multiprocessing 7 | 8 | import mozinfo 9 | import mss 10 | import os 11 | import time 12 | 13 | from moziris.api.enums import OSPlatform 14 | from moziris.api.errors import APIHelperError 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | OS_NAME = mozinfo.os 19 | OS_VERSION = mozinfo.os_version 20 | OS_BITS = mozinfo.bits 21 | PROCESSOR = mozinfo.processor 22 | 23 | MONITORS = mss.mss().monitors[1:] 24 | MULTI_MONITOR_AREA = mss.mss().monitors[0] 25 | 26 | 27 | class OSHelper: 28 | 29 | LOCALES = [ 30 | "en-US", 31 | "zh-CN", 32 | "es-ES", 33 | "de", 34 | "fr", 35 | "ru", 36 | "ar", 37 | "ko", 38 | "pt-PT", 39 | "vi", 40 | "pl", 41 | "tr", 42 | "ro", 43 | "ja", 44 | "it", 45 | "pt-BR", 46 | "in", 47 | "en-GB", 48 | "id", 49 | "ca", 50 | "be", 51 | "kk", 52 | ] 53 | 54 | @staticmethod 55 | def is_high_def_display(): 56 | """Checks if the primary display is high definition.""" 57 | main_display = MONITORS[0] 58 | screenshot = mss.mss().grab(main_display) 59 | if ( 60 | screenshot.width > main_display["width"] 61 | or screenshot.height > main_display["height"] 62 | ): 63 | return True 64 | return False 65 | 66 | @staticmethod 67 | def get_display_factor(): 68 | main_display = MONITORS[0] 69 | screenshot = mss.mss().grab(main_display) 70 | display_factor = screenshot.width / screenshot.height 71 | return display_factor 72 | 73 | @staticmethod 74 | def get_os(): 75 | """Get the type of the operating system your script is running on.""" 76 | if OS_NAME == "win": 77 | return OSPlatform.WINDOWS 78 | elif OS_NAME == "linux": 79 | return OSPlatform.LINUX 80 | elif OS_NAME == "mac": 81 | return OSPlatform.MAC 82 | else: 83 | raise APIHelperError( 84 | "Iris does not yet support your current environment: %s" % OS_NAME 85 | ) 86 | 87 | @staticmethod 88 | def is_mac(): 89 | """Checks if we are running on a Mac system. 90 | 91 | :return: True if we are running on a Mac system, False otherwise. 92 | """ 93 | return OSHelper.get_os() is OSPlatform.MAC 94 | 95 | @staticmethod 96 | def is_windows(): 97 | """Checks if we are running on a Windows system. 98 | 99 | :return: True if we are running on a Windows system, False otherwise. 100 | """ 101 | return OSHelper.get_os() is OSPlatform.WINDOWS 102 | 103 | @staticmethod 104 | def is_linux(): 105 | """Checks if we are running on a Linux system. 106 | 107 | :return: True if we are running on a Linux system, False otherwise. 108 | """ 109 | return OSHelper.get_os() is OSPlatform.LINUX 110 | 111 | @staticmethod 112 | def get_os_version(): 113 | """ 114 | Get the version string of the operating system your script is running on. 115 | 116 | :return: String value of the current operating system. 117 | """ 118 | current_os = OSHelper.get_os() 119 | if current_os is OSPlatform.WINDOWS and OS_VERSION == "6.1": 120 | return "win7" 121 | elif current_os is OSPlatform.MAC: 122 | return ( 123 | "osx_%s" % "retina " if OSHelper.is_high_def_display() else "non_retina" 124 | ) 125 | else: 126 | return current_os.value 127 | 128 | @staticmethod 129 | def get_os_bits(): 130 | return mozinfo.bits 131 | 132 | @staticmethod 133 | def get_processor(): 134 | return mozinfo.processor 135 | 136 | @staticmethod 137 | def use_multiprocessing(): 138 | return multiprocessing.cpu_count() >= 4 and OSHelper.get_os() != "win" 139 | 140 | @staticmethod 141 | def _is_locked(filepath): 142 | """Checks if a file is locked by opening it in append mode. 143 | If no exception thrown, then the file is not locked. 144 | """ 145 | locked = None 146 | file_object = None 147 | if os.path.exists(filepath): 148 | try: 149 | logger.debug("Trying to open file: %s" % filepath) 150 | buffer_size = 8 151 | # Open file in append mode and read the first 8 characters. 152 | file_object = open(filepath, "a", buffer_size) 153 | if file_object: 154 | logger.debug("File is not locked: %s" % filepath) 155 | locked = False 156 | except IOError as message: 157 | logger.debug( 158 | "File is locked (unable to open in append mode): %s." % message 159 | ) 160 | locked = True 161 | finally: 162 | if file_object: 163 | file_object.close() 164 | logger.debug("File closed: %s" % filepath) 165 | else: 166 | logger.debug("File not found: %s" % filepath) 167 | return locked 168 | 169 | @staticmethod 170 | def wait_for_files(filepath): 171 | """Checks if the files are ready. 172 | For a file to be ready it must exist and can be opened in append mode. 173 | """ 174 | wait_time = 5 175 | # If the file doesn't exist, wait wait_time seconds and try again 176 | # until it's found. 177 | while not os.path.exists(filepath): 178 | logger.debug( 179 | "%s hasn't arrived. Waiting %s seconds." % (filepath, wait_time) 180 | ) 181 | time.sleep(wait_time) 182 | # If the file exists but locked, wait wait_time seconds and check 183 | # again until it's no longer locked by another process. 184 | while OSHelper._is_locked(filepath): 185 | logger.debug( 186 | "%s is currently in use. Waiting %s seconds." % (filepath, wait_time) 187 | ) 188 | time.sleep(wait_time) 189 | -------------------------------------------------------------------------------- /moziris/api/rectangle.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | from moziris.api.enums import Alignment 7 | from moziris.api.location import Location 8 | 9 | 10 | class Rectangle: 11 | """Rectangle class represents the coordinates and size of a region/screen.""" 12 | 13 | def __init__( 14 | self, x_start: int = 0, y_start: int = 0, width: int = 0, height: int = 0 15 | ): 16 | self.x = x_start 17 | self.y = y_start 18 | self.width = width 19 | self.height = height 20 | 21 | def __repr__(self): 22 | return "%s(%r, %r, %r, %r)" % ( 23 | self.__class__.__name__, 24 | self.x, 25 | self.y, 26 | self.width, 27 | self.height, 28 | ) 29 | 30 | def apply_alignment(self, align: Alignment = Alignment.TOP_LEFT): 31 | """Returns rectangle location based on alignment. 32 | 33 | :param align: Alignment could be top_left, center, top_right, bottom_left, bottom_right. 34 | :return: Location object. 35 | """ 36 | if align is Alignment.CENTER: 37 | return Location(self.x + int(self.width / 2), self.y + int(self.height / 2)) 38 | elif align is Alignment.TOP_RIGHT: 39 | return Location(self.x + self.width, self.y) 40 | elif align is Alignment.BOTTOM_LEFT: 41 | return Location(self.x, self.y + self.height) 42 | elif align is Alignment.BOTTOM_RIGHT: 43 | return Location(self.x + self.width, self.y + self.height) 44 | else: 45 | return Location(self.x, self.y) 46 | -------------------------------------------------------------------------------- /moziris/api/save_debug_image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/api/save_debug_image/__init__.py -------------------------------------------------------------------------------- /moziris/api/save_debug_image/save_image.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import datetime 7 | import logging 8 | import os 9 | import re 10 | 11 | import cv2 12 | import numpy as np 13 | 14 | from moziris.api.settings import Settings 15 | 16 | try: 17 | import Image 18 | except ImportError: 19 | from PIL import Image 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def save_debug_image(needle, haystack, locations): 25 | """Saves input Image for debug. 26 | 27 | :param Image || None needle: Input needle image that needs to be highlighted. 28 | :param haystack: Input Region as Image. 29 | :param List[Location] || Location locations: Location or list of Location as coordinates. 30 | :return: None. 31 | """ 32 | logger.debug("Debug image is enabled: %s" % Settings.debug_image) 33 | if Settings.debug_image is False: 34 | return 35 | 36 | w, h = needle.get_size() 37 | 38 | path = Settings.debug_image_path 39 | logger.debug("Debug image directory path: %s" % path) 40 | 41 | timestamp_str = re.sub("[ :.-]", "_", str(datetime.datetime.now())) 42 | resolution_str = "_not_found" if len(locations) == 0 else "_found" 43 | 44 | temp_f = timestamp_str + resolution_str 45 | 46 | file_name = "%s.jpg" % os.path.join(path, temp_f) 47 | logger.debug("Debug image location: %s" % file_name) 48 | 49 | if not os.path.exists(path): 50 | os.makedirs(path) 51 | 52 | not_found_txt = " <<< Pattern not found!" 53 | 54 | if len(locations) > 0: 55 | for loc in locations: 56 | cv2.rectangle( 57 | haystack.get_gray_array(), 58 | (loc.x, loc.y), 59 | (loc.x + w, loc.y + h), 60 | (0, 0, 255), 61 | 2, 62 | ) 63 | cv2.imwrite( 64 | file_name, haystack.get_gray_array(), [int(cv2.IMWRITE_JPEG_QUALITY), 50] 65 | ) 66 | else: 67 | gray_img = haystack.get_gray_image() 68 | search_for_image = needle.get_color_image() 69 | v_align_pos = int(gray_img.size[1] / 2 - h / 2) 70 | 71 | d_image = Image.new("RGB", (gray_img.size[0], gray_img.size[1])) 72 | d_image.paste(gray_img) 73 | d_image.paste(search_for_image, (0, v_align_pos)) 74 | d_array = np.array(d_image) 75 | text_size, baseline = cv2.getTextSize( 76 | not_found_txt, cv2.FONT_HERSHEY_TRIPLEX, 0.5, 1 77 | ) 78 | 79 | cv2.rectangle( 80 | d_array, 81 | (w, v_align_pos), 82 | (w + text_size[0], v_align_pos + h), 83 | (255, 255, 255), 84 | cv2.FILLED, 85 | ) 86 | cv2.putText( 87 | d_array, 88 | not_found_txt, 89 | (w, v_align_pos + h - 5), 90 | cv2.FONT_HERSHEY_TRIPLEX, 91 | 0.5, 92 | (0, 0, 0), 93 | 1, 94 | 16, 95 | ) 96 | cv2.imwrite(file_name, d_array, [int(cv2.IMWRITE_JPEG_QUALITY), 50]) 97 | 98 | 99 | def save_debug_ocr_image(text, haystack, text_occurrences): 100 | """Saves input Image for debug. 101 | 102 | :param text: Input text that needs to be highlighted. 103 | :param haystack: Input Region as Image. 104 | :param List[Location] || Location text_occurrences: Location or list of Location as coordinates. 105 | :return: None. 106 | """ 107 | logger.debug("Debug image is enabled: %s" % Settings.debug_image) 108 | if Settings.debug_image is False: 109 | return 110 | 111 | path = Settings.debug_image_path 112 | logger.debug("Debug image directory path: %s" % path) 113 | 114 | timestamp_str = re.sub("[ :.-]", "_", str(datetime.datetime.now())) 115 | resolution_str = "_not_found" if len(text_occurrences) == 0 else "_found" 116 | 117 | temp_f = timestamp_str + resolution_str 118 | 119 | file_name = "%s.jpg" % os.path.join(path, temp_f) 120 | logger.debug("Debug image location: %s" % file_name) 121 | 122 | if not os.path.exists(path): 123 | os.makedirs(path) 124 | 125 | not_found_txt = " '{}' not found!".format(text) 126 | 127 | if text_occurrences and len(text_occurrences) > 0: 128 | for occurrence in text_occurrences: 129 | cv2.rectangle( 130 | haystack.get_gray_array(), 131 | (occurrence.x, occurrence.y), 132 | (occurrence.x + occurrence.width, occurrence.y + occurrence.height), 133 | (0, 0, 255), 134 | 2, 135 | ) 136 | cv2.imwrite( 137 | file_name, haystack.get_gray_array(), [int(cv2.IMWRITE_JPEG_QUALITY), 50] 138 | ) 139 | else: 140 | gray_img = haystack.get_gray_image() 141 | v_align_pos = int(gray_img.size[1] / 2 - 20 / 2) 142 | 143 | d_image = Image.new("RGB", (gray_img.size[0], gray_img.size[1])) 144 | d_image.paste(gray_img) 145 | d_array = np.array(d_image) 146 | cv2.rectangle( 147 | d_array, 148 | (0, v_align_pos), 149 | (haystack.width, v_align_pos + 20), 150 | (255, 255, 255), 151 | cv2.FILLED, 152 | ) 153 | cv2.putText( 154 | d_array, 155 | not_found_txt, 156 | (0, v_align_pos + 20 - 5), 157 | cv2.FONT_HERSHEY_TRIPLEX, 158 | 0.4, 159 | (0, 0, 0), 160 | 1, 161 | 16, 162 | ) 163 | cv2.imwrite(file_name, d_array, [int(cv2.IMWRITE_JPEG_QUALITY), 50]) 164 | -------------------------------------------------------------------------------- /moziris/api/screen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/api/screen/__init__.py -------------------------------------------------------------------------------- /moziris/api/screen/display.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import logging 7 | 8 | import mss 9 | 10 | from moziris.api.rectangle import Rectangle 11 | 12 | MONITORS = mss.mss().monitors[1:] 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class Display: 17 | def __init__(self, screen_id: int = 0): 18 | self.bounds = _get_screen_details(screen_id) 19 | self.scale = _get_scale(screen_id) 20 | 21 | def __repr__(self): 22 | return "%s(%r, %r, %r, %r)" % ( 23 | self.__class__.__name__, 24 | self.bounds.x, 25 | self.bounds.y, 26 | self.bounds.width, 27 | self.bounds.height, 28 | ) 29 | 30 | 31 | def _get_screen_details(screen_id: int) -> Rectangle: 32 | """Get the screen details. 33 | 34 | :param screen_id: Screen ID. 35 | :return: Region object. 36 | """ 37 | if len(MONITORS) == 0: 38 | logger.error("Could not retrieve list of available monitors.") 39 | else: 40 | try: 41 | details = MONITORS[screen_id] 42 | return Rectangle( 43 | details["left"], details["top"], details["width"], details["height"] 44 | ) 45 | except IndexError: 46 | logger.warning( 47 | "Screen %s does not exist. Available monitors: %s" 48 | % (screen_id, ", ".join(_get_available_monitors(MONITORS))) 49 | ) 50 | return Rectangle() 51 | 52 | 53 | def _get_available_monitors(screen_list): 54 | """Return a list with all the available monitors.""" 55 | res = [] 56 | for screen in screen_list: 57 | res.append("Screen(%s)" % screen_list.index(screen)) 58 | return res 59 | 60 | 61 | def _get_display_collection(): 62 | res = [] 63 | for index, item in enumerate(MONITORS): 64 | res.append(Display(index)) 65 | return res 66 | 67 | 68 | def _get_scale(screen_id): 69 | try: 70 | display = MONITORS[screen_id] 71 | display_width = display["width"] 72 | screenshot = mss.mss().grab(display) 73 | return screenshot.width / display_width 74 | except IndexError: 75 | return 1 76 | 77 | 78 | DisplayCollection = _get_display_collection() 79 | -------------------------------------------------------------------------------- /moziris/api/screen/region_utils.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import logging 6 | 7 | from moziris.api import * 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class RegionUtils: 13 | @staticmethod 14 | def generate_region_by_markers( 15 | top_left_marker_img=None, bottom_right_marker_img=None 16 | ): 17 | """Generate a region starting from 2 markers. 18 | 19 | :param top_left_marker_img: Top left pattern used to generate the region. 20 | :param bottom_right_marker_img: Bottom right pattern used to generate the region. 21 | :return: Screen region generated. 22 | """ 23 | try: 24 | wait(top_left_marker_img, 10) 25 | exists(bottom_right_marker_img, 10) 26 | except FindError: 27 | raise FindError("Unable to find page markers.") 28 | 29 | top_left_pos = find(top_left_marker_img) 30 | bottom_right_pos = find(bottom_right_marker_img) 31 | 32 | marker_width, marker_height = bottom_right_marker_img.get_size() 33 | 34 | return Region( 35 | top_left_pos.x, 36 | top_left_pos.y, 37 | (bottom_right_pos.x + marker_width), 38 | bottom_right_pos.y - top_left_pos.y + marker_height, 39 | ) 40 | 41 | @staticmethod 42 | def create_region_from_patterns( 43 | top=None, 44 | bottom=None, 45 | left=None, 46 | right=None, 47 | padding_top=None, 48 | padding_bottom=None, 49 | padding_left=None, 50 | padding_right=None, 51 | ): 52 | """Returns a region created from combined area of one or more patterns. Argument names are just for convenience 53 | and don't influence outcome. 54 | 55 | :param top: Top pattern used to generate the region. 56 | :param bottom: Bottom pattern used to generate the region. 57 | :param left: Left pattern used to generate the region. 58 | :param right: Right pattern used to generate the region. 59 | :param padding_top: Padding to be added to the pattern's top. 60 | :param padding_bottom: Padding to be added to the pattern's bottom. 61 | :param padding_left: Padding to be added to the pattern's left. 62 | :param padding_right: Padding to be added to the pattern's right. 63 | :return: region created from combined area of one or more patterns. 64 | """ 65 | 66 | patterns = [] 67 | if top: 68 | patterns.append(top) 69 | if bottom: 70 | patterns.append(bottom) 71 | if left: 72 | patterns.append(left) 73 | if right: 74 | patterns.append(right) 75 | 76 | if len(patterns) == 0: 77 | raise ValueError("One or more patterns required.") 78 | 79 | logger.debug("Creating region from %s pattern(s)." % len(patterns)) 80 | 81 | a, b = (Screen().width, Screen().height) 82 | p1 = Location(a, b) 83 | p2 = Location(0, 0) 84 | 85 | for pattern in patterns: 86 | if exists(pattern, 5): 87 | current_pattern = find(pattern) 88 | if current_pattern.x < p1.x: 89 | p1.x = current_pattern.x 90 | if current_pattern.y < p1.y: 91 | p1.y = current_pattern.y 92 | 93 | w, h = pattern.get_size() 94 | 95 | if current_pattern.x + w > p2.x: 96 | p2.x = current_pattern.x + w 97 | if current_pattern.y + h > p2.y: 98 | p2.y = current_pattern.y + h 99 | else: 100 | raise FindError("Pattern not found: %s " % pattern) 101 | 102 | found_region = Region(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y) 103 | 104 | if padding_top or padding_bottom or padding_left or padding_right: 105 | logger.debug("Adding padding to region.") 106 | 107 | if padding_top: 108 | found_region.y -= padding_top 109 | found_region.height += padding_top 110 | 111 | if padding_bottom: 112 | found_region.height += padding_bottom 113 | 114 | if padding_left: 115 | found_region.x -= padding_left 116 | found_region.width += padding_left 117 | 118 | if padding_right: 119 | found_region.width += padding_right 120 | 121 | return found_region 122 | -------------------------------------------------------------------------------- /moziris/api/screen/screen.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import logging 7 | 8 | from moziris.api.screen.display import DisplayCollection 9 | from moziris.api.screen.region import Region 10 | from moziris.api.rectangle import Rectangle 11 | 12 | import pyautogui 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class Screen(Region): 18 | """Class Screen is the representation for a physical monitor where the capturing process (grabbing a rectangle 19 | from a screenshot). It is used for further processing with find operations. For Multi Monitor Environments it 20 | contains features to map to the relevant monitor. 21 | """ 22 | 23 | def __init__(self, screen_id: int = 0): 24 | self.screen_id = screen_id 25 | self.screen_list = DisplayCollection[screen_id] 26 | self._bounds = DisplayCollection[screen_id].bounds 27 | Region.__init__( 28 | self, 29 | self._bounds.x, 30 | self._bounds.y, 31 | self._bounds.width, 32 | self._bounds.height, 33 | ) 34 | 35 | SCREEN_WIDTH, SCREEN_HEIGHT = pyautogui.size() 36 | screen_region = Region(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) 37 | 38 | TOP_HALF = Region.screen_regions(screen_region, "TOP_HALF") 39 | BOTTOM_HALF = Region.screen_regions(screen_region, "BOTTOM_HALF") 40 | 41 | LEFT_HALF = Region.screen_regions(screen_region, "LEFT_HALF") 42 | RIGHT_HALF = Region.screen_regions(screen_region, "RIGHT_HALF") 43 | 44 | TOP_THIRD = Region.screen_regions(screen_region, "TOP_THIRD") 45 | MIDDLE_THIRD_HORIZONTAL = Region.screen_regions( 46 | screen_region, "MIDDLE_THIRD_HORIZONTAL" 47 | ) 48 | BOTTOM_THIRD = Region.screen_regions(screen_region, "BOTTOM_THIRD") 49 | 50 | LEFT_THIRD = Region.screen_regions(screen_region, "LEFT_THIRD") 51 | MIDDLE_THIRD_VERTICAL = Region.screen_regions( 52 | screen_region, "MIDDLE_THIRD_VERTICAL" 53 | ) 54 | RIGHT_THIRD = Region.screen_regions(screen_region, "RIGHT_THIRD") 55 | 56 | UPPER_LEFT_CORNER = Region.screen_regions(screen_region, "UPPER_LEFT_CORNER") 57 | UPPER_RIGHT_CORNER = Region.screen_regions(screen_region, "UPPER_RIGHT_CORNER") 58 | LOWER_LEFT_CORNER = Region.screen_regions(screen_region, "LOWER_LEFT_CORNER") 59 | LOWER_RIGHT_CORNER = Region.screen_regions(screen_region, "LOWER_RIGHT_CORNER") 60 | 61 | def __repr__(self): 62 | return "%s(x: %r, y: %r, size: %r x %r)" % ( 63 | self.__class__.__name__, 64 | self._bounds.x, 65 | self.y, 66 | self._bounds.width, 67 | self._bounds.height, 68 | ) 69 | 70 | def get_number_screens(self) -> int: 71 | """Get the number of screens in a multi-monitor environment at the time the script is running.""" 72 | return len(self.screen_list) 73 | 74 | def get_bounds(self) -> Rectangle: 75 | """Get the dimensions of monitor represented by the screen object.""" 76 | return self._bounds 77 | -------------------------------------------------------------------------------- /moziris/api/screen/screenshot_image.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import cv2 7 | import mss 8 | import numpy as np 9 | import logging 10 | 11 | from pyautogui import screenshot 12 | 13 | from moziris.api.errors import ScreenshotError 14 | from moziris.api.os_helpers import OSHelper 15 | from moziris.api.screen.display import DisplayCollection 16 | from moziris.api.rectangle import Rectangle 17 | 18 | try: 19 | import Image 20 | except ImportError: 21 | from PIL import Image 22 | 23 | logger = logging.getLogger(__name__) 24 | _mss = mss.mss() 25 | 26 | 27 | class ScreenshotImage: 28 | """This class represents the visual representation of a region/screen.""" 29 | 30 | def __init__(self, region: Rectangle = None, screen_id: int = None): 31 | if screen_id is None: 32 | screen_id = 0 33 | 34 | if region is None: 35 | region = DisplayCollection[screen_id].bounds 36 | 37 | if OSHelper.is_linux(): 38 | screen_region = region 39 | else: 40 | screen_region = { 41 | "top": int(region.y), 42 | "left": int(region.x), 43 | "width": int(region.width), 44 | "height": int(region.height), 45 | } 46 | 47 | self._raw_image = _region_to_image(screen_region) 48 | self._gray_array = _convert_image_to_gray(self._raw_image) 49 | self._color_array = _convert_image_to_color(self._raw_image) 50 | 51 | height, width = self._gray_array.shape 52 | self.width = width 53 | self.height = height 54 | 55 | scale = DisplayCollection[screen_id].scale 56 | 57 | if scale != 1: 58 | self.width = int(width / scale) 59 | self.height = int(height / scale) 60 | self._color_array = cv2.resize( 61 | self._color_array, 62 | dsize=(self.width, self.height), 63 | interpolation=cv2.INTER_CUBIC, 64 | ) 65 | self._gray_array = cv2.resize( 66 | self._gray_array, 67 | dsize=(self.width, self.height), 68 | interpolation=cv2.INTER_CUBIC, 69 | ) 70 | 71 | def get_gray_array(self): 72 | """Getter for the gray_array property.""" 73 | return self._gray_array 74 | 75 | def get_gray_image(self): 76 | """Getter for the gray_image property.""" 77 | return Image.fromarray(self._gray_array) 78 | 79 | def binarize(self): 80 | return cv2.threshold( 81 | self._gray_array, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU 82 | )[1] 83 | 84 | def get_raw_image(self): 85 | """Getter raw_image property.""" 86 | return Image.fromarray(self._raw_image) 87 | 88 | def get_raw_array(self): 89 | """Getter array property.""" 90 | return self._raw_image 91 | 92 | def get_color_array(self): 93 | """Getter color array property.""" 94 | return self._color_array 95 | 96 | def show_image(self): 97 | """Displays this image. This method is mainly intended for 98 | debugging purposes.""" 99 | image = self.get_raw_image() 100 | return image.show() 101 | 102 | 103 | def _region_to_image(region) -> Image or ScreenshotError: 104 | # On Linux, try to use pyautogui to take screenshots, and revert to mss if it fails. 105 | # On Windows/Mac, do the reverse. 106 | try: 107 | if OSHelper.is_linux(): 108 | try: 109 | grabbed_area = _pyautogui_screenshot(region) 110 | except ScreenshotError as e: 111 | logger.debug(e) 112 | grabbed_area = _mss_screenshot(region) 113 | else: 114 | try: 115 | grabbed_area = _mss_screenshot(region) 116 | except ScreenshotError as e: 117 | logger.debug(e) 118 | grabbed_area = _pyautogui_screenshot(region) 119 | except ScreenshotError as e: 120 | logger.error("Screenshot failed: %s" % e) 121 | raise ScreenshotError("Cannot create screenshot: %s" % e) 122 | return grabbed_area 123 | 124 | 125 | def _convert_image_to_gray(image): 126 | """Converts an Image to Gray 127 | :returns np array""" 128 | return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 129 | 130 | 131 | def _convert_image_to_color(image): 132 | """Converts an Image to Color 133 | :returns np array""" 134 | 135 | return cv2.cvtColor(np.array(image), cv2.COLOR_BGR2RGB) 136 | 137 | 138 | def _mss_screenshot(region): 139 | try: 140 | return np.array(_mss.grab(region)) 141 | except Exception as e: 142 | raise ScreenshotError("Call to _mss.grab failed: %s" % e) 143 | 144 | 145 | def _pyautogui_screenshot(region): 146 | try: 147 | return np.array( 148 | screenshot(region=(region.x, region.y, region.width, region.height)) 149 | ) 150 | except (IOError, OSError): 151 | raise ScreenshotError("Call to pyautogui.screenshot failed.") 152 | -------------------------------------------------------------------------------- /moziris/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/base/__init__.py -------------------------------------------------------------------------------- /moziris/base/testcase.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import logging 7 | 8 | import pytest 9 | 10 | from moziris.api import * 11 | from moziris.util.region_utils import RegionUtils 12 | from moziris.util.path_manager import PathManager 13 | from funcy import compose 14 | 15 | 16 | class BaseTest: 17 | def setup(self): 18 | return 19 | 20 | @classmethod 21 | def setup_class(cls): 22 | return 23 | 24 | @classmethod 25 | def teardown_class(cls): 26 | return 27 | 28 | def setup_method(self, method): 29 | return 30 | 31 | def teardown_method(self, method): 32 | return 33 | -------------------------------------------------------------------------------- /moziris/configuration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/configuration/__init__.py -------------------------------------------------------------------------------- /moziris/configuration/config_parser.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | import logging 7 | import os.path 8 | from configparser import ConfigParser 9 | 10 | from moziris.api.settings import Settings 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def get_config_section(section): 16 | """Returns all properties of a section as a dict or None if section does not exist.""" 17 | config_file = os.path.join(Settings.code_root, "config.ini") 18 | logger.debug("Config file location: %s" % config_file) 19 | file_exists = os.path.isfile(config_file) 20 | logger.debug("Config file exists: %s" % file_exists) 21 | config = ConfigParser() 22 | if file_exists: 23 | try: 24 | config.read(config_file) 25 | if config.has_section(section): 26 | result = dict(config.items(section)) 27 | return result 28 | 29 | except EOFError: 30 | logger.warning("Config file error.") 31 | return None 32 | logger.warning("Config file not found.") 33 | return None 34 | 35 | 36 | def get_config_property(section, prop): 37 | """Returns the config property for a specific section.""" 38 | logger.debug("Extracting {} for section {}".format(prop, section)) 39 | section_dict = get_config_section(section) 40 | if section_dict is not None: 41 | try: 42 | return section_dict[prop] 43 | except KeyError: 44 | logger.warning( 45 | "Property '{}' not found in section {}".format(prop, section) 46 | ) 47 | return None 48 | 49 | 50 | def validate_section(section): 51 | """Validate a config.ini section.""" 52 | err_msg = "" 53 | section_dict = get_config_section(section) 54 | if section_dict is None: 55 | return "[{}] section not found in [config.ini]".format(section) 56 | else: 57 | invalid_list = [] 58 | for key in section_dict: 59 | if len(str(section_dict[key]).strip()) == 0: 60 | invalid_list.append(key) 61 | if len(invalid_list) > 0: 62 | err_msg = "[{}] section has properties with no values: [{}]".format( 63 | section, ", ".join(invalid_list) 64 | ) 65 | return err_msg 66 | 67 | 68 | def validate_config_ini(args): 69 | if args.email: 70 | email_s = validate_section("Email") 71 | if len(email_s) > 0: 72 | logger.warning("{}. Submit email report was disabled.".format(email_s)) 73 | args.email = False 74 | -------------------------------------------------------------------------------- /moziris/control_center/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/control_center/__init__.py -------------------------------------------------------------------------------- /moziris/control_center/assets/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.c73dc7db.chunk.css", 4 | "main.js": "/static/js/main.5deb8d43.chunk.js", 5 | "main.js.map": "/static/js/main.5deb8d43.chunk.js.map", 6 | "runtime~main.js": "/static/js/runtime~main.a8a9905a.js", 7 | "runtime~main.js.map": "/static/js/runtime~main.a8a9905a.js.map", 8 | "static/css/2.3c6b4139.chunk.css": "/static/css/2.3c6b4139.chunk.css", 9 | "static/js/2.d4e7d496.chunk.js": "/static/js/2.d4e7d496.chunk.js", 10 | "static/js/2.d4e7d496.chunk.js.map": "/static/js/2.d4e7d496.chunk.js.map", 11 | "index.html": "/index.html", 12 | "precache-manifest.a07c099c384c1d54159b54660d570ddf.js": "/precache-manifest.a07c099c384c1d54159b54660d570ddf.js", 13 | "service-worker.js": "/service-worker.js", 14 | "static/css/2.3c6b4139.chunk.css.map": "/static/css/2.3c6b4139.chunk.css.map", 15 | "static/css/main.c73dc7db.chunk.css.map": "/static/css/main.c73dc7db.chunk.css.map", 16 | "static/media/fonts.css": "/static/media/open-sans-v15-latin-regular.cffb686d.woff2" 17 | } 18 | } -------------------------------------------------------------------------------- /moziris/control_center/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/control_center/assets/favicon.ico -------------------------------------------------------------------------------- /moziris/control_center/assets/images/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/control_center/assets/images/firefox.png -------------------------------------------------------------------------------- /moziris/control_center/assets/images/notepad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/iris/883ee08925e98efd8116701958dc4b1aa4c733df/moziris/control_center/assets/images/notepad.png -------------------------------------------------------------------------------- /moziris/control_center/assets/index.html: -------------------------------------------------------------------------------- 1 |