-
Notifications
You must be signed in to change notification settings - Fork 0
/
circle.py
119 lines (84 loc) · 4.52 KB
/
circle.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# basic dependencies
import discord
from discord.ext import commands
# aiohttp should be installed if discord.py is
import aiohttp
# PIL can be installed through
# `pip install -U Pillow`
from PIL import Image, ImageDraw
# partial lets us prepare a new function with args for run_in_executor
from functools import partial
# BytesIO allows us to convert bytes into a file-like byte stream.
from io import BytesIO
# this just allows for nice function annotation, and stops my IDE from complaining.
from typing import Union
class ImageCog:
def __init__(self, bot):
# we need to include a reference to the bot here so we can access its loop later.
self.bot = bot
# create a ClientSession to be used for downloading avatars
self.session = aiohttp.ClientSession(loop=bot.loop)
async def get_avatar(self, user: Union[discord.User, discord.Member]) -> bytes:
# generally an avatar will be 1024x1024, but we shouldn't rely on this
avatar_url = user.avatar_url
async with self.session.get(avatar_url) as response:
# this gives us our response object, and now we can read the bytes from it.
avatar_bytes = await response.read()
return avatar_bytes
@staticmethod
def processing(avatar_bytes: bytes, colour: tuple) -> BytesIO:
# we must use BytesIO to load the image here as PIL expects a stream instead of
# just raw bytes.
with Image.open(BytesIO(avatar_bytes)) as im:
# this creates a new image the same size as the user's avatar, with the
# background colour being the user's colour.
with Image.new("RGB", im.size, colour) as background:
# this ensures that the user's avatar lacks an alpha channel, as we're
# going to be substituting our own here.
rgb_avatar = im.convert("RGB")
# this is the mask image we will be using to create the circle cutout
# effect on the avatar.
with Image.new("L", im.size, 0) as mask:
# ImageDraw lets us draw on the image, in this instance, we will be
# using it to draw a white circle on the mask image.
mask_draw = ImageDraw.Draw(mask)
mask_text = ImageDraw.Draw(mask)
mask_text.text((0, 0), 'hi', fill=255, font=None, anchor=None, spacing=0, align="center")
# draw the white circle from 0, 0 to the bottom right corner of the image
mask_draw.ellipse([(0, 0), im.size], fill=255)
# paste the alpha-less avatar on the background using the new circle mask
# we just created.
background.paste(rgb_avatar, (0, 0), mask=mask)
# prepare the stream to save this image into
final_buffer = BytesIO()
# save into the stream, using png format.
background.save(final_buffer, "png")
# seek back to the start of the stream
final_buffer.seek(0)
return final_buffer
@commands.command(pass_context=True)
async def circle(self, ctx, *, member: discord.Member = None):
"""Display the user's avatar on their colour."""
# this means that if the user does not supply a member, it will default to the
# author of the message.
member = member or self.ctx.message.author
# this means the bot will type while it is processing and uploading the image
if isinstance(member, discord.Member):
# get the user's colour, pretty self explanatory
member_colour = member.color.to_tuple()
else:
# if this is in a DM or something went seriously wrong
member_colour = (0, 0, 0)
# grab the user's avatar as bytes
avatar_bytes = await self.get_avatar(member)
# create partial function so we don't have to stack the args in run_in_executor
fn = partial(self.processing, avatar_bytes, member_colour)
# this runs our processing in an executor, stopping it from blocking the thread loop.
# as we already seeked back the buffer in the other thread, we're good to go
final_buffer = await self.bot.loop.run_in_executor(None, fn)
# prepare the file
# send it
await self.bot.send_file(ctx.message.channel, fp=final_buffer, filename="circle.png")
# setup function so this can be loaded as an extension
def setup(bot):
bot.add_cog(ImageCog(bot))