├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── Readme.txt ├── docs ├── AddIcon_blank-new.png ├── AddedSprites.png ├── CharacterNamePoseExample.png ├── IconGrid_after_adding_icon-new.png ├── IconGrid_done_icon_too_big-new.png ├── IconGrid_done_successful-new.png ├── IconGrid_done_warning-new.png ├── IconGrid_withGrid-new.png ├── IconGrid_withGrid.png ├── InitScreen.png ├── addYourOwnCharacter-char-hx.png ├── bbox-comparison.png ├── final-files-new.png ├── frame-buttons-new.png ├── on-mouse-hover.png └── place-to-find-addByPrefix-character-hx.png ├── requirements.txt └── src ├── NewXMLPngUI.ui ├── PreviewAnimationWindow.ui ├── SpritesheetGenSettings.ui ├── XMLTableWidget.ui ├── animationwindow.py ├── animpreviewwindow.py ├── assets ├── AddImg.png ├── app-styles.qss ├── appicon.ico ├── appicon.png └── remove-frame-icon.svg ├── engine ├── icongridutils.py ├── imgutils.py ├── packingalgorithms.py ├── spritesheetutils.py └── xmlpngengine.py ├── framedata.py ├── mainUI.py ├── settingswindow.py ├── spriteframe.py ├── spritesheetgensettings.py ├── utils.py ├── xmlpngUI.py ├── xmltablewindow.py └── xmltablewindowUI.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Template for submitting any bug reports 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | The exact steps that were followed to cause the issue 16 | 17 | **Expected behavior** 18 | A clear and concise description of what was expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Which version was it on?** 24 | The version number of the tool this bug was found on. 25 | 26 | **Application ran from exe or from source?** 27 | - OS: 28 | - Was the application run from source or was it the executable from gamebanana? 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | 10 | createrelease: 11 | name: Create Release 12 | runs-on: [ubuntu-latest] 13 | steps: 14 | - name: Create Release 15 | id: create_release 16 | uses: actions/create-release@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | tag_name: ${{ github.ref }} 21 | release_name: Release ${{ github.ref }} 22 | draft: false 23 | prerelease: false 24 | - name: Output Release URL File 25 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 26 | - name: Save Release URL File for publish 27 | uses: actions/upload-artifact@v1 28 | with: 29 | name: release_url 30 | path: release_url.txt 31 | 32 | build: 33 | name: Build packages 34 | needs: createrelease 35 | runs-on: ${{ matrix.os }} 36 | strategy: 37 | matrix: 38 | include: 39 | - os: macos-latest 40 | TARGET: macos 41 | CMD_BUILD: > 42 | pyinstaller --noconfirm --onedir --windowed --icon "./src/assets/appicon.ico" --name "Spritesheet and XML Generator for Friday Night Funkin" --noupx --add-data "./src/assets:assets/" "./src/xmlpngUI.py" && 43 | cd dist/ && 44 | zip -r9 "FnFXMLGen-MacOS.zip" "Spritesheet and XML Generator for Friday Night Funkin/" 45 | OUT_FILE_NAME: FnFXMLGen-MacOS.zip 46 | ASSET_MIME: application/zip 47 | - os: windows-latest 48 | TARGET: windows 49 | CMD_BUILD: pyinstaller --noconfirm --onedir --windowed --icon "./src/assets/appicon.ico" --name "Spritesheet-and-XML-Generator-for-Friday-Night-Funkin" --noupx --add-data "./src/assets;assets/" "./src/xmlpngUI.py" && cd dist/ && powershell Compress-Archive -LiteralPath ".\Spritesheet-and-XML-Generator-for-Friday-Night-Funkin" -DestinationPath ".\Spritesheet-XML-Generator-FnF-Windows.zip" 50 | OUT_FILE_NAME: Spritesheet-XML-Generator-FnF-Windows.zip 51 | ASSET_MIME: application/zip 52 | - os: ubuntu-latest 53 | TARGET: ubuntu 54 | CMD_BUILD: > 55 | pyinstaller --noconfirm --onedir --windowed --icon "./src/assets/appicon.ico" --name "Spritesheet and XML Generator for Friday Night Funkin" --noupx --add-data "./src/assets:assets/" "./src/xmlpngUI.py" && 56 | cd dist/ && 57 | zip -r9 "FnFXMLGen-Linux.zip" "Spritesheet and XML Generator for Friday Night Funkin/" 58 | OUT_FILE_NAME: FnFXMLGen-Linux.zip 59 | ASSET_MIME: application/zip 60 | steps: 61 | - uses: actions/checkout@v1 62 | - name: Set up Python 3.9 63 | uses: actions/setup-python@v2 64 | with: 65 | python-version: 3.9 66 | - name: Install dependencies 67 | run: | 68 | python -m pip install --upgrade pip --verbose 69 | pip install -r requirements.txt 70 | pip install pyinstaller 71 | pip list 72 | - name: Build with pyinstaller for ${{matrix.TARGET}} 73 | run: ${{matrix.CMD_BUILD}} 74 | - name: Load Release URL File from release job 75 | uses: actions/download-artifact@v1 76 | with: 77 | name: release_url 78 | - name: Get Release File Name & Upload URL 79 | id: get_release_info 80 | shell: bash 81 | run: | 82 | value=`cat release_url/release_url.txt` 83 | echo ::set-output name=upload_url::$value 84 | - name: Upload Release Asset 85 | id: upload-release-asset 86 | uses: actions/upload-release-asset@v1 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | with: 90 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 91 | asset_path: ./dist/${{ matrix.OUT_FILE_NAME}} 92 | asset_name: ${{ matrix.OUT_FILE_NAME}} 93 | asset_content_type: ${{ matrix.ASSET_MIME}} 94 | 95 | buildwin32bit: 96 | name: Build packages for win-32bit 97 | needs: createrelease 98 | runs-on: ${{ matrix.os }} 99 | strategy: 100 | matrix: 101 | include: 102 | - os: windows-latest 103 | TARGET: windows 104 | CMD_BUILD: pyinstaller --noconfirm --onedir --windowed --icon "./src/assets/appicon.ico" --name "Spritesheet-and-XML-Generator-for-Friday-Night-Funkin" --noupx --add-data "./src/assets;assets/" "./src/xmlpngUI.py" && cd dist/ && powershell Compress-Archive -LiteralPath ".\Spritesheet-and-XML-Generator-for-Friday-Night-Funkin" -DestinationPath ".\Spritesheet-XML-Generator-FnF-Windows-32bit.zip" 105 | OUT_FILE_NAME: Spritesheet-XML-Generator-FnF-Windows-32bit.zip 106 | ASSET_MIME: application/zip 107 | steps: 108 | - uses: actions/checkout@v1 109 | - name: Set up Python 3.9 32bit 110 | uses: actions/setup-python@v2 111 | with: 112 | python-version: 3.9 113 | architecture: 'x86' 114 | - name: Install dependencies 115 | run: | 116 | python -m pip install --upgrade pip --verbose 117 | pip install -r requirements.txt 118 | pip install pyinstaller==3.6 119 | - name: Build with pyinstaller for ${{matrix.TARGET}} 120 | run: ${{matrix.CMD_BUILD}} 121 | - name: Load Release URL File from release job 122 | uses: actions/download-artifact@v1 123 | with: 124 | name: release_url 125 | - name: Get Release File Name & Upload URL 126 | id: get_release_info 127 | shell: bash 128 | run: | 129 | value=`cat release_url/release_url.txt` 130 | echo ::set-output name=upload_url::$value 131 | - name: Upload Release Asset 132 | id: upload-release-asset 133 | uses: actions/upload-release-asset@v1 134 | env: 135 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 136 | with: 137 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 138 | asset_path: ./dist/${{ matrix.OUT_FILE_NAME}} 139 | asset_name: ${{ matrix.OUT_FILE_NAME}} 140 | asset_content_type: ${{ matrix.ASSET_MIME}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyo 3 | .vscode 4 | env 5 | .python-version 6 | .venv 7 | .qt_for_python 8 | preferences.json 9 | BIG_REMINDER_DO_NOT_FORGET.txt 10 | *.log 11 | _tmp.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FnF-Spritesheet-and-XML-Maker 2 | A Friday Night Funkin' mod making helper tool that allows you to generate XML files and spritesheets from individual pngs. This is a free and open-source mini-replacement tool to the "Generate Spritesheet" functionality in Adobe Animate/Flash 3 |
4 | To download the application for windows instead of running it from source click on this link. 5 | 6 | ## Big Update: There is a web version of this tool available [here](https://uncertainprod.github.io/FNF-Spritesheet-XML-generator-Web). If the desktop app does not work for you, then you can try this out instead. [Instructions and source code of the web version](https://github.com/UncertainProd/FNF-Spritesheet-XML-generator-Web) 7 | 8 | ##### Disclaimer: The execuatable on gamebanana only works on windows. Other operating system users should run this from source (or try out the experimental build from the releases here). Instructions to run from source at the bottom of this page. 9 | 10 | ## How to generate spritesheet and XML files for your character: 11 | ### Update: The instructions below may be a bit outdated, so consider using this video tutorial instead: 12 | Youtube Video tutorial 13 | 14 | 15 | The name of your character goes into the textbox at the top. This is necessary as the final xml and png files will be named accordingly. 16 | Eg: If you name you character Pixel-GF the files generated will be named Pixel-GF.png and Pixel-GF.xml 17 | Leaving this box blank will cause an error to show up. 18 | 19 | ### Adding sprite frames 20 | Click the button named "Add Frame Image" to add each pose as frame in the spritesheet, as shown below: 21 | 22 | Alternatively, go to File > Import Images... to do the same. You can also import frames from existing spritesheets using File > Import Existing Spritesheet and XML 23 | 24 | Each "frame" of your spritesheet has a button (to delete that frame) and a checkbox (to change it's animation name):
25 |
26 | Use the "Set Animation (Pose) Name" button to name each pose that has been selected (has its checkbox checked). Animation names refer to names like: 'sing down' or 'idle'. To delete any individual frame click the "Delete Frame" button. Pose names can repeat if needed (Eg: You can name 2 or more frames 'sing left' and it'll be taken care of in the xml). 27 | 28 | #### To find out current animation (pose) name of any frame, simply hover the mouse over it and it'll display information about that particular frame.
29 |
30 | 31 | ### Clip to bounding box 32 | If your individual frames have extra whitespace in them and you want them all cropped to just their bounding box, click this checkbox before generating the files. This checkbox will show up on clicking the "Spritesheet Generation Settings" button
33 |
34 | On left is how the image will be considered if this checkbox is left unchecked. On the right is how it'll be considered if it is checked. (Side note: Most of the time you won't really have to use this feature, but it is provided just in case) 35 | 36 | ### Generating the final XML and PNG files 37 | When you're done adding all the frames and giving them animation names, it's time to generate the final PNG and XML files! 38 | To do so, just click the "Generate XML" button. Select the location you want the files saved and the xml and png files will be generated.
39 | 40 | 41 |

42 | Note: Although the main functionality of this application is complete, there are still minor crashing issues and bugs that may need fixing. Updates will be on the way soon. Stay tuned! 43 | 44 | ## UPDATE: The instructions you see in this section below are only to be followed if you are modding the base game, not if you are using kade engine/psych engine. 45 | #### However, the Spritesheets and XMLs generated by this tool should work for those engines too as they all follow a similar format. If you are using Kade Engine or Psych Engine, follow the specific instructions for that engine. 46 | ### How to use these files in the source code 47 | Now that you have the .xml and the .png files you can follow the instructions as per this guide by Tuxsuper on gamebanana to add your character into the game. This particular application, as of now, is to help with section 1.3 of the guide in particular (without the use of adobe animate), excluding the parts that have to do with the icon grid. Basically, inside of Character.hx, inside the switch statement shown below:
48 |

49 | Add another case like so:
50 | 51 |
52 | 53 | #### Keep in mind: 54 | 55 |
56 | 57 | ## How to add your character's icon into the icon-grid PNG using this tool: 58 | The latest version of this tool now comes with icon-grid support, so now you can add your character's icon(s) into the icon grid provided in the game files of FNF. Here's how to add your icons into the icon-grid ( Funkin\assets\preload\images\iconGrid.png ) 59 | 60 | ### Click on the tab named "Add Icons to Icon-grid" 61 | 62 | 63 | ### Upload your iconGrid.png file into the application 64 | Click on the "Upload Icon-grid" button to upload your "iconGrid.png" file (Note the this application will OVERWRITE this iconGrid image, so keep some backups of the iconGrid.png file at hand, just in case). 65 | 66 | 67 | ### Add your icon(s): 68 | Friday Night Funkin' healthbar icons MUST be 150x150 pixels in size (according to it's source code). So make sure your icon(s) are all 150x150 pixels. If it's smaller, the application will just center the image into the next available 150x150 square so it can still work sometimes. However, the app will give you an error if your icon is any bigger than 150x150 pixels. Once you have added your icon(s), the number of icons that will be added will be shown at the bottom of the window as shown here: 69 | 70 | 71 | ### Generate your new icongrid 72 | Click the "Generate New Icon-grid" button and it will modify the iconGrid.png that was uploaded, adding your character icons to the grid, wherever it finds free space. In case the grid is full, it will show an error. But if it's successful, it will tell you the indices at which the icons were added (which is needed in order to add the icons into the game). Now you have successfully generated your new iconGrid.png file.
73 | Update: There is now a checkbox called "Psych Engine mode", which when checked will allow you to make Psych-engine compatible icons. Just upload the 2 images and click "Generate New Icon-grid". 74 | 75 |
Dialog box when icons are added successfully
76 |

77 | 78 |
Dialog box when one of the icons are smaller than 150 x 150
79 |

80 | 81 | 82 |
Dialog box when one of the icons is bigger than 150 x 150
83 |

84 | 85 | 86 | ## Running from source: 87 | In order to run this from source, you will need python and pip installed on your device (pip should come pre-installed with python). Clone/download this repository by clicking the green button labelled "Code" and downloading the zip, then extract the contents of the zip file. Install the dependencies by opening the command line, navigating to this directory and typing ``` pip install -r requirements.txt ```. Once that is done type ``` cd src ```
``` python xmlpngUI.py ``` to run the application (Sometimes you need to type ``` python3 ``` instead of just ``` python ```). This is a required step for non-windows users! 88 | 89 | #### Side note: Feel free to make ports of this tool (for andriod, iOS, web etc.) if you can, just remember to open source it and credit this repository 90 | -------------------------------------------------------------------------------- /Readme.txt: -------------------------------------------------------------------------------- 1 | Usage instructions (for all platforms) and installation instructions (for Mac and Linux users) are available on: 2 | https://github.com/UncertainProd/FnF-Spritesheet-and-XML-Maker 3 | 4 | A video tutorial is available at: https://www.youtube.com/watch?v=lcxpa7Gc3i0 5 | 6 | Important: Inside the unzipped folder, click on the application named "XML Generator for Friday Night Funkin", which will be an exe file for windows. 7 | 8 | People on other Operating Systems (Mac and Linux) should consider running the application from souce from the github link given above, or try out some of the experimental builds under the releases on github. 9 | 10 | The app is written in python and uses the PyQt5 framework. 11 | 12 | UPDATE: A web version of this tool is available at https://uncertainprod.github.io/FNF-Spritesheet-XML-generator-Web/ 13 | which you can use in case the desktop app does not work or isn't supported on your device -------------------------------------------------------------------------------- /docs/AddIcon_blank-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/AddIcon_blank-new.png -------------------------------------------------------------------------------- /docs/AddedSprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/AddedSprites.png -------------------------------------------------------------------------------- /docs/CharacterNamePoseExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/CharacterNamePoseExample.png -------------------------------------------------------------------------------- /docs/IconGrid_after_adding_icon-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/IconGrid_after_adding_icon-new.png -------------------------------------------------------------------------------- /docs/IconGrid_done_icon_too_big-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/IconGrid_done_icon_too_big-new.png -------------------------------------------------------------------------------- /docs/IconGrid_done_successful-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/IconGrid_done_successful-new.png -------------------------------------------------------------------------------- /docs/IconGrid_done_warning-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/IconGrid_done_warning-new.png -------------------------------------------------------------------------------- /docs/IconGrid_withGrid-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/IconGrid_withGrid-new.png -------------------------------------------------------------------------------- /docs/IconGrid_withGrid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/IconGrid_withGrid.png -------------------------------------------------------------------------------- /docs/InitScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/InitScreen.png -------------------------------------------------------------------------------- /docs/addYourOwnCharacter-char-hx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/addYourOwnCharacter-char-hx.png -------------------------------------------------------------------------------- /docs/bbox-comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/bbox-comparison.png -------------------------------------------------------------------------------- /docs/final-files-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/final-files-new.png -------------------------------------------------------------------------------- /docs/frame-buttons-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/frame-buttons-new.png -------------------------------------------------------------------------------- /docs/on-mouse-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/on-mouse-hover.png -------------------------------------------------------------------------------- /docs/place-to-find-addByPrefix-character-hx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/docs/place-to-find-addByPrefix-character-hx.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==9.4.0 2 | PyQt5==5.15.7 3 | PyQt5-Qt5==5.15.2 4 | PyQt5-sip==12.11.0 5 | -------------------------------------------------------------------------------- /src/NewXMLPngUI.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1066 10 | 790 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 16 23 | 75 24 | true 25 | 26 | 27 | 28 | Spritesheet XML Generator for Friday Night Funkin' 29 | 30 | 31 | Qt::AlignCenter 32 | 33 | 34 | 35 | 36 | 37 | 38 | QFrame::StyledPanel 39 | 40 | 41 | QFrame::Raised 42 | 43 | 44 | 45 | 46 | 47 | 48 | 10 49 | 50 | 51 | 52 | Character Name: 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 10 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 0 72 | 73 | 74 | 75 | XML from Frames 76 | 77 | 78 | 79 | 80 | 81 | QFrame::StyledPanel 82 | 83 | 84 | QFrame::Raised 85 | 86 | 87 | 88 | 89 | 90 | Qt::ScrollBarAlwaysOff 91 | 92 | 93 | true 94 | 95 | 96 | 97 | 98 | 0 99 | 0 100 | 990 101 | 456 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | QFrame::StyledPanel 111 | 112 | 113 | QFrame::Raised 114 | 115 | 116 | 117 | 118 | 119 | 120 | 0 121 | 40 122 | 123 | 124 | 125 | Spritesheet 126 | Generation Settings 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 0 135 | 40 136 | 137 | 138 | 139 | Set Animation (Pose) Name 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 0 148 | 0 149 | 150 | 151 | 152 | 153 | 0 154 | 45 155 | 156 | 157 | 158 | 159 | 75 160 | true 161 | 162 | 163 | 164 | Generate XML 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | Add Icons to Icon-grid 179 | 180 | 181 | 182 | 183 | 184 | QFrame::StyledPanel 185 | 186 | 187 | QFrame::Raised 188 | 189 | 190 | 191 | 192 | 193 | true 194 | 195 | 196 | 197 | 198 | 0 199 | 0 200 | 990 201 | 451 202 | 203 | 204 | 205 | 206 | 0 207 | 208 | 209 | 0 210 | 211 | 212 | 0 213 | 214 | 215 | 0 216 | 217 | 218 | 0 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | QFrame::StyledPanel 235 | 236 | 237 | QFrame::Raised 238 | 239 | 240 | 241 | 242 | 243 | 244 | 0 245 | 50 246 | 247 | 248 | 249 | Upload Icon-grid 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 0 258 | 50 259 | 260 | 261 | 262 | Upload Icons 263 | 264 | 265 | 266 | 267 | 268 | 269 | true 270 | 271 | 272 | Psych Engine mode 273 | 274 | 275 | 276 | 277 | 278 | 279 | No. of 280 | icons selected: 281 | 0 282 | 283 | 284 | Qt::AlignCenter 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 75 293 | true 294 | 295 | 296 | 297 | Tip: Use ctrl+i and ctrl+o to zoom in or out respectively 298 | 299 | 300 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 301 | 302 | 303 | 304 | 305 | 306 | 307 | Zoom: 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 0 316 | 50 317 | 318 | 319 | 320 | Generate New 321 | Icon-grid 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 0 341 | 0 342 | 1066 343 | 26 344 | 345 | 346 | 347 | 348 | File 349 | 350 | 351 | 352 | Export... 353 | 354 | 355 | 356 | 357 | 358 | 359 | Import... 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | Edit 375 | 376 | 377 | 378 | Edit Selected Images 379 | 380 | 381 | 382 | Flip 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | View 398 | 399 | 400 | 401 | Qt::ActionsContextMenu 402 | 403 | 404 | Theme 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | Import existing Spritesheet and XML 420 | 421 | 422 | 423 | 424 | Import Images.... 425 | 426 | 427 | 428 | 429 | Clear Spritesheet Grid 430 | 431 | 432 | 433 | 434 | Edit Frame Properties 435 | 436 | 437 | 438 | 439 | false 440 | 441 | 442 | Import icons 443 | 444 | 445 | 446 | 447 | false 448 | 449 | 450 | Import IconGrid 451 | 452 | 453 | 454 | 455 | false 456 | 457 | 458 | Import Icons 459 | 460 | 461 | 462 | 463 | false 464 | 465 | 466 | Clear IconGrid 467 | 468 | 469 | 470 | 471 | false 472 | 473 | 474 | Clear Icon selection 475 | 476 | 477 | 478 | 479 | Export as Spritesheet and XML 480 | 481 | 482 | 483 | 484 | Export individual images 485 | 486 | 487 | 488 | 489 | Preview Animation 490 | 491 | 492 | 493 | 494 | true 495 | 496 | 497 | true 498 | 499 | 500 | Default 501 | 502 | 503 | 504 | 505 | true 506 | 507 | 508 | Dark mode 509 | 510 | 511 | 512 | 513 | View XML structure 514 | 515 | 516 | 517 | 518 | Default 519 | 520 | 521 | 522 | 523 | Dark 524 | 525 | 526 | 527 | 528 | true 529 | 530 | 531 | true 532 | 533 | 534 | Default 535 | 536 | 537 | 538 | 539 | Dark 540 | 541 | 542 | 543 | 544 | FlipX 545 | 546 | 547 | 548 | 549 | FlipY 550 | 551 | 552 | 553 | 554 | tets 555 | 556 | 557 | 558 | 559 | Import from GIF 560 | 561 | 562 | 563 | 564 | 565 | 566 | -------------------------------------------------------------------------------- /src/PreviewAnimationWindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | animation_view 4 | 5 | 6 | 7 | 0 8 | 0 9 | 578 10 | 542 11 | 12 | 13 | 14 | Animation Preview 15 | 16 | 17 | 18 | 19 | 20 | QFrame::StyledPanel 21 | 22 | 23 | QFrame::Raised 24 | 25 | 26 | 27 | 28 | 29 | Animation Name to be played: 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | QAbstractSpinBox::UpDownArrows 40 | 41 | 42 | fps 43 | 44 | 45 | 1 46 | 47 | 48 | 140 49 | 50 | 51 | 24 52 | 53 | 54 | 55 | 56 | 57 | 58 | Play Animation 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 2000 70 | 2000 71 | 72 | 73 | 74 | --PIXMAP GOES HERE-- 75 | 76 | 77 | Qt::AlignCenter 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/SpritesheetGenSettings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 603 13 | 552 14 | 15 | 16 | 17 | Spritesheet Generation Settings 18 | 19 | 20 | 21 | 22 | 23 | 24 | 9 25 | 26 | 27 | 28 | Clip to bounding box (applies to every frame you add after this box checked) 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | QFrame::StyledPanel 39 | 40 | 41 | QFrame::Raised 42 | 43 | 44 | 45 | 46 | 47 | 48 | 9 49 | 50 | 51 | 52 | Animation Name prefixing: 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 8 61 | 62 | 63 | 64 | Add Character Name 65 | Before Animation Prefix 66 | 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 8 77 | 78 | 79 | 80 | Custom general animation prefix 81 | 82 | 83 | 84 | 85 | 86 | 87 | QFrame::StyledPanel 88 | 89 | 90 | QFrame::Raised 91 | 92 | 93 | 94 | 0 95 | 96 | 97 | 98 | 99 | 100 | 8 101 | 102 | 103 | 104 | Custom Prefix: 105 | 106 | 107 | 108 | 109 | 110 | 111 | false 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | Don't use any prefix (what you type in the prefix box is exactly what will show up in the XML) 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 8 133 | 134 | 135 | 136 | Use Prefix even if frame is imported from existing XML 137 | 138 | 139 | 140 | 141 | 142 | 143 | Do not merge look-alike frames 144 | (WARNING: Can cause extremely large spritesheets which may cause windows to 145 | refuse to open them. 146 | May also cause crashes!) 147 | 148 | 149 | 150 | 151 | 152 | 153 | QFrame::StyledPanel 154 | 155 | 156 | QFrame::Raised 157 | 158 | 159 | 160 | 161 | 162 | px 163 | 164 | 165 | 20 166 | 167 | 168 | 169 | 170 | 171 | 172 | Frame Padding (use this to add empty pixels to the edge of each frame, helps prevent 173 | sprites clipping into each other) 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | QFrame::StyledPanel 184 | 185 | 186 | QFrame::Raised 187 | 188 | 189 | 190 | 191 | 192 | Packing Algorithm 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | Qt::Vertical 206 | 207 | 208 | 209 | 20 210 | 40 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | QFrame::StyledPanel 219 | 220 | 221 | QFrame::Raised 222 | 223 | 224 | 225 | 30 226 | 227 | 228 | 229 | 230 | 231 | 0 232 | 35 233 | 234 | 235 | 236 | 237 | 9 238 | 239 | 240 | 241 | Save Settings 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 0 250 | 35 251 | 252 | 253 | 254 | 255 | 9 256 | 257 | 258 | 259 | Cancel 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /src/XMLTableWidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TableWidgetThing 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1181 10 | 586 11 | 12 | 13 | 14 | XML Table View 15 | 16 | 17 | 18 | QLayout::SetDefaultConstraint 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 800 28 | 0 29 | 30 | 31 | 32 | 33 | 800 34 | 16777215 35 | 36 | 37 | 38 | QFrame::StyledPanel 39 | 40 | 41 | QFrame::Raised 42 | 43 | 44 | 45 | 46 | 47 | QFrame::StyledPanel 48 | 49 | 50 | QFrame::Raised 51 | 52 | 53 | 54 | 20 55 | 56 | 57 | 58 | 59 | 60 | 61 | FrameX 62 | 63 | 64 | 65 | 66 | 67 | 68 | -10000 69 | 70 | 71 | 10000 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | FrameY 83 | 84 | 85 | 86 | 87 | 88 | 89 | -10000 90 | 91 | 92 | 10000 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | FrameWidth 104 | 105 | 106 | 107 | 108 | 109 | 110 | 1 111 | 112 | 113 | 10000 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | FrameHeight 125 | 126 | 127 | 128 | 129 | 130 | 131 | 1 132 | 133 | 134 | 10000 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | true 147 | 148 | 149 | 150 | 151 | 0 152 | 0 153 | 774 154 | 456 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 0 163 | 0 164 | 165 | 166 | 167 | 168 | 50 169 | 50 170 | 171 | 172 | 173 | Frame preview goes here 174 | 175 | 176 | Qt::AlignCenter 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 9 189 | 190 | 191 | 192 | Info about the frame 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /src/animationwindow.py: -------------------------------------------------------------------------------- 1 | from animpreviewwindow import Ui_animation_view 2 | from PyQt5.QtWidgets import QWidget 3 | from PyQt5.QtCore import QTimer 4 | import engine.spritesheetutils as spritesheetutils 5 | from utils import imghashes 6 | 7 | class AnimationView(QWidget): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | self.ui = Ui_animation_view() 11 | self.ui.setupUi(self) 12 | 13 | self.ui.play_anim_button.clicked.connect(self.play_animation) 14 | self.ui.animation_display_area.setText("Click 'Play Animation' to start the animation preview") 15 | self.ui.animation_display_area.setStyleSheet("background-color:#696969;") 16 | 17 | self.animframes = [] 18 | self.anim_names = {} 19 | self.frameindex = 0 20 | self.animstarted = False 21 | self.timer = QTimer() 22 | self.timer.timeout.connect(self.set_next_frame) 23 | 24 | def parse_and_load_frames(self, frames): 25 | for f in frames: 26 | if f.data.pose_name in self.anim_names: 27 | self.anim_names[f.data.pose_name].append(f) 28 | else: 29 | self.anim_names[f.data.pose_name] = [ f ] 30 | self.ui.pose_combobox.addItems(list(self.anim_names.keys())) 31 | 32 | def play_animation(self): 33 | if self.animstarted: 34 | self.timer.stop() 35 | self.animstarted = False 36 | self.ui.play_anim_button.setText("Play Animation") 37 | else: 38 | self.animstarted = True 39 | framerate = self.ui.framerate_adjust.value() 40 | animname = self.ui.pose_combobox.currentText() 41 | self.animframes = self.anim_names[animname] 42 | self.frameindex = 0 43 | print(f"Playing {animname} at {framerate}fps with nframes:{len(self.animframes)}") 44 | self.ui.play_anim_button.setText("Stop Animation") 45 | self.timer.start(int(1000/framerate)) 46 | 47 | def set_next_frame(self): 48 | curframe = self.animframes[self.frameindex] 49 | curframeimg = imghashes.get(curframe.data.img_hash) 50 | truframe_pixmap = spritesheetutils.get_true_frame( 51 | curframeimg, 52 | curframe.data.framex if curframe.data.framex is not None else 0, 53 | curframe.data.framey if curframe.data.framey is not None else 0, 54 | curframe.data.framew if curframe.data.framew is not None else curframeimg.width, 55 | curframe.data.frameh if curframe.data.frameh is not None else curframeimg.height, 56 | ).toqpixmap() 57 | self.ui.animation_display_area.setPixmap(truframe_pixmap) 58 | self.frameindex = (self.frameindex + 1) % len(self.animframes) 59 | 60 | def closeEvent(self, a0): 61 | self.timer.stop() 62 | self.animstarted = False 63 | self.ui.animation_display_area.clear() 64 | self.ui.pose_combobox.clear() 65 | self.animframes.clear() 66 | self.anim_names.clear() 67 | self.frameindex = 0 68 | return super().closeEvent(a0) 69 | -------------------------------------------------------------------------------- /src/animpreviewwindow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'PreviewAnimationWindow.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_animation_view(object): 15 | def setupUi(self, animation_view): 16 | animation_view.setObjectName("animation_view") 17 | animation_view.resize(578, 542) 18 | self.verticalLayout = QtWidgets.QVBoxLayout(animation_view) 19 | self.verticalLayout.setObjectName("verticalLayout") 20 | self.frame = QtWidgets.QFrame(animation_view) 21 | self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 22 | self.frame.setFrameShadow(QtWidgets.QFrame.Raised) 23 | self.frame.setObjectName("frame") 24 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame) 25 | self.horizontalLayout.setObjectName("horizontalLayout") 26 | self.animation_info_label = QtWidgets.QLabel(self.frame) 27 | self.animation_info_label.setObjectName("animation_info_label") 28 | self.horizontalLayout.addWidget(self.animation_info_label) 29 | self.pose_combobox = QtWidgets.QComboBox(self.frame) 30 | self.pose_combobox.setObjectName("pose_combobox") 31 | self.horizontalLayout.addWidget(self.pose_combobox) 32 | self.framerate_adjust = QtWidgets.QSpinBox(self.frame) 33 | self.framerate_adjust.setButtonSymbols(QtWidgets.QAbstractSpinBox.UpDownArrows) 34 | self.framerate_adjust.setMinimum(1) 35 | self.framerate_adjust.setMaximum(140) 36 | self.framerate_adjust.setProperty("value", 24) 37 | self.framerate_adjust.setObjectName("framerate_adjust") 38 | self.horizontalLayout.addWidget(self.framerate_adjust) 39 | self.play_anim_button = QtWidgets.QPushButton(self.frame) 40 | self.play_anim_button.setObjectName("play_anim_button") 41 | self.horizontalLayout.addWidget(self.play_anim_button) 42 | self.verticalLayout.addWidget(self.frame) 43 | self.animation_display_area = QtWidgets.QLabel(animation_view) 44 | self.animation_display_area.setMaximumSize(QtCore.QSize(2000, 2000)) 45 | self.animation_display_area.setAlignment(QtCore.Qt.AlignCenter) 46 | self.animation_display_area.setObjectName("animation_display_area") 47 | self.verticalLayout.addWidget(self.animation_display_area) 48 | self.verticalLayout.setStretch(1, 1) 49 | 50 | self.retranslateUi(animation_view) 51 | QtCore.QMetaObject.connectSlotsByName(animation_view) 52 | 53 | def retranslateUi(self, animation_view): 54 | _translate = QtCore.QCoreApplication.translate 55 | animation_view.setWindowTitle(_translate("animation_view", "Animation Preview")) 56 | self.animation_info_label.setText(_translate("animation_view", "Animation Name to be played:")) 57 | self.framerate_adjust.setSuffix(_translate("animation_view", "fps")) 58 | self.play_anim_button.setText(_translate("animation_view", "Play Animation")) 59 | self.animation_display_area.setText(_translate("animation_view", "--PIXMAP GOES HERE--")) 60 | -------------------------------------------------------------------------------- /src/assets/AddImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/src/assets/AddImg.png -------------------------------------------------------------------------------- /src/assets/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/src/assets/appicon.ico -------------------------------------------------------------------------------- /src/assets/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UncertainProd/FnF-Spritesheet-and-XML-Maker/fd6ce1092a44df68ecd8d656027748c6964eebfc/src/assets/appicon.png -------------------------------------------------------------------------------- /src/assets/remove-frame-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Layer 1 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/engine/icongridutils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | 3 | def _icongrid_add_row(icongrid, iconsize=150): 4 | icongrid_cpy = icongrid.copy() 5 | new_icongrid = Image.new('RGBA', (icongrid_cpy.size[0], icongrid_cpy.size[1]+iconsize), (0, 0, 0, 0)) 6 | new_icongrid.paste(icongrid_cpy) 7 | icongrid_cpy.close() 8 | return new_icongrid 9 | 10 | def _get_last_row_and_col(icongrid, iconsize=150): 11 | _box = icongrid.getbbox() 12 | pix_to_index = lambda pixels: pixels//iconsize 13 | 14 | if _box: 15 | # finding last row index 16 | _, _, _, lastrow_y = _box 17 | last_row_index = pix_to_index(lastrow_y) 18 | 19 | # finding last col index 20 | _last_row_img = icongrid.crop((0, last_row_index*iconsize, icongrid.width, last_row_index*iconsize + iconsize)) 21 | 22 | _box = _last_row_img.getbbox() 23 | if _box: 24 | _, _, lastcol_x, _ = _box 25 | else: 26 | print("ERROR: error in trying to find last column, setting to 0") 27 | lastcol_x = 0 28 | last_col_index = pix_to_index(lastcol_x) 29 | else: 30 | print("ERROR: icongrid is empty, cannot find bbox") 31 | last_row_index = 0 32 | last_col_index = 0 33 | 34 | return last_row_index, last_col_index 35 | 36 | ICON_PERFECT_FIT = 0 37 | ICON_BIGGER_THAN_AREA = 1 38 | ICON_SMALLER_THAN_AREA = 2 39 | 40 | def _check_icon_size(icon, check_width=150, check_height=150): 41 | # 0 = icon is 150x150 42 | # 1 = icon is too wide/tall 43 | # 2 = icon is smaller than 150x150 area 44 | if icon.width == check_width and icon.height == check_height: 45 | return ICON_PERFECT_FIT 46 | elif icon.width > 150 or icon.height > 150: 47 | return ICON_BIGGER_THAN_AREA 48 | else: 49 | return ICON_SMALLER_THAN_AREA 50 | 51 | def _center_icon(icon): 52 | w, h = icon.size 53 | final_icon = Image.new('RGBA', (150, 150), (0, 0, 0, 0)) 54 | dx = (150 - w) // 2 55 | dy = (150 - h) // 2 56 | final_icon.paste(icon, (dx, dy)) 57 | return final_icon 58 | 59 | def appendIconToGrid(icongrid_path, iconpaths, iconsize=150): 60 | print("Icongrid from: {} \nIcons: {}".format(icongrid_path, len(iconpaths))) 61 | 62 | return_status = 0 63 | indices = [] 64 | problem_icon = None 65 | exception_msg = None 66 | 67 | IMAGES_PER_COLUMN = 10 68 | 69 | try: 70 | # icongrid = Image.open(icongrid_path) 71 | with Image.open(icongrid_path).convert('RGBA') as icongrid: 72 | # icongrid = icongrid.convert('RGBA') 73 | for iconpath in iconpaths: 74 | # icon_img = Image.open(iconpath) 75 | with Image.open(iconpath).convert('RGBA') as icon_img: 76 | # check if icon_img is 150x150 77 | can_fit = _check_icon_size(icon_img) 78 | if can_fit == ICON_BIGGER_THAN_AREA: 79 | # if the icon is too big, ignore it (for now) 80 | return_status = 2 81 | problem_icon = iconpath 82 | continue 83 | elif can_fit == ICON_SMALLER_THAN_AREA: 84 | print(f"Icon: {iconpath} is smaller than 150x150, centering it....") 85 | icon_img = _center_icon(icon_img) 86 | 87 | # get location to paste it 88 | last_row_idx, last_col_idx = _get_last_row_and_col(icongrid) 89 | 90 | new_index = last_row_idx*IMAGES_PER_COLUMN + last_col_idx + 1 91 | indices.append(new_index) 92 | new_row_idx = new_index // IMAGES_PER_COLUMN 93 | new_col_idx = new_index % IMAGES_PER_COLUMN 94 | 95 | if new_row_idx * iconsize >= icongrid.height: 96 | print("Icongrid is full. Expanding it....") 97 | icongrid = _icongrid_add_row(icongrid) 98 | 99 | icongrid.paste(icon_img, (new_col_idx*iconsize, new_row_idx*iconsize)) 100 | icongrid.save(icongrid_path) 101 | except Exception as e: 102 | return_status = -1 103 | exception_msg = f"{e.__class__.__name__} : {str(e)}" 104 | 105 | 106 | return return_status, indices, problem_icon, exception_msg 107 | 108 | def makePsychEngineIconGrid(iconpaths, savepath, img_size=150): 109 | # this function works for any number of icons that you provide, even though psych engine uses 2 icons for a character 110 | status = 0 111 | problemimg = None 112 | exception_msg = None 113 | 114 | good_icons = [] 115 | for iconpath in iconpaths: 116 | try: 117 | icon = Image.open(iconpath).convert('RGBA') 118 | except Exception as e: 119 | exception_msg = f"{e.__class__.__name__} : {str(e)}" 120 | continue 121 | fit_status = _check_icon_size(icon) 122 | if fit_status == ICON_BIGGER_THAN_AREA: 123 | problemimg = iconpath 124 | status = 1 125 | icon.close() 126 | continue 127 | elif fit_status == ICON_SMALLER_THAN_AREA: 128 | print(f"Icon: {iconpath} is smaller than 150x150, centering it....") 129 | icon = _center_icon(icon) 130 | good_icons.append(icon) 131 | 132 | if len(good_icons) == 0: 133 | return 1, problemimg, exception_msg 134 | 135 | final_icongrid = Image.new('RGBA', (img_size*len(good_icons), img_size), (0, 0, 0, 0)) 136 | for i, icon in enumerate(good_icons): 137 | final_icongrid.paste(icon, (i*img_size, 0)) 138 | icon.close() 139 | 140 | final_icongrid.save(savepath) 141 | final_icongrid.close() 142 | 143 | return status, problemimg, exception_msg -------------------------------------------------------------------------------- /src/engine/imgutils.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | 3 | DEFAULT_PADDING = 1 4 | 5 | def pad_img(img, clip=False, top=DEFAULT_PADDING, right=DEFAULT_PADDING, bottom=DEFAULT_PADDING, left=DEFAULT_PADDING): 6 | if clip: 7 | img = img.crop(img.getbbox()) 8 | 9 | width, height = img.size 10 | new_width = width + right + left 11 | new_height = height + top + bottom 12 | result = Image.new('RGBA', (new_width, new_height), (0, 0, 0, 0)) 13 | result.paste(img, (left, top)) 14 | return result 15 | 16 | def clean_up(*args): 17 | for img in args: 18 | img.close() -------------------------------------------------------------------------------- /src/engine/packingalgorithms.py: -------------------------------------------------------------------------------- 1 | # Packing Algorithm based on https://github.com/jakesgordon/bin-packing/blob/master/js/packer.growing.js 2 | # converted to python 3 | # By (Jake Gordon)[https://github.com/jakesgordon] 4 | class GrowingPacker: 5 | def __init__(self): 6 | self.root = None 7 | 8 | def fit(self, blocks): 9 | num_blocks = len(blocks) 10 | w = blocks[0].get("w", 0) if num_blocks > 0 else 0 11 | h = blocks[0].get("h", 0) if num_blocks > 0 else 0 12 | self.root = { "x":0, "y":0, "w":w, "h":h } 13 | for block in blocks: 14 | node = self.find_node(self.root, block.get("w", 0), block.get("h", 0)) 15 | if node: 16 | block["fit"] = self.split_node(node, block.get("w", 0), block.get("h", 0)) 17 | else: 18 | block["fit"] = self.grow_node(block.get("w", 0), block.get("h", 0)) 19 | 20 | def find_node(self, root, w, h): 21 | if root.get("used"): 22 | return self.find_node(root.get("right"), w, h) or self.find_node(root.get("down"), w, h) 23 | elif w <= root.get("w", 0) and h <= root.get("h", 0): 24 | return root 25 | else: 26 | return None 27 | 28 | def split_node(self, node, w, h): 29 | node["used"] = True 30 | node['down'] = { "x": node.get("x"), "y": node.get("y") + h, "w": node.get("w"), "h":node.get("h") - h } 31 | node['right'] = { "x": node.get("x") + w, "y": node.get("y"), "w": node.get("w") - w, "h": h } 32 | return node 33 | 34 | def grow_node(self, w, h): 35 | canGrowDown = (w <= self.root.get("w")) 36 | canGrowRight = (h <= self.root.get("h")) 37 | 38 | shouldGrowRight = canGrowRight and (self.root.get("h") >= (self.root.get("w") + w)) 39 | shouldGrowDown = canGrowDown and (self.root.get("w") >= (self.root.get("h") + h)) 40 | 41 | if shouldGrowRight: 42 | return self.grow_right(w, h) 43 | elif shouldGrowDown: 44 | return self.grow_down(w, h) 45 | elif canGrowRight: 46 | return self.grow_right(w, h) 47 | elif canGrowDown: 48 | return self.grow_down(w, h) 49 | else: 50 | return None 51 | 52 | def grow_right(self, w, h): 53 | self.root = { 54 | "used": True, 55 | "x":0, 56 | "y":0, 57 | "w":self.root.get("w")+w, 58 | "h":self.root.get("h"), 59 | "down":self.root, 60 | "right": { "x": self.root.get("w"), "y":0, "w":w, "h":self.root.get("h") } 61 | } 62 | node = self.find_node(self.root, w, h) 63 | if node: 64 | return self.split_node(node, w, h) 65 | else: 66 | return None 67 | 68 | def grow_down(self, w, h): 69 | self.root = { 70 | "used": True, 71 | "x":0, 72 | "y":0, 73 | "w":self.root.get("w"), 74 | "h":self.root.get("h") + h, 75 | "down": { "x": 0, "y": self.root.get("h"), "w": self.root.get("w"), "h": h }, 76 | "right": self.root 77 | } 78 | node = self.find_node(self.root, w, h) 79 | if node: 80 | return self.split_node(node, w, h) 81 | else: 82 | return None 83 | 84 | 85 | # This algorithm does not change the order of the sprites (so no "scrambling" of sprites) 86 | # but at the cost of being *slightly* less space-efficient (so you get a slightly bigger spritesheet) 87 | class OrderedPacker: 88 | def __init__(self): 89 | self.blocks = None 90 | self.root = None # not actually a root but named so for consistency 91 | 92 | def fit(self, blocks): 93 | self.blocks = blocks 94 | 95 | blocks_per_row = int(self._get_blocks_per_row_estimate()) 96 | blocks_matrix = [] 97 | for i in range(len(self.blocks)//blocks_per_row + 1): 98 | blocks_matrix.append( self.blocks[i*blocks_per_row:(i+1)*blocks_per_row] ) 99 | 100 | if blocks_matrix[-1] == []: 101 | blocks_matrix = blocks_matrix[:-1] 102 | 103 | final_w = self._get_final_width(blocks_matrix) 104 | final_h = self._get_final_height(blocks_matrix) 105 | 106 | self.root = { 107 | 'w': final_w, 108 | 'h': final_h 109 | } 110 | 111 | curr_x = 0 112 | curr_y = 0 113 | max_heights = [ max([b["h"] for b in row]) for row in blocks_matrix ] 114 | 115 | for i, row in enumerate(blocks_matrix): 116 | for bl in row: 117 | bl["fit"] = { 118 | 'x': curr_x, 119 | 'y': curr_y 120 | } 121 | curr_x += bl["w"] 122 | curr_y += max_heights[i] 123 | curr_x = 0 124 | 125 | 126 | def _get_blocks_per_row_estimate(self): 127 | tot_area = sum([ x["w"]*x["h"] for x in self.blocks ]) 128 | estimated_sidelen = tot_area**0.5 129 | avg_width = self._get_total_width()/len(self.blocks) 130 | return estimated_sidelen // avg_width 131 | 132 | def _get_total_width(self): 133 | return sum([ x["w"] for x in self.blocks ]) 134 | 135 | def _get_final_width(self, rows): 136 | # maximum value of total width among all the rows 137 | row_sums = [ sum([b["w"] for b in row]) for row in rows ] 138 | return max(row_sums) 139 | 140 | def _get_final_height(self, rows): 141 | # sum of the maximum heights of each row 142 | max_heights = [ max([b["h"] for b in row]) for row in rows ] 143 | return sum(max_heights) -------------------------------------------------------------------------------- /src/engine/spritesheetutils.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from engine.imgutils import pad_img 3 | from utils import spritesheet_split_cache 4 | from spriteframe import SpriteFrame 5 | from PIL import Image 6 | 7 | def get_true_frame(img , framex, framey, framew, frameh, flipx=False, flipy=False): 8 | # if framex < 0, we pad, else we crop 9 | final_frame = img 10 | if framex < 0: 11 | final_frame = pad_img(final_frame, False, 0, 0, 0, -framex) 12 | else: 13 | final_frame = final_frame.crop((framex, 0, final_frame.width, final_frame.height)) 14 | 15 | # same for framey 16 | if framey < 0: 17 | final_frame = pad_img(final_frame, False, -framey, 0, 0, 0) 18 | else: 19 | final_frame = final_frame.crop((0, framey, final_frame.width, final_frame.height)) 20 | 21 | # if framex + framew > img.width, we pad else we crop 22 | if framex + framew > img.width: 23 | final_frame = pad_img(final_frame, False, 0, framex+framew - img.width, 0, 0) 24 | else: 25 | final_frame = final_frame.crop((0, 0, framew, final_frame.height)) 26 | 27 | # same for framey + frameh > img.height 28 | if framey + frameh > img.height: 29 | final_frame = pad_img(final_frame, False, 0, 0, framey + frameh - img.height, 0) 30 | else: 31 | final_frame = final_frame.crop((0, 0, final_frame.width, frameh)) 32 | 33 | return final_frame 34 | 35 | def add_pose_numbers(frame_arr): 36 | pose_arr = [ frame.data.pose_name for frame in frame_arr ] 37 | unique_poses = list(set(pose_arr)) 38 | pose_counts = dict([ (ele, 0) for ele in unique_poses ]) 39 | new_pose_arr = list(pose_arr) 40 | for i in range(len(new_pose_arr)): 41 | pose_counts[new_pose_arr[i]] += 1 42 | new_pose_arr[i] = new_pose_arr[i] + str(pose_counts[new_pose_arr[i]] - 1).zfill(4) 43 | return new_pose_arr 44 | 45 | 46 | def split_spsh(pngpath, xmlpath, udpdatefn): 47 | # spritesheet = Image.open(pngpath) 48 | try: 49 | cleaned_xml = "" 50 | quotepairity = 0 51 | with open(xmlpath, 'r', encoding='utf-8') as f: 52 | ch = f.read(1) 53 | while ch and ch != '<': 54 | ch = f.read(1) 55 | cleaned_xml += ch 56 | while True: 57 | ch = f.read(1) 58 | if ch == '"': 59 | quotepairity = 1 - quotepairity 60 | elif (ch == '<' or ch == '>') and quotepairity == 1: 61 | ch = '<' if ch == '<' else '>' 62 | else: 63 | if not ch: 64 | break 65 | cleaned_xml += ch 66 | 67 | xmltree = ET.fromstring(cleaned_xml) # ET.parse(xmlpath) 68 | print("XML cleaned") 69 | except ET.ParseError as e: 70 | print("Error!", str(e)) 71 | return [] 72 | sprites = [] 73 | 74 | root = xmltree # .getroot() 75 | subtextures = root.findall("SubTexture") 76 | # get_true_val = lambda val: int(val) if val else None 77 | 78 | # initialize cache for this spritesheet 79 | if not spritesheet_split_cache.get(pngpath): 80 | spritesheet_split_cache[pngpath] = {} 81 | 82 | # debug: current cache 83 | print("Current cache:\n", spritesheet_split_cache) 84 | 85 | for i, subtex in enumerate(subtextures): 86 | tex_x = int(subtex.attrib['x']) 87 | tex_y = int(subtex.attrib['y']) 88 | tex_width = int(subtex.attrib['width']) 89 | tex_height = int(subtex.attrib['height']) 90 | pose_name = subtex.attrib['name'] 91 | fx = int(subtex.attrib.get("frameX", 0)) 92 | fy = int(subtex.attrib.get("frameY", 0)) 93 | fw = int(subtex.attrib.get("frameWidth", tex_width)) 94 | fh = int(subtex.attrib.get("frameHeight", tex_height)) 95 | # sprite_img = spritesheet.crop((tex_x, tex_y, tex_x+tex_width, tex_y+tex_height)).convert('RGBA') 96 | # sprite_img = sprite_img.convert('RGBA') 97 | # qim = ImageQt(sprite_img) 98 | # sprites.append((sprite_img.toqpixmap(), pose_name, tex_x, tex_y, tex_width, tex_height)) 99 | sprites.append( 100 | SpriteFrame( 101 | None, pngpath, False, pose_name, 102 | tx=tex_x, ty=tex_y, tw=tex_width, th=tex_height, 103 | framex=fx, framey=fy, framew=fw, frameh=fh 104 | ) 105 | ) 106 | udpdatefn((i+1)*50//len(subtextures), pose_name) 107 | 108 | return sprites 109 | 110 | def get_gif_frames(gifpath, updatefn=None): 111 | sprites = [] 112 | with Image.open(gifpath) as gif: 113 | for i in range(gif.n_frames): 114 | gif.seek(i) 115 | gif.save("_tmp.png") 116 | sprites.append(SpriteFrame(None, "_tmp.png", True)) 117 | if updatefn is not None: 118 | updatefn((i+1)*50//gif.n_frames, f"Adding Frame-{i+1}") 119 | 120 | return sprites 121 | -------------------------------------------------------------------------------- /src/engine/xmlpngengine.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from PIL import Image, ImageChops 3 | from os import path, linesep 4 | 5 | from utils import imghashes, g_settings, clean_filename 6 | 7 | from engine.packingalgorithms import GrowingPacker, OrderedPacker 8 | from engine.spritesheetutils import get_true_frame, add_pose_numbers 9 | from engine.imgutils import pad_img 10 | 11 | 12 | def fast_image_cmp(im1, im2): # im1 == im2 ? 13 | if im1.size != im2.size: 14 | return False 15 | if im1.tobytes() != im2.tobytes(): 16 | return False 17 | 18 | return ImageChops.difference(im1, im2).getbbox() is None 19 | 20 | def make_png_xml(frames, save_dir, character_name="Result", progressupdatefn=None, settings=None): 21 | if settings is None: 22 | settings = g_settings 23 | prefix_type = settings.get('prefix_type', 'charname') # use character name or use a custom prefix instead 24 | custom_prefix = settings.get('custom_prefix', '') # the custom prefix to use 25 | must_use_prefix = settings.get('must_use_prefix', 0) != 0 # use the custom prefix even if frame is from existing spritesheet 26 | padding_pixels = settings.get('frame_padding', 0) 27 | packing_algorithm = settings.get('packing_algo', 0) # 0 = Growing Packer, 1 = Ordered Packer 28 | # no_merge = settings.get('no_merge', 0) != 0 # no merging lookalike frames 29 | 30 | # print(len(imghashes)) 31 | # print(len(frames)) 32 | 33 | try: 34 | # init XML 35 | root = ET.Element("TextureAtlas") 36 | root.text = "\n" 37 | root.tail = linesep 38 | root.attrib['imagePath'] = f"{character_name}.png" 39 | 40 | new_pose_names = add_pose_numbers(frames) 41 | for f, pose in zip(frames, new_pose_names): 42 | final_pose_name = pose 43 | if f.data.from_single_png or (not f.data.from_single_png and f.modified): 44 | if prefix_type == 'charname': 45 | final_pose_name = f"{character_name} {final_pose_name}" 46 | elif prefix_type == 'custom': 47 | final_pose_name = f"{custom_prefix} {final_pose_name}" 48 | else: 49 | if must_use_prefix and prefix_type == 'custom': 50 | final_pose_name = f"{custom_prefix} {final_pose_name}" 51 | 52 | f.data.xml_pose_name = final_pose_name 53 | 54 | frame_dict_arr = [] 55 | current_img_hashes = set([x.data.img_hash for x in frames]) 56 | 57 | # Doesn't quite work yet, still a WIP 58 | # if no_merge: 59 | # for f in frames: 60 | # frame_dict_arr.append({ 61 | # "id": f.data.img_hash, 62 | # "w": imghashes.get(f.data.img_hash).width + 2*padding_pixels, 63 | # "h": imghashes.get(f.data.img_hash).height + 2*padding_pixels, 64 | # "frame": f # this comes in handy later on 65 | # }) 66 | # else: 67 | # pass 68 | # add the padding to width and height, then actually padding the images (kind of a hack but it works TODO: work out a better way to do this) 69 | for imhash, img in imghashes.items(): 70 | if imhash in current_img_hashes: 71 | frame_dict_arr.append({ 72 | "id": imhash, 73 | "w": img.width + 2*padding_pixels, 74 | "h": img.height + 2*padding_pixels 75 | }) 76 | 77 | if packing_algorithm == 1: 78 | packer = OrderedPacker() 79 | else: 80 | packer = GrowingPacker() 81 | frame_dict_arr.sort(key= lambda rect: rect.get("h", -100), reverse=True) 82 | 83 | packer.fit(frame_dict_arr) 84 | 85 | final_img = Image.new("RGBA", (packer.root['w'], packer.root['h']), (0, 0, 0, 0)) 86 | # frame_dict_arr.sort(key=lambda x: x['id'].img_xml_data.xml_posename) 87 | prgs = 0 88 | for r in frame_dict_arr: 89 | fit = r.get("fit") 90 | 91 | # accounting for user-defined padding 92 | imhash_img = imghashes.get(r['id']) 93 | imhash_img = pad_img(imhash_img, False, padding_pixels, padding_pixels, padding_pixels, padding_pixels) 94 | 95 | final_img.paste( imhash_img, (fit["x"], fit["y"]) ) 96 | prgs += 1 97 | progressupdatefn(prgs, "Adding images to spritesheet...") 98 | 99 | # Doesn't quite work yet, still a WIP 100 | # if no_merge: 101 | # for framedict in frame_dict_arr: 102 | # frame = framedict['frame'] 103 | # subtexture_element = ET.Element("SubTexture") 104 | # subtexture_element.tail = linesep 105 | # w, h = imghashes.get(frame.data.img_hash).size 106 | # subtexture_element.attrib = { 107 | # "name" : frame.data.xml_pose_name, 108 | # "x": str(framedict['fit']['x']), 109 | # "y": str(framedict['fit']['y']), 110 | # "width": str(w + 2*padding_pixels), 111 | # "height": str(h + 2*padding_pixels), 112 | # "frameX": str(frame.data.framex), 113 | # "frameY": str(frame.data.framey), 114 | # "frameWidth": str(frame.data.framew), 115 | # "frameHeight": str(frame.data.frameh), 116 | # } 117 | # root.append(subtexture_element) 118 | # prgs += 1 119 | # progressupdatefn(prgs, f"Saving {frame.data.xml_pose_name} to XML...") 120 | # else: 121 | # pass 122 | # convert frame_dict_arr into a dict[image_hash -> position in spritesheet]: 123 | imghash_dict = { rect['id']: (rect['fit']['x'], rect['fit']['y']) for rect in frame_dict_arr } 124 | for frame in frames: 125 | subtexture_element = ET.Element("SubTexture") 126 | subtexture_element.tail = linesep 127 | w, h = imghashes.get(frame.data.img_hash).size 128 | subtexture_element.attrib = { 129 | "name" : frame.data.xml_pose_name, 130 | "x": str(imghash_dict[frame.data.img_hash][0]), 131 | "y": str(imghash_dict[frame.data.img_hash][1]), 132 | "width": str(w + 2*padding_pixels), 133 | "height": str(h + 2*padding_pixels), 134 | "frameX": str(frame.data.framex), 135 | "frameY": str(frame.data.framey), 136 | "frameWidth": str(frame.data.framew), 137 | "frameHeight": str(frame.data.frameh), 138 | } 139 | root.append(subtexture_element) 140 | prgs += 1 141 | progressupdatefn(prgs, f"Saving {frame.data.xml_pose_name} to XML...") 142 | # im.close() 143 | print("Saving XML...") 144 | xmltree = ET.ElementTree(root) 145 | cleanpath = path.join(save_dir, clean_filename(character_name)) 146 | with open(cleanpath + ".xml", 'wb') as f: 147 | xmltree.write(f, xml_declaration=True, encoding='utf-8') 148 | 149 | print("Saving Image...") 150 | final_img = final_img.crop(final_img.getbbox()) 151 | final_img.save(cleanpath + ".png") 152 | final_img.close() 153 | 154 | print("Done!") 155 | except Exception as e: 156 | return -1, str(e) 157 | 158 | return 0, None 159 | 160 | def save_img_sequence(frames, savedir, updatefn): 161 | # Saves each frame as a png 162 | newposes = add_pose_numbers(frames) 163 | for i, (frame, pose) in enumerate(zip(frames, newposes)): 164 | try: 165 | im = imghashes.get(frame.data.img_hash) 166 | im = get_true_frame(im, frame.data.framex, frame.data.framey, frame.data.framew, frame.data.frameh) 167 | 168 | cleanpath = path.join(savedir, clean_filename(f"{pose}.png")) 169 | im.save(cleanpath) 170 | im.close() 171 | updatefn(i+1, f"Saving: {pose}.png") 172 | except Exception as e: 173 | return str(e) 174 | return None 175 | 176 | 177 | if __name__ == '__main__': 178 | print("This program is just the engine! To run the actual application, Please type: \npython xmlpngUI.py\nor \npython3 xmlpngUI.py \ndepending on what works") -------------------------------------------------------------------------------- /src/framedata.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from utils import g_settings, imghashes, spritesheet_split_cache 3 | 4 | class FrameData: 5 | def __init__(self, impath, from_single_png, pose_name, **xmlinfo): 6 | # get imgpath 7 | # get img from imgpath and ?xmlinfo 8 | # set appropriate frame stuff: depending on from_single_png 9 | # hash img 10 | # other stuff... 11 | # needed in XML table window 12 | self.imgpath = impath 13 | self.tx = None 14 | self.ty = None 15 | 16 | # w, h needed to crop img 17 | self.tw = None 18 | self.th = None 19 | self.from_single_png = from_single_png 20 | 21 | img = Image.open(impath).convert('RGBA') 22 | self.framex = 0 23 | self.framey = 0 24 | self.framew = img.width 25 | self.frameh = img.height 26 | should_clip = g_settings['isclip'] != 0 27 | cached_hash = None 28 | if not self.from_single_png: 29 | self.tx = xmlinfo.get("tx", 0) 30 | self.ty = xmlinfo.get("ty", 0) 31 | self.tw = xmlinfo.get("tw", 0) 32 | self.th = xmlinfo.get("th", 0) 33 | # impath, tex_coords (, clip) -> img 34 | # if clip == True here, then skip clip step ("if should_clip:...") 35 | # check if this img is in cache 36 | cached_hash = spritesheet_split_cache[impath].get((self.tx, self.ty, self.tw, self.th, should_clip)) 37 | if not cached_hash: 38 | # crop the image 39 | img = img.crop((self.tx, self.ty, self.tx + self.tw, self.ty + self.th)) 40 | else: 41 | # print("[DEBUG] Img found in cache!") 42 | img = imghashes.get(cached_hash) 43 | # set frame properties from xml 44 | self.framex = xmlinfo.get("framex", 0) 45 | self.framey = xmlinfo.get("framey", 0) 46 | self.framew = xmlinfo.get("framew", 0) 47 | self.frameh = xmlinfo.get("frameh", 0) 48 | 49 | # clipping the image if i didn't already find it in the cache 50 | if not cached_hash and should_clip: 51 | imbbox = img.getbbox() 52 | if imbbox: 53 | # crop img 54 | img = img.crop(imbbox) 55 | # adjust frame properties such that image can be reconstructed from them 56 | x1, y1, _, _ = imbbox 57 | self.framex -= x1 58 | self.framey -= y1 59 | # Note: frame width and height stay the same 60 | else: 61 | print("Unable to crop image!") 62 | 63 | # storing some img properties here for efficiency(kinda) 64 | self.img_width = img.width 65 | self.img_height = img.height 66 | 67 | # get hash 68 | self.img_hash = cached_hash if cached_hash else hash(img.tobytes()) 69 | # if hash isnt in imghashes then add it 70 | if self.img_hash not in imghashes: 71 | imghashes[self.img_hash] = img 72 | # cache image for re-use (only imgs from xmls are cached this way) 73 | if not self.from_single_png: 74 | spritesheet_split_cache[impath][(self.tx, self.ty, self.tw, self.th, should_clip)] = self.img_hash 75 | elif cached_hash: 76 | pass 77 | else: 78 | img.close() 79 | 80 | self.pose_name = pose_name 81 | self.xml_pose_name = "" 82 | 83 | def change_img(self, newimg): 84 | # this method will mostly only be called when flipping the images 85 | # so frame data will be unaltered 86 | self.img_width = newimg.width 87 | self.img_height = newimg.height 88 | 89 | self.img_hash = hash(newimg.tobytes()) 90 | if self.img_hash not in imghashes: 91 | imghashes[self.img_hash] = newimg 92 | else: 93 | newimg.close() 94 | 95 | # not used as of now 96 | class FrameImgData: 97 | def __init__(self, imgpath, from_single_png, **texinfo): 98 | self.imgpath = imgpath 99 | self.from_single_png = from_single_png 100 | if self.from_single_png: 101 | self.img = Image.open(imgpath) 102 | self.img_width = self.img.width 103 | self.img_height = self.img.height 104 | else: 105 | im = Image.open(imgpath) 106 | self.tx = int(texinfo.get("tx", 0)) 107 | self.ty = int(texinfo.get("ty", 0)) 108 | self.tw = int(texinfo.get("tw", 0)) 109 | self.th = int(texinfo.get("th", 0)) 110 | self.img_width = self.tw 111 | self.img_height = self.th 112 | self.img = im.crop((self.tx, self.ty, self.tx + self.tw, self.ty + self.th)) 113 | im.close() 114 | 115 | self.img_hash = hash(self.img.tobytes()) 116 | self.img.close() 117 | self.is_flip_x = False 118 | self.is_flip_y = False 119 | 120 | def __str__(self): 121 | return f"""Frame Image data: 122 | Image path: {repr(self.imgpath)} 123 | From single png: {repr(self.from_single_png)} 124 | Width: {repr(self.img_width)} 125 | Height: {repr(self.img_height)} 126 | Flip-X: {repr(self.is_flip_x)} 127 | Flip-Y: {repr(self.is_flip_y)} 128 | """ 129 | 130 | def modify_image_to(self, im): 131 | # modifies PIL image object itself (does not change imgpath though) 132 | self.img = im 133 | self.img_width = im.width 134 | self.img_height = im.height 135 | 136 | # not used as of now 137 | # class FrameXMLData: 138 | # def __init__(self, pose_name, x, y, w, h, framex, framey, framew, frameh, flipx=False, flipy=False): 139 | # self.pose_name = pose_name 140 | # self.x = x 141 | # self.y = y 142 | # self.w = w 143 | # self.h = h 144 | # self.framex = framex 145 | # self.framey = framey 146 | # self.framew = framew 147 | # self.frameh = frameh 148 | # self.is_flip_x = flipx 149 | # self.is_flip_y = flipy 150 | # self.xml_posename = None 151 | # # not exactly relevant to the xml but still 152 | # self.from_single_png = False 153 | 154 | # def convert_to_dict(self): 155 | # attribs = { 156 | # "name": self.pose_name, 157 | # "x": self.x, 158 | # "y": self.y, 159 | # "width": self.w, 160 | # "height": self.h 161 | # } 162 | 163 | # if self.framex: 164 | # attribs.update({ 165 | # "frameX": self.framex, 166 | # "frameY": self.framey, 167 | # "frameWidth": self.framew, 168 | # "frameHeight": self.frameh, 169 | # }) 170 | 171 | # return attribs 172 | 173 | # def __str__(self): 174 | # return f"""Frame XML data: 175 | # FrameX: {repr(self.framex)} 176 | # FrameY: {repr(self.framey)} 177 | # FrameWidth: {repr(self.framew)} 178 | # FrameHeight: {repr(self.frameh)} 179 | # """ -------------------------------------------------------------------------------- /src/mainUI.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'NewXMLPngUI.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_MainWindow(object): 15 | def setupUi(self, MainWindow): 16 | MainWindow.setObjectName("MainWindow") 17 | MainWindow.resize(1066, 790) 18 | self.centralwidget = QtWidgets.QWidget(MainWindow) 19 | self.centralwidget.setObjectName("centralwidget") 20 | self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) 21 | self.verticalLayout.setObjectName("verticalLayout") 22 | self.title_label = QtWidgets.QLabel(self.centralwidget) 23 | font = QtGui.QFont() 24 | font.setPointSize(16) 25 | font.setBold(True) 26 | font.setWeight(75) 27 | self.title_label.setFont(font) 28 | self.title_label.setAlignment(QtCore.Qt.AlignCenter) 29 | self.title_label.setObjectName("title_label") 30 | self.verticalLayout.addWidget(self.title_label) 31 | self.charname_input_frame = QtWidgets.QFrame(self.centralwidget) 32 | self.charname_input_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 33 | self.charname_input_frame.setFrameShadow(QtWidgets.QFrame.Raised) 34 | self.charname_input_frame.setObjectName("charname_input_frame") 35 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.charname_input_frame) 36 | self.horizontalLayout.setObjectName("horizontalLayout") 37 | self.charname_label = QtWidgets.QLabel(self.charname_input_frame) 38 | font = QtGui.QFont() 39 | font.setPointSize(10) 40 | self.charname_label.setFont(font) 41 | self.charname_label.setObjectName("charname_label") 42 | self.horizontalLayout.addWidget(self.charname_label) 43 | self.charname_textbox = QtWidgets.QLineEdit(self.charname_input_frame) 44 | font = QtGui.QFont() 45 | font.setPointSize(10) 46 | self.charname_textbox.setFont(font) 47 | self.charname_textbox.setObjectName("charname_textbox") 48 | self.horizontalLayout.addWidget(self.charname_textbox) 49 | self.verticalLayout.addWidget(self.charname_input_frame) 50 | self.myTabs = QtWidgets.QTabWidget(self.centralwidget) 51 | self.myTabs.setObjectName("myTabs") 52 | self.xmlframes_tab = QtWidgets.QWidget() 53 | self.xmlframes_tab.setObjectName("xmlframes_tab") 54 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.xmlframes_tab) 55 | self.verticalLayout_2.setObjectName("verticalLayout_2") 56 | self.container_frame = QtWidgets.QFrame(self.xmlframes_tab) 57 | self.container_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 58 | self.container_frame.setFrameShadow(QtWidgets.QFrame.Raised) 59 | self.container_frame.setObjectName("container_frame") 60 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.container_frame) 61 | self.verticalLayout_3.setObjectName("verticalLayout_3") 62 | self.frames_area = QtWidgets.QScrollArea(self.container_frame) 63 | self.frames_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 64 | self.frames_area.setWidgetResizable(True) 65 | self.frames_area.setObjectName("frames_area") 66 | self.sprite_frame_content = QtWidgets.QWidget() 67 | self.sprite_frame_content.setGeometry(QtCore.QRect(0, 0, 990, 456)) 68 | self.sprite_frame_content.setObjectName("sprite_frame_content") 69 | self.frames_area.setWidget(self.sprite_frame_content) 70 | self.verticalLayout_3.addWidget(self.frames_area) 71 | self.controls_frame = QtWidgets.QFrame(self.container_frame) 72 | self.controls_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 73 | self.controls_frame.setFrameShadow(QtWidgets.QFrame.Raised) 74 | self.controls_frame.setObjectName("controls_frame") 75 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.controls_frame) 76 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 77 | self.spsh_settings_btn = QtWidgets.QPushButton(self.controls_frame) 78 | self.spsh_settings_btn.setMinimumSize(QtCore.QSize(0, 40)) 79 | self.spsh_settings_btn.setObjectName("spsh_settings_btn") 80 | self.horizontalLayout_2.addWidget(self.spsh_settings_btn) 81 | self.posename_btn = QtWidgets.QPushButton(self.controls_frame) 82 | self.posename_btn.setMinimumSize(QtCore.QSize(0, 40)) 83 | self.posename_btn.setObjectName("posename_btn") 84 | self.horizontalLayout_2.addWidget(self.posename_btn) 85 | self.generatexml_btn = QtWidgets.QPushButton(self.controls_frame) 86 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) 87 | sizePolicy.setHorizontalStretch(0) 88 | sizePolicy.setVerticalStretch(0) 89 | sizePolicy.setHeightForWidth(self.generatexml_btn.sizePolicy().hasHeightForWidth()) 90 | self.generatexml_btn.setSizePolicy(sizePolicy) 91 | self.generatexml_btn.setMinimumSize(QtCore.QSize(0, 45)) 92 | font = QtGui.QFont() 93 | font.setBold(True) 94 | font.setWeight(75) 95 | self.generatexml_btn.setFont(font) 96 | self.generatexml_btn.setObjectName("generatexml_btn") 97 | self.horizontalLayout_2.addWidget(self.generatexml_btn) 98 | self.horizontalLayout_2.setStretch(0, 2) 99 | self.horizontalLayout_2.setStretch(1, 2) 100 | self.horizontalLayout_2.setStretch(2, 3) 101 | self.verticalLayout_3.addWidget(self.controls_frame) 102 | self.verticalLayout_2.addWidget(self.container_frame) 103 | self.myTabs.addTab(self.xmlframes_tab, "") 104 | self.icongrid_tab = QtWidgets.QWidget() 105 | self.icongrid_tab.setObjectName("icongrid_tab") 106 | self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.icongrid_tab) 107 | self.verticalLayout_4.setObjectName("verticalLayout_4") 108 | self.container_frame_icongrid = QtWidgets.QFrame(self.icongrid_tab) 109 | self.container_frame_icongrid.setFrameShape(QtWidgets.QFrame.StyledPanel) 110 | self.container_frame_icongrid.setFrameShadow(QtWidgets.QFrame.Raised) 111 | self.container_frame_icongrid.setObjectName("container_frame_icongrid") 112 | self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.container_frame_icongrid) 113 | self.verticalLayout_5.setObjectName("verticalLayout_5") 114 | self.scrollArea_2 = QtWidgets.QScrollArea(self.container_frame_icongrid) 115 | self.scrollArea_2.setWidgetResizable(True) 116 | self.scrollArea_2.setObjectName("scrollArea_2") 117 | self.scrollAreaWidgetContents_2 = QtWidgets.QWidget() 118 | self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 990, 451)) 119 | self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2") 120 | self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_2) 121 | self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) 122 | self.verticalLayout_6.setSpacing(0) 123 | self.verticalLayout_6.setObjectName("verticalLayout_6") 124 | self.icongrid_holder_label = QtWidgets.QLabel(self.scrollAreaWidgetContents_2) 125 | self.icongrid_holder_label.setText("") 126 | self.icongrid_holder_label.setObjectName("icongrid_holder_label") 127 | self.verticalLayout_6.addWidget(self.icongrid_holder_label) 128 | self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2) 129 | self.verticalLayout_5.addWidget(self.scrollArea_2) 130 | self.controls_frame_icongrid = QtWidgets.QFrame(self.container_frame_icongrid) 131 | self.controls_frame_icongrid.setFrameShape(QtWidgets.QFrame.StyledPanel) 132 | self.controls_frame_icongrid.setFrameShadow(QtWidgets.QFrame.Raised) 133 | self.controls_frame_icongrid.setObjectName("controls_frame_icongrid") 134 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.controls_frame_icongrid) 135 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 136 | self.uploadicongrid_btn = QtWidgets.QPushButton(self.controls_frame_icongrid) 137 | self.uploadicongrid_btn.setMinimumSize(QtCore.QSize(0, 50)) 138 | self.uploadicongrid_btn.setObjectName("uploadicongrid_btn") 139 | self.horizontalLayout_3.addWidget(self.uploadicongrid_btn) 140 | self.uploadicons_btn = QtWidgets.QPushButton(self.controls_frame_icongrid) 141 | self.uploadicons_btn.setMinimumSize(QtCore.QSize(0, 50)) 142 | self.uploadicons_btn.setObjectName("uploadicons_btn") 143 | self.horizontalLayout_3.addWidget(self.uploadicons_btn) 144 | self.use_psychengine_checkbox = QtWidgets.QCheckBox(self.controls_frame_icongrid) 145 | self.use_psychengine_checkbox.setEnabled(True) 146 | self.use_psychengine_checkbox.setObjectName("use_psychengine_checkbox") 147 | self.horizontalLayout_3.addWidget(self.use_psychengine_checkbox) 148 | self.iconselected_label = QtWidgets.QLabel(self.controls_frame_icongrid) 149 | self.iconselected_label.setAlignment(QtCore.Qt.AlignCenter) 150 | self.iconselected_label.setObjectName("iconselected_label") 151 | self.horizontalLayout_3.addWidget(self.iconselected_label) 152 | self.tip_label = QtWidgets.QLabel(self.controls_frame_icongrid) 153 | font = QtGui.QFont() 154 | font.setBold(True) 155 | font.setWeight(75) 156 | self.tip_label.setFont(font) 157 | self.tip_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 158 | self.tip_label.setObjectName("tip_label") 159 | self.horizontalLayout_3.addWidget(self.tip_label) 160 | self.zoom_label = QtWidgets.QLabel(self.controls_frame_icongrid) 161 | self.zoom_label.setObjectName("zoom_label") 162 | self.horizontalLayout_3.addWidget(self.zoom_label) 163 | self.generateicongrid_btn = QtWidgets.QPushButton(self.controls_frame_icongrid) 164 | self.generateicongrid_btn.setMinimumSize(QtCore.QSize(0, 50)) 165 | self.generateicongrid_btn.setObjectName("generateicongrid_btn") 166 | self.horizontalLayout_3.addWidget(self.generateicongrid_btn) 167 | self.horizontalLayout_3.setStretch(0, 1) 168 | self.horizontalLayout_3.setStretch(1, 1) 169 | self.horizontalLayout_3.setStretch(5, 1) 170 | self.horizontalLayout_3.setStretch(6, 1) 171 | self.verticalLayout_5.addWidget(self.controls_frame_icongrid) 172 | self.verticalLayout_5.setStretch(0, 10) 173 | self.verticalLayout_5.setStretch(1, 1) 174 | self.verticalLayout_4.addWidget(self.container_frame_icongrid) 175 | self.myTabs.addTab(self.icongrid_tab, "") 176 | self.verticalLayout.addWidget(self.myTabs) 177 | self.verticalLayout.setStretch(1, 1) 178 | self.verticalLayout.setStretch(2, 10) 179 | MainWindow.setCentralWidget(self.centralwidget) 180 | self.menubar = QtWidgets.QMenuBar(MainWindow) 181 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1066, 26)) 182 | self.menubar.setObjectName("menubar") 183 | self.menuFile = QtWidgets.QMenu(self.menubar) 184 | self.menuFile.setObjectName("menuFile") 185 | self.menuExport = QtWidgets.QMenu(self.menuFile) 186 | self.menuExport.setObjectName("menuExport") 187 | self.menuImport = QtWidgets.QMenu(self.menuFile) 188 | self.menuImport.setObjectName("menuImport") 189 | self.menuEdit = QtWidgets.QMenu(self.menubar) 190 | self.menuEdit.setObjectName("menuEdit") 191 | self.menuEdit_Selected_Images = QtWidgets.QMenu(self.menuEdit) 192 | self.menuEdit_Selected_Images.setObjectName("menuEdit_Selected_Images") 193 | self.menuFlip = QtWidgets.QMenu(self.menuEdit_Selected_Images) 194 | self.menuFlip.setObjectName("menuFlip") 195 | self.menuView = QtWidgets.QMenu(self.menubar) 196 | self.menuView.setObjectName("menuView") 197 | self.menuDefault_Dark_mode = QtWidgets.QMenu(self.menuView) 198 | self.menuDefault_Dark_mode.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) 199 | self.menuDefault_Dark_mode.setObjectName("menuDefault_Dark_mode") 200 | MainWindow.setMenuBar(self.menubar) 201 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 202 | self.statusbar.setObjectName("statusbar") 203 | MainWindow.setStatusBar(self.statusbar) 204 | self.action_import_existing = QtWidgets.QAction(MainWindow) 205 | self.action_import_existing.setObjectName("action_import_existing") 206 | self.actionImport_Images = QtWidgets.QAction(MainWindow) 207 | self.actionImport_Images.setObjectName("actionImport_Images") 208 | self.actionClear_Spritesheet_Grid = QtWidgets.QAction(MainWindow) 209 | self.actionClear_Spritesheet_Grid.setObjectName("actionClear_Spritesheet_Grid") 210 | self.actionEdit_Frame_Properties = QtWidgets.QAction(MainWindow) 211 | self.actionEdit_Frame_Properties.setObjectName("actionEdit_Frame_Properties") 212 | self.actionImport_icons = QtWidgets.QAction(MainWindow) 213 | self.actionImport_icons.setEnabled(False) 214 | self.actionImport_icons.setObjectName("actionImport_icons") 215 | self.actionImport_IconGrid = QtWidgets.QAction(MainWindow) 216 | self.actionImport_IconGrid.setEnabled(False) 217 | self.actionImport_IconGrid.setObjectName("actionImport_IconGrid") 218 | self.actionImport_Icons = QtWidgets.QAction(MainWindow) 219 | self.actionImport_Icons.setEnabled(False) 220 | self.actionImport_Icons.setObjectName("actionImport_Icons") 221 | self.actionClear_IconGrid = QtWidgets.QAction(MainWindow) 222 | self.actionClear_IconGrid.setEnabled(False) 223 | self.actionClear_IconGrid.setObjectName("actionClear_IconGrid") 224 | self.actionClear_Icon_selection = QtWidgets.QAction(MainWindow) 225 | self.actionClear_Icon_selection.setEnabled(False) 226 | self.actionClear_Icon_selection.setObjectName("actionClear_Icon_selection") 227 | self.actionExport_as_Spritesheet_and_XML = QtWidgets.QAction(MainWindow) 228 | self.actionExport_as_Spritesheet_and_XML.setObjectName("actionExport_as_Spritesheet_and_XML") 229 | self.actionExport_induvidual_images = QtWidgets.QAction(MainWindow) 230 | self.actionExport_induvidual_images.setObjectName("actionExport_induvidual_images") 231 | self.actionPreview_Animation = QtWidgets.QAction(MainWindow) 232 | self.actionPreview_Animation.setObjectName("actionPreview_Animation") 233 | self.actiondefault = QtWidgets.QAction(MainWindow) 234 | self.actiondefault.setCheckable(True) 235 | self.actiondefault.setChecked(True) 236 | self.actiondefault.setObjectName("actiondefault") 237 | self.actiondark_mode = QtWidgets.QAction(MainWindow) 238 | self.actiondark_mode.setCheckable(True) 239 | self.actiondark_mode.setObjectName("actiondark_mode") 240 | self.actionView_XML_structure = QtWidgets.QAction(MainWindow) 241 | self.actionView_XML_structure.setObjectName("actionView_XML_structure") 242 | self.actionDefault = QtWidgets.QAction(MainWindow) 243 | self.actionDefault.setObjectName("actionDefault") 244 | self.actionDark = QtWidgets.QAction(MainWindow) 245 | self.actionDark.setObjectName("actionDark") 246 | self.actionDefault_2 = QtWidgets.QAction(MainWindow) 247 | self.actionDefault_2.setCheckable(True) 248 | self.actionDefault_2.setChecked(True) 249 | self.actionDefault_2.setObjectName("actionDefault_2") 250 | self.actionDark_2 = QtWidgets.QAction(MainWindow) 251 | self.actionDark_2.setObjectName("actionDark_2") 252 | self.actionFlipX = QtWidgets.QAction(MainWindow) 253 | self.actionFlipX.setObjectName("actionFlipX") 254 | self.actionFlipY = QtWidgets.QAction(MainWindow) 255 | self.actionFlipY.setObjectName("actionFlipY") 256 | self.actiontets = QtWidgets.QAction(MainWindow) 257 | self.actiontets.setObjectName("actiontets") 258 | self.actionImport_from_GIF = QtWidgets.QAction(MainWindow) 259 | self.actionImport_from_GIF.setObjectName("actionImport_from_GIF") 260 | self.menuExport.addAction(self.actionExport_as_Spritesheet_and_XML) 261 | self.menuExport.addAction(self.actionExport_induvidual_images) 262 | self.menuImport.addAction(self.action_import_existing) 263 | self.menuImport.addAction(self.actionImport_Images) 264 | self.menuImport.addAction(self.actionImport_from_GIF) 265 | self.menuFile.addAction(self.menuImport.menuAction()) 266 | self.menuFile.addSeparator() 267 | self.menuFile.addAction(self.actionImport_IconGrid) 268 | self.menuFile.addAction(self.actionImport_Icons) 269 | self.menuFile.addSeparator() 270 | self.menuFile.addAction(self.menuExport.menuAction()) 271 | self.menuFlip.addAction(self.actionFlipX) 272 | self.menuFlip.addAction(self.actionFlipY) 273 | self.menuEdit_Selected_Images.addAction(self.menuFlip.menuAction()) 274 | self.menuEdit.addAction(self.actionClear_Spritesheet_Grid) 275 | self.menuEdit.addAction(self.menuEdit_Selected_Images.menuAction()) 276 | self.menuEdit.addSeparator() 277 | self.menuEdit.addAction(self.actionClear_IconGrid) 278 | self.menuEdit.addAction(self.actionClear_Icon_selection) 279 | self.menuView.addAction(self.actionPreview_Animation) 280 | self.menuView.addAction(self.actionView_XML_structure) 281 | self.menuView.addSeparator() 282 | self.menuView.addAction(self.menuDefault_Dark_mode.menuAction()) 283 | self.menubar.addAction(self.menuFile.menuAction()) 284 | self.menubar.addAction(self.menuEdit.menuAction()) 285 | self.menubar.addAction(self.menuView.menuAction()) 286 | 287 | self.retranslateUi(MainWindow) 288 | self.myTabs.setCurrentIndex(0) 289 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 290 | 291 | def retranslateUi(self, MainWindow): 292 | _translate = QtCore.QCoreApplication.translate 293 | MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) 294 | self.title_label.setText(_translate("MainWindow", "Spritesheet XML Generator for Friday Night Funkin\'")) 295 | self.charname_label.setText(_translate("MainWindow", "Character Name:")) 296 | self.spsh_settings_btn.setText(_translate("MainWindow", "Spritesheet\n" 297 | "Generation Settings")) 298 | self.posename_btn.setText(_translate("MainWindow", "Set Animation (Pose) Name")) 299 | self.generatexml_btn.setText(_translate("MainWindow", "Generate XML")) 300 | self.myTabs.setTabText(self.myTabs.indexOf(self.xmlframes_tab), _translate("MainWindow", "XML from Frames")) 301 | self.uploadicongrid_btn.setText(_translate("MainWindow", "Upload Icon-grid")) 302 | self.uploadicons_btn.setText(_translate("MainWindow", "Upload Icons")) 303 | self.use_psychengine_checkbox.setText(_translate("MainWindow", "Psych Engine mode")) 304 | self.iconselected_label.setText(_translate("MainWindow", "No. of\n" 305 | "icons selected:\n" 306 | "0")) 307 | self.tip_label.setText(_translate("MainWindow", "Tip: Use ctrl+i and ctrl+o to zoom in or out respectively")) 308 | self.zoom_label.setText(_translate("MainWindow", "Zoom:")) 309 | self.generateicongrid_btn.setText(_translate("MainWindow", "Generate New\n" 310 | "Icon-grid")) 311 | self.myTabs.setTabText(self.myTabs.indexOf(self.icongrid_tab), _translate("MainWindow", "Add Icons to Icon-grid")) 312 | self.menuFile.setTitle(_translate("MainWindow", "File")) 313 | self.menuExport.setTitle(_translate("MainWindow", "Export...")) 314 | self.menuImport.setTitle(_translate("MainWindow", "Import...")) 315 | self.menuEdit.setTitle(_translate("MainWindow", "Edit")) 316 | self.menuEdit_Selected_Images.setTitle(_translate("MainWindow", "Edit Selected Images")) 317 | self.menuFlip.setTitle(_translate("MainWindow", "Flip")) 318 | self.menuView.setTitle(_translate("MainWindow", "View")) 319 | self.menuDefault_Dark_mode.setTitle(_translate("MainWindow", "Theme")) 320 | self.action_import_existing.setText(_translate("MainWindow", "Import existing Spritesheet and XML")) 321 | self.actionImport_Images.setText(_translate("MainWindow", "Import Images....")) 322 | self.actionClear_Spritesheet_Grid.setText(_translate("MainWindow", "Clear Spritesheet Grid")) 323 | self.actionEdit_Frame_Properties.setText(_translate("MainWindow", "Edit Frame Properties")) 324 | self.actionImport_icons.setText(_translate("MainWindow", "Import icons")) 325 | self.actionImport_IconGrid.setText(_translate("MainWindow", "Import IconGrid")) 326 | self.actionImport_Icons.setText(_translate("MainWindow", "Import Icons")) 327 | self.actionClear_IconGrid.setText(_translate("MainWindow", "Clear IconGrid")) 328 | self.actionClear_Icon_selection.setText(_translate("MainWindow", "Clear Icon selection")) 329 | self.actionExport_as_Spritesheet_and_XML.setText(_translate("MainWindow", "Export as Spritesheet and XML")) 330 | self.actionExport_induvidual_images.setText(_translate("MainWindow", "Export individual images")) 331 | self.actionPreview_Animation.setText(_translate("MainWindow", "Preview Animation")) 332 | self.actiondefault.setText(_translate("MainWindow", "Default")) 333 | self.actiondark_mode.setText(_translate("MainWindow", "Dark mode")) 334 | self.actionView_XML_structure.setText(_translate("MainWindow", "View XML structure")) 335 | self.actionDefault.setText(_translate("MainWindow", "Default")) 336 | self.actionDark.setText(_translate("MainWindow", "Dark")) 337 | self.actionDefault_2.setText(_translate("MainWindow", "Default")) 338 | self.actionDark_2.setText(_translate("MainWindow", "Dark")) 339 | self.actionFlipX.setText(_translate("MainWindow", "FlipX")) 340 | self.actionFlipY.setText(_translate("MainWindow", "FlipY")) 341 | self.actiontets.setText(_translate("MainWindow", "tets")) 342 | self.actionImport_from_GIF.setText(_translate("MainWindow", "Import from GIF")) 343 | -------------------------------------------------------------------------------- /src/settingswindow.py: -------------------------------------------------------------------------------- 1 | import spritesheetgensettings 2 | from PyQt5.QtWidgets import QWidget 3 | from utils import g_settings 4 | 5 | class SettingsWindow(QWidget): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.ui = spritesheetgensettings.Ui_Form() 9 | self.ui.setupUi(self) 10 | 11 | self.ui.packingalgo_combobox.addItems([ 12 | "Growing Packer (Fits the frames as tightly as possible but doesn't maintain frame ordering)", 13 | "Ordered Packer (Fits the frames in the order they were added but produces a slightly bigger spritesheet)" 14 | ]) 15 | self.ui.packingalgo_combobox.setCurrentIndex(0) 16 | # self.setStyleSheet(get_stylesheet_from_file("app-styles.qss")) 17 | 18 | # self.isclip = self.ui.clip_checkbox.checkState() 19 | # self.prefix_type = 'custom' if self.ui.custom_prefix_radiobtn.isChecked() else 'charname' 20 | # self.custom_prefix = self.ui.custom_prefix_text.text() 21 | # self.must_use_prefix = self.ui.insist_prefix_checkbox.checkState() 22 | self.saveSettings(False) 23 | 24 | self.ui.custom_prefix_radiobtn.toggled.connect(lambda is_toggled: self.ui.custom_prefix_text.setEnabled(is_toggled)) 25 | self.ui.save_settings_btn.clicked.connect(lambda: self.saveSettings()) # make sure event related parameters don't get accidentally sent to self.saveSettings 26 | self.ui.settings_cancel_btn.clicked.connect(self.restoreToNormal) 27 | 28 | # hide the no_merge checkbox for now as it is a WIP 29 | self.ui.no_merge_checkbox.setVisible(False) 30 | 31 | def _get_prefix_type(self): 32 | if self.ui.custom_prefix_radiobtn.isChecked(): 33 | return 'custom' 34 | elif self.ui.charname_first_radiobtn.isChecked(): 35 | return 'charname' 36 | elif self.ui.no_prefix_radiobtn.isChecked(): 37 | return 'noprefix' 38 | 39 | def _set_radiobuttons(self): 40 | self.ui.custom_prefix_radiobtn.setChecked(self.prefix_type == 'custom') 41 | self.ui.charname_first_radiobtn.setChecked(self.prefix_type == 'charname') 42 | self.ui.no_prefix_radiobtn.setChecked(self.prefix_type == 'noprefix') 43 | 44 | def restoreToNormal(self): 45 | self.ui.clip_checkbox.setCheckState(self.isclip) 46 | self._set_radiobuttons() 47 | self.ui.custom_prefix_text.setText(self.custom_prefix) 48 | self.ui.insist_prefix_checkbox.setCheckState(self.must_use_prefix) 49 | self.ui.frame_padding_spinbox.setValue(self.frame_padding) 50 | self.ui.packingalgo_combobox.setCurrentIndex(self.packing_algo) 51 | # self.ui.no_merge_checkbox.setCheckState(self.no_merge) 52 | self.close() 53 | 54 | def saveSettings(self, shouldclose=True): 55 | self.isclip = self.ui.clip_checkbox.checkState() 56 | # self.prefix_type = 'custom' if self.ui.custom_prefix_radiobtn.isChecked() else 'charname' 57 | self.prefix_type = self._get_prefix_type() 58 | self.custom_prefix = self.ui.custom_prefix_text.text() 59 | self.must_use_prefix = self.ui.insist_prefix_checkbox.checkState() 60 | self.frame_padding = self.ui.frame_padding_spinbox.value() 61 | self.packing_algo = self.ui.packingalgo_combobox.currentIndex() 62 | # self.no_merge = self.ui.no_merge_checkbox.checkState() 63 | # saving to global settings obj 64 | g_settings['isclip'] = self.isclip 65 | g_settings['prefix_type'] = self.prefix_type 66 | g_settings['custom_prefix'] = self.custom_prefix 67 | g_settings['must_use_prefix'] = self.must_use_prefix 68 | g_settings['frame_padding'] = self.frame_padding 69 | g_settings['packing_algo'] = self.packing_algo 70 | # g_settings['no_merge'] = self.no_merge 71 | if shouldclose: 72 | self.close() 73 | 74 | def closeEvent(self, a0): 75 | self.restoreToNormal() 76 | # return super().closeEvent(a0) -------------------------------------------------------------------------------- /src/spriteframe.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from PyQt5.QtCore import QSize, Qt 3 | from PyQt5.QtGui import QIcon 4 | from PyQt5.QtWidgets import QCheckBox, QFrame, QPushButton, QWidget, QLabel, QApplication 5 | from framedata import FrameData 6 | from utils import SPRITEFRAME_SIZE, imghashes 7 | from os import path 8 | 9 | 10 | class SpriteFrame(QWidget): 11 | def __init__(self, parent, imgpath, from_single_png = True, posename = "", **texinfo): 12 | super().__init__(parent) 13 | self._frameparent = parent 14 | 15 | # non-ui stuff 16 | fromsinglepng = from_single_png 17 | # if from_single_png is None: 18 | # fromsinglepng = True 19 | 20 | # "calculate" the pose_name 21 | first_num_index = 0 22 | if not fromsinglepng: 23 | first_num_index = len(posename) 24 | for i in range(len(posename)-1, 0, -1): 25 | if posename[i].isnumeric(): 26 | first_num_index = i 27 | else: 28 | break 29 | true_pname = "idle" if fromsinglepng else posename[:first_num_index] 30 | 31 | self.data = FrameData(imgpath, fromsinglepng, true_pname, **texinfo) 32 | self.modified = False 33 | 34 | # ui stuff 35 | self.image_pixmap = imghashes.get(self.data.img_hash).toqpixmap() 36 | self.myframe = QFrame(self) 37 | self.img_label = QLabel(self.myframe) 38 | 39 | self.img_label.setPixmap(self.image_pixmap.scaled(SPRITEFRAME_SIZE, SPRITEFRAME_SIZE)) 40 | 41 | self.setFixedSize(QSize(SPRITEFRAME_SIZE, SPRITEFRAME_SIZE)) 42 | 43 | self.remove_btn = QPushButton(self.myframe) 44 | self.remove_btn.move(90, 90) 45 | self.remove_btn.setIcon(QIcon('./assets/remove-frame-icon.svg')) 46 | self.remove_btn.setIconSize(QSize(40, 40)) 47 | self.remove_btn.setFixedSize(40, 40) 48 | self.remove_btn.setToolTip("Delete Frame") 49 | self.remove_btn.clicked.connect(lambda: self.remove_self(self.frameparent)) 50 | 51 | self.select_checkbox = QCheckBox(self.myframe) 52 | self.select_checkbox.move(5, 5) 53 | self.select_checkbox.stateChanged.connect(lambda : self.add_to_selected_arr(self.frameparent)) 54 | 55 | self.current_border_color = "black" 56 | self.myframe.setStyleSheet("QFrame{border-style:solid; border-color:" + self.current_border_color + "; border-width:2px}") 57 | 58 | @property 59 | def frameparent(self): 60 | return self._frameparent 61 | 62 | @frameparent.setter 63 | def frameparent(self, newparent): 64 | self._frameparent = newparent 65 | self.setParent(self._frameparent) 66 | 67 | # overriding the default mousePressEvent 68 | def mousePressEvent(self, event): 69 | btnpressed = event.button() 70 | if btnpressed == 1: # left mouse button 71 | prevstate = self.select_checkbox.checkState() 72 | newstate = 0 if prevstate != 0 else 1 73 | self.select_checkbox.setChecked(newstate) 74 | modifiers = QApplication.keyboardModifiers() 75 | if modifiers == Qt.ShiftModifier: 76 | self.frameparent.ranged_selection_handler(self) 77 | else: 78 | modifiers = QApplication.keyboardModifiers() 79 | if modifiers == Qt.ShiftModifier: 80 | self.frameparent.ranged_deletion_handler(self) 81 | 82 | # overriding the default enterEvent 83 | def enterEvent(self, event): 84 | self.myframe.setStyleSheet("QFrame{ border-style:solid; border-color:#FFC9DEF5; border-width:4px }") 85 | 86 | # overriding the default leaveEvent 87 | def leaveEvent(self, event): 88 | self.myframe.setStyleSheet("QFrame{border-style:solid; border-color:" + self.current_border_color + "; border-width:2px}") 89 | 90 | def remove_self(self, parent): 91 | parent.labels.remove(self) 92 | if self in parent.selected_labels: 93 | parent.selected_labels.remove(self) 94 | parent.num_labels -= 1 95 | 96 | parent.frames_layout.removeWidget(self) 97 | # parent.update_frame_dict(self.img_xml_data.pose_name, self, remove=True) 98 | self.deleteLater() 99 | 100 | parent.re_render_grid() 101 | # print("Deleting image, count: ", parent.num_labels, "Len of labels", len(parent.labels)) 102 | if len(parent.labels) == 0: 103 | parent.ui.posename_btn.setDisabled(True) 104 | parent.ui.actionPreview_Animation.setEnabled(False) 105 | parent.ui.actionView_XML_structure.setEnabled(False) 106 | # parent.ui.actionChange_Frame_Ordering.setEnabled(False) 107 | 108 | def add_to_selected_arr(self, parent): 109 | if self.select_checkbox.checkState() == 0: 110 | parent.selected_labels.remove(self) 111 | self.current_border_color = "black" 112 | self.myframe.setStyleSheet("QFrame{border-style:solid; border-color:" + self.current_border_color + "; border-width:2px}") 113 | else: 114 | parent.selected_labels.append(self) 115 | self.current_border_color = "green" 116 | self.myframe.setStyleSheet("QFrame{border-style:solid; border-color:" + self.current_border_color + "; border-width:2px}") 117 | 118 | parent.ui.actionEdit_Frame_Properties.setDisabled(len(parent.selected_labels) <= 0) 119 | 120 | def get_tooltip_string(self, parent): 121 | charname = parent.ui.charname_textbox.text() 122 | charname = charname.strip() if charname.strip() != "" else "[ENTER YOUR CHARACTER NAME]" 123 | inside_subtex_name = f"{charname} {self.data.pose_name}####" if self.data.from_single_png or self.modified else f"{self.data.pose_name}####" 124 | 125 | ttstring = f'''Image: {path.basename(self.data.imgpath)} 126 | Current Pose: {self.data.pose_name} 127 | Will appear in XML as: 128 | \t 129 | \t# = digit from 0-9''' 130 | 131 | return ttstring 132 | 133 | def flip_img(self, dxn): 134 | # flip the PIL img of self 135 | if dxn == 'X': 136 | img = imghashes.get(self.data.img_hash).transpose(Image.FLIP_LEFT_RIGHT) 137 | elif dxn == 'Y': 138 | img = imghashes.get(self.data.img_hash).transpose(Image.FLIP_TOP_BOTTOM) 139 | else: 140 | print("Something went wrong!") 141 | 142 | # change hash accordingly 143 | self.data.change_img(img) 144 | 145 | # do pixmap stuff 146 | # Note: the above fn could have closed the img so pass the hash instead 147 | self.change_ui_img(self.data.img_hash) 148 | 149 | def change_ui_img(self, newimghash): 150 | self.image_pixmap = imghashes.get(newimghash).toqpixmap() 151 | self.img_label.setPixmap(self.image_pixmap.scaled(SPRITEFRAME_SIZE, SPRITEFRAME_SIZE)) 152 | 153 | 154 | if __name__ == '__main__': 155 | print("To run the actual application, Please type: \npython xmlpngUI.py\nor \npython3 xmlpngUI.py \ndepending on what works") -------------------------------------------------------------------------------- /src/spritesheetgensettings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'SpritesheetGenSettings.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_Form(object): 15 | def setupUi(self, Form): 16 | Form.setObjectName("Form") 17 | Form.setWindowModality(QtCore.Qt.ApplicationModal) 18 | Form.resize(603, 552) 19 | self.verticalLayout_3 = QtWidgets.QVBoxLayout(Form) 20 | self.verticalLayout_3.setObjectName("verticalLayout_3") 21 | self.clip_checkbox = QtWidgets.QCheckBox(Form) 22 | font = QtGui.QFont() 23 | font.setPointSize(9) 24 | self.clip_checkbox.setFont(font) 25 | self.clip_checkbox.setChecked(True) 26 | self.clip_checkbox.setObjectName("clip_checkbox") 27 | self.verticalLayout_3.addWidget(self.clip_checkbox) 28 | self.frame = QtWidgets.QFrame(Form) 29 | self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 30 | self.frame.setFrameShadow(QtWidgets.QFrame.Raised) 31 | self.frame.setObjectName("frame") 32 | self.verticalLayout = QtWidgets.QVBoxLayout(self.frame) 33 | self.verticalLayout.setObjectName("verticalLayout") 34 | self.label = QtWidgets.QLabel(self.frame) 35 | font = QtGui.QFont() 36 | font.setPointSize(9) 37 | self.label.setFont(font) 38 | self.label.setObjectName("label") 39 | self.verticalLayout.addWidget(self.label) 40 | self.charname_first_radiobtn = QtWidgets.QRadioButton(self.frame) 41 | font = QtGui.QFont() 42 | font.setPointSize(8) 43 | self.charname_first_radiobtn.setFont(font) 44 | self.charname_first_radiobtn.setChecked(True) 45 | self.charname_first_radiobtn.setObjectName("charname_first_radiobtn") 46 | self.verticalLayout.addWidget(self.charname_first_radiobtn) 47 | self.custom_prefix_radiobtn = QtWidgets.QRadioButton(self.frame) 48 | font = QtGui.QFont() 49 | font.setPointSize(8) 50 | self.custom_prefix_radiobtn.setFont(font) 51 | self.custom_prefix_radiobtn.setObjectName("custom_prefix_radiobtn") 52 | self.verticalLayout.addWidget(self.custom_prefix_radiobtn) 53 | self.frame_2 = QtWidgets.QFrame(self.frame) 54 | self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) 55 | self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) 56 | self.frame_2.setObjectName("frame_2") 57 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame_2) 58 | self.verticalLayout_2.setSpacing(0) 59 | self.verticalLayout_2.setObjectName("verticalLayout_2") 60 | self.label_2 = QtWidgets.QLabel(self.frame_2) 61 | font = QtGui.QFont() 62 | font.setPointSize(8) 63 | self.label_2.setFont(font) 64 | self.label_2.setObjectName("label_2") 65 | self.verticalLayout_2.addWidget(self.label_2) 66 | self.custom_prefix_text = QtWidgets.QLineEdit(self.frame_2) 67 | self.custom_prefix_text.setEnabled(False) 68 | self.custom_prefix_text.setObjectName("custom_prefix_text") 69 | self.verticalLayout_2.addWidget(self.custom_prefix_text) 70 | self.verticalLayout.addWidget(self.frame_2) 71 | self.no_prefix_radiobtn = QtWidgets.QRadioButton(self.frame) 72 | self.no_prefix_radiobtn.setObjectName("no_prefix_radiobtn") 73 | self.verticalLayout.addWidget(self.no_prefix_radiobtn) 74 | self.verticalLayout_3.addWidget(self.frame) 75 | self.insist_prefix_checkbox = QtWidgets.QCheckBox(Form) 76 | font = QtGui.QFont() 77 | font.setPointSize(8) 78 | self.insist_prefix_checkbox.setFont(font) 79 | self.insist_prefix_checkbox.setObjectName("insist_prefix_checkbox") 80 | self.verticalLayout_3.addWidget(self.insist_prefix_checkbox) 81 | self.no_merge_checkbox = QtWidgets.QCheckBox(Form) 82 | self.no_merge_checkbox.setObjectName("no_merge_checkbox") 83 | self.verticalLayout_3.addWidget(self.no_merge_checkbox) 84 | self.frame_4 = QtWidgets.QFrame(Form) 85 | self.frame_4.setFrameShape(QtWidgets.QFrame.StyledPanel) 86 | self.frame_4.setFrameShadow(QtWidgets.QFrame.Raised) 87 | self.frame_4.setObjectName("frame_4") 88 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.frame_4) 89 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 90 | self.frame_padding_spinbox = QtWidgets.QSpinBox(self.frame_4) 91 | self.frame_padding_spinbox.setMaximum(20) 92 | self.frame_padding_spinbox.setObjectName("frame_padding_spinbox") 93 | self.horizontalLayout_2.addWidget(self.frame_padding_spinbox) 94 | self.frame_padding_label = QtWidgets.QLabel(self.frame_4) 95 | self.frame_padding_label.setObjectName("frame_padding_label") 96 | self.horizontalLayout_2.addWidget(self.frame_padding_label) 97 | self.horizontalLayout_2.setStretch(1, 1) 98 | self.verticalLayout_3.addWidget(self.frame_4) 99 | self.frame_5 = QtWidgets.QFrame(Form) 100 | self.frame_5.setFrameShape(QtWidgets.QFrame.StyledPanel) 101 | self.frame_5.setFrameShadow(QtWidgets.QFrame.Raised) 102 | self.frame_5.setObjectName("frame_5") 103 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.frame_5) 104 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 105 | self.packingalgo_label = QtWidgets.QLabel(self.frame_5) 106 | self.packingalgo_label.setObjectName("packingalgo_label") 107 | self.horizontalLayout_3.addWidget(self.packingalgo_label) 108 | self.packingalgo_combobox = QtWidgets.QComboBox(self.frame_5) 109 | self.packingalgo_combobox.setObjectName("packingalgo_combobox") 110 | self.horizontalLayout_3.addWidget(self.packingalgo_combobox) 111 | self.horizontalLayout_3.setStretch(1, 1) 112 | self.verticalLayout_3.addWidget(self.frame_5) 113 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) 114 | self.verticalLayout_3.addItem(spacerItem) 115 | self.frame_3 = QtWidgets.QFrame(Form) 116 | self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) 117 | self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) 118 | self.frame_3.setObjectName("frame_3") 119 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame_3) 120 | self.horizontalLayout.setSpacing(30) 121 | self.horizontalLayout.setObjectName("horizontalLayout") 122 | self.save_settings_btn = QtWidgets.QPushButton(self.frame_3) 123 | self.save_settings_btn.setMinimumSize(QtCore.QSize(0, 35)) 124 | font = QtGui.QFont() 125 | font.setPointSize(9) 126 | self.save_settings_btn.setFont(font) 127 | self.save_settings_btn.setObjectName("save_settings_btn") 128 | self.horizontalLayout.addWidget(self.save_settings_btn) 129 | self.settings_cancel_btn = QtWidgets.QPushButton(self.frame_3) 130 | self.settings_cancel_btn.setMinimumSize(QtCore.QSize(0, 35)) 131 | font = QtGui.QFont() 132 | font.setPointSize(9) 133 | self.settings_cancel_btn.setFont(font) 134 | self.settings_cancel_btn.setObjectName("settings_cancel_btn") 135 | self.horizontalLayout.addWidget(self.settings_cancel_btn) 136 | self.verticalLayout_3.addWidget(self.frame_3) 137 | 138 | self.retranslateUi(Form) 139 | QtCore.QMetaObject.connectSlotsByName(Form) 140 | 141 | def retranslateUi(self, Form): 142 | _translate = QtCore.QCoreApplication.translate 143 | Form.setWindowTitle(_translate("Form", "Spritesheet Generation Settings")) 144 | self.clip_checkbox.setText(_translate("Form", "Clip to bounding box (applies to every frame you add after this box checked)")) 145 | self.label.setText(_translate("Form", "Animation Name prefixing:")) 146 | self.charname_first_radiobtn.setText(_translate("Form", "Add Character Name \n" 147 | "Before Animation Prefix")) 148 | self.custom_prefix_radiobtn.setText(_translate("Form", "Custom general animation prefix")) 149 | self.label_2.setText(_translate("Form", "Custom Prefix:")) 150 | self.no_prefix_radiobtn.setText(_translate("Form", "Don\'t use any prefix (what you type in the prefix box is exactly what will show up in the XML)")) 151 | self.insist_prefix_checkbox.setText(_translate("Form", "Use Prefix even if frame is imported from existing XML")) 152 | self.no_merge_checkbox.setText(_translate("Form", "Do not merge look-alike frames\n" 153 | "(WARNING: Can cause extremely large spritesheets which may cause windows to\n" 154 | "refuse to open them.\n" 155 | "May also cause crashes!)")) 156 | self.frame_padding_spinbox.setSuffix(_translate("Form", "px")) 157 | self.frame_padding_label.setText(_translate("Form", "Frame Padding (use this to add empty pixels to the edge of each frame, helps prevent \n" 158 | " sprites clipping into each other)")) 159 | self.packingalgo_label.setText(_translate("Form", "Packing Algorithm")) 160 | self.save_settings_btn.setText(_translate("Form", "Save Settings")) 161 | self.settings_cancel_btn.setText(_translate("Form", "Cancel")) 162 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QMessageBox 2 | 3 | SPRITEFRAME_SIZE = 128 4 | imghashes = {} # dict[Int(hash) -> PIL.Image object] 5 | spritesheet_split_cache = {} # dict[str(spritesheet_path) -> dict[ (x,y,w,h, clipped)-> int(hash) ] ] 6 | g_settings = { 7 | "isclip": 1, 8 | "prefix_type": "charname", 9 | "custom_prefix": "", 10 | "must_use_prefix": 0 11 | } # dict containing all settings (check settingswindow.py) 12 | 13 | def get_stylesheet_from_file(filename): 14 | with open(filename, 'r') as f: 15 | style = f.read() 16 | return style 17 | 18 | def temp_path_shortener(pathstr): 19 | if '/' in pathstr: 20 | return "/".join(pathstr.split('/')[-2:]) 21 | else: 22 | return pathstr 23 | 24 | def display_msg_box(parent, window_title="MessageBox", text="Text Here", icon=None): 25 | msgbox = QMessageBox(parent) 26 | msgbox.setWindowTitle(window_title) 27 | msgbox.setText(text) 28 | if not icon: 29 | msgbox.setIcon(QMessageBox.Information) 30 | else: 31 | msgbox.setIcon(icon) 32 | x = msgbox.exec_() 33 | print("[DEBUG] Exit status of msgbox: "+str(x)) 34 | 35 | def parse_value(val, exceptions=None, fallback=0, dtype=int): 36 | if exceptions is None: 37 | exceptions = dict() 38 | 39 | if val in exceptions.keys(): 40 | return exceptions.get(val) 41 | else: 42 | try: 43 | return dtype(val) 44 | except Exception as e: 45 | print("Could not convert into required type") 46 | print(e) 47 | return fallback 48 | 49 | def clean_filename(filename): 50 | replacers = { 51 | '\\': '_backslash_', 52 | '/': '_fwdslash_', 53 | ':': '_colon_', 54 | '*': '_asterisk_', 55 | '?': '_questionmark_', 56 | '"': '_quot_', 57 | '<': '_lt_', 58 | '>': '_gt_', 59 | '|': '_pipe_' 60 | } 61 | for ch, replch in replacers.items(): 62 | filename = filename.replace(ch, replch) 63 | return filename 64 | 65 | if __name__ == '__main__': 66 | print("To run the actual application, Please type: \npython xmlpngUI.py\nor \npython3 xmlpngUI.py \ndepending on what works") -------------------------------------------------------------------------------- /src/xmlpngUI.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5.QtCore import QSize, Qt 3 | from PyQt5.QtGui import QIcon, QPixmap 4 | from PyQt5.QtWidgets import QAction, QActionGroup, QApplication, QGridLayout, QInputDialog, QLineEdit, QMainWindow, QMessageBox, QProgressDialog, QPushButton, QSpacerItem, QLabel, QFileDialog 5 | from os import path 6 | from animationwindow import AnimationView 7 | import engine.icongridutils as icongridutils 8 | import engine.spritesheetutils as spritesheetutils 9 | # from frameorderscreen import FrameOrderScreen 10 | from xmltablewindow import XMLTableView 11 | import json 12 | 13 | import engine.xmlpngengine as xmlpngengine 14 | from mainUI import Ui_MainWindow 15 | from spriteframe import SpriteFrame 16 | from utils import SPRITEFRAME_SIZE, get_stylesheet_from_file 17 | from settingswindow import SettingsWindow 18 | 19 | 20 | def display_progress_bar(parent, title="Sample text", startlim=0, endlim=100): 21 | def update_prog_bar(progress, progresstext): 22 | progbar.setValue(progress) 23 | progbar.setLabel(QLabel(progresstext)) 24 | progbar = QProgressDialog(title, None, startlim, endlim, parent) 25 | progbar.setWindowModality(Qt.WindowModal) 26 | progbar.show() 27 | 28 | return update_prog_bar, progbar 29 | 30 | def set_preferences(prefdict): 31 | try: 32 | with open('preferences.json', 'w') as f: 33 | json.dump(prefdict, f) 34 | except Exception as e: 35 | with open("error.log", 'a') as errlog: 36 | errlog.write(str(e)) 37 | 38 | class MyApp(QMainWindow): 39 | def __init__(self, prefs): 40 | super().__init__() 41 | 42 | self.ui = Ui_MainWindow() 43 | self.ui.setupUi(self) 44 | self.setWindowTitle("XML Generator") 45 | 46 | self.ui.generatexml_btn.clicked.connect(self.generate_xml) 47 | self.ui.actionExport_as_Spritesheet_and_XML.triggered.connect(self.generate_xml) 48 | self.ui.actionExport_induvidual_images.triggered.connect(self.export_bunch_of_imgs) 49 | self.ui.frames_area.setWidgetResizable(True) 50 | self.frames_layout = QGridLayout(self.ui.sprite_frame_content) 51 | self.ui.frames_area.setWidget(self.ui.sprite_frame_content) 52 | 53 | self.num_labels = 0 54 | self.labels = [] 55 | self.selected_labels = [] 56 | # self.frame_dict = {} # dict< pose_name: str -> frames: list[SpriteFrame] > 57 | 58 | self.add_img_button = QPushButton() 59 | self.add_img_button.setIcon(QIcon("./assets/AddImg.png")) 60 | self.add_img_button.setGeometry(0, 0, SPRITEFRAME_SIZE, SPRITEFRAME_SIZE) 61 | self.add_img_button.setFixedSize(QSize(SPRITEFRAME_SIZE, SPRITEFRAME_SIZE)) 62 | self.add_img_button.setIconSize(QSize(SPRITEFRAME_SIZE, SPRITEFRAME_SIZE)) 63 | self.add_img_button.clicked.connect(self.open_frame_imgs) 64 | 65 | self.frames_layout.addWidget(self.add_img_button, 0, 0, Qt.AlignmentFlag(0x1|0x20)) 66 | self.ui.myTabs.setCurrentIndex(0) 67 | 68 | self.setWindowIcon(QIcon("./assets/appicon.png")) 69 | self.icongrid_zoom = 1 70 | self.ui.uploadicongrid_btn.clicked.connect(self.uploadIconGrid) 71 | self.ui.actionImport_IconGrid.triggered.connect(self.uploadIconGrid) 72 | self.ui.generateicongrid_btn.clicked.connect(self.getNewIconGrid) 73 | self.ui.uploadicons_btn.clicked.connect(self.appendIcon) 74 | self.ui.actionImport_Icons.triggered.connect(self.appendIcon) 75 | self.ui.actionClear_IconGrid.triggered.connect(self.clearIconGrid) 76 | self.ui.actionClear_Icon_selection.triggered.connect(self.clearSelectedIcons) 77 | 78 | self.action_zoom_in = QAction(self.ui.icongrid_holder_label) 79 | self.ui.icongrid_holder_label.addAction(self.action_zoom_in) 80 | self.action_zoom_in.triggered.connect(self.zoomInPixmap) 81 | self.action_zoom_in.setShortcut("Ctrl+i") 82 | 83 | self.action_zoom_out = QAction(self.ui.icongrid_holder_label) 84 | self.ui.icongrid_holder_label.addAction(self.action_zoom_out) 85 | self.action_zoom_out.triggered.connect(self.zoomOutPixmap) 86 | self.action_zoom_out.setShortcut("Ctrl+o") 87 | 88 | self.ui.zoom_label.setText("Zoom: 100%") 89 | 90 | self.iconpaths = [] 91 | self.icongrid_path = "" 92 | 93 | self.ui.posename_btn.clicked.connect(self.setAnimationNames) 94 | self.ui.posename_btn.setDisabled(True) 95 | self.ui.charname_textbox.textChanged.connect(self.onCharacterNameChange) 96 | 97 | self.num_cols = 6 98 | self.num_rows = 1 99 | 100 | self.ui.actionImport_Images.triggered.connect(self.open_frame_imgs) 101 | self.ui.action_import_existing.triggered.connect(self.open_existing_spsh_xml) 102 | self.ui.actionImport_from_GIF.triggered.connect(self.open_gif) 103 | 104 | self.num_rows = 1 + self.num_labels//self.num_cols 105 | 106 | for i in range(self.num_cols): 107 | self.frames_layout.setColumnMinimumWidth(i, 0) 108 | self.frames_layout.setColumnStretch(i, 0) 109 | for i in range(self.num_rows): 110 | self.frames_layout.setRowMinimumHeight(i, 0) 111 | self.frames_layout.setRowStretch(i, 0) 112 | 113 | vspcr = QSpacerItem(1, 1) 114 | self.frames_layout.addItem(vspcr, self.num_rows, 0, 1, 4) 115 | 116 | hspcr = QSpacerItem(1, 1) 117 | self.frames_layout.addItem(hspcr, 0, self.num_cols, self.num_rows, 1) 118 | 119 | self.ui.actionClear_Spritesheet_Grid.triggered.connect(self.clear_spriteframe_grid) 120 | self.ui.myTabs.currentChanged.connect(self.handle_tab_change) 121 | self.ui.spsh_settings_btn.clicked.connect(self.show_settings) 122 | 123 | self.settings_widget = SettingsWindow() 124 | 125 | self.anim_view_window = AnimationView() 126 | self.ui.actionPreview_Animation.triggered.connect(self.show_anim_preview) 127 | self.ui.actionPreview_Animation.setEnabled(len(self.labels) > 0) 128 | # adding a QActionGroup at runtime :/ 129 | darkmode_action_group = QActionGroup(self.ui.menuDefault_Dark_mode) 130 | theme_opts = ["Default", "Dark Mode"] 131 | checked_action = "Default" if prefs.get("theme", 'default') != 'dark' else "Dark Mode" 132 | for opt in theme_opts: 133 | action = QAction(opt, self.ui.menuDefault_Dark_mode, checkable=True, checked=(opt == checked_action)) 134 | self.ui.menuDefault_Dark_mode.addAction(action) 135 | darkmode_action_group.addAction(action) 136 | darkmode_action_group.setExclusive(True) 137 | darkmode_action_group.triggered.connect(self.set_dark_mode) 138 | 139 | self.xml_table = XMLTableView(['Image Path', 'Name', 'Width', 'Height', 'FrameX', 'FrameY', 'FrameWidth', 'FrameHeight']) 140 | self.ui.actionView_XML_structure.triggered.connect(self.show_table_view) 141 | self.ui.actionView_XML_structure.setEnabled(len(self.labels) > 0) 142 | self.ui.actionFlipX.triggered.connect(lambda: self.flip_labels('X')) 143 | self.ui.actionFlipY.triggered.connect(lambda: self.flip_labels('Y')) 144 | 145 | self.ui.use_psychengine_checkbox.clicked.connect(self.handle_psychengine_checkbox) 146 | 147 | # self.frame_order_screen = FrameOrderScreen() 148 | # self.ui.actionChange_Frame_Ordering.triggered.connect(self.show_frame_order_screen) 149 | # self.ui.actionChange_Frame_Ordering.setEnabled(len(self.labels) > 0) 150 | 151 | # Note: Add any extra windows before this if your want the themes to apply to them 152 | if prefs.get("theme", 'default') == 'dark': 153 | self.set_theme(get_stylesheet_from_file("assets/app-styles.qss")) 154 | 155 | 156 | def ranged_selection_handler(self, selected_spriteframe): 157 | first_selected_spriteframe = None 158 | for sprf in self.labels: 159 | if sprf == selected_spriteframe: 160 | break 161 | 162 | if sprf.select_checkbox.checkState() != 0 and sprf != selected_spriteframe: 163 | first_selected_spriteframe = sprf 164 | break 165 | 166 | if first_selected_spriteframe is not None: 167 | start_selecting = False 168 | for sprf in self.labels: 169 | if sprf == first_selected_spriteframe: 170 | start_selecting = True 171 | 172 | if start_selecting: 173 | # checks the box and adds it to the selected list 174 | sprf.select_checkbox.setChecked(1) 175 | 176 | if sprf == selected_spriteframe: 177 | break 178 | 179 | def ranged_deletion_handler(self, selected_spriteframe): 180 | first_selected_spriteframe = None 181 | for sprf in self.labels: 182 | if sprf == selected_spriteframe: 183 | break 184 | 185 | if sprf.select_checkbox.checkState() != 0 and sprf != selected_spriteframe: 186 | first_selected_spriteframe = sprf 187 | break 188 | 189 | if first_selected_spriteframe is not None: 190 | start_selecting = False 191 | for sprf in self.labels: 192 | if sprf == first_selected_spriteframe: 193 | start_selecting = True 194 | 195 | if start_selecting: 196 | # unchecks the box and removes it from the selected list 197 | sprf.select_checkbox.setChecked(0) 198 | 199 | if sprf == selected_spriteframe: 200 | break 201 | 202 | 203 | def open_gif(self): 204 | gifpath = self.get_asset_path("Select the GIF file", "GIF images (*.gif)") 205 | if gifpath != '': 206 | update_prog_bar, progbar = display_progress_bar(self, "Extracting sprite frames....") 207 | QApplication.processEvents() 208 | 209 | sprites = spritesheetutils.get_gif_frames(gifpath, update_prog_bar) 210 | for i, spfr in enumerate(sprites): 211 | spfr.frameparent = self 212 | self.add_spriteframe(spfr) 213 | update_prog_bar(50 + ((i+1)*50//len(sprites)), f"Adding frames from: {gifpath}") 214 | progbar.close() 215 | 216 | self.ui.posename_btn.setDisabled(self.num_labels <= 0) 217 | 218 | def handle_psychengine_checkbox(self, checked): 219 | self.ui.uploadicongrid_btn.setEnabled(not checked) 220 | 221 | # def show_frame_order_screen(self): 222 | # self.frame_order_screen.set_frame_dict(self.frame_dict) 223 | # self.frame_order_screen.show() 224 | 225 | def flip_labels(self, dxn='X'): 226 | for lab in self.selected_labels: 227 | lab.flip_img(dxn) 228 | 229 | for lab in list(self.selected_labels): 230 | # this automatically removes it from self.selected_labels 231 | lab.select_checkbox.setChecked(False) 232 | 233 | def show_table_view(self): 234 | print("Showing table view...") 235 | self.xml_table.fill_data(self.labels) 236 | self.xml_table.show() 237 | 238 | def set_dark_mode(self, event): 239 | if event.text() == "Dark Mode": 240 | styles = get_stylesheet_from_file("./assets/app-styles.qss") 241 | self.set_theme(styles) 242 | else: 243 | self.set_theme("") 244 | 245 | def set_theme(self, stylestr): 246 | self.setStyleSheet(stylestr) 247 | self.settings_widget.setStyleSheet(stylestr) 248 | self.anim_view_window.setStyleSheet(stylestr) 249 | self.xml_table.setStyleSheet(stylestr) 250 | # self.frame_order_screen.setStyleSheet(stylestr) 251 | if stylestr == "": 252 | set_preferences({ "theme":"default" }) 253 | else: 254 | set_preferences({ "theme":"dark" }) 255 | 256 | def show_anim_preview(self): 257 | self.anim_view_window.parse_and_load_frames(self.labels) 258 | self.anim_view_window.show() 259 | 260 | def show_settings(self): 261 | self.settings_widget.show() 262 | 263 | def handle_tab_change(self, newtabind): 264 | self.ui.actionClear_Spritesheet_Grid.setDisabled(newtabind != 0) 265 | self.ui.action_import_existing.setDisabled(newtabind != 0) 266 | self.ui.actionImport_from_GIF.setDisabled(newtabind != 0) 267 | self.ui.actionImport_Images.setDisabled(newtabind != 0) 268 | self.ui.actionEdit_Frame_Properties.setDisabled(newtabind != 0 or len(self.selected_labels) <= 0) 269 | self.ui.menuExport.setDisabled(newtabind != 0) 270 | self.ui.menuEdit_Selected_Images.setDisabled(newtabind != 0) 271 | 272 | self.ui.actionImport_IconGrid.setDisabled(newtabind != 1) 273 | self.ui.actionImport_Icons.setDisabled(newtabind != 1) 274 | self.ui.actionClear_IconGrid.setDisabled(newtabind != 1) 275 | self.ui.actionClear_Icon_selection.setDisabled(newtabind != 1) 276 | 277 | def onCharacterNameChange(self): 278 | for label in self.labels: 279 | label.img_label.setToolTip(label.get_tooltip_string(self)) 280 | 281 | def clear_spriteframe_grid(self): 282 | labs = list(self.labels) 283 | for lab in labs: 284 | lab.remove_self(self) 285 | self.ui.actionEdit_Frame_Properties.setDisabled(len(self.selected_labels) <= 0) 286 | 287 | def resizeEvent(self, a0): 288 | w = self.width() 289 | # print("Current width", w) 290 | if w < 1228: 291 | self.num_cols = 6 292 | elif 1228 <= w <= 1652: 293 | self.num_cols = 8 294 | else: 295 | self.num_cols = 12 296 | self.re_render_grid() 297 | return super().resizeEvent(a0) 298 | 299 | def open_existing_spsh_xml(self): 300 | imgpath = self.get_asset_path("Select Spritesheet File", "PNG Images (*.png)") 301 | 302 | if imgpath != '': 303 | xmlpath = self.get_asset_path("Select XML File", "XML Files (*.xml)") 304 | if xmlpath != '': 305 | trubasenamefn = lambda fpath: path.basename(fpath).split('.')[0] 306 | charname = trubasenamefn(xmlpath) 307 | if trubasenamefn(imgpath) != trubasenamefn(xmlpath): 308 | self.msgbox = QMessageBox(self) 309 | self.msgbox.setWindowTitle("Conflicting file names") 310 | self.msgbox.setText("The Spritesheet and the XML file have different file names.\nThe character name will not be auto-filled") 311 | self.msgbox.setIcon(QMessageBox.Warning) 312 | self.msgbox.addButton("OK", QMessageBox.YesRole) 313 | cancel_import = self.msgbox.addButton("Cancel import", QMessageBox.NoRole) 314 | x = self.msgbox.exec_() 315 | clickedbtn = self.msgbox.clickedButton() 316 | if clickedbtn == cancel_import: 317 | return 318 | charname = self.ui.charname_textbox.text() # trubasenamefn(imgpath) if clickedbtn == usespsh else trubasenamefn(xmlpath) 319 | print("[DEBUG] Exit status of msgbox: "+str(x)) 320 | 321 | 322 | update_prog_bar, progbar = display_progress_bar(self, "Extracting sprite frames....") 323 | QApplication.processEvents() 324 | 325 | sprites = spritesheetutils.split_spsh(imgpath, xmlpath, update_prog_bar) 326 | for i, spfr in enumerate(sprites): 327 | spfr.frameparent = self 328 | self.add_spriteframe(spfr) 329 | update_prog_bar(50 + ((i+1)*50//len(sprites)), f"Adding: {imgpath}") 330 | progbar.close() 331 | 332 | self.ui.posename_btn.setDisabled(self.num_labels <= 0) 333 | 334 | self.ui.charname_textbox.setText(charname) 335 | 336 | 337 | 338 | def open_frame_imgs(self): 339 | imgpaths = self.get_asset_path("Select sprite frames", "PNG Images (*.png)", True) 340 | 341 | if imgpaths: 342 | update_prog_bar, progbar = display_progress_bar(self, "Importing sprite frames....", 0, len(imgpaths)) 343 | QApplication.processEvents() 344 | 345 | for i, pth in enumerate(imgpaths): 346 | # self.add_img(pth) 347 | self.add_spriteframe(SpriteFrame(self, pth)) 348 | update_prog_bar(i+1, f"Adding: {pth}") 349 | progbar.close() 350 | 351 | if len(self.labels) > 0: 352 | self.ui.posename_btn.setDisabled(False) 353 | 354 | def add_spriteframe(self, sp): 355 | self.num_rows = 1 + self.num_labels//self.num_cols 356 | 357 | self.frames_layout.setRowMinimumHeight(self.num_rows - 1, 0) 358 | self.frames_layout.setRowStretch(self.num_rows - 1, 0) 359 | 360 | vspcr = QSpacerItem(1, 1) 361 | self.frames_layout.addItem(vspcr, self.num_rows, 0, 1, 4) 362 | 363 | hspcr = QSpacerItem(1, 1) 364 | self.frames_layout.addItem(hspcr, 0, self.num_cols, self.num_rows, 1) 365 | 366 | self.labels.append(sp) 367 | self.frames_layout.removeWidget(self.add_img_button) 368 | self.frames_layout.addWidget(self.labels[-1], self.num_labels // self.num_cols, self.num_labels % self.num_cols, Qt.AlignmentFlag(0x1|0x20)) 369 | self.num_labels += 1 370 | self.frames_layout.addWidget(self.add_img_button, self.num_labels // self.num_cols, self.num_labels % self.num_cols, Qt.AlignmentFlag(0x1|0x20)) 371 | self.ui.actionPreview_Animation.setEnabled(len(self.labels) > 0) 372 | self.ui.actionView_XML_structure.setEnabled(len(self.labels) > 0) 373 | # self.ui.actionChange_Frame_Ordering.setEnabled(len(self.labels) > 0) 374 | 375 | # self.update_frame_dict(sp.img_xml_data.pose_name, sp) 376 | 377 | def update_frame_dict(self, key, val, remove=False): 378 | # TODO 379 | return 380 | 381 | def re_render_grid(self): 382 | self.num_rows = 1 + self.num_labels//self.num_cols 383 | for i in range(self.num_cols): 384 | self.frames_layout.setColumnMinimumWidth(i, 0) 385 | self.frames_layout.setColumnStretch(i, 0) 386 | for i in range(self.num_rows): 387 | self.frames_layout.setRowMinimumHeight(i, 0) 388 | self.frames_layout.setRowStretch(i, 0) 389 | 390 | vspcr = QSpacerItem(1, 1) 391 | self.frames_layout.addItem(vspcr, self.num_rows, 0, 1, 4) 392 | 393 | hspcr = QSpacerItem(1, 1) 394 | self.frames_layout.addItem(hspcr, 0, self.num_cols, self.num_rows, 1) 395 | 396 | for i, sp in enumerate(self.labels): 397 | self.frames_layout.addWidget(sp, i//self.num_cols, i%self.num_cols, Qt.AlignmentFlag(0x1|0x20)) 398 | self.frames_layout.removeWidget(self.add_img_button) 399 | self.frames_layout.addWidget(self.add_img_button, self.num_labels // self.num_cols, self.num_labels % self.num_cols, Qt.AlignmentFlag(0x1|0x20)) 400 | 401 | def export_bunch_of_imgs(self): 402 | savedir = QFileDialog.getExistingDirectory(caption="Save image sequence to...") 403 | updatefn, progbar = display_progress_bar(self, "Exporting Image Sequence", startlim=0, endlim=len(self.labels)) 404 | QApplication.processEvents() 405 | 406 | errmsg = xmlpngengine.save_img_sequence(self.labels, savedir, updatefn) 407 | progbar.close() 408 | if errmsg: 409 | self.display_msg_box("Error!", text=f"An error occured: {errmsg}", icon=QMessageBox.Critical) 410 | else: 411 | self.display_msg_box("Success!", text="Image sequence saved successfully!", icon=QMessageBox.Information) 412 | 413 | def generate_xml(self): 414 | charname = self.ui.charname_textbox.text() 415 | charname = charname.strip() 416 | if self.num_labels > 0 and charname != '': 417 | savedir = QFileDialog.getExistingDirectory(caption="Save files to...") 418 | print("Stuff saved to: ", savedir) 419 | if savedir != '': 420 | update_prog_bar, progbar = display_progress_bar(self, "Generating....", 0, len(self.labels)) 421 | QApplication.processEvents() 422 | 423 | statuscode, errmsg = xmlpngengine.make_png_xml( 424 | self.labels, 425 | savedir, 426 | charname, 427 | update_prog_bar 428 | ) 429 | progbar.close() 430 | if errmsg is None: 431 | self.display_msg_box( 432 | window_title="Done!", 433 | text="Your files have been generated!\nCheck the folder you had selected", 434 | icon=QMessageBox.Information 435 | ) 436 | else: 437 | self.display_msg_box( 438 | window_title="Error!", 439 | text=("Some error occured! Error message: " + errmsg), 440 | icon=QMessageBox.Critical 441 | ) 442 | else: 443 | errtxt = "Please enter some frames" if self.num_labels <= 0 else "Please enter the name of your character" 444 | self.display_msg_box( 445 | window_title="Error!", 446 | text=errtxt, 447 | icon=QMessageBox.Critical 448 | ) 449 | 450 | def zoomInPixmap(self): 451 | if self.icongrid_path and self.icongrid_zoom <= 5: 452 | self.icongrid_zoom *= 1.1 453 | icongrid_pixmap = QPixmap(self.icongrid_path) 454 | w = icongrid_pixmap.width() 455 | h = icongrid_pixmap.height() 456 | icongrid_pixmap = icongrid_pixmap.scaled(int(w*self.icongrid_zoom), int(h*self.icongrid_zoom), 1) 457 | self.ui.icongrid_holder_label.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 458 | self.ui.scrollAreaWidgetContents_2.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 459 | self.ui.icongrid_holder_label.setPixmap(icongrid_pixmap) 460 | self.ui.zoom_label.setText("Zoom: %.2f %%" % (self.icongrid_zoom*100)) 461 | 462 | 463 | def zoomOutPixmap(self): 464 | if self.icongrid_path and self.icongrid_zoom >= 0.125: 465 | self.icongrid_zoom /= 1.1 466 | icongrid_pixmap = QPixmap(self.icongrid_path) 467 | w = icongrid_pixmap.width() 468 | h = icongrid_pixmap.height() 469 | icongrid_pixmap = icongrid_pixmap.scaled(int(w*self.icongrid_zoom), int(h*self.icongrid_zoom), 1) 470 | self.ui.icongrid_holder_label.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 471 | self.ui.scrollAreaWidgetContents_2.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 472 | self.ui.icongrid_holder_label.setPixmap(icongrid_pixmap) 473 | self.ui.zoom_label.setText("Zoom: %.2f %%" % (self.icongrid_zoom*100)) 474 | 475 | def uploadIconGrid(self): 476 | print("Uploading icongrid...") 477 | self.icongrid_path = self.get_asset_path("Select the Icon-grid", "PNG Images (*.png)") 478 | icongrid_pixmap = QPixmap(self.icongrid_path) 479 | self.ui.icongrid_holder_label.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 480 | self.ui.scrollAreaWidgetContents_2.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 481 | self.ui.icongrid_holder_label.setPixmap(icongrid_pixmap) 482 | 483 | def clearIconGrid(self): 484 | self.icongrid_path = "" 485 | self.ui.icongrid_holder_label.clear() 486 | 487 | def getNewIconGrid(self): 488 | if self.ui.use_psychengine_checkbox.isChecked(): 489 | if len(self.iconpaths) > 0: 490 | print("Using psych engine style icon grid generation....") 491 | savepath, _ = QFileDialog.getSaveFileName(self, "Save as filename", filter="PNG files (*.png)") 492 | 493 | stat, problemimg, exception_msg = icongridutils.makePsychEngineIconGrid(self.iconpaths, savepath) 494 | 495 | if exception_msg is not None: 496 | self.display_msg_box( 497 | window_title="Error!", 498 | text=f"An error occured: {exception_msg}", 499 | icon=QMessageBox.Critical 500 | ) 501 | else: 502 | if stat == 0: 503 | self.display_msg_box( 504 | window_title="Done!", 505 | text="Your icon-grid has been generated!", 506 | icon=QMessageBox.Information 507 | ) 508 | # display final image onto the icon display area 509 | icongrid_pixmap = QPixmap(savepath) 510 | self.ui.icongrid_holder_label.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 511 | self.ui.scrollAreaWidgetContents_2.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 512 | self.ui.icongrid_holder_label.setPixmap(icongrid_pixmap) 513 | elif stat == 1: 514 | self.display_msg_box( 515 | window_title="Icon image error", 516 | text=f"The icon {problemimg} is bigger than 150x150 and couldn't be added to the final grid\nThe final grid was generated without it", 517 | icon=QMessageBox.Warning 518 | ) 519 | else: 520 | self.display_msg_box( 521 | window_title="Error!", 522 | text="Please select some icons", 523 | icon=QMessageBox.Critical 524 | ) 525 | 526 | # no need to continue past this if in psych-engine mode 527 | return 528 | 529 | if self.icongrid_path != '' and len(self.iconpaths) > 0: 530 | print("Valid!") 531 | # savedir = QFileDialog.getExistingDirectory(caption="Save New Icongrid to...") 532 | # if savedir != '': 533 | stat, newinds, problemimg, exception_msg = icongridutils.appendIconToGrid(self.icongrid_path, self.iconpaths) #, savedir) 534 | print("[DEBUG] Function finished with status: ", stat) 535 | errmsgs = [ 536 | 'Icon grid was too full to insert a new icon', 537 | 'Your character icon: {} is too big! Max size: 150 x 150', 538 | 'Unable to find suitable location to insert your icon' 539 | ] 540 | 541 | if exception_msg is not None: 542 | self.display_msg_box( 543 | window_title="An Error occured", 544 | text=("An Exception (Error) occurred somewhere\nError message:\n"+exception_msg), 545 | icon=QMessageBox.Critical 546 | ) 547 | else: 548 | if stat == 0: 549 | self.display_msg_box( 550 | window_title="Done!", 551 | text="Your icon-grid has been generated!\nYour icon's indices are {}".format(newinds), 552 | icon=QMessageBox.Information 553 | ) 554 | elif stat == 4: 555 | self.display_msg_box( 556 | window_title="Warning!", 557 | text="One of your icons was smaller than the 150 x 150 icon size!\nHowever, your icon-grid is generated but the icon has been re-adjusted. \nYour icon's indices: {}".format(newinds), 558 | icon=QMessageBox.Warning 559 | ) 560 | else: 561 | self.display_msg_box( 562 | window_title="Error!", 563 | text=errmsgs[stat - 1].format(problemimg), 564 | icon=QMessageBox.Critical 565 | ) 566 | icongrid_pixmap = QPixmap(self.icongrid_path) 567 | self.ui.icongrid_holder_label.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 568 | self.ui.scrollAreaWidgetContents_2.setFixedSize(icongrid_pixmap.width(), icongrid_pixmap.height()) 569 | self.ui.icongrid_holder_label.setPixmap(icongrid_pixmap) 570 | else: 571 | errtxt = "Please add an icon-grid image" if self.icongrid_path == '' else "Please add an icon" 572 | self.display_msg_box( 573 | window_title="Error!", 574 | text=errtxt, 575 | icon=QMessageBox.Critical 576 | ) 577 | 578 | def appendIcon(self): 579 | print("Appending icon") 580 | self.iconpaths = self.get_asset_path("Select your character icons", "PNG Images (*.png)", True) 581 | print("Got icon: ", self.iconpaths) 582 | if len(self.iconpaths) > 0: 583 | print("Valid selected") 584 | self.ui.iconselected_label.setText("No. of\nicons selected:\n{}".format(len(self.iconpaths))) 585 | else: 586 | self.ui.iconselected_label.setText("No. of\nicons selected:\n0") 587 | 588 | def clearSelectedIcons(self): 589 | self.iconpaths = [] 590 | self.ui.iconselected_label.setText("Number of\nicons selected:\n{}".format(len(self.iconpaths))) 591 | 592 | def setAnimationNames(self): 593 | if len(self.selected_labels) == 0: 594 | self.display_msg_box(window_title="Error", text="Please select some frames to rename by checking the checkboxes on them", icon=QMessageBox.Critical) 595 | else: 596 | text, okPressed = QInputDialog.getText(self, "Change Animation (Pose) Prefix Name", "Current Animation (Pose) prefix:"+(" "*50), QLineEdit.Normal) # very hack-y soln but it works! 597 | if okPressed and text != '': 598 | print("new pose prefix = ", text) 599 | for label in self.selected_labels: 600 | # self.update_frame_dict(label.img_xml_data.pose_name, label, remove=True) 601 | label.data.pose_name = text 602 | label.modified = True 603 | # self.update_frame_dict(text, label) 604 | label.img_label.setToolTip(label.get_tooltip_string(self)) 605 | 606 | for label in list(self.selected_labels): 607 | # this automatically removes it from self.selected_labels 608 | label.select_checkbox.setChecked(False) 609 | else: 610 | print("Cancel pressed!") 611 | 612 | def display_msg_box(self, window_title="MessageBox", text="Text Here", icon=None): 613 | self.msgbox = QMessageBox(self) 614 | self.msgbox.setWindowTitle(window_title) 615 | self.msgbox.setText(text) 616 | if not icon: 617 | self.msgbox.setIcon(QMessageBox.Information) 618 | else: 619 | self.msgbox.setIcon(icon) 620 | x = self.msgbox.exec_() 621 | print("[DEBUG] Exit status of msgbox: "+str(x)) 622 | 623 | def get_asset_path(self, wintitle="Sample", fileformat=None, multiple=False): 624 | if multiple: 625 | return QFileDialog.getOpenFileNames( 626 | caption=wintitle, 627 | filter=fileformat, 628 | )[0] 629 | else: 630 | return QFileDialog.getOpenFileName( 631 | caption=wintitle, 632 | filter=fileformat, 633 | )[0] 634 | 635 | 636 | 637 | 638 | if __name__ == '__main__': 639 | app = QApplication(sys.argv) 640 | 641 | prefs = None 642 | try: 643 | with open('preferences.json') as f: 644 | prefs = json.load(f) 645 | except FileNotFoundError as fnfe: 646 | with open("error.log", 'a') as errlog: 647 | errlog.write(str(fnfe)) 648 | 649 | with open('preferences.json', 'w') as f: 650 | prefs = { "theme":"default" } 651 | json.dump(prefs, f) 652 | 653 | myapp = MyApp(prefs) 654 | myapp.show() 655 | 656 | try: 657 | sys.exit(app.exec_()) 658 | except SystemExit: 659 | print("Closing...") -------------------------------------------------------------------------------- /src/xmltablewindow.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QTableWidgetItem, QWidget, QMenu, QAction, QMessageBox, QLineEdit, QInputDialog 2 | from PyQt5.QtCore import Qt 3 | from PyQt5.QtGui import QCursor 4 | from utils import temp_path_shortener, imghashes 5 | import engine.spritesheetutils as spritesheetutils 6 | from xmltablewindowUI import Ui_TableWidgetThing 7 | from utils import display_msg_box 8 | 9 | class XMLTableView(QWidget): 10 | def __init__(self, table_headings): 11 | super().__init__() 12 | self.ui = Ui_TableWidgetThing() 13 | self.ui.setupUi(self) 14 | 15 | self.table_headings = table_headings 16 | self.ui.xmltable.setColumnCount(len(table_headings)) 17 | self.ui.xmltable.setHorizontalHeaderLabels(table_headings) 18 | self.ui.xmltable.contextMenuEvent = self.handle_context_menu_event 19 | 20 | # self.ui.xmltable.cellClicked.connect(self.handle_cell_click) 21 | # self.ui.xmltable.cellActivated.connect(self.handle_cell_click) 22 | # self.ui.xmltable.cellPressed.connect(self.handle_cell_click) 23 | self.ui.xmltable.currentCellChanged.connect(self.handle_curr_cell_change) 24 | self.ui.frame_preview_label.setStyleSheet("QFrame{ border: 1px solid black; }") 25 | # self.ui.xmltable.selectionModel().selectionChanged.connect(self.handle_cell_selection) 26 | 27 | # list[SpriteFrame] 28 | self.tabledata = [] 29 | self.canchange = True 30 | self.frame_info = [None, None, None, None] 31 | self.frame_spinboxes = [self.ui.framex_spinbox, self.ui.framey_spinbox, self.ui.framewidth_spinbox, self.ui.frameheight_spinbox] 32 | 33 | self.ui.framex_spinbox.valueChanged.connect(self.handle_framex_change) 34 | self.ui.framey_spinbox.valueChanged.connect(self.handle_framey_change) 35 | self.ui.framewidth_spinbox.valueChanged.connect(self.handle_framew_change) 36 | self.ui.frameheight_spinbox.valueChanged.connect(self.handle_frameh_change) 37 | 38 | self.selected_row = None 39 | self.selected_row_index = None 40 | 41 | self.was_opened = False 42 | 43 | self.selected_cells = [] 44 | 45 | def handle_curr_cell_change(self, current_row, current_col, prev_row, prev_col): 46 | self.selected_row_index = current_row 47 | self.handle_display_stuff(self.selected_row_index) 48 | 49 | def handle_context_menu_event(self, event): 50 | self.menu = QMenu(self) 51 | renameAction = QAction('Set Value', self) 52 | renameAction.triggered.connect(lambda: self.set_value_handle()) 53 | self.menu.addAction(renameAction) 54 | # add other required actions 55 | self.menu.popup(QCursor.pos()) 56 | 57 | def set_value_handle(self): 58 | _cells = self.ui.xmltable.selectedItems() 59 | idx = -1 60 | for _cell in _cells: 61 | if not (_cell.flags() & Qt.ItemIsEditable): 62 | display_msg_box(self, "Bad cell selection", "There are un-editable cells in your selection!\nSelect cells from the same column, valid columns being\nFrameX, FrameY, FrameWidth or FrameHeight", QMessageBox.Critical) 63 | return 64 | else: 65 | if idx != -1 and _cell.column() != idx: 66 | display_msg_box(self, "Multiple Columns Selected", "Your selection spans multiple columns. Make sure to select cells that belong to the same column, valid columns being\nFrameX, FrameY, FrameWidth or FrameHeight", QMessageBox.Critical) 67 | return 68 | else: 69 | idx = _cell.column() 70 | 71 | rows = [ x.row() for x in _cells ] 72 | text, okPressed = QInputDialog.getText(self, f"Change Value of {self.table_headings[idx - 4]}", "New value:"+(" "*50), QLineEdit.Normal) 73 | is_real_number = lambda s: s.isnumeric() or (s[0] == '-' and s[1:].isnumeric()) 74 | if okPressed and text != '' and is_real_number(text): 75 | val = int(text) 76 | old_selected_row_index = self.selected_row_index 77 | old_selected_row = self.selected_row 78 | for row_num in rows: 79 | self.ui.xmltable.setItem(row_num, idx, QTableWidgetItem(str(val))) 80 | self.selected_row_index = row_num 81 | self.selected_row = self.tabledata[row_num] 82 | self.handle_cell_change(row_num, idx) 83 | 84 | # restoring things back to normal 85 | self.selected_row_index = old_selected_row_index 86 | self.selected_row = old_selected_row 87 | self.set_true_frame() 88 | else: 89 | print("Text invalid / cancel was pressed") 90 | 91 | 92 | def handle_framex_change(self, newval): 93 | if self.canchange: 94 | if self.selected_row: 95 | self.selected_row.data.framex = newval 96 | self.ui.xmltable.setItem(self.selected_row_index, 4, QTableWidgetItem(str(newval))) 97 | self.set_true_frame() 98 | 99 | def handle_framey_change(self, newval): 100 | if self.canchange: 101 | if self.selected_row: 102 | self.selected_row.data.framey = newval 103 | self.ui.xmltable.setItem(self.selected_row_index, 5, QTableWidgetItem(str(newval))) 104 | self.set_true_frame() 105 | 106 | def handle_framew_change(self, newval): 107 | if self.canchange: 108 | if self.selected_row: 109 | self.selected_row.data.framew = newval 110 | self.ui.xmltable.setItem(self.selected_row_index, 6, QTableWidgetItem(str(newval))) 111 | self.set_true_frame() 112 | 113 | def handle_frameh_change(self, newval): 114 | if self.canchange: 115 | if self.selected_row: 116 | self.selected_row.data.frameh = newval 117 | self.ui.xmltable.setItem(self.selected_row_index, 7, QTableWidgetItem(str(newval))) 118 | self.set_true_frame() 119 | 120 | def set_true_frame(self): 121 | # set the frame pixmap 122 | curimg = imghashes.get(self.selected_row.data.img_hash) 123 | truframe = spritesheetutils.get_true_frame( 124 | curimg, 125 | self.selected_row.data.framex if self.selected_row.data.framex is not None else 0, 126 | self.selected_row.data.framey if self.selected_row.data.framey is not None else 0, 127 | self.selected_row.data.framew if self.selected_row.data.framew is not None else curimg.width, 128 | self.selected_row.data.frameh if self.selected_row.data.frameh is not None else curimg.height, 129 | ).toqpixmap() 130 | self.ui.frame_preview_label.setPixmap(truframe) 131 | self.ui.frame_preview_label.setFixedSize(truframe.width(), truframe.height()) 132 | 133 | def fill_data(self, data): 134 | # data: list[Spriteframe] 135 | table = self.ui.xmltable 136 | if self.was_opened: 137 | table.cellChanged.disconnect(self.handle_cell_change) 138 | self.tabledata = data 139 | table.setRowCount(len(data)) 140 | for rownum, label in enumerate(data): 141 | tabledat = [label.data.imgpath, label.data.pose_name, label.data.img_width, label.data.img_height, label.data.framex, label.data.framey, label.data.framew, label.data.frameh] 142 | for colnum, col in enumerate(tabledat): 143 | table_cell = QTableWidgetItem(str(col)) 144 | if colnum < 4: 145 | table_cell.setFlags(table_cell.flags() ^ Qt.ItemIsEditable) 146 | table.setItem(rownum, colnum, table_cell) 147 | 148 | table.cellChanged.connect(self.handle_cell_change) 149 | self.was_opened = True 150 | 151 | def handle_cell_change(self, row, col): 152 | idx = col - 4 153 | 154 | if idx >= 0: 155 | self.canchange = False 156 | newval = self.ui.xmltable.item(row, col).text() 157 | if newval.lower() == 'default': 158 | # default framex = framey = 0, framew = img.width, frameh = img.height 159 | if idx <= 1: 160 | newval = 0 161 | elif idx == 2: 162 | newval = self.selected_row.data.img_width 163 | elif idx == 3: 164 | newval = self.selected_row.data.img_height 165 | else: 166 | print("Something's wrong") 167 | self.ui.xmltable.setItem(row, col, QTableWidgetItem(str(newval))) 168 | else: 169 | try: 170 | newval = int(newval) 171 | assert (idx >= 2 and newval > 0) or (idx < 2) 172 | except Exception as e: 173 | print("Exception:\n", e) 174 | if idx == 0: 175 | newval = self.selected_row.data.framex 176 | elif idx == 1: 177 | newval = self.selected_row.data.framey 178 | elif idx == 2: 179 | newval = self.selected_row.data.framew 180 | elif idx == 3: 181 | newval = self.selected_row.data.frameh 182 | else: 183 | print("Something's wrong") 184 | self.ui.xmltable.setItem(row, col, QTableWidgetItem(str(newval))) 185 | 186 | self.frame_spinboxes[idx].setValue(newval if newval else 0) 187 | self.canchange = True 188 | 189 | # idx: 0 = framex, 1 = framey, 2 = framew, 3 = frameh 190 | if idx == 0: 191 | self.selected_row.data.framex = newval 192 | elif idx == 1: 193 | self.selected_row.data.framey = newval 194 | elif idx == 2: 195 | self.selected_row.data.framew = newval 196 | elif idx == 3: 197 | self.selected_row.data.frameh = newval 198 | else: 199 | print("[ERROR] Some error occured!") 200 | 201 | self.set_true_frame() 202 | 203 | # def handle_cell_selection(self, selected, deselected): 204 | # if selected.indexes(): 205 | # self.selected_cells.extend(selected.indexes()) 206 | # self.selected_row_index = selected.indexes()[-1].row() 207 | # self.handle_display_stuff(self.selected_row_index) 208 | # elif deselected.indexes(): 209 | # for _cell in deselected.indexes(): 210 | # print(f"Removing: {_cell.row()}, {_cell.column()}") 211 | # self.selected_cells.remove(_cell) 212 | # self.selected_row_index = deselected.indexes()[-1].row() 213 | # self.handle_display_stuff(self.selected_row_index) 214 | # print(f'{[ (c.row(), c.column()) for c in self.selected_cells ]}') 215 | # else: 216 | # print("Something's weird here") 217 | 218 | def handle_display_stuff(self, row): 219 | self.selected_row = self.tabledata[row] 220 | short_path = temp_path_shortener(self.selected_row.data.imgpath) 221 | 222 | self.ui.frame_preview_label.clear() 223 | self.set_true_frame() 224 | 225 | if self.selected_row.data.from_single_png: 226 | self.ui.frame_info_label.setText(f"Image path: {short_path}\tFrom existing spritesheet: No") 227 | else: 228 | self.ui.frame_info_label.setText(f"Image path: {short_path}\tFrom existing spritesheet: Yes\tCo-ords in source spritesheet: x={self.selected_row.data.tx} y={self.selected_row.data.ty} w={self.selected_row.data.tw} h={self.selected_row.data.th}") 229 | 230 | self.frame_info = [self.selected_row.data.framex, self.selected_row.data.framey, self.selected_row.data.framew, self.selected_row.data.frameh] 231 | for spinbox, info in zip(self.frame_spinboxes, self.frame_info): 232 | self.canchange = False 233 | spinbox.setValue(int(info) if info is not None and str(info).lower() != "default" else 0) 234 | self.canchange = True -------------------------------------------------------------------------------- /src/xmltablewindowUI.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'XMLTableWidget.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.4 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_TableWidgetThing(object): 15 | def setupUi(self, TableWidgetThing): 16 | TableWidgetThing.setObjectName("TableWidgetThing") 17 | TableWidgetThing.resize(1181, 586) 18 | self.horizontalLayout = QtWidgets.QHBoxLayout(TableWidgetThing) 19 | self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) 20 | self.horizontalLayout.setObjectName("horizontalLayout") 21 | self.xmltable = QtWidgets.QTableWidget(TableWidgetThing) 22 | self.xmltable.setObjectName("xmltable") 23 | self.xmltable.setColumnCount(0) 24 | self.xmltable.setRowCount(0) 25 | self.horizontalLayout.addWidget(self.xmltable) 26 | self.frame = QtWidgets.QFrame(TableWidgetThing) 27 | self.frame.setMinimumSize(QtCore.QSize(800, 0)) 28 | self.frame.setMaximumSize(QtCore.QSize(800, 16777215)) 29 | self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) 30 | self.frame.setFrameShadow(QtWidgets.QFrame.Raised) 31 | self.frame.setObjectName("frame") 32 | self.verticalLayout = QtWidgets.QVBoxLayout(self.frame) 33 | self.verticalLayout.setObjectName("verticalLayout") 34 | self.frame_2 = QtWidgets.QFrame(self.frame) 35 | self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) 36 | self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) 37 | self.frame_2.setObjectName("frame_2") 38 | self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.frame_2) 39 | self.horizontalLayout_6.setSpacing(20) 40 | self.horizontalLayout_6.setObjectName("horizontalLayout_6") 41 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 42 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 43 | self.framex_label = QtWidgets.QLabel(self.frame_2) 44 | self.framex_label.setObjectName("framex_label") 45 | self.horizontalLayout_2.addWidget(self.framex_label) 46 | self.framex_spinbox = QtWidgets.QSpinBox(self.frame_2) 47 | self.framex_spinbox.setMinimum(-10000) 48 | self.framex_spinbox.setMaximum(10000) 49 | self.framex_spinbox.setObjectName("framex_spinbox") 50 | self.horizontalLayout_2.addWidget(self.framex_spinbox) 51 | self.horizontalLayout_6.addLayout(self.horizontalLayout_2) 52 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout() 53 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 54 | self.framey_label = QtWidgets.QLabel(self.frame_2) 55 | self.framey_label.setObjectName("framey_label") 56 | self.horizontalLayout_3.addWidget(self.framey_label) 57 | self.framey_spinbox = QtWidgets.QSpinBox(self.frame_2) 58 | self.framey_spinbox.setMinimum(-10000) 59 | self.framey_spinbox.setMaximum(10000) 60 | self.framey_spinbox.setObjectName("framey_spinbox") 61 | self.horizontalLayout_3.addWidget(self.framey_spinbox) 62 | self.horizontalLayout_6.addLayout(self.horizontalLayout_3) 63 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout() 64 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 65 | self.framewidth_label = QtWidgets.QLabel(self.frame_2) 66 | self.framewidth_label.setObjectName("framewidth_label") 67 | self.horizontalLayout_4.addWidget(self.framewidth_label) 68 | self.framewidth_spinbox = QtWidgets.QSpinBox(self.frame_2) 69 | self.framewidth_spinbox.setMinimum(1) 70 | self.framewidth_spinbox.setMaximum(10000) 71 | self.framewidth_spinbox.setObjectName("framewidth_spinbox") 72 | self.horizontalLayout_4.addWidget(self.framewidth_spinbox) 73 | self.horizontalLayout_6.addLayout(self.horizontalLayout_4) 74 | self.horizontalLayout_5 = QtWidgets.QHBoxLayout() 75 | self.horizontalLayout_5.setObjectName("horizontalLayout_5") 76 | self.frameheight_label = QtWidgets.QLabel(self.frame_2) 77 | self.frameheight_label.setObjectName("frameheight_label") 78 | self.horizontalLayout_5.addWidget(self.frameheight_label) 79 | self.frameheight_spinbox = QtWidgets.QSpinBox(self.frame_2) 80 | self.frameheight_spinbox.setMinimum(1) 81 | self.frameheight_spinbox.setMaximum(10000) 82 | self.frameheight_spinbox.setObjectName("frameheight_spinbox") 83 | self.horizontalLayout_5.addWidget(self.frameheight_spinbox) 84 | self.horizontalLayout_6.addLayout(self.horizontalLayout_5) 85 | self.verticalLayout.addWidget(self.frame_2) 86 | self.scrollArea = QtWidgets.QScrollArea(self.frame) 87 | self.scrollArea.setWidgetResizable(True) 88 | self.scrollArea.setObjectName("scrollArea") 89 | self.scrollAreaWidgetContents = QtWidgets.QWidget() 90 | self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 774, 456)) 91 | self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") 92 | self.gridLayout = QtWidgets.QGridLayout(self.scrollAreaWidgetContents) 93 | self.gridLayout.setObjectName("gridLayout") 94 | self.frame_preview_label = QtWidgets.QLabel(self.scrollAreaWidgetContents) 95 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 96 | sizePolicy.setHorizontalStretch(0) 97 | sizePolicy.setVerticalStretch(0) 98 | sizePolicy.setHeightForWidth(self.frame_preview_label.sizePolicy().hasHeightForWidth()) 99 | self.frame_preview_label.setSizePolicy(sizePolicy) 100 | self.frame_preview_label.setMinimumSize(QtCore.QSize(50, 50)) 101 | self.frame_preview_label.setAlignment(QtCore.Qt.AlignCenter) 102 | self.frame_preview_label.setObjectName("frame_preview_label") 103 | self.gridLayout.addWidget(self.frame_preview_label, 0, 0, 1, 1) 104 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 105 | self.verticalLayout.addWidget(self.scrollArea) 106 | self.frame_info_label = QtWidgets.QLabel(self.frame) 107 | font = QtGui.QFont() 108 | font.setPointSize(9) 109 | self.frame_info_label.setFont(font) 110 | self.frame_info_label.setObjectName("frame_info_label") 111 | self.verticalLayout.addWidget(self.frame_info_label) 112 | self.horizontalLayout.addWidget(self.frame) 113 | self.horizontalLayout.setStretch(0, 1) 114 | self.horizontalLayout.setStretch(1, 1) 115 | 116 | self.retranslateUi(TableWidgetThing) 117 | QtCore.QMetaObject.connectSlotsByName(TableWidgetThing) 118 | 119 | def retranslateUi(self, TableWidgetThing): 120 | _translate = QtCore.QCoreApplication.translate 121 | TableWidgetThing.setWindowTitle(_translate("TableWidgetThing", "XML Table View")) 122 | self.framex_label.setText(_translate("TableWidgetThing", "FrameX")) 123 | self.framey_label.setText(_translate("TableWidgetThing", "FrameY")) 124 | self.framewidth_label.setText(_translate("TableWidgetThing", "FrameWidth")) 125 | self.frameheight_label.setText(_translate("TableWidgetThing", "FrameHeight")) 126 | self.frame_preview_label.setText(_translate("TableWidgetThing", "Frame preview goes here")) 127 | self.frame_info_label.setText(_translate("TableWidgetThing", "Info about the frame")) 128 | --------------------------------------------------------------------------------