├── .github ├── SETUP.md ├── dependabot.yml └── workflows │ └── e2e.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── command_line.py ├── config.py ├── files ├── base_url.txt ├── compressors │ ├── upx-linux-x32 │ ├── upx-linux-x64 │ ├── upx-mac │ └── upx-win.exe ├── env_vars.bash ├── env_vars.bat ├── env_vars.py ├── images │ ├── app_settings.png │ ├── app_settings.svg │ ├── compress_settings.png │ ├── compress_settings.svg │ ├── download_settings.png │ ├── download_settings.svg │ ├── export_settings.png │ ├── export_settings.svg │ ├── folder_open.png │ ├── icon.png │ ├── icon.svg │ ├── warning.png │ ├── warning.svg │ ├── window_settings.png │ └── window_settings.svg ├── nw-versions.txt ├── settings.cfg └── version.txt ├── image_utils ├── __init__.py ├── icns_info.py ├── image_utils.py ├── png.py └── pycns.py ├── images ├── icon.icns ├── icon.ico └── icon.png ├── main.py ├── pe.py ├── requirements.txt ├── scripts ├── Info.plist ├── Web2Exe.nsi ├── build_command_line_linux.bash ├── build_mac.bash ├── build_windows.bat └── upload_release.py ├── tests ├── __init__.py ├── conftest.py └── test_command_line.py ├── util_classes.py └── utils.py /.github/SETUP.md: -------------------------------------------------------------------------------- 1 | #Setup Guide 2 | 3 | In order to run Web2Executable from Python code to make modifications to this repo, you'll need to install a few prerequisites first. 4 | 5 | ##Prerequisites 6 | 7 | ###Qt 4.8.7 8 | 9 | Download and install Qt 4.8.7 from [here](https://download.qt.io/official_releases/qt/4.8/4.8.7/) or from your package manager. PySide uses Qt 4.8.X and is incompatible with Qt 5 and higher. 10 | 11 | For Mac OSX, it might be needed to install the latest 4.8.X version via compiling the source. Download the `qt-everywhere-opensource-src-4.8.X` version and run the following in the extracted directory: 12 | 13 | ``` 14 | ./configure 15 | make 16 | sudo make install 17 | ``` 18 | 19 | Alternatively, you may wish to install it via homebrew if you have it installed: 20 | 21 | ``` 22 | brew install qt 23 | ``` 24 | 25 | ###Python 3.4 26 | 27 | Download and install the latest Python3.4.X release from the Python website. Python 3.5 and higher may work, but this repo was only tested with Python 3.4. 28 | 29 | ###System requirements 30 | 31 | If you want to use the conversion of any image to icns and png using Pillow, you must install some libraries. 32 | 33 | libjpeg, libpng, openjpeg 34 | 35 | ###Pip Requirements 36 | 37 | Install pip requirements with 38 | 39 | ``` 40 | pip install -r requirements.txt 41 | ``` 42 | 43 | ##Running 44 | 45 | Once all of the above are installed, simply run: 46 | 47 | ``` 48 | python3.4 main.py 49 | ``` 50 | 51 | ##Tests 52 | 53 | Tests are located in the tests directory and were created with pytest. 54 | They can be run with: 55 | 56 | ``` 57 | pytest 58 | ``` 59 | 60 | in the root directory. 61 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "." 5 | schedule: 6 | interval: "weekly" 7 | day: "saturday" 8 | groups: 9 | pip: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: "github-actions" 13 | directory: ".github/" 14 | schedule: 15 | interval: "weekly" 16 | day: "saturday" 17 | groups: 18 | gha: 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | concurrency: 8 | group: ${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | e2e: 13 | strategy: 14 | matrix: 15 | os: [macos-14, ubuntu-22.04, windows-2022] 16 | fail-fast: false 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v3.3.0 20 | - uses: actions/setup-python@v4.6.1 21 | with: 22 | python-version: '3.12.0' 23 | cache: pip 24 | - if: matrix.os == 'ubuntu-22.04' 25 | uses: awalsh128/cache-apt-pkgs-action@v1.3.0 26 | with: 27 | packages: libegl1 28 | version: 1.0 29 | - run: python -m pip install -r requirements.txt 30 | # - run: python -m pylint ./**/*.py 31 | - run: python -m pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.sw* 3 | *.spec 4 | 5 | tests/test_data 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # "No Ideologies" Code of Conduct 2 | 3 | The following are the guidelines we expect our community members and maintainers to follow. 4 | 5 | * * * 6 | 7 | ## Terminology and Scope 8 | 9 | **What defines a "maintainer"?** 10 | 11 | * A maintainer is anyone that interacts with the community on behalf of this project. Amount of code written is not a qualifier. A maintainer may include those who solely help in support roles such as in resolving issues, improving documentation, administrating or moderating forums/chatrooms, or any other non-coding specific roles. Maintainers also include those that are responsible for the building and upkeep of the project. 12 | 13 | **What defines a "community member"?** 14 | 15 | * Anyone interacting with this project directly, including maintainers. 16 | 17 | **What is the scope of these guidelines?** 18 | 19 | * These guidelines apply only to this project and forms of communication directly related to it, such as issue trackers, forums, chatrooms, and in person events specific to this project. If a member is violating these guidelines outside of this project or on other platforms, that is beyond our scope and any grievances should be handled on those platforms. 20 | 21 | **Discussing the guidelines:** 22 | 23 | * Discussions around these guidelines, improving, updating, or altering them, is permitted so long as the discussions do not violate any existing guidelines. 24 | 25 | * * * 26 | 27 | ## Guidelines 28 | 29 | ### Guidelines for community members 30 | 31 | This project is technical in nature and not based around any particular non-technical ideology. As such, communication that is based primarily around ideologies unrelated to the technologies used by this repository are not permitted. 32 | 33 | Any discussion or communication that is primarily focused around an ideology, be it about race, gender, politics, religion, or anything else non-technical, is not allowed. Everyone has their own ideological preferences, beliefs, and opinions. We do not seek to marginalize, exclude, or judge anyone for their ideologies. To prevent conflict between those with differing or opposing ideologies, all communication on these subjects are prohibited. Some discussions around these topics may be important, however this project is not the proper channel for these discussions. 34 | 35 | ### Guidelines for maintainers 36 | 37 | * Maintainers must abide by the same rules as all other community members mentioned above. However, in addition, maintainers are held to a higher standard, explained below. 38 | * Maintainers should answer all questions politely. 39 | * If someone is upset or angry about something, it's probably because it's difficult to use, so thank them for bringing it to your attention and address ways to solve the problem. Maintainers should focus on the content of the message, and not on how it was delivered. 40 | * A maintainer should seek to update members when an issue they brought up is resolved. 41 | 42 | * * * 43 | 44 | ## Appropriate response to violations 45 | 46 | How to respond to a community member or maintainer violating a guideline. 47 | 48 | 1. If an issue is created that violates a guideline a maintainer should close and lock the issue, explaining "This issue is in violation of our code of conduct. Please review it before posting again." with a link to this document. 49 | 1. If a member repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. 50 | 1. If a maintainer is in violation of a guideline, they should be informed of such with a link to this document. If additional actions are required of the maintainer but not taken, then other maintainers should be informed of these inactions. 51 | 1. If a maintainer repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. In addition, future contributions to this project may be ignored as well. 52 | 53 | * * * 54 | 55 | Based on version 1.0.3 from https://github.com/CodifiedConduct/coc-no-ideologies 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2023 SimplyPixelated 4 | Copyright (c) 2022-2023 NW.js Utilities 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This repository is in maintainance mode. Bug fixes will be provided on a best effort basis. If you use this project, please consider contributing back. 2 | 3 | Web2Executable 4 | ============== 5 | 6 | [![Github Releases (by Release)](https://img.shields.io/github/downloads/jyapayne/Web2Executable/latest/total.svg)]() 7 | 8 | [Releases (Downloads)](https://github.com/jyapayne/Web2Executable/releases) (new!) 9 | 10 | [![Github All Releases](https://img.shields.io/github/downloads/jyapayne/Web2Executable/total.svg)]() 11 | 12 | What is it? 13 | ----------- 14 | 15 | Web2Executable is a friendly command line and GUI application that can transform your Nodejs (or any other JS/HTML) app into an executable (and libraries) that can run in a contained, desktop-like style. It can export to Mac OS X, Windows and Linux all from one platform, so no need to go out and buy expensive hardware. 16 | 17 | It's powered by the very awesome project [NWJS](https://github.com/nwjs) and PySide, is open source, and is just dang awesome and easy to use. 18 | 19 | If you have an idea for a feature, please create a new issue with a format like this: "Feature - My Awesome New Feature", along with a good description of what you'd like the feature to do. 20 | 21 | On the other hand, if you have any annoyances with the application and want to contribute to making it better for everyone, please file an issue with "Annoyance:" as the first part of the title. Sometimes it's hard to know what is annoying for people and input is much appreciated :) 22 | 23 | What About Electron? 24 | -------------------- 25 | 26 | If you want to export using Electron instead of NW.js, try [Electrify](https://github.com/jyapayne/Electrify), my other app based on Web2Executable. 27 | 28 | 29 | Who's Using It? 30 | --------------- 31 | 32 | Lots of people! There are currently thousands of downloads and several articles written about using Web2Executable. 33 | 34 | Some articles include: 35 | 36 | [Getting a Phaser Game on Steam](http://phaser.io/news/2015/10/getting-a-phaser-game-on-steam) 37 | 38 | [Packt Publishing NW.js Essentials Tutorial](https://www.packtpub.com/packtlib/book/Web-Development/9781785280863/7/ch07lvl1sec53/Web2Executable) and [Ebook](https://books.google.ca/books?id=wz6qCQAAQBAJ&pg=PA135&lpg=PA135&dq=web2executable&source=bl&ots=sPP-3BOMXX&sig=UolyF31WcTgA-lrel2UTIfzs65U&hl=en&sa=X&redir_esc=y#v=onepage&q=web2executable&f=false) 39 | 40 | [A Russian NW.js Tutoral](http://canonium.com/articles/nwjs-web-to-executable) 41 | 42 | [Marv's Blog](http://www.marv.ph/tag/web2exe/) 43 | 44 | [Shotten.com Node-webkit for Poets](http://www.shotton.com/wp/2014/10/27/node-webkit-for-poets-mac-version/) 45 | 46 | If you have a project you'd like to see listed here that was successfully built using Web2Executable or you have written an article that mentions it, feel free to send me an email with a link and I'd be super stoked to paste it here :) 47 | 48 | 49 | Features 50 | -------- 51 | 52 | - Cross platform to Mac, Windows, Linux 53 | - Working media out of the box (sound and video) 54 | - Easy to use and straightforward 55 | - Streamlined workflow from project -> working standalone exe 56 | - Same performance as Google Chrome 57 | - Works with Phaser; should work with other HTML5 game libraries 58 | - Export web applications to all platforms from your current OS 59 | - Ability to specify a node-webkit version to download 60 | - Automatic insertion of icon files into Windows exe's or Mac Apps by filling out the icon fields as necessary 61 | - A command line utility with functionality equivalent to the GUI. Useful for automated builds. 62 | - Compression of executables with upx 63 | 64 | Planned New Features 65 | -------------------- 66 | 67 | - The ability to add external files to the project 68 | - Minifying JS and HTML 69 | 70 | 71 | Getting Started 72 | --------------- 73 | 74 | ### Using Prebuilt Binaries 75 | 76 | When using the prebuilt binaries for Windows, Mac, or Ubuntu, there are NO dependencies or extra applications needed. Simply download Web2Exe for the platform of your choice, extract, and double click the app/exe/binary to start. This applies to both the command-line version and the GUI version. 77 | 78 | **NOTE!**: Some people report needing the Microsoft Visual C++ 2008/2010 SP1 and regular Redistributable package. I don't have a machine to test this, but just know that you might need the package if the application won't open or spits out the following error: 79 | 80 | ``` 81 | Error: The application has failed to start because the side by side configuration is incorrect please see the application event log or use the command line sxstrace.exe tool for more detail. 82 | ``` 83 | 84 | 85 | ### Building from Source 86 | 87 | Requires the PySide library and Python 3.4.3 or higher. If you want to replace the icon in the Windows Exe's, this will do it automatically with the latest code if you have PIL or Pillow installed. 88 | 89 | ### Command line interface 90 | 91 | Dependencies: configobj (install with pip) and Pillow if you want icon replacement (not necessary) 92 | 93 | Run the command_line.py with the --help option to see a list of export options. Optionally, if you don't want to install python, there are builds for Mac and Windows in the command_line_builds folder of this repository. 94 | 95 | Example usage (if using the prebuilt binary, replace `python3.4 command_line.py` with the exe name): 96 | 97 | ``` 98 | python3.4 command_line.py /var/www/html/CargoBlaster/ --main html/index.html --export-to linux-x64 windows mac --width 900 --height 700 --nw-version 0.10.5 99 | ``` 100 | 101 | ### GUI 102 | 103 | Install dependencies: 104 | 105 | ``` 106 | pip install -r requirements.txt 107 | ``` 108 | 109 | Initiate submodules: 110 | 111 | ``` 112 | git submodule update --init --recursive 113 | ``` 114 | 115 | Run with: 116 | 117 | ``` 118 | python3.4 main.py 119 | ``` 120 | 121 | General Instructions for exporting: 122 | 123 | 1. Choose a project folder with at least one html or php file. The name of the export application will be autogenerated from the folder that you choose, so change it if you so desire. 124 | 2. Modify the options as needed. 125 | 3. Choose at least one export platform and then the Export button should be enabled (as long as the field names marked with a star are filled out and all files in the fields exist). 126 | 4. Click the export button and once it's done, click the "Open Export Folder" button to go to the folder where your exported project will be. 127 | 128 | 129 | ### Issues? 130 | 131 | If you have an issue, please check the FAQ before filing an issue to see if it helps. 132 | 133 | [FAQ](https://github.com/jyapayne/Web2Executable/wiki/FAQ) 134 | 135 | 136 | ### Additional Info 137 | 138 | [Changelog](https://github.com/jyapayne/Web2Executable/releases) 139 | 140 | [Screenshots](https://github.com/jyapayne/Web2Executable/wiki/Screenshots) 141 | 142 | 143 | License 144 | ------- 145 | 146 | The MIT License (MIT) 147 | 148 | Copyright (c) 2015 SimplyPixelated 149 | 150 | Permission is hereby granted, free of charge, to any person obtaining a copy 151 | of this software and associated documentation files (the "Software"), to deal 152 | in the Software without restriction, including without limitation the rights 153 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 154 | copies of the Software, and to permit persons to whom the Software is 155 | furnished to do so, subject to the following conditions: 156 | 157 | The above copyright notice and this permission notice shall be included in all 158 | copies or substantial portions of the Software. 159 | 160 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 161 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 162 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 163 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 164 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 165 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 166 | SOFTWARE. 167 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration module that holds some configuration options for 3 | Web2Executable. 4 | """ 5 | 6 | from __future__ import print_function 7 | import os 8 | import logging 9 | import logging.handlers as lh 10 | import traceback 11 | import sys 12 | import zipfile 13 | import ssl 14 | 15 | import utils 16 | 17 | ZIP_MODE = zipfile.ZIP_STORED 18 | 19 | MAX_RECENT = 10 20 | 21 | DEBUG = False 22 | TESTING = False 23 | 24 | SSL_CONTEXT = ssl._create_unverified_context() 25 | 26 | ### The following sections are code that needs to be run when importing 27 | ### from main.py. 28 | 29 | ## CWD Computation -------------------------------------- 30 | 31 | inside_packed_exe = getattr(sys, "frozen", "") 32 | 33 | if inside_packed_exe: 34 | # we are running in a |PyInstaller| bundle 35 | CWD = os.path.dirname(sys.executable) 36 | else: 37 | # we are running in a normal Python environment 38 | CWD = os.path.dirname(os.path.realpath(__file__)) 39 | 40 | ## CMD Utility functions -------------------------------- 41 | 42 | 43 | def get_file(path): 44 | parts = path.split("/") 45 | independent_path = utils.path_join(CWD, *parts) 46 | return independent_path 47 | 48 | 49 | def is_installed(): 50 | uninst = get_file("uninst.exe") 51 | return utils.is_windows() and os.path.exists(uninst) 52 | 53 | 54 | ## Version Setting ---------------------------------------- 55 | 56 | __version__ = "v0.0.0" 57 | 58 | with open(get_file("files/version.txt")) as f: 59 | __version__ = f.read().strip() 60 | 61 | ICON_PATH = "files/images/icon.png" 62 | WARNING_ICON = "files/images/warning.png" 63 | APP_SETTINGS_ICON = "files/images/app_settings.png" 64 | WINDOW_SETTINGS_ICON = "files/images/window_settings.png" 65 | EXPORT_SETTINGS_ICON = "files/images/export_settings.png" 66 | COMPRESS_SETTINGS_ICON = "files/images/compress_settings.png" 67 | DOWNLOAD_SETTINGS_ICON = "files/images/download_settings.png" 68 | FOLDER_OPEN_ICON = "files/images/folder_open.png" 69 | 70 | W2E_VER_FILE = "files/version.txt" 71 | 72 | TEMP_DIR = utils.get_temp_dir() 73 | 74 | ERROR_LOG_FILE = "files/error.log" 75 | VER_FILE = "files/nw-versions.txt" 76 | SETTINGS_FILE = "files/settings.cfg" 77 | GLOBAL_JSON_FILE = "files/global.json" 78 | WEB2EXE_JSON_FILE = "web2exe.json" 79 | 80 | LAST_PROJECT_FILE = "files/last_project_path.txt" 81 | RECENT_FILES_FILE = "files/recent_files.txt" 82 | 83 | NW_BRANCH_FILE = "files/nw-branch.txt" 84 | 85 | UPX_WIN_PATH = "files/compressors/upx-win.exe" 86 | UPX_MAC_PATH = "files/compressors/upx-mac" 87 | UPX_LIN32_PATH = "files/compressors/upx-linux-x32" 88 | UPX_LIN64_PATH = "files/compressors/upx-linux-x64" 89 | 90 | ENV_VARS_PY_PATH = "files/env_vars.py" 91 | ENV_VARS_BAT_PATH = "files/env_vars.bat" 92 | ENV_VARS_BASH_PATH = "files/env_vars.bash" 93 | 94 | ## Logger setup ---------------------------------------------- 95 | 96 | LOG_FILENAME = utils.get_data_file_path(ERROR_LOG_FILE) 97 | 98 | 99 | def getLogHandler(): 100 | return lh.RotatingFileHandler( 101 | LOG_FILENAME, maxBytes=100000, backupCount=2, encoding="utf-8" 102 | ) 103 | 104 | 105 | if DEBUG: 106 | logging.basicConfig( 107 | format=( 108 | "%(levelname) -10s %(asctime)s %(module)s.py: " 109 | "%(lineno)s %(funcName)s - %(message)s" 110 | ), 111 | level=logging.DEBUG, 112 | handlers=[getLogHandler()], 113 | ) 114 | else: 115 | logging.basicConfig( 116 | format=( 117 | "%(levelname) -10s %(asctime)s %(module)s.py: " 118 | "%(lineno)s %(funcName)s - %(message)s" 119 | ), 120 | level=logging.INFO, 121 | handlers=[getLogHandler()], 122 | ) 123 | 124 | 125 | def getLogger(name): 126 | logger = logging.getLogger(name) 127 | logger.addHandler(getLogHandler()) 128 | return logger 129 | 130 | 131 | logger = getLogger(__name__) 132 | 133 | 134 | ## Custom except hook to log all errors ---------------------- 135 | 136 | 137 | def my_excepthook(type_, value, tback): 138 | output_err = "".join([x for x in traceback.format_exception(type_, value, tback)]) 139 | logger.error("{}".format(output_err)) 140 | sys.__excepthook__(type_, value, tback) 141 | 142 | 143 | sys.excepthook = my_excepthook 144 | 145 | 146 | def download_path(path=None): 147 | # Ensure that the default download path exists 148 | path = path or utils.get_data_path("files/downloads") 149 | try: 150 | os.makedirs(path) 151 | except: 152 | pass 153 | return path 154 | -------------------------------------------------------------------------------- /files/base_url.txt: -------------------------------------------------------------------------------- 1 | http://dl.node-webkit.org/v{}/ 2 | -------------------------------------------------------------------------------- /files/compressors/upx-linux-x32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-linux-x32 -------------------------------------------------------------------------------- /files/compressors/upx-linux-x64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-linux-x64 -------------------------------------------------------------------------------- /files/compressors/upx-mac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-mac -------------------------------------------------------------------------------- /files/compressors/upx-win.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/compressors/upx-win.exe -------------------------------------------------------------------------------- /files/env_vars.bash: -------------------------------------------------------------------------------- 1 | PROJECT_DIR='{proj_dir}' 2 | PROJECT_NAME='{proj_name}' 3 | EXPORT_DIR='{export_dir}' 4 | EXPORT_DIRS=({export_dirs}) 5 | MAC64_EXPORT_DIR='{mac-x64_dir}' 6 | MAC32_EXPORT_DIR='{mac-x32_dir}' 7 | WIN64_EXPORT_DIR='{windows-x64_dir}' 8 | WIN32_EXPORT_DIR='{windows-x32_dir}' 9 | LINUX64_EXPORT_DIR='{linux-x64_dir}' 10 | LINUX32_EXPORT_DIR='{linux-x32_dir}' 11 | NUM_DIRS='{num_dirs}' 12 | -------------------------------------------------------------------------------- /files/env_vars.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set "PROJECT_DIR={proj_dir}" 3 | set "PROJECT_NAME={proj_name}" 4 | set "EXPORT_DIR={export_dir}" 5 | set "MAC64_EXPORT_DIR={mac-x64_dir}" 6 | set "MAC32_EXPORT_DIR={mac-x32_dir}" 7 | set "WIN64_EXPORT_DIR={windows-x64_dir}" 8 | set "WIN32_EXPORT_DIR={windows-x32_dir}" 9 | set "LINUX64_EXPORT_DIR={linux-x64_dir}" 10 | set "LINUX32_EXPORT_DIR={linux-x32_dir}" 11 | set "NUM_DIRS={num_dirs}" 12 | {export_dirs} 13 | -------------------------------------------------------------------------------- /files/env_vars.py: -------------------------------------------------------------------------------- 1 | PROJECT_DIR = "{proj_dir}" 2 | PROJECT_NAME = "{proj_name}" 3 | EXPORT_DIR = "{export_dir}" 4 | EXPORT_DIRS = {export_dirs} 5 | MAC64_EXPORT_DIR = "{mac-x64_dir}" 6 | MAC32_EXPORT_DIR = "{mac-x32_dir}" 7 | WIN64_EXPORT_DIR = "{windows-x64_dir}" 8 | WIN32_EXPORT_DIR = "{windows-x32_dir}" 9 | LINUX64_EXPORT_DIR = "{linux-x64_dir}" 10 | LINUX32_EXPORT_DIR = "{linux-x32_dir}" 11 | NUM_DIRS = {num_dirs} 12 | -------------------------------------------------------------------------------- /files/images/app_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/app_settings.png -------------------------------------------------------------------------------- /files/images/app_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 39 | 41 | 44 | 48 | 52 | 53 | 56 | 60 | 64 | 65 | 75 | 84 | 94 | 95 | 97 | 98 | 100 | image/svg+xml 101 | 103 | 104 | 105 | 106 | 107 | 111 | 119 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /files/images/compress_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/compress_settings.png -------------------------------------------------------------------------------- /files/images/compress_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 42 | 44 | 47 | 51 | 55 | 56 | 59 | 63 | 67 | 68 | 78 | 87 | 97 | 98 | 100 | 101 | 103 | image/svg+xml 104 | 106 | 107 | 108 | 109 | 110 | 114 | 122 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /files/images/download_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/download_settings.png -------------------------------------------------------------------------------- /files/images/download_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 42 | 44 | 47 | 51 | 55 | 56 | 59 | 63 | 67 | 68 | 71 | 75 | 79 | 80 | 90 | 99 | 109 | 118 | 119 | 121 | 122 | 124 | image/svg+xml 125 | 127 | 128 | 129 | 130 | 131 | 135 | 143 | 149 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /files/images/export_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/export_settings.png -------------------------------------------------------------------------------- /files/images/export_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 42 | 44 | 47 | 51 | 55 | 56 | 59 | 63 | 67 | 68 | 71 | 75 | 79 | 80 | 90 | 99 | 109 | 119 | 120 | 122 | 123 | 125 | image/svg+xml 126 | 128 | 129 | 130 | 131 | 132 | 136 | 144 | 150 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /files/images/folder_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/folder_open.png -------------------------------------------------------------------------------- /files/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/icon.png -------------------------------------------------------------------------------- /files/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /files/images/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/warning.png -------------------------------------------------------------------------------- /files/images/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 41 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 59 | 62 | 68 | 74 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /files/images/window_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/files/images/window_settings.png -------------------------------------------------------------------------------- /files/images/window_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 42 | 44 | 47 | 51 | 55 | 56 | 59 | 63 | 67 | 68 | 78 | 87 | 97 | 98 | 100 | 101 | 103 | image/svg+xml 104 | 106 | 107 | 108 | 109 | 110 | 114 | 122 | 128 | 134 | 140 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /files/nw-versions.txt: -------------------------------------------------------------------------------- 1 | 0.12.2 2 | 0.12.1 3 | 0.12.0 4 | 0.12.0-rc1 5 | 0.12.0-alpha3 6 | 0.12.0-alpha2 7 | 0.12.0-alpha1 8 | 0.11.3 9 | 0.11.2 10 | 0.11.1 11 | 0.11.0 12 | 0.11.0-rc1 13 | 0.10.5 14 | 0.10.4 15 | 0.10.3 16 | 0.10.2 17 | 0.10.1 18 | 0.10.0 19 | 0.10.0-rc2 20 | 0.10.0-rc1 21 | 0.9.2 22 | 0.9.1 23 | 0.9.0 24 | 0.8.6 25 | 0.8.5 26 | 0.8.4 27 | 0.8.3 28 | 0.8.2 29 | 0.8.1 30 | 0.8.0 31 | 0.7.2 32 | 0.7.1 33 | 0.7.0 34 | 0.6.3 35 | 0.6.2 36 | 0.6.1 37 | 0.6.0 38 | -------------------------------------------------------------------------------- /files/settings.cfg: -------------------------------------------------------------------------------- 1 | base_url='https://dl.nwjs.io/v{}/' 2 | win_32_dir_prefix = 'nwjs-v{}-win-ia32' 3 | mac_32_dir_prefix = 'nwjs-v{}-osx-ia32' 4 | linux_32_dir_prefix = 'nwjs-v{}-linux-ia32' 5 | 6 | win_64_dir_prefix = 'nwjs-v{}-win-x64' 7 | mac_64_dir_prefix = 'nwjs-v{}-osx-x64' 8 | linux_64_dir_prefix = 'nwjs-v{}-linux-x64' 9 | 10 | [setting_groups] 11 | [[app_settings]] 12 | [[[main]]] 13 | display_name='Main html file' 14 | required=True 15 | type='file' 16 | file_types='*.html *.php *.htm' 17 | description='Main html file relative to the project directory.' 18 | [[[name]]] 19 | display_name='Name' 20 | required=True 21 | type='string' 22 | description='The name in the internal package.json. Must be alpha-numeric with no spaces.' 23 | filter='[a-z0-9_\-\.]+' 24 | filter_action='lower' 25 | [[[app_name]]] 26 | display_name='App Name' 27 | required=False 28 | type='string' 29 | description='The name that your executable or app will have when exported.' 30 | [[[description]]] 31 | default_value=None 32 | type='string' 33 | [[[version]]] 34 | default_value=None 35 | type='string' 36 | [[[keywords]]] 37 | default_value=None 38 | type='string' 39 | [[[nodejs]]] 40 | display_name='Include Nodejs' 41 | default_value=None 42 | type='check' 43 | [[[node-main]]] 44 | display_name='Node Main' 45 | default_value=None 46 | type='file' 47 | file_types='*.js' 48 | description='A path to a nodejs script file that will be executed on startup.' 49 | [[[domain]]] 50 | default_value=None 51 | type='string' 52 | description='Specify the host in the chrome-extension:// protocol URL used fo\n the application. The web engine will share the same cookies between your\napplication and the website under the same domain.' 53 | [[[user-agent]]] 54 | display_name='User Agent' 55 | default_value=None 56 | type='string' 57 | description='Overrides the User-Agent header in http requests.\n\nThe following placeholders are available to composite the user agent dynamically:\n\n%name: replaced by the name field in the manifest.\n%ver: replaced by the version field in the manifest, if available.\n%nwver: replaced by the version of NW.js.\n%webkit_ver: replaced by the version of WebKit engine.\n%osinfo: replaced by the OS and CPU information you would see in browser’s user agent string.' 58 | [[[node-remote]]] 59 | display_name='Node Remote' 60 | default_value=None 61 | type='strings' 62 | description='Enable calling node in remote pages. See the node-webkit\nmanifest format for more info.' 63 | [[[chromium-args]]] 64 | display_name='Chromium Args' 65 | default_value=None 66 | type='string' 67 | description='Specify chromium command line arguments.\nExample value: "--disable-accelerated-video --force-cpu-draw"' 68 | [[[js-flags]]] 69 | display_name='JS Flags' 70 | default_value=None 71 | type='string' 72 | description='Specify flags passed to the js engine.\nExample value: "--harmony_proxies --harmony_collecions"' 73 | [[[bg-script]]] 74 | display_name='Background Script' 75 | default_value=None 76 | type='file' 77 | file_types='*.js' 78 | description='Background script. The script is executed in the background page at\nthe start of application.' 79 | [[[inject_js_start]]] 80 | display_name='Inject JS Start' 81 | default_value=None 82 | type='file' 83 | file_types='*.js' 84 | description='A path to a js file that will be executed before any\nother script is run.' 85 | [[[inject_js_end]]] 86 | display_name='Inject JS End' 87 | default_value=None 88 | type='file' 89 | file_types='*.js' 90 | description='A path to a js file that will be executed after the\nDOM is loaded.' 91 | [[[additional_trust_anchors]]] 92 | display_name='Trust Anchors' 93 | default_value=None 94 | type='strings' 95 | description='A list of PEM-encoded certificates. Used as additional root\ncertificates for validation to allow connecting to services using a self-signed certificate.' 96 | [[[dom_storage_quota]]] 97 | display_name='DOM Storage (MB)' 98 | default_value=None 99 | type='int' 100 | description='Number of mega bytes (MB) for the quota of the DOM storage.\nThe suggestion is to put double the value you want.' 101 | 102 | [[webkit_settings]] 103 | [[[plugin]]] 104 | display_name='Load Plugins' 105 | default_value=None 106 | type='check' 107 | description='Whether to load external browser plugins like Flash.\nFor example, put the Pepper flash dll or so file in a directory at the root of\nyour project called "PepperFlash" and it will be loaded.' 108 | [[[double_tap_to_zoom_enabled]]] 109 | display_name='Double-Tap Zoom' 110 | default_value=None 111 | type='check' 112 | description='Enable zooming with double tapping on Mac OS X with 2 fingers. Mac OS X only' 113 | 114 | [[web2exe_settings]] 115 | [[[export_dir]]] 116 | display_name='Output Directory' 117 | default_value='' 118 | type='string' 119 | description='The output directory relative to the project directory.' 120 | [[[custom_script]]] 121 | display_name='Execute Script' 122 | default_value='' 123 | copy=False 124 | type='file' 125 | description='The script to execute after a project was successfully exported.' 126 | [[[output_pattern]]] 127 | display_name='Output Name Pattern' 128 | default_value='' 129 | type='string' 130 | description='Type "%(" to see a list of options to reference. Name your output folder.\n Include slashes to make sub-directories.' 131 | [[[blacklist]]] 132 | display_name='Blacklist' 133 | default_value='' 134 | type='string' 135 | description='Glob-style blacklist files/directories. Each line is a new pattern. Ex: *.jpeg, .git, *file[s].txt' 136 | [[[whitelist]]] 137 | display_name='Whitelist' 138 | default_value='' 139 | type='string' 140 | description='Glob-style whitelist files/directories. Each line is a new pattern. Ex: *.jpeg, .git, *file[s].txt.\nWhitelist trumps blacklist.' 141 | 142 | [[window_settings]] 143 | [[[id]]] 144 | default_value=None 145 | type='string' 146 | description='The id used to identify the window. This will be used to remember the\nsize and position of the window and restore that geometry when a\nwindow with the same id is later opened. ' 147 | [[[title]]] 148 | default_value=None 149 | type='string' 150 | description='The default title of window created by NW.js, it’s very useful\nif you want to show your own title when the app is starting.' 151 | [[[icon]]] 152 | display_name='Window Icon' 153 | default_value=None 154 | type='file' 155 | action='set_window_icon' 156 | file_types='*.png *.jpg *.jpeg' 157 | [[[mac_icon]]] 158 | default_value=None 159 | type='file' 160 | action='set_mac_icon' 161 | file_types='*.png *.jpg *.jpeg *.icns' 162 | description='This icon to be displayed for the Mac Application.\nDefaults to Window Icon' 163 | [[[exe_icon]]] 164 | default_value=None 165 | type='file' 166 | action='set_exe_icon' 167 | file_types='*.png *.jpg *.jpeg' 168 | description='This icon to be displayed for the windows exe of the app.\nDefaults to Window icon.' 169 | [[[width]]] 170 | default_value=None 171 | type='int' 172 | [[[height]]] 173 | default_value=None 174 | type='int' 175 | [[[min_width]]] 176 | default_value=None 177 | type='int' 178 | [[[min_height]]] 179 | default_value=None 180 | type='int' 181 | [[[max_width]]] 182 | default_value=None 183 | type='int' 184 | [[[max_height]]] 185 | default_value=None 186 | type='int' 187 | [[[always_on_top]]] 188 | display_name='Keep on Top' 189 | default_value=None 190 | type='check' 191 | description='Makes the window always on top of other windows.' 192 | [[[frame]]] 193 | display_name='Window Frame' 194 | default_value=True 195 | type='check' 196 | description='Show the frame of the window' 197 | [[[show_in_taskbar]]] 198 | display_name='Taskbar' 199 | default_value=True 200 | type='check' 201 | description='Show the app running in the taskbar' 202 | [[[show]]] 203 | display_name='Show' 204 | default_value=True 205 | type='check' 206 | description='Uncheck to make your app hidden on startup.' 207 | [[[visible_on_all_workspaces]]] 208 | display_name='All Workspaces' 209 | default_value=None 210 | type='check' 211 | description='If checked, the exported app will be visible on all workspaces.\nMac & Linux Only' 212 | [[[visible]]] 213 | default_value=True 214 | type='check' 215 | description='If unchecked, the app will have to be manually set to\nvisible in javascript.' 216 | [[[resizable]]] 217 | default_value=True 218 | type='check' 219 | description='' 220 | [[[fullscreen]]] 221 | default_value=None 222 | type='check' 223 | description='' 224 | [[[position]]] 225 | display_name='Position by' 226 | default_value=None 227 | values=[None, 'mouse', 'center'] 228 | type='list' 229 | description='The position to place the window when it opens.' 230 | [[[as_desktop]]] 231 | default_value=None 232 | type='check' 233 | description='Show as desktop background window under X11 environment. Linux Only.' 234 | [[[transparent]]] 235 | default_value=None 236 | type='check' 237 | description='Allows window tranparency.' 238 | [[[kiosk]]] 239 | default_value=None 240 | type='check' 241 | description='Puts the application is kiosk mode.' 242 | [[[kiosk_emulation]]] 243 | default_value=None 244 | type='check' 245 | description='Puts the application is kiosk emulation mode. Will\nautomatically check off required settings that will emulate kiosk.' 246 | check_action='set_kiosk_emulation_options' 247 | 248 | [[download_settings]] 249 | [[[nw_version]]] 250 | display_name='NW.js version' 251 | required=False 252 | default_value=None 253 | values=[] 254 | type='list' 255 | button='Update' 256 | button_callback='update_nw_versions' 257 | action='refresh_export' 258 | [[[sdk_build]]] 259 | display_name='SDK build' 260 | default_value=False 261 | type='check' 262 | description='Downloads the SDK version of NW.js to support devtools by\npressing F12 or ⌘+⌥+i.' 263 | [[[force_download]]] 264 | default_value=False 265 | type='check' 266 | [[[download_dir]]] 267 | display_name='Download location' 268 | default_value='' 269 | type='folder' 270 | 271 | [export_settings] 272 | [[windows-x32]] 273 | default_value=None 274 | type='check' 275 | url='%(base_url)s%(win_32_dir_prefix)s.zip' 276 | binary_location='nw.exe' 277 | system='windows' 278 | short_system='win' 279 | arch='x32' 280 | [[windows-x64]] 281 | default_value=None 282 | type='check' 283 | url='%(base_url)s%(win_64_dir_prefix)s.zip' 284 | binary_location='nw.exe' 285 | system='windows' 286 | short_system='win' 287 | arch='x64' 288 | [[mac-x64]] 289 | default_value=None 290 | type='check' 291 | url='%(base_url)s%(mac_64_dir_prefix)s.zip' 292 | system='mac' 293 | short_system='mac' 294 | arch='x64' 295 | [[linux-x64]] 296 | default_value=None 297 | type='check' 298 | url='%(base_url)s%(linux_64_dir_prefix)s.tar.gz' 299 | binary_location='nw' 300 | system='linux' 301 | short_system='lin' 302 | arch='x64' 303 | [[linux-x32]] 304 | default_value=None 305 | type='check' 306 | url='%(base_url)s%(linux_32_dir_prefix)s.tar.gz' 307 | binary_location='nw' 308 | system='linux' 309 | short_system='lin' 310 | arch='x32' 311 | 312 | [compression] 313 | [[nw_compression_level]] 314 | display_name='Compression Level' 315 | default_value=0 316 | min=0 317 | max=9 318 | type='range' 319 | description='Compression to be applied to the executable\'s nwjs binary.\n0 is no compression, 9 is maximum. They all use lzma.' 320 | [[uncompressed_folder]] 321 | display_name='Uncompressed Folder' 322 | type='check' 323 | default_value=False 324 | description='This option makes the resulting app.nw inside the app just a\nplain folder. This is useful to mitigate startup\ntimes and to modify files.' 325 | 326 | 327 | [order] 328 | application_setting_order="""['main', 'name', 'app_name', 'node-main', 'description', 'version', 'keywords', 329 | 'user-agent', 'chromium-args', 'domain', 330 | 'node-remote', 'js-flags', 'bg-script', 'inject_js_start', 'inject_js_end', 331 | 'additional_trust_anchors', 332 | 'nodejs', 'plugin', 'double_tap_to_zoom_enabled']""" 333 | window_setting_order = """['id','title', 'icon', 'mac_icon', 'exe_icon', 'position', 'width', 'height', 334 | 'min_width', 'min_height', 335 | 'max_width', 'max_height', 'dom_storage_quota', 'always_on_top', 'frame', 336 | 'show_in_taskbar', 'show', 'visible', 'visible_on_all_workspaces', 337 | 'resizable', 'fullscreen', 'as_desktop', 338 | 'kiosk', 'kiosk_emulation', 'transparent']""" 339 | 340 | export_setting_order = """['windows-x32', 'windows-x64', 'mac-x64', 'linux-x64', 'linux-x32']""" 341 | compression_setting_order = """['nw_compression_level', 'uncompressed_folder']""" 342 | 343 | download_setting_order = """['nw_version', 'sdk_build', 'download_dir', 344 | 'force_download']""" 345 | 346 | [version_info] 347 | urls="""[('https://raw.githubusercontent.com/nwjs/nw.js/{}/CHANGELOG.md', '(\S+) / \d{2}-\d{2}-\d{4}'), ('http://nwjs.io/blog/', 'NW.js v(\S+) ')]""" 348 | github_api_url="https://api.github.com/repos/nwjs/nw.js" 349 | -------------------------------------------------------------------------------- /files/version.txt: -------------------------------------------------------------------------------- 1 | v0.7.2b 2 | -------------------------------------------------------------------------------- /image_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/image_utils/__init__.py -------------------------------------------------------------------------------- /image_utils/image_utils.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | try: 4 | from PIL import Image as im 5 | 6 | IMAGE_UTILS_AVAILABLE = True 7 | Image = im.open 8 | 9 | def resize(image, size): 10 | output = BytesIO() 11 | back = im.new("RGBA", size, (0, 0, 0, 0)) 12 | image.thumbnail(size, im.ANTIALIAS) 13 | offset = [0, 0] 14 | if image.size[0] >= image.size[1]: 15 | offset[1] = int(back.size[1] / 2 - image.size[1] / 2) 16 | else: 17 | offset[0] = int(back.size[0] / 2 - image.size[0] / 2) 18 | back.paste(image, tuple(offset)) 19 | back.save(output, image.format) 20 | contents = output.getvalue() 21 | output.close() 22 | return contents 23 | 24 | except ImportError: 25 | IMAGE_UTILS_AVAILABLE = False 26 | 27 | LARGEST_ICON_SIZE = 1024 28 | SMALLEST_ICON_SIZE = 16 29 | sizes = [LARGEST_ICON_SIZE, 512, 256, 128, 48, 32, SMALLEST_ICON_SIZE] 30 | 31 | 32 | def nearest_icon_size(width, height): 33 | maximum = max(width, height) 34 | 35 | if maximum >= LARGEST_ICON_SIZE: 36 | return LARGEST_ICON_SIZE 37 | if maximum <= SMALLEST_ICON_SIZE: 38 | return SMALLEST_ICON_SIZE 39 | 40 | for i in range(len(sizes) - 1): 41 | current_size = sizes[i] 42 | next_size = sizes[i + 1] 43 | if current_size > maximum >= next_size: 44 | return next_size 45 | -------------------------------------------------------------------------------- /image_utils/pycns.py: -------------------------------------------------------------------------------- 1 | from image_utils.icns_info import ICNSHeader, icns_to_png 2 | from image_utils.image_utils import Image 3 | import sys 4 | 5 | """This module takes any image that is readable by PIL and exports it to an icns file. 6 | The image will be scaled keeping the aspect ratio if it is a non square image. 7 | """ 8 | 9 | 10 | def encode_image_to_icns(image_path): 11 | """Takes an image and converts it to icns. Image aspect ratio will remain intact.""" 12 | i = Image(image_path) 13 | icns_header = ICNSHeader() 14 | f_data = icns_header.parse_image(i) 15 | return f_data 16 | 17 | 18 | def save_icns(image_path, icns_path): 19 | im_data = encode_image_to_icns(image_path) 20 | if im_data is not None: 21 | icns_path = icns_path if icns_path.endswith(".icns") else icns_path + ".icns" 22 | open(icns_path, "wb+").write(im_data) 23 | 24 | 25 | def pngs_from_icns(icns_path): 26 | return icns_to_png(icns_path) 27 | 28 | 29 | if __name__ == "__main__": 30 | if len(sys.argv) != 3: 31 | print("Usage: pycns input_image_path output_icns_path") 32 | sys.exit() 33 | 34 | image_path, icns_path = sys.argv[1:3] 35 | 36 | save_icns(image_path, icns_path) 37 | -------------------------------------------------------------------------------- /images/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/images/icon.icns -------------------------------------------------------------------------------- /images/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/images/icon.ico -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwutils/Web2Executable/15f257d1c923983b6ad2f3b0780c186f8b4e49e9/images/icon.png -------------------------------------------------------------------------------- /pe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright Joey Payne All Rights Reserved 3 | """ 4 | 5 | import os 6 | import struct 7 | from io import BytesIO 8 | 9 | from PIL import Image 10 | 11 | 12 | def resize(image, size, format=None): 13 | output = BytesIO() 14 | back = Image.new("RGBA", size, (0, 0, 0, 0)) 15 | 16 | if image.size[0] < size[0] or image.size[1] < size[1]: 17 | if image.height > image.width: 18 | factor = size[0] / image.height 19 | else: 20 | factor = size[1] / image.width 21 | image = image.resize( 22 | (int(image.width * factor), int(image.height * factor)), Image.ANTIALIAS 23 | ) 24 | else: 25 | image.thumbnail(size, Image.ANTIALIAS) 26 | 27 | offset = [0, 0] 28 | if image.size[0] > image.size[1]: 29 | offset[1] = int(back.size[1] / 2 - image.size[1] / 2) 30 | elif image.size[0] < image.size[1]: 31 | offset[0] = int(back.size[0] / 2 - image.size[0] / 2) 32 | else: 33 | offset[0] = int(back.size[0] / 2 - image.size[0] / 2) 34 | offset[1] = int(back.size[1] / 2 - image.size[1] / 2) 35 | 36 | back.paste(image, tuple(offset)) 37 | format = format or image.format 38 | back.save(output, format, sizes=[size]) 39 | contents = output.getvalue() 40 | output.close() 41 | return contents 42 | 43 | 44 | struct_symbols = { 45 | 1: "B", # byte 46 | 2: "H", # word 47 | 4: "L", # long word 48 | 8: "Q", # double long word 49 | } 50 | endian_symbols = {"little": "<", "big": ">"} 51 | 52 | name_dictionary = { 53 | "PEHeader_Machine": { 54 | 0: "IMAGE_FILE_MACHINE_UNKNOWN", 55 | 0x014C: "IMAGE_FILE_MACHINE_I386", 56 | 0x0162: "IMAGE_FILE_MACHINE_R3000", 57 | 0x0166: "IMAGE_FILE_MACHINE_R4000", 58 | 0x0168: "IMAGE_FILE_MACHINE_R10000", 59 | 0x0169: "IMAGE_FILE_MACHINE_WCEMIPSV2", 60 | 0x0184: "IMAGE_FILE_MACHINE_ALPHA", 61 | 0x01A2: "IMAGE_FILE_MACHINE_SH3", 62 | 0x01A3: "IMAGE_FILE_MACHINE_SH3DSP", 63 | 0x01A4: "IMAGE_FILE_MACHINE_SH3E", 64 | 0x01A6: "IMAGE_FILE_MACHINE_SH4", 65 | 0x01A8: "IMAGE_FILE_MACHINE_SH5", 66 | 0x01C0: "IMAGE_FILE_MACHINE_ARM", 67 | 0x01C2: "IMAGE_FILE_MACHINE_THUMB", 68 | 0x01C4: "IMAGE_FILE_MACHINE_ARMNT", 69 | 0x01D3: "IMAGE_FILE_MACHINE_AM33", 70 | 0x01F0: "IMAGE_FILE_MACHINE_POWERPC", 71 | 0x01F1: "IMAGE_FILE_MACHINE_POWERPCFP", 72 | 0x0200: "IMAGE_FILE_MACHINE_IA64", 73 | 0x0266: "IMAGE_FILE_MACHINE_MIPS16", 74 | 0x0284: "IMAGE_FILE_MACHINE_ALPHA64", 75 | 0x0284: "IMAGE_FILE_MACHINE_AXP64", # same 76 | 0x0366: "IMAGE_FILE_MACHINE_MIPSFPU", 77 | 0x0466: "IMAGE_FILE_MACHINE_MIPSFPU16", 78 | 0x0520: "IMAGE_FILE_MACHINE_TRICORE", 79 | 0x0CEF: "IMAGE_FILE_MACHINE_CEF", 80 | 0x0EBC: "IMAGE_FILE_MACHINE_EBC", 81 | 0x8664: "IMAGE_FILE_MACHINE_AMD64", 82 | 0x9041: "IMAGE_FILE_MACHINE_M32R", 83 | 0xC0EE: "IMAGE_FILE_MACHINE_CEE", 84 | }, 85 | "PEHeader_Characteristics": { 86 | 0x0001: "IMAGE_FILE_RELOCS_STRIPPED", 87 | 0x0002: "IMAGE_FILE_EXECUTABLE_IMAGE", 88 | 0x0004: "IMAGE_FILE_LINE_NUMS_STRIPPED", 89 | 0x0008: "IMAGE_FILE_LOCAL_SYMS_STRIPPED", 90 | 0x0010: "IMAGE_FILE_AGGRESIVE_WS_TRIM", 91 | 0x0020: "IMAGE_FILE_LARGE_ADDRESS_AWARE", 92 | 0x0040: "IMAGE_FILE_16BIT_MACHINE", 93 | 0x0080: "IMAGE_FILE_BYTES_REVERSED_LO", 94 | 0x0100: "IMAGE_FILE_32BIT_MACHINE", 95 | 0x0200: "IMAGE_FILE_DEBUG_STRIPPED", 96 | 0x0400: "IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP", 97 | 0x0800: "IMAGE_FILE_NET_RUN_FROM_SWAP", 98 | 0x1000: "IMAGE_FILE_SYSTEM", 99 | 0x2000: "IMAGE_FILE_DLL", 100 | 0x4000: "IMAGE_FILE_UP_SYSTEM_ONLY", 101 | 0x8000: "IMAGE_FILE_BYTES_REVERSED_HI", 102 | }, 103 | "OptionalHeader_Subsystem": { 104 | 0: "IMAGE_SUBSYSTEM_UNKNOWN", 105 | 1: "IMAGE_SUBSYSTEM_NATIVE", 106 | 2: "IMAGE_SUBSYSTEM_WINDOWS_GUI", 107 | 3: "IMAGE_SUBSYSTEM_WINDOWS_CUI", 108 | 5: "IMAGE_SUBSYSTEM_OS2_CUI", 109 | 7: "IMAGE_SUBSYSTEM_POSIX_CUI", 110 | 8: "IMAGE_SUBSYSTEM_NATIVE_WINDOWS", 111 | 9: "IMAGE_SUBSYSTEM_WINDOWS_CE_GUI", 112 | 10: "IMAGE_SUBSYSTEM_EFI_APPLICATION", 113 | 11: "IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER", 114 | 12: "IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER", 115 | 13: "IMAGE_SUBSYSTEM_EFI_ROM", 116 | 14: "IMAGE_SUBSYSTEM_XBOX", 117 | 16: "IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION", 118 | }, 119 | "OptionalHeader_DLL_Characteristics": { 120 | 0x0001: "IMAGE_LIBRARY_PROCESS_INIT", # reserved 121 | 0x0002: "IMAGE_LIBRARY_PROCESS_TERM", # reserved 122 | 0x0004: "IMAGE_LIBRARY_THREAD_INIT", # reserved 123 | 0x0008: "IMAGE_LIBRARY_THREAD_TERM", # reserved 124 | 0x0020: "IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA", 125 | 0x0040: "IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE", 126 | 0x0080: "IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY", 127 | 0x0100: "IMAGE_DLLCHARACTERISTICS_NX_COMPAT", 128 | 0x0200: "IMAGE_DLLCHARACTERISTICS_NO_ISOLATION", 129 | 0x0400: "IMAGE_DLLCHARACTERISTICS_NO_SEH", 130 | 0x0800: "IMAGE_DLLCHARACTERISTICS_NO_BIND", 131 | 0x1000: "IMAGE_DLLCHARACTERISTICS_APPCONTAINER", 132 | 0x2000: "IMAGE_DLLCHARACTERISTICS_WDM_DRIVER", 133 | 0x4000: "IMAGE_DLLCHARACTERISTICS_GUARD_CF", 134 | 0x8000: "IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE", 135 | }, 136 | "SectionHeader_Characteristics": { 137 | 0x00000000: "IMAGE_SCN_TYPE_REG", # reserved 138 | 0x00000001: "IMAGE_SCN_TYPE_DSECT", # reserved 139 | 0x00000002: "IMAGE_SCN_TYPE_NOLOAD", # reserved 140 | 0x00000004: "IMAGE_SCN_TYPE_GROUP", # reserved 141 | 0x00000008: "IMAGE_SCN_TYPE_NO_PAD", # reserved 142 | 0x00000010: "IMAGE_SCN_TYPE_COPY", # reserved 143 | 0x00000020: "IMAGE_SCN_CNT_CODE", 144 | 0x00000040: "IMAGE_SCN_CNT_INITIALIZED_DATA", 145 | 0x00000080: "IMAGE_SCN_CNT_UNINITIALIZED_DATA", 146 | 0x00000100: "IMAGE_SCN_LNK_OTHER", 147 | 0x00000200: "IMAGE_SCN_LNK_INFO", 148 | 0x00000400: "IMAGE_SCN_LNK_OVER", # reserved 149 | 0x00000800: "IMAGE_SCN_LNK_REMOVE", 150 | 0x00001000: "IMAGE_SCN_LNK_COMDAT", 151 | 0x00004000: "IMAGE_SCN_MEM_PROTECTED", # obsolete 152 | 0x00004000: "IMAGE_SCN_NO_DEFER_SPEC_EXC", 153 | 0x00008000: "IMAGE_SCN_GPREL", 154 | 0x00008000: "IMAGE_SCN_MEM_FARDATA", 155 | 0x00010000: "IMAGE_SCN_MEM_SYSHEAP", # obsolete 156 | 0x00020000: "IMAGE_SCN_MEM_PURGEABLE", 157 | 0x00020000: "IMAGE_SCN_MEM_16BIT", 158 | 0x00040000: "IMAGE_SCN_MEM_LOCKED", 159 | 0x00080000: "IMAGE_SCN_MEM_PRELOAD", 160 | 0x00100000: "IMAGE_SCN_ALIGN_1BYTES", 161 | 0x00200000: "IMAGE_SCN_ALIGN_2BYTES", 162 | 0x00300000: "IMAGE_SCN_ALIGN_4BYTES", 163 | 0x00400000: "IMAGE_SCN_ALIGN_8BYTES", 164 | 0x00500000: "IMAGE_SCN_ALIGN_16BYTES", # default alignment 165 | 0x00600000: "IMAGE_SCN_ALIGN_32BYTES", 166 | 0x00700000: "IMAGE_SCN_ALIGN_64BYTES", 167 | 0x00800000: "IMAGE_SCN_ALIGN_128BYTES", 168 | 0x00900000: "IMAGE_SCN_ALIGN_256BYTES", 169 | 0x00A00000: "IMAGE_SCN_ALIGN_512BYTES", 170 | 0x00B00000: "IMAGE_SCN_ALIGN_1024BYTES", 171 | 0x00C00000: "IMAGE_SCN_ALIGN_2048BYTES", 172 | 0x00D00000: "IMAGE_SCN_ALIGN_4096BYTES", 173 | 0x00E00000: "IMAGE_SCN_ALIGN_8192BYTES", 174 | 0x00F00000: "IMAGE_SCN_ALIGN_MASK", 175 | 0x01000000: "IMAGE_SCN_LNK_NRELOC_OVFL", 176 | 0x02000000: "IMAGE_SCN_MEM_DISCARDABLE", 177 | 0x04000000: "IMAGE_SCN_MEM_NOT_CACHED", 178 | 0x08000000: "IMAGE_SCN_MEM_NOT_PAGED", 179 | 0x10000000: "IMAGE_SCN_MEM_SHARED", 180 | 0x20000000: "IMAGE_SCN_MEM_EXECUTE", 181 | 0x40000000: "IMAGE_SCN_MEM_READ", 182 | 0x80000000: "IMAGE_SCN_MEM_WRITE", 183 | }, 184 | } 185 | 186 | DEFAULT_ENDIAN = "little" 187 | 188 | 189 | def read_data(file_data, offset, number_of_bytes, string_data=None): 190 | """Just reads the straight data with no endianness.""" 191 | 192 | if number_of_bytes > 0: 193 | data = file_data[offset : offset + number_of_bytes] 194 | # if len(data) != number_of_bytes: 195 | # print 'data out of bounds:', 'offset', hex(offset), 'data', data, 'data_len', len(data), 'num_bytes', number_of_bytes, 'total', hex(len(file_data)) 196 | return data 197 | else: 198 | return bytearray("") 199 | 200 | 201 | def read_bytes(file_data, offset, number_of_bytes, endian=None, string_data=None): 202 | """Returns a tuple of the data value and string representation. 203 | Will read 1,2,4,8 bytes with little endian as the default 204 | (value, string) 205 | """ 206 | 207 | if number_of_bytes > 0: 208 | endian = endian or DEFAULT_ENDIAN 209 | endian = endian_symbols[endian] 210 | 211 | data = bytes(file_data[offset : offset + number_of_bytes]) 212 | if len(data) != number_of_bytes: 213 | return 0, "" 214 | 215 | return struct.unpack(endian + struct_symbols[number_of_bytes], data)[0], data 216 | else: 217 | return 0, "" 218 | 219 | 220 | def value_to_byte_string(value, number_of_bytes, endian=None): 221 | endian = endian or DEFAULT_ENDIAN 222 | endian = endian_symbols[endian] 223 | 224 | return struct.pack(endian + struct_symbols[number_of_bytes], value) 225 | 226 | 227 | class ResourceTypes(object): 228 | Cursor = 1 229 | Bitmap = 2 230 | Icon = 3 231 | Menu = 4 232 | Dialog = 5 233 | String = 6 234 | Font_Directory = 7 235 | Font = 8 236 | Accelerator = 9 237 | RC_Data = 10 238 | Message_Table = 11 239 | Group_Cursor = 12 240 | Group_Icon = 14 241 | Version_Info = 16 242 | DLG_Include = 17 243 | Plug_Play = 19 244 | VXD = 20 245 | Animated_Cursor = 21 246 | Animated_Icon = 22 247 | HTML = 23 248 | Manifest = 24 249 | 250 | 251 | resource_types = { 252 | 1: "Cursor", 253 | 2: "Bitmap", 254 | 3: "Icon", 255 | 4: "Menu", 256 | 5: "Dialog", 257 | 6: "String", 258 | 7: "Font Directory", 259 | 8: "Font", 260 | 9: "Accelerator", 261 | 10: "RC Data", 262 | 11: "Message Table", 263 | 12: "Group Cursor", 264 | 14: "Group Icon", 265 | 16: "Version Info", 266 | 17: "DLG Include", 267 | 19: "Plug and Play", 268 | 20: "VXD", 269 | 21: "Animated Cursor", 270 | 22: "Animated Icon", 271 | 23: "HTML", 272 | 24: "Manifest", 273 | } 274 | 275 | _32BIT_PLUS_MAGIC = 0x20B 276 | _32BIT_MAGIC = 0x10B 277 | _ROM_MAGIC = 0x107 278 | 279 | 280 | def read_from_name_dict(obj, field_name): 281 | dict_field = "{}_{}".format(obj.__class__.__name__, field_name) 282 | return name_dictionary.get(dict_field, {}) 283 | 284 | 285 | def test_bit(value, index): 286 | mask = 1 << index 287 | return value & mask 288 | 289 | 290 | def set_bit(value, index): 291 | mask = 1 << index 292 | return value | mask 293 | 294 | 295 | def clear_bit(value, index): 296 | mask = ~(1 << index) 297 | return value & mask 298 | 299 | 300 | def toggle_bit(value, index): 301 | mask = 1 << index 302 | return value ^ mask 303 | 304 | 305 | class PEFormatError(Exception): 306 | pass 307 | 308 | 309 | class Printable(object): 310 | def _attrs(self): 311 | a = [] 312 | for attr in dir(self): 313 | if not attr.startswith("_") and not callable(getattr(self, attr)): 314 | a.append(attr) 315 | return a 316 | 317 | def _dict_items(self): 318 | for a in reversed(self._attrs()): 319 | yield a, getattr(self, a) 320 | 321 | def _dict_string(self): 322 | vals = [] 323 | for key, val in self._dict_items(): 324 | try: 325 | vals.append("{}={}".format(key, val)) 326 | except UnicodeDecodeError: 327 | vals.append("{}=".format(key)) 328 | return ", ".join(vals) 329 | 330 | def __repr__(self): 331 | return str(self) 332 | 333 | def __str__(self): 334 | return "{} [{}]".format(self.__class__.__name__, self._dict_string()) 335 | 336 | 337 | class Structure(Printable): 338 | _fields = {} 339 | 340 | def __init__( 341 | self, 342 | size=0, 343 | value=None, 344 | data=None, 345 | absolute_offset=0, 346 | name="", 347 | friendly_name="", 348 | *args, 349 | **kwargs 350 | ): 351 | self._value = value 352 | self.size = size 353 | self.data = data 354 | self.name = name 355 | self.friendly_name = friendly_name 356 | self._absolute_offset = absolute_offset 357 | self._file_data = None 358 | 359 | for k, v in kwargs.items(): 360 | setattr(self, k, v) 361 | 362 | @property 363 | def absolute_offset(self): 364 | return self._absolute_offset 365 | 366 | @absolute_offset.setter 367 | def absolute_offset(self, abs_offset): 368 | self._absolute_offset = abs_offset 369 | for k, v in self._fields.items(): 370 | field = getattr(self, k) 371 | field.absolute_offset = self.absolute_offset + field.offset 372 | 373 | @property 374 | def value(self): 375 | return self._value 376 | 377 | @value.setter 378 | def value(self, value): 379 | if self._file_data is not None: 380 | self.data = value_to_byte_string(value, self.size) 381 | self._file_data[ 382 | self.absolute_offset : self.absolute_offset + self.size 383 | ] = bytearray(self.data) 384 | self._value = value 385 | 386 | def process_field(self, file_data, field_name, field_info): 387 | if hasattr(self, "process_" + field_name) and callable( 388 | getattr(self, "process_" + field_name) 389 | ): 390 | getattr(self, "process_" + field_name)(file_data, field_name, field_info) 391 | else: 392 | absolute_offset = field_info["offset"] + self.absolute_offset 393 | size = field_info["size"] 394 | self.size += size 395 | int_value, data = read_bytes(file_data, absolute_offset, size) 396 | field_name_dict = read_from_name_dict(self, field_name) 397 | name = field_name_dict.get(int_value, "") 398 | friendly_name = name.replace("_", " ").capitalize() 399 | 400 | setattr( 401 | self, 402 | field_name, 403 | Structure( 404 | offset=field_info["offset"], 405 | size=size, 406 | value=int_value, 407 | data=data, 408 | absolute_offset=absolute_offset, 409 | name=name, 410 | friendly_name=friendly_name, 411 | ), 412 | ) 413 | getattr(self, field_name)._file_data = file_data 414 | 415 | def process_Characteristics(self, file_data, field_name, field_info): 416 | absolute_offset = field_info["offset"] + self.absolute_offset 417 | size = field_info["size"] 418 | self.size += size 419 | int_value, data = read_bytes(file_data, absolute_offset, size) 420 | field_name_dict = read_from_name_dict(self, field_name) 421 | 422 | bit_length = len(bin(int_value)) - 2 423 | 424 | characteristics = {} 425 | for i in range(bit_length): 426 | set_bit = test_bit(int_value, i) 427 | char_name = field_name_dict.get(set_bit, "") 428 | if set_bit != 0 and char_name: 429 | characteristics[char_name] = set_bit 430 | 431 | setattr( 432 | self, 433 | field_name, 434 | Structure( 435 | offset=field_info["offset"], 436 | size=size, 437 | value=int_value, 438 | data=data, 439 | absolute_offset=absolute_offset, 440 | values=characteristics, 441 | ), 442 | ) 443 | getattr(self, field_name)._file_data = file_data 444 | 445 | @classmethod 446 | def parse_from_data(cls, file_data, **cls_args): 447 | """Parses the Structure from the file data.""" 448 | self = cls(**cls_args) 449 | self._file_data = file_data 450 | for field_name, field_info in self._fields.items(): 451 | self.process_field(file_data, field_name, field_info) 452 | return self 453 | 454 | 455 | class DOSHeader(Structure): 456 | """The dos header of the PE file""" 457 | 458 | _fields = { 459 | "Signature": {"offset": 0, "size": 2}, 460 | "PEHeaderOffset": {"offset": 0x3C, "size": 4}, 461 | } 462 | 463 | 464 | class PEHeader(Structure): 465 | """PE signature plus the COFF header""" 466 | 467 | _fields = { 468 | "Signature": {"offset": 0, "size": 4}, 469 | "Machine": {"offset": 4, "size": 2}, 470 | "NumberOfSections": {"offset": 6, "size": 2}, 471 | "TimeDateStamp": {"offset": 8, "size": 4}, 472 | "PointerToSymbolTable": {"offset": 12, "size": 4}, 473 | "NumberOfSymbols": {"offset": 16, "size": 4}, 474 | "SizeOfOptionalHeader": {"offset": 20, "size": 2}, 475 | "Characteristics": {"offset": 22, "size": 2}, 476 | } 477 | 478 | 479 | class OptionalHeader(Structure): 480 | _fields_32_plus = { 481 | "Magic": {"offset": 0, "size": 2}, 482 | "MajorLinkerVersion": {"offset": 2, "size": 1}, 483 | "MinorLinkerVersion": {"offset": 3, "size": 1}, 484 | "SizeOfCode": {"offset": 4, "size": 4}, 485 | "SizeOfInitializedData": {"offset": 8, "size": 4}, 486 | "SizeOfUninitializedData": {"offset": 12, "size": 4}, 487 | "AddressOfEntryPoint": {"offset": 16, "size": 4}, 488 | "BaseOfCode": {"offset": 20, "size": 4}, 489 | "ImageBase": {"offset": 24, "size": 8}, 490 | "SectionAlignment": {"offset": 32, "size": 4}, 491 | "FileAlignment": {"offset": 36, "size": 4}, 492 | "MajorOperatingSystemVersion": {"offset": 40, "size": 2}, 493 | "MinorOperatingSystemVersion": {"offset": 42, "size": 2}, 494 | "MajorImageVersion": {"offset": 44, "size": 2}, 495 | "MinorImageVersion": {"offset": 46, "size": 2}, 496 | "MajorSubsystemVersion": {"offset": 48, "size": 2}, 497 | "MinorSubsystemVersion": {"offset": 50, "size": 2}, 498 | "Reserved": {"offset": 52, "size": 4}, 499 | "SizeOfImage": {"offset": 56, "size": 4}, 500 | "SizeOfHeaders": {"offset": 60, "size": 4}, 501 | "SizeOfHeaders": {"offset": 60, "size": 4}, 502 | "CheckSum": {"offset": 64, "size": 4}, 503 | "Subsystem": {"offset": 68, "size": 2}, 504 | "DLL_Characteristics": {"offset": 70, "size": 2}, 505 | "SizeOfStackReserve": {"offset": 72, "size": 8}, 506 | "SizeOfStackCommit": {"offset": 80, "size": 8}, 507 | "SizeOfHeapReserve": {"offset": 88, "size": 8}, 508 | "SizeOfHeapCommit": {"offset": 96, "size": 8}, 509 | "LoaderFlags": {"offset": 104, "size": 4}, 510 | "NumberOfRvaAndSizes": {"offset": 108, "size": 4}, 511 | "ExportTableAddress": {"offset": 112, "size": 4}, 512 | "ExportTableSize": {"offset": 116, "size": 4}, 513 | "ImportTableAddress": {"offset": 120, "size": 4}, 514 | "ImportTableSize": {"offset": 124, "size": 4}, 515 | "ResourceTableAddress": {"offset": 128, "size": 4}, 516 | "ResourceTableSize": {"offset": 132, "size": 4}, 517 | "ExceptionTableAddress": {"offset": 136, "size": 4}, 518 | "ExceptionTableSize": {"offset": 140, "size": 4}, 519 | "CertificateTableAddress": {"offset": 144, "size": 4}, 520 | "CertificateTableSize": {"offset": 148, "size": 4}, 521 | "BaseRelocationTableAddress": {"offset": 152, "size": 4}, 522 | "BaseRelocationTableSize": {"offset": 156, "size": 4}, 523 | "DebugAddress": {"offset": 160, "size": 4}, 524 | "DebugSize": {"offset": 164, "size": 4}, 525 | "ArchitectureAddress": {"offset": 168, "size": 4}, 526 | "ArchitectureSize": {"offset": 172, "size": 4}, 527 | "GlobalPtrAddress": {"offset": 176, "size": 8}, 528 | "GlobalPtrSize": {"offset": 184, "size": 0}, 529 | "ThreadLocalStorageTableAddress": {"offset": 184, "size": 4}, 530 | "ThreadLocalStorageTableSize": {"offset": 188, "size": 4}, 531 | "LoadConfigTableAddress": {"offset": 192, "size": 4}, 532 | "LoadConfigTableSize": {"offset": 196, "size": 4}, 533 | "BoundImportAddress": {"offset": 200, "size": 4}, 534 | "BoundImportSize": {"offset": 204, "size": 4}, 535 | "ImportAddressTableAddress": {"offset": 208, "size": 4}, 536 | "ImportAddressTableSize": {"offset": 212, "size": 4}, 537 | "DelayImportDescriptorAddress": {"offset": 216, "size": 4}, 538 | "DelayImportDescriptorSize": {"offset": 220, "size": 4}, 539 | "COMRuntimeHeaderAddress": {"offset": 224, "size": 4}, 540 | "COMRuntimeHeaderSize": {"offset": 228, "size": 4}, 541 | "Reserved2": {"offset": 232, "size": 8}, 542 | } 543 | 544 | _fields_32 = { 545 | "Magic": {"offset": 0, "size": 2}, 546 | "MajorLinkerVersion": {"offset": 2, "size": 1}, 547 | "MinorLinkerVersion": {"offset": 3, "size": 1}, 548 | "SizeOfCode": {"offset": 4, "size": 4}, 549 | "SizeOfInitializedData": {"offset": 8, "size": 4}, # 550 | "SizeOfUninitializedData": {"offset": 12, "size": 4}, 551 | "AddressOfEntryPoint": {"offset": 16, "size": 4}, 552 | "BaseOfCode": {"offset": 20, "size": 4}, 553 | "BaseOfData": {"offset": 24, "size": 4}, 554 | "ImageBase": {"offset": 28, "size": 4}, 555 | "SectionAlignment": {"offset": 32, "size": 4}, 556 | "FileAlignment": {"offset": 36, "size": 4}, 557 | "MajorOperatingSystemVersion": {"offset": 40, "size": 2}, 558 | "MinorOperatingSystemVersion": {"offset": 42, "size": 2}, 559 | "MajorImageVersion": {"offset": 44, "size": 2}, 560 | "MinorImageVersion": {"offset": 46, "size": 2}, 561 | "MajorSubsystemVersion": {"offset": 48, "size": 2}, 562 | "MinorSubsystemVersion": {"offset": 50, "size": 2}, 563 | "Reserved": {"offset": 52, "size": 4}, 564 | "SizeOfImage": {"offset": 56, "size": 4}, # 565 | "SizeOfHeaders": {"offset": 60, "size": 4}, 566 | "CheckSum": {"offset": 64, "size": 4}, 567 | "Subsystem": {"offset": 68, "size": 2}, 568 | "DLL_Characteristics": {"offset": 70, "size": 2}, 569 | "SizeOfStackReserve": {"offset": 72, "size": 4}, 570 | "SizeOfStackCommit": {"offset": 76, "size": 4}, 571 | "SizeOfHeapReserve": {"offset": 80, "size": 4}, 572 | "SizeOfHeapCommit": {"offset": 84, "size": 4}, 573 | "LoaderFlags": {"offset": 88, "size": 4}, 574 | "NumberOfRvaAndSizes": {"offset": 92, "size": 4}, 575 | "ExportTableAddress": {"offset": 96, "size": 4}, 576 | "ExportTableSize": {"offset": 100, "size": 4}, 577 | "ImportTableAddress": {"offset": 104, "size": 4}, 578 | "ImportTableSize": {"offset": 108, "size": 4}, 579 | "ResourceTableAddress": {"offset": 112, "size": 4}, 580 | "ResourceTableSize": {"offset": 116, "size": 4}, # 581 | "ExceptionTableAddress": {"offset": 120, "size": 4}, 582 | "ExceptionTableSize": {"offset": 124, "size": 4}, 583 | "CertificateTableAddress": {"offset": 128, "size": 4}, 584 | "CertificateTableSize": {"offset": 132, "size": 4}, 585 | "BaseRelocationTableAddress": {"offset": 136, "size": 4}, # 586 | "BaseRelocationTableSize": {"offset": 140, "size": 4}, 587 | "DebugAddress": {"offset": 144, "size": 4}, 588 | "DebugSize": {"offset": 148, "size": 4}, 589 | "ArchitectureAddress": {"offset": 152, "size": 4}, 590 | "ArchitectureSize": {"offset": 156, "size": 4}, 591 | "GlobalPtrAddress": {"offset": 160, "size": 8}, 592 | "GlobalPtrSize": {"offset": 168, "size": 0}, 593 | "ThreadLocalStorageTableAddress": {"offset": 168, "size": 4}, 594 | "ThreadLocalStorageTableSize": {"offset": 172, "size": 4}, 595 | "LoadConfigTableAddress": {"offset": 176, "size": 4}, 596 | "LoadConfigTableSize": {"offset": 180, "size": 4}, 597 | "BoundImportAddress": {"offset": 184, "size": 4}, 598 | "BoundImportSize": {"offset": 188, "size": 4}, 599 | "ImportAddressTableAddress": {"offset": 192, "size": 4}, 600 | "ImportAddressTableSize": {"offset": 196, "size": 4}, 601 | "DelayImportDescriptorAddress": {"offset": 200, "size": 4}, 602 | "DelayImportDescriptorSize": {"offset": 204, "size": 4}, 603 | "COMRuntimeHeaderAddress": {"offset": 208, "size": 4}, 604 | "COMRuntimeHeaderSize": {"offset": 212, "size": 4}, 605 | "Reserved2": {"offset": 216, "size": 8}, 606 | } 607 | 608 | def process_DLL_Characteristics(self, file_data, field_name, field_info): 609 | self.process_Characteristics(file_data, field_name, field_info) 610 | 611 | def process_field(self, file_data, field_name, field_info): 612 | if hasattr(self, "process_" + field_name) and callable( 613 | getattr(self, "process_" + field_name) 614 | ): 615 | getattr(self, "process_" + field_name)(file_data, field_name, field_info) 616 | else: 617 | absolute_offset = field_info["offset"] + self.absolute_offset 618 | size = field_info["size"] 619 | self.size += size 620 | int_value, data = read_bytes(file_data, absolute_offset, size) 621 | field_name_dict = read_from_name_dict(self, field_name) 622 | name = field_name_dict.get(int_value, "") 623 | friendly_name = name.replace("_", " ").capitalize() 624 | 625 | setattr( 626 | self, 627 | field_name, 628 | Structure( 629 | offset=field_info["offset"], 630 | size=size, 631 | value=int_value, 632 | data=data, 633 | absolute_offset=absolute_offset, 634 | name=name, 635 | friendly_name=friendly_name, 636 | ), 637 | ) 638 | getattr(self, field_name)._file_data = file_data 639 | 640 | @classmethod 641 | def parse_from_data(cls, file_data, **cls_args): 642 | """Parses the Structure from the file data.""" 643 | self = cls(**cls_args) 644 | self._file_data = file_data 645 | magic, x = read_bytes(file_data, self.absolute_offset, 2) 646 | 647 | if magic == _32BIT_MAGIC: 648 | self._fields = self._fields_32 649 | elif magic == _32BIT_PLUS_MAGIC: 650 | self._fields = self._fields_32_plus 651 | else: 652 | print(magic, _32BIT_MAGIC, _32BIT_PLUS_MAGIC) 653 | raise PEFormatError("Magic for Optional Header is invalid.") 654 | 655 | for field_name, field_info in self._fields.items(): 656 | self.process_field(file_data, field_name, field_info) 657 | 658 | return self 659 | 660 | 661 | class SectionHeader(Structure): 662 | """Section Header. Each section header is a row in the section table""" 663 | 664 | _fields = { 665 | "Name": {"offset": 0, "size": 8}, 666 | "VirtualSize": {"offset": 8, "size": 4}, # .rsrc 667 | "VirtualAddress": {"offset": 12, "size": 4}, # .reloc 668 | "SizeOfRawData": {"offset": 16, "size": 4}, # .rsrc 669 | "PointerToRawData": {"offset": 20, "size": 4}, # .reloc 670 | "PointerToRelocations": {"offset": 24, "size": 4}, 671 | "PointerToLineNumbers": {"offset": 28, "size": 4}, 672 | "NumberOfRelocations": {"offset": 32, "size": 2}, 673 | "NumberOfLineNumbers": {"offset": 34, "size": 2}, 674 | "Characteristics": {"offset": 36, "size": 4}, 675 | } 676 | 677 | 678 | class ResourceDirectoryTable(Structure): 679 | _fields = { 680 | "Characteristics": {"offset": 0, "size": 4}, 681 | "TimeDateStamp": {"offset": 4, "size": 4}, 682 | "MajorVersion": {"offset": 8, "size": 2}, 683 | "MinorVersion": {"offset": 10, "size": 2}, 684 | "NumberOfNameEntries": {"offset": 12, "size": 2}, 685 | "NumberOfIDEntries": {"offset": 14, "size": 2}, 686 | } 687 | 688 | def __init__(self, *args, **kwargs): 689 | self.name_entries = [] 690 | self.id_entries = [] 691 | self.subdirectory_tables = [] 692 | self.data_entries = [] 693 | 694 | super(ResourceDirectoryTable, self).__init__(*args, **kwargs) 695 | 696 | 697 | class ResourceDirectoryEntryName(Structure): 698 | _fields = { 699 | "NameRVA": {"offset": 0, "size": 4}, 700 | "DataOrSubdirectoryEntryRVA": { 701 | "offset": 4, # high bit 1 for subdir RVA 702 | "size": 4, 703 | }, 704 | } 705 | 706 | directory_string = None 707 | 708 | def is_data_entry(self): 709 | return test_bit(self.DataOrSubdirectoryEntryRVA.value, 31) == 0 710 | 711 | def data_rva_empty(self): 712 | return self.get_data_or_subdirectory_rva() == 0 713 | 714 | def get_data_or_subdirectory_rva(self, virtual_to_physical=0): 715 | return clear_bit( 716 | self.DataOrSubdirectoryEntryRVA.value - virtual_to_physical, 31 717 | ) 718 | 719 | def get_data_or_subdirectory_absolute_offset(self): 720 | return ( 721 | self.get_data_or_subdirectory_rva() 722 | + self._section_header.PointerToRawData.value 723 | ) 724 | 725 | def get_name_absolute_offset(self): 726 | return ( 727 | clear_bit(self.NameRVA.value, 31) 728 | + self._section_header.PointerToRawData.value 729 | ) 730 | 731 | 732 | class ResourceDirectoryEntryID(Structure): 733 | _fields = { 734 | "IntegerID": {"offset": 0, "size": 4}, 735 | "DataOrSubdirectoryEntryRVA": { 736 | "offset": 4, # high bit 1 for Subdir RVA 737 | "size": 4, 738 | }, 739 | } 740 | 741 | def is_data_entry(self): 742 | return test_bit(self.DataOrSubdirectoryEntryRVA.value, 31) == 0 743 | 744 | def data_rva_empty(self): 745 | return self.get_data_or_subdirectory_rva() == 0 746 | 747 | def get_data_or_subdirectory_rva(self, virtual_to_physical=0): 748 | return clear_bit( 749 | self.DataOrSubdirectoryEntryRVA.value - virtual_to_physical, 31 750 | ) 751 | 752 | def get_data_or_subdirectory_absolute_offset(self, vtp=0): 753 | return ( 754 | self.get_data_or_subdirectory_rva(vtp) 755 | + self._section_header.PointerToRawData.value 756 | ) 757 | 758 | 759 | class ResourceDirectoryString(Structure): 760 | _fields = { 761 | "Length": {"offset": 0, "size": 2}, 762 | # String : offset=2, len=Length 763 | } 764 | 765 | @classmethod 766 | def parse_from_data(cls, file_data, **cls_args): 767 | """Parses the Structure from the file data.""" 768 | self = cls(**cls_args) 769 | self._file_data = file_data 770 | str_len, _ = read_bytes(file_data, self.absolute_offset, 2) 771 | 772 | self._fields["String"] = {"offset": 2, "size": str_len} 773 | 774 | for field_name, field_info in self._fields.items(): 775 | self.process_field(file_data, field_name, field_info) 776 | 777 | return self 778 | 779 | def process_String(self, file_data, field_name, field_info): 780 | absolute_offset = field_info["offset"] + self.absolute_offset 781 | size = field_info["size"] 782 | self.size += size 783 | data = "" 784 | for i in range(size): 785 | val, dat = read_bytes(file_data, absolute_offset + i * 2, 2) 786 | data += str(dat, "utf-8") 787 | 788 | setattr( 789 | self, 790 | field_name, 791 | Structure( 792 | offset=field_info["offset"], 793 | size=size, 794 | data=data, 795 | absolute_offset=absolute_offset, 796 | ), 797 | ) 798 | 799 | 800 | class ResourceDataEntry(Structure): 801 | _fields = { 802 | "DataRVA": {"offset": 0, "size": 4}, 803 | "Size": {"offset": 4, "size": 4}, 804 | "Codepage": {"offset": 8, "size": 4}, 805 | "Reserved": {"offset": 12, "size": 4}, 806 | } 807 | 808 | def get_data_absolute_offset(self): 809 | return ( 810 | self._section_header.PointerToRawData.value 811 | - self._section_header.VirtualAddress.value 812 | + self.DataRVA.value 813 | ) 814 | 815 | def process_field(self, file_data, field_name, field_info): 816 | if hasattr(self, "process_" + field_name) and callable( 817 | getattr(self, "process_" + field_name) 818 | ): 819 | getattr(self, "process_" + field_name)(file_data, field_name, field_info) 820 | else: 821 | absolute_offset = field_info["offset"] + self.absolute_offset 822 | size = field_info["size"] 823 | self.size += size 824 | int_value, data = read_bytes(file_data, absolute_offset, size) 825 | field_name_dict = read_from_name_dict(self, field_name) 826 | name = field_name_dict.get(int_value, "") 827 | friendly_name = name.replace("_", " ").capitalize() 828 | 829 | setattr( 830 | self, 831 | field_name, 832 | Structure( 833 | offset=field_info["offset"], 834 | size=size, 835 | value=int_value, 836 | data=data, 837 | absolute_offset=absolute_offset, 838 | name=name, 839 | friendly_name=friendly_name, 840 | ), 841 | ) 842 | getattr(self, field_name)._file_data = file_data 843 | 844 | @classmethod 845 | def parse_from_data(cls, file_data, **cls_args): 846 | """Parses the Structure from the file data.""" 847 | self = cls(**cls_args) 848 | self._file_data = file_data 849 | for field_name, field_info in self._fields.items(): 850 | self.process_field(file_data, field_name, field_info) 851 | 852 | self.data = read_data( 853 | file_data, self.get_data_absolute_offset(), self.Size.value 854 | ) 855 | 856 | return self 857 | 858 | 859 | class ResourceHeader(Structure): 860 | _fields = { 861 | "DataSize": {"offset": 0, "size": 4}, 862 | "HeaderSize": {"offset": 4, "size": 4}, 863 | "Type": {"offset": 8, "size": 4}, 864 | "Name": {"offset": 12, "size": 4}, 865 | "DataVersion": {"offset": 16, "size": 4}, 866 | "MemoryFlags": {"offset": 20, "size": 2}, 867 | "LanguageID": {"offset": 22, "size": 2}, 868 | "Version": {"offset": 24, "size": 4}, 869 | "Characteristics": {"offset": 28, "size": 4}, 870 | } 871 | 872 | def get_name(self): 873 | return resource_types[self.Type.value] 874 | 875 | def set_name(self, value): 876 | for k, v in resource_types.items(): 877 | if v == value: 878 | self.Type.value = k 879 | return 880 | 881 | 882 | class IconHeader(Structure): 883 | _fields = { 884 | "Reserved": {"offset": 0, "size": 2}, 885 | "ImageType": {"offset": 2, "size": 2}, # 1 for ICO, 2 for CUR, others invalid 886 | "ImageCount": {"offset": 4, "size": 2}, 887 | } 888 | 889 | def copy_from(self, group_header): 890 | self.Reserved.value = group_header.Reserved.value 891 | self.ImageType.value = group_header.ResourceType.value 892 | self.ImageCount.value = group_header.ResourceCount.value 893 | 894 | self.entries = [] 895 | entry_offset = 0 896 | self.total_size = self.size 897 | for group_entry in group_header.entries: 898 | icon_entry = IconEntry.parse_from_data( 899 | bytearray(""), 900 | absolute_offset=self.absolute_offset + self.size + entry_offset, 901 | offset=entry_offset, 902 | ) 903 | icon_entry._file_data = self._file_data 904 | icon_entry.copy_from(group_entry) 905 | icon_entry.number = group_entry.number 906 | self.entries.append(icon_entry) 907 | entry_offset += icon_entry.size 908 | self.total_size += icon_entry.size 909 | 910 | @classmethod 911 | def parse_from_data(cls, file_data, **cls_args): 912 | """Parses the Structure from the file data.""" 913 | self = cls(**cls_args) 914 | self._file_data = file_data 915 | 916 | for field_name, field_info in self._fields.items(): 917 | self.process_field(file_data, field_name, field_info) 918 | 919 | self.entries = [] 920 | entry_offset = 0 921 | self.total_size = self.size 922 | for i in range(self.ImageCount.value): 923 | entry = IconEntry.parse_from_data( 924 | file_data, 925 | absolute_offset=self.absolute_offset + self.size + entry_offset, 926 | offset=entry_offset, 927 | ) 928 | entry.number = i + 1 929 | self.entries.append(entry) 930 | entry_offset += entry.size 931 | self.total_size += entry.size 932 | 933 | return self 934 | 935 | 936 | class GroupHeader(Structure): 937 | _fields = { 938 | "Reserved": {"offset": 0, "size": 2}, 939 | "ResourceType": { 940 | "offset": 2, # 1 for ICO, 2 for CUR, others invalid 941 | "size": 2, 942 | }, 943 | "ResourceCount": {"offset": 4, "size": 2}, 944 | } 945 | 946 | def copy_from(self, icon_header): 947 | self.Reserved._file_data = self._file_data 948 | self.ResourceType._file_data = self._file_data 949 | self.ResourceCount._file_data = self._file_data 950 | 951 | self.Reserved.value = icon_header.Reserved.value 952 | self.ResourceType.value = icon_header.ImageType.value 953 | self.ResourceCount.value = icon_header.ImageCount.value 954 | 955 | self.entries = [] 956 | entry_offset = 0 957 | self.total_size = self.size 958 | for icon_entry in icon_header.entries: 959 | group_entry = GroupEntry.parse_from_data( 960 | bytearray(b""), 961 | absolute_offset=self.absolute_offset + self.size + entry_offset, 962 | offset=entry_offset, 963 | ) 964 | group_entry._file_data = self._file_data 965 | group_entry.copy_from(icon_entry) 966 | group_entry.number = icon_entry.number 967 | self.entries.append(group_entry) 968 | entry_offset += group_entry.size 969 | self.total_size += group_entry.size 970 | 971 | @classmethod 972 | def parse_from_data(cls, file_data, **cls_args): 973 | """Parses the Structure from the file data.""" 974 | self = cls(**cls_args) 975 | self._file_data = file_data 976 | 977 | for field_name, field_info in self._fields.items(): 978 | self.process_field(file_data, field_name, field_info) 979 | 980 | self.entries = [] 981 | entry_offset = 0 982 | self.total_size = self.size 983 | for i in range(self.ResourceCount.value): 984 | entry = GroupEntry.parse_from_data( 985 | file_data, 986 | absolute_offset=self.absolute_offset + self.size + entry_offset, 987 | offset=entry_offset, 988 | ) 989 | entry.number = i + 1 990 | self.entries.append(entry) 991 | entry_offset += entry.size 992 | self.total_size += entry.size 993 | 994 | return self 995 | 996 | 997 | class IconEntry(Structure): 998 | _fields = { 999 | "Width": {"offset": 0, "size": 1}, 1000 | "Height": {"offset": 1, "size": 1}, 1001 | "ColorCount": {"offset": 2, "size": 1}, 1002 | "Reserved": {"offset": 3, "size": 1}, 1003 | "ColorPlanes": {"offset": 4, "size": 2}, 1004 | "BitCount": {"offset": 6, "size": 2}, # bits per pixel 1005 | "DataSize": {"offset": 8, "size": 4}, 1006 | "OffsetToData": {"offset": 12, "size": 4}, # from start of file 1007 | } 1008 | 1009 | def copy_from(self, group_entry, entries): 1010 | self.Width.value = group_entry.Width.value 1011 | self.Height.value = group_entry.Height.value 1012 | self.ColorCount.value = group_entry.ColorCount.value 1013 | self.Reserved.value = group_entry.Reserved.value 1014 | self.ColorPlanes.value = group_entry.ColorPlanes.value 1015 | self.BitCount.value = group_entry.BitCount.value 1016 | self.DataSize.value = group_entry.DataSize.value 1017 | self.OffsetToData.value = self._get_entry_offset(group_entry, entries) 1018 | 1019 | def _get_entry_offset(self, group_entry, group_entries): 1020 | offset = 6 # Default icon header size 1021 | offset += self.size * len(group_entries) 1022 | 1023 | for i in range(group_entry.number - 1): 1024 | offset += group_entries[i].DataSize.value 1025 | 1026 | return offset 1027 | 1028 | @classmethod 1029 | def parse_from_data(cls, file_data, **cls_args): 1030 | """Parses the Structure from the file data.""" 1031 | self = cls(**cls_args) 1032 | self._file_data = file_data 1033 | for field_name, field_info in self._fields.items(): 1034 | self.process_field(file_data, field_name, field_info) 1035 | 1036 | self.data = read_data(file_data, self.OffsetToData.value, self.DataSize.value) 1037 | 1038 | return self 1039 | 1040 | 1041 | class GroupEntry(Structure): 1042 | _fields = { 1043 | "Width": {"offset": 0, "size": 1}, 1044 | "Height": {"offset": 1, "size": 1}, 1045 | "ColorCount": {"offset": 2, "size": 1}, 1046 | "Reserved": {"offset": 3, "size": 1}, 1047 | "ColorPlanes": {"offset": 4, "size": 2}, 1048 | "BitCount": {"offset": 6, "size": 2}, 1049 | "DataSize": {"offset": 8, "size": 4}, 1050 | "IconCursorId": {"offset": 12, "size": 2}, 1051 | } 1052 | 1053 | def copy_from(self, icon_entry): 1054 | self.Width._file_data = self._file_data 1055 | self.Height._file_data = self._file_data 1056 | self.ColorCount._file_data = self._file_data 1057 | self.Reserved._file_data = self._file_data 1058 | self.ColorPlanes._file_data = self._file_data 1059 | self.BitCount._file_data = self._file_data 1060 | self.DataSize._file_data = self._file_data 1061 | self.IconCursorId._file_data = self._file_data 1062 | 1063 | self.Width.value = icon_entry.Width.value 1064 | self.Height.value = icon_entry.Height.value 1065 | self.ColorCount.value = icon_entry.ColorCount.value 1066 | self.Reserved.value = icon_entry.Reserved.value 1067 | self.ColorPlanes.value = icon_entry.ColorPlanes.value 1068 | self.BitCount.value = icon_entry.BitCount.value 1069 | self.DataSize.value = icon_entry.DataSize.value 1070 | self.IconCursorId.value = icon_entry.number 1071 | 1072 | 1073 | class PEFile(Printable): 1074 | """Reads a portable exe file in either big or little endian. 1075 | Right now this only reads the .rsrc section. 1076 | """ 1077 | 1078 | signature = b"MZ" 1079 | dos_header = None 1080 | 1081 | def __init__(self, file_path, endian="little"): 1082 | self.file_path = os.path.abspath(os.path.expanduser(file_path)) 1083 | 1084 | self.endian = endian 1085 | if not self.is_PEFile(): 1086 | raise PEFormatError( 1087 | "File is not a proper portable executable formatted file!" 1088 | ) 1089 | 1090 | self.pe_file_data = bytearray(open(self.file_path, "rb").read()) 1091 | 1092 | self.dos_header = DOSHeader.parse_from_data(self.pe_file_data) 1093 | self.pe_header = PEHeader.parse_from_data( 1094 | self.pe_file_data, absolute_offset=self.dos_header.PEHeaderOffset.value 1095 | ) 1096 | self.optional_header = OptionalHeader.parse_from_data( 1097 | self.pe_file_data, 1098 | absolute_offset=self.pe_header.size + self.pe_header.absolute_offset, 1099 | ) 1100 | 1101 | number_of_sections = self.pe_header.NumberOfSections.value 1102 | section_size = 40 1103 | section_offset = ( 1104 | self.pe_header.size 1105 | + self.pe_header.absolute_offset 1106 | + self.pe_header.SizeOfOptionalHeader.value 1107 | ) 1108 | self.sections = {} 1109 | 1110 | for section_number in range(number_of_sections): 1111 | section_header = SectionHeader.parse_from_data( 1112 | self.pe_file_data, absolute_offset=section_offset 1113 | ) 1114 | section_offset += section_size 1115 | header_name = str(section_header.Name.data, "utf-8").strip("\x00") 1116 | self.sections[header_name] = section_header 1117 | 1118 | if section_header.PointerToLineNumbers.value != 0: 1119 | print( 1120 | "{} section contains line number COFF table, which is not implemented yet.".format( 1121 | section_header.Name 1122 | ) 1123 | ) 1124 | 1125 | if section_header.PointerToRelocations.value != 0: 1126 | print( 1127 | "{} section contains relocation table, which is not implemented yet.".format( 1128 | section_header.Name 1129 | ) 1130 | ) 1131 | 1132 | if section_header.Name.data == b".rsrc\x00\x00\x00": 1133 | current_table_pointer = section_header.PointerToRawData.value 1134 | current_resource_directory_table = ( 1135 | ResourceDirectoryTable.parse_from_data( 1136 | self.pe_file_data, 1137 | absolute_offset=current_table_pointer, 1138 | _section_header=section_header, 1139 | type=None, 1140 | ) 1141 | ) 1142 | self.resource_directory_table = current_resource_directory_table 1143 | cur_level = 0 1144 | stack = [(current_resource_directory_table, cur_level)] 1145 | 1146 | delta = ( 1147 | section_header.VirtualAddress.value 1148 | - section_header.PointerToRawData.value 1149 | ) 1150 | 1151 | while stack: 1152 | resource_directory_table, level = stack.pop() 1153 | num_name_entries = ( 1154 | resource_directory_table.NumberOfNameEntries.value 1155 | ) 1156 | num_id_entries = resource_directory_table.NumberOfIDEntries.value 1157 | current_offset = ( 1158 | resource_directory_table.absolute_offset 1159 | + resource_directory_table.size 1160 | ) 1161 | 1162 | for i in range(num_name_entries): 1163 | name_entry = ResourceDirectoryEntryName.parse_from_data( 1164 | self.pe_file_data, 1165 | absolute_offset=current_offset, 1166 | _section_header=section_header, 1167 | ) 1168 | current_offset += name_entry.size 1169 | 1170 | string_offset = name_entry.get_name_absolute_offset() 1171 | name_entry.directory_string = ( 1172 | ResourceDirectoryString.parse_from_data( 1173 | self.pe_file_data, 1174 | absolute_offset=string_offset, 1175 | _section_header=section_header, 1176 | ) 1177 | ) 1178 | 1179 | offset = name_entry.get_data_or_subdirectory_absolute_offset() 1180 | 1181 | if not name_entry.data_rva_empty(): 1182 | if name_entry.is_data_entry(): 1183 | rd = ResourceDataEntry.parse_from_data( 1184 | self.pe_file_data, 1185 | absolute_offset=offset, 1186 | _section_header=section_header, 1187 | ) 1188 | resource_directory_table.data_entries.append(rd) 1189 | else: 1190 | rd = ResourceDirectoryTable.parse_from_data( 1191 | self.pe_file_data, 1192 | absolute_offset=offset, 1193 | _section_header=section_header, 1194 | type=None, 1195 | ) 1196 | resource_directory_table.subdirectory_tables.append(rd) 1197 | 1198 | stack.append((rd, level + 1)) 1199 | 1200 | resource_directory_table.name_entries.append(name_entry) 1201 | 1202 | for i in range(num_id_entries): 1203 | id_entry = ResourceDirectoryEntryID.parse_from_data( 1204 | self.pe_file_data, 1205 | absolute_offset=current_offset, 1206 | _section_header=section_header, 1207 | ) 1208 | current_offset += id_entry.size 1209 | 1210 | offset = id_entry.get_data_or_subdirectory_absolute_offset() 1211 | 1212 | if id_entry.is_data_entry(): 1213 | rd = ResourceDataEntry.parse_from_data( 1214 | self.pe_file_data, 1215 | absolute_offset=offset, 1216 | _section_header=section_header, 1217 | ) 1218 | resource_directory_table.data_entries.append(rd) 1219 | else: 1220 | id_entry.name = str(id_entry.IntegerID.value) 1221 | if level + 1 == 1: 1222 | id_entry.name = resource_types[id_entry.IntegerID.value] 1223 | rd = ResourceDirectoryTable.parse_from_data( 1224 | self.pe_file_data, 1225 | absolute_offset=offset, 1226 | _section_header=section_header, 1227 | type=id_entry.IntegerID.value, 1228 | ) 1229 | resource_directory_table.subdirectory_tables.append(rd) 1230 | stack.append((rd, level + 1)) 1231 | resource_directory_table.id_entries.append(id_entry) 1232 | 1233 | def replace_icon(self, icon_path): 1234 | """Replaces an icon in the pe file with the one specified. 1235 | This only replaces the largest icon and resizes the input 1236 | image to match so that the data is undisturbed. I tried to 1237 | update the pointers automatically by moving the data to the end 1238 | of the file, but that did not work. Comments were left as history 1239 | to what I attempted. 1240 | """ 1241 | icon_path = os.path.expanduser( 1242 | icon_path 1243 | ) # this needs to be a string and not unicode 1244 | 1245 | if not os.path.exists(icon_path): 1246 | raise Exception("Icon {} does not exist".format(icon_path)) 1247 | 1248 | resource_section = self.sections[".rsrc"] 1249 | 1250 | g_icon_dir = self.get_directory_by_type(ResourceTypes.Group_Icon) 1251 | g_icon_data_entry = g_icon_dir.subdirectory_tables[0].data_entries[0] 1252 | icon_dir = self.get_directory_by_type(ResourceTypes.Icon) 1253 | icon_data_entry = icon_dir.subdirectory_tables[0].data_entries[0] 1254 | 1255 | group_header = GroupHeader.parse_from_data( 1256 | self.pe_file_data, 1257 | absolute_offset=g_icon_data_entry.get_data_absolute_offset(), 1258 | ) 1259 | g_entry = group_header.entries[0] 1260 | 1261 | icon = Image.open(icon_path) 1262 | width = g_entry.Width.value 1263 | height = g_entry.Height.value 1264 | 1265 | if width == 0: 1266 | width = 256 1267 | if height == 0: 1268 | height = 256 1269 | 1270 | i_data = resize(icon, (width, height), format="ico") 1271 | 1272 | new_icon_size = len(i_data) 1273 | icon_file_size = g_entry.DataSize.value + group_header.size + g_entry.size + 2 1274 | 1275 | # 9662 is the exact length of the icon in nw.exe 1276 | extra_size = icon_file_size - new_icon_size 1277 | 1278 | if extra_size < 0: 1279 | extra_size = 0 1280 | icon_data = bytearray(i_data) + bytearray(extra_size) 1281 | 1282 | icon_header = IconHeader.parse_from_data(icon_data, absolute_offset=0) 1283 | 1284 | # group_header.absolute_offset = len(self.pe_file_data) 1285 | # g_icon_data_entry.DataRVA.value = len(self.pe_file_data) - resource_section.PointerToRawData.value + resource_section.VirtualAddress.value 1286 | # padding = 6+14*len(icon_header.entries) 1287 | # g_icon_data_entry.Size.value = padding 1288 | 1289 | # self.pe_file_data += bytearray(padding) 1290 | group_header._file_data = self.pe_file_data 1291 | group_header.copy_from(icon_header) 1292 | 1293 | # icon_data_entry.DataRVA.value = len(self.pe_file_data) - resource_section.PointerToRawData.value + resource_section.VirtualAddress.value 1294 | # print hex(icon_data_entry.DataRVA.value), hex(len(self.pe_file_data)), hex(icon_data_entry.get_data_absolute_offset()) 1295 | 1296 | # print hex(read_bytes(self.pe_file_data[icon_data_entry.DataRVA.absolute_offset:icon_data_entry.DataRVA.absolute_offset+icon_data_entry.DataRVA.size],0, icon_data_entry.DataRVA.size)[0]) 1297 | 1298 | # data = bytearray() 1299 | # for entry in icon_header.entries: 1300 | # data += entry.data 1301 | # self.pe_file_data += entry.data 1302 | 1303 | # icon_data_entry.Size.value = len(data) 1304 | 1305 | # self.optional_header.SizeOfImage.value = self.optional_header.SizeOfImage.value + len(data) + padding 1306 | # self.optional_header.ResourceTableSize.value = self.optional_header.ResourceTableSize.value + len(data) + padding 1307 | # self.optional_header.SizeOfInitializedData.value = self.optional_header.SizeOfInitializedData.value + len(data) + padding 1308 | # resource_section.SizeOfRawData.value = resource_section.SizeOfRawData.value + len(data) + padding 1309 | # resource_section.VirtualSize.value = resource_section.VirtualSize.value + len(data) + padding 1310 | # print icon_header.total_size 1311 | data_address = icon_data_entry.get_data_absolute_offset() 1312 | data_size = icon_data_entry.Size.value 1313 | self.pe_file_data = ( 1314 | self.pe_file_data[:data_address] 1315 | + icon_data[icon_header.total_size :] 1316 | + self.pe_file_data[data_address + data_size :] 1317 | ) 1318 | 1319 | def write(self, file_name): 1320 | with open(file_name, "wb+") as f: 1321 | f.write(self.pe_file_data) 1322 | 1323 | def get_directory_by_type(self, type): 1324 | """Gets the directory by resource type.""" 1325 | for d in self.resource_directory_table.subdirectory_tables: 1326 | if d.type == type: 1327 | return d 1328 | 1329 | def is_PEFile(self): 1330 | """Checks if the file is a proper PE file""" 1331 | signature = None 1332 | try: 1333 | with open(self.file_path, "rb") as f: 1334 | signature = f.read(2) 1335 | except IOError as e: 1336 | raise e 1337 | finally: 1338 | return signature == self.signature 1339 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | black==24.8.0 3 | configobj==5.0.9 4 | pillow==10.4.0 5 | pyinstaller==6.10.0 6 | pylint==3.3.0 7 | pyside6==6.7.2 8 | pytest==8.3.3 9 | requests==2.32.3 10 | semantic_version==2.10.0 11 | validators==0.34.0 12 | -------------------------------------------------------------------------------- /scripts/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleIconFile 5 | icon-windowed.icns 6 | CFBundlePackageType 7 | APPL 8 | CFBundleIdentifier 9 | Web2Executable 10 | CFBundleName 11 | Web2Executable 12 | CFBundleShortVersionString 13 | 0.6.0 14 | CFBundleExecutable 15 | MacOS/Web2Executable 16 | CFBundleDisplayName 17 | Web2Executable 18 | CFBundleInfoDictionaryVersion 19 | 6.0 20 | LSBackgroundOnly 21 | 0 22 | NSPrincipalClass 23 | NSApplication 24 | 25 | 26 | -------------------------------------------------------------------------------- /scripts/Web2Exe.nsi: -------------------------------------------------------------------------------- 1 | ; Script generated by the HM NIS Edit Script Wizard. 2 | 3 | ; HM NIS Edit Wizard helper defines 4 | !define PRODUCT_NAME "Web2Exe" 5 | !define /file PRODUCT_VERSION "..\files\version.txt" 6 | !define PRODUCT_PUBLISHER "SimplyPixelated" 7 | !define PRODUCT_WEB_SITE "http://www.simplypixelated.com" 8 | !define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\${PRODUCT_NAME}.exe" 9 | !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" 10 | !define PRODUCT_UNINST_ROOT_KEY "HKLM" 11 | !define PRODUCT_STARTMENU_REGVAL "NSIS:StartMenuDir" 12 | 13 | SetCompressor lzma 14 | 15 | ; MUI 1.67 compatible ------ 16 | !include "MUI.nsh" 17 | 18 | ; MUI Settings 19 | !define MUI_ABORTWARNING 20 | !define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\modern-install.ico" 21 | !define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico" 22 | 23 | ; Welcome page 24 | !insertmacro MUI_PAGE_WELCOME 25 | ; License page 26 | !insertmacro MUI_PAGE_LICENSE "..\license.rtf" 27 | ; Components page 28 | !insertmacro MUI_PAGE_COMPONENTS 29 | ; Directory page 30 | !insertmacro MUI_PAGE_DIRECTORY 31 | ; Start menu page 32 | var ICONS_GROUP 33 | !define MUI_STARTMENUPAGE_NODISABLE 34 | !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${PRODUCT_NAME}" 35 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}" 36 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "${PRODUCT_UNINST_KEY}" 37 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${PRODUCT_STARTMENU_REGVAL}" 38 | !insertmacro MUI_PAGE_STARTMENU Application $ICONS_GROUP 39 | ; Instfiles page 40 | !insertmacro MUI_PAGE_INSTFILES 41 | ; Finish page 42 | !insertmacro MUI_PAGE_FINISH 43 | 44 | ; Uninstaller pages 45 | !insertmacro MUI_UNPAGE_INSTFILES 46 | 47 | ; Language files 48 | !insertmacro MUI_LANGUAGE "English" 49 | 50 | ; Reserve files 51 | !insertmacro MUI_RESERVEFILE_INSTALLOPTIONS 52 | 53 | ; MUI end ------ 54 | 55 | Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" 56 | OutFile "..\Web2Exe-Setup.exe" 57 | 58 | RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on) 59 | 60 | !include LogicLib.nsh 61 | 62 | Function .onInit 63 | UserInfo::GetAccountType 64 | pop $0 65 | ${If} $0 != "admin" ;Require admin rights on NT4+ 66 | MessageBox mb_iconstop "Administrator rights required!" 67 | SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED 68 | Quit 69 | ${EndIf} 70 | FunctionEnd 71 | 72 | InstallDir "$PROGRAMFILES\${PRODUCT_NAME}" 73 | InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" "" 74 | ShowInstDetails show 75 | ShowUnInstDetails show 76 | 77 | Section "Web2Executable" SEC01 78 | SectionIn RO 79 | SetOutPath "$INSTDIR" 80 | SetOverwrite try 81 | File /r /x compressors "..\Web2ExeWin\" 82 | 83 | CreateDirectory "$LocalAppData\Web2Executable\Web2Executable\files\compressors" 84 | SetOutPath "$LocalAppData\Web2Executable\Web2Executable\files\compressors" 85 | File "..\Web2ExeWin\files\compressors\upx-win.exe" 86 | 87 | ; Shortcuts 88 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 89 | CreateDirectory "$SMPROGRAMS\$ICONS_GROUP" 90 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" 91 | CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" 92 | !insertmacro MUI_STARTMENU_WRITE_END 93 | SectionEnd 94 | 95 | LangString DESC_SEC01 ${LANG_ENGLISH} "A tool to convert web applications to exe's." 96 | 97 | !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN 98 | !insertmacro MUI_DESCRIPTION_TEXT ${SEC01} $(DESC_SEC01) 99 | !insertmacro MUI_FUNCTION_DESCRIPTION_END 100 | 101 | Section -AdditionalIcons 102 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 103 | WriteIniStr "$INSTDIR\${PRODUCT_NAME}.url" "InternetShortcut" "URL" "${PRODUCT_WEB_SITE}" 104 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Website.lnk" "$INSTDIR\${PRODUCT_NAME}.url" 105 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Uninstall.lnk" "$INSTDIR\uninst.exe" 106 | !insertmacro MUI_STARTMENU_WRITE_END 107 | SectionEnd 108 | 109 | Section -Post 110 | WriteUninstaller "$INSTDIR\uninst.exe" 111 | WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\${PRODUCT_NAME}.exe" 112 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" 113 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe" 114 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe" 115 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" 116 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" 117 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" 118 | SectionEnd 119 | 120 | 121 | Function un.onUninstSuccess 122 | HideWindow 123 | MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer." 124 | FunctionEnd 125 | 126 | Function un.onInit 127 | MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Are you sure you want to completely remove $(^Name) and all of its components?" IDYES +2 128 | Abort 129 | FunctionEnd 130 | 131 | Section Uninstall 132 | RMDir /r "$INSTDIR" 133 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" 134 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\Website.lnk" 135 | Delete "$DESKTOP\${PRODUCT_NAME}.lnk" 136 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" 137 | 138 | DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" 139 | DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}" 140 | SetAutoClose true 141 | SectionEnd 142 | -------------------------------------------------------------------------------- /scripts/build_command_line_linux.bash: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | 3 | README="" 4 | PROJ_DIR=$DIR 5 | 6 | while [[ -z $README ]] && [[ $PROJ_DIR != "/" ]]; do 7 | README=$(find $PROJ_DIR -maxdepth 1 -name "README.md") 8 | 9 | if [[ -z $README ]]; then 10 | PROJ_DIR="$(dirname $PROJ_DIR)" 11 | fi 12 | done 13 | 14 | if [[ $PROJ_DIR == "/" ]]; then 15 | echo "No suitable project directory was found. Exiting." 16 | exit 1 17 | fi 18 | 19 | BUILD_DIR="$PROJ_DIR/Web2ExeBuild" 20 | 21 | ## Remove old build directories 22 | rm -rf $PROJ_DIR/build $BUILD_DIR 23 | rm -rf $PROJ_DIR/Web2ExeLinux-* 24 | 25 | VERSION=$(cat $PROJ_DIR/files/version.txt) 26 | 27 | ################# Build CMD Version ################### 28 | 29 | pyinstaller --onefile --exclude-module PyQt5 --exclude-module PyQt4 \ 30 | --hidden-import PIL.Jpeg2KImagePlugin \ 31 | --hidden-import pkg_resources \ 32 | --hidden-import PIL._imaging \ 33 | --hidden-import configobj \ 34 | --distpath $BUILD_DIR/Web2ExeLinux-CMD \ 35 | -n web2exe-linux $PROJ_DIR/command_line.py 36 | 37 | cp -rf $PROJ_DIR/files $BUILD_DIR/Web2ExeLinux-CMD/files 38 | 39 | ## Remove any unneeded files 40 | rm -rf $BUILD_DIR/Web2ExeLinux-CMD/files/downloads/* 41 | rm $BUILD_DIR/Web2ExeLinux-CMD/files/error.log \ 42 | $BUILD_DIR/Web2ExeLinux-CMD/files/last_project_path.txt \ 43 | $BUILD_DIR/Web2ExeLinux-CMD/files/recent_files.txt \ 44 | $BUILD_DIR/Web2ExeLinux-CMD/files/compressors/upx-mac \ 45 | $BUILD_DIR/Web2ExeLinux-CMD/files/compressors/upx-win.exe 46 | 47 | ################# Build GUI Version ################### 48 | 49 | pyinstaller -F --exclude-module PyQt5 --exclude-module PyQt4 \ 50 | --hidden-import PIL.Jpeg2KImagePlugin \ 51 | --hidden-import configobj \ 52 | --hidden-import PIL._imaging \ 53 | --hidden-import pkg_resources \ 54 | -n web2exe --distpath $BUILD_DIR/Web2ExeLinux $PROJ_DIR/main.py 55 | 56 | ## Copy the files directory over 57 | cp -rf $PROJ_DIR/files $BUILD_DIR/Web2ExeLinux/files 58 | 59 | ## Remove any unneeded files 60 | rm -rf $BUILD_DIR/Web2ExeLinux/files/downloads/* 61 | rm $BUILD_DIR/Web2ExeLinux/files/error.log \ 62 | $BUILD_DIR/Web2ExeLinux/files/last_project_path.txt \ 63 | $BUILD_DIR/Web2ExeLinux/files/recent_files.txt \ 64 | $BUILD_DIR/Web2ExeLinux/files/compressors/upx-mac \ 65 | $BUILD_DIR/Web2ExeLinux/files/compressors/upx-win.exe 66 | 67 | 68 | ################# Zip and Upload to Github ################### 69 | 70 | cd $BUILD_DIR 71 | 72 | zip -r -9 $PROJ_DIR/Web2ExeLinux-CMD.zip Web2ExeLinux-CMD/* 73 | zip -r -9 $PROJ_DIR/Web2ExeLinux-${VERSION}.zip Web2ExeLinux 74 | 75 | cd - 76 | 77 | python3.4 $DIR/upload_release.py 78 | -------------------------------------------------------------------------------- /scripts/build_mac.bash: -------------------------------------------------------------------------------- 1 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 2 | 3 | README="" 4 | PROJ_DIR=$DIR 5 | 6 | while [[ -z $README ]] && [[ $PROJ_DIR != "/" ]]; do 7 | README=$(find $PROJ_DIR -maxdepth 1 -name "README.md") 8 | 9 | if [[ -z $README ]]; then 10 | PROJ_DIR="$(dirname $PROJ_DIR)" 11 | fi 12 | done 13 | 14 | if [[ $PROJ_DIR == "/" ]]; then 15 | echo "No suitable project directory was found. Exiting." 16 | exit 1 17 | fi 18 | 19 | BUILD_DIR="$PROJ_DIR/Web2ExeBuild" 20 | 21 | ## Remove old build directories 22 | rm -rf $PROJ_DIR/build $BUILD_DIR 23 | 24 | VERSION=$(cat $PROJ_DIR/files/version.txt) 25 | 26 | ################# Build CMD Version ################### 27 | 28 | pyinstaller --hidden-import PIL.Jpeg2KImagePlugin \ 29 | --hidden-import configobj \ 30 | --hidden-import pkg_resources \ 31 | --distpath $BUILD_DIR/Web2ExeMac-CMD \ 32 | --onefile -n web2exe-mac $PROJ_DIR/command_line.py 33 | 34 | CMD_FILES_DIR=$BUILD_DIR/Web2ExeMac-CMD/files 35 | 36 | cp -rf $PROJ_DIR/files $CMD_FILES_DIR 37 | 38 | rm -rf $CMD_FILES_DIR/downloads/* 39 | rm $CMD_FILES_DIR/error.log \ 40 | $CMD_FILES_DIR/last_project_path.txt \ 41 | $CMD_FILES_DIR/recent_files.txt \ 42 | $CMD_FILES_DIR/compressors/upx-linux-x64 \ 43 | $CMD_FILES_DIR/compressors/upx-linux-x32 \ 44 | $CMD_FILES_DIR/compressors/upx-win.exe 45 | 46 | rm -rf $PROJ_DIR/build $PROJ_DIR/dist 47 | 48 | ################# Build GUI Version ################### 49 | 50 | pyinstaller -w --hidden-import PIL.Jpeg2KImagePlugin \ 51 | --hidden-import PyQt4 \ 52 | --hidden-import PIL \ 53 | --hidden-import configobj \ 54 | --hidden-import pkg_resources \ 55 | --distpath $BUILD_DIR/ \ 56 | --onefile -n Web2Executable $PROJ_DIR/main.py 57 | 58 | FILES_DIR=$BUILD_DIR/Web2Executable.app/Contents/MacOS/files 59 | 60 | cp $PROJ_DIR/images/icon.icns $BUILD_DIR/Web2Executable.app/Contents/Resources/icon-windowed.icns 61 | cp -rf $PROJ_DIR/files $FILES_DIR 62 | 63 | rm -rf $FILES_DIR/downloads/* 64 | rm $FILES_DIR/error.log \ 65 | $FILES_DIR/last_project_path.txt \ 66 | $FILES_DIR/recent_files.txt \ 67 | $FILES_DIR/compressors/upx-linux-x64 \ 68 | $FILES_DIR/compressors/upx-linux-x32 \ 69 | $FILES_DIR/compressors/upx-win.exe 70 | 71 | rm -rf $PROJ_DIR/build $PROJ_DIR/dist 72 | 73 | ################# Zip and Upload to Github ################### 74 | 75 | rm -rf Web2ExeMac*.zip 76 | 77 | cp $PROJ_DIR/scripts/Info.plist $BUILD_DIR/Web2Executable.app/Contents/ 78 | 79 | /Applications/Keka.app/Contents/Resources/keka7z a -r \ 80 | Web2ExeMac-CMD.zip $BUILD_DIR/Web2ExeMac-CMD 81 | 82 | /Applications/Keka.app/Contents/Resources/keka7z a -r \ 83 | Web2ExeMac-${VERSION}.zip $BUILD_DIR/Web2Executable.app 84 | 85 | python3 $DIR/upload_release.py 86 | -------------------------------------------------------------------------------- /scripts/build_windows.bat: -------------------------------------------------------------------------------- 1 | rd /S /Q Web2ExeWin 2 | rd /S /Q build 3 | del *.zip 4 | call pyinstaller --onefile ^ 5 | --hidden-import PIL.Jpeg2KImagePlugin ^ 6 | --hidden-import configobj ^ 7 | --hidden-import pkg_resources ^ 8 | -i images\icon.ico ^ 9 | --distpath Web2ExeWin-CMD ^ 10 | -n web2exe-win command_line.py 11 | 12 | rd /S /Q Web2ExeWin-CMD\files 13 | echo D | xcopy /s files Web2ExeWin-CMD\files 14 | 15 | 16 | call pyinstaller -w --onefile ^ 17 | --hidden-import PIL.Jpeg2KImagePlugin ^ 18 | --hidden-import pkg_resources ^ 19 | --hidden-import configobj ^ 20 | -i images\icon.ico ^ 21 | --distpath Web2ExeWin -n Web2Exe main.py 22 | 23 | echo D | xcopy /s files Web2ExeWin\files 24 | 25 | del Web2ExeWin\files\compressors\upx-mac 26 | del Web2ExeWin\files\compressors\upx-linux-x64 27 | del Web2ExeWin\files\compressors\upx-linux-x32 28 | 29 | del Web2ExeWin-CMD\files\compressors\upx-mac 30 | del Web2ExeWin-CMD\files\compressors\upx-linux-x64 31 | del Web2ExeWin-CMD\files\compressors\upx-linux-x32 32 | 33 | makensis /V4 scripts/Web2Exe.nsi 34 | 35 | set /p Version= 0 102 | 103 | 104 | def test_download_nwjs(command_base): 105 | command_base.get_setting("nw_version").value = "0.19.0" 106 | command_base.get_setting("windows-x64").value = True 107 | command_base.init() 108 | command_base.get_files_to_download() 109 | 110 | command_base.download_file_with_error_handling() 111 | 112 | base, _ = os.path.split(__file__) 113 | 114 | assert os.path.exists( 115 | utils.path_join( 116 | base, "test_data", "files", "downloads", "nwjs-v0.19.0-win-x64.zip" 117 | ) 118 | ) 119 | -------------------------------------------------------------------------------- /util_classes.py: -------------------------------------------------------------------------------- 1 | """Utility classes that are used both in the GUI and the CMD.""" 2 | 3 | import os 4 | import re 5 | from fnmatch import fnmatch 6 | import zipfile 7 | import tarfile 8 | import time 9 | import logging 10 | from pprint import pformat 11 | 12 | import config 13 | import utils 14 | 15 | from PySide6 import QtGui, QtCore 16 | from PySide6 import QtWidgets 17 | from PySide6.QtCore import Qt 18 | 19 | 20 | class FileItem(QtWidgets.QTreeWidgetItem): 21 | def __init__(self, parent=None, path=None): 22 | super(FileItem, self).__init__(parent) 23 | self.path = path 24 | 25 | 26 | class FileTree(object): 27 | def __init__(self, directory=None, whitelist=None, blacklist=None): 28 | self.whitelist = None 29 | self.blacklist = None 30 | 31 | self.paths = [] 32 | self.walkcache = {} 33 | self.cache = True 34 | self.time = time.time() 35 | 36 | self.files = [] 37 | self.dirs = [] 38 | 39 | self.init(directory, whitelist, blacklist) 40 | 41 | def init(self, directory=None, whitelist=None, blacklist=None): 42 | self.logger = config.getLogger(__name__) 43 | 44 | if directory: 45 | self.directory = directory + os.sep 46 | else: 47 | self.directory = directory 48 | 49 | self.refresh(whitelist, blacklist) 50 | 51 | def clear(self): 52 | self.files = [] 53 | self.dirs = [] 54 | 55 | def refresh(self, whitelist=None, blacklist=None): 56 | self.set_filters(whitelist, blacklist) 57 | 58 | self.clear() 59 | 60 | self.generate_files() 61 | 62 | def walk(self, directory): 63 | refresh = False 64 | 65 | if (time.time() - self.time) > 10: 66 | refresh = True 67 | self.time = time.time() 68 | 69 | if not self.walkcache.get(directory) or refresh: 70 | self.walkcache[directory] = [] 71 | return os.walk(directory) 72 | 73 | return self.walkcache[directory] 74 | 75 | def determine_skip(self, path, *args, **kwargs): 76 | skip = False 77 | 78 | for blacklist in self.blacklist: 79 | match = fnmatch(path, blacklist) 80 | if match: 81 | skip = True 82 | self.on_blacklist_match(path, *args, **kwargs) 83 | break 84 | 85 | for whitelist in self.whitelist: 86 | match = fnmatch(path, whitelist) 87 | if match: 88 | skip = False 89 | self.on_whitelist_match(path, *args, **kwargs) 90 | break 91 | 92 | return skip 93 | 94 | def set_filters(self, whitelist=None, blacklist=None): 95 | self.whitelist = whitelist or self.whitelist or [] 96 | self.blacklist = blacklist or self.blacklist or [] 97 | 98 | def on_whitelist_match(self, path, *args, **kwargs): 99 | pass 100 | 101 | def on_blacklist_match(self, path, *args, **kwargs): 102 | pass 103 | 104 | def init_cache(self): 105 | if self.walkcache.get(self.directory) is None: 106 | self.walkcache[self.directory] = [] 107 | 108 | self.cache = False 109 | if not self.walkcache[self.directory]: 110 | self.cache = True 111 | 112 | def add_to_cache(self, *args): 113 | if self.cache: 114 | self.walkcache[self.directory].append(args) 115 | 116 | def is_in_skipped(self, skipped, path): 117 | temp = path 118 | 119 | while temp: 120 | if temp in skipped: 121 | return True 122 | if temp == os.path.dirname(temp): 123 | return False 124 | temp = os.path.dirname(temp) 125 | return False 126 | 127 | def generate_files(self): 128 | if self.directory is None: 129 | return 130 | 131 | self.logger.debug("Blacklist pattern:") 132 | self.logger.debug(pformat(self.blacklist)) 133 | self.logger.debug("") 134 | self.logger.debug("Whitelist pattern:") 135 | self.logger.debug(pformat(self.whitelist)) 136 | self.logger.debug("") 137 | 138 | self.init_cache() 139 | 140 | skipped_files = set() 141 | 142 | for root, dirs, files in self.walk(self.directory): 143 | self.add_to_cache(root, dirs, files) 144 | 145 | proj_path = root.replace(self.directory, "") 146 | 147 | for directory in dirs: 148 | path = os.path.join(proj_path, directory) 149 | 150 | if self.determine_skip(path): 151 | if not self.is_in_skipped(skipped_files, path): 152 | self.logger.debug("Skipping dir: %s", path) 153 | skipped_files.add(path) 154 | continue 155 | else: 156 | if self.is_in_skipped(skipped_files, path): 157 | self.logger.debug("Keeping dir: %s", path) 158 | 159 | self.dirs.append(path) 160 | 161 | for file in files: 162 | path = os.path.join(proj_path, file) 163 | 164 | if self.determine_skip(path): 165 | if not self.is_in_skipped(skipped_files, path): 166 | self.logger.debug("Skipping file: %s", path) 167 | skipped_files.add(path) 168 | continue 169 | else: 170 | if self.is_in_skipped(skipped_files, path): 171 | self.logger.debug("Keeping file: %s", path) 172 | 173 | self.files.append(path) 174 | 175 | 176 | class TreeBrowser(QtWidgets.QWidget): 177 | def __init__(self, directory=None, whitelist=None, blacklist=None, parent=None): 178 | QtWidgets.QWidget.__init__(self, parent=parent) 179 | self.root = QtWidgets.QTreeWidget() 180 | self.root.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) 181 | self.root.header().setStretchLastSection(False) 182 | self.root.setHeaderLabel("Included files") 183 | 184 | self.parent_map = {} 185 | 186 | layout = QtWidgets.QVBoxLayout() 187 | layout.setContentsMargins(0, 0, 0, 0) 188 | layout.addWidget(self.root) 189 | self.setLayout(layout) 190 | 191 | self.file_tree = FileTree(directory, whitelist, blacklist) 192 | self.init(directory, whitelist, blacklist) 193 | 194 | def clear(self): 195 | self.root.clear() 196 | self.file_tree.clear() 197 | 198 | def init(self, directory=None, whitelist=None, blacklist=None): 199 | self.file_tree.init(directory, whitelist, blacklist) 200 | 201 | self.refresh(whitelist, blacklist) 202 | 203 | def refresh(self, whitelist=None, blacklist=None): 204 | self.file_tree.set_filters(whitelist, blacklist) 205 | self.clear() 206 | self.generate_files() 207 | 208 | def fix_tree(self, path, parent): 209 | temp = parent 210 | tree = [] 211 | root = None 212 | base, direc = os.path.split(path) 213 | new_path = os.path.join(base, direc) 214 | 215 | while new_path and root is None: 216 | temp = self.parent_map.get(os.path.join(base, direc)) 217 | if temp is not None: 218 | root = temp 219 | break 220 | 221 | child = FileItem(None, os.path.join(base, direc)) 222 | child.setText(0, direc) 223 | 224 | tree.insert(0, child) 225 | base, direc = os.path.split(base) 226 | new_path = os.path.join(base, direc) 227 | 228 | self.parent_map[child.path] = child 229 | 230 | # if we reached the top of the directory chain 231 | if root is None: 232 | # if the path is still valid, that means we need to create 233 | # a new top level item 234 | if new_path: 235 | root = FileItem(None, new_path) 236 | root.setText(0, direc) 237 | self.parent_map[root.path] = root 238 | else: 239 | # otherwise, the if the path is empty, we already 240 | # have a top level item, so use that 241 | root = tree.pop(0) 242 | 243 | self.root.addTopLevelItem(root) 244 | 245 | # add all the children to the root node 246 | temp = root 247 | for child in tree: 248 | temp.addChild(child) 249 | temp = child 250 | 251 | def on_whitelist_match(self, path, parent=None): 252 | if parent is None: 253 | self.fix_tree(path, parent) 254 | 255 | def generate_files(self): 256 | directory = self.file_tree.directory 257 | 258 | if directory is None: 259 | return 260 | 261 | self.parent_map = {"": self.root} 262 | 263 | self.file_tree.init_cache() 264 | 265 | for root, dirs, files in self.file_tree.walk(directory): 266 | self.file_tree.add_to_cache(root, dirs, files) 267 | 268 | proj_path = root.replace(directory, "") 269 | 270 | for dir in dirs: 271 | parent = self.parent_map.get(proj_path) 272 | 273 | path = os.path.join(proj_path, dir) 274 | 275 | if self.file_tree.determine_skip(path, parent=parent) or parent is None: 276 | continue 277 | 278 | child = FileItem(parent, path) 279 | child.setText(0, dir) 280 | self.parent_map[path] = child 281 | self.file_tree.dirs.append(path) 282 | 283 | for file in files: 284 | parent = self.parent_map.get(proj_path) 285 | 286 | path = os.path.join(proj_path, file) 287 | 288 | if self.file_tree.determine_skip(path, parent=parent) or parent is None: 289 | continue 290 | 291 | child = FileItem(parent, path) 292 | child.setText(0, file) 293 | self.file_tree.files.append(path) 294 | 295 | self.root.sortItems(0, Qt.AscendingOrder) 296 | 297 | 298 | class ExistingProjectDialog(QtWidgets.QDialog): 299 | def __init__(self, recent_projects, directory_callback, parent=None): 300 | super(ExistingProjectDialog, self).__init__(parent) 301 | self.setWindowTitle("Open Project Folder") 302 | self.setWindowIcon(QtGui.QIcon(config.get_file("files/images/icon.png"))) 303 | self.setMinimumWidth(500) 304 | 305 | group_box = QtWidgets.QGroupBox("Existing Projects") 306 | gbox_layout = QtWidgets.QVBoxLayout() 307 | self.project_list = QtWidgets.QListWidget() 308 | 309 | gbox_layout.addWidget(self.project_list) 310 | group_box.setLayout(gbox_layout) 311 | 312 | self.callback = directory_callback 313 | 314 | self.projects = recent_projects 315 | 316 | for project in recent_projects: 317 | text = "{} - {}".format(os.path.basename(project), project) 318 | self.project_list.addItem(text) 319 | 320 | self.project_list.itemClicked.connect(self.project_clicked) 321 | 322 | self.cancel = QtWidgets.QPushButton("Cancel") 323 | self.open = QtWidgets.QPushButton("Open Selected") 324 | self.open_readonly = QtWidgets.QPushButton("Open Read-only") 325 | self.browse = QtWidgets.QPushButton("Browse...") 326 | 327 | self.open.setEnabled(False) 328 | self.open.clicked.connect(self.open_clicked) 329 | 330 | self.open_readonly.setEnabled(False) 331 | self.open_readonly.clicked.connect(self.open_readonly_clicked) 332 | 333 | self.browse.clicked.connect(self.browse_clicked) 334 | 335 | buttons = QtWidgets.QWidget() 336 | 337 | button_layout = QtWidgets.QHBoxLayout() 338 | button_layout.addWidget(self.cancel) 339 | button_layout.addWidget(QtWidgets.QWidget()) 340 | button_layout.addWidget(self.browse) 341 | button_layout.addWidget(self.open_readonly) 342 | button_layout.addWidget(self.open) 343 | 344 | buttons.setLayout(button_layout) 345 | 346 | layout = QtWidgets.QVBoxLayout() 347 | layout.addWidget(group_box) 348 | layout.addWidget(buttons) 349 | 350 | self.setLayout(layout) 351 | self.cancel.clicked.connect(self.cancelled) 352 | 353 | def browse_clicked(self): 354 | default = self.parent().project_dir() or self.parent().last_project_dir 355 | 356 | directory = QtWidgets.QFileDialog.getExistingDirectory( 357 | self, "Find Project Directory", default 358 | ) 359 | 360 | if directory: 361 | self.callback(directory) 362 | self.close() 363 | 364 | def open_clicked(self): 365 | pos = self.project_list.currentRow() 366 | self.callback(self.projects[pos]) 367 | self.close() 368 | 369 | def open_readonly_clicked(self): 370 | pos = self.project_list.currentRow() 371 | self.callback(self.projects[pos], readonly=True) 372 | self.close() 373 | 374 | def project_clicked(self, _): 375 | self.open.setEnabled(True) 376 | self.open_readonly.setEnabled(True) 377 | 378 | def cancelled(self): 379 | self.close() 380 | 381 | 382 | class Validator(QtGui.QRegularExpressionValidator): 383 | def __init__(self, regex, action, parent=None): 384 | self.exp = regex 385 | self.action = str 386 | if hasattr(str, action): 387 | self.action = getattr(str, action) 388 | reg = QtCore.QRegularExpression(regex) 389 | super(Validator, self).__init__(reg, parent) 390 | 391 | def validate(self, text, pos): 392 | result = super(Validator, self).validate(text, pos) 393 | return result 394 | 395 | def fixup(self, text): 396 | return "".join(re.findall(self.exp, self.action(text))) 397 | 398 | 399 | class BackgroundThread(QtCore.QThread): 400 | def __init__(self, widget, method_name, parent=None): 401 | QtCore.QThread.__init__(self, parent) 402 | self.widget = widget 403 | self.method_name = method_name 404 | 405 | def run(self): 406 | if hasattr(self.widget, self.method_name): 407 | func = getattr(self.widget, self.method_name) 408 | func() 409 | 410 | 411 | class Setting(object): 412 | """Class that describes a setting from the setting.cfg file""" 413 | 414 | def __init__( 415 | self, 416 | name="", 417 | display_name=None, 418 | value=None, 419 | required=False, 420 | type=None, 421 | file_types=None, 422 | *args, 423 | **kwargs 424 | ): 425 | self.name = name 426 | self.display_name = ( 427 | display_name if display_name else name.replace("_", " ").capitalize() 428 | ) 429 | self.value = value 430 | self.last_value = None 431 | self.required = required 432 | self.type = type 433 | self.url = kwargs.pop("url", "") 434 | self.copy = kwargs.pop("copy", True) 435 | self.file_types = file_types 436 | self.scope = kwargs.pop("scope", "local") 437 | 438 | self.default_value = kwargs.pop("default_value", None) 439 | self.button = kwargs.pop("button", None) 440 | self.button_callback = kwargs.pop("button_callback", None) 441 | self.description = kwargs.pop("description", "") 442 | self.values = kwargs.pop("values", []) 443 | self.filter = kwargs.pop("filter", ".*") 444 | self.filter_action = kwargs.pop("filter_action", "None") 445 | self.check_action = kwargs.pop("check_action", "None") 446 | self.action = kwargs.pop("action", None) 447 | 448 | self.set_extra_attributes_from_keyword_args(**kwargs) 449 | 450 | if self.value is None: 451 | self.value = self.default_value 452 | 453 | self.save_path = kwargs.pop("save_path", "") 454 | 455 | self.get_file_information_from_url() 456 | 457 | def filter_name(self, text): 458 | """Use the filter action to filter out invalid text""" 459 | if text and hasattr(self.filter_action, text): 460 | action = getattr(self.filter_action, text) 461 | return action(text) 462 | return text or "" 463 | 464 | def get_file_information_from_url(self): 465 | """Extract the file information from the setting url""" 466 | if hasattr(self, "url"): 467 | self.file_name = self.url.split("/")[-1] 468 | self.full_file_path = utils.path_join(self.save_path, self.file_name) 469 | self.file_ext = os.path.splitext(self.file_name)[1] 470 | if self.file_ext == ".zip": 471 | self.extract_class = zipfile.ZipFile 472 | self.extract_args = () 473 | elif self.file_ext == ".gz": 474 | self.extract_class = tarfile.TarFile.open 475 | self.extract_args = ("r:gz",) 476 | 477 | def save_file_path(self, version, location=None, sdk_build=False): 478 | """Get the save file path based on the version""" 479 | if location: 480 | self.save_path = location 481 | else: 482 | self.save_path = location or config.download_path() 483 | 484 | self.get_file_information_from_url() 485 | 486 | if self.full_file_path: 487 | path = self.full_file_path.format(version) 488 | 489 | if sdk_build: 490 | path = utils.replace_right(path, "nwjs", "nwjs-sdk", 1) 491 | 492 | return path 493 | 494 | return "" 495 | 496 | def set_extra_attributes_from_keyword_args(self, **kwargs): 497 | for undefined_key, undefined_value in kwargs.items(): 498 | setattr(self, undefined_key, undefined_value) 499 | 500 | def extract(self, ex_path, version, location=None, sdk_build=False): 501 | if os.path.exists(ex_path): 502 | utils.rmtree(ex_path, ignore_errors=True) 503 | 504 | path = location or self.save_file_path(version, sdk_build=sdk_build) 505 | 506 | file = self.extract_class(path, *self.extract_args) 507 | # currently, python's extracting mechanism for zipfile doesn't 508 | # copy file permissions, resulting in a binary that 509 | # that doesn't work. Copied from a patch here: 510 | # http://bugs.python.org/file34873/issue15795_cleaned.patch 511 | if path.endswith(".zip"): 512 | members = file.namelist() 513 | for zipinfo in members: 514 | minfo = file.getinfo(zipinfo) 515 | target = file.extract(zipinfo, ex_path) 516 | mode = minfo.external_attr >> 16 & 0x1FF 517 | os.chmod(target, mode) 518 | else: 519 | file.extractall(ex_path) 520 | 521 | if path.endswith(".tar.gz"): 522 | dir_name = utils.path_join( 523 | ex_path, os.path.basename(path).replace(".tar.gz", "") 524 | ) 525 | else: 526 | dir_name = utils.path_join( 527 | ex_path, os.path.basename(path).replace(".zip", "") 528 | ) 529 | 530 | if os.path.exists(dir_name): 531 | for p in os.listdir(dir_name): 532 | abs_file = utils.path_join(dir_name, p) 533 | utils.move(abs_file, ex_path) 534 | utils.rmtree(dir_name, ignore_errors=True) 535 | 536 | def __repr__(self): 537 | url = "" 538 | if hasattr(self, "url"): 539 | url = self.url 540 | return ( 541 | "Setting: (name={}, " 542 | "display_name={}, " 543 | "value={}, required={}, " 544 | "type={}, url={})" 545 | ).format( 546 | self.name, self.display_name, self.value, self.required, self.type, url 547 | ) 548 | 549 | 550 | class CompleterLineEdit(QtWidgets.QLineEdit): 551 | def __init__(self, tag_dict, *args): 552 | QtWidgets.QLineEdit.__init__(self, *args) 553 | 554 | self.pref = "" 555 | self.tag_dict = tag_dict 556 | 557 | def text_changed(self, text): 558 | all_text = str(text) 559 | text = all_text[: self.cursorPosition()] 560 | prefix = re.split(r"(?<=\))(.*)(?=%\()", text)[-1].strip() 561 | self.pref = prefix 562 | if prefix.strip() != prefix: 563 | self.pref = "" 564 | 565 | def complete_text(self, text): 566 | cursor_pos = self.cursorPosition() 567 | before_text = str(self.text())[:cursor_pos] 568 | after_text = str(self.text())[cursor_pos:] 569 | prefix_len = len(re.split(r"(?<=\))(.*)(?=%\()", before_text)[-1].strip()) 570 | tag_text = self.tag_dict.get(text) 571 | 572 | if tag_text is None: 573 | tag_text = text 574 | 575 | new_text = "{}{}{}".format( 576 | before_text[: cursor_pos - prefix_len], tag_text, after_text 577 | ) 578 | self.setText(new_text) 579 | self.setCursorPosition(len(new_text)) 580 | 581 | 582 | class TagsCompleter(QtWidgets.QCompleter): 583 | def __init__(self, parent, all_tags): 584 | self.keys = sorted(all_tags.keys()) 585 | self.vals = sorted([val for val in all_tags.values()]) 586 | self.tags = list(sorted(self.vals + self.keys)) 587 | QtWidgets.QCompleter.__init__(self, self.tags, parent) 588 | self.editor = parent 589 | 590 | def update(self, text): 591 | obj = self.editor 592 | completion_prefix = obj.pref 593 | model = QtWidgets.QStringListModel(self.tags, self) 594 | self.setModel(model) 595 | 596 | self.setCompletionPrefix(completion_prefix) 597 | if completion_prefix.strip() != "": 598 | self.complete() 599 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for Web2Executable 2 | 3 | This module holds utility functions that are useful to both the command line 4 | and GUI modules, but aren't related to either module. 5 | """ 6 | from __future__ import print_function 7 | import os 8 | import zipfile 9 | import io 10 | import platform 11 | import urllib.request as request 12 | import tempfile 13 | import codecs 14 | import shutil 15 | import subprocess 16 | from appdirs import AppDirs 17 | import validators 18 | import traceback 19 | import logging 20 | import config 21 | 22 | from PySide6 import QtCore 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | def url_exists(path): 28 | if validators.url(path): 29 | return True 30 | return False 31 | 32 | 33 | def format_exc_info(exc_info): 34 | """Return exception string with traceback""" 35 | exc_format = traceback.format_exception(exc_info[0], exc_info[1], exc_info[2]) 36 | error = "".join([x for x in exc_format]) 37 | return error 38 | 39 | 40 | def load_last_project_path(): 41 | """Load the last open project. 42 | 43 | Returns: 44 | string: the last opened project path 45 | """ 46 | proj_path = "" 47 | proj_file = get_data_file_path(config.LAST_PROJECT_FILE) 48 | if os.path.exists(proj_file): 49 | with codecs.open(proj_file, encoding="utf-8") as f: 50 | proj_path = f.read().strip() 51 | 52 | if not proj_path: 53 | proj_path = QtCore.QDir.currentPath() 54 | 55 | return proj_path 56 | 57 | 58 | def load_recent_projects(): 59 | """Load the most recent projects opened. 60 | 61 | Returns: 62 | list: project files sorted by most recent 63 | """ 64 | files = [] 65 | history_file = get_data_file_path(config.RECENT_FILES_FILE) 66 | if not os.path.exists(history_file): 67 | return files 68 | with codecs.open(history_file, encoding="utf-8") as f: 69 | for line in f: 70 | line = line.strip() 71 | if line and os.path.exists(line): 72 | files.append(line) 73 | files.reverse() 74 | return files 75 | 76 | 77 | def save_project_path(path): 78 | """Save the last open project path.""" 79 | proj_file = get_data_file_path(config.LAST_PROJECT_FILE) 80 | with codecs.open(proj_file, "w+", encoding="utf-8") as f: 81 | f.write(path) 82 | 83 | 84 | def save_recent_project(proj): 85 | """Save the most recent projects to a text file.""" 86 | recent_file_path = get_data_file_path(config.RECENT_FILES_FILE) 87 | max_length = config.MAX_RECENT 88 | recent_files = [] 89 | if os.path.exists(recent_file_path): 90 | file_contents = codecs.open(recent_file_path, encoding="utf-8").read() 91 | recent_files = file_contents.split("\n") 92 | try: 93 | recent_files.remove(proj) 94 | except ValueError: 95 | pass 96 | recent_files.append(proj) 97 | with codecs.open(recent_file_path, "w+", encoding="utf-8") as f: 98 | for recent_file in recent_files[-max_length:]: 99 | if recent_file and os.path.exists(recent_file): 100 | f.write("{}\n".format(recent_file)) 101 | 102 | 103 | def replace_right(source, target, replacement, replacements=None): 104 | """ 105 | String replace rightmost instance of a string. 106 | 107 | Args: 108 | source (string): the source to perform the replacement on 109 | target (string): the string to search for 110 | replacement (string): the replacement string 111 | replacements (int or None): if an integer, only replaces N occurrences 112 | otherwise only one occurrence is replaced 113 | """ 114 | return replacement.join(source.rsplit(target, replacements)) 115 | 116 | 117 | def is_windows(): 118 | return platform.system() == "Windows" 119 | 120 | 121 | def get_temp_dir(): 122 | return tempfile.gettempdir() 123 | 124 | 125 | ## File operations ------------------------------------------------------ 126 | # These are overridden because shutil gets Windows directories confused 127 | # and cannot write to them even if they are valid in cmd.exe 128 | 129 | 130 | def path_join(base, *rest): 131 | new_rest = [] 132 | for r in rest: 133 | new_rest.append(str(r)) 134 | 135 | rpath = "/".join(new_rest) 136 | 137 | if not os.path.isabs(rpath): 138 | rpath = base + "/" + rpath 139 | 140 | if is_windows(): 141 | rpath = rpath.replace("/", "\\") 142 | 143 | rpath = os.path.normpath(rpath) 144 | 145 | return rpath 146 | 147 | 148 | def get_data_path(dir_path): 149 | parts = dir_path.split("/") 150 | if config.TESTING: 151 | data_path = path_join(config.CWD, "tests", "test_data", *parts) 152 | else: 153 | dirs = AppDirs("Web2Executable", "Web2Executable") 154 | data_path = path_join(dirs.user_data_dir, *parts) 155 | 156 | if is_windows(): 157 | data_path = data_path.replace("\\", "/") 158 | 159 | if not os.path.exists(data_path): 160 | os.makedirs(data_path) 161 | 162 | return data_path 163 | 164 | 165 | def abs_path(file_path): 166 | path = os.path.abspath(file_path) 167 | 168 | if is_windows(): 169 | path = path.replace("/", "\\") 170 | 171 | return path 172 | 173 | 174 | def get_data_file_path(file_path): 175 | parts = file_path.split("/") 176 | data_path = get_data_path("/".join(parts[:-1])) 177 | return path_join(data_path, parts[-1]) 178 | 179 | 180 | def rmtree(path, **kwargs): 181 | if is_windows(): 182 | if os.path.isabs(path): 183 | path = "\\\\?\\" + path.replace("/", "\\") 184 | shutil.rmtree(path, **kwargs) 185 | 186 | 187 | def copy(src, dest, **kwargs): 188 | if is_windows(): 189 | if os.path.isabs(src): 190 | src = "\\\\?\\" + src.replace("/", "\\") 191 | if os.path.isabs(dest): 192 | dest = "\\\\?\\" + dest.replace("/", "\\") 193 | shutil.copy2(src, dest, **kwargs) 194 | 195 | 196 | def move(src, dest, **kwargs): 197 | if is_windows(): 198 | if os.path.isabs(src): 199 | src = "\\\\?\\" + src.replace("/", "\\") 200 | if os.path.isabs(dest): 201 | dest = "\\\\?\\" + dest.replace("/", "\\") 202 | shutil.move(src, dest, **kwargs) 203 | 204 | 205 | def copytree(src, dest, **kwargs): 206 | if is_windows(): 207 | if os.path.isabs(src) and not src.startswith("\\\\"): 208 | src = "\\\\?\\" + src.replace("/", "\\") 209 | if os.path.isabs(dest) and not dest.startswith("\\\\"): 210 | dest = "\\\\?\\" + dest.replace("/", "\\") 211 | shutil.copytree(src, dest, **kwargs) 212 | 213 | 214 | ## ------------------------------------------------------------ 215 | 216 | 217 | def log(*args): 218 | """Print logging information or log it to a file.""" 219 | if config.DEBUG: 220 | print(*args) 221 | logger.info(", ".join(args)) 222 | 223 | 224 | def open_folder_in_explorer(path): 225 | """Cross platform open folder window.""" 226 | if platform.system() == "Windows": 227 | os.startfile(path) 228 | elif platform.system() == "Darwin": 229 | subprocess.Popen(["open", path]) 230 | else: 231 | subprocess.Popen(["xdg-open", path]) 232 | 233 | 234 | def zip_files(zip_file_name, project_dir, *args, **kwargs): 235 | """ 236 | Zip files into an archive programmatically. 237 | 238 | Args: 239 | zip_file_name (string): the name of the resulting zip file 240 | args: the files to zip 241 | kwargs: Options 242 | verbose (bool): if True, gives verbose output 243 | exclude_paths (list): a list of paths to exclude 244 | """ 245 | zip_file = zipfile.ZipFile(zip_file_name, "w", config.ZIP_MODE) 246 | verbose = kwargs.pop("verbose", False) 247 | old_path = os.getcwd() 248 | 249 | os.chdir(project_dir) 250 | 251 | for arg in args: 252 | if os.path.exists(arg): 253 | file_loc = arg 254 | if verbose: 255 | log(file_loc) 256 | try: 257 | zip_file.write(file_loc) 258 | except ValueError: 259 | os.utime(file_loc, None) 260 | zip_file.write(file_loc) 261 | 262 | os.chdir(old_path) 263 | 264 | zip_file.close() 265 | 266 | 267 | def join_files(destination, *args, **kwargs): 268 | """ 269 | Join any number of files together by stitching bytes together. 270 | 271 | This is used to take advantage of NW.js's ability to execute a zip file 272 | contained at the end of the exe file. 273 | 274 | Args: 275 | destination (string): the name of the resulting file 276 | args: the files to stitch together 277 | """ 278 | with io.open(destination, "wb") as dest_file: 279 | for arg in args: 280 | if os.path.exists(arg): 281 | with io.open(arg, "rb") as file: 282 | while True: 283 | bytes = file.read(4096) 284 | if len(bytes) == 0: 285 | break 286 | dest_file.write(bytes) 287 | 288 | 289 | def urlopen(url): 290 | """ 291 | Call urllib.request.urlopen with a modified SSL context to prevent 292 | "SSL: CERTIFICATE_VERIFY_FAILED” errors when no verification is 293 | actually needed. 294 | """ 295 | return request.urlopen(url, context=config.SSL_CONTEXT) 296 | 297 | 298 | # To avoid a circular import, we import config at the bottom of the file 299 | # and reference it on the module level from within the functions 300 | import config 301 | --------------------------------------------------------------------------------