├── .gitignore ├── LICENSE ├── README.md ├── assets ├── fonts │ ├── LuckiestGuy-Regular.ttf │ └── README.md └── images │ ├── background.png │ ├── box.psd │ ├── box_bottom_common.png │ ├── box_bottom_dark.png │ ├── box_bottom_dc.png │ ├── box_bottom_epic.png │ ├── box_bottom_frozen.png │ ├── box_bottom_icon.png │ ├── box_bottom_lava.png │ ├── box_bottom_legendary.png │ ├── box_bottom_marvel.png │ ├── box_bottom_rare.png │ ├── box_bottom_shadow.png │ ├── box_bottom_starwars.png │ ├── box_bottom_uncommon.png │ ├── box_faceplate_common.png │ ├── box_faceplate_dark.png │ ├── box_faceplate_dc.png │ ├── box_faceplate_epic.png │ ├── box_faceplate_frozen.png │ ├── box_faceplate_icon.png │ ├── box_faceplate_lava.png │ ├── box_faceplate_legendary.png │ ├── box_faceplate_marvel.png │ ├── box_faceplate_rare.png │ ├── box_faceplate_shadow.png │ ├── box_faceplate_starwars.png │ ├── box_faceplate_uncommon.png │ ├── card.psd │ ├── card_bottom_common.png │ ├── card_bottom_dark.png │ ├── card_bottom_dc.png │ ├── card_bottom_epic.png │ ├── card_bottom_frozen.png │ ├── card_bottom_icon.png │ ├── card_bottom_lava.png │ ├── card_bottom_legendary.png │ ├── card_bottom_marvel.png │ ├── card_bottom_rare.png │ ├── card_bottom_shadow.png │ ├── card_bottom_starwars.png │ ├── card_bottom_uncommon.png │ ├── card_faceplate_common.png │ ├── card_faceplate_dark.png │ ├── card_faceplate_dc.png │ ├── card_faceplate_epic.png │ ├── card_faceplate_frozen.png │ ├── card_faceplate_icon.png │ ├── card_faceplate_lava.png │ ├── card_faceplate_legendary.png │ ├── card_faceplate_marvel.png │ ├── card_faceplate_rare.png │ ├── card_faceplate_shadow.png │ ├── card_faceplate_starwars.png │ ├── card_faceplate_uncommon.png │ ├── card_top_common.png │ ├── card_top_dark.png │ ├── card_top_dc.png │ ├── card_top_epic.png │ ├── card_top_frozen.png │ ├── card_top_icon.png │ ├── card_top_lava.png │ ├── card_top_legendary.png │ ├── card_top_marvel.png │ ├── card_top_rare.png │ ├── card_top_shadow.png │ ├── card_top_starwars.png │ ├── card_top_uncommon.png │ ├── logo.png │ └── vbucks.png ├── configuration_example.json ├── itemshop.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Visual Studio Code 107 | .vscode/ 108 | 109 | # Burbank font (paid) 110 | # https://fonts.adobe.com/fonts/burbank 111 | assets/fonts/BurbankBigCondensed-Black.otf 112 | 113 | # Output 114 | itemshop.png 115 | 116 | # Credentials 117 | configuration.json 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ethan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Athena 2 | 3 | Athena is a utility which generates the current Fortnite Item Shop into a stylized image and shares it on Twitter. 4 | 5 | As seen on [@FNMasterCom](https://twitter.com/FNMasterCom/status/1197666123078160386?s=20)... 6 | 7 |

8 | 9 |

