├── disrank ├── __init__.py ├── assets │ ├── card.png │ ├── dnd.png │ ├── font.ttf │ ├── font2.ttf │ ├── idle.png │ ├── offline.png │ ├── online.png │ └── streaming.png └── generator.py ├── MANIFEST.in ├── CHANGELOG.txt ├── README.md ├── LICENSE ├── setup.py ├── examples └── discord.py └── .gitignore /disrank/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-include *.txt *.py *.png *.ttf *.md -------------------------------------------------------------------------------- /disrank/assets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/card.png -------------------------------------------------------------------------------- /disrank/assets/dnd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/dnd.png -------------------------------------------------------------------------------- /disrank/assets/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/font.ttf -------------------------------------------------------------------------------- /disrank/assets/font2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/font2.ttf -------------------------------------------------------------------------------- /disrank/assets/idle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/idle.png -------------------------------------------------------------------------------- /disrank/assets/offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/offline.png -------------------------------------------------------------------------------- /disrank/assets/online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/online.png -------------------------------------------------------------------------------- /disrank/assets/streaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shahriyardx/disrank/HEAD/disrank/assets/streaming.png -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Change Log 2 | =========== 3 | 4 | 0.0.1 (15/7/2020) 5 | -------------------------------- 6 | - Release 7 | 8 | 0.0.2 (15/7/2020) 9 | -------------------------------- 10 | - Added docs 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Disrank 2 | A lib to make good looking discord profile card 3 | 4 | # Usage 5 | ```py 6 | from disrank.generator import Generator 7 | 8 | args = { 9 | 'bg_image' : '', # Background image link 10 | 'profile_image' : '', # User profile picture link 11 | 'level' : 1, # User current level 12 | 'current_xp' : 0, # Current level minimum xp 13 | 'user_xp' : 10, # User current xp 14 | 'next_xp' : 100, # xp required for next level 15 | 'user_position' : 1, # User position in leaderboard 16 | 'user_name' : 'Name#0001', # user name with descriminator 17 | 'user_status' : 'online', # User status eg. online, offline, idle, streaming, dnd 18 | } 19 | 20 | image = Generator().generate_profile(**args) 21 | 22 | # In a discord command 23 | file = discord.File(fp=image, filename='image.png') 24 | await ctx.send(file=file) 25 | ``` 26 | 27 | Make sure to run the generate part in an executor. 28 | Any help need [Join here](https://discord.gg/7SaE8v2) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Md Shahriyar Alam 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # See note below for more information about classifiers 4 | classifiers = [ 5 | "Development Status :: 5 - Production/Stable", 6 | "Intended Audience :: Education", 7 | "Operating System :: POSIX :: Linux", 8 | "Operating System :: Microsoft :: Windows", 9 | "Operating System :: MacOS", 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: Python :: 3", 12 | ] 13 | 14 | setup( 15 | name="disrank", 16 | version="0.0.2", 17 | description="A lib", 18 | long_description=open("README.md").read(), 19 | long_description_content_type='text/markdown', 20 | url="https://github.com/shahprog/disrank/", 21 | author="Md Shahriyar Alam", 22 | author_email="mdshahriyaralam552@gmail.com", 23 | license="MIT", # note the American spelling 24 | classifiers=classifiers, 25 | keywords="discord discord-rank discord-profile discord-leveling", # used when people are searching for a module, keywords separated with a space 26 | packages=find_packages(), 27 | install_requires=[ 28 | "Pillow" 29 | ], # a list of other Python modules which this module depends on. For example RPi.GPIO 30 | include_package_data=True 31 | ) 32 | -------------------------------------------------------------------------------- /examples/discord.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from disrank.generator import Generator 4 | import functools 5 | import asyncio 6 | 7 | class Profile(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | def get_card(self, args): 12 | image = Generator().generate_profile(**args) 13 | return image 14 | 15 | @commands.command() 16 | async def rank(self, ctx, member:discord.Member=None): 17 | if member is None: 18 | member = ctx.author 19 | args = { 20 | 'bg_image' : '', # Background image link (Optional) 21 | 'profile_image' : str(member.avatar_url_as(format='png')), # User profile picture link 22 | 'level' : 1, # User current level 23 | 'current_xp' : 0, # Current level minimum xp 24 | 'user_xp' : 10, # User current xp 25 | 'next_xp' : 100, # xp required for next level 26 | 'user_position' : 1, # User position in leaderboard 27 | 'user_name' : str(member), # user name with descriminator 28 | 'user_status' : member.status.name, # User status eg. online, offline, idle, streaming, dnd 29 | } 30 | 31 | func = functools.partial(self.get_card, args) 32 | image = await asyncio.get_event_loop().run_in_executor(None, func) 33 | 34 | file = discord.File(fp=image, filename='image.png') 35 | await ctx.send(file=file) 36 | 37 | def setup(bot): 38 | bot.add_cog(Profile(bot)) -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ -------------------------------------------------------------------------------- /disrank/generator.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from PIL import Image, ImageDraw, ImageFont 3 | import requests 4 | import math 5 | import os 6 | 7 | class Generator: 8 | def __init__(self): 9 | self.default_bg = os.path.join(os.path.dirname(__file__), 'assets', 'card.png') 10 | self.online = os.path.join(os.path.dirname(__file__), 'assets', 'online.png') 11 | self.offline = os.path.join(os.path.dirname(__file__), 'assets', 'offline.png') 12 | self.idle = os.path.join(os.path.dirname(__file__), 'assets', 'idle.png') 13 | self.dnd = os.path.join(os.path.dirname(__file__), 'assets', 'dnd.png') 14 | self.streaming = os.path.join(os.path.dirname(__file__), 'assets', 'streaming.png') 15 | self.font1 = os.path.join(os.path.dirname(__file__), 'assets', 'font.ttf') 16 | self.font2 = os.path.join(os.path.dirname(__file__), 'assets', 'font2.ttf') 17 | 18 | def generate_profile(self, bg_image:str=None, profile_image:str=None, level:int=1, current_xp:int=0, user_xp:int=20, next_xp:int=100, user_position:int=1, user_name:str='Shahriyar#9770', user_status:str='online'): 19 | if not bg_image: 20 | card = Image.open(self.default_bg).convert("RGBA") 21 | else: 22 | bg_bytes = BytesIO(requests.get(bg_image).content) 23 | card = Image.open(bg_bytes).convert("RGBA") 24 | 25 | width, height = card.size 26 | if width == 900 and height == 238: 27 | pass 28 | else: 29 | x1 = 0 30 | y1 = 0 31 | x2 = width 32 | nh = math.ceil(width * 0.264444) 33 | y2 = 0 34 | 35 | if nh < height: 36 | y1 = (height / 2) - 119 37 | y2 = nh + y1 38 | 39 | card = card.crop((x1, y1, x2, y2)).resize((900, 238)) 40 | 41 | profile_bytes = BytesIO(requests.get(profile_image).content) 42 | profile = Image.open(profile_bytes) 43 | profile = profile.convert('RGBA').resize((180, 180)) 44 | 45 | if user_status == 'online': 46 | status = Image.open(self.online) 47 | if user_status == 'offline': 48 | status = Image.open(self.offline) 49 | if user_status == 'idle': 50 | status = Image.open(self.idle) 51 | if user_status == 'streaming': 52 | status = Image.open(self.streaming) 53 | if user_status == 'dnd': 54 | status = Image.open(self.dnd) 55 | 56 | status = status.convert("RGBA").resize((40,40)) 57 | 58 | profile_pic_holder = Image.new( 59 | "RGBA", card.size, (255, 255, 255, 0) 60 | ) # Is used for a blank image so that i can mask 61 | 62 | # Mask to crop image 63 | mask = Image.new("RGBA", card.size, 0) 64 | mask_draw = ImageDraw.Draw(mask) 65 | mask_draw.ellipse( 66 | (29, 29, 209, 209), fill=(255, 25, 255, 255) 67 | ) # The part need to be cropped 68 | 69 | # Editing stuff here 70 | 71 | # ======== Fonts to use ============= 72 | font_normal = ImageFont.truetype(self.font1, 36) 73 | font_small = ImageFont.truetype(self.font1, 20) 74 | font_signa = ImageFont.truetype(self.font2, 25) 75 | 76 | # ======== Colors ======================== 77 | WHITE = (189, 195, 199) 78 | DARK = (252, 179, 63) 79 | YELLOW = (255, 234, 167) 80 | 81 | def get_str(xp): 82 | if xp < 1000: 83 | return str(xp) 84 | if xp >= 1000 and xp < 1000000: 85 | return str(round(xp / 1000, 1)) + "k" 86 | if xp > 1000000: 87 | return str(round(xp / 1000000, 1)) + "M" 88 | 89 | draw = ImageDraw.Draw(card) 90 | draw.text((245, 22), user_name, DARK, font=font_normal) 91 | draw.text((245, 98), f"Rank #{user_position}", DARK, font=font_small) 92 | draw.text((245, 123), f"Level {level}", DARK, font=font_small) 93 | draw.text( 94 | (245, 150), 95 | f"Exp {get_str(user_xp)}/{get_str(next_xp)}", 96 | DARK, 97 | font=font_small, 98 | ) 99 | 100 | # Adding another blank layer for the progress bar 101 | # Because drawing on card dont make their background transparent 102 | blank = Image.new("RGBA", card.size, (255, 255, 255, 0)) 103 | blank_draw = ImageDraw.Draw(blank) 104 | blank_draw.rectangle( 105 | (245, 185, 750, 205), fill=(255, 255, 255, 0), outline=DARK 106 | ) 107 | 108 | xpneed = next_xp - current_xp 109 | xphave = user_xp - current_xp 110 | 111 | current_percentage = (xphave / xpneed) * 100 112 | length_of_bar = (current_percentage * 4.9) + 248 113 | 114 | blank_draw.rectangle((248, 188, length_of_bar, 202), fill=DARK) 115 | blank_draw.ellipse((20, 20, 218, 218), fill=(255, 255, 255, 0), outline=DARK) 116 | 117 | profile_pic_holder.paste(profile, (29, 29, 209, 209)) 118 | 119 | pre = Image.composite(profile_pic_holder, card, mask) 120 | pre = Image.alpha_composite(pre, blank) 121 | 122 | # Status badge 123 | # Another blank 124 | blank = Image.new("RGBA", pre.size, (255, 255, 255, 0)) 125 | blank.paste(status, (169, 169)) 126 | 127 | final = Image.alpha_composite(pre, blank) 128 | final_bytes = BytesIO() 129 | final.save(final_bytes, 'png') 130 | final_bytes.seek(0) 131 | return final_bytes --------------------------------------------------------------------------------