├── .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 |
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 |
--------------------------------------------------------------------------------