10 | 11 | ## Requirements 12 | 13 | - [Python 3.7](https://www.python.org/downloads/) 14 | - [Requests](http://docs.python-requests.org/en/master/user/install/) 15 | - [coloredlogs](https://pypi.org/project/coloredlogs/) 16 | - [Pillow](https://pillow.readthedocs.io/en/stable/installation.html#basic-installation) 17 | - [python-twitter](https://github.com/bear/python-twitter#installing) 18 | 19 | A [Fortnite-API API Key](https://fortnite-api.com/profile) is required to obtain the Item Shop data, [Twitter API credentials](https://developer.twitter.com/en/apps) are required to Tweet the image. 20 | 21 | ## Usage 22 | 23 | Open `configuration_example.json` in your preferred text editor, fill the configurable values. Once finished, save and rename the file to `configuration.json`. 24 | 25 | - `delayStart`: Set to `0` to begin the process immediately 26 | - `language`: Set the language for the Item Shop data ([Supported Languages](https://fortnite-api.com/documentation)) 27 | - `supportACreator`: Leave blank to omit the Support-A-Creator tag section of the Tweet 28 | - `twitter`: Set `enabled` to `false` if you wish for `itemshop.png` to not be Tweeted 29 | 30 | Edit the images found in `assets/images/` to your liking, avoid changing image dimensions for optimal results. 31 | 32 | Athena is designed to be ran using a scheduler, such as [cron](https://en.wikipedia.org/wiki/Cron). 33 | 34 | ``` 35 | python itemshop.py 36 | ``` 37 | 38 | ## Credits 39 | 40 | - Item Shop data provided by [Fortnite-API](https://fortnite-api.com/) 41 | - Burbank font property of [Adobe](https://fonts.adobe.com/fonts/burbank) 42 | - Luckiest Guy font property of [Google](https://fonts.google.com/specimen/Luckiest+Guy) 43 | -------------------------------------------------------------------------------- /assets/fonts/LuckiestGuy-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/fonts/LuckiestGuy-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/README.md: -------------------------------------------------------------------------------- 1 | # Important 2 | 3 | You will need to *obtain* a copy of `BurbankBigCondensed-Black.otf` for optimal results! This is the font which Epic Games uses for Fortnite assets. 4 | 5 | If `BurbankBigCondensed-Black.otf` is not found, the font used will default to [Luckiest Guy](https://fonts.google.com/specimen/Luckiest+Guy). 6 | 7 | ## Purchase Burbank 8 | 9 | - Adobe: https://fonts.adobe.com/fonts/burbank 10 | -------------------------------------------------------------------------------- /assets/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/background.png -------------------------------------------------------------------------------- /assets/images/box.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box.psd -------------------------------------------------------------------------------- /assets/images/box_bottom_common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_common.png -------------------------------------------------------------------------------- /assets/images/box_bottom_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_dark.png -------------------------------------------------------------------------------- /assets/images/box_bottom_dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_dc.png -------------------------------------------------------------------------------- /assets/images/box_bottom_epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_epic.png -------------------------------------------------------------------------------- /assets/images/box_bottom_frozen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_frozen.png -------------------------------------------------------------------------------- /assets/images/box_bottom_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_icon.png -------------------------------------------------------------------------------- /assets/images/box_bottom_lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_lava.png -------------------------------------------------------------------------------- /assets/images/box_bottom_legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_legendary.png -------------------------------------------------------------------------------- /assets/images/box_bottom_marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_marvel.png -------------------------------------------------------------------------------- /assets/images/box_bottom_rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_rare.png -------------------------------------------------------------------------------- /assets/images/box_bottom_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_shadow.png -------------------------------------------------------------------------------- /assets/images/box_bottom_starwars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_starwars.png -------------------------------------------------------------------------------- /assets/images/box_bottom_uncommon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_bottom_uncommon.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_common.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_dark.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_dc.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_epic.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_frozen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_frozen.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_icon.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_lava.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_legendary.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_marvel.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_rare.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_shadow.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_starwars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_starwars.png -------------------------------------------------------------------------------- /assets/images/box_faceplate_uncommon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/box_faceplate_uncommon.png -------------------------------------------------------------------------------- /assets/images/card.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card.psd -------------------------------------------------------------------------------- /assets/images/card_bottom_common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_common.png -------------------------------------------------------------------------------- /assets/images/card_bottom_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_dark.png -------------------------------------------------------------------------------- /assets/images/card_bottom_dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_dc.png -------------------------------------------------------------------------------- /assets/images/card_bottom_epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_epic.png -------------------------------------------------------------------------------- /assets/images/card_bottom_frozen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_frozen.png -------------------------------------------------------------------------------- /assets/images/card_bottom_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_icon.png -------------------------------------------------------------------------------- /assets/images/card_bottom_lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_lava.png -------------------------------------------------------------------------------- /assets/images/card_bottom_legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_legendary.png -------------------------------------------------------------------------------- /assets/images/card_bottom_marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_marvel.png -------------------------------------------------------------------------------- /assets/images/card_bottom_rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_rare.png -------------------------------------------------------------------------------- /assets/images/card_bottom_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_shadow.png -------------------------------------------------------------------------------- /assets/images/card_bottom_starwars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_starwars.png -------------------------------------------------------------------------------- /assets/images/card_bottom_uncommon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_bottom_uncommon.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_common.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_dark.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_dc.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_epic.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_frozen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_frozen.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_icon.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_lava.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_legendary.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_marvel.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_rare.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_shadow.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_starwars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_starwars.png -------------------------------------------------------------------------------- /assets/images/card_faceplate_uncommon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_faceplate_uncommon.png -------------------------------------------------------------------------------- /assets/images/card_top_common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_common.png -------------------------------------------------------------------------------- /assets/images/card_top_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_dark.png -------------------------------------------------------------------------------- /assets/images/card_top_dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_dc.png -------------------------------------------------------------------------------- /assets/images/card_top_epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_epic.png -------------------------------------------------------------------------------- /assets/images/card_top_frozen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_frozen.png -------------------------------------------------------------------------------- /assets/images/card_top_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_icon.png -------------------------------------------------------------------------------- /assets/images/card_top_lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_lava.png -------------------------------------------------------------------------------- /assets/images/card_top_legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_legendary.png -------------------------------------------------------------------------------- /assets/images/card_top_marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_marvel.png -------------------------------------------------------------------------------- /assets/images/card_top_rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_rare.png -------------------------------------------------------------------------------- /assets/images/card_top_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_shadow.png -------------------------------------------------------------------------------- /assets/images/card_top_starwars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_starwars.png -------------------------------------------------------------------------------- /assets/images/card_top_uncommon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/card_top_uncommon.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/vbucks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthanC/Athena/7eceb0bb1e31d51242161f62f8b70973fecb03e4/assets/images/vbucks.png -------------------------------------------------------------------------------- /configuration_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "delayStart": 10, 3 | "fortniteAPI": { 4 | "apiKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 5 | }, 6 | "language": "en", 7 | "supportACreator": "YourSupportACreatorCode", 8 | "twitter": { 9 | "enabled": true, 10 | "apiKey": "XXXXXXXXXX", 11 | "apiSecret": "XXXXXXXXXX", 12 | "accessToken": "XXXXXXXXXX", 13 | "accessSecret": "XXXXXXXXXX" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /itemshop.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from math import ceil 4 | from sys import exit 5 | from time import sleep 6 | 7 | import coloredlogs 8 | import twitter 9 | from PIL import Image, ImageDraw 10 | 11 | from util import ImageUtil, Utility 12 | 13 | log = logging.getLogger(__name__) 14 | coloredlogs.install(level="INFO", fmt="[%(asctime)s] %(message)s", datefmt="%I:%M:%S") 15 | 16 | 17 | class Athena: 18 | """Fortnite Item Shop Generator.""" 19 | 20 | def main(self): 21 | print("Athena - Fortnite Item Shop Generator") 22 | print("https://github.com/EthanC/Athena\n") 23 | 24 | initialized = Athena.LoadConfiguration(self) 25 | 26 | if initialized is True: 27 | if self.delay > 0: 28 | log.info(f"Delaying process start for {self.delay}s...") 29 | sleep(self.delay) 30 | 31 | itemShop = Utility.GET( 32 | self, 33 | "https://fortnite-api.com/shop/br", 34 | {"x-api-key": self.apiKey}, 35 | {"language": self.language}, 36 | ) 37 | 38 | if itemShop is not None: 39 | itemShop = json.loads(itemShop)["data"] 40 | 41 | # Strip time from the timestamp, we only need the date 42 | date = Utility.ISOtoHuman( 43 | self, itemShop["date"].split("T")[0], self.language 44 | ) 45 | log.info(f"Retrieved Item Shop for {date}") 46 | 47 | shopImage = Athena.GenerateImage(self, date, itemShop) 48 | 49 | if shopImage is True: 50 | if self.twitterEnabled is True: 51 | Athena.Tweet(self, date) 52 | 53 | def LoadConfiguration(self): 54 | """ 55 | Set the configuration values specified in configuration.json 56 | 57 | Return True if configuration sucessfully loaded. 58 | """ 59 | 60 | configuration = json.loads(Utility.ReadFile(self, "configuration", "json")) 61 | 62 | try: 63 | self.delay = configuration["delayStart"] 64 | self.apiKey = configuration["fortniteAPI"]["apiKey"] 65 | self.language = configuration["language"] 66 | self.supportACreator = configuration["supportACreator"] 67 | self.twitterEnabled = configuration["twitter"]["enabled"] 68 | self.twitterAPIKey = configuration["twitter"]["apiKey"] 69 | self.twitterAPISecret = configuration["twitter"]["apiSecret"] 70 | self.twitterAccessToken = configuration["twitter"]["accessToken"] 71 | self.twitterAccessSecret = configuration["twitter"]["accessSecret"] 72 | 73 | log.info("Loaded configuration") 74 | 75 | return True 76 | except Exception as e: 77 | log.critical(f"Failed to load configuration, {e}") 78 | 79 | def GenerateImage(self, date: str, itemShop: dict): 80 | """ 81 | Generate the Item Shop image using the provided Item Shop. 82 | 83 | Return True if image sucessfully saved. 84 | """ 85 | 86 | try: 87 | featured = itemShop["featured"] 88 | daily = itemShop["daily"] 89 | 90 | # Ensure both Featured and Daily have at least 1 item 91 | if (len(featured) <= 0) or (len(daily) <= 0): 92 | raise Exception(f"Featured: {len(featured)}, Daily: {len(daily)}") 93 | except Exception as e: 94 | log.critical(f"Failed to parse Item Shop Featured and Daily items, {e}") 95 | 96 | return False 97 | 98 | # Determine the max amount of rows required for the current 99 | # Item Shop when there are 3 columns for both Featured and Daily. 100 | # This allows us to determine the image height. 101 | rows = max(ceil(len(featured) / 3), ceil(len(daily) / 3)) 102 | shopImage = Image.new("RGB", (1920, ((545 * rows) + 340))) 103 | 104 | try: 105 | background = ImageUtil.Open(self, "background.png") 106 | background = ImageUtil.RatioResize( 107 | self, background, shopImage.width, shopImage.height 108 | ) 109 | shopImage.paste( 110 | background, ImageUtil.CenterX(self, background.width, shopImage.width) 111 | ) 112 | except FileNotFoundError: 113 | log.warn("Failed to open background.png, defaulting to dark gray") 114 | shopImage.paste((18, 18, 18), [0, 0, shopImage.size[0], shopImage.size[1]]) 115 | 116 | logo = ImageUtil.Open(self, "logo.png") 117 | logo = ImageUtil.RatioResize(self, logo, 0, 210) 118 | shopImage.paste( 119 | logo, ImageUtil.CenterX(self, logo.width, shopImage.width, 20), logo 120 | ) 121 | 122 | canvas = ImageDraw.Draw(shopImage) 123 | font = ImageUtil.Font(self, 48) 124 | textWidth, _ = font.getsize(date) 125 | canvas.text( 126 | ImageUtil.CenterX(self, textWidth, shopImage.width, 255), 127 | date, 128 | (255, 255, 255), 129 | font=font, 130 | ) 131 | canvas.text((20, 255), "Featured", (255, 255, 255), font=font) 132 | textWidth, _ = font.getsize("Daily") 133 | canvas.text( 134 | (shopImage.width - (textWidth + 20), 255), 135 | "Daily", 136 | (255, 255, 255), 137 | font=font, 138 | ) 139 | 140 | # Track grid position 141 | i = 0 142 | 143 | for item in featured: 144 | card = Athena.GenerateCard(self, item) 145 | 146 | if card is not None: 147 | shopImage.paste( 148 | card, 149 | ( 150 | (20 + ((i % 3) * (card.width + 5))), 151 | (315 + ((i // 3) * (card.height + 5))), 152 | ), 153 | card, 154 | ) 155 | 156 | i += 1 157 | 158 | # Reset grid position 159 | i = 0 160 | 161 | for item in daily: 162 | card = Athena.GenerateCard(self, item) 163 | 164 | if card is not None: 165 | shopImage.paste( 166 | card, 167 | ( 168 | (990 + ((i % 3) * (card.width + 5))), 169 | (315 + ((i // 3) * (card.height + 5))), 170 | ), 171 | card, 172 | ) 173 | 174 | i += 1 175 | 176 | try: 177 | shopImage.save("itemshop.png") 178 | log.info("Generated Item Shop image") 179 | 180 | return True 181 | except Exception as e: 182 | log.critical(f"Failed to save Item Shop image, {e}") 183 | 184 | def GenerateCard(self, item: dict): 185 | """Return the card image for the provided Fortnite Item Shop item.""" 186 | 187 | try: 188 | name = item["items"][0]["name"] 189 | rarity = item["items"][0]["rarity"] 190 | category = item["items"][0]["type"] 191 | price = item["finalPrice"] 192 | if isinstance(item["items"][0]["images"]["featured"], dict): 193 | icon = item["items"][0]["images"]["featured"]["url"] 194 | else: 195 | icon = item["items"][0]["images"]["icon"]["url"] 196 | except Exception as e: 197 | log.error(f"Failed to parse item {name}, {e}") 198 | 199 | return 200 | 201 | if rarity == "frozen": 202 | blendColor = (148, 223, 255) 203 | elif rarity == "lava": 204 | blendColor = (234, 141, 35) 205 | elif rarity == "legendary": 206 | blendColor = (211, 120, 65) 207 | elif rarity == "dark": 208 | blendColor = (251, 34, 223) 209 | elif rarity == "starwars": 210 | blendColor = (231, 196, 19) 211 | elif rarity == "marvel": 212 | blendColor = (197, 51, 52) 213 | elif rarity == "dc": 214 | blendColor = (84, 117, 199) 215 | elif rarity == "icon": 216 | blendColor = (54, 183, 183) 217 | elif rarity == "shadow": 218 | blendColor = (113, 113, 113) 219 | elif rarity == "epic": 220 | blendColor = (177, 91, 226) 221 | elif rarity == "rare": 222 | blendColor = (73, 172, 242) 223 | elif rarity == "uncommon": 224 | blendColor = (96, 170, 58) 225 | elif rarity == "common": 226 | blendColor = (190, 190, 190) 227 | else: 228 | blendColor = (255, 255, 255) 229 | 230 | card = Image.new("RGBA", (300, 545)) 231 | 232 | try: 233 | layer = ImageUtil.Open(self, f"card_top_{rarity}.png") 234 | except FileNotFoundError: 235 | log.warn(f"Failed to open card_top_{rarity}.png, defaulted to Common") 236 | layer = ImageUtil.Open(self, "card_top_common.png") 237 | 238 | card.paste(layer) 239 | 240 | icon = ImageUtil.Download(self, icon) 241 | if (category == "outfit") or (category == "emote"): 242 | icon = ImageUtil.RatioResize(self, icon, 285, 365) 243 | elif category == "wrap": 244 | icon = ImageUtil.RatioResize(self, icon, 230, 310) 245 | else: 246 | icon = ImageUtil.RatioResize(self, icon, 310, 390) 247 | if (category == "outfit") or (category == "emote"): 248 | card.paste(icon, ImageUtil.CenterX(self, icon.width, card.width), icon) 249 | else: 250 | card.paste(icon, ImageUtil.CenterX(self, icon.width, card.width, 15), icon) 251 | 252 | if len(item["items"]) > 1: 253 | # Track grid position 254 | i = 0 255 | 256 | # Start at position 1 in items array 257 | for extra in item["items"][1:]: 258 | try: 259 | extraRarity = extra["rarity"] 260 | extraIcon = extra["images"]["smallIcon"]["url"] 261 | except Exception as e: 262 | log.error(f"Failed to parse item {name}, {e}") 263 | 264 | return 265 | 266 | try: 267 | layer = ImageUtil.Open(self, f"box_bottom_{extraRarity}.png") 268 | except FileNotFoundError: 269 | log.warn( 270 | f"Failed to open box_bottom_{extraRarity}.png, defaulted to Common" 271 | ) 272 | layer = ImageUtil.Open(self, "box_bottom_common.png") 273 | 274 | card.paste( 275 | layer, 276 | ( 277 | (card.width - (layer.width + 9)), 278 | (9 + ((i // 1) * (layer.height))), 279 | ), 280 | ) 281 | 282 | extraIcon = ImageUtil.Download(self, extraIcon) 283 | extraIcon = ImageUtil.RatioResize(self, extraIcon, 75, 75) 284 | 285 | card.paste( 286 | extraIcon, 287 | ( 288 | (card.width - (layer.width + 9)), 289 | (9 + ((i // 1) * (extraIcon.height))), 290 | ), 291 | extraIcon, 292 | ) 293 | 294 | try: 295 | layer = ImageUtil.Open(self, f"box_faceplate_{extraRarity}.png") 296 | except FileNotFoundError: 297 | log.warn( 298 | f"Failed to open box_faceplate_{extraRarity}.png, defaulted to Common" 299 | ) 300 | layer = ImageUtil.Open(self, "box_faceplate_common.png") 301 | 302 | card.paste( 303 | layer, 304 | ( 305 | (card.width - (layer.width + 9)), 306 | (9 + ((i // 1) * (layer.height))), 307 | ), 308 | layer, 309 | ) 310 | 311 | i += 1 312 | 313 | try: 314 | layer = ImageUtil.Open(self, f"card_faceplate_{rarity}.png") 315 | except FileNotFoundError: 316 | log.warn(f"Failed to open card_faceplate_{rarity}.png, defaulted to Common") 317 | layer = ImageUtil.Open(self, "card_faceplate_common.png") 318 | 319 | card.paste(layer, layer) 320 | 321 | try: 322 | layer = ImageUtil.Open(self, f"card_bottom_{rarity}.png") 323 | except FileNotFoundError: 324 | log.warn(f"Failed to open card_bottom_{rarity}.png, defaulted to Common") 325 | layer = ImageUtil.Open(self, "card_bottom_common.png") 326 | 327 | card.paste(layer, layer) 328 | 329 | canvas = ImageDraw.Draw(card) 330 | 331 | font = ImageUtil.Font(self, 30) 332 | textWidth, _ = font.getsize(f"{rarity.capitalize()} {category.capitalize()}") 333 | canvas.text( 334 | ImageUtil.CenterX(self, textWidth, card.width, 385), 335 | f"{rarity.capitalize()} {category.capitalize()}", 336 | blendColor, 337 | font=font, 338 | ) 339 | 340 | vbucks = ImageUtil.Open(self, "vbucks.png") 341 | vbucks = ImageUtil.RatioResize(self, vbucks, 25, 25) 342 | 343 | price = str(f"{price:,}") 344 | textWidth, _ = font.getsize(price) 345 | canvas.text( 346 | ImageUtil.CenterX(self, ((textWidth - 5) - vbucks.width), card.width, 495), 347 | price, 348 | blendColor, 349 | font=font, 350 | ) 351 | 352 | card.paste( 353 | vbucks, 354 | ImageUtil.CenterX(self, (vbucks.width + (textWidth + 5)), card.width, 495), 355 | vbucks, 356 | ) 357 | 358 | font = ImageUtil.Font(self, 56) 359 | textWidth, _ = font.getsize(name) 360 | change = 0 361 | if textWidth >= 270: 362 | # Ensure that the item name does not overflow 363 | font, textWidth, change = ImageUtil.FitTextX(self, name, 56, 260) 364 | canvas.text( 365 | ImageUtil.CenterX(self, textWidth, card.width, (425 + (change / 2))), 366 | name, 367 | (255, 255, 255), 368 | font=font, 369 | ) 370 | 371 | return card 372 | 373 | def Tweet(self, date: str): 374 | """ 375 | Tweet the current `itemshop.png` local file to Twitter using the credentials provided 376 | in `configuration.json`. 377 | """ 378 | 379 | try: 380 | twitterAPI = twitter.Api( 381 | consumer_key=self.twitterAPIKey, 382 | consumer_secret=self.twitterAPISecret, 383 | access_token_key=self.twitterAccessToken, 384 | access_token_secret=self.twitterAccessSecret, 385 | ) 386 | 387 | twitterAPI.VerifyCredentials() 388 | except Exception as e: 389 | log.critical(f"Failed to authenticate with Twitter, {e}") 390 | 391 | return 392 | 393 | body = f"#Fortnite Item Shop for {date}" 394 | 395 | if self.supportACreator is not None: 396 | body = f"{body}\n\nSupport-a-Creator Code: {self.supportACreator}" 397 | 398 | try: 399 | with open("itemshop.png", "rb") as shopImage: 400 | twitterAPI.PostUpdate(body, media=shopImage) 401 | 402 | log.info("Tweeted Item Shop") 403 | except Exception as e: 404 | log.critical(f"Failed to Tweet Item Shop, {e}") 405 | 406 | 407 | if __name__ == "__main__": 408 | try: 409 | Athena.main(Athena) 410 | except KeyboardInterrupt: 411 | log.info("Exiting...") 412 | exit() 413 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import locale 3 | import logging 4 | from datetime import datetime 5 | 6 | import requests 7 | from PIL import Image, ImageFont 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class Utility: 13 | """Class containing utilitarian functions intended to reduce duplicate code.""" 14 | 15 | def GET(self, url: str, headers: dict, parameters: dict = {"language": "en"}): 16 | """ 17 | Return the response of a successful HTTP GET request to the specified 18 | URL with the optionally provided header values. 19 | """ 20 | 21 | res = requests.get(url, headers=headers, params=parameters) 22 | 23 | # HTTP 200 (OK) 24 | if res.status_code == 200: 25 | return res.text 26 | else: 27 | log.critical(f"Failed to GET {url} (HTTP {res.status_code})") 28 | 29 | def nowISO(self): 30 | """Return the current utc time in ISO8601 timestamp format.""" 31 | 32 | return datetime.utcnow().isoformat() 33 | 34 | def ISOtoHuman(self, date: str, language: str): 35 | """Return the provided ISO8601 timestamp in human-readable format.""" 36 | 37 | try: 38 | locale.setlocale(locale.LC_ALL, language) 39 | except locale.Error: 40 | log.warn(f"Unsupported locale configured, using system default") 41 | 42 | try: 43 | # Unix-supported zero padding removal 44 | return datetime.strptime(date, "%Y-%m-%d").strftime("%A, %B %-d, %Y") 45 | except ValueError: 46 | try: 47 | # Windows-supported zero padding removal 48 | return datetime.strptime(date, "%Y-%m-%d").strftime("%A, %B %#d, %Y") 49 | except Exception as e: 50 | log.error(self, f"Failed to convert to human-readable time, {e}") 51 | 52 | def ReadFile(self, filename: str, extension: str, directory: str = ""): 53 | """ 54 | Read and return the contents of the specified file. 55 | 56 | Optionally specify a relative directory. 57 | """ 58 | 59 | try: 60 | with open( 61 | f"{directory}{filename}.{extension}", "r", encoding="utf-8" 62 | ) as file: 63 | return file.read() 64 | except Exception as e: 65 | log.critical(f"Failed to read {filename}.{extension}, {e}") 66 | 67 | 68 | class ImageUtil: 69 | """Class containing utilitarian image-based functions intended to reduce duplicate code.""" 70 | 71 | def Open(self, filename: str, directory: str = "assets/images/"): 72 | """Return the specified image file.""" 73 | 74 | return Image.open(f"{directory}{filename}") 75 | 76 | def Download(self, url: str): 77 | """Download and return the raw file from the specified url as an image object.""" 78 | 79 | res = requests.get(url, stream=True) 80 | 81 | # HTTP 200 (OK) 82 | if res.status_code == 200: 83 | return Image.open(res.raw) 84 | else: 85 | log.critical(f"Failed to GET {url} (HTTP {res.status_code})") 86 | 87 | def RatioResize(self, image: Image.Image, maxWidth: int, maxHeight: int): 88 | """Resize and return the provided image while maintaining aspect ratio.""" 89 | 90 | ratio = max(maxWidth / image.width, maxHeight / image.height) 91 | 92 | return image.resize( 93 | (int(image.width * ratio), int(image.height * ratio)), Image.ANTIALIAS 94 | ) 95 | 96 | def CenterX(self, foregroundWidth: int, backgroundWidth: int, distanceTop: int = 0): 97 | """Return the tuple necessary for horizontal centering and an optional vertical distance.""" 98 | 99 | return (int(backgroundWidth / 2) - int(foregroundWidth / 2), distanceTop) 100 | 101 | def Font( 102 | self, 103 | size: int, 104 | font: str = "BurbankBigCondensed-Black.otf", 105 | directory: str = "assets/fonts/", 106 | ): 107 | """Return a font object with the specified font file and size.""" 108 | 109 | try: 110 | return ImageFont.truetype(f"{directory}{font}", size) 111 | except OSError: 112 | log.warn( 113 | "BurbankBigCondensed-Black.otf not found, defaulted font to LuckiestGuy-Regular.ttf" 114 | ) 115 | 116 | return ImageFont.truetype(f"{directory}LuckiestGuy-Regular.ttf", size) 117 | except Exception as e: 118 | log.error(f"Failed to load font, {e}") 119 | 120 | def FitTextX( 121 | self, 122 | text: str, 123 | size: int, 124 | maxSize: int, 125 | font: str = "BurbankBigCondensed-Black.otf", 126 | ): 127 | """Return the font and width which fits the provided text within the specified maxiumum width.""" 128 | 129 | font = ImageUtil.Font(self, size) 130 | textWidth, _ = font.getsize(text) 131 | change = 0 132 | 133 | while textWidth >= maxSize: 134 | change += 1 135 | size -= 1 136 | font = ImageUtil.Font(self, size) 137 | textWidth, _ = font.getsize(text) 138 | 139 | return ImageUtil.Font(self, size), textWidth, change 140 | --------------------------------------------------------------------------